DEV Community

Cover image for Creating a rudimentary pool table game using React, Three JS and react-three-fiber: Part 2
Manan Joshi
Manan Joshi

Posted on

Creating a rudimentary pool table game using React, Three JS and react-three-fiber: Part 2

Welcome to part 2 of a three-part series of articles where we will see how we can use React, three.js, and react-three-fiber to create a game of pool table.

I highly recommend going through part 1 before starting with part 2 as it explains the basics of how things works and gives a primer on setting up a React, three.js and react-three-fiber project.

BTW, I forgot to add this in the previous article but a working copy of the project can be found here and the source code over here

  • Part 1: Getting started with React, three.js, and react-three-fiber.
  • Part 2: Setting up the basic scene.
  • Part 3: Adding physics and finishing up(coming soon).

In this part, we will be setting up the scene for our game. We will be looking at many things along the way and understand the subtleties of how things will work.

Recap

In Part 1 we created a scene with a cube in it that didn't do anything but gave us an overview of the project.

At the end of the article, we were able to render something like this image.
shot2

I hope that now you are a little less intimidated by the libraries that we have used. On this note, let's jump right back into creating the scene. We want to start by adding lights to the scene.

Creating a Light component

  • Let us create a new file called Lights.js and copy and paste the code below.
import React from 'react';
import PropTypes from 'prop-types';

function Lights(props) {
  const { type } = props;
  const Light = type;

  return <Light {...props} />;
}

Lights.propTypes = {
  type: PropTypes.string
};

Lights.defaultProps = {
  type: ''
};

export default Lights;
Enter fullscreen mode Exit fullscreen mode
  • What we did here is, created a common component for all types of lights provided by three js.
  • Now let us make use of this light component in our scene.
  • First, let's start by adding an AmbientLight to the scene.
  • Open Scene.js and replace the code with the one below.
import React from 'react';
import { useThree } from 'react-three-fiber';
import Lights from '../components/Lights';

function Scene() {
  const { camera } = useThree();

  camera.fov = 45;
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.near = 0.1;
  camera.far = 1000;

  camera.up.set(0, 0, 1);
  camera.position.set(-5, 7, 5);

  return (
    <>
      <Lights
        type='AmbientLight'
        color={0xffffff}
        intensity={0.2}
        position={[0, 0, 0]}
      />
    </>
  );
}

export default Scene;
Enter fullscreen mode Exit fullscreen mode
  • As you can see we added a Lights component to the render function. The type prop says what kind of light we want with a bunch of other properties.
  • The next step is adding a bunch of PointLights to the scene.
  • Replace the contents of the return with the code given below in the render function.
return (
    <>
      <Lights
        type='AmbientLight'
        color={0xffffff}
        intensity={0.2}
        position={[0, 0, 0]}
      />
      {[[-5, -12, 20], [5, -12, 20], [-5, 12, 20], [5, 12, 20]].map(pos => (
        <Lights
        type='PointLight'
        color={0xffffff}
        intensity={0.4}
        distance={100}
        position={pos}
        castShadow
        />
      ))}
  </>
);

Enter fullscreen mode Exit fullscreen mode
  • This is going to create four point lights for us at the positions specified in the array. A full catalog of point light properties can be found here.

With this, we conclude our lighting section for the scene. Feel free to change positions of the lights, play around with colors, etc.

Next, we will be looking into adding a pool table mesh to the scene.

Adding a pool table mesh to the scene

  • Let's create a new file called PoolTable.js and add the code given below.
import React from 'react';
import { useLoader } from 'react-three-fiber';

import {
  TextureLoader,
  RepeatWrapping,
  Shape,
  ExtrudeGeometry,
  BoxGeometry,
  MeshStandardMaterial,
  CylinderGeometry,
  MeshBasicMaterial
} from 'three';

import ClothTextureURL from '../assets/cloth.jpg';
import WoodTextureURL from '../assets/hardwood_floor.jpg';

// shape for the cushion
const shape = new Shape();
shape.moveTo(0, 0);
shape.lineTo(0, 22);
shape.lineTo(0.5, 21.2);
shape.lineTo(0.5, 0.8);
shape.lineTo(0, 0);

