Animating the Vehicles

Crossy Road with React Three Fiber ● Chapter 4

In this chapter, we will animate the cars and trucks in their lanes according to their speed and direction.

Animating in a Performant way

This is where things start to diverge from how you would typically use React. The React way would be to update a state or a prop and let React re-render the whole component. This is fast when working with HTML elements, but it is not very effective when working with 3D objects. We want to avoid re-rendering the whole scene and, instead, update the position of the underlying objects directly.

We only use React to set up the scene and the objects, and then we let Three.js do the heavy lifting. React Three Fiber is just a thin layer on top of Three.js, so we can access the underlying Three.js objects directly to update the position of the cars and trucks.

Calling a Hook with a Reference to the Vehicle

We are going to use a custom hook useVehicleAnimation to animate the vehicles. This hook will need a reference to the 3D object it should manipulate. Before defining this hook, let’s get a reference to the Three.js group, which represents the car. We use React’s useRef hook to store the reference and bind it to the group element.

Then, we pass on this reference to the useVehicleAnimation hook, along with the direction and speed of the car.

src/components/Car.tsx
import * as THREE from "three";
import { useRef } from "react";
import { tileSize } from "../constants";
import { Wheel } from "./Wheel";
import useVehicleAnimation from "../hooks/useVehicleAnimation";
7 collapsed lines
type Props = {
rowIndex: number;
initialTileIndex: number;
direction: boolean;
speed: number;
color: THREE.ColorRepresentation;
};
export function Car({
rowIndex,
initialTileIndex,
direction,
speed,
color,
}: Props) {
const car = useRef<THREE.Group>(null);
useVehicleAnimation(car, direction, speed);
return (
<group
position-x={initialTileIndex * tileSize}
rotation-z={direction ? 0 : Math.PI}
ref={car}
>
10 collapsed lines
<mesh position={[0, 0, 12]} castShadow receiveShadow>
<boxGeometry args={[60, 30, 15]} />
<meshLambertMaterial color={color} flatShading />
</mesh>
<mesh position={[-6, 0, 25.5]} castShadow receiveShadow>
<boxGeometry args={[33, 24, 12]} />
<meshLambertMaterial color={0xffffff} flatShading />
</mesh>
<Wheel x={-18} />
<Wheel x={18} />
</group>
);
}
src/components/Car.jsx
import { useRef } from "react";
import { tileSize } from "../constants";
import { Wheel } from "./Wheel";
import useVehicleAnimation from "../hooks/useVehicleAnimation";
export function Car({
rowIndex,
initialTileIndex,
direction,
speed,
color,
}) {
const car = useRef(null);
useVehicleAnimation(car, direction, speed);
return (
<group
position-x={initialTileIndex * tileSize}
rotation-z={direction ? 0 : Math.PI}
ref={car}
>
10 collapsed lines
<mesh position={[0, 0, 12]} castShadow receiveShadow>
<boxGeometry args={[60, 30, 15]} />
<meshLambertMaterial color={color} flatShading />
</mesh>
<mesh position={[-6, 0, 25.5]} castShadow receiveShadow>
<boxGeometry args={[33, 24, 12]} />
<meshLambertMaterial color={0xffffff} flatShading />
</mesh>
<Wheel x={-18} />
<Wheel x={18} />
</group>
);
}

We do the same in the Truck component. We attach a reference to the group element and use the same hook.

