Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"test": "ava",
"test:coverage": "c8 check-coverage --lines 90 --functions 90 --branches 90 npm test",
"coverage": "c8 npm test",
"pretty": "prettier ./src/ ./test ./benchmark --write",
"pretty": "prettier -c './src/**' './test/**' './benchmark/**' --write",
"copy:to:dist": "node --experimental-modules ./scripts/copy-to-dist.js"
},
"devDependencies": {
Expand Down
134 changes: 134 additions & 0 deletions src/command-buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Entity } from './entity.js';
import { SequentialPool } from './pool/sequential-pool.js';
import { ComponentClass, Nullable, Option, PropertiesOf } from './types';
import { Component } from './component.js';

enum CommandType {
None = 0,
Remove = 2,
AddComponent = 3,
RemoveComponent = 4
}

class Command {
public type: CommandType;
public entity: Entity;
public componentClass: Nullable<ComponentClass>;
public componentOptions: Option<PropertiesOf<Component>>;

public constructor() {
this.type = CommandType.None;
this.entity = null!;
this.componentClass = null;
this.componentOptions = undefined;
}

public init(entity: Entity): Command {
this.type = CommandType.None;
this.entity = entity;
this.componentClass = null;
this.componentOptions = undefined;
return this;
}
}

/**
* A command buffer is used to save operations to apply on entities, such as
* destruction / creation of entity, or addition / removal of component.
*
* The user registes command into the command buffer, and later applies the
* saved commands using the `playback()` method.
*
* Command buffers are useful to defer modification applies to the world and
* its entities. In a multithreaded environment, they would also allow to
* better synchronization read & write operations applied to the world
*
* @category entity
*/
export class CommandBuffer {
/** @hidden */
private _pool: SequentialPool<Command>;

/** @hidden */
private _executor = (cmd: Command): void => {
// @todo: batch entity removal in world if possible.
// Right now, adding / removing multiple components will be slow!
switch (cmd.type) {
case CommandType.Remove:
cmd.entity.destroy();
break;
case CommandType.AddComponent:
// @todo: batch archetype removal in world if possible.
cmd.entity.add(
cmd.componentClass as ComponentClass,
cmd.componentOptions
);
break;
case CommandType.RemoveComponent:
// @todo: batch archetype removal in world if possible.
cmd.entity.remove(cmd.componentClass as ComponentClass);
break;
}
};

public constructor() {
this._pool = new SequentialPool(Command);
}

/**
* Registers a command to remove a given entity.
*
* On playback, the target entity will be removed
*
* @param entity - Entity to later remove
*/
public remove(entity: Entity): void {
const cmd = this._pool.acquire().init(entity);
cmd.type = CommandType.Remove;
}

/**
* Registers a command to add a component to a given entity.
*
* On playback, the given component will be added to the target entity
*
* @param entity - Entity to later remove
* @param componentClass - The class of the component to add
*/
public addComponent<T extends Component>(
entity: Entity,
componentClass: ComponentClass<T>,
opts?: PropertiesOf<T>
): void {
const cmd = this._pool.acquire().init(entity);
cmd.type = CommandType.AddComponent;

// @todo: create API point in World to pre-create a component.
// This is tricky though because if the command buffer is never called, the
// component pool will never be freed.
cmd.componentClass = componentClass;
cmd.componentOptions = opts;
}

/**
* Registers a command to remove a component from a given entity.
*
* On playback, the given component will be removed from the target entity
*
* @param entity - Entity to later remove
* @param componentClass - The class of the component to add
*/
public removeComponent(entity: Entity, componentClass: ComponentClass): void {
const cmd = this._pool.acquire().init(entity);
cmd.type = CommandType.RemoveComponent;
cmd.componentClass = componentClass;
}

/**
* Applies the list of registered commands to the world
*/
public playback(): void {
this._pool.execute(this._executor);
this._pool.release();
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export { ComponentRegisterOptions } from './internals/component-manager';

/** Misc */

export { DefaultPool, ObjectPool } from './pool.js';
export { DefaultPool, ObjectPool } from './pool/pool.js';

/** Properties. */

Expand Down
2 changes: 1 addition & 1 deletion src/internals/component-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, ComponentState, ComponentData } from '../component.js';
import { Entity } from '../entity.js';
import { ObjectPool } from '../pool.js';
import { ObjectPool } from '../pool/pool.js';
import { World } from '../world.js';
import { Archetype } from './archetype.js';
import {
Expand Down
4 changes: 2 additions & 2 deletions src/pool.ts → src/pool/pool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Constructor } from './types';
import { Constructor } from '../types';

export class DefaultPool<T> {
protected readonly _class;
Expand Down Expand Up @@ -66,7 +66,7 @@ export interface ObjectPool<T> {
expand(count: number): void;
}

interface DefaultPoolOptions<T> {
export interface DefaultPoolOptions<T> {
initialCount: number;
growthPercentage: number;
}
63 changes: 63 additions & 0 deletions src/pool/sequential-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Constructor } from '../types';
import { DefaultPoolOptions } from './pool';

export class SequentialPool<T> {
protected readonly _class;
protected readonly _list: T[];
protected readonly _growPercentage: number;
protected _freeSlot: number;

public constructor(
Class: Constructor<T>,
options: Partial<DefaultPoolOptions<T>> = {}
) {
this._class = Class;
this._list = [];
this._growPercentage = options.growthPercentage ?? 0.2;
this._freeSlot = 0;
if (options.initialCount) {
this.expand(options.initialCount);
}
}

public acquire(): T {
if (this._freeSlot === this._list.length) {
this.expand(Math.round(this._list.length * 0.2) + 1);
}
const value = this._list[this._freeSlot];
this._freeSlot++;
return value;
}

public release(): void {
this._freeSlot = 0;
}

public execute(cb: (value: T) => void): void {
const list = this._list;
for (let i = 0; i < this._freeSlot; ++i) {
cb(list[i]);
}
}

public expand(count: number): void {
if (count <= 0) {
return;
}
const Class = this._class;
const start = this._list.length;
const end = start + count;
this._list.length = end;
for (let i = start; i < end; ++i) {
this._list[i] = new Class();
}
}

public get allocatedSize(): number {
return this._list.length;
}

public get used(): number {
return this._freeSlot;
}
}
29 changes: 13 additions & 16 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, ComponentData, Properties } from './component';
import { Entity } from './entity';
import { ObjectPool } from './pool';
import { ObjectPool } from './pool/pool';
import { Property } from './property';
import { StaticQueries, System } from './system';
import { SystemGroup } from './system-group';
Expand All @@ -24,11 +24,10 @@ export type Constructor<T> = new (...args: any[]) => T;
export type EntityClass<T extends Entity> = new (name?: string) => T;

/** Class type for a SystemGroup derived type */
export type SystemGroupClass<
T extends SystemGroup = SystemGroup
> = Constructor<T> & {
readonly Mame?: string;
};
export type SystemGroupClass<T extends SystemGroup = SystemGroup> =
Constructor<T> & {
readonly Mame?: string;
};

/** Class type for a System derived type */
export type SystemClass<T extends System = System> = (new (
Expand All @@ -48,15 +47,13 @@ export type ComponentClass<T extends Component = Component> = Constructor<T> & {
};

/** Class type for a ComponentData derived type */
export type DataComponentClass<
T extends ComponentData = ComponentData
> = Constructor<T> & {
Name?: string;
Properties?: Properties;
readonly _MergedProoperties: Properties;
};
export type DataComponentClass<T extends ComponentData = ComponentData> =
Constructor<T> & {
Name?: string;
Properties?: Properties;
readonly _MergedProoperties: Properties;
};

/** Class type for a Property derived type */
export type PropertyClass<
T extends Property<any> = Property<any>
> = Constructor<T>;
export type PropertyClass<T extends Property<any> = Property<any>> =
Constructor<T>;
25 changes: 20 additions & 5 deletions src/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import { System } from './system.js';
import { SystemGroup } from './system-group.js';
import { Component } from './component.js';
import { Query, QueryComponents } from './query.js';
import { DefaultPool, ObjectPool } from './pool.js';
import { DefaultPool, ObjectPool } from './pool/pool.js';
import {
ComponentClass,
ComponentOf,
Constructor,
EntityOf,
EntityClass,
Expand All @@ -23,6 +22,7 @@ import {
SystemGroupClass
} from './types';
import { Archetype } from './internals/archetype.js';
import { CommandBuffer } from './command-buffer.js';

/**
* The world is the link between entities and systems. The world is composed
Expand Down Expand Up @@ -81,6 +81,9 @@ export class World<E extends Entity = Entity> {
/** @hidden */
protected _entityPool: Nullable<EntityPool<this>>;

/** @hidden */
protected readonly _postExecuteCmdBuffer: CommandBuffer;

/** Public API. */

public constructor(options: Partial<WorldOptions<E>> = {}) {
Expand Down Expand Up @@ -108,6 +111,8 @@ export class World<E extends Entity = Entity> {
) as EntityPool<this>;
}

this._postExecuteCmdBuffer = new CommandBuffer();

for (const component of components) {
this.registerComponent(component);
}
Expand Down Expand Up @@ -208,6 +213,7 @@ export class World<E extends Entity = Entity> {
*/
public execute(delta: number): void {
this._systems.execute(delta);
this._postExecuteCmdBuffer.playback();
}

/**
Expand Down Expand Up @@ -274,9 +280,9 @@ export class World<E extends Entity = Entity> {
return this._systems.group(Class);
}

public setComponentPool<P extends ObjectPool<any>>(
Class: ComponentClass<ComponentOf<P>>,
pool: Nullable<P>
public setComponentPool<C extends Component>(
Class: ComponentClass<C>,
pool: Nullable<ObjectPool<C>>
): this {
this._components.setComponentPool(Class, pool);
return this;
Expand All @@ -292,12 +298,21 @@ export class World<E extends Entity = Entity> {
* Returns the unique identifier of the given component typee
*
* @param Class - Type of the component to retrieve the id for
*
* @return The identifier of the component
*/
public getComponentId(Class: ComponentClass): number {
return this._components.getIdentifier(Class);
}

/**
* Default command buffer that gets flushed after **every** systems has
* executed upon calling `world.execute()`
*/
public get postExecuteCmdBuffer(): CommandBuffer {
return this._postExecuteCmdBuffer;
}

/**
* Returns the max number of components this world can store.
*
Expand Down
Loading