8 minutes
The ECS Mental Model
TL;DR — We watch a classA blueprint for making objects of one type, bundling their fields and methods together. hierarchy break on Pac-Man's possessable ghost. Then we rebuild it in ECSEntity Component System, the architecture that builds game objects by composing data and behavior instead of subclassing them.: data-only components, queryThe filter a system declares so the framework hands it only the entities whose components and tags match.-driven systems, and a WorldThe @babylonjsmarket/ecs object that owns every entity and system and drives the frame loop via update. that runs them each frameOne tick of the game's heartbeat - the game redraws dozens of times a second, and each redraw is a frame.. We end with the smallest loop you can run. Skip this if you have already built on an entityA bare ID with a container of components and tags; it holds no behavior of its own.-componentA pure-data block of fields you attach to an entity; it carries state, never logic.-systemA class that declares a query and runs behavior every frame over the entities that match it. and know why compositionBuilding behavior by combining small independent parts at runtime, rather than locking it into a class hierarchy. beats an inheritanceDeriving one class from another so it reuses and extends the parent's fields and methods. tree.
This course teaches you @babylonjsmarket/ecs, the framework behind these arcade games. We cover one idea per lesson. By the end, building a game from parts will feel normal. We start with the idea the other eight lessons build on. It is called ECS, short for Entity Component System. Most game frameworks use this pattern to organize a game. To see why it exists, we first watch the usual approach fall apart.
Say you are building Pac-Man. A class is a blueprint for making objects of one type. A hierarchy stacks classes so a more specific class reuses a general one. You start with a clean class hierarchy. GameObject sits at the root. Enemy sits below it. Ghost sits below that. Blinky, Pinky, Inky, and Clyde are all instances of Ghost. This is the textbook way to do it.
Then the design changes, because designs always change. A power pellet makes ghosts flee. So Ghost gets a frightened flag and a tangle of if statements. Next, Pac-Man can possess a ghost and steer it. But the input code lives in Player, on a branch you cannot reach. Here is where this approach breaks down:
New to reading TypeScript? A few marks show up below. class X extends Y means X is built on top of Y. update(dt: number) is a named block of code, called a function. It takes one input called dt, and that input's type is number. { } wraps the body of a class or function. import pulls a name in from another file. You do not need to read every line. The comments and prose carry the idea.
PossessableGhost is where this breaks. It needs ghost behavior and player behavior. But a class has only one parent. So you copy the input code over from Player. Now you have two copies, and they drift apart with every fix. This is the diamond problemThe conflict that arises when one class needs behavior from two parents that a single-parent tree can't supply.. Games run into it all the time, because game design is about mixing behaviors. Inheritance asks you to plan every combination up front, as a tree. Designers do not think in trees. They think, "what if the fruit could also shoot?"
The problem always looks the same. A design change asks for behavior from a branch you cannot reach.
| Designer asks for | Inheritance answer | What breaks |
|---|---|---|
| Ghosts that flee a power pellet | frightened flag + if in Ghost.update | Logic piles up in one update method |
| A ghost Pac-Man can possess | Needs Ghost and Player behavior | One parent only — copy the input code |
| Fruit that flees like a ghost | A new class, or hoist flee logic up | Flee logic now lives in the wrong place |
| A boss that does all of it | A class inheriting from… everything | The diamond; no single tree fits |
Most game frameworks solve this a different way. They use the ECS pattern, which this course is built on.
Think in data and behavior, not object types
ECS stops asking "what is this object?" It asks two other questions instead. What does this entity have? And what processes it?
So there is no PossessableGhost class. There is just a plain entity, which is really only an ID. That entity has a Position, a GhostBrain, and a Possessable. The behavior lives in systems. Each frame, the systems sweep the world and ask "give me everything with a GhostBrain." A web app waits for events. A game is different. It redraws over and over, dozens of times a second. Each redraw is a frame, and your systems run once per frame.

