An Entity Component System (ECS) implementation in TypeScript, extensible, working with any renderer, type safe and composable ๐น๏ธ
Defines a component class with a tag and properties.
In the example below, the component is tagged as "Position"
and has two properties: x
and y
.
export class Position extends Component("Position")<{
x: number;
y: number;
}> {}
You can then create instances of the component like any other class:
const position = new Position({ x: 10, y: 20 });
You can also copy the properties of the component using the spread operator:
const position = new Position({ x: 10, y: 20 });
const newPosition = new Position({ ...position, x: 30 });
Component classes are mutable, so you can change the properties of the component inside a system.
Defines a systems' factory. It accepts two generic parameters:
- A union of all the tags of the systems in the world
- An
EventMap
of all the possible emitted events in the world
import { type EntityId, type EventMap, System } from "@typeonce/ecs";
export const FoodEatenEvent = Symbol("FoodEaten");
export interface GameEventMap extends EventMap {
[FoodEatenEvent]: { entityId: EntityId };
}
export type SystemTags =
| "Movement"
| "PostMovement"
| "Render"
| "Input"
| "Collision"
| "ApplyMovement";
const SystemFactory = System<SystemTags, GameEventMap>();
SystemFactory
is then used to create systems. A system is defined as a class:
- The generic parameter defines the input type required to create an instance of the system
- The first parameter is the tag of the class (must be included in the
SystemTags
used when creatingSystemFactory
fromSystem
) - The second parameter requires an
execute
function and an optionaldependencies
execute
is the implementation of the systemdependencies
defines the tags of the systems that are required to execute before the current one
const SystemFactory = System<SystemTags, GameEventMap>();
export class CollisionSystem extends SystemFactory<{
// ๐ Input required
gridSize: { width: number; height: number };
}>("Collision", {
dependencies: ["Movement"],
execute: (params) => {
// ๐ System logic
},
}) {}
params
inside execute
provide utility functions to manage entities, components, and systems in the game:
deltaTime
world
: Reference to current instance of the game worldaddSystem
: Adds one or more systems to the gamecreateEntity
: Creates an entity and returns itsEntityId
(number
)destroyEntity
: Removes an entity from itsEntityId
addComponent
: Adds one or more components to an entity from itsEntityId
removeComponent
: Removes one or more components to an entity from itsEntityId
getComponentRequired
: Gets one or more components from an entity from itsEntityId
. The components are expected to be found, otherwise the function will throw anError
getComponent
: Gets one or more components from an entity from itsEntityId
(not required, it may returnundefined
)emit
: Emits an event that something happened in the gamepoll
: Reads events emitted by other systems during the current update cycle
An actual instance of World
is created using the ECS class from ECS.create
. You can provide two generic parameters (same as System
):
- A union of all the tags of the systems in the world
- An
EventMap
of all the possible emitted events in the world
You can implement a function to initialize the game using the following provided utility functions:
addSystem
createEntity
addComponent
Defines a map of components used to query the world for all the entities that have the defined components attached.
It can be defined outside a system and reused between them.
// A query for all the entities with both `Position` and `Movement` components
const moving = query({ position: Position, movement: Movement });
You can then provide an instance of World
to extract all the entities:
const moving = query({ position: Position, movement: Movement });
export class MovementSystem extends SystemFactory<{}>("Movement", {
execute: ({ world }) => {
moving(world).forEach(({ position, movement, entityId }) => {
// Do something with each entity and its `position` and `movement` components
});
},
}) {}
Defines a map of components used to query the world for all the entities that have the defined components attached (same as query
).
It requires at least one entity to exist in the game, otherwise executing the query will throw (returns a non-empty array of entities).
This is useful to extract a single entity you know must exist in the game, for example a "player" entity.
It can be defined outside a system and reused between them.
// A query for all the entities with both `Movement` and `Player` components
const playerQuery = queryRequired({ movement: Movement, player: Player });
You can then provide an instance of World
to extract all the entities:
const playerQuery = queryRequired({ movement: Movement, player: Player });
export class InputSystem extends SystemFactory<{}>("Input", {
execute: ({ world }) => {
// ๐ The first element in the array is guaranteed to exist (`[0]`)
const { movement, player, entityId } = playerQuery(world)[0];
},
}) {}