DEV Community

Uigla
Uigla

Posted on • Updated on

Cannon physics - 3D web3 serie

Hey Reader,

This is the 3rd post of 3D-web3 Series.

1 - Vite config and basic three.js
2 - Three.js (fiber & drei)
3 - 3D web - Cannon physics
4 - Web3

"Cannon" is the rigid body physics engine who includes simple collision detection, various body shapes, contacts, friction and constraints.

npm i @react-three/cannon

Simple steps to make it work:
1_ Import and Create a physics world

import { Physics, useBox, ... } from '@react-three/cannon'

<Physics>{/* Physics related objects in here please */}</Physics>
Enter fullscreen mode Exit fullscreen mode

2_ Pick a shape that suits your objects contact surface, it could be a box, plane, sphere, etc. Give it a mass, too

const [ref, api] = useBox(() => ({ mass: 1 }))
Enter fullscreen mode Exit fullscreen mode

3_ Take your object, it could be a mesh, line, gltf, anything, and tie it to the reference you have just received. It will now be affected by gravity and other objects inside the physics world.

<mesh ref={ref} geometry={...} material={...} />
Enter fullscreen mode Exit fullscreen mode

4_ You can interact with it by using the api, which lets you apply positions, rotations, velocities, forces and impulses

useFrame(({ clock }) => api.position.set(Math.sin(clock.getElapsedTime()) * 5, 0, 0))
Enter fullscreen mode Exit fullscreen mode

5_ You can use the body api to subscribe to properties to get updates on each frame

const velocity = useRef([0, 0, 0])
useEffect(() => {
  const unsubscribe = api.velocity.subscribe((v) => (velocity.current = v))
  return unsubscribe
}, [])
Enter fullscreen mode Exit fullscreen mode

All the steps in "Box.jsx" component looks like:

import { Physics, useBox } from '@react-three/cannon'
import { useFrame } from '@react-three/fiber';

const Box = () => {

    const [ref, api] = useBox(() => ({ mass: 1 }))

    useFrame(({ clock }) => api.position.set(Math.sin(clock.getElapsedTime()) * 5, 0, 0))

    const velocity = useRef([0, 0, 0])
    useEffect(() => {
        const unsubscribe = api.velocity.subscribe((v) => (velocity.current = v))
        return unsubscribe
    }, [])

    return (
        <Physics>
            <mesh ref={ref}>
                <boxGeometry attach='geometry' args={[1, 1, 1]} />
                <meshStandardMaterial attach="material" color={'#000'} />
            </mesh>

        </Physics>
    )
}

export default Box
Enter fullscreen mode Exit fullscreen mode

Let's apply this package to our repo.

App logic__

First check previous post if you haven't, to understand why we are not using in code-sand-box our gltf model
Instead, we are using a "yellow box" with an onClick method to change between two "camera modes"

Include the "ActivateSpawner" component which will be the parent of the other 3 components we need.

In camera RIG mode we'll see a "black box" with an onClick method to activate :

a) "Spawner" component: It creates "x" number of bubbles with "y" velocity. "Spawner" has "Bubble" component as child.

b) "PlayerBox" component: Mimics your movement and you've to avoid coming bubbles

Both components have a collider property. So, if "PlayerBox" collides with a "Bubble" component, game will be stoped

We'll be using (previous tutorial "objects/hooks" are not included):

  • From "Fiber": useThree, useFrame
  • From "Cannon": useBox, useSphere
  • From "Three": Vector3

Step_1 Create a "ActivateSpawner" component

Notice we're giving a "mass" of 0 to the box

import React from 'react'
import { useBox } from '@react-three/cannon';
import { useState } from 'react';

import Spawner from './Spawner';
import PlayerBox from './PlayerBox';

const ActivateSpawner = () => {

    const [play, setPlay] = useState(false);

    // This box is used to start the game
    const [ref] = useBox(() => ({
        mass: 0,
        position: [-5, 2, -10],
        type: 'Dynamic',
        args: [1, 1, 1],
    }));

    return (
        <group>
            <mesh
                ref={ref}
                onClick={() => {
                    console.log(!play)
                    setPlay(!play)
                }}
            >
                <boxGeometry attach='geometry' args={[1, 1, 1]} />
                <meshStandardMaterial attach="material" color={'#000'} />
            </mesh>
            {play && (<>
                <Spawner />
                <PlayerBox setPlay={setPlay} />
            </>
            )}
        </group>
    )
}

export default ActivateSpawner
Enter fullscreen mode Exit fullscreen mode

Step_2 Create "Spawner" component

Get random data (position, delay, color) for each "bubble" using a for loop and "randomIntBetween(a,b)" & randomIntBetweenAlsoNegatives(a,b) functions

import { Vector3 } from 'three';
import Bubble from './Bubble';


