threeplay threeplay

Examples / third-person

Guard patrol

The guard is a small state machine (patrol, chase, rest) written with state(). Buttons are named actions mapped to keyboard, mouse and gamepad, so one handler serves every device.

third-person/main.js

Source

examples/third-person/main.js
// Third-person adventure: explore, kick crates, and avoid the guard.
// Shows thirdPerson(), state() machines, child objects and timers.
import threeplay from "threeplay";

threeplay({
  width: 960,
  height: 540,
  background: "#cdeac0",
  buttons: {
    left: { keyboard: ["left", "a"], gamepad: "dpad-left" },
    right: { keyboard: ["right", "d"], gamepad: "dpad-right" },
    up: { keyboard: ["up", "w"], gamepad: "dpad-up" },
    down: { keyboard: ["down", "s"], gamepad: "dpad-down" },
    jump: { keyboard: "space", gamepad: "south" },
    kick: { keyboard: ["e", "k"], mouse: "left", gamepad: "west" },
  },
});

const WAYPOINTS = [vec3(-8, 0, -8), vec3(8, 0, -8), vec3(8, 0, 8), vec3(-8, 0, 8)];

scene("world", () => {
  setGravity(25);
  add([box(40, 1, 40), pos(0, -0.5, 0), color("#95d5b2"), area(), body({ isStatic: true })]);
  for (let i = 0; i < 12; i++) {
    const p = vec3(rand(-15, 15), 0, rand(-15, 15));
    add([cylinder(0.3, 2), pos(p.x, 1, p.z), color("#7f5539"), area(), body({ isStatic: true })]);
    add([sphere(1.2), pos(p.x, 2.6, p.z), color("#2d6a4f")]);
  }
  for (let i = 0; i < 8; i++) {
    add([
      box(1, 1, 1),
      pos(rand(-6, 6), 0.5, rand(-6, 6)),
      color("#dda15e"),
      area(),
      body({ mass: 2 }),
      "crate",
    ]);
  }

  // The player: a capsule with a "nose" child that shows where it faces.
  const player = add([
    capsule(0.45, 1.7),
    pos(0, 1, 4),
    rotate(),
    color("#277da1"),
    area(),
    character(),
    "player",
  ]);
  player.add([box(0.25, 0.25, 0.4), pos(0, 0.45, -0.45), color("#f9c74f")]);
  thirdPerson(player, { distance: 7, height: 1.4 });

  // The guard patrols between waypoints and chases the player when close.
  const guard = add([
    box(1, 1.8, 1),
    pos(-8, 0.9, -8),
    rotate(),
    color("#d62828"),
    area(),
    character(),
    state("patrol", ["patrol", "chase", "rest"]),
    "guard",
  ]);
  let target = 1;
  guard.onStateUpdate("patrol", () => {
    const wp = WAYPOINTS[target];
    guard.moveTo(vec3(wp.x, guard.pos.y, wp.z), 3);
    guard.lookAt(vec3(wp.x, guard.pos.y, wp.z));
    if (guard.distanceTo(vec3(wp.x, guard.pos.y, wp.z)) < 0.5)
      target = (target + 1) % WAYPOINTS.length;
    if (guard.distanceTo(player) < 6) guard.enterState("chase");
  });
  guard.onStateUpdate("chase", () => {
    guard.moveTo(player, 4.5);
    guard.lookAt(vec3(player.pos.x, guard.pos.y, player.pos.z));
    if (guard.distanceTo(player) > 10) guard.enterState("patrol");
  });
  guard.onStateEnter("rest", () => wait(2, () => guard.enterState("patrol")));

  let caught = 0;
  const hud = add([text("", { size: 18 }), pos(20, 16), fixed()]);
  add([
    text("WASD move · space jump · E / click kick crates · drag to look", { size: 14 }),
    pos(20, 510),
    fixed(),
  ]);
  const label = guard.add([text("!", { size: 30 }), pos(0, 1.5, 0)]); // floats over the guard
  onUpdate(() => {
    hud.text = `Caught ${caught} times · guard: ${guard.state}`;
    label.hidden = guard.state !== "chase";
    const dir = inputDir();
    player.move(dir.clone().multiplyScalar(6));
    if (dir.lengthSq() > 0) player.lookAt(player.worldPos().add(dir));
  });
  onButtonPress("jump", () => player.isGrounded() && player.jump(9));
  onButtonPress("kick", () => {
    for (const crate of get("crate")) {
      if (crate.distanceTo(player) < 2.2) {
        crate.applyImpulse(
          player
            .forward()
            .multiplyScalar(14)
            .add(vec3(0, 6, 0)),
        );
      }
    }
  });
  player.onCollide("guard", () => {
    if (guard.state !== "chase") return;
    caught++;
    camShake(0.4, 0.4);
    player.teleport(vec3(0, 1, 4));
    guard.enterState("rest");
  });
});

go("world");