Hire the author: Martin K

3D game ReactJs citation here 

GitHub link for this project GitHub

Introduction

Since their first appearance in computers in the 1960s, 3D environments have gone tremendous updates. In the 1970s we invented 3D modelling. The ability to visualize and work within the third dimension has been loved by users and developers, thus increasing in its popularity. The real-life object characteristics of length, width, and height are duplicated in this. 3D objects are experienced when interacting with computers and are fascinating to behold. Its features such as more light, shade, and contrast are visible and more appealing than 2D objects. We all expect discoveries, inventions, and the popularity of 3D  to increase with time.

In this article, we’ll build a 3D block-based game implementation in Javascript using React.js, Three.js, and React Three Fiber. Dirt, Grass, Wood, Log, and Glass are the five Minecraft block types supported by the game. The mouse and WASD keys can be used to move around. You can add and remove blocks by clicking and holding down the Alt key. Many React topics are covered in the essay, including useState, useEffect, useRef, and custom Hooks for state management and keyboard input. Zustand is a state management tool. Due to React’s ability to construct reusable components, we may develop units or components of our application that may be reused across numerous areas of our application. The steps listed below will be used to develop a Minecraft game based on this.

Photo Preview

What is Three.js?

Displaying animated 3D computer graphics in a web browser using WebGL requires Three.js. The graphical processing unit (GPU)-accelerated 3D animations is made possible by Three.js without relying on browser plugins. React-Three-Fiber is a 3D game ReactJs renderer that makes integrating Three.js into React components easier. 

Step by Step Procedure

Step 1: Creating the App.js file

First, import the canvas from react-three-fiber, the sky, and the physics from use-cannon. Define the function app with crucial parameters such as the position of the sky, light intensity, player, and ground position. Also, add ambient light and some spotlight to get some nice shadows as shown below

import React from 'react';
import { Canvas } from 'react-three-fiber';
import { Sky } from 'drei';
import { Physics } from 'use-cannon';
import { Ground } from './components/Ground';
import Cubes from './components/Cubes';
import { Player } from './components/Player';
import { Hud } from './components/Hud';
function App() {
return (
<Canvas shadowMap sRGB>
<Sky sunPosition={[100, 20, 100]} />
<ambientLight intensity={0.25} />
<pointLight castShadow intensity={0.7} position={[100, 100, 100]} />
<Hud position={[0, 0, -2]} />
<Physics gravity={[0, -30, 0]}>
<Ground position={[0, 0.5, 0]} />
<Player position={[0, 3, 10]} />
<Cubes />
</Physics>
</Canvas>
);
}
export default App;
view raw App.js hosted with ❤ by GitHub

Step 2: Creating the ground component

Definitely, the ground has some texture therefore add the grass texture by importing the TextureLoader and grass image. The ground uses repeat wrapping with the same size as the actual plane. We have to rotate the plane to ninety degrees because if we don’t, it will be vertical and yet we need it to lie down. The plane utilizes usePlane the hook from use-cannon to make it a physical object that can react to physics as shown below.

import React from 'react';
import { usePlane } from 'use-cannon';
import { TextureLoader, RepeatWrapping, NearestFilter, LinearMipMapLinearFilter } from 'three';
import grass from '../images/grass.jpg';
import { useStore } from '../hooks/useStore';
export const Ground = (props) => {
const [ref] = usePlane(() => ({ rotation: [-Math.PI / 2, 0, 0], ...props }));
const texture = new TextureLoader().load(grass);
const [addCube, activeTexture] = useStore((state) => [
state.addCube,
state.texture,
]);
texture.magFilter = NearestFilter;
texture.minFilter = LinearMipMapLinearFilter;
texture.wrapS = RepeatWrapping;
texture.wrapT = RepeatWrapping;
texture.repeat.set(100, 100);
return (
<mesh
ref={ref}
receiveShadow
onClick={(e) => {
e.stopPropagation();
const [x, y, z] = Object.values(e.point).map((coord) =>
Math.ceil(coord),
);
addCube(x, y, z, activeTexture);
}}
>
<planeBufferGeometry attach="geometry" args={[100, 100]} />
<meshStandardMaterial map={texture} attach="material" />
</mesh>
);
};
view raw Ground.js hosted with ❤ by GitHub

Step 3: Adding the player component