const Spawner = () => {


    function randomIntBetween(min, max) { // min and max included 
        return Math.floor(Math.random() * (max - min + 1) + min)
    }

    function randomIntBetweenAlsoNegatives(min, max) { // min and max included 
        const math = Math.floor(Math.random() * (max - min + 1) + min)
        const random = Math.random()
        const zeroOrOne = Math.round(random)
        if (zeroOrOne) return -(math)
        return math
    }

    const attackersArray = [];

    for (let i = 0; i < 20; i++) {

        let position = new Vector3(
            randomIntBetweenAlsoNegatives(0, 2),
            randomIntBetweenAlsoNegatives(0, 2),
            0)

        let wait = randomIntBetween(1, 12) * 10

        let color = `#${Math.random().toString(16).substring(2, 8)}`

        const att = [position, wait, color]
        attackersArray.push(att)
    }

    return (
        <group>
            {attackersArray.map((attackers, key) => {
                return <Bubble
                    key={key}
                    pos={attackers[0]}
                    wait={attackers[1]}
                    color={attackers[2]}
                />
            })}
        </group>
    );
};

export default Spawner;

Enter fullscreen mode Exit fullscreen mode

Step_3 Create "PlayerBox" component

Use "useThree" hook from '@react-three/fiber' to create a reference to our canvas "camera" object. Now we are able to give same value to our "PlayerBox" using "useFrame" hook

This hook calls you back every frame, which is good for running effects, updating controls, etc.

Add to our "Box" a "collisionFilterGroup" and "collisionFilterMask" properties.
The first defines in which group it is and the second which group it may collide with

import { useBox, } from '@react-three/cannon';
import { useFrame } from '@react-three/fiber';
import { useThree } from '@react-three/fiber'

const PlayerBox = (props) => {

    const { camera } = useThree()

    const [ref, api] = useBox(() => ({
        mass: 0,
        type: 'Dynamic',
        position: [0, 0, -5],
        args: [0.3, 0.3, 0.1], // collision box size
        collisionFilterGroup: 1,
        // 1 PlayerBox 2 Objetive 3 BulletBox 4 Attackers
        collisionFilterMask: 4,
        onCollide: (e) => {
            props.setPlay(false);
            console.log('game over')
        },
    }));

    // Tambien simula el movimiento de la camara (y por lo tnato el del objetivo), para poder tener un collider para el game over
    useFrame(() => {
        api.position.set(camera.position.x, camera.position.y, -2);
    });

    return (
        <>
            <mesh ref={ref}>
                <boxBufferGeometry attach='geometry' args={[0.1, 0.1, 0.1]} /> {/* box size */}
                <meshStandardMaterial attach="material" color={'#000'} />

            </mesh>
        </>
    );
};

export default PlayerBox;

Enter fullscreen mode Exit fullscreen mode

Step_4 Create "Bubble" component

In order to use the same "bubble" object to run the same race "x" number of times, add "setTimeout" function to reset the bubble position inside for loop.

import { useSphere } from '@react-three/cannon';
import { useFrame } from '@react-three/fiber';

const Bubble = (props) => {

    let zMovement = -20;

    const [ref, api] = useSphere(() => ({
        mass: 0,
        position: [props.pos.x, props.pos.y, props.pos.z - 200],
        type: 'Dynamic',
        // args: [1, 1, 1],
        // 1 PlayerBox 2 Objetive 3 BulletBox 4 Bubble
        collisionFilterGroup: 4,
        // No te va a colisionar, sino que vas a colisionar contra el
        collisionFilterMask: 1,
    }));

    useFrame(() => {
        api.position.set(
            props.pos.x,
            props.pos.y,
            (zMovement += 0.1) - props.wait
        );
    });

    for (let i = 1; i < 3; i++) {
        window.setTimeout(() => {
            zMovement = -50;
            api.position.set(0, 0, -zMovement);
            // 6 segs * i * wait= posicion de cada cubo para hacer que algunos salgan antes que otros
        }, 6 * 1000 + props.wait * 100);
    }

    return (
        <mesh ref={ref}>
            <sphereGeometry attach='geometry' args={[1, 32, 32]} />
            <meshStandardMaterial attach="material" color={props.color} />
        </mesh>
    );
};

export default Bubble;

Enter fullscreen mode Exit fullscreen mode

Step_5 Add "ActivateSpawner" in our App.jsx using "physics" node imported from "@react-three/cannon"

All components we've defined will be rendered in our DOM when
cameraMode is false => camera RIG mode setted

import { Canvas } from '@react-three/fiber';
import ActivateSpawner from './geometry/ActivateSpawner';
...
return (
...
{!cameraMode &&
                        < Physics >
                            <ActivateSpawner />
                        </Physics>
                    }
...
)
Enter fullscreen mode Exit fullscreen mode

Resume of components: ActivateSpawner , Spawner, PlayerBox, Bubble

Web3 will be added in next post

I hope it has been helpful.

Top comments (0)