logo

Babylon.js Market

By Lawrence

8 minutes

Classes

TL;DR — This lesson teaches TypeScript's classA blueprint that describes what something is (its data) and what it can do (its behavior), used to stamp out objects of one type. grammar: fields, the constructorThe special method that runs once when you write new, setting up the instance's starting data., methods, public/private/protected, readonly, extends/super, and the abstract methodA named behavior defined on a class and called on one of its instances. the compiler makes every subclass fill in. We read it all off the framework's real Component and System classes. Skip thisInside a method, the word an instance uses to refer to itself and reach its own fields. if TypeScript classes already feel easy (extends, super(), abstract, the three access modifiers). If not, read on. Every later lesson, and every component you open, is written in this class vocabulary.

Last lesson you learned to read a type signature out loud. emit<T = any>(type: string, data?: T): void stopped being a wall of punctuation and turned into a plain sentence. But each type you read that way described one small thing: a value, a parameter, a return value. Now you learn to read the container those small types live in.

Those containers are the real classes inside @babylonjsmarket/ecs. (ECS stands for Entity, Component, System, the three pieces it is built around.) This is real code that ships in production, and this course reads it to learn from. The whole framework is really a small set of cookie cutters. Once you can read its classes, the rest is filling them in. But one of those classes will not let you use new at all. Another one makes the compiler force you to write a method before your code will build. What in a class can refuse to be built? And what can make you finish it?

A class is a blueprint

A cookie cutter is not a cookie. It is the shape that stamps out cookies. Stamp a hundred and they all share the shape. But each one is its own cookie, and you can frost each one differently.

A class is that cookie cutter. It is a blueprint that describes what something is (its data) and what it can do (its behavior). Stamp one out and you have an instanceOne real object stamped out from a class with new, carrying its own copy of the class's fields., made with new. Here is the smallest class we can write, just to see the parts:

class Counter {
  // A FIELD: a piece of data each instance carries.
  count: number;

  // The CONSTRUCTOR: runs once, when you say `new Counter(...)`.
  constructor(start: number) {
    this.count = start;
  }

  // A METHOD: a behavior the instance can perform.
  increment(): void {
    this.count = this.count + 1;
  }
}

const c = new Counter(10); // stamp one out, starting at 10
c.increment();             // call its behavior
console.log(c.count);      // 11

Three pieces show up in every class you will ever read:

PieceWhat it isIn CounterLives on
Fielda named slot for datacounteach instance, separately
Constructorspecial method, runs once on newconstructor(start)the instance it builds
Methoda named behaviorincrement()the instance you call it on

Write new Counter(10) and the 10 flows into start. We save that into this.count. Inside a method, this is how an instance refers to itself. So this.count means "the count that belongs to this instance." Make two counters and each one's this.count points at its own slot.

dark-navy neon-wireframe diagram: a glowing class blueprint on the left labelled with fields and a constructor, an arrow marked new stamping out three identical wireframe instances on the right, each with its own data slots

Counter shows you the whole grammar. Everything below uses that same grammar, written by people building a real game framework.

The framework's four core classes

@babylonjsmarket/ecs is built around four classes. Learn their shapes and you have the skeleton. Three of them make up the ECS pattern: Entity, Component, and System. The fourth is a container that runs everything:

ClassHoldsWhat it is
Entityan id + a bag of componentsa thing in your game (player, bullet, tree); no behavior of its own
Componentstate, nothing elsepure data attached to an entity — a Position (x, y, z), a Health (hit points)
Systemper-frameOne redraw of the screen. A game produces many frames a second; each one re-runs the game's logic once. behaviorruns every frame over the entities it asked for; where game logic lives
Worldevery entity and systemthe container that drives the loop — world.update(...) ticks them all

A game redraws itself many times a second. Each redraw is one frame. The World runs a loop, and each pass through that loop is one tickOne pass of the World's loop. A tick advances every system once; world.update() performs one tick.. A tick advances every system once, and that is what world.update(...) does. A later lesson measures that loop down to the millisecond. For now you only need the picture: systems run over and over, not just once.

That is the architecture, and the sibling course teaches it as architecture. Here we use it as reading material. We open these classes to learn the keywords, not to learn how queries filter entities. (A queryThe set of entities a system asks to receive, described by the components those entities must have. is the set of entities a system asks to receive. Here, that means everything with a Position.) Let's start with the class that will not let you use new.

Component: pure data, and the two hooks you override

The real Component is an abstractA marker on a class that forbids new, or on a method that names it but leaves the body for a subclass to fill in. class. An abstract class is a blueprint you cannot stamp out directly. The compiler will not let you write new Component(). It exists only to be extended by classes that fill in the details. Inside it sit two methods worth reading in the framework's own words:

