Building a Custom Game
The Small World Engine is designed to scale from simple rotating cubes to fully-fledged gameloops with custom logic, controllers, and UI.
The Architecture of a Game
A typical project should be split into the following modular pieces:
- The App (
MyGameApp.ts): ExtendsSmallWorld. It setups up the scene, loads textures, and initializes the camera and UI. - The Level Builder: Uses
GridLevelBuilderor procedurally generates the scene graph. - The Controller (
MyController.ts): ExtendsFirstPersonControllerorOrbitController. This is the core input and movement logic. - The UI (
MyHud.ts): A decoupled HTML overlay that listens toEvents.
1. Creating a Custom Controller
In Small World, controllers are simply Behavior components attached to a camera or object. You can extend the built-in controllers to add game-specific logic like shooting, raycasting, or item pickups.
import { FirstPersonController, FirstPersonControllerOptions } from "small-world";
import { Keys } from "small-world";
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 your custom logic (e.g. Shooting)
if (this._options.input.isPressed(Keys.SPACE)) {
// Fire a bullet, do a raycast...
console.log("Pew pew!");
// Use the injected EventBus
this.events.dispatchEvent("shoot-weapon", { ammoCost: 1 });
}
}
}2. Bootstrapping the Application
Now we tie the Controller, the Scene, and the UI together in our SmallWorld subclass.
import { SmallWorld } from "small-world";
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 custom controller to the camera as a Behavior.
// 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 your game
const app = new MyGameApp();
app.start();By structuring your code this way, you ensure that your Game Logic (Controller), Rendering Logic (App/Scene), and User Interface (HUD) remain completely independent and easy to test or refactor!
Reference Implementation: YAD (Yet Another Dungeon)
The Small World Engine includes a complete, functional showcase called YAD (Yet Another Dungeon). 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 chase logic (detection range, chase, attack proximity). YAD does not use the engine'sStateMachine/FSM module for enemies — but it's available (see State Machines) for cases where richer state logic is needed. - Custom Controllers:
YadControllerinherits fromFirstPersonController, adding footsteps (using an injectedAudioSysteminstance), weapon-sway animation (rendered byYadHud), and raycasted attacks. - Decoupled UI (
YadHud): A strict HTML overlay that listens toAppEventsto update health bars and log chat messages.
When starting a new project, we highly recommend reading through src/apps/yad to understand how the architecture scales!