Building Your Own Game
The Small World Engine is designed to scale from simple rotating cubes to fully-fledged game loops with custom logic, controllers, and UI.
The Architecture of a Game
A typical project should be split into the following modular parts:
- The app (
MyGameApp.ts): ExtendsSmallWorld. Builds the scene, loads textures, and initializes the camera and UI. - The level builder: Uses
GridLevelBuilderor generates the scene graph procedurally. - The controller (
MyController.ts): ExtendsFirstPersonControllerorOrbitController. This is the core logic for input and movement. - The UI (
MyHud.ts): A decoupled HTML overlay that listens toEvents.
1. Creating Your Own Controller
In Small World, controllers are simply Behavior components attached to a camera or an object. The built-in controllers can be extended to add game-specific logic such as shooting, raycasting, or item pickup.
import { FirstPersonController, FirstPersonControllerOptions } from "@small-world/engine";
import { Keys } from "@small-world/engine";
export class MyController extends FirstPersonController {
constructor(options: FirstPersonControllerOptions = {}) {
super(options);
}
public override update(deltaTime: number): void {
// 1. Let the base class handle WASD movement and collision
super.update(deltaTime);
// 2. Add custom logic (e.g. shooting)
if (this._options.input.isPressed(Keys.SPACE)) {
// Fire a bullet, perform a raycast...
console.log("Pew pew!");
// Use the injected EventBus
this.events.dispatchEvent("shoot-weapon", { ammoCost: 1 });
}
}
}2. Starting the Application (Bootstrapping)
Now let's wire up the controller, scene, and UI in our SmallWorld subclass.
import { SmallWorld } from "@small-world/engine";
import { MyController } from "./MyController.js";
import { MyHud } from "./MyHud.js";
export class MyGameApp extends SmallWorld {
private _hud!: MyHud;
protected async setupScene(): Promise<void> {
// Initialize the UI - pass the event bus explicitly
this._hud = new MyGameHUD(this.events);
// Attach our own controller as a behavior on the camera.
// The behavior system handles the update loop automatically.
this.camera.addBehavior(
new MyController({
scene: this.scene,
input: this.input,
moveSpeed: 15.0,
})
);
}
protected override update(deltaTime: number): void {
// Game loop logic...
}
}
// Start the game
const app = new MyGameApp();
app.start();This code structure keeps game logic (controller), rendering logic (app/scene), and the user interface (HUD) fully independent and easy to test or refactor!
Reference Implementation: YAD (Yet Another Dungeon)
The Small World Engine includes a complete, working showcase called YAD (Yet Another Dungeon), located at apps/sample-apps/yad. YAD is the canonical reference architecture for building a real game.
YAD demonstrates:
- Seamless tool integration: How standalone tools (
Pixler,MapGenerator,Xtractor) communicate with the game viaapp.eventswithout interrupting the render loop - no Forge overlay required. - Procedural level generation: How the
GridLevelBuilderextension parses an ASCII string map into 3D meshes, spawningEnemyBehavior-driven enemy sprites and pickup sprites. - Enemy logic: How
EnemyBehaviorimplements simple distance-based pursuit logic (detection radius, pursuit, attack range). YAD does not use the engine'sStateMachine/FSM module for its enemies - it's available (see State Machines) if richer state logic is needed. - A custom controller:
Controller(incore/behaviors/Controller.ts) inherits fromFirstPersonControllerand adds footstep sounds (via an injectedAudioSysteminstance), weapon-sway animation (rendered byHud), and raycasted attacks. - Decoupled UI (
Hud, incore/Hud.ts): A strict HTML overlay that listens toAppEventsto update health bars and log chat messages.
When starting a new project, it is strongly recommended to read through apps/sample-apps/yad to understand how the architecture scales!