Entity Component System in TypeScript

tpetrina

Toni Petrina

Posted on May 28, 2020

Entity Component System in TypeScript

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;
};
Enter fullscreen mode Exit fullscreen mode

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
      });
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Now we can finish our render system:

entities.forEach((e) => {
  const p = getComponent(e);
  ctx.fillRect(p.x, p.y, 1, 1);
});
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
tpetrina
Toni Petrina

Posted on May 28, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related