// settings for the extrude geometry
const extrudeSettings = { steps: 1, depth: 1, bevelEnabled: false };

// geometry for the cushion
const cushionGeometry = new ExtrudeGeometry(shape, extrudeSettings);

// material for the play area
const clothMaterial = new MeshStandardMaterial({
  color: 0x42a8ff,
  roughness: 0.4,
  metalness: 0,
  bumpScale: 1
});

// geometry for the side edge
const edgeSideGeometry = new BoxGeometry(1, 22, 1);

// geometry for the top edge
const edgeTopGeometry = new BoxGeometry(22, 1, 1);

// geometry for pockets
const pocketGeometry = new CylinderGeometry(1, 1, 1.4, 20);

// material for pockets
const pocketMaterial = new MeshBasicMaterial({ color: 0x000000 });

function PoolTable() {
  // loading texture for the play area
  const clothTexture = useLoader(TextureLoader, ClothTextureURL);
  clothTexture.wrapS = RepeatWrapping;
  clothTexture.wrapT = RepeatWrapping;
  clothTexture.offset.set(0, 0);
  clothTexture.repeat.set(3, 6);

  // loading texture for the sides
  const woodTexture = useLoader(TextureLoader, WoodTextureURL);

  // applying texture to the sides material
  const edgeMaterial = new MeshStandardMaterial({ map: woodTexture });

  // applying texture to the play area material
  clothMaterial.map = clothTexture;

  return (
    <object3D position={[0, 0, -1]}>
      {/* mesh for the playing area */}
      <mesh receiveShadow>
        <boxGeometry attach='geometry' args={[24, 48, 1]} />
        <meshStandardMaterial
          attach='material'
          color={0x42a8ff}
          roughness={0.4}
          metalness={0}
          bumpScale={1}
          map={clothTexture}
        />
      </mesh>

      {/* mesh for the side edges */}
      {[
        [-12.5, 12, 0.7],
        [12.5, 12, 0.7],
        [-12.5, -12, 0.7],
        [12.5, -12, 0.7]
      ].map((pos, i) => {
        const idx = i;
        return (
          <mesh
            key={idx}
            args={[edgeSideGeometry, edgeMaterial]}
            position={pos}
          />
        );
      })}

      {/* mesh for the top edges */}
      {[[0, 24.5, 0.7], [0, -24.5, 0.7]].map((pos, i) => {
        const idx = i;
        return (
          <mesh
            key={idx}
            args={[edgeTopGeometry, edgeMaterial]}
            position={pos}
          />
        );
      })}

      {/* mesh for the side cushions */}
      {[[-12, 1, 0.2], [12, 1, 1.2], [-12, -23, 0.2], [12, -23, 1.2]].map(
        (pos, i) => {
          const idx = i;
          return (
            <mesh
              key={idx}
              args={[cushionGeometry, clothMaterial]}
              position={pos}
              rotation={
                idx === 1 || idx === 3
                  ? [0, (180 * Math.PI) / 180, 0]
                  : [0, 0, 0]
              }
            />
          );
        }
      )}

      {/* mesh for the top cushions */}
      {[[-11, 24, 0.2], [11, -24, 0.2]].map((pos, i) => {
        const idx = i;
        return (
          <mesh
            key={idx}
            args={[cushionGeometry, clothMaterial]}
            position={pos}
            rotation={
              idx === 0
                ? [0, 0, (-90 * Math.PI) / 180, 0]
                : [0, 0, (90 * Math.PI) / 180, 0]
            }
          />
        );
      })}

      {/* mesh for the pockets */}
      {[
        [-12, 24, 0],
        [12, 24, 0],
        [-12.5, 0, 0],
        [12.5, 0, 0],
        [-12, -24, 0],
        [12, -24, 0]
      ].map((pos, i) => {
        const idx = i;
        return (
          <mesh
            key={idx}
            args={[pocketGeometry, pocketMaterial]}
            position={pos}
            rotation={[1.5708, 0, 0]}
          />
        );
      })}
    </object3D>
  );
}