src/components/Truck.tsx
import * as THREE from "three";
import { useRef } from "react";
import { tileSize } from "../constants";
import { Wheel } from "./Wheel";
import useVehicleAnimation from "../hooks/useVehicleAnimation";
7 collapsed lines
type Props = {
rowIndex: number;
initialTileIndex: number;
direction: boolean;
speed: number;
color: THREE.ColorRepresentation;
};
export function Truck({
rowIndex,
initialTileIndex,
direction,
speed,
color,
}: Props) {
const truck = useRef<THREE.Group>(null);
useVehicleAnimation(truck, direction, speed);
return (
<group
position-x={initialTileIndex * tileSize}
rotation-z={direction ? 0 : Math.PI}
ref={truck}
>
11 collapsed lines
<mesh position={[-15, 0, 25]} castShadow receiveShadow>
<boxGeometry args={[70, 35, 35]} />
<meshLambertMaterial color={0xb4c6fc} flatShading />
</mesh>
<mesh position={[35, 0, 20]} castShadow receiveShadow>
<boxGeometry args={[30, 30, 30]} />
<meshLambertMaterial color={color} flatShading />
</mesh>
<Wheel x={-35} />
<Wheel x={5} />
<Wheel x={37} />
</group>
);
}
src/components/Truck.jsx
import { useRef } from "react";
import { tileSize } from "../constants";
import { Wheel } from "./Wheel";
import useVehicleAnimation from "../hooks/useVehicleAnimation";
export function Truck({
rowIndex,
initialTileIndex,
direction,
speed,
color,
}) {
const truck = useRef(null);
useVehicleAnimation(truck, direction, speed);
return (
<group
position-x={initialTileIndex * tileSize}
rotation-z={direction ? 0 : Math.PI}
ref={truck}
>
11 collapsed lines
<mesh position={[-15, 0, 25]} castShadow receiveShadow>
<boxGeometry args={[70, 35, 35]} />
<meshLambertMaterial color={0xb4c6fc} flatShading />
</mesh>
<mesh position={[35, 0, 20]} castShadow receiveShadow>
<boxGeometry args={[30, 30, 30]} />
<meshLambertMaterial color={color} flatShading />
</mesh>
<Wheel x={-35} />
<Wheel x={5} />
<Wheel x={37} />
</group>
);
}

Animating the Vehicles

Let’s implement the useVehicleAnimation hook to animate the vehicles. It moves them based on their speed and direction until the end of the lane and then re-spawns them at the other end. This way, the vehicles move in an infinite loop.

This hook uses the useFrame hook that React Three Fiber provides. This hook is similar to setAnimationLoop in Three.js. It runs a function on every animation frame.

Conveniently, this function receives the time delta—the time that passed since the previous animation frame. We multiply this value by the vehicle’s speed to get the distance the car took during this time.

We directly update the position.x property of the underlying Three.js group. If the vehicle reaches the end of the lane, we re-spawn it at the other end.

Note that the reference passed to the hook might be null because it is only set after the first render. If the reference is not set, we return early from the function. Then, the animation starts in the next frame.

src/hooks/useVehicleAnimation.ts
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { tileSize, minTileIndex, maxTileIndex } from "../constants";
export default function useVehicleAnimation(
ref: React.RefObject<THREE.Group | null>,
direction: boolean,
speed: number
) {
useFrame((state, delta) => {
if (!ref.current) return;
const vehicle = ref.current;
const beginningOfRow = (minTileIndex - 2) * tileSize;
const endOfRow = (maxTileIndex + 2) * tileSize;
if (direction) {
vehicle.position.x =
vehicle.position.x > endOfRow
? beginningOfRow
: vehicle.position.x + speed * delta;
} else {
vehicle.position.x =
vehicle.position.x < beginningOfRow
? endOfRow
: vehicle.position.x - speed * delta;
}
});
}
src/hooks/useVehicleAnimation.js
import { useFrame } from "@react-three/fiber";
import { tileSize, minTileIndex, maxTileIndex } from "../constants";
export default function useVehicleAnimation(ref, direction, speed) {
useFrame((state, delta) => {
if (!ref.current) return;
const vehicle = ref.current;
const beginningOfRow = (minTileIndex - 2) * tileSize;
const endOfRow = (maxTileIndex + 2) * tileSize;
if (direction) {
vehicle.position.x =
vehicle.position.x > endOfRow
? beginningOfRow
: vehicle.position.x + speed * delta;
} else {
vehicle.position.x =
vehicle.position.x < beginningOfRow
? endOfRow
: vehicle.position.x - speed * delta;
}
});
}
Previous:
Adding Shadows
Next:
Moving the Player