Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 123 additions & 51 deletions DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const component = new HealthComponent();
console.log(component.value); // '100'
```

> Note: TypeScript users can declare propertiess with [decorators](#decorators).
> NOTE: TypeScript users can declare propertiess with [decorators](#decorators).

The `DataComponent` class exposes a simple interface:

Expand Down Expand Up @@ -174,68 +174,36 @@ custom logic.

Coming soon.

# Systems & Queries
# Systems

Systems are run when the world ticks. They are schedule to run one after
the other, one group at a time. Running systems can query entities based on the components they hold.

## Example
Systems are run when the world ticks. They are scheduled to run one after
the other, one group at a time. Systems can query entities based on the components they hold.

```js
import { NumberProp, System } from 'ecstra';

class TransformComponent extends ComponentData { }
TransformComponent.Properties = {
x: NumberProp(), // Defaults to 0.
y: NumberProp(), // Defaults to 0.
};


class SpeedComponent extends ComponentData { }
SpeedComponent.Properties = {
value: NumberProp(150.0)
};
import { System } from 'ecstra';

class PhysicsSystem extends System {

init() {
// Triggered on initialization. Note: you can also use the
// constructor for that.
}

execute(delta) {
// `query` contains **every** entity that has at least the
// components `SpeedComponent` and `TransformComponent`.
const query = this.queries.entitiesWithBox;
// Loops over every entity.
query.execute((entity) => {
const transform = entity.write(TransformComponent);
const speed = entity.read(SpeedComponent);
transform.y = Math.max(0.0, transform.y - speed.value * delta);
});
// Performs update logic here.
}

dispose() {
// Triggered when system is removed from the world.
}

}
PhysicsSystem.Queries = {
entitiesWithBox: [ SpeedComponent, TransformComponent ]
};
```

The `execute()` method is automatically called. This is where most (all if possible) of your logic should happen.

The `Queries` static properties list all the queries you want to
cache. Queries are created when the system is instantiated, and are
cached until the system is unregistered.

Queries can also specify that they want to deal with entities that
**do not** have a given component:

```js
import { Not } from 'ecstra';

PhysicsSystem.Queries = {
entitiesWithBoxThatArentPlayers: [
SpeedComponent,
TransformComponent,
Not(PlayerComponent)
]
};
```
Systems have the following lifecycle:
* `init()` → Triggered upon system instanciation in the world
* `execute()` → Triggered when the world execute
* `dispose()` → Triggered when system is destroyed by the world

## Order

Expand Down Expand Up @@ -298,6 +266,110 @@ system.group.sort();

```

# Queries

System may have a `Queries` static properties that list all the queries you want to
cache. Queries are created upon system instanciation, and are
cached until the system is unregistered.

```js
import { NumberProp, System } from 'ecstra';

class TransformComponent extends ComponentData { }
TransformComponent.Properties = {
x: NumberProp(), // Defaults to 0.
y: NumberProp(), // Defaults to 0.
};

class SpeedComponent extends ComponentData { }
SpeedComponent.Properties = {
value: NumberProp(150.0)
};

class PhysicsSystem extends System {

execute(delta) {
// `query` contains **every** entity that has at least the
// components `SpeedComponent` and `TransformComponent`.
const query = this.queries.entitiesWithBox;
// Loops over every entity.
query.execute((entity) => {
const transform = entity.write(TransformComponent);
const speed = entity.read(SpeedComponent);
transform.y = Math.max(0.0, transform.y - speed.value * delta);
});
}

}
// The static property `Queries` list the query you want to automatically
// create with the system.
PhysicsSystem.Queries = {
// The `entitiesWithBox` matches every entity with the `SpeedComponent` and
// `TransformComponent` components.
entitiesWithBox: [ SpeedComponent, TransformComponent ]
};
```

## Operators

### Not

Queries can also specify that they want to deal with entities that
**do not** have a given component:

```js
import { Not } from 'ecstra';

...

PhysicsSystem.Queries = {
// Matches entities with `SpeedComponent` and `TransformComponent but
// without `PlayerComponent`.
entitiesWithBoxThatArentPlayers: [
SpeedComponent,
TransformComponent,
Not(PlayerComponent)
]
};
```

## Events

A query will notifiy when a new entity is matching its component
layout:

```js
class MySystem extends System {

init() {
this.queries.myQuery.onEntityAdded = () => {
// Triggered when a new entity matches the component layout of the
// query `myQuery`.
};
this.queries.myQuery.onEntityRemoved = () => {
// Triggered when an entity that was previously matching query isn't
// matching anymore.
};
}

}
MySystem.Queries = {
myQuery: [ ... ]
}
```

You can use those two events to perform initialization and disposal of
resources.

### Events Order

Those events are **synchronous** and can be called in **any** order. If you
have two queries, never assumes the `onEntityAdded` and `onEntityRemoved` events
of one will be triggered before the other.

> NOTE: the current behaviour could be later changed in the library if
> events **must** be based on the systems execution order.

# Decorators

## ComponentData
Expand Down
67 changes: 67 additions & 0 deletions src/data/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Nullable } from '../types';

export class Observer<T = void> {
public id: Nullable<string> = null;
public autoRemove = false;
public callback: ObserverCallback<T> = () => {
/** Empty. */
};

public constructor(cb?: ObserverCallback<T>) {
if (cb) {
this.callback = cb;
}
}
}

export class Observable<T = void> {
/** @hidden */
private _observers: Observer<T>[] = [];

public observe(observer: Observer<T>): this {
this._observers.push(observer);
return this;
}

public unobserve(observer: Observer<T>): this {
const index = this._observers.indexOf(observer);
if (index >= 0) {
this._observers.splice(index, 1);
}
return this;
}

public unobserveFn(cb: ObserverCallback<T>): this {
const observers = this._observers;
for (let i = 0; i < observers.length; ++i) {
if (observers[i].callback === cb) {
observers.splice(i, 1);
return this;
}
}
return this;
}

public unobserveId(id: string): this {
const observers = this._observers;
for (let i = observers.length - 1; i >= 0; --i) {
if (observers[i].id === id) {
observers.splice(i, 1);
}
}
return this;
}

public notify(data: T): void {
const observers = this._observers;
for (const o of observers) {
o.callback(data);
}
}

public get count(): number {
return this._observers.length;
}
}

type ObserverCallback<T> = (data: T) => void;
37 changes: 37 additions & 0 deletions src/internals/archetype.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
import { Observable } from '../data/observer.js';
import { Entity } from '../entity.js';
import { ComponentClass } from '../types';

export class Archetype<E extends Entity> {
public readonly entities: E[];

private readonly _components: Set<ComponentClass>;
private readonly _hash: string;
private readonly _onEntityAdded: Observable<E>;
private readonly _onEntityRemoved: Observable<E>;

public constructor(components: ComponentClass[], hash: string) {
this.entities = [];
this._hash = hash;
this._components = new Set(components);
this._onEntityAdded = new Observable();
this._onEntityRemoved = new Observable();
}

public add(entity: E): void {
entity._indexInArchetype = this.entities.length;
entity._archetype = this;
this.entities.push(entity);
this._onEntityAdded.notify(entity);
}

public remove(entity: E): void {
const entities = this.entities;
// Move last entity to removed location.
if (entities.length > 1) {
const last = entities[entities.length - 1];
last._indexInArchetype = entity._indexInArchetype;
entities[entity._indexInArchetype] = last;
entities.pop();
} else {
entities.length = 0;
}
entity._archetype = null;
entity._indexInArchetype = -1;
this._onEntityRemoved.notify(entity);
}

public hasEntity(entity: E): boolean {
Expand All @@ -27,4 +56,12 @@ export class Archetype<E extends Entity> {
public get empty(): boolean {
return this.entities.length === 0;
}

public get onEntityAdded(): Observable<E> {
return this._onEntityAdded;
}

public get onEntityRemoved(): Observable<E> {
return this._onEntityRemoved;
}
}
31 changes: 4 additions & 27 deletions src/internals/component-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,10 @@ export class ComponentManager<WorldType extends World> {
entity._archetype = this._emptyArchetype;
}

public destroyEntity(entity: Entity): void {
public destroyEntity(entity: EntityOf<WorldType>): void {
const archetype = entity.archetype;
if (archetype) {
archetype.entities.splice(archetype.entities.indexOf(entity), 1);
entity._archetype = null;
// @todo: that may not be really efficient if an archetype is always
// composed of one entity getting attached / dettached.
if (archetype.entities.length === 0) {
this.archetypes.delete(archetype.hash);
}
this._removeEntityFromArchetype(entity);
}
}

Expand Down Expand Up @@ -193,29 +187,12 @@ export class ComponentManager<WorldType extends World> {
const archetype = this.archetypes.get(hash) as Archetype<
EntityOf<WorldType>
>;
const entities = archetype.entities;
entity._indexInArchetype = entities.length;
entity._archetype = archetype;
entities.push(entity);
archetype.add(entity);
}

private _removeEntityFromArchetype(entity: EntityOf<WorldType>): void {
const archetype = entity.archetype as Archetype<EntityOf<WorldType>>;
const entities = archetype.entities;

// Move last entity to removed location.
if (entities.length > 1) {
const last = entities[entities.length - 1];
last._indexInArchetype = entity._indexInArchetype;
entities[entity._indexInArchetype] = last;
entities.pop();
} else {
entities.length = 0;
}

entity._archetype = null;
entity._indexInArchetype = -1;

archetype.remove(entity);
// @todo: that may not be really efficient if an archetype is always
// composed of one entity getting attached / dettached.
if (archetype !== this._emptyArchetype && archetype.empty) {
Expand Down
Loading