Cannon physics - 3D web3 serie
Uigla
Posted on August 19, 2022
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>
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 }))
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={...} />
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))
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
}, [])
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
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
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;
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;
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;
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>
}
...
)
Resume of components: ActivateSpawner , Spawner, PlayerBox, Bubble
Web3 will be added in next post
I hope it has been helpful.
Posted on August 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.