🍬 Miski ECS
Miski: Quechuan adjective meaning "sweet".
ECS: Entity-Component-System; a software architecture pattern.
Miski ECS: A sweet ECS architecture written in Typescript.
Contents
- Purpose
- Features
- Importing
- API Reference
- Docs
- Demos
- Benchmarks
- Building
- To-Do
- Contributing
- Feature Requests
- Acknowledgements
- License
Purpose
Miski's purpose is to provide a stable, developer-friendly ECS architecture for modern Javascript projects.
Goals
- To provide good and predictable performance
- To provide a developer-friendly API
- To provide a clean, readable, self-documenting, open-source codebase
Not Goals
Because Miski is designed to be used inside your own projects, we let you configure bundling and performance tuning to suit your needs, therefore the following are not priorities of this project:
- To be the fastest or smallest ECS on the web
- To provide polyfills, workarounds, or older browser support for modern ECMAScript features
Features
- Simple, developer-friendly, human-readable API
- Modern modular ES2020 data-oriented Typescript codebase
- Fast, cache-friendly ArrayBuffer-based component data storage
- Ability to use more than 32 components in one world using Uint32Array bitfields
- Ability to limit the number of entities a component can be added to
- Basic serialization methods (
world.load
&world.save
) - Use
component.changed
to get an iterator of entities whose properties were changed viacomponent.proxy
- Define components, systems and queries once, reuse them across multiple worlds
-
AND
,OR
,NOT
operators in Queries -
world.getQueryEntered
&world.getQueryExited
methods - ~5kb gzipped
- No dependencies
- MIT license
Importing
The javascript module miski.min.js
is found in the ./dist
folder, along with a sourcemap file and typescript definitions .d.ts
file.
import { Component, Query, System, World } from './miski.min.js';
See API Reference below for a complete list of named exports.
Various type definitions are also exported - see index.ts
for a complete list.
Quick Start API Reference
Below are the essentials of the Miski API. For full documentation see Docs
below.
Each concept in this reference builds on the previous concept, it should be read in order.
World
The world object is the primary container for all things Miski.
We can create a new world like so:
const world = new World({
capacity: 1000, // The maximum number of entities to allow in the world
components: [
positionComponent, // We'll create this in the components section below
],
});
The world requires frequent maintenance (i.e., once per frame):
world.refresh();
Components
A component is a data structure that gives entities their state.
Components can be created once and used across multiple worlds.
constants.ts
.
For example, to create a 2d position component:
interface Vec2 = { x: number, y: number };
const positionComponent = new Component<Vec2>({
name: "position",
schema: {
x: Float32Array,
y: Float32Array,
},
});
We can create a tag component by omitting the schema object:
const activeComponent = new Component<null>({
name: "active"
});
By default a component can be added to as many entities as the world's capacity, we can change this behavior like so:
const player = new Component<null>({
name: "player",
maxEntities: 1,
});
We can add and remove components from entities like so:
// Create the adder factory:
const addPositionToEntity = world.addComponentsToEntity(positionComponent); // you can provide multiple components here.
// Add the component to an entity:
addPositionToEntity(entity);
// Create the remover factory:
const removePositionFromEntity = world.removeComponentFromEntity(positionComponent) // you can provide multiple components here.
// Remove the component from an entity:
removePositionFromEntity(entity);
We can also test if entities have components:
// Has a single component?
const hasPosition: boolean = world.hasComponent(positionComponent)(entity);
// Has multiple components?
const hasXYZ: boolean[] = world.hasComponents(positionComponent, ...)(entity);
To access the component's data relevant to a specific world, we have to get the ComponentInstance, like so:
// returns ComponentInstance<T> or undefined
const positionInstance = world.getComponentInstance(positionComponent);
// For multiple components: (ComponentInstance<unknown> | undefined)[]
const instances = world.getComponentInstances(positionComponent, ...);
The component instance is accessible quickly using Systems (see below).
Once we have the component instance we can modify entity properties.
There are two ways to do this:
The first is quick but unsafe:
positionInstance.x[entity] = 1;
The second is slower but safer:
positionInstance.proxy.entity = entity;
positionInstance.proxy.x = 1;
The second way, using .proxy
has the advantage of also adding the entity to the .changed
array as well as performing some basic typeguarding.
For example:
positionInstance.x[101] = 1;
positionInstance.proxy.entity = 444;
positionInstance.proxy.x = 1;
[...positionInstance.changed] = [444] // does not include entity 101
N.B. The .changed
array is reset with every world.refresh()
.
You can also access the changed entities of a component like so:
const changed = world.getChangedFromComponents(positionComponent);
Entities
Entities are just integers. They are essentially indexes or pointers into various arrays in the world.
// Create (will return undefined if no entities are available)
const entity = world.createEntity();
// Destroy
world.destroyEntity(entity);
// Test if entity is active in the world
world.isEntityActive(entity);
// Test if an entity is valid in the world
world.isValidEntity(4235); // will return false if the world capacity is 1000 as above
// Get the number of active entities in a world
const active = world.residents;
// Get the number of remaining available entities in a world
const available = world.available;
// Get all the component properties for an entity in a world
const props = world.getEntityProperties(entity);
Queries
Queries help us to find relationships between entities and components.
const positionQuery = new Query({
all: [positionComponent],
any: [...],
none: [...],
});
We can then access the entities and components which match our query:
const components = world.getQueryComponents(positionQuery);
const entities = world.getQueryEntities(positionQuery);
We can also access entities which have entered or exited the query since the last world.refresh()
:
const entered = world.getQueryEntered(positionQuery);
const exited = world.getQueryExited(positionQuery);
getQueryEntities
, getQueryEntered
, and getQueryExited
optionally take an array as a second argument to avoid creating a new underlying array each time, reducing GC cost.
Systems
Systems are functions which use queries to modify entity properties.
It is recommended (but not necessary) that all data mutation take place inside a system.
const positionSystemPrefab = new System({
query: positionQuery,
system: (components, entities) => {
const { position } = components;
const { x, y } = position;
for (const entity of entities) {
x[entity] += 1;
y[entity] += 1;
}
},
});
Once created a system can be initialized into worlds which helps with caching etc.:
const positionSystem = positionSystemPrefab.init(world);
Once initialized, systems are then used just like normal fuctions:
positionSystem();
Docs
See ./docs
or the live docs page on Github.
Demos
See ./demo
for demo code or the demo page for live examples.
Building
To build Miski from source, run:
git clone https://github.com/phughesmcr/Miski.git
cd Miski
npm install
npm run build
To-Do
Before Beta
- Finalise API
- Write comprehensive tests
- Write consistent code documentation throughout
Before 1.0.0
- Optimise performance
- Consistent code style throughout
- Object pooling where necessary
Future
- Multithreading support / playing nicely with WebWorkers / SharedArrayBuffers
- Proper Deno support
Contributing
Contributions are welcome and invited. See CONTRIBUTING.md
for details.
If you want inspiration, there are plenty of /** @todo */
comments in the code.
Feature Requests
Feature requests are welcome and invited. Please open an issue on Github to make a request.
Acknowledgements
Miski is inspired by ape-ecs, BECSY, bitECS, ECSY, Geotic, HECS, Wolf ECS, and Structurae.
License
Miski ECS is released under the MIT license. See LICENSE
for further details.
© 2021-2022 The Miski Authors. All rights reserved.
See AUTHORS.md
for author details.