Entity Component System in TypeScript
Toni Petrina
Posted on May 28, 2020
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 System
s.
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.
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
November 29, 2024