For the player use the camera from useThree hooks then represent the player with a sphere which is also from use-cannon. Set it to dynamic so that it can react to gravity and then on every frame take the camera that you get from three. Take the position of the player and copy it to the camera so that the camera will be in the middle of the player at all times.  

import React, { useEffect, useRef } from 'react';
import { useSphere } from 'use-cannon';
import { useThree, useFrame } from 'react-three-fiber';
import { FPVControls } from './FPVControls';
import { useKeyboardControls } from '../hooks/useKeyboardControls';
import { Vector3 } from 'three';
const SPEED = 6;
export const Player = (props) => {
const { camera } = useThree();
const {
moveForward,
moveBackward,
moveLeft,
moveRight,
jump,
} = useKeyboardControls();
const [ref, api] = useSphere(() => ({
mass: 1,
type: 'Dynamic',
...props,
}));
const velocity = useRef([0, 0, 0]);
useEffect(() => {
api.velocity.subscribe((v) => (velocity.current = v));
}, [api.velocity]);
useFrame(() => {
camera.position.copy(ref.current.position);
const direction = new Vector3();
const frontVector = new Vector3(
0,
0,
(moveBackward ? 1 : 0) - (moveForward ? 1 : 0),
);
const sideVector = new Vector3(
(moveLeft ? 1 : 0) - (moveRight ? 1 : 0),
0,
0,
);
direction
.subVectors(frontVector, sideVector)
.normalize()
.multiplyScalar(SPEED)
.applyEuler(camera.rotation);
api.velocity.set(direction.x, velocity.current[1], direction.z);
if (jump && Math.abs(velocity.current[1].toFixed(2)) < 0.05) {
api.velocity.set(velocity.current[0], 8, velocity.current[2]);
}
});
return (
<>
<FPVControls />
<mesh ref={ref} />
</>
);
};
view raw Player.js hosted with ❤ by GitHub

Step 4: Create useStore hook for state setup 

The useStore hook is for creating the state of our virtual world using the various imported features. The state takes a callback function that returns an object and asks for input to that callback function. Now model the state beginning with cubes by setting it with an empty array then creating the cube functions. Since we’ll use multiple textures on our cubes, we need to store the active texture by using the setTexture function. Use the saveWorld function to save the state of the game. If we have stored the state of our world in the local storage, we need the initial state to become what is already in the local storage. Therefore create the two functions for getting the local storage i.e. getLocalStorage and setLocalStorage.

import create from 'zustand';
import { nanoid } from 'nanoid';
const getLocalStorage = (key) => JSON.parse(window.localStorage.getItem(key));
const setLocalStorage = (key, value) =>
window.localStorage.setItem(key, JSON.stringify(value));
export const useStore = create((set) => ({
texture: 'dirt',
cubes: getLocalStorage('world') || [],
addCube: (x, y, z) =>
set((state) => ({
cubes: [
...state.cubes,
{ key: nanoid(), pos: [x, y, z], texture: state.texture },
],
})),
removeCube: (x, y, z) => {
set((state) => ({
cubes: state.cubes.filter((cube) => {
const [_x, _y, _z] = cube.pos;
return _x !== x || _y !== y || _z !== z;
}),
}));
},
setTexture: (texture) => {
set((state) => ({
texture,
}));
},
saveWorld: () =>
set((state) => {
setLocalStorage('world', state.cubes);
}),
}));
view raw useStore.js hosted with ❤ by GitHub

Step 5: Creating and texturizing the cube component 

Since the cube is a physical object which we can stand on, import the useBox from use-cannon as shown below. Receive the position and the type of cube from the props variable. For the cube to cast shades will use the castShadow property which is returned from mesh. Get a reference of useBox and pass the callback function and which should return an object. This object will have a static type because the boxes in Minecraft are not falling around since they remain where they are placed.

