Creating a rudimentary pool table game using React, Three JS and react-three-fiber: Part 2
Oct 31, 2019
10 min read
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.
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;
- 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;
- 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
/>
))}
</>
);
- 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;
- 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 theuseMemo
react hook. The code looks something like the one below.
const texture = useMemo(() => new TextureLoader().load(textureURL), [textureURL]);
- 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>
</>
)
- 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 useSuspense
React will complain to you about adding a fallback.- Go ahead and start the app and the output should look something like the image.
- 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;
- 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>
</>
);
- 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';
- 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.
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 feel free to DM me on Twitter or Instagram with any questions, comments or any feedback and I'll be happy to answer them for you.
Peace out and happy coding!!!