You build behavior by snapping data blocks onto an ID. No new class, no changes to a tree. ECS is four ideas working together. Each one is simpler than its name.
The four parts of ECS: entity, component, system, World
An entity is an ID plus a container. It has no behavior and no update method. It holds components and string tags. A tagA plain string label on an entity, like "player", that systems can match without a dedicated component. is a label like "player" that you can match on without a full component. A component holds fields and nothing else. Position holds x, y, z. GhostBrain holds a mode and a target. You do not have to remember the "fields and nothing else" rule. It is built into the base class. Here is the real Component from the package. It has two override hooks, one for attach and one for detach, and no per-frame method at all.
There is no update method here. A component reacts when it joins an entity and when it leaves. That is all it does. The per-frame work happens somewhere else.
It happens in a system. A system declares a query. Every frame, the framework hands it the entities that match. The system implements onUpdate(dt), where dt is the seconds since the last frame. The query is the other half of ECS that lives in the source. It is the ISystemQuery interface. It has five filter fields, and the framework reads them to decide which entities a system gets:
A system declares its query once. Then the framework keeps the matching set filled. No system ever goes hunting for its entities. This lesson uses only required. All five filters get their own lesson later.
The World is in charge. Every frame you call world.update(dt), and it runs each system's onUpdate. There is one more thing it does. The moment you add or remove a component, the World re-checks which queries that entity matches. This happens right away, not on the next frame.

The OOPObject-Oriented Programming, the style that models a program as classes of objects that inherit from one another. hierarchy crammed all four jobs into one inheritance chain. That is why it kept breaking. ECS gives each job to a different part.
| Idea | Its one job | Holds | Pac-Man example |
|---|---|---|---|
| Entity | Identity | An ID, a set of components, a set of tags | Blinky; one pellet; the score readout |
| Component | State | Pure data fields, no logic | Position { x, y, z }, Health { current, max } |
| System | Behavior | A query + an onUpdate(dt) | GhostAISystem steers everything with a GhostBrain |
| World | Orchestration | Every entity, every system, the frame loop | world.update(dt) ticks all systems once, in priority order (covered later; with one system, order doesn't matter yet) |
The smallest runnable ECS program
Here is the smallest real program you can write with @babylonjsmarket/ecs. It has one component, one system, and one entity, drifting steadily to the right.
First the component. It is pure data, exactly as the base class requires:
Now the system. It declares required: [Position] and implements onUpdate:
The query is the filter. this.entities gains an entity the moment it gains a Position. It loses that entity the moment the Position is gone. This is why the trailing ! in entity.get(Position)! is safe. The ! promises the language that this value exists, so the language stops warning us. The * dt keeps movement frame-rate independent. Drop it, and pos.x += 2 runs faster on a 144 Hz monitor than on a 60 Hz one.
Finally the bootstrap, where the World starts up:
Run that and you have a working ECS loop. Nothing shows on screen yet. Position is just numbers until a render component joins it. That render component is the arcade library's MeshPrimitive, and it arrives in the scenes lesson. Even so, every piece is running. The World ticks, the query matches, and the system moves the data.
The new BabylonAdapter() line is worth a look even now. The World never imports BabylonJSThe 3D rendering engine that draws the scene; @babylonjsmarket/ecs drives it through the renderer adapter.. It takes a renderer adapterThe seam that wraps a graphics engine so the rest of the game never touches Babylon or Three directly. and talks to the graphics engine only through that adapter. To swap the engine, you change one line here. The last lesson shows how. For now, notice that no component or system above names BabylonJS at all.
An entity does not have many methods. You can attach, read, check, and remove:
The methods are named get and has. Some other ECS libraries call these getComponent and hasComponent, but there is no such pair here.
Building the possessable ghost with composition
The possessable ghost that broke the hierarchy is not a class anymore. It is a recipe. GhostBrain and Possessable are more data-only components, like Position. We build them for real in a later lesson, so do not look for their full definitions here:
The GhostAISystem queries for GhostBrain and steers it. A PossessionSystem queries for Possessable and routes input to it. Neither system knows the other exists. The fleeing fruit is the same, but with a GhostBrain and no Possessable. The boss adds every component. There is no diamond because there is no tree. There is only data, and systems that match on it.

Here is the big shift in one line. Inheritance answers "what is it?" once, at design time, and that answer is fixed. Composition answers "what does it have?" at runtime, for each entity, and you can change it. A frightened ghost is not a new kind of thing. It is the same entity with one field flipped, and the systems follow the data.
This changes debugging, too. In OOP, behavior hides inside methods spread across a hierarchy. In ECS, the whole game state is data sitting on entities. So you can dump the world and read it directly.
This is how you will think for the rest of the course. Every gameplay idea breaks down into three parts: some data (a component), some logic that processes it (a system), and a query that connects them. Next we build them for real. You will see defaulted constructors, the one method every system must implement, and a hand-written component shown next to one straight from the arcade library.
Next: Writing Components and Systems — the defaulted-field constructor pattern, the single required onUpdate override, and the data split that keeps serialize and clone working.