import React, { memo } from 'react';
import { useBox } from 'use-cannon';
import { useState } from 'react';
import * as textures from '../textures';
const Cube = ({ position, texture, addCube, removeCube }) => {
const [hover, setHover] = useState(null);
const [ref] = useBox(() => ({
type: 'Static',
position,
}));
const color = texture === 'glass' ? 'skyblue' : 'white';
return (
<mesh
castShadow
ref={ref}
onPointerMove={(e) => {
e.stopPropagation();
setHover(Math.floor(e.faceIndex / 2));
}}
onPointerOut={() => {
setHover(null);
}}
onClick={(e) => {
e.stopPropagation();
const clickedFace = Math.floor(e.faceIndex / 2);
const { x, y, z } = ref.current.position;
if (clickedFace === 0) {
e.altKey ? removeCube(x, y, z) : addCube(x + 1, y, z);
return;
}
if (clickedFace === 1) {
e.altKey ? removeCube(x, y, z) : addCube(x - 1, y, z);
return;
}
if (clickedFace === 2) {
e.altKey ? removeCube(x, y, z) : addCube(x, y + 1, z);
return;
}
if (clickedFace === 3) {
e.altKey ? removeCube(x, y, z) : addCube(x, y - 1, z);
return;
}
if (clickedFace === 4) {
e.altKey ? removeCube(x, y, z) : addCube(x, y, z + 1);
return;
}
if (clickedFace === 5) {
e.altKey ? removeCube(x, y, z) : addCube(x, y, z - 1);
return;
}
}}
>
<boxBufferGeometry attach="geometry" /> <meshStandardMaterial attach="material" map={textures[texture]} color={hover!=null ? 'gray' : color} opacity={texture === 'glass' ? 0.7 : 1} transparent={true} />
</mesh>
);
};
function equalProps(prevProps, nextProps) {
const equalPosition =
prevProps.position.x === nextProps.position.x &&
prevProps.position.y === nextProps.position.y &&
prevProps.position.z === nextProps.position.z;
return equalPosition && prevProps.texture === nextProps.texture;
}
export default memo(Cube, equalProps);
view raw Cube.js hosted with ❤ by GitHub

The next step will be adding some textures to the cube using the file below which has all the textures required for this project. Where we want to use all the textures us the textures variable and their respective names. Since we have six sides to the cube, we need to use a loop by creating an array and mapping it to meshStandardMaterial. Now you can be able to see a cube and its shadow.

import dirtImg from './images/dirt.jpg';
import grassImg from './images/grass.jpg';
import glassImg from './images/glass.png';
import logImg from './images/log.jpg';
import woodImg from './images/wood.png';
import { TextureLoader, NearestFilter, LinearMipMapLinearFilter } from 'three';
export const dirt = new TextureLoader().load(dirtImg);
export const grass = new TextureLoader().load(grassImg);
export const glass = new TextureLoader().load(glassImg);
export const wood = new TextureLoader().load(woodImg);
export const log = new TextureLoader().load(logImg);
dirt.magFilter = NearestFilter;
dirt.minFilter = LinearMipMapLinearFilter;
grass.magFilter = NearestFilter;
grass.minFilter = LinearMipMapLinearFilter;
glass.magFilter = NearestFilter;
glass.minFilter = LinearMipMapLinearFilter;
wood.magFilter = NearestFilter;
wood.minFilter = LinearMipMapLinearFilter;
log.magFilter = NearestFilter;
log.minFilter = LinearMipMapLinearFilter
view raw textures.js hosted with ❤ by GitHub

Future Directions

3D game in ReactJs objects are extremely effective artifacts. They depict our world items in great detail as we view them with our eyes and how they interact with one another. They display this data in computer settings for amusement as well as detailed study and business services. 3D modeling pays well because there are fewer experienced creators and developers than there are for static apps. For programmers that know how to construct, interact with, and explore them, this has proven out to be a tremendous ‘Gold-mine.’

Learning Strategies and Tools

3D game development in ReactJs necessitates the use of specific tools in addition to regular app development techniques. Fortunately, there are several solutions available, including  Cannon.js, use-Cannon, Three.js, and React-Three-Fiber. The development of the 3D game app in React would not have been possible without the use of an open-source JavaScript package that is simple to use. It makes it simple to integrate objects into our apps, as well as create layers and import data from a variety of sources.

Reflective Analysis

The game begins as a simple react application that can be created in the same way as any other react application. I discussed the game engine itself, which uses a set of things and systems to update those entities in order to generate a view for the user. I went over how to use hooks to update the game’s state, which is then used by entities when they render themselves. It’s a simple example, but the types of games that may be made with this engine are truly endless. Because I work on React apps for a living, it’s fantastic that I can use the same technology to make some fun stuff.

Conclusion

In this post, we’ve learned about React and how to use React to build a web game, we’ve also seen how to use Three.js, and React-Three- Fiber to write game logic. Finally, state management of the game application is made possible with the use of React Hooks. You can read more React articles here on the LD blog. The code used in this article can be found on GitHub, a working version of this game will only be available when you run the code. 

Hire the author: Martin K