Building a Game Engine with TypeScript: Part 1 — Getting Started

Getting started with the basics — How does it “all” work?

Mark Ringtved Nielsen
6 min readDec 20, 2022
Photo by Pixabay: https://www.pexels.com/photo/turned-on-red-and-green-nintendo-switch-371924/

If you’re thinking about creating a game engine, you may be wondering where to start. I faced this challenge myself and initially struggled to find a solution. However, I eventually learned that the key is to start small and gradually build up the features of your engine.

Creating a fully-featured game engine from scratch is a daunting task, especially if you’re working alone. It requires a large team with a range of skills and expertise. That’s why I decided to explore a different approach: Entity-Component-System (ECS).

ECS is a way of separating logic and adding features to your game engine over time. It works by dividing your game into entities, which are made up of data components, and systems that operate on these components. ECS follows the principle of composition over inheritance, meaning that each entity is defined by its associated components rather than by a hierarchy of types. This allows for greater flexibility and modularity in your game development process.

To begin building your game engine, the first step is to set up a project in a code editor like VSCode. This involves creating a configuration and organizing your files in a logical folder structure. It’s important to keep a clear and organized folder structure to make it easier to navigate and work on your project.

The folder structure I am using while writing this “Dev Blog” is as follows:

src/
- core/
- types/
- utils/
- components/
- entities/
- systems/
main.ts
assets/
tests/
docs/

Once I had the main folder structure in place, I created an ECS class in the “core” folder that serves as the central part of the engine. This class is responsible for managing the overall functioning of the system. It is what allows the entire engine to operate as intended.

What is an entity?

A entity is just a type of number

export type Entity = number

Entity Component System Class

The ECS class needs to have two key properties:

private entities: Map<Entity, ComponentCollection>()
private systems: Map<System, Set<Entity>>()

Both of these properties should have public setters to allow for modification. The entities property is a map that associates each entity with a collection of components, while the systems property is a map that associates each system with a set of entities. These properties are essential for managing the components and systems within the ECS system.

The addEntity setter is a method that is used to add a new entity to the ECS system. It is defined as follows:

public addEntity(): Entity {
let entity = this.nextId++;
this.entities.set(entity, new ComponentCollection());
return entity;
}

This method creates a new entity by incrementing a nextId counter and then adding it to the entities map along with a new ComponentCollection. The ComponentCollection is a class that is used to store and manage the components associated with the entity. The method returns the newly created entity, which can then be used to add components or register it with systems.

public addComponent(entity: Entity, component: Component): void {
this.entities.get(entity)?.add(component);
this.checkEntity(entity);
}

public addSystem(system: System): void {
system.ecs = this;

this.systems.set(system, new Set());
for (let entity of this.entities.keys()) {
this.checkEntitySystem(entity, system);
}
}

The addComponent function takes two arguments: entity, which is the entity to which the component will be added, and component, which is the component object to be added.

The function uses the add method of the Set object to add the component to the set of components associated with the entity. It also calls the checkEntity method, which we will cover later in the article.

The addSystem function is another method of the ECS class that allows you to add a new system to the ECS. The function takes a single argument: system, which is the system object to be added.

The function begins by setting the ecs property of the system object to this, which refers to the current instance of the ECS object. This gives the system a reference to the ECS, which it can use to access the entities and components managed by the ECS.

What is a Component?

public addComponent(entity: Entity, component: Component): void {
this.entities.get(entity)?.add(component);
this.checkEntity(entity);
}

In the provided code snippet, the addComponent function is used to add a component to an entity. The function takes in an entity and a component as arguments and adds the component to the entity using the add method.

It is important to note that in an ECS architecture, entities do not have any behavior or state on their own. Instead, they are simply containers that hold references to their associated components. By attaching different combinations of components to entities, developers can create complex systems with diverse behavior and functionality.

The Component base class is simple.

export abstract class Component {}

Example of an component (Health)

export class HealthComponent extends Component {
constructor(public health: number) {
super();
}
}

Quick look at the ComponentCollection class

export type ComponentClass<T extends Component> = new (...args: any[]) => T
export class ComponentCollection {
private map = new Map<Function, Component>()

public add(component: Component): void {
this.map.set(component.constructor, component);
}

public get<T extends Component>(componentClass: ComponentClass<T>): T {
return this.map.get(componentClass) as T;
}

public has(componentClass: Function): boolean {
return this.map.has(componentClass);
}

public hasAll(componentClasses: Iterable<Function>): boolean {
for (let cls of componentClasses) {
if (!this.map.has(cls)) {
return false;
}
}
return true;
}

public delete(componentClass: Function): void {
this.map.delete(componentClass);
}
}

How does the system knows when a entity is added?

private checkEntity(entity: Entity): void {
for (let system of this.systems.keys()) {
this.checkEntitySystem(entity, system);
}
}

private checkEntitySystem(entity: Entity, system: System): void {
let componentCollection = this.entities.get(entity);

if (componentCollection === undefined) {
return;
}

if (componentCollection.hasAll(system.componentsRequired)) {
this.systems.get(system)?.add(entity);
return
}
this.systems.get(system)?.delete(entity);
}

The checkEntity function takes in an entity as an argument and loops through a list of systems. For each system, it calls the checkEntitySystem function, passing in the entity and the system as arguments.

The checkEntitySystem function is used to check if the given entity has all of the components that the given system requires. If the entity does have all of the required components, the entity is added to the system using the add method. If it does not have all of the required components, the entity is removed from the system using the delete method.

By calling the checkEntity function, we can ensure that all systems only contain entities that have the appropriate data to run in them. This helps to ensure that the systems are only processing entities that meet the necessary requirements, and that the system's logic is not being applied to entities that do not have the necessary components. #Performance😇

How does the system class look?

The System class has three members:

  • componentsRequired: This is a set of Component classes that are required before the system is run on an entity. This set should be defined at compile time and should not change.
  • update(entities: Set<Entity>): This is an abstract method that is called on the System every frame. It takes a set of entities as an argument and must be implemented by any class that extends the System class.
  • ecs: This is a reference to the ECS (Entity-Component-System) instance that the system belongs to. The ECS is given to all systems, and systems contain most of the game code, so they need to be able to create, mutate, and destroy entities and components.
export abstract class System {
public abstract componentsRequired: Set<Function>
public abstract update(entities: Set<Entity>): void
public ecs: ECS | undefined
}

Here are an example of a HealthBarSystem

export class HealthBarSystem extends System {
componentsRequired = new Set<Function>([HealthComponent]);

update(entities: Set<Entity>): void {
for (let entity of entities) {
const components = this.ecs?.getComponents(entity);

const healthComponent = components?.get(HealthComponent)

if (healthComponent === undefined) {
break;
}

console.log(healthComponent.health)
}
}
}

What can we do with the game engine in the currenct state?

const ecs = new ECS();
ecs.addSystem(new HealthBarSystem());

let health = new HealthComponent(100);

const playerEntity = ecs.addEntity();
ecs.addComponent(playerEntity, health);

ecs.update(); // HealthBarSystem/console: 100

health.health = 90;

ecs.update(); // HealthBarSystem/console: 90

This is the basic about the way the game engine is going to work so we will setup a way for the computer/browser/phone to call the update function for us so we don’t have to do that manually and the data will not change this way that will happen in the systems.

Thank you for reading this article about the game engine. If you enjoyed it, please consider giving it a “clap” to show your support. I hope to see you in my future posts. Thank you again for reading! ❤️

--

--

Mark Ringtved Nielsen

I believe that learning is important and always try to keep an open mind Im someone who enjoys stepping outside of my comfort zone and exploring new experiences