@javelin/ecs
Advanced tools
Comparing version 0.8.0 to 0.9.0
@@ -6,2 +6,13 @@ # Change Log | ||
# 0.9.0 (2020-07-10) | ||
### Features | ||
* reverse system arguments ([4317036](https://github.com/3mcd/javelin/commit/431703646e866c3c7dcadbc8bb5202c6b02ab28c)) | ||
# 0.8.0 (2020-07-08) | ||
@@ -8,0 +19,0 @@ |
@@ -7,10 +7,6 @@ import { Component } from "./component"; | ||
export interface Archetype { | ||
table: ReadonlyArray<ReadonlyArray<Readonly<Component | null>>>; | ||
layout: ReadonlyArray<number>; | ||
entities: ReadonlyArray<number>; | ||
indices: ReadonlyArray<number>; | ||
/** | ||
* Insert an entity into the Archetype. | ||
* | ||
* @param entity Entity to associate components with | ||
* @param entity Subject entity | ||
* @param components Array of components | ||
@@ -23,6 +19,38 @@ * @returns void | ||
* | ||
* @param entity Entity to associate components with | ||
* @param entity Subject entity | ||
* @returns void | ||
*/ | ||
remove(entity: number): void; | ||
/** | ||
* Two-dimensional array of component type->component[] where each index | ||
* (column) of the component array corresponds to an entity. | ||
* | ||
* (index) 0 1 2 | ||
* (entity) 1 3 9 | ||
* (Position) [[ p, p, p ] | ||
* (Velocity) [ v, v, v ]] | ||
* | ||
* The index of each entity is tracked in the `indices array`. | ||
*/ | ||
readonly table: ReadonlyArray<ReadonlyArray<Readonly<Component | null>>>; | ||
/** | ||
* Array where each value is a component type and the index is the column of | ||
* the type's collection in the archetype table. | ||
*/ | ||
readonly layout: ReadonlyArray<number>; | ||
/** | ||
* Array of entities tracked by this archetype. Not used internally: | ||
* primarily a convenience for iteration/checks by consumers. | ||
*/ | ||
readonly entities: ReadonlyArray<number>; | ||
/** | ||
* Array where each index corresponds to an entity, and each value | ||
* corresponds to that entity's index in the component table. In the example | ||
* above, this array might look like: | ||
* | ||
* 1 3 9 | ||
* [empty, 0, empty, 1, empty x5, 2] | ||
* | ||
*/ | ||
readonly indices: ReadonlyArray<number>; | ||
} | ||
@@ -29,0 +57,0 @@ /** |
@@ -23,3 +23,4 @@ "use strict"; | ||
const component = components[i]; | ||
table[layout.indexOf(component._t)][head] = component; | ||
const componentTypeIndex = layout.indexOf(component._t); | ||
table[componentTypeIndex][head] = component; | ||
} | ||
@@ -29,5 +30,4 @@ entities[head] = entity; | ||
} | ||
let index; | ||
function remove(entity) { | ||
index = indices[entity]; | ||
const index = indices[entity]; | ||
if (index === head) { | ||
@@ -34,0 +34,0 @@ for (const components of table) |
@@ -16,3 +16,3 @@ import { AnySchema, Schema, PropsOfSchema } from "./schema/schema_types"; | ||
} & P; | ||
export declare type ComponentWithoutEntity<T extends number = number, P extends ComponentProps = ComponentProps> = { | ||
export declare type ComponentSpec<T extends number = number, P extends ComponentProps = ComponentProps> = { | ||
_t: T; | ||
@@ -19,0 +19,0 @@ } & P; |
@@ -6,3 +6,3 @@ "use strict"; | ||
matchEntity(entity, world) { | ||
return !world.isEphemeral(entity); | ||
return world.isCommitted(entity); | ||
}, | ||
@@ -9,0 +9,0 @@ matchComponent() { |
@@ -47,4 +47,13 @@ import { Component, ComponentOf, ComponentType } from "./component"; | ||
run(world: World): IterableIterator<SelectorResult<S>>; | ||
filter(filter: Filter | (() => Filter)): QueryLike<S>; | ||
length: number; | ||
/** | ||
* Narrow the results of this query using the provided filters. | ||
* | ||
* @param filters Filters to add | ||
*/ | ||
filter(...filters: (Filter | (() => Filter))[]): QueryLike<S>; | ||
/** | ||
* The length of the result set of this query, i.e. the number of components | ||
* in the query selector. | ||
*/ | ||
readonly length: number; | ||
} | ||
@@ -51,0 +60,0 @@ /** |
@@ -24,8 +24,10 @@ "use strict"; | ||
const filters = []; | ||
function filter(f) { | ||
const filter = typeof f === "function" ? f() : f; | ||
if (filters.indexOf(filter) > -1) { | ||
return query; | ||
function filter(...filtersToAdd) { | ||
for (const filter of filtersToAdd) { | ||
const f = typeof filter === "function" ? filter() : filter; | ||
if (filters.indexOf(f) > -1) { | ||
return query; | ||
} | ||
filters.push(f); | ||
} | ||
filters.push(filter); | ||
return query; | ||
@@ -32,0 +34,0 @@ } |
import { Archetype } from "./archetype"; | ||
import { Component, ComponentOf, ComponentType, ComponentWithoutEntity } from "./component"; | ||
import { Component, ComponentOf, ComponentType, ComponentSpec } from "./component"; | ||
import { ComponentFactoryLike } from "./helpers"; | ||
export interface Storage { | ||
/** | ||
* Collection of Archetypes in the world. | ||
*/ | ||
readonly archetypes: ReadonlyArray<Archetype>; | ||
/** | ||
* Create a new entity. | ||
@@ -16,3 +12,3 @@ * | ||
*/ | ||
create(entity: number, components: ComponentWithoutEntity[], tag?: number): number; | ||
create(entity: number, components: ComponentSpec[], tag?: number): number; | ||
/** | ||
@@ -24,3 +20,3 @@ * Insert components into an existing entity. | ||
*/ | ||
insert(entity: number, ...components: ComponentWithoutEntity[]): void; | ||
insert(entity: number, ...components: ComponentSpec[]): void; | ||
/** | ||
@@ -30,3 +26,3 @@ * Destroy an entity. Attempts to release pooled components if their types | ||
* | ||
* @param entity Entity to destroy | ||
* @param entity Subject entity | ||
*/ | ||
@@ -82,4 +78,8 @@ destroy(entity: number): void; | ||
registerComponentFactory(factory: ComponentFactoryLike): void; | ||
/** | ||
* Collection of Archetypes in the world. | ||
*/ | ||
readonly archetypes: ReadonlyArray<Archetype>; | ||
} | ||
export declare function createStorage(): Storage; | ||
//# sourceMappingURL=storage.d.ts.map |
@@ -116,3 +116,3 @@ "use strict"; | ||
function patch(component) { | ||
const { _e, _t } = component; | ||
const { _e: _e, _t: _t } = component; | ||
for (let i = 0; i < archetypes.length; i++) { | ||
@@ -119,0 +119,0 @@ const archetype = archetypes[i]; |
@@ -1,28 +0,111 @@ | ||
import { Component, ComponentWithoutEntity } from "./component"; | ||
import { Component, ComponentSpec } from "./component"; | ||
import { ComponentFactoryLike } from "./helpers"; | ||
import { QueryLike, Selector, SelectorResult } from "./query"; | ||
import { Storage } from "./storage"; | ||
import { QueryLike, Selector, SelectorResult } from "./query"; | ||
import { Mutable } from "./types"; | ||
import { ComponentFactoryLike } from "./helpers"; | ||
declare type QueryMethod = <S extends Selector>(query: QueryLike<S>) => IterableIterator<SelectorResult<S>>; | ||
export declare type World<T = any> = { | ||
export interface World<T = any> { | ||
/** | ||
* Move the world forward one tick by executing all systems in order with the | ||
* provided tick data. | ||
* | ||
* @param data Tick data | ||
*/ | ||
tick(data: T): void; | ||
/** | ||
* Register a system to be executed each tick. | ||
* | ||
* @param system | ||
*/ | ||
addSystem(system: System<T>): void; | ||
create(components: ReadonlyArray<ComponentWithoutEntity>, tags?: number): number; | ||
/** | ||
* Create an entity with a provided component makeup. | ||
* | ||
* @param components The new entity's components | ||
* @param tags The new entity's tags | ||
*/ | ||
create(components: ReadonlyArray<ComponentSpec>, tags?: number): number; | ||
/** | ||
* Add new components to a target entity. | ||
* | ||
* @param entity Subject entity | ||
* @param components Components to insert | ||
*/ | ||
insert(entity: number, ...components: ReadonlyArray<Component>): void; | ||
/** | ||
* Destroy an entity and de-reference its components. | ||
* | ||
* @param entity Subject entity | ||
*/ | ||
destroy(entity: number): void; | ||
created: ReadonlySet<number>; | ||
destroyed: ReadonlySet<number>; | ||
storage: Storage; | ||
query: QueryMethod; | ||
isEphemeral(entity: number): boolean; | ||
/** | ||
* Execute a query against this world. | ||
* | ||
* @param query Query to execute | ||
*/ | ||
query<S extends Selector>(query: QueryLike<S>): IterableIterator<SelectorResult<S>>; | ||
/** | ||
* Determine if an entity is committed; that is, it was neither added nor | ||
* removed during the previous tick. | ||
* | ||
* @param entity Subject entity | ||
*/ | ||
isCommitted(entity: number): boolean; | ||
/** | ||
* Add a bit flag to an entity's bitmask. | ||
* | ||
* @param entity Subject entity | ||
* @param tags Tags to add | ||
*/ | ||
addTag(entity: number, tags: number): void; | ||
/** | ||
* Remove a bit flag from an entity's bitmask. | ||
* | ||
* @param entity Subject entity | ||
* @param tags Tags to add | ||
*/ | ||
removeTag(entity: number, tags: number): void; | ||
/** | ||
* Determine if an entity's bitmask has a given bit flag. | ||
* | ||
* @param entity Subject entity | ||
* @param tags Tags to check for | ||
*/ | ||
hasTag(entity: number, tags: number): boolean; | ||
/** | ||
* Get a mutable reference to a component. | ||
* | ||
* @param component Subject component | ||
*/ | ||
mut<C extends Component>(component: C): Mutable<C>; | ||
/** | ||
* Register a component factory with the world, automatically pooling its | ||
* components. | ||
* @param factory Component factory | ||
*/ | ||
registerComponentFactory(factory: ComponentFactoryLike): void; | ||
registeredComponentFactories: ComponentFactoryLike[]; | ||
/** | ||
* Set of entities that were created during the previous tick. | ||
*/ | ||
readonly created: ReadonlySet<number>; | ||
/** | ||
* Set of entities that were destroyed during the previous tick. | ||
*/ | ||
readonly destroyed: ReadonlySet<number>; | ||
/** | ||
* Entity <-> Component storage. | ||
*/ | ||
readonly storage: Storage; | ||
/** | ||
* Array of registered component factories. | ||
*/ | ||
readonly registeredComponentFactories: ReadonlyArray<ComponentFactoryLike>; | ||
} | ||
export declare type System<T> = (world: World<T>, data: T) => void; | ||
declare type WorldOptions<T> = { | ||
systems?: System<T>[]; | ||
componentFactories?: ComponentFactoryLike[]; | ||
componentPoolSize?: number; | ||
}; | ||
export declare type System<T> = (data: T, world: World<T>) => void; | ||
export declare const createWorld: <T>(systems: System<T>[]) => World<T>; | ||
export declare const createWorld: <T>(options?: WorldOptions<T>) => World<T>; | ||
export {}; | ||
//# sourceMappingURL=world.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const stack_pool_1 = require("./pool/stack_pool"); | ||
const storage_1 = require("./storage"); | ||
const stack_pool_1 = require("./pool/stack_pool"); | ||
var WorldOpType; | ||
@@ -11,5 +11,6 @@ (function (WorldOpType) { | ||
})(WorldOpType || (WorldOpType = {})); | ||
exports.createWorld = (systems) => { | ||
exports.createWorld = (options = {}) => { | ||
const { systems = [], componentFactories = [], componentPoolSize = 1000, } = options; | ||
const ops = []; | ||
const opPool = stack_pool_1.createStackPool(() => [], op => op, 1000); | ||
const opPool = stack_pool_1.createStackPool(() => [], op => op, componentPoolSize); | ||
const storage = storage_1.createStorage(); | ||
@@ -56,3 +57,3 @@ const created = new Set(); | ||
for (let i = 0; i < systems.length; i++) { | ||
systems[i](data, world); | ||
systems[i](world, data); | ||
} | ||
@@ -87,4 +88,4 @@ } | ||
} | ||
function isEphemeral(entity) { | ||
return created.has(entity) || destroyed.has(entity); | ||
function isCommitted(entity) { | ||
return !(created.has(entity) || destroyed.has(entity)); | ||
} | ||
@@ -102,2 +103,3 @@ function query(q) { | ||
} | ||
componentFactories.forEach(registerComponentFactory); | ||
const { addTag, removeTag, hasTag } = storage; | ||
@@ -114,3 +116,3 @@ const world = { | ||
query, | ||
isEphemeral, | ||
isCommitted, | ||
addTag, | ||
@@ -117,0 +119,0 @@ removeTag, |
{ | ||
"name": "@javelin/ecs", | ||
"version": "0.8.0", | ||
"version": "0.9.0", | ||
"main": "dist/index.js", | ||
@@ -21,3 +21,3 @@ "license": "MIT", | ||
], | ||
"gitHead": "58c1c919f3669ed983574f8a84b55a0364174695" | ||
"gitHead": "1ab5e13fb25e203300a9229391938541f70c9b5f" | ||
} |
263
README.md
@@ -5,5 +5,9 @@ # `@javelin/ecs` | ||
## Docs | ||
https://3mcd.github.io/javelin | ||
## Primer | ||
ECS is a pattern commonly used in game development to associate components (state) with stateless entities (game objects). Systems then operate on collections of entities of shared composition. | ||
ECS is a pattern commonly used in game development to associate **components** (state) with stateless **entities** (game objects). **Systems** then operate on collections of entities of shared composition. | ||
@@ -23,239 +27,2 @@ For example, a system could add a `Burn` component to entities with `Position` and `Health` components when their position intersects with a lava pit. | ||
## Basics | ||
### Entity and component creation | ||
#### Creating entities | ||
Entities are integers. Components are associated with entities inside of a `World`. | ||
```ts | ||
import { createWorld } from "@javelin/ecs" | ||
const world = createWorld() | ||
``` | ||
Entities are created with the `world.create` method. | ||
```ts | ||
const entity = world.create([ | ||
{ _t: 1, x: 0, y: 0 }, // Position | ||
{ _t: 2, x: 0, y: 0 }, // Velocity | ||
]) | ||
``` | ||
Components are just plain objects; unremarkable, other than a few reserved properties: | ||
- `_t` is a unique integer identifying the component's **type** | ||
- `_e` references the **entity** the component is actively associated with | ||
- `_v` maintains the current **version** of the component, which is useful for change detection | ||
A position component assigned to entity `5` that has been modified three times might look like: | ||
```ts | ||
{ | ||
_t: 1, | ||
_e: 5, | ||
_v: 3, | ||
x: 10, | ||
y: 12, | ||
} | ||
``` | ||
The `createComponentFactory` helper is provided to make component creation easier. | ||
```ts | ||
import { createComponentFactory, number } from "@javelin/ecs" | ||
const Position = createComponentFactory({ | ||
type: 1, | ||
schema: { | ||
x: number, | ||
y: number, | ||
}, | ||
}) | ||
const position = Position.create() | ||
const entity = world.create([position]) | ||
``` | ||
#### Destroying entities | ||
Entities can be removed (and all components subsequently de-referenced) via the `world.destroy` method: | ||
```ts | ||
world.destroy(entity) | ||
``` | ||
Components created via factory are automatically pooled; however, you must manually release the component back to the pool when it should be discarded: | ||
```ts | ||
world.destroy(entity) | ||
Position.destroy(position) | ||
``` | ||
`World` can do this automatically if you register the component factory via the `world.registerComponentFactory` method. | ||
```ts | ||
world.registerComponentFactory(Position) | ||
world.destroy(entity) // position automatically released | ||
``` | ||
### Querying and iteration | ||
A system is just a function executed each simulation tick. Systems execute queries to access entities' components. | ||
Queries are created with the `createQuery` function, which takes one or more component types (or factories). | ||
```ts | ||
const players = createQuery(Position, Player) | ||
``` | ||
The query can then be executed for a given world: | ||
```ts | ||
for (const [position, player] of world.query(players)) { | ||
// render each player with a name tag | ||
draw(position, player.name) | ||
} | ||
``` | ||
### Filtering and change detection | ||
Queried components are readonly by default. A mutable copy of a component can be obtained via `mut(ComponentType)`. | ||
```ts | ||
import { query, mut } from "@javelin/ecs" | ||
const burning = query(mut(Health), Burn) | ||
for (const [health, burn] of world.query(burning)) { | ||
health.value -= burn.damagePerTick | ||
} | ||
``` | ||
Components are versioned as alluded to earlier. `world.mut` simply increments the component's `_v` property. If you are optimizing query performance and want to conditionally mutate a component (i.e. you are using a generic query), you can manually call `world.mut(component)` to obtain a mutable reference, e.g.: | ||
```ts | ||
import { query, mut } from "@javelin/ecs" | ||
const burning = query(Health, Burn) | ||
for (const [health, burn] of world.query(burning)) { | ||
world.mut(health).value -= burn.damagePerTick | ||
} | ||
``` | ||
`changed` produces a filter that excludes entities whose components haven't changed since the entity was last iterated with the filter instance. This filter uses the component's version (`_v`) to this end. | ||
```ts | ||
import { changed, query } from "@javelin/ecs" | ||
// ... | ||
const healthy = query(Player, Health).filter(changed(Health)) | ||
for (const [health] of world.query(healthy)) { | ||
// `health` has changed since last tick | ||
} | ||
``` | ||
A query can take one or more filters as arguments to `filter`. | ||
```ts | ||
query.filter(changed, awake, ...); | ||
``` | ||
The ECS also provides the following filters | ||
- `committed` ignores "ephemeral" entities, i.e. entities that were created or destroyed last tick | ||
- `created` detects newly created entities | ||
- `destroyed` detects recently destroyed entities | ||
- `tag` isolates entities by tags, which are discussed below | ||
### Tagging | ||
Entities can be tagged with bit flags via the `world.addTag` method. | ||
```ts | ||
enum Tags { | ||
Awake = 1, | ||
Dying = 2, | ||
} | ||
world.addTag(entity, Tags.Awake | Tags.Dying) | ||
``` | ||
The `world.hasTag` method can be used to check if an entity has a tag. `world.removeTag` removes tags from an entity. | ||
```ts | ||
world.hasTag(entity, Tags.Awake) // -> true | ||
world.hasTag(entity, Tags.Dying) // -> true | ||
world.removeTag(entity, Tags.Awake) | ||
world.hasTag(entity, Tags.Awake) // -> false | ||
``` | ||
`tag` produces a filter that will exclude entities which do not have the provided tag(s): | ||
```ts | ||
enum Tags { | ||
Nasty = 2 ** 0, | ||
Goopy = 2 ** 1, | ||
} | ||
const nastyAndGoopy = createQuery(Player).filter(tag(Tags.Nasty | Tags.Goopy)) | ||
for (const [player] of world.query(nastyAndGoopy)) { | ||
// `player` belongs to an entity with Nasty and Goopy tags | ||
} | ||
``` | ||
## Common Pitfalls | ||
### Storing query results | ||
The tuple of components yielded by `world.query()` is re-used each iteration. This means that you can't store the results of a query for use later like this: | ||
```ts | ||
const results = [] | ||
for (const s of world.query(shocked)) { | ||
results.push(s) | ||
} | ||
// Every index of `results` corresponds to the same array! | ||
``` | ||
The same applies to `Array.from()`, or any other method that expands an iterator into another container. If you _do_ need to store components between queries (e.g. you are optimizing a nested query), you could push the components of interest into a temporary array, e.g. | ||
```ts | ||
const shocked = [] | ||
for (const [enemy] of world.query(shocked)) { | ||
shocked.push(enemy) | ||
} | ||
``` | ||
Try nested queries before you prematurely optimize. Iteration is pretty fast thanks to Archetypes, and a nested query will probably perform fine. | ||
### Filter state | ||
A filter does not have access to the query that executed it, meaning it can't track state for multiple queries. For example, if two queries use the same `changed` filter, no entities will be yielded by the second query unless entities were created between the first and second queries. | ||
```ts | ||
const moved = world.query(createQuery(Position).filter(changed)) | ||
for (const [position] of world.query(moved)) // 100 iterations | ||
for (const [position] of world.query(moved)) // 0 iterations | ||
``` | ||
The solution is to simply use a unique filter per query. | ||
```ts | ||
const moved1 = world.query(createQuery(Position).filter(changed)) | ||
const moved2 = world.query(createQuery(Position).filter(changed)) | ||
for (const [position] of world.query(moved1)) // 100 iterations | ||
for (const [position] of world.query(moved2)) // 100 iterations | ||
``` | ||
## Performance | ||
@@ -271,17 +38,25 @@ | ||
======================================== | ||
create: 129.333ms | ||
run: 14684.131ms | ||
destroy: 19.823ms | ||
entities | 300000 | ||
components | 4 | ||
queries | 4 | ||
ticks | 100 | ||
iter_tick | 353500 | ||
avg_tick | 13.7ms | ||
ticks | 1000 | ||
iter | 350350000 | ||
iter_tick | 350350 | ||
avg_tick | 14.982ms | ||
======================================== | ||
perf_storage_pooled | ||
======================================== | ||
create: 133.237ms | ||
run: 16789.444ms | ||
destroy: 25.286ms | ||
entities | 300000 | ||
components | 4 | ||
queries | 4 | ||
ticks | 100 | ||
iter_tick | 353500 | ||
avg_tick | 14.62ms | ||
ticks | 1000 | ||
iter | 350350000 | ||
iter_tick | 350350 | ||
avg_tick | 16.789ms | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1254
88749
60