Animating the Vehicles

Crossy Road with Three.js ● Chapter 4

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

Collecting References to the Vehicles

To move the vehicles, we first need to be able to access them. So far, we have added them to the scene, and theoretically, we could traverse the scene and figure out which object represents a vehicle. But it’s much easier to collect their references in our metadata and access them through these references.

Let’s modify the addRows function. After generating a car or truck, we not only add them to the container group but also save their reference together with their metadata.

src/components/Map.ts
55 collapsed lines
import type { Row } from "../types";
import * as THREE from "three";
import { Grass } from "./Grass";
import { Road } from "./Road";
import { Tree } from "./Tree";
import { Car } from "./Car";
import { Truck } from "./Truck";
export const metadata: Row[] = [
{
type: "car",
direction: false,
speed: 188,
vehicles: [
{ initialTileIndex: -4, color: 0xbdb638 },
{ initialTileIndex: -1, color: 0x78b14b },
{ initialTileIndex: 4, color: 0xa52523 },
],
},
{
type: "forest",
trees: [
{ tileIndex: -5, height: 50 },
{ tileIndex: 0, height: 30 },
{ tileIndex: 3, height: 50 },
],
},
{
type: "truck",
direction: true,
speed: 125,
vehicles: [
{ initialTileIndex: -4, color: 0x78b14b },
{ initialTileIndex: 0, color: 0xbdb638 },
],
},
{
type: "forest",
trees: [
{ tileIndex: -8, height: 30 },
{ tileIndex: -3, height: 50 },
{ tileIndex: 2, height: 30 },
],
},
];
export const map = new THREE.Group();
export function initializeMap() {
for (let rowIndex = 0; rowIndex > -5; rowIndex--) {
const grass = Grass(rowIndex);
map.add(grass);
}
addRows();
}
export function addRows() {
metadata.forEach((rowData, index) => {
const rowIndex = index + 1;
10 collapsed lines
if (rowData.type === "forest") {
const row = Grass(rowIndex);
rowData.trees.forEach(({ tileIndex, height }) => {
const three = Tree(tileIndex, height);
row.add(three);
});
map.add(row);
}
if (rowData.type === "car") {
const row = Road(rowIndex);
rowData.vehicles.forEach((vehicle) => {
const car = Car(
vehicle.initialTileIndex,
rowData.direction,
vehicle.color
);
vehicle.ref = car;
row.add(car);
});
map.add(row);
}
if (rowData.type === "truck") {
const row = Road(rowIndex);
rowData.vehicles.forEach((vehicle) => {
const truck = Truck(
vehicle.initialTileIndex,
rowData.direction,
vehicle.color
);
vehicle.ref = truck;
row.add(truck);
});
map.add(row);
}
});
}
src/components/Map.js
54 collapsed lines
import * as THREE from "three";
import { Grass } from "./Grass";
import { Road } from "./Road";
import { Tree } from "./Tree";
import { Car } from "./Car";
import { Truck } from "./Truck";
export const metadata = [
{
type: "car",
direction: false,
speed: 188,
vehicles: [
{ initialTileIndex: -4, color: 0xbdb638 },
{ initialTileIndex: -1, color: 0x78b14b },
{ initialTileIndex: 4, color: 0xa52523 },
],
},
{
type: "forest",
trees: [
{ tileIndex: -5, height: 50 },
{ tileIndex: 0, height: 30 },
{ tileIndex: 3, height: 50 },
],
},
{
type: "truck",
direction: true,
speed: 125,
vehicles: [
{ initialTileIndex: -4, color: 0x78b14b },
{ initialTileIndex: 0, color: 0xbdb638 },
],
},
{
type: "forest",
trees: [
{ tileIndex: -8, height: 30 },
{ tileIndex: -3, height: 50 },
{ tileIndex: 2, height: 30 },
],
},
];
export const map = new THREE.Group();
export function initializeMap() {
for (let rowIndex = 0; rowIndex > -5; rowIndex--) {
const grass = Grass(rowIndex);
map.add(grass);
}
addRows();
}
export function addRows() {
metadata.forEach((rowData, index) => {
const rowIndex = index + 1;
10 collapsed lines
if (rowData.type === "forest") {
const row = Grass(rowIndex);
rowData.trees.forEach(({ tileIndex, height }) => {
const three = Tree(tileIndex, height);
row.add(three);
});
map.add(row);
}
if (rowData.type === "car") {
const row = Road(rowIndex);
rowData.vehicles.forEach((vehicle) => {
const car = Car(
vehicle.initialTileIndex,
rowData.direction,
vehicle.color
);
vehicle.ref = car;
row.add(car);
});
map.add(row);
}
if (rowData.type === "truck") {
const row = Road(rowIndex);
rowData.vehicles.forEach((vehicle) => {
const truck = Truck(
vehicle.initialTileIndex,
rowData.direction,
vehicle.color
);
vehicle.ref = truck;
row.add(truck);
});
map.add(row);
}
});
}

Define an Animation Cycle

Next, let’s define an animate function that will be called on every animation frame. For now, we only call the animateVehicles function in it, which we are about to define. Later, we extend this function with logic to animate the player, and hit detection.

We also move the renderer’s render call here to re-render the scene on every animation loop. To call this function on every frame, we pass it on to the renderer’s setAnimationLoop function.

src/main.ts
import * as THREE from "three";
import { Renderer } from "./components/Renderer";
import { Camera } from "./components/Camera";
import { DirectionalLight } from "./components/DirectionalLight";
import { player } from "./components/Player";
import { map, initializeMap } from "./components/Map";
import { animateVehicles } from "./animateVehicles";
import "./style.css";
18 collapsed lines
const scene = new THREE.Scene();
scene.add(player);
scene.add(map);
const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);
const dirLight = DirectionalLight();
scene.add(dirLight);
const camera = Camera();
scene.add(camera);
initializeGame();
function initializeGame() {
initializeMap();
}
const renderer = Renderer();
renderer.render(scene, camera);
renderer.setAnimationLoop(animate);
function animate() {
animateVehicles();
renderer.render(scene, camera);
}
src/main.js
import * as THREE from "three";
import { Renderer } from "./components/Renderer";
import { Camera } from "./components/Camera";
import { DirectionalLight } from "./components/DirectionalLight";
import { player } from "./components/Player";
import { map, initializeMap } from "./components/Map";
import { animateVehicles } from "./animateVehicles";
import "./style.css";
18 collapsed lines
const scene = new THREE.Scene();
scene.add(player);
scene.add(map);
const ambientLight = new THREE.AmbientLight();
scene.add(ambientLight);
const dirLight = DirectionalLight();
scene.add(dirLight);
const camera = Camera();
scene.add(camera);
initializeGame();
function initializeGame() {
initializeMap();
}
const renderer = Renderer();
renderer.render(scene, camera);
renderer.setAnimationLoop(animate);
function animate() {
animateVehicles();
renderer.render(scene, camera);
}

Animating the Vehicles

Let’s implement the animateVehicles function to animate the vehicles. It moves them based on their speed and direction until the end of the lane and then respawns them at the other end.

This is part of the animation function, which is called on every animation frame. We use a Three.js clock to calculate how much time passed between the animation frames.

Then, we loop over the metadata, take every vehicle from every car and truck lane, and move them along the x-axis based on their speed, direction, and the time passed.

If the vehicle reaches the end of the lane, we respawn it at the other end. This way, we have an infinite loop in which cars go from left to right or right to left, depending on their direction, and once they reach the end of a lane, they start over from the beginning.

src/animateVehicles.ts
import * as THREE from "three";
import { metadata as rows } from "./components/Map";
import { minTileIndex, maxTileIndex, tileSize } from "./constants";
const clock = new THREE.Clock();
export function animateVehicles() {
const delta = clock.getDelta();
// Animate cars and trucks
rows.forEach((rowData) => {
if (rowData.type === "car" || rowData.type === "truck") {
const beginningOfRow = (minTileIndex - 2) * tileSize;
const endOfRow = (maxTileIndex + 2) * tileSize;
rowData.vehicles.forEach(({ ref }) => {
if (!ref) throw Error("Vehicle reference is missing");
if (rowData.direction) {
ref.position.x =
ref.position.x > endOfRow
? beginningOfRow
: ref.position.x + rowData.speed * delta;
} else {
ref.position.x =
ref.position.x < beginningOfRow
? endOfRow
: ref.position.x - rowData.speed * delta;
}
});
}
});
}
src/animateVehicles.js
import * as THREE from "three";
import { metadata as rows } from "./components/Map";
import { minTileIndex, maxTileIndex, tileSize } from "./constants";
const clock = new THREE.Clock();
export function animateVehicles() {
const delta = clock.getDelta();
// Animate cars and trucks
rows.forEach((rowData) => {
if (rowData.type === "car" || rowData.type === "truck") {
const beginningOfRow = (minTileIndex - 2) * tileSize;
const endOfRow = (maxTileIndex + 2) * tileSize;
rowData.vehicles.forEach(({ ref }) => {
if (!ref) throw Error("Vehicle reference is missing");
if (rowData.direction) {
ref.position.x =
ref.position.x > endOfRow
? beginningOfRow
: ref.position.x + rowData.speed * delta;
} else {
ref.position.x =
ref.position.x < beginningOfRow
? endOfRow
: ref.position.x - rowData.speed * delta;
}
});
}
});
}
Previous:
Adding Shadows
Next:
Moving the Player