Miski: Quechuan adjective meaning "sweet".
ECS: Entity-Component-System; a software architecture pattern.
Miski ECS: A sweet ECS architecture written in Typescript.
⚠️ Miski is currently in alpha. Expect breaking changes every version until beta.
Miski's purpose is to provide a stable, developer-friendly ECS architecture for modern Javascript projects.
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:
world.load
& world.save
)component.changed
to get an iterator of entities whose properties were changed via component.proxy
AND
,OR
,NOT
operators in Queriesworld.getQueryEntered
& world.getQueryExited
methodsThe 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.
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.
The world object is the primary container for all things Miski.
⚠️ Components cannot be added to a world after its creation.
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();
A component is a data structure that gives entities their state.
Components can be created once and used across multiple worlds.
⚠️ There are a few names you cannot use for components or their schema properties. See 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 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 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 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();
See ./docs
or the live docs page on Github.
See ./demo
for demo code or the demo page for live examples.
To build Miski from source, run:
git clone https://github.com/phughesmcr/Miski.git
cd Miski
npm install
npm run build
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 are welcome and invited. Please open an issue on Github to make a request.
Miski is inspired by ape-ecs, BECSY, bitECS, ECSY, Geotic, HECS, Wolf ECS, and Structurae.
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.
Generated using TypeDoc