A tiny platform game

A character on a meadow, a few stacks of crates and fifteen coins, some of them only reachable by jumping. Collect them all and the game shows your time. This tutorial covers the character controller, standing on objects, pickups and a score drawn in plain HTML over the canvas.

Click the scene. Move with WASD or the arrow keys, hold Shift to run, Space to jump, drag to turn the camera. On a phone, drag to walk and tap to jump.

The whole program

import { createApp, createCharacter, createGround, createCoin, createCrate, createTree, CharacterController, spin, bob, createRandom } from "wgpu3d";

const app = await createApp({ canvas: document.querySelector("#coin-demo"), environment: "noon" });
const scoreLabel = document.querySelector("#coin-score");
const random = createRandom(3);

app.add(createGround({ size: 90, tiles: 18, color: "#7cb35b", alternateColor: "#72a853" }));

const coinSpots = [];
for (let tower = 0; tower < 5; tower++) {
  const angle = (tower / 5) * Math.PI * 2;
  const x = Math.cos(angle) * 9, z = Math.sin(angle) * 9;
  const levels = tower % 3 + 1;
  for (let level = 0; level < levels; level++) {
    app.add(createCrate({ size: 1.4, position: [x + level * 1.4, 0, z], yaw: angle }));
    for (let stacked = 1; stacked <= level; stacked++) {
      app.add(createCrate({ size: 1.4, position: [x + level * 1.4, stacked * 1.4, z], yaw: angle }));
    }
  }
  coinSpots.push([x + (levels - 1) * 1.4, levels * 1.4 + 0.9, z]);
}
for (let index = 0; index < 10; index++) {
  const angle = random() * Math.PI * 2, distance = random.range(3, 16);
  coinSpots.push([Math.cos(angle) * distance, 0.9, Math.sin(angle) * distance]);
}
for (let index = 0; index < 18; index++) {
  const angle = random() * Math.PI * 2, distance = random.range(22, 40);
  app.add(createTree({ seed: index, height: random.range(4, 7), position: [Math.cos(angle) * distance, 0, Math.sin(angle) * distance] }));
}

const coins = coinSpots.map((position) => {
  const coin = createCoin({ position });
  coin.addBehavior(spin(2.5));
  coin.addBehavior(bob({ height: 0.15, speed: 3 }));
  return coin;
});
app.add(...coins);

const hero = createCharacter({ shirt: "#e8553d" });
hero.addBehavior(new CharacterController());
app.add(hero);
app.orbit({ subject: hero, distance: 10, pitch: 0.55, minDistance: 4, maxDistance: 20 });

let collected = 0;
let finishTime = null;
app.onUpdate(({ scene, timeSeconds }) => {
  for (const coin of scene.touching(hero, { tag: "coin", radius: 0.7 })) {
    coin.removeFromParent();
    collected += 1;
  }
  if (collected === coins.length && finishTime === null) finishTime = timeSeconds;
  scoreLabel.textContent = finishTime === null
    ? `Coins ${collected} / ${coins.length}`
    : `All ${coins.length} coins in ${finishTime.toFixed(1)} s`;
});
app.start();

Building the level

Five stacks of crates stand in a ring, each a little staircase one, two or three crates high, built with nested loops. A coin floats above the top of every stack, and ten more are scattered on the grass. Their positions are collected in coinSpots first and turned into coins afterwards, which keeps the level layout separate from what lives in it. Trees around the edge frame the play area.

Crates have box colliders, so the character cannot walk through them. More interesting is that the character can stand on them, which is what makes the stacks climbable.

The character controller

createCharacter builds a blocky figure whose arms and legs are separate child objects pivoting at the shoulders and hips. CharacterController is the behavior that plays it:

Pickups

Coins are prefabs tagged "coin" with a sphere collider that does not block movement. Two stacked behaviors, spin and bob, make them easy to spot. The game logic lives in app.onUpdate, which runs once a frame after every behavior:

The score itself is an ordinary <output> element positioned over the canvas. HTML is good at text, so there is no need to draw it in 3D.

A following orbit camera

app.orbit({ subject: hero }) orbits the moving character instead of a fixed point. You can drag to look around while you walk, and because movement follows the camera, turning the view is also how you steer. On touch screens one finger moves the character, so the camera keeps to its last angle.

Things to try