export default PoolTable;
Enter fullscreen mode Exit fullscreen mode
  • This will create a mesh for the pool table for us.
  • As you can see this file is a lot more involved than any of the other components that we have written till now.
  • So let's see what the code is doing here.
  • First of all, we will need textures for the play area and the sides. You can download those here and here, but feel free to use any image.
  • Next up we define geometry for the side and top cushions.
  • It uses Shape from three.js along with extrudeGeometry which create an extruded geometry from a given path shape.
  • After that, as seen earlier we use different materials and other geometries to create sides and pockets.
  • Now we want to load out texture for the play area. We use the useLoader hook provided by react-three-fiber that takes as argument the type of loader we want to use as well as path url and an optional callback function.
  • There are lots and lots of loaders provided by three.js and all of them can be used with the useLoader hook.
  • For our purposes, since we want to load a texture we will be using the TextureLoader.
  • There is also another way to use loaders in your app if for some reason you don't want to use the useLoader hook by using the useMemo react hook. The code looks something like the one below.
const texture = useMemo(() => new TextureLoader().load(textureURL), [textureURL]);
Enter fullscreen mode Exit fullscreen mode
  • The idea here is to wrap the loading inside useMemo so that it is computationally efficient.
  • We would do the same process to load our texture for the sides as well.
  • Now since our textures are loaded the last thing we want to do is apply our textures to their respective materials. This can be done by using the map key of the material where the texture is needed to be applied.
  • With this, we can go ahead and start putting up our pool table mesh together.
  • We start with the play area first and then start adding the sides, cushions, and pockets on top of it.
  • Now, it's time to add this component to our Scene.
return (
  <>
      <Lights
        type='AmbientLight'
        color={0xffffff}
        intensity={0.2}
        position={[0, 0, 0]}
      />
      {[[-5, -12, 20], [5, -12, 20], [-5, 12, 20], [5, 12, 20]].map(pos => (
        <Lights
          type='PointLight'
          color={0xffffff}
          intensity={0.4}
          distance={100}
          position={pos}
          castShadow
        />
      ))}
      <React.Suspense fallback={<mesh />}>
        <PoolTable />
      </React.Suspense>
    </>
)
Enter fullscreen mode Exit fullscreen mode
  • We wrap the PoolTable component using Suspense so that all the textures can be loaded correctly before the pool table is rendered.
  • useLoader hook that we had used in our pool table component suspends the rendering while it's loading the texture and hence if you don't use Suspense React will complain to you about adding a fallback.
  • Go ahead and start the app and the output should look something like the image.

pool-table-shot

  • You'll also be able to use the zoom-in, zoom-out, rotate controls that we had created earlier. Go ahead and try that.
  • I hope you are happy with everything that we did here. The last part for this article will be to add balls on to the pool table

Adding Pool Table balls

  • Let's create a new file called PoolBall.js and add the code given below.
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { TextureLoader, Vector2 } from 'three';

function PoolBall({ setRef, position, textureURL }) {
  const ballTexture = useMemo(() => new TextureLoader().load(textureURL), [
    textureURL
  ]);

  return (
    <mesh ref={setRef} position={position} speed={new Vector2()} castShadow>
      <sphereGeometry attach='geometry' args={[0.5, 128, 128]} />
      <meshStandardMaterial
        attach='material'
        color={0xffffff}
        roughness={0.25}
        metalness={0}
        map={ballTexture}
      />
    </mesh>
  );
}

PoolBall.propTypes = {
  setRef: PropTypes.objectOf(PropTypes.any),
  position: PropTypes.arrayOf(PropTypes.number),
  textureURL: PropTypes.string
};

PoolBall.defaultProps = {
  setRef: {},
  position: [],
  textureURL: ''
};

export default PoolBall;
Enter fullscreen mode Exit fullscreen mode
  • This will create a pool ball for us.
  • As you can see in the code we have used the useMemo way of loading the texture for the ball.
  • The render function is pretty straight-forward here and this is a short exercise for you to see what it does base on everything that we have seen so far.
  • If you have any questions, please post it in the comments below and I'll get back to you.
  • Just one additional thing to note here is that the speed prop is not an actual property on the mesh but we will need it to compute the speed of the ball when we do physics calculations. But, now you can see that we can pass in custom props as well.
  • Let's add the balls to our pool table now.
  • Open Scene.js and update the return of the render function as follow.
