In this chapter, we implement hit detection and add a result screen. We check if the player gets hit by a vehicle, and if so, we show the final score and add logic to reset the game.
Hit Detection
We add a new hook that checks from the vehicles’ perspective if they hit the player. If so, they change the game status to over.
Introducing the Game Status
We add a second property, status, to the game store to indicate whether the game is running or over. By default, the game is running.
We also add an endGame method to set the status to over. We will call this method when the player gets hit by a vehicle.
So far, the player and the vehicles have handled their own movement independently. They have no notion of each other. To handle hit detection, either the player needs to know about the vehicles or the vehicles need to know about the player.
We chose the former approach because this way, we only need to store one reference to the player in the store, and all the vehicles can check against this reference.
Let’s extend the player store with a ref property to store the player object’s reference. We also expose a setRef method that sets this reference.
Then, we call the setRef method in the Player component to set the reference to the player object. We already have the player reference, so we can pass its value to the setRef method in the useEffect hook once it is set.
Let’s define another hook to handle hit detection. We check if the player intersects with any of the vehicles. If they do, we set the game status to over.
This hook is from the perspective of a vehicle. It receives the vehicle reference and the rowIndex. We check if the vehicle intersects with the player if the player is in the same row, the row before, or the row after the vehicle. We use the useFrame hook to run the hit detection logic on every frame.
We create bounding boxes for the player and the vehicle to check for an intersection. This might be an overkill, as the shape of our objects is known, but it is a nice generic way to handle hit detection. If the bounding boxes intersect, we call the endGame function from the game store.
src/hooks/useHitDetection.ts
import*as THREE from"three";
import { useFrame } from"@react-three/fiber";
import { state as player } from"../stores/player";
if (playerBoundingBox.intersectsBox(vehicleBoundingBox)) {
endGame();
}
}
});
}
Then, we call this hook in the vehicle components. First, we add it to the Car component. We pass the car reference and the rowIndex to the useHitDetection hook.
Then, we do the same in the Truck component. If either a car or a truck hits the player, the game changes its status to over.
You can add further logic to, for instance, disable further movement of the player. We don’t cover this here, but you can add it as an exercise. Instead, let’s display a result screen with the final score.
On game over, we display a popup with the final score and a reset button.
The Result Screen
Once the player gets hit by a vehicle, the game status is set to over. We show a result screen with the final score and a button to restart the game.
Let’s add a new component called Result. This regular React component reads the game state from the store and displays the result if the game is over. It also has a button to restart the game. In the next section, we add the necessary logic to reset the game.
src/components/Result.tsx
import useStore from"../stores/game";
import"./Result.css";
exportfunctionResult() {
conststatus=useStore((state) => state.status);
constscore=useStore((state) => state.score);
constreset=useStore((state) => state.reset);
if (status ==="running") returnnull;
return (
<divid="result-container">
<divid="result">
<h1>Game Over</h1>
<p>Your score: {score}</p>
<buttononClick={reset}>Retry</button>
</div>
</div>
);
}
src/components/Result.jsx
import useStore from"../stores/game";
import"./Result.css";
exportfunctionResult() {
conststatus=useStore((state) => state.status);
constscore=useStore((state) => state.score);
constreset=useStore((state) => state.reset);
if (status ==="running") returnnull;
return (
<divid="result-container">
<divid="result">
<h1>Game Over</h1>
<p>Your score: {score}</p>
<buttononClick={reset}>Retry</button>
</div>
</div>
);
}
The Result component also has some style. We style the result as a centered container with a white background and a red button to restart the game. By default, the result screen is hidden.
src/components/Result.css
#result-container {
position: absolute;
min-width: 100%;
min-height: 100%;
top: 0;
display: flex;
align-items: center;
justify-content: center;
#result {
display: flex;
flex-direction: column;
align-items: center;
background-color: white;
padding: 20px;
}
button {
background-color: red;
padding: 20px50px20px50px;
font-family: inherit;
font-size: inherit;
cursor: pointer;
}
}
Finally, let’s add this new component to the root of the Game component. With that, only one piece is missing to complete the game: the logic to reset the game.
src/Game.tsx
import { Scene } from"./components/Scene";
import { Player } from"./components/Player";
import { Map } from"./components/Map";
import { Score } from"./components/Score";
import { Controls } from"./components/Controls";
import { Result } from"./components/Result";
import"./index.css";
exportdefaultfunctionGame() {
return (
<divclassName="game">
<Scene>
<Player />
<Map />
</Scene>
<Score />
<Controls />
<Result />
</div>
);
}
src/Game.jsx
import { Scene } from"./components/Scene";
import { Player } from"./components/Player";
import { Map } from"./components/Map";
import { Score } from"./components/Score";
import { Controls } from"./components/Controls";
import { Result } from"./components/Result";
import"./index.css";
exportdefaultfunctionGame() {
return (
<divclassName="game">
<Scene>
<Player />
<Map />
</Scene>
<Score />
<Controls />
<Result />
</div>
);
}
Resetting the Game
Once the player is hit by a vehicle, the result screen appears. Here, we have a button to restart the game. Let’s implement the logic behind it.
Resetting the Game
A result screen is shown when the player gets hit by a vehicle. There, if the player clicks the “Retry” button, we call the reset method of the game store. Let’s implement this method.
The reset method resets the game to running status, sets the score to 0, and calls the other two stores to reset themselves.
Let’s look into these two other stores next.
src/stores/game.ts
import { create } from"zustand";
import useMapStore from"./map";
import { reset as resetPlayerStore } from"./player";
To reset the map store, we expose a reset method that generates new rows and sets them as the current map. We use the same generateRows function that we used to initialize the rows in the first place.
Calling this method will reactively re-render the map.
You made it to the end of this tutorial. You have learned how to create a simple game using React Three Fiber and Three.js. The game is far from perfect. You can now extend this game by adding more features, obstacles, or a new level design.
To grow this site it would help a lot if you could share it with your friends or colleagues. So far this is a pilot project, but I plan to extend this site with more tutorials in the future. If you have any feedback or suggestions, please let me know and in the meanwhile follow me on YouTube and LinkedIn.