๐ฎ Game Development: 3D Breakout
Are you a fan of classic games like Breakout, but looking to add a modern twist?
Look no further than @react-three/fiber, a powerful library for creating 3D graphics in React. In this tutorial, weโll explore how to use @react-three/fiber to build a 3D version of Breakout. Letโs get started and bring Breakout into the world of 3D!
If you are not familiar with the game, you can play it online here or here.
Game design
- Render bricks, ball and paddle in 3D. All motion is in 2D.
- The ball starts over in a random direction if it misses the paddle.
- When the ball strikes sth, it should bounce off with reflected arrival angle.
- User can move the paddle to the left and right.
- When the ball strikes brick, it disappears.
- The game ends when all bricks disappear.
Basic scene
As the official documentation said, the Canvas component does some important setup work behind the scenes: It sets up a Scene and a Camera, the basic building blocks necessary for rendering; And it renders our scene every frame, you do not need a traditional render-loop.
Then add some lights to the scene by putting components into canvas like ambientLight that is directly equivalent to new THREE.AmbientLight()
.
1 | <Canvas shadows camera={{ position: [0, 0, -11], fov: 50 }}> |
Create bricks, ball and paddle
The mesh component is a basic scene object in three.js
that is directly equivalent to new THREE.Mesh()
, and it is used to hold the geometry and materials needed to represent a shape in 3D space. Then we can create a new mesh using a BoxGeometry
and a MeshStandardMaterial
which automatically attach to their parent.
To build the paddle, we use the boxGeometry
component to create a 3D box geometry, and it accepts arguments include the width, height and depth of the box. Same way to build the wall.
1 | function Paddle({ args = [WIDTH, HEIGHT, DEPTH], color, position }) { |
To build the ball, we use the sphereGeometry
component to create a sphere, and it accepts five arguments:
- radius: The radius of the sphere (default is 1).
- widthSegments: The number of segments along the width of sphere (default 8).
- heightSegments: The number of segments along the height of sphere (default 6).
- phiStart: The angle at which to start creating sphere (default 0).
- phiLength: The angle at which to end creating sphere (default Math.PI * 2).
1 | function Ball({ args = [RADIUS, 32, 32], color, position }) { |
Interaction
The user can move the paddle left and right.
Unlike traditional game rules, the position of the paddle will be manipulated using a change of mouse position instead of using keyboard keys. Keeps the mouse offset a multiple of the paddle position when the user changes the position of the 3D camera by calculating the distance from the 3D camera to the paddle. And limit the paddle position interval according to the position of the left and right walls.
1 | const ref = useRef(); |
3D collision detection
Using AABB
AABB stands for Axis-Aligned Bounding Box.
To test whether a sphere and an AABB (Axis-Aligned Bounding Box) are colliding, we can use the Separating Axis Theorem (SAT). This theorem states that if two convex shapes are not colliding, then there must exist an axis along which the two shapes are separated.
First check if the sphere is within the AABBโs boundaries. If it is, then the two shapes are colliding. If not, then we can use the SAT to test for a collision. To do this, we need to check if the sphere is separated from the AABB along any of the three axes (x, y, and z). If the sphere is not separated from the AABB along any of the axes, then the two shapes are colliding.
1 | function intersect(sphere, box) { |
Using Box3 and Sphere
We can use the Box3.intersectsSphere()
or Sphere.intersectsBox()
method and it returns a boolean value indicating whether the two objects are intersecting.
1 | const box = new THREE.Box3(new THREE.Vector3(), new THREE.Vector3()); |
Using Raycaster
Create a Raycaster
object and set its origin to the center of the sphere and its direction to the center of the box. Then create a Box3
object and set its min and max points to the boxโs corner points. Finally, call the Raycaster
โs intersectObject()
method passing in the Box3
object as an argument. If the method returns a non-null value, then the sphere and the box are colliding.
1 | const { scene } = useThree(); |
Possible problem: The main limitation of using Raycaster to test for collisions between a sphere and a box in three.js is that it is not very accurate. Raycaster can only detect collisions between two objects if the ray intersects with the surface of the object. This means that if the sphere is slightly offset from the box, the ray may not intersect with the surface of the sphere and the collision will not be detected. Additionally, Raycaster is not able to detect collisions between two objects that are moving, as the ray must be cast from a static point.
Animate the ball
Define speed as a constant and ballDirection as the direction of the ballโs motion on x and y. The useState hook is used to create a new Vector3 object with the initial position of (0, 0, 0). The useFrame hook is used to update the position of the Vector3 object.
1 | const [position, setPosition] = useState(new Vector3(0, 0, 0)); |
Randomness in bounces
When the ball hits a wall, brick or racket, it should bounce off at a reflected angle of arrival, like Specular as follows. But there is a limitation that the ball will always bounce off the walls and blocks at the same angle, no matter where it hits. The trajectory of the ball is fixed, and there will be positions that the ball cannot reach, making the game uninteresting.
1 | const { x, y } = ballDirection; |
For a better physics experience, randomness can be added to the calculation of the reflection angle of the ball. Here is an example:
1 | const getFlag = n => n > 0 ? 1 : -1; |
The ball should bounce off the walls by reversing its x-velocity. For example, if the ball is moving to the right, when it collides with the left wall, its x-velocity should be reversed and it should start moving to the left. The same applies for the right wall.
On this basis, add the randomness of the angle with a range limitation.
1 | if (["leftWall", "rightWall"].includes(intersectedObject)) { |
Disappearing bricks
The brick disappears when the ball hits it. The game ends when all the bricks disappear.
Remove the object from a scene by scene.remove()
. It takes a single argument, which is the object to be removed. It removes the object from the scene and all of its associated properties, such as its position, rotation, scale, and any other properties that were set on the object.
1 | const { scene } = useThree(); |
Track score
When the ball collides with bricks, update the score according to the number of bricks.
1 | const [score, setScore] = useState(0); |
Add music
On game events play a sound, e.g. on ball collisions. I selected free sound samples from here. We are going to add sound effects for: when the ball hitting the paddle, and when the ball hitting a brick.
Create a new Audio() object and set the source to the file that want to play, then call the play() method on the Audio() object to begin playing the sound.
For example:
1 | const sound = new Audio('sound.wav'); |
Possible problem: play() failed because the user didnโt interact with the document
. My solution was to add a start button.
3D view control
The OrbitControls
component provides a convenient way to interact with a 3D scene without having to manually update the camera position. With it we can control the 3D camera which allows for panning, zooming, and rotating around a target point in all directions by mouse and touch events.
1 | <OrbitControls autoRotateSpeed={0.85} zoomSpeed={0.75} minPolarAngle={1} maxPolarAngle={Math.PI} /> |
References:
๐ Check out my code in github.
๐ฎ If find any errors, please feel free to discuss and correct them: biqingsue@[google email].com.