Games can be architected in different ways, but Entity Component System, also known as ECS, is one of the more popular ones. I won’t spend too much time explaining why one should pick ECS over OOP or any other style, there are better resources out there.
In this series I will document my experience building ECS in TypeScript and why I did what I did.
To begin, we need a Component:
// All components must identify themselves.
type Component = {
  type: string;
};
Entities are composed of components and merely identifiers. However, I want to start with something easier:
type Entity = {
  id: number;
  components: Component[];
};
let nextId = 1;
function createEntity(...components: Component[]) {
  return {
    id: nextId++,
    components,
  };
}
So far, so good.
It’s important that components carry state and there should be no logic in either components or entities. So where is the logic? Inside Systems.
System will receive a list of entities and process them in some way. They should have no state and merely operate on components/entities. They can read them, update components, create or remove components or even whole entities.
The simplest abstraction is as follows:
// something we need to supply every frame
// can be current time, current frame number, both or more
type TickInfo = number;
type System = {
  update: (tickInfo: TickInfo, entities: Entity[]) => void;
};
And that’s it! Let’s build something with this!
An example
The first useful component we can think of is position:
class PositionComponent implements Component {
  type = 'position';
  constructor(public x: number, public y: number) {}
}
// easy way of creating position
const position = new PositionComponent(10, 10);
So how would we render it? Our game loop should be rather simple. Given a list of systems we call update on every one of them per frame.
const systems: System[] = [];
const entities: Entity[] = [];
function render() {
  const now = new Date().getTime();
  systems.forEach((system) => system.update(now, entities));
  requestAnimationFrame(render);
}
render();
Let’s create a simple rendering system:
function createRenderSystem(): System {
  const canvas = document.createElement('canvas');
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');
  return {
    update: () => {
      ctx.save();
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
      ctx.restore();
      entities.forEach((e) => {
        // so how do we get the rendering data
      });
    },
  };
}
So…given a list of entities we only care about a few select components. Let’s introduce a new helper function: getComponent:
function getComponent<T extends Component>(e: Entity, type: string) {
  return e.components.find((c) => c.type === type) as T;
}
Now we can finish our render system:
entities.forEach((e) => {
  const p = getComponent(e);
  ctx.fillRect(p.x, p.y, 1, 1);
});
Running this code yields a black rectangle on screen. Nothing exciting yet.
Source code for this code can be found at https://github.com/tpetrina/ecs-ts-test/blob/master/examples/ecs1.ts and live demo at https://ecs-ts-test.netlify.app.
 

 
    
Top comments (1)
check out ECSY