return (
    <>
      <Lights
        type='AmbientLight'
        color={0xffffff}
        intensity={0.2}
        position={[0, 0, 0]}
      />
      {[[-5, -12, 20], [5, -12, 20], [-5, 12, 20], [5, 12, 20]].map(pos => (
        <Lights
          type='PointLight'
          color={0xffffff}
          intensity={0.4}
          distance={100}
          position={pos}
          castShadow
        />
      ))}
      <React.Suspense fallback={<mesh />}>
        <PoolTable />
      </React.Suspense>
      <object3D>
        <PoolBall position={[0, -16, 0]} textureURL={zero} />
        <PoolBall position={[-1.01, 15, 0]} textureURL={one} />
        <PoolBall position={[1.01, 17, 0]} textureURL={two} />
        <PoolBall position={[-0.51, 16, 0]} textureURL={three} />
        <PoolBall position={[-1.01, 17, 0]} textureURL={four} />
        <PoolBall position={[-2.02, 17, 0]} textureURL={five} />
        <PoolBall position={[1.53, 16, 0]} textureURL={six} />
        <PoolBall position={[0.51, 14, 0]} textureURL={seven} />
        <PoolBall position={[0, 15, 0]} textureURL={eight} />
        <PoolBall position={[0, 13, 0]} textureURL={nine} />
        <PoolBall position={[0.51, 16, 0]} textureURL={ten} />
        <PoolBall position={[2.02, 17, 0]} textureURL={eleven} />
        <PoolBall position={[-0.51, 14, 0]} textureURL={twelve} />
        <PoolBall position={[0, 17, 0]} textureURL={thirteen} />
        <PoolBall position={[-1.53, 16, 0]} textureURL={fourteen} />
        <PoolBall position={[1.01, 15, 0]} textureURL={fifteen} />
      </object3D>
    </>
  );
Enter fullscreen mode Exit fullscreen mode
  • Here, as you can see we are grouping all the balls as a single object. This is not always necessary but is useful while debugging.
  • Also, I have used all the 16 balls here, but you can work with any number of balls. It can be 5, 8, 12 any number that you like, however, you will have to give correct positions to make everything look in order.
  • I have used different textures for all the balls but you can use only one texture if you want or no texture will work as well.
  • Textures need to imported like the code below into the scene. For all the textures that I have used in this example, you can find them here.
import zero from '../assets/textures/0.png';
Enter fullscreen mode Exit fullscreen mode
  • At this point, we are done just restart your app and you'll be able to see the balls on the table. It should look something like the image below.

pool-table-with-balls-shot

With this, we conclude part-2. In the next part, we will be seeing how we can write a small physics engine that can detect collisions and hit the balls and see how they behave when they collide.

As always, please post your questions, comments or any feedback in the comments section below and I'll be happy to answer them for you. Find me on Twitter and Instagram.

Peace out and happy coding!!!

Top comments (9)

Collapse
 
vasco3 profile image
JC

do you know if there would be any caveat on building this in nextjs?

Collapse
 
follow_cy profile image
ᴄʏ

There is one actually. You might need to import your webgl scene with const WebglNoSSR = dynamic(() => import("../components/Scene"), {
ssr: false
});
depending on what you do with it.

Collapse
 
manan30 profile image
Manan Joshi

I do not see any. At the end of the day, this is just JavaScript written with the expressiveness of React.

Collapse
 
fasani profile image
Michael Fasani

Did you make part 3?

Collapse
 
manan30 profile image
Manan Joshi

Hey Michael, I haven't had the chance yet, hopefully soon. I'll DM you once I write it.

Collapse
 
hackgod2000 profile image
HackGod2000

How do you the exact position of the balls and where to place the cushions too.

Collapse
 
manan30 profile image
Manan Joshi

It's statically added at the beginning.

Collapse
 
gscrawley profile image
Gideon S Crawley

hey Manan, did you ever make part 3? I was about to start this tutorial but if there's no collision engine then there's not much point. Or is that included in the source code?

Collapse
 
manan30 profile image
Manan Joshi

I haven't had time to write part 3, but the source code does include the physics engine