Miski: Quechuan adjective meaning “sweet”.
ECS: Entity-Component-System; a software architecture pattern.
Miski ECS: A sweet, high-performance ECS library written in Typescript.
See jsr.io/@phughesmcr/miski for complete documentation.
Note: The runtime versions above are indicative, Miski should work on any modern JavaScript runtime.
Miski’s purpose is to provide a performant, stable, developer-friendly ECS architecture for modern web projects.
Since ECS libraries are primarily used in games and other performance-critical applications, performant here means:
ArrayBuffer and WeakMap to provide fast, cache-friendly component storage, querying, and iteration.Stable here means:
Developer-friendly here means:
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:
AND,OR,NOT operators in Queriesworld.getQueryEntered & world.getQueryExited methodscomponent.changed to get an iterator of entities whose properties were changed via component.proxynpx jsr add @phughesmcr/miski
import { World, ... } from "@phughesmcr/miski";
deno add jsr:@phughesmcr/miski
import { World, ... } from "@phughesmcr/miski";
bunx jsr add @phughesmcr/miski
import { World, ... } from "@phughesmcr/miski";
Below are the essentials of the Miski API. For full API documentation see jsr.io/@phughesmcr/miski.
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.
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
],
});
⚠️ Components cannot be added to a world after its creation.
ℹ️ The world requires frequent maintenance (usually 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.
For example, to create a 2d position component:
// Optional schema:
type Vec2 = { x: Float32ArrayConstructor, y: Float32ArrayConstructor }; // defines what input we want (number only)
const positionComponent = new Component<Vec2>({
// ⚠️ There are some names you cannot use for components or their schema properties.
// You can use `isValidName()` to check if a name is valid.
name: "position",
// The schema relates to the input type above, in this case Vec2.
// It defines how we want to store the expected datatype (number).
// Since we know a Vec2 requires X and Y to be Float32Array, we can define the schema like so:
schema: {
x: Float32Array,
y: Float32Array,
},
});
We can create a tag component by omitting the schema object and (optionally) providing a null type:
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 behaviour like so:
const player = new Component<null>({
name: "player",
maxEntities: 1,
});
We can add and remove components from entities like so:
// Add the component to an entity:
world.components.addToEntity(positionComponent, entity);
// Add with initial data:
world.components.addToEntity(positionComponent, entity, { x: 10, y: 20 });
// Remove the component from an entity:
world.components.removeFromEntity(positionComponent, entity);
We can also test if entities have components:
// Check if an entity has a component
const hasPosition: boolean = world.components.entityHas(positionComponent, entity);
To access the component’s data from a specific world, we have to get the ComponentInstance, like so:
// returns ComponentInstance<T> or undefined
const positionInstance = world.components.getInstance(positionComponent);
// For multiple components:
const instances = world.components.getInstances([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 (no change tracking):
positionInstance.storage.partitions.x[entity] = 1;
The second is slower but safer (with change tracking and type guards):
positionInstance.proxy.entity = entity;
positionInstance.proxy.x = 1;
The second way, using .proxy has the advantage of also adding the entity to the changed tracking as well as performing some basic typeguarding.
For example:
// Direct storage access - no change tracking
positionInstance.storage.partitions.x[101] = 1;
// Proxy access - with change tracking
positionInstance.proxy.entity = 444;
positionInstance.proxy.x = 1;
// Only entity 444 appears in changed tracking
const changed = world.components.getChanged(positionComponent);
for (const entity of changed) {
console.log(entity); // 444 only, not 101
}
ℹ️ The changed tracking is reset with every world.refresh().
You can also access the changed entities of a component like so:
const changed = world.components.getChanged(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.entities.create();
// Destroy
world.entities.destroy(entity);
// Test if entity is active in the world
world.entities.isActive(entity);
// Test if an entity is valid in the world
world.entities.isEntity(4235); // will return false if the world capacity is 1000 as above
// Get the number of active entities in a world
const active = world.entities.getActiveCount();
// Get the number of remaining available entities in a world
const available = world.entities.getAvailableCount();
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.components.query(positionQuery);
const entities = world.entities.query(positionQuery);
We can also access entities which have entered or exited the query since the last world.refresh():
const entered = world.archetypes.queryEntered(positionQuery);
const exited = world.archetypes.queryExited(positionQuery);
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 positionSystem = new System({
name: "positionSystem",
query: positionQuery,
callback: (components, entities) => {
const { position } = components;
const { x, y } = position.storage.partitions;
for (const entity of entities) {
x[entity] += 1;
y[entity] += 1;
}
},
});
Once created a system can be registered with the world:
const systemInstance = world.systems.create(positionSystem);
Once registered, systems are then called like normal functions:
systemInstance();
Contributions are welcome and encouraged. The aim of the project is performance - both in terms of speed and GC allocation pressure.
Please run deno test, deno bench and deno task prep to run tests, benchmarks, and formatting before committing.
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 is released under the MIT license. See LICENSE for further details.
© 2024 The Miski Authors. All rights reserved.
See AUTHORS.md for author details.
**