Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions src/content/cube.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { ColliderDesc, RigidBodyDesc, World } from "@dimforge/rapier3d";
import {
ColliderDesc,
DynamicRayCastVehicleController,
RigidBodyDesc,
World,
} from "@dimforge/rapier3d";
import { HasModel } from "../systems/load-models";
import { Physical } from "../systems/physics";
import { PRESSED_CONTROLS } from "../systems/controls";
import { Stateful } from "../systems/state-machine";
import { Lifecycleable } from "../systems/lifecycle";
import { Engine, State } from "../lib/types";

const desc = ColliderDesc.cuboid(0.85, 0.85, 0.85);

const createRigidBody = (world: World) => {
return world.createRigidBody(
RigidBodyDesc.dynamic().setTranslation(0.0, 5.0, 0.0).setRotation({
RigidBodyDesc.dynamic().setTranslation(0.0, 1.0, 0.0).setRotation({
x: 0.5,
y: 1.0,
z: 1.5,
Expand All @@ -15,8 +24,62 @@ const createRigidBody = (world: World) => {
);
};

export const cube: Physical & HasModel = {
type CubeState = "idle" | "accelerate" | "accelerateRight" | "accelerateLeft";

const processCubeControls = (): CubeState => {
if (!PRESSED_CONTROLS.forward) {
return "idle";
}

if (PRESSED_CONTROLS.right) {
return "accelerateRight";
}

if (PRESSED_CONTROLS.left) {
return "accelerateLeft";
}

return "accelerate";
};

let controller: DynamicRayCastVehicleController | undefined = undefined;

export const cube: Physical & HasModel & Stateful<CubeState> & Lifecycleable = {
assetPath: "box.glb",
desc,
createRigidBody,
currentState: "idle",
onInit: (_: State, engine: Engine) => {
if (cube.rigidBody) {
controller = engine.world.createVehicleController(cube.rigidBody);
}
},
onTick: (_, engine: Engine) => {
controller.updateVehicle(1 / 60);

if (cube.rigidBody === undefined) {
return;
}

if (controller === undefined) {
controller = engine.world.createVehicleController(cube.rigidBody);
}

controller.updateVehicle(engine.world.timestep);
controller.addWheel();
},
stateMachine: {
idle: (): CubeState => {
return processCubeControls();
},
accelerate: (): CubeState => {
return processCubeControls();
},
accelerateRight: (): CubeState => {
return processCubeControls();
},
accelerateLeft: (): CubeState => {
return processCubeControls();
},
},
};
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { GRAVITY } from "./lib/constants";
import { loadModels } from "./systems/load-models";
import { physics } from "./systems/physics";
import { GLTFLoader } from "three/examples/jsm/Addons";
import { controls } from "./systems/controls";
import { stateMachine } from "./systems/state-machine";

import("@dimforge/rapier3d").then(async (rapier: Rapier) => {
if (document.body.children.length > 1) {
Expand Down Expand Up @@ -38,7 +40,15 @@ import("@dimforge/rapier3d").then(async (rapier: Rapier) => {
currentTick,
};

const systems: System[] = [tick, render, resize, loadModels, physics];
const systems: System[] = [
render,
resize,
loadModels,
physics,
controls,
stateMachine,
tick,
];

renderer.setSize(window.innerWidth, window.innerHeight, true);
document.body.appendChild(renderer.domElement);
Expand Down
37 changes: 37 additions & 0 deletions src/systems/controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { System } from "../lib/types";

const CONTROLS = {
FORWARD: "forward",
RIGHT: "right",
LEFT: "left",
JUMP: "jump",
} as const;

export type Control = (typeof CONTROLS)[keyof typeof CONTROLS];

export const PRESSED_KEYS: Record<string, boolean> = {};
export const PRESSED_CONTROLS: Partial<Record<Control, boolean>> = {};

export const KEYS_TO_CONTROLS: Record<string, Control> = {
w: CONTROLS.FORWARD,
d: CONTROLS.RIGHT,
a: CONTROLS.LEFT,
Space: CONTROLS.JUMP,
};

const setKeyState = (to: boolean) => {
return (event: KeyboardEvent) => {
PRESSED_KEYS[event.key] = to;
PRESSED_CONTROLS[KEYS_TO_CONTROLS[event.key]] = to;
};
};

export const controls: System = {
tick() {
// do nothing.
},
init() {
document.addEventListener("keydown", setKeyState(true));
document.addEventListener("keyup", setKeyState(false));
},
};
28 changes: 28 additions & 0 deletions src/systems/state-machine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { query } from "../lib/queries";
import { Component, State, System } from "../lib/types";

type StateMachine<T extends string> = Record<T, () => T>;

export type Stateful<T extends string> = Component & {
currentState: T;
stateMachine: StateMachine<T>;
};

function isStateful<T extends string>(
component: Component,
): component is Stateful<T> {
return "currentState" in component && "stateMachine" in component;
}

export const stateMachine: System = {
tick: (state: State) => {
const statefuls = query(state, isStateful);

statefuls.forEach((stateful) => {
stateful.currentState = stateful.stateMachine[stateful.currentState]();
});
},
init: () => {
// do nothing.
},
};
71 changes: 71 additions & 0 deletions tests/systems/controls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it, vi } from "vitest";
import {
Control,
controls,
KEYS_TO_CONTROLS,
PRESSED_CONTROLS,
PRESSED_KEYS,
} from "../../src/systems/controls";
import { testEngine } from "../../vitest.setup";
import { userEvent } from "@vitest/browser/context";

const press = (key: string) => userEvent.keyboard(`{${key}>}`);
const release = (key: string) => userEvent.keyboard(`{/${key}}`);

// These are in waitFor()s since the controls system uses events.
describe("controls system", () => {
controls.init({}, testEngine);

it("sets a key to being pressed when it is pressed", async () => {
press("a");
await vi.waitFor(() => {
expect(PRESSED_KEYS.a).toBeTruthy();
});
release("a");
});

it("does not set a key to being pressed when it isn't", async () => {
press("a");
await vi.waitFor(() => {
expect(PRESSED_KEYS.b).toBeFalsy();
});
release("a");
});

it("unsets a key when the key is released", async () => {
press("a");
release("a");
await vi.waitFor(() => {
expect(PRESSED_KEYS.a).toBeFalsy();
});
});

it("does not unset a key when the key isn't released", async () => {
press("a");
press("b");

release("a");
await vi.waitFor(() => {
expect(PRESSED_KEYS.b).toBeTruthy();
});
release("b");
});

it("sets the corresponding control when a key is pressed", async () => {
KEYS_TO_CONTROLS["a"] = "test" as Control;
press("a");
await vi.waitFor(() => {
expect(PRESSED_CONTROLS["test" as Control]).toBeTruthy();
});
release("a");
});

it("unsets the corresponding control when a key is released", async () => {
KEYS_TO_CONTROLS["a"] = "test" as Control;
press("a");
release("a");
await vi.waitFor(() => {
expect(PRESSED_CONTROLS["test" as Control]).toBeFalsy();
});
});
});