Project Layout
This page documents the recommended directory layout for a YAGE game. It’s
the layout the create-yage starter templates embody and the layout that’s
worked well on real projects. Not a hard rule — adapt as your game grows —
but starting here will save you refactors later.
The layout
Section titled “The layout”src/├── main.ts # entry: create Engine, register plugins, push initial scene├── scenes/│ └── <SceneName>.ts # one file per scene├── entities/│ ├── <SimpleEntity>.ts # single-file class for entities that fit in ~50 lines│ └── <ComplexEntity>/ # folder for entities that need supporting files│ ├── index.ts # the Entity subclass│ ├── <Entity>Controller.ts│ └── <other entity-specific components>.ts├── components/ # ONLY for components reused across multiple entities│ └── <SharedComponent>.ts└── traits/ # optional, advanced └── <TraitToken>.ts
public/└── assets/ # images, audio, data files — served at site root1. One scene per file
Section titled “1. One scene per file”A scene class assembles the scene: it preloads assets, sets up the camera and spawns entities. It holds no game state and no rules. Geometry belongs in entity classes. Movement, collision callbacks, event handlers and game state such as the score belong in those entities’ components.
Rule of thumb: if a scene grows past ~150 lines, that’s a signal you’re
inlining entity logic. Extract entity classes and components until onEnter
is a list of spawns.
import { Entity, Scene, defineEvent } from "@yagejs/core";import { texture } from "@yagejs/renderer";import { sound } from "@yagejs/audio";
// Good — the scene is a spawn listclass GameScene extends Scene { readonly name = "game"; readonly preload = [playerTexture, jumpSfx];
onEnter() { this.spawn(Hud, { key: HUD_KEY }); // hosts the score this.spawn(Player, { x: 120, y: 400 }); // respawns itself on PlayerHit this.spawn(Platform, { x: 800, y: 560, width: 1600, height: 60 }); this.spawn(Coin, { x: 380, y: 410 }); this.spawn(Hazard, { x: 620, y: 340, amplitude: 60, period: 2.2 }); }}
// Asset handles and the event, declared with the scene:const playerTexture = texture("/assets/player.png");const jumpSfx = sound("/assets/jump.wav");const PlayerHit = defineEvent("game:player-hit");
// Defined in entities/ and elsewhere in the game:declare class Hud extends Entity {}declare const HUD_KEY: string;declare class Player extends Entity { setup(params: { x: number; y: number }): void }declare class Platform extends Entity { setup(params: { x: number; y: number; width: number; height: number }): void }declare class Coin extends Entity { setup(params: { x: number; y: number }): void }declare class Hazard extends Entity { setup(params: { x: number; y: number; amplitude: number; period: number }): void }The player’s controller component reacts to PlayerHit itself, with
this.listenScene(PlayerHit, () => this.respawn()). When a component should
not listen for itself, onEnter may connect the event to a component method in
one line, such as this.on(PlayerHit, () => player.get(Respawn).respawn()).
State Management shows where the score lives.
2. Simple entity → single file; complex entity → folder
Section titled “2. Simple entity → single file; complex entity → folder”A simple entity is one Entity subclass that fits comfortably in one file:
entities/<Name>.ts.
entities/├── Platform.ts # 20 lines — just Transform + Graphics + physics├── Wall.ts # 15 lines — no graphics, just a collider└── Coin.ts # 35 lines — single trigger callbackA complex entity is one that needs supporting files next to it (a
dedicated controller, multiple components, an asset module). Promote to a
folder named after the entity, with an index.ts exporting the class:
entities/├── Player/│ ├── index.ts # the Player Entity subclass│ ├── PlayerController.ts # reads input, drives the rigid body│ └── PlayerInventory.ts # another player-specific component└── Enemy/ ├── index.ts ├── EnemyAI.ts └── EnemyAnimations.tsStart simple — promote to a folder only when you actually need the second file. Don’t pre-create folders for entities that fit in one file.
3. Entity-specific components live next to the entity
Section titled “3. Entity-specific components live next to the entity”src/components/ is reserved for components that are used by multiple
entities. A PlayerController that only the Player uses belongs in
entities/Player/, not components/.
If a second entity starts using the same component, move it to components/
at that point. That’s exactly the threshold the starter’s Oscillate
component crosses — it’s used by both Coin (vertical bob) and Hazard
(horizontal slide), so it lives in components/Oscillate.ts:
import { Component, Entity } from "@yagejs/core";
// components/Oscillate.ts — shared between Coin and Hazardexport class Oscillate extends Component { constructor(readonly options: { axis: "x" | "y"; amplitude: number; period: number }) { super(); } // ... sine-wave motion along an axis ...}
// entities/Coin.tsexport class Coin extends Entity { setup() { this.add(new Oscillate({ axis: "y", amplitude: 4, period: 1.2 })); }}
// entities/Hazard.tsexport class Hazard extends Entity { setup() { this.add(new Oscillate({ axis: "x", amplitude: 60, period: 2.2 })); }}If the starter had only Coin using Oscillate, the component would live at
entities/Coin/Oscillate.ts instead (and Coin.ts would be promoted to a
folder).
4. main.ts stays short
Section titled “4. main.ts stays short”Engine creation, plugin registration, scene push. No game logic. If main.ts
grows, the overflow belongs in a scene or a plugin.
// main.ts — entire fileimport { Engine } from "@yagejs/core";import { RendererPlugin } from "@yagejs/renderer";import { PhysicsPlugin } from "@yagejs/physics";import { InputPlugin } from "@yagejs/input";import { GameScene } from "./scenes/GameScene.js";
const engine = new Engine({ debug: true });engine.use(new RendererPlugin({ width: 800, height: 600, container: document.getElementById("game")! }));engine.use(new PhysicsPlugin({ gravity: { x: 0, y: 980 } }));engine.use(new InputPlugin({ actions: { jump: ["Space"] } }));await engine.start();engine.scenes.push(new GameScene());5. Traits are optional and advanced
Section titled “5. Traits are optional and advanced”Traits (defineTrait / trait()) are capability markers that let you query
across entity types without depending on specific classes — “all Damageable
things”, “all Interactable things”. They pay off once you have several
entity types sharing the same capability. For simple games, skip traits/
entirely; you don’t need it until you do.
6. Assets live in public/assets/
Section titled “6. Assets live in public/assets/”Vite serves everything in public/ from the site root. A handle declared as
texture("/assets/player.png") resolves to public/assets/player.png at
runtime.
Declare handles with texture() / sound() at module scope in the scene
that uses them, and list them in Scene.preload — YAGE guarantees they’re
loaded before onEnter runs.
import { Scene } from "@yagejs/core";import { texture } from "@yagejs/renderer";import { sound } from "@yagejs/audio";
export const playerTexture = texture("/assets/player.png");export const jumpSfx = sound("/assets/jump.wav");
export class GameScene extends Scene { readonly name = "game"; readonly preload = [playerTexture, jumpSfx]; // ...}Exporting the handles means entity files in entities/ can import them for
use in their own setup() methods — no string duplication.
When to refactor
Section titled “When to refactor”These rules describe a starting point, not a prescription. Promote when you feel friction:
| Signal | Refactor |
|---|---|
| Scene file > 150 lines with inline spawn helpers | Extract entity classes |
| Entity file > 100 lines with a dedicated controller component | Promote to entities/<Name>/ folder |
| Same component code appearing in two entity files | Move to src/components/ |
main.ts contains game logic | Move it into a scene or plugin |
| Two scenes need the same asset path constants | Export them from a shared module |
See also
Section titled “See also”- Entity Subclasses — the
setup()pattern in depth - Scene Management — scene stack, pausing, transitions
- Your First Game — guided walkthrough
Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/patterns.md.