@babylonjsmarket/ecs/src/ECS/Component/Component.ts
protected onAttachOverride(): void {}

/**
 * Override this method in subclasses to run cleanup logic
 * when the component is removed from an entity.
 *
 * Example: A Physics component might destroy its physics body here.
 */
protected onDetachOverride(): void {}

Two newThe keyword that runs a class's constructor and hands back a fresh instance. words in two lines. (The /** ... */ block between them is a comment. It is a note for the reader, and the compiler ignores it.) protected is an access modifier. We will cover it in the next section. The : void on each method means it hands nothing back. The word to notice is overrideReplacing a method inherited from a base class with the subclass's own version.. These two methods are hooks: methods the framework calls for you at a fixed moment. onAttachOverride runs the instant a component attaches to an entity. onDetachOverride runs when it is removed. A physics component might build its physics body on attach and tear it down on detach. Both methods do nothing by default. That empty {} is the whole body. A subclass overrides one only when it has setup or cleanup to run. Notice what is missing here: there is no update. A component holds state. The per-frame work happens somewhere else.

public, private, protected: which code may touch what

Three little words show up all over these classes. These access modifiers answer one question: which code is allowed to touch this fieldA named data slot that lives on each instance separately. or method?

ModifierWho may touch itDefault?In the framework
publicany other codeyesentity.id — any code holding an entity reads its id
protectedthis class and its subclasses ("family only")nothe two Component hooks — overridable by a component, not callable from outside
privateonly code inside this same classnoEntity's components map — reachable only through add / get / has

Limiting access like this is called encapsulationRestricting which code may touch a class's internals so the class keeps control of its own rules.. A class marks which fields the rest of the code may touch, so it keeps control of its own rules. Say some outside code reached straight into Entity's components map. It could add a component without the framework noticing, and no system would track it. Marking the map private and sending every change through add keeps the framework in the loop. The compiler flags any code that tries to reach around it, right as you type.

readonly stacks on top of these. It is not about who may touch a field. It is about when: you set it once in the constructor and never change it again. An entity's id is public readonly. Any code may read it, but no other code may change it after the entity is made. So an entity's id stays fixed, and the code that depends on it can trust it.

dark-navy neon-wireframe diagram: three concentric glowing rings around a class core labelled public outermost open, protected middle ring marked family only, private innermost ring sealed; an outside arrow bounces off the private ring and is redirected through a public getEventBus doorway

The hooks are protected for the same reason. A component is meant to override them, not to have outside code call them. A class and its subclasses may reach in. Other code may not.

extends and abstract: where your behavior plugs in

You never write game behavior by editing the framework. You write it by extending the System class with your own. extends means "build a new class on top of an existing one, and inherit everything it has." Your class gets all of System's fields and machinery for free, and you add the details. That family relationship is called inheritanceThe family relationship between a base class and the classes built from it with extends..

But System is abstract too, and it does something Component did not. It declares a method with no body, and the compiler makes you fill it in:

@babylonjsmarket/ecs/src/ECS/System/System.ts
/**
 * REQUIRED: Override this method with your system's update logic.
 * Called every frame with the time since last frame.
 *
 * @param deltaTime - Time since last frame in seconds
 */
protected abstract onUpdate(deltaTime: number): void;

This is an abstract methodA method a base class declares with no body, which the compiler forces every subclass to implement.. The base class names it but leaves the body blank. Because System declares onUpdate as abstract, every class that extendsThe keyword that builds a new class on top of an existing one, inheriting all its fields and methods. System must implement it. The compiler enforces this. Forget onUpdate and the code will not compile. That one method is where all your game logic goes.

Here is a small system that does all three at once: extends, super(), and the required onUpdate:

class MovementSystem extends System {
  constructor() {
    super();                               // run the base System's constructor first
    this.query = { required: [Position] }; // declare which entities I want
  }

  // The compiler demanded this method. Here it is.
  protected onUpdate(dt: number): void {
    for (const e of this.entities) {
      const pos = e.get(Position);
      if (pos) pos.x += dt; // dt is seconds since the last frame
    }
  }
}

Here is what happens inside onUpdate. The for loop visits each entity the system asked for (this.entities). e.get(Position) fetches that entity's Position data. The if (pos) check skips any entity that has no Position. Then += dt adds to the value in place. dt is short for deltaTime: how long the previous frame took, in seconds. You multiply movement by it so a character covers the same ground whether the loop runs 30 or 120 times a second.

extends System makes MovementSystem a kind of System. It inherits System's fields and query machinery. super() runs the base class's constructor. super means "the base class." You must call it before you touch this in a subclass constructor, because the base has to finish setting itself up first. We pass nothing to it. The World hands the system its event bus when the system is added. (An event bus is a shared channel systems use to talk, covered next lesson.) And onUpdate is the abstract method filled in. Leave it out and TypeScript stops you before the game runs.

Every behavior in this framework uses the same three moves: extend System, set a query, and fill in onUpdate. The compiler will not let you forget the third one. The exact form can vary a little. A system can set its query inside the constructor (as we did here) or as a class field, and it can take the bus directly. Open the real KeyboardMoverSystem and you will see it does both: protected query = { ... } and constructor(eventBus: EventBus) { super(eventBus); }. Those are the same three moves in a slightly different form.

A real component is Counter plus extends Component

Now the payoff. KeyboardMover is a published component from @babylonjsmarket/arcade. It drives a character with WASD. Read its data class and you will recognize every part from Counter:

@babylonjsmarket/arcade/src/Components/KeyboardMover/KeyboardMover.ts
export interface KeyboardMoverInput {
  /** World units per second at full input. */
  speed?: number;
  /** When true, the mesh yaws to face its direction of motion. */
  faceMotion?: boolean;
}

const FORWARD_KEYS = new Set(['KeyW', 'ArrowUp']);
const BACK_KEYS = new Set(['KeyS', 'ArrowDown']);
const LEFT_KEYS = new Set(['KeyA', 'ArrowLeft']);
const RIGHT_KEYS = new Set(['KeyD', 'ArrowRight']);

export class KeyboardMoverComponent extends Component {
  speed: number;
  faceMotion: boolean;

  constructor(data: KeyboardMoverInput = {}) {
    super();
    this.speed = data.speed ?? 4;
    this.faceMotion = data.faceMotion ?? true;
  }

  serialize(): KeyboardMoverInput {
    return { speed: this.speed, faceMotion: this.faceMotion };
  }
}

The comments mention a mesh, a yaw, and world units. Those are 3D terms. A mesh is the visible shape, world units are how it measures distance, and yaw is turning to face a direction. A later lesson covers them, so ignore them for now. This lesson is about the class shape, not what the fields mean. Strip away the names and this is Counter. It has two fields (speed and faceMotion). It has a constructor that runs super() first, then fills the fields. It runs super() first because the abstract base Component must set itself up before the subclass touches this. And it has a method, serialize, that hands the component back as plain data for saving. The class extends a real abstract base, the same one whose hooks you read three sections ago. It overrides neither hook, because a keyboard mover has no body to build on attach. It inherits the empty defaults and leaves them alone.

Two details earned their own glossary entry. The interface uses ?. speed? and faceMotion? are optional, so a caller can leave them out. And the constructor uses ??, the nullish-coalescing operator. data.speed ?? 4 means "use data.speed, but if it is null or undefined, use 4." An explicit speed: 0 is kept, not replaced. So new KeyboardMoverComponent() gives a character that moves at 4 units a second and turns to face its motion. And new KeyboardMoverComponent({ speed: 8 }) overrides one field and defaults the rest. Optional inputs come in, and sensible defaults fill the gaps. Every published component uses this same pattern.

The nine keywords, read off real source

You just read every word this lesson taught in code that ships in production:

WordWhat it meansSeen in
classa blueprint for data + behaviorCounter, KeyboardMoverComponent
instanceone real thing stamped out with newnew Counter(10)
fielda named data slot on each instancespeed, faceMotion
methoda named behavior on each instanceincrement, serialize
constructorsets up an instance's starting dataKeyboardMover's constructor
publicAn access modifier marking a field or method as reachable by any other code; it is the default. / protectedAn access modifier marking a field or method as reachable only inside its class and the classes that extend it. / privateAn access modifier marking a field or method as reachable only by code inside the same class.which code may touch a field or methodthe protected hooks
readonlyA modifier that lets a field be set once in the constructor and never reassigned afterward.set once in the constructor, never afteran entity's id
abstracta class or method a subclass must fill inComponent, abstract onUpdate
extends / superInside a subclass, the keyword that reaches the base class; super() runs the base class's constructor.()build on a base class and run its constructorextends Component, super()

That is the shape of @babylonjsmarket/ecs, and of every component you will ever open. A published component is not so different from the Counter you wrote at the top. It has the same fields, constructor, and methods, with extends Component on the front and the compiler holding it to the contract. You know the keywords now. The rest of the framework is filling them in.

Next: Generics and Interfaces — how one definition stays type-safe for data it has never seen, and one contract stands in for engines it was never written for.

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search