Building Your First Browser Game with Three.js and React: Part 3 - Adding Interactivity and Physics
Tags: webdev, threejs, beginners, react
Published: 2024-05-14 10:30:00


## Introduction

Welcome back to our series on creating a **3D basketball game** using **Three.js** and **React** in your browser!
So far, we've set up our project and incorporated essential 3D models into our scene. Now, it's time to animate our game with **interactivity** and **physics**.
This installment will focus on integrating physics to simulate realistic ball movements and interactions, making our game genuinely interactive and enjoyable.

## Step 1: Setting Up Physics with @react-three/rapier

When it comes to integrating physics into **React Three Fiber**, several libraries are available. [@react-three/rapier](https://github.com/pmndrs/react-three-rapier) stands out as one of the most recent updated (created in 2019 and written in Rust), making it a great choice for adding physics in 3D web environments.

While **Rapier** is known for its ease of use and good performance, other libraries might also be excellent choices depending on specific use cases, like [Cannon](https://github.com/pmndrs/cannon-es) for example. (The fork by **pmndrs**, that is awesome too)

First, ensure that `@react-three/rapier` is installed in your project:

```bash
npm install @react-three/rapier
```

Initialize the physics engine in your Experience file where all our scene is set up.

To accomplish this, we add the `Physics` component, provided by **rapier**, around our Table and Ball. We don't need to include our lights, controls, or environment, as they don't require physics.

We are adding a `debug` parameter to the Physics component. This will allow us to view all the colliders of our object.

```jsx
import React from 'react';
import { Box, Center, Environment, OrbitControls } from '@react-three/drei';
import Table from './Components/Table';
import Ball from './Components/Ball';
import { Physics } from '@react-three/rapier';

const Experience = () => {
  return (
    <>
      <color attach="background" args={["#ddc28d"]} />

      <ambientLight />
      <directionalLight position={[0, 1, 2]} intensity={1.5} />
      <Environment preset="city" />

      <OrbitControls makeDefault />

      <Physics debug>
        <Center>
          <Table position={[0, 0, 0]} />
          <Ball position={[0.25, 1.5, 0]} />
        </Center>
      </Physics>
    </>
  );
}

export default Experience;

```

This wrapper will manage all physics-related updates automatically.

## Step 2: Adding Physics to the Ball

Let's add physics to our ball. We'll adjust the `Ball` component to include the `RigidBody` component from **rapier**.

```jsx
import { RigidBody } from '@react-three/rapier';

const Ball = ({ position }) => {
  return (
    <group position={position} scale={0.7}>
      <RigidBody>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Sphere.geometry}
          material={materials["Material.001"]}
        />
      </RigidBody>
    </group>
	);
};
```

We also include a **group** in our mesh to set the position and scale for the ball, along with our rigid body. To slightly decrease the ball's size, we incorporate a scale parameter of 0.7, which we'll use later to fit the hoop ring.

This setup adds **physics** to our ball. You should see the ball falling into the depths of our scene. The ball falls because we have not integrated physics into our table yet.

Before the ball falls, you should see a square around it, as shown in the screenshot below. This square represents the object's colliders. By default, all colliders are **square-shaped** and adapt to the size of the object to which we add a `RigidBody`. However, we can adjust the shape using the `colliders` parameter of RigidBody.

![Screenshot of the ball component with colliders see around](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w0zmdvig1j77cpb13b9q.png)

Let's try another collider, such as `ball`:

```jsx
import { RigidBody } from '@react-three/rapier';

const Ball = ({ position }) => {
  const { nodes, materials } = useGLTF("/models/basketball.glb");

  return (
    <group position={position} scale={0.7}>
      <RigidBody colliders="ball">
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Sphere.geometry}
          material={materials["Material.001"]}
        />
      </RigidBody>
    </group>
  );
};
```

You should now see the collider in a "round shape" (like a ball). This is exactly what we want!

![Screenshot of the ball component with ball collider see around](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c70ypzssoftau9wtw5yk.png)

It exist some colliders provided by rapier:

- `cuboid` , the default one, for square shape.
- `ball`, the one we use, for round shape.
- `hull`, a collider that fits a shape very closely but ignores holes within it. It's useful for complex objects without holes.
- `trimesh` is similar to `hull`, but it doesn't ignore holes. While this collider is powerful, it can [complicate collision detection and potentially introduce bugs](https://rapier.rs/docs/user_guides/bevy_plugin/colliders/#triangle-meshes-and-polylines). It's best to avoid it if possible, but for smaller projects with less collision, it’s acceptable. (Am I saying this because I use it in this project? Perhaps..)

## Step 3: Adding physics to the Table

Now, let's modify the `Table` component to respond to the ball. This way, the ball will be "trapped" inside the table. First, let’s add `RigidBody` for the **Table** mesh:

```jsx
import { RigidBody, useRigidBody } from '@react-three/rapier';

const Table = (props) => {
  return (
    <group {...props}>
      <RigidBody type='fixed'>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Table.geometry}
          material={materials.Wood}
          position={[0, 0.068, 0]}
        />
      </RigidBody>
	    {/* ... */}
    </group>
  );
};

```

Now, we should observe the ball falling and coming to rest on the table. We're adding a 'fixed' type to the rigid body because the table shouldn't move; it simply acts as a "floor" for our ball. Take note of the table's colliders as illustrated below:

![Screenshot of our scene with colliders for ball and table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7uds3141ugti5emqxxoc.png)

Our table has a shape more complex than a basic cuboid, so let's use trimesh instead (yes, I know, emergency collider only..).

```jsx
import { RigidBody, useRigidBody } from '@react-three/rapier';

const Table = (props) => {
  return (
    <group {...props}>
      <RigidBody type='fixed' colliders='trimesh'>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Table.geometry}
          material={materials.Wood}
          position={[0, 0.068, 0]}
        />
      </RigidBody>
      {/* ... */}
    </group>
  );
};

```

Now, you can observe that our collider perfectly matches the shape of our table.

![Screenshot of our table with trimesh collider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1qb7tep1qafv7b4l7cma.png)

Now that our base table is set up, we need to add physics to our glass as well. This should be the final mesh of the Table component (if you followed the code given in this serie):

```jsx
import { RigidBody, useRigidBody } from '@react-three/rapier';

const Table = (props) => {
  return (
    <group {...props}>
      {/* ... */}
      <RigidBody type='fixed' colliders='trimesh'>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Glass.geometry}
          position={[0.497, 1.54, 0.005]}
        >
          <MeshTransmissionMaterial anisotropy={0.1} chromaticAberration={0.04} distortionScale={0} temporalDistortion={0} />
        </mesh>
      </RigidBody>
    </group>
  );
};

```

The last physics component to add is our basketball hoop. It comprises four meshes: Base, Cylinder, Panel, and Ring. Like the others, enclose these four meshes in a RigidBody with a fixed type and trimesh colliders.

```jsx
import { RigidBody, useRigidBody } from '@react-three/rapier';

const Table = (props) => {
  return (
    <group {...props}>
      {/* ... */}
      <RigidBody type='fixed' colliders='trimesh'>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Base.geometry}
          material={materials.Wood}
          position={[-2.235, 0.565, 0]}
        />
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Cylinder.geometry}
          material={materials.Red}
          position={[-2.235, 1.177, 0]}
        />
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Panel.geometry}
          material={materials.Wood}
          position={[-2.234, 1.814, 0]}
        />
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Ring.geometry}
          material={materials.Red}
          position={[-1.686, 1.46, 0]}
        />
      </RigidBody>
      {/* ... */}
    </group>
  );
};

```

We have now integrated physics into all of our models (except for a few used for controls)! You should be able to see all the colliders in your scene like this:

![Screenshot of our scene with all colliders activated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oo8zv13ra75n96hm62ur.png)

To observe our physics in action, let's adjust the ball's position slightly to the left and watch it roll across the table:

```jsx
const Experience = () => {
  return (
    <>
      {/* ... */}
      <Physics debug>
        <Center>
          <Table position={[0, 0, 0]} />
          {/* Let's change the third parameter of position to 0.2 */}
          <Ball position={[0.25, 1.5, 0.2]} />
        </Center>
      </Physics>
    </>
  );
}

export default Experience;
```

## Step 4: Tweak the Restitution and the Friction

Let's assign some default values to our existing physics to improve the ball's reaction to the floor, among other things.

Return to our Ball component and add **restitution** and **friction** to the RigidBody:

```jsx
import { RigidBody } from '@react-three/rapier';

const Ball = ({ position }) => {
  const { nodes, materials } = useGLTF("/models/basketball.glb");

  return (
    <group position={position} scale={0.7}>
      <RigidBody colliders="ball" restitution={1} friction={0.2}>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Sphere.geometry}
          material={materials["Material.001"]}
        />
      </RigidBody>
    </group>
  );
};
```

**Restitution** refers to the "**bounciness**" of an object. The default value is 0, meaning the object does not bounce. We set it to 1 to give our basketball a lot of bounce!

**Friction** refers to the degree to **which surfaces rub against each other**. The default value is 0.7. The higher the friction, the quicker the object will stop. To let our ball slide more on the table, we’ll reduce the friction to 0.2.

You should see the ball jumping more. Feel free to tweak these parameters to observe the changes!

Now, let's return to our table. We will add both restitution and friction to the table and the glass.

```jsx
import { RigidBody, useRigidBody } from '@react-three/rapier';

const Table = (props) => {
  return (
    <group {...props}>
      <RigidBody type='fixed' colliders='trimesh' restitution={0.6} friction={0}>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Table.geometry}
          material={materials.Wood}
          position={[0, 0.068, 0]}
        />
      </RigidBody>
      {/* ... */}
      <RigidBody type='fixed' colliders='trimesh' restitution={0.2} friction={0}>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Glass.geometry}
          position={[0.497, 1.54, 0.005]}
        >
          <MeshTransmissionMaterial anisotropy={0.1} chromaticAberration={0.04} distortionScale={0} temporalDistortion={0} />
        </mesh>
      </RigidBody>
    </group>
  );
};
```

You should see the ball jumping even more! We have now the perfect setup for our game. We can now add the **controls**.

## Step 5: Integrating Controls

To make our game **interactive**, we need to give players control over the ball. We'll implement click controllers to trigger thrusters that propel the ball towards the basket ring.

First, we are going to add the “onclick” control on our `Control_A` and `Control_B` mesh.

### Adding Control on Mesh

To identify our controls, we need to use `useRef()`, a hook from React that allows us to reference a value. This value can be anything we want, such as a boolean, a number, a string, or an object. After assigning this reference to our mesh, we can retrieve it later.

Let's incorporate it into our Table component:

```jsx
import { useRef } from 'react'

const Table = (props) => {

  const controlA = useRef(null);
  const controlB = useRef(null);

  return (
    <group {...props}>
      {/* ... */}
      <mesh
        ref={controlA}
        castShadow
        receiveShadow
        geometry={nodes.Control_A.geometry}
        material={materials.Red}
        position={[4.184, 0.129, 0.744]}
      >
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Control_A_Text.geometry}
          material={materials.White}
          position={[0.237, 0.046, 0.21]}
          rotation={[Math.PI / 2, 1.179, -Math.PI / 2]}
        />
      </mesh>
      <mesh
        ref={controlB}
        castShadow
        receiveShadow
        geometry={nodes.Control_B.geometry}
        material={materials.Green}
        position={[4.183, 0.128, -0.754]}
      >
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Control_B_Text.geometry}
          material={materials.White}
          position={[0.25, 0.043, 0.207]}
          rotation={[Math.PI / 2, 1.184, -Math.PI / 2]}
        />
      </mesh>
      {/* ... */}
    </group>
  );
};
```

Now that we have our references to Control A and Control B, we need to activate the "click" control. For this, the `<mesh>` component from Three.js offers two functions that we can use: `onPointerUp` and `onPointerDown`.

We can use these functions to trigger a function we've created when a player clicks on ControlA or ControlB. Let's create these two functions and assign them to the controls:

```jsx
import { useRef } from 'react'

const Table = (props) => {

  const controlA = useRef(null);
  const controlB = useRef(null);

  const clickUp = (controlRef) => {
    if (controlRef.current) {
      controlRef.current.position.y = 0.128
    }
  }

  const clickDown = (controlRef) => {
    if (controlRef.current) {
      controlRef.current.position.y = 0.128 - 0.1
    }
  }

  return (
    <group {...props}>
      {/* ... */}
      <mesh
        ref={controlA}
        castShadow
        receiveShadow
        geometry={nodes.Control_A.geometry}
        material={materials.Red}
        position={[4.184, 0.129, 0.744]}
        onPointerUp={() => clickUp(controlA)}
        onPointerDown={() => clickDown(controlA)}
      >
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Control_A_Text.geometry}
          material={materials.White}
          position={[0.237, 0.046, 0.21]}
          rotation={[Math.PI / 2, 1.179, -Math.PI / 2]}
        />
      </mesh>
      <mesh
        ref={controlB}
        castShadow
        receiveShadow
        geometry={nodes.Control_B.geometry}
        material={materials.Green}
        position={[4.183, 0.128, -0.754]}
        onPointerUp={() => clickUp(controlB)}
        onPointerDown={() => clickDown(controlB)}
      >
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Control_B_Text.geometry}
          material={materials.White}
          position={[0.25, 0.043, 0.207]}
          rotation={[Math.PI / 2, 1.184, -Math.PI / 2]}
        />
      </mesh>
      {/* ... */}
    </group>
  );
};
```

In this function, we "simulate" a click by slightly lowering the position of our control, similar to pressing a button. We trigger the `clickDown` action when the user clicks the button, and `clickUp` action when they release the click.

You can test it on your scene, you should see the button going down when clicking on it.

We can now proceed to the next part: adding physics to the thruster.

### Adding Physics on Thrusters

In this section, we need to identify our thrusters using `useRef`, add a special rigid body to them, and activate them when we use the controls. Let's get started:

```jsx
const Table = (props) => {

  // Add useRef on thruster to identify them
  const thrusterA = useRef(null);
  const thrusterB = useRef(null);


  const clickUp = (controlRef) => {
    if (controlRef.current) {
      controlRef.current.position.y = 0.128
    }
  }

  const clickDown = (controlRef) => {
    if (controlRef.current) {
      controlRef.current.position.y = 0.128 - 0.1
    }
  }

  return (
    <group {...props}>
      {/* ... */}
      <RigidBody
        ref={thrusterA}
        type="kinematicPosition"
        colliders="hull"
        lockRotations={true}
        enabledTranslations={[false, true, false]}
      >
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Thruster_A.geometry}
          material={materials.Black}
          position={[2.259, -0.189, 0.765]}
        />
      </RigidBody>
      <RigidBody
        ref={thrusterB}
        type="kinematicPosition"
        colliders="hull"
        lockRotations={true}
        enabledTranslations={[false, true, false]}
      >
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Thruster_B.geometry}
          material={materials.Black}
          position={[2.259, -0.189, -0.764]}
        />
      </RigidBody>
      {/* ... */}
    </group>
  );
};
```

We assign a specific type of RigidBody, **kinematicPosition**, to the thrusters. This type allows our object to be moved using special functions, such as translate/rotate, through code. The rigid body doesn't move until we instruct it to, and can't be moved by other rigid bodies, for example.

We also restrict rotations on this mesh, as we only want it to move up and down.

Now, we simply need to move it up or down when we press or release our controls.  Let's do it:

```jsx
const Table = (props) => {

  const thrusterA = useRef(null);
  const thrusterB = useRef(null);

  const clickUp = (controlRef) => {
    if (controlRef.current) {
    controlRef.current.position.y = 0.128

    if(controlRef === controlA) {
      const position = vec3(thrusterA.current.translation())
      thrusterA.current.setNextKinematicTranslation({
        x: position.x,
        y: position.y - 0.5,
        z: position.z
      })
    } else {
      const position = vec3(thrusterB.current.translation())
      thrusterB.current.setNextKinematicTranslation({
        x: position.x,
        y: position.y - 0.5,
        z: position.z
      })
    }
    }
  }

  const clickDown = (controlRef) => {
    if (controlRef.current) {
    controlRef.current.position.y = 0.128 - 0.1

    if(controlRef === controlA) {
      const position = vec3(thrusterA.current.translation())
      thrusterA.current.setNextKinematicTranslation({
        x: position.x,
        y: position.y + 0.5,
        z: position.z
      })
    } else {
      const position = vec3(thrusterB.current.translation())
      thrusterB.current.setNextKinematicTranslation({
        x: position.x,
        y: position.y + 0.5,
        z: position.z
      })
    }
    }
  }

  return (
    {/* ... */}
  );
};
```

Now, when we activate the `clickUp` and `clickDown` functions by pressing our controls, thruster A for control A and thruster B for control B will propel the ball across our table.

To accomplish this, we simply use the `setNextKinematicTranslation` function to apply a translation to our object. We just need to add a number, such as 0.5, to the y-axis to move it upwards.

When the user clicks, the thruster elevates by 0.5. When the user releases the click, the thruster reverts back to -0.5.

### Small Tweak to Improve Game

To enhance our game, I propose adding a parameter to the rigid body of our Ball component: **gravityScale**. This parameter enables us to influence how gravity impacts our object. By default, this value simulates Earth's gravity (-9.81 approximately). However, I want the ball to be more energetic and faster, so let's introduce a new value:

```jsx
const Ball = ({ position }) => {
  return (
    <group position={position} scale={0.7}>
      <RigidBody colliders="ball" restitution={1} friction={0.2} gravityScale={3.5}>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.Sphere.geometry}
          material={materials["Material.001"]}
          />
      </RigidBody>
    </group>
  );
};
```

## Step 6: Testing and Debugging

With physics and controls established, thoroughly testing the game is crucial to observe their interactions. Based on this testing, adjust the physics parameters and controls as needed. Observe how different settings alter the gameplay.

If you encounter any issues, please don't hesitate to contact me!

## Conclusion

Congratulations! You have successfully added interactivity and physics to your browser-based basketball game. The ball now moves realistically, and players can control actions within the game. It's time to start scoring some baskets!

In the next installment, we will introduce game mechanics to make the game fully playable.

Stay tuned and happy coding!