One of the first design decisions in any game framework is how to represent game objects. Inheritance-based approaches seem natural at first:
Player extends Character extends MovableEntity extends Entity
But they fall apart quickly. What happens when you want a FlyingPlayer? Or a GhostEnemy that phases through walls and flies? Deep inheritance trees become brittle fast.
Craters uses the Entity-Component-System (ECS) pattern instead, and it makes a real difference.
In Craters, an entity isn’t a class with methods. It’s essentially a unique identifier — a container that components get attached to. This keeps things flat and avoids the problem of “where does this method live in the hierarchy?”
const scene = new Scene();
const player = scene.createEntity();
const enemy = scene.createEntity();
That’s it. Neither player nor enemy has any behaviour yet. Behaviour comes from components.
Components hold state and optionally logic related to a single concern. Each component is attached to an entity:
scene.addComponent(player, new PositionComponent({ x: 100, y: 200 }));
scene.addComponent(player, new VelocityComponent({ vx: 0, vy: 0 }));
scene.addComponent(player, new SpriteComponent({ src: 'player.png' }));
scene.addComponent(enemy, new PositionComponent({ x: 400, y: 300 }));
scene.addComponent(enemy, new SpriteComponent({ src: 'enemy.png' }));
// enemy has no VelocityComponent — it's stationary
Now player has position, velocity, and a sprite. enemy has position and a sprite. No inheritance, no shared base class — just components.
Systems are where the actual game logic runs. A system queries the scene for entities that have a specific combination of components, then processes them each tick:
class PhysicsSystem extends System {
update(delta: number) {
// Only processes entities that have BOTH Position AND Velocity
for (const entity of this.scene.query([PositionComponent, VelocityComponent])) {
const pos = entity.get(PositionComponent);
const vel = entity.get(VelocityComponent);
pos.x += vel.vx * delta;
pos.y += vel.vy * delta;
}
}
}
The physics system doesn’t care whether the entity is a player, an enemy, or a floating powerup. If it has the right components, the system processes it. If it doesn’t, it’s skipped automatically.
| Approach | Object composition | Adding behaviours | Sharing logic |
|---|---|---|---|
| Deep inheritance | Rigid hierarchy | Add a subclass | Extract to base class (messy) |
| ECS | Flat, component-based | Add a component | Write a shared system |
With inheritance you’d need FlyingEnemy extends Enemy — or worse, multiple inheritance workarounds. With ECS:
// Give the enemy velocity — now the PhysicsSystem processes it too
scene.addComponent(enemy, new VelocityComponent({ vx: 0, vy: -1 }));
One line. The existing PhysicsSystem picks it up automatically on the next tick, because the query now matches.
Browser games often have wildly different object types sharing common behaviours. ECS handles this gracefully:
Craters brings all of this to TypeScript with a clean API and no external runtime dependencies.
Source: github.com/john-swana/craters