In this chapter, we will render the game map. The map consists of multiple rows, each described by metadata. Each row can be a forest, car, or truck lane. We go through each type and define the 3D objects that represent them.
In this chapter, we will render the game map. The map consists of multiple rows, each described by metadata. Each row can be a forest, car, or truck lane. We go through each type and define the 3D objects that represent them.
The game map consists of multiple rows, each representing a distinct environment or obstacle.
The forest
is a row of grass and trees.
The car
lane contains cars.
The truck
lane contains trucks.
Let’s define the types for the rows in types.ts
. The RowType
type is a union of the different row types. The Row
type is a union of the different row objects. We will go through each row type in detail as we implement it.
import * as THREE from "three";
export type RowType = "forest" | "car" | "truck";
export type Row = | { type: "forest"; trees: { tileIndex: number; height: number }[]; } | { type: "car"; direction: boolean; speed: number; vehicles: { initialTileIndex: number; color: THREE.ColorRepresentation; ref?: THREE.Object3D; }[]; } | { type: "truck"; direction: boolean; speed: number; vehicles: { initialTileIndex: number; color: THREE.ColorRepresentation; ref?: THREE.Object3D; }[]; };
export {};
The game map consists of multiple rows, each representing a distinct environment or obstacle.
The forest
is a row of grass and trees.
The car
lane contains cars.
The truck
lane contains trucks.
Let’s define the types for the rows in types.ts
. The RowType
type is a union of the different row types. The Row
type is a union of the different row objects. We will go through each row type in detail as we implement it.
import * as THREE from "three";
export type RowType = "forest" | "car" | "truck";
export type Row = | { type: "forest"; trees: { tileIndex: number; height: number }[]; } | { type: "car"; direction: boolean; speed: number; vehicles: { initialTileIndex: number; color: THREE.ColorRepresentation; ref?: THREE.Object3D; }[]; } | { type: "truck"; direction: boolean; speed: number; vehicles: { initialTileIndex: number; color: THREE.ColorRepresentation; ref?: THREE.Object3D; }[]; };
export {};
Each row consists of multiple tiles. The player moves from tile to tile. Trees are also placed on tiles. Cars and trucks do not relate to tiles; they move freely through the lane.
We define in a constants.js
constants.ts
file how many tiles each row has. In this case, we have 17 tiles per row, going from -8 to 8. The player will start in the middle at tile 0.
export const minTileIndex = -8;export const maxTileIndex = 8;export const tilesPerRow = maxTileIndex - minTileIndex + 1;export const tileSize = 42;
export const minTileIndex = -8;export const maxTileIndex = 8;export const tilesPerRow = maxTileIndex - minTileIndex + 1;export const tileSize = 42;
In this section, we define some of the components we use to render the map, and we render the initial row.
Let’s create a new component called Map
. This file will expose the map’s metadata, the 3D objects representing the map, and functions to extend and reset the map.
Let’s export a Group
called map
. This container will contain the 3D objects of each row. We will add this group to the scene later.
We also export the initializeMap
function, which sets the map’s content. Later, we will extend this function to generate the 3D objects based on the metadata and use it to reset the map.
For now, let’s call the Grass
function, which will return another Three.js group. We call the Grass
function with the row index. The Grass
component will position itself based on this index. We add the returned object to the map.
Now, let’s define the Grass
function.
import * as THREE from "three";import { Grass } from "./Grass";
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass);}
import * as THREE from "three";import { Grass } from "./Grass";
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass);}
The Grass
function returns the foundation and container of the forest
rows, and it is also used for the starting row. It returns a group that contains a flat, wide green box.
The dimensions of this box are determined by the constants tileSize
and tilesPerRow
. It also has a bit of height, so it sticks out compared to a road, which will be completely flat.
The Grass
can serve as the container for the trees in the row. That’s why we wrap the green box into a group so that we can add children later.
We position the group along the y-axis based on the rowIndex
. This way, each row will have a different position.
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Grass(rowIndex: number) { const grass = new THREE.Group(); grass.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.BoxGeometry(tilesPerRow * tileSize, tileSize, 3), new THREE.MeshLambertMaterial({ color: 0xbaf455 }) ); foundation.position.z = 1.5; grass.add(foundation);
return grass;}
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Grass(rowIndex) { const grass = new THREE.Group(); grass.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.BoxGeometry(tilesPerRow * tileSize, tileSize, 3), new THREE.MeshLambertMaterial({ color: 0xbaf455 }) ); foundation.position.z = 1.5; grass.add(foundation);
return grass;}
Let’s add the map to the scene in the main file. Since the map is initially an empty group, we also have to call the initializeMap
function to render its content.
Let’s also define the initializeGame
function, which will set the map’s content. Later, we will extend this function and use it also to reset the game. Make sure to call this function before we render the scene. Otherwise, an empty map will be rendered.
import * as THREE from "three";import { Renderer } from "./components/Renderer";import { Camera } from "./components/Camera";import { player } from "./components/Player";import { map, initializeMap } from "./components/Map";import "./style.css";
const scene = new THREE.Scene();scene.add(player);scene.add(map);
const ambientLight = new THREE.AmbientLight();scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight();dirLight.position.set(-100, -100, 200);scene.add(dirLight);
const camera = Camera();scene.add(camera);
initializeGame();
function initializeGame() { initializeMap();}
const renderer = Renderer();renderer.render(scene, camera);
import * as THREE from "three";import { Renderer } from "./components/Renderer";import { Camera } from "./components/Camera";import { player } from "./components/Player";import { map, initializeMap } from "./components/Map";import "./style.css";
const scene = new THREE.Scene();scene.add(player);scene.add(map);
const ambientLight = new THREE.AmbientLight();scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight();dirLight.position.set(-100, -100, 200);scene.add(dirLight);
const camera = Camera();scene.add(camera);
initializeGame();
function initializeGame() { initializeMap();}
const renderer = Renderer();renderer.render(scene, camera);
It’s time to extend our map with a forest row. We define metadata for the map and render the rows based on it.
Let’s define the metadata for the map. The metadata is an array of objects that contain information about each row. Each row object contains a type
property that decides if we have a forest row, a car, or a truck lane. The rest of the properties depend on the row type.
For now, let’s hardcode the values for one single row. Later, we will generate the rows dynamically.
The metadata for a forest row has the type forest
and a list of trees. Each tree has the following properties:
tileIndex
: The tile number of the tree within the row. In this case, we have 17 tiles per row, going from -8 to 8height
: The height of the crown in unitsimport type { Row } from "../types";import * as THREE from "three";import { Grass } from "./Grass";
export const metadata: Row[] = [ { type: "forest", trees: [ { tileIndex: -3, height: 50 }, { tileIndex: 2, height: 30 }, { tileIndex: 5, height: 50 }, ], },];
6 collapsed lines
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass);}
import * as THREE from "three";import { Grass } from "./Grass";
export const metadata = [ { type: "forest", trees: [ { tileIndex: -3, height: 50 }, { tileIndex: 2, height: 30 }, { tileIndex: 5, height: 50 }, ], },];
6 collapsed lines
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass);}
To render the rows, we call the addRows
function in the initializeMap
function. This function generates 3D objects based on the metadata and adds them to the map container.
The addRows
function loops over the metadata and creates a 3D object for each row based on the row type. For the forest type, it calls the Grass
function, which returns a Three.js group.
We call the Grass
function with the row index. The row index is off by one compared to the array index because the first item in the metadata array will become the second row (after the starting row).
Forest rows have trees. For each item in the trees
array, we render a tree. The Tree
function will return a 3D object representing the tree. We pass on to this function the tileIndex
that we will use to position the tree within the row and its height
. We add the trees to the group the Grass
function returns.
We add the row to the map. Since we already added the map to the scene, the forest will appear on the screen. But first, we need to define how to render a tree.
import type { Row } from "../types";import * as THREE from "three";import { Grass } from "./Grass";import { Tree } from "./Tree";
10 collapsed lines
export const metadata: Row[] = [ { type: "forest", trees: [ { tileIndex: -3, height: 50 }, { tileIndex: 2, height: 30 }, { tileIndex: 5, height: 50 }, ], },];
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
if (rowData.type === "forest") { const row = Grass(rowIndex);
rowData.trees.forEach(({ tileIndex, height }) => { const three = Tree(tileIndex, height); row.add(three); });
map.add(row); } });}
import * as THREE from "three";import { Grass } from "./Grass";import { Tree } from "./Tree";
10 collapsed lines
export const metadata = [ { type: "forest", trees: [ { tileIndex: -3, height: 50 }, { tileIndex: 2, height: 30 }, { tileIndex: 5, height: 50 }, ], },];
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
if (rowData.type === "forest") { const row = Grass(rowIndex);
rowData.trees.forEach(({ tileIndex, height }) => { const three = Tree(tileIndex, height); row.add(three); });
map.add(row); } });}
A tree consists of a trunk and a crown. Both are simple boxes. The trunk is placed on top of the ground (lifted along the z-axis by half its height), and the crown is placed on top of the trunk. The crown’s height is based on the height
property.
The two meshes are wrapped together in a group
positioned along the x-axis based on the tileIndex
attribute.
Grab the tree to see it from different angles.
import * as THREE from "three";import { tileSize } from "../constants";
export function Tree(tileIndex: number, height: number) { const tree = new THREE.Group(); tree.position.x = tileIndex * tileSize;
const trunk = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: 0x4d2926, flatShading: true, }) ); trunk.position.z = 10; tree.add(trunk);
const crown = new THREE.Mesh( new THREE.BoxGeometry(30, 30, height), new THREE.MeshLambertMaterial({ color: 0x7aa21d, flatShading: true, }) ); crown.position.z = height / 2 + 20; tree.add(crown);
return tree;}
import * as THREE from "three";import { tileSize } from "../constants";
export function Tree(tileIndex, height) { const tree = new THREE.Group(); tree.position.x = tileIndex * tileSize;
const trunk = new THREE.Mesh( new THREE.BoxGeometry(15, 15, 20), new THREE.MeshLambertMaterial({ color: 0x4d2926, flatShading: true, }) ); trunk.position.z = 10; tree.add(trunk);
const crown = new THREE.Mesh( new THREE.BoxGeometry(30, 30, height), new THREE.MeshLambertMaterial({ color: 0x7aa21d, flatShading: true, }) ); crown.position.z = height / 2 + 20; tree.add(crown);
return tree;}
Adding car lanes follows a similar structure to the forest. We define metadata for the vehicles and then map them to 3D objects.
Let’s replace the forest row with a car
lane. The car lane will contain a single red car moving to the left.
Each car lane object will have the following properties:
direction
: A boolean that sets the direction of the vehicles. true
means the cars move to the right, false
means the vehicles move to the leftspeed
: The speed of the vehicles in units per secondvehicles
: An array of objects that contain the metadata for each vehicle on the lane.Each vehicle will have the following properties:
initialTileIndex
: The tile number of the vehicle’s initial position within the lane. We have 17 tiles per lane, going from -8 to 8.color
: The color of the vehicle in hexadecimal format.import type { Row } from "../types";import * as THREE from "three";import { Grass } from "./Grass";import { Tree } from "./Tree";
export const metadata: Row[] = [ { type: "car", direction: false, speed: 1, vehicles: [{ initialTileIndex: 2, color: 0xff0000 }], },];
24 collapsed lines
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
if (rowData.type === "forest") { const row = Grass(rowIndex);
rowData.trees.forEach(({ tileIndex, height }) => { const three = Tree(tileIndex, height); row.add(three); });
map.add(row); } });}
import * as THREE from "three";import { Grass } from "./Grass";import { Tree } from "./Tree";
export const metadata = [ { type: "car", direction: false, speed: 1, vehicles: [{ initialTileIndex: 2, color: 0xff0000 }], },];
24 collapsed lines
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
if (rowData.type === "forest") { const row = Grass(rowIndex);
rowData.trees.forEach(({ tileIndex, height }) => { const three = Tree(tileIndex, height); row.add(three); });
map.add(row); } });}
Let’s extend the addRows
function to support car
lanes. We add another if block that is very similar to rendering a forest.
In the case of the car
type, we call the Road
function, which will also return a Three.js group. We also call this function with the row index to position the group.
Then, for each item in the vehicles
array, we create a 3D object representing a car with the Car
function. We add the cars to the group returned by the Road
function.
To the Car
function, we pass the initialTileIndex
that we will use to position the car within the row, its direction,
and its color.
The Road
and Car
functions are new here. Let’s take a look at them next.
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";
16 collapsed lines
export const metadata: Row[] = [ { type: "car", direction: false, speed: 1, vehicles: [{ initialTileIndex: 2, color: 0xff0000 }], },];
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); row.add(car); });
map.add(row); } });}
import * as THREE from "three";import { Grass } from "./Grass";import { Road } from "./Road";import { Tree } from "./Tree";import { Car } from "./Car";
16 collapsed lines
export const metadata = [ { type: "car", direction: false, speed: 1, vehicles: [{ initialTileIndex: 2, color: 0xff0000 }], },];
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); row.add(car); });
map.add(row); } });}
The Road
function returns the foundation and container of the car
and truck
lanes. It returns a group that contains a gray plane.
Similar to the Grass
function, the size of the plane is determined by the constants tileSize
and tilesPerRow
. Unlike the Grass
function, though, the plane doesn’t have any height.
The Road
will serve as the container for the cars and trucks in the row. That’s why we wrap the plane into a group so that we can add children later.
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Road(rowIndex: number) { const road = new THREE.Group(); road.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.PlaneGeometry(tilesPerRow * tileSize, tileSize), new THREE.MeshLambertMaterial({ color: 0x454a59 }) ); road.add(foundation);
return road;}
import * as THREE from "three";import { tilesPerRow, tileSize } from "../constants";
export function Road(rowIndex) { const road = new THREE.Group(); road.position.y = rowIndex * tileSize;
const foundation = new THREE.Mesh( new THREE.PlaneGeometry(tilesPerRow * tileSize, tileSize), new THREE.MeshLambertMaterial({ color: 0x454a59 }) ); road.add(foundation);
return road;}
The Car
function returns a simplified 3D model of a car. It consists of a box for the body, a smaller box for the top, and two Wheel
meshes for the wheels.
We group all these elements together, position them based on the initialTileIndex
property, and turn them based on the direction
property. If the car goes to the left, we rotate it 180 degrees, which is equivalent to Math.PI
in radians.
Grab the car to see it from different angles.
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Car( initialTileIndex: number, direction: boolean, color: THREE.ColorRepresentation) { const car = new THREE.Group(); car.position.x = initialTileIndex * tileSize; if (!direction) car.rotation.z = Math.PI;
const main = new THREE.Mesh( new THREE.BoxGeometry(60, 30, 15), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); main.position.z = 12; car.add(main);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(33, 24, 12), new THREE.MeshLambertMaterial({ color: "white", flatShading: true, }) ); cabin.position.x = -6; cabin.position.z = 25.5; car.add(cabin);
const frontWheel = Wheel(18); car.add(frontWheel);
const backWheel = Wheel(-18); car.add(backWheel);
return car;}
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Car(initialTileIndex, direction, color) { const car = new THREE.Group(); car.position.x = initialTileIndex * tileSize; if (!direction) car.rotation.z = Math.PI;
const main = new THREE.Mesh( new THREE.BoxGeometry(60, 30, 15), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); main.position.z = 12; car.add(main);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(33, 24, 12), new THREE.MeshLambertMaterial({ color: "white", flatShading: true, }) ); cabin.position.x = -6; cabin.position.z = 25.5; car.add(cabin);
const frontWheel = Wheel(18); car.add(frontWheel);
const backWheel = Wheel(-18); car.add(backWheel);
return car;}
To continue with our boxy theme, the Wheel
function returns a simple box with a dark color. Because we never see the cars from under, we don’t need to separate the wheels into left and right. We can use one long box for the front wheels and another for the back wheels.
import * as THREE from "three";
export function Wheel(x: number) { const wheel = new THREE.Mesh( new THREE.BoxGeometry(12, 33, 12), new THREE.MeshLambertMaterial({ color: 0x333333, flatShading: true, }) ); wheel.position.x = x; wheel.position.z = 6; return wheel;}
import * as THREE from "three";
export function Wheel(x) { const wheel = new THREE.Mesh( new THREE.BoxGeometry(12, 33, 12), new THREE.MeshLambertMaterial({ color: 0x333333, flatShading: true, }) ); wheel.position.x = x; wheel.position.z = 6; return wheel;}
Truck lanes are almost the same as car lanes. The metadata and the components follow the same structure. Except this time, we render trucks instead of cars.
Let’s replace our car lane with a truck lane. The metadata structure is the same. We define the direction
and speed
properties and set a vehicles
array. The only difference is that the type of the row is truck
this time.
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";
export const metadata: Row[] = [ { type: "truck", direction: true, speed: 0, vehicles: [{ initialTileIndex: -4, color: 0x00ff00 }], },];
39 collapsed lines
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); row.add(car); });
map.add(row); } });}
import * as THREE from "three";import { Grass } from "./Grass";import { Road } from "./Road";import { Tree } from "./Tree";import { Car } from "./Car";
export const metadata = [ { type: "truck", direction: true, speed: 0, vehicles: [{ initialTileIndex: -4, color: 0x00ff00 }], },];
39 collapsed lines
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); row.add(car); });
map.add(row); } });}
Let’s extend our addRows
function again to support truck
lanes. We add another if block that is almost the same as the one rendering a car lane.
We use the same Road
function again as a container and foundation, and this time, we map the vehicles to the result of the Truck
function.
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";
16 collapsed lines
export const metadata: Row[] = [ { type: "truck", direction: true, speed: 0, vehicles: [{ initialTileIndex: -4, color: 0x00ff00 }], },];
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); 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 ); row.add(truck); });
map.add(row); } });}
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";
16 collapsed lines
export const metadata = [ { type: "truck", direction: true, speed: 0, vehicles: [{ initialTileIndex: -4, color: 0x00ff00 }], },];
export const map = new THREE.Group();
export function initializeMap() { const grass = Grass(0); map.add(grass); addRows();}
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); 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 ); row.add(truck); });
map.add(row); } });}
A truck is also built up from simple boxes. We have a long gray box for the cargo and a smaller box for the cabin. The cabin’s color depends on the color
property.
We position the truck on the lane using the initialTilePosition
property and turn it in the right direction using the direction
property.
We use the same Wheel
component as the car did. This time we use three of them.
Grab the truck to see it from different angles.
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Truck( initialTileIndex: number, direction: boolean, color: THREE.ColorRepresentation) { const truck = new THREE.Group(); truck.position.x = initialTileIndex * tileSize; if (!direction) truck.rotation.z = Math.PI;
const cargo = new THREE.Mesh( new THREE.BoxGeometry(70, 35, 35), new THREE.MeshLambertMaterial({ color: 0xb4c6fc, flatShading: true, }) ); cargo.position.x = -15; cargo.position.z = 25; truck.add(cargo);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(30, 30, 30), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); cabin.position.x = 35; cabin.position.z = 20; truck.add(cabin);
const frontWheel = Wheel(37); truck.add(frontWheel);
const middleWheel = Wheel(5); truck.add(middleWheel);
const backWheel = Wheel(-35); truck.add(backWheel);
return truck;}
import * as THREE from "three";import { tileSize } from "../constants";import { Wheel } from "./Wheel";
export function Truck(initialTileIndex, direction, color) { const truck = new THREE.Group(); truck.position.x = initialTileIndex * tileSize; if (!direction) truck.rotation.z = Math.PI;
const cargo = new THREE.Mesh( new THREE.BoxGeometry(70, 35, 35), new THREE.MeshLambertMaterial({ color: 0xb4c6fc, flatShading: true, }) ); cargo.position.x = -15; cargo.position.z = 25; truck.add(cargo);
const cabin = new THREE.Mesh( new THREE.BoxGeometry(30, 30, 30), new THREE.MeshLambertMaterial({ color, flatShading: true }) ); cabin.position.x = 35; cabin.position.z = 20; truck.add(cabin);
const frontWheel = Wheel(37); truck.add(frontWheel);
const middleWheel = Wheel(5); truck.add(middleWheel);
const backWheel = Wheel(-35); truck.add(backWheel);
return truck;}
Now, we can render a map with several rows based on metadata. For aesthetic reasons, let’s add a couple empty rows before the player.
Let’s add a few more empty grass objects to the map before the player’s position. In the initializeMap
function, instead of rendering one Grass
object for the starting row, we render five of them. Each has its own negative row index.
47 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();}
46 collapsed lines
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); 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 ); row.add(truck); });
map.add(row); } });}
46 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();}
46 collapsed lines
export function addRows() { metadata.forEach((rowData, index) => { const rowIndex = index + 1;
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 ); 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 ); row.add(truck); });
map.add(row); } });}
The metadata for the next few chapters and the demo above is as follows.
7 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 }, ], },];
56 collapsed lines
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;
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 ); 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 ); row.add(truck); });
map.add(row); } });}
6 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 }, ], },];
56 collapsed lines
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;
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 ); 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 ); row.add(truck); });
map.add(row); } });}