diff --git a/CHANGELOG.md b/CHANGELOG.md index 184f428d9..f9a131140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,39 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Remove confusing Graphics Layering from `ex.GraphicsComponent`, recommend we use the `ex.GraphicsGroup` to manage this behavior * Update `ex.GraphicsGroup` to be consistent and use `offset` instead of `pos` for graphics relative positioning +- ECS implementation has been updated to remove the "stringly" typed nature of components & systems + * For average users of Excalibur folks shouldn't notice any difference + * For folks leveraging the ECS, Systems/Components no longer have type parameters based on strings. The type itself is used to track changes. + * `class MySystem extends System<'ex.component'>` becomes `class MySystem extends System` + * `class MyComponent extends Component<'ex.component'>` becomes `class MyComponent extends Component` + * `ex.System.update(elapsedMs: number)` is only passed an elapsed time - Prevent people from inadvertently overriding `update()` in `ex.Scene` and `ex.Actor`. This method can still be overridden with the `//@ts-ignore` pragma + ### Deprecated - ### Added +- New simplified way to query entities `ex.World.query([MyComponentA, MyComponentB])` +- New way to query for tags on entities `ex.World.queryTags(['A', 'B'])` +- Systems can be added as a constructor to a world, if they are the world will construct and pass a world instance to them + ```typescript + world.add(MySystem); + ... + + class MySystem extends System { + query: Query; + constructor(world: World) { + super() + this.query = world.query([MyComponent]); + } + + update + } + + ``` - Added `RayCastHit`as part of every raycast not just the physics world query! * Additionally added the ray distance and the contact normal for the surface - Added the ability to log a message once to all log levels diff --git a/sandbox/tests/scenepredraw/index.ts b/sandbox/tests/scenepredraw/index.ts index 840f5d785..d2384cfa8 100644 --- a/sandbox/tests/scenepredraw/index.ts +++ b/sandbox/tests/scenepredraw/index.ts @@ -1,3 +1,4 @@ + var paddle = new ex.Actor({ x: 150, y: 150, width: 200, height: 200, @@ -24,14 +25,18 @@ class MyScene extends ex.Scene { } -class CustomDraw extends ex.System { - public readonly types = ['ex.graphics'] as const; +class CustomDraw extends ex.System { public readonly systemType = ex.SystemType.Draw; private _graphicsContext?: ex.ExcaliburGraphicsContext; private _engine?: ex.Engine; + query: ex.Query; + constructor(public world: ex.World) { + super(); + this.query = this.world.query([ex.GraphicsComponent]); + } - public initialize(scene: ex.Scene): void { + public initialize(world: ex.World, scene: ex.Scene): void { this._graphicsContext = scene.engine.graphicsContext; this._engine = scene.engine; } @@ -44,7 +49,7 @@ class CustomDraw extends ex.System { this._graphicsContext = this._engine.graphicsContext; } - public update(entities: ex.Entity[], delta: number) { + public update( delta: number) { if (this._graphicsContext == null) { throw new Error("Uninitialized ObjectSystem"); } @@ -63,9 +68,9 @@ var game = new ex.Engine({ height: 600, }); -var thescene = new MyScene(); -thescene.world.add(new CustomDraw()); -game.addScene('test', thescene); +var theScene = new MyScene(); +theScene.world.add(CustomDraw); +game.addScene('test', theScene); game.goToScene('test'); game.add(paddle); diff --git a/src/engine/Actions/ActionQueue.ts b/src/engine/Actions/ActionQueue.ts index 78aaf0b62..3565ebe8d 100644 --- a/src/engine/Actions/ActionQueue.ts +++ b/src/engine/Actions/ActionQueue.ts @@ -15,7 +15,7 @@ import { Action } from './Action'; export class ActionQueue { private _entity: Entity; private _actions: Action[] = []; - private _currentAction: Action; + private _currentAction: Action | null = null; private _completedActions: Action[] = []; constructor(entity: Entity) { this._entity = entity; @@ -100,7 +100,10 @@ export class ActionQueue { if (this._currentAction.isComplete(this._entity)) { this._entity.emit('actioncomplete', new ActionCompleteEvent(this._currentAction, this._entity)); - this._completedActions.push(this._actions.shift()); + const complete = this._actions.shift(); + if (complete) { + this._completedActions.push(complete); + } } } } diff --git a/src/engine/Actions/ActionsComponent.ts b/src/engine/Actions/ActionsComponent.ts index b5a08d557..737c970ce 100644 --- a/src/engine/Actions/ActionsComponent.ts +++ b/src/engine/Actions/ActionsComponent.ts @@ -12,10 +12,9 @@ import { Action } from './Action'; export interface ActionContextMethods extends Pick { }; -export class ActionsComponent extends Component<'ex.actions'> implements ActionContextMethods { - public readonly type = 'ex.actions'; +export class ActionsComponent extends Component implements ActionContextMethods { dependencies = [TransformComponent, MotionComponent]; - private _ctx: ActionContext; + private _ctx: ActionContext | null = null; onAdd(entity: Entity) { this._ctx = new ActionContext(entity); @@ -25,16 +24,29 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC this._ctx = null; } + private _getCtx() { + if (!this._ctx) { + throw new Error('Actions component not attached to an entity, no context available'); + } + return this._ctx; + } + /** * Returns the internal action queue * @returns action queue */ public getQueue(): ActionQueue { - return this._ctx?.getQueue(); + if (!this._ctx) { + throw new Error('Actions component not attached to an entity, no queue available'); + } + return this._ctx.getQueue(); } public runAction(action: Action): ActionContext { - return this._ctx?.runAction(action); + if (!this._ctx) { + throw new Error('Actions component not attached to an entity, cannot run action'); + } + return this._ctx.runAction(action); } /** @@ -72,13 +84,13 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC */ public easeTo(x: number, y: number, duration: number, easingFcn?: EasingFunction): ActionContext; public easeTo(...args: any[]): ActionContext { - return this._ctx.easeTo.apply(this._ctx, args); + return this._getCtx().easeTo.apply(this._ctx, args as any); } public easeBy(offset: Vector, duration: number, easingFcn?: EasingFunction): ActionContext; public easeBy(offsetX: number, offsetY: number, duration: number, easingFcn?: EasingFunction): ActionContext; public easeBy(...args: any[]): ActionContext { - return this._ctx.easeBy.apply(this._ctx, args); + return this._getCtx().easeBy.apply(this._ctx, args as any); } /** @@ -99,7 +111,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC */ public moveTo(x: number, y: number, speed: number): ActionContext; public moveTo(xOrPos: number | Vector, yOrSpeed: number, speedOrUndefined?: number): ActionContext { - return this._ctx.moveTo.apply(this._ctx, [xOrPos, yOrSpeed, speedOrUndefined]); + return this._getCtx().moveTo.apply(this._ctx, [xOrPos, yOrSpeed, speedOrUndefined] as any); } /** @@ -118,7 +130,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC */ public moveBy(xOffset: number, yOffset: number, speed: number): ActionContext; public moveBy(xOffsetOrVector: number | Vector, yOffsetOrSpeed: number, speedOrUndefined?: number): ActionContext { - return this._ctx.moveBy.apply(this._ctx, [xOffsetOrVector, yOffsetOrSpeed, speedOrUndefined]); + return this._getCtx().moveBy.apply(this._ctx, [xOffsetOrVector, yOffsetOrSpeed, speedOrUndefined] as any); } /** @@ -130,7 +142,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param rotationType The [[RotationType]] to use for this rotation */ public rotateTo(angleRadians: number, speed: number, rotationType?: RotationType): ActionContext { - return this._ctx.rotateTo(angleRadians, speed, rotationType); + return this._getCtx().rotateTo(angleRadians, speed, rotationType); } /** @@ -142,7 +154,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param rotationType The [[RotationType]] to use for this rotation, default is shortest path */ public rotateBy(angleRadiansOffset: number, speed: number, rotationType?: RotationType): ActionContext { - return this._ctx.rotateBy(angleRadiansOffset, speed, rotationType); + return this._getCtx().rotateBy(angleRadiansOffset, speed, rotationType); } /** @@ -170,7 +182,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC sizeYOrSpeed: number | Vector, speedXOrUndefined?: number, speedYOrUndefined?: number): ActionContext { - return this._ctx.scaleTo.apply(this._ctx, [sizeXOrVector, sizeYOrSpeed, speedXOrUndefined, speedYOrUndefined]); + return this._getCtx().scaleTo.apply(this._ctx, [sizeXOrVector, sizeYOrSpeed, speedXOrUndefined, speedYOrUndefined] as any); } /** @@ -191,7 +203,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC */ public scaleBy(sizeOffsetX: number, sizeOffsetY: number, speed: number): ActionContext; public scaleBy(sizeOffsetXOrVector: number | Vector, sizeOffsetYOrSpeed: number, speed?: number): ActionContext { - return this._ctx.scaleBy.apply(this._ctx, [sizeOffsetXOrVector, sizeOffsetYOrSpeed, speed]); + return this._getCtx().scaleBy.apply(this._ctx, [sizeOffsetXOrVector, sizeOffsetYOrSpeed, speed] as any); } /** @@ -204,7 +216,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param numBlinks The number of times to blink */ public blink(timeVisible: number, timeNotVisible: number, numBlinks?: number): ActionContext { - return this._ctx.blink(timeVisible, timeNotVisible, numBlinks); + return this._getCtx().blink(timeVisible, timeNotVisible, numBlinks); } /** @@ -215,7 +227,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param time The time it should take to fade the actor (in milliseconds) */ public fade(opacity: number, time: number): ActionContext { - return this._ctx.fade(opacity, time); + return this._getCtx().fade(opacity, time); } /** @@ -225,7 +237,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param time The amount of time to delay the next action in the queue from executing in milliseconds */ public delay(time: number): ActionContext { - return this._ctx.delay(time); + return this._getCtx().delay(time); } /** @@ -234,7 +246,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * action queue after this action will not be executed. */ public die(): ActionContext { - return this._ctx.die(); + return this._getCtx().die(); } /** @@ -243,7 +255,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * action, i.e An actor arrives at a destination after traversing a path */ public callMethod(method: () => any): ActionContext { - return this._ctx.callMethod(method); + return this._getCtx().callMethod(method); } /** @@ -264,7 +276,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * will repeat forever */ public repeat(repeatBuilder: (repeatContext: ActionContext) => any, times?: number): ActionContext { - return this._ctx.repeat(repeatBuilder, times); + return this._getCtx().repeat(repeatBuilder, times); } /** @@ -283,7 +295,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param repeatBuilder The builder to specify the repeatable list of actions */ public repeatForever(repeatBuilder: (repeatContext: ActionContext) => any): ActionContext { - return this._ctx.repeatForever(repeatBuilder); + return this._getCtx().repeatForever(repeatBuilder); } /** @@ -292,7 +304,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param followDistance The distance to maintain when following, if not specified the actor will follow at the current distance. */ public follow(entity: Actor, followDistance?: number): ActionContext { - return this._ctx.follow(entity, followDistance); + return this._getCtx().follow(entity, followDistance); } /** @@ -302,7 +314,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * @param speed The speed in pixels per second to move, if not specified it will match the speed of the other actor */ public meet(entity: Actor, speed?: number): ActionContext { - return this._ctx.meet(entity, speed); + return this._getCtx().meet(entity, speed); } /** @@ -310,6 +322,6 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC * is finished. */ public toPromise(): Promise { - return this._ctx.toPromise(); + return this._getCtx().toPromise(); } } \ No newline at end of file diff --git a/src/engine/Actions/ActionsSystem.ts b/src/engine/Actions/ActionsSystem.ts index be09a3d9b..3d41ddf65 100644 --- a/src/engine/Actions/ActionsSystem.ts +++ b/src/engine/Actions/ActionsSystem.ts @@ -1,28 +1,27 @@ -import { Entity } from '../EntityComponentSystem'; -import { AddedEntity, isAddedSystemEntity, RemovedEntity, System, SystemType } from '../EntityComponentSystem/System'; +import { Query, SystemPriority, World } from '../EntityComponentSystem'; +import { System, SystemType } from '../EntityComponentSystem/System'; import { ActionsComponent } from './ActionsComponent'; - -export class ActionsSystem extends System { - public readonly types = ['ex.actions'] as const; +export class ActionsSystem extends System { systemType = SystemType.Update; - priority = -1; - + priority = SystemPriority.Higher; private _actions: ActionsComponent[] = []; - public notify(entityAddedOrRemoved: AddedEntity | RemovedEntity): void { - if (isAddedSystemEntity(entityAddedOrRemoved)) { - const action = entityAddedOrRemoved.data.get(ActionsComponent); - this._actions.push(action); - } else { - const action = entityAddedOrRemoved.data.get(ActionsComponent); + query: Query; + + constructor(public world: World) { + super(); + this.query = this.world.query([ActionsComponent]); + + this.query.entityAdded$.subscribe(e => this._actions.push(e.get(ActionsComponent))); + this.query.entityRemoved$.subscribe(e => { + const action = e.get(ActionsComponent); const index = this._actions.indexOf(action); if (index > -1) { this._actions.splice(index, 1); } - } + }); } - - update(_entities: Entity[], delta: number): void { + update(delta: number): void { for (const actions of this._actions) { actions.update(delta); } diff --git a/src/engine/Actions/tsconfig.json b/src/engine/Actions/tsconfig.json new file mode 100644 index 000000000..b2e9e0b8f --- /dev/null +++ b/src/engine/Actions/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strict": true + } +} \ No newline at end of file diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 0ca4a7951..7885901eb 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -537,7 +537,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia ...config }; - this._setName(name); + this.name = name ?? this.name; this.anchor = anchor ?? Actor.defaults.anchor.clone(); const tx = new TransformComponent(); this.addComponent(tx); diff --git a/src/engine/Collision/BodyComponent.ts b/src/engine/Collision/BodyComponent.ts index 67b56065d..e437ea3fc 100644 --- a/src/engine/Collision/BodyComponent.ts +++ b/src/engine/Collision/BodyComponent.ts @@ -28,8 +28,7 @@ export enum DegreeOfFreedom { * Body describes all the physical properties pos, vel, acc, rotation, angular velocity for the purpose of * of physics simulation. */ -export class BodyComponent extends Component<'ex.body'> implements Clonable { - public readonly type = 'ex.body'; +export class BodyComponent extends Component implements Clonable { public dependencies = [TransformComponent, MotionComponent]; public static _ID = 0; public readonly id: Id<'body'> = createId('body', BodyComponent._ID++); diff --git a/src/engine/Collision/ColliderComponent.ts b/src/engine/Collision/ColliderComponent.ts index c0a2675d3..d9d40b216 100644 --- a/src/engine/Collision/ColliderComponent.ts +++ b/src/engine/Collision/ColliderComponent.ts @@ -15,8 +15,7 @@ import { Shape } from './Colliders/Shape'; import { EventEmitter } from '../EventEmitter'; import { Actor } from '../Actor'; -export class ColliderComponent extends Component<'ex.collider'> { - public readonly type = 'ex.collider'; +export class ColliderComponent extends Component { public events = new EventEmitter(); /** diff --git a/src/engine/Collision/CollisionSystem.ts b/src/engine/Collision/CollisionSystem.ts index c530688a6..7f6433d55 100644 --- a/src/engine/Collision/CollisionSystem.ts +++ b/src/engine/Collision/CollisionSystem.ts @@ -1,7 +1,7 @@ -import { Entity } from '../EntityComponentSystem'; +import { ComponentCtor, Query, SystemPriority, World } from '../EntityComponentSystem'; import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; -import { AddedEntity, isAddedSystemEntity, RemovedEntity, System, SystemType } from '../EntityComponentSystem/System'; +import { System, SystemType } from '../EntityComponentSystem/System'; import { CollisionEndEvent, CollisionStartEvent, ContactEndEvent, ContactStartEvent } from '../Events'; import { CollisionResolutionStrategy, Physics } from './Physics'; import { ArcadeSolver } from './Solver/ArcadeSolver'; @@ -17,10 +17,10 @@ import { Scene } from '../Scene'; import { Side } from '../Collision/Side'; import { DynamicTreeCollisionProcessor } from './Detection/DynamicTreeCollisionProcessor'; import { PhysicsWorld } from './PhysicsWorld'; -export class CollisionSystem extends System { - public readonly types = ['ex.transform', 'ex.motion', 'ex.collider'] as const; +export class CollisionSystem extends System { public systemType = SystemType.Update; - public priority = -1; + public priority = SystemPriority.Higher; + public query: Query | ComponentCtor | ComponentCtor>; private _engine: Engine; private _realisticSolver = new RealisticSolver(); @@ -32,44 +32,42 @@ export class CollisionSystem extends System void; private _untrackCollider: (c: Collider) => void; - constructor(physics: PhysicsWorld) { + constructor(world: World, physics: PhysicsWorld) { super(); this._processor = physics.collisionProcessor; this._trackCollider = (c: Collider) => this._processor.track(c); this._untrackCollider = (c: Collider) => this._processor.untrack(c); - } - - notify(message: AddedEntity | RemovedEntity) { - if (isAddedSystemEntity(message)) { - const colliderComponent = message.data.get(ColliderComponent); + this.query = world.query([TransformComponent, MotionComponent, ColliderComponent]); + this.query.entityAdded$.subscribe(e => { + const colliderComponent = e.get(ColliderComponent); colliderComponent.$colliderAdded.subscribe(this._trackCollider); colliderComponent.$colliderRemoved.subscribe(this._untrackCollider); const collider = colliderComponent.get(); if (collider) { this._processor.track(collider); } - } else { - const colliderComponent = message.data.get(ColliderComponent); + }); + this.query.entityRemoved$.subscribe(e => { + const colliderComponent = e.get(ColliderComponent); const collider = colliderComponent.get(); if (colliderComponent && collider) { this._processor.untrack(collider); } - } + }); } - initialize(scene: Scene) { + initialize(world: World, scene: Scene) { this._engine = scene.engine; - } - update(entities: Entity[], elapsedMs: number): void { + update(elapsedMs: number): void { if (!Physics.enabled) { return; } // Collect up all the colliders and update them let colliders: Collider[] = []; - for (const entity of entities) { + for (const entity of this.query.entities) { const colliderComp = entity.get(ColliderComponent); const collider = colliderComp?.get(); if (colliderComp && colliderComp.owner?.active && collider) { diff --git a/src/engine/Collision/MotionSystem.ts b/src/engine/Collision/MotionSystem.ts index be0c28997..a3ed8d8d8 100644 --- a/src/engine/Collision/MotionSystem.ts +++ b/src/engine/Collision/MotionSystem.ts @@ -1,4 +1,4 @@ -import { Entity } from '../EntityComponentSystem'; +import { Query, SystemPriority, World } from '../EntityComponentSystem'; import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { System, SystemType } from '../EntityComponentSystem/System'; @@ -7,14 +7,19 @@ import { BodyComponent } from './BodyComponent'; import { CollisionType } from './CollisionType'; import { EulerIntegrator } from './Integrator'; -export class MotionSystem extends System { - public readonly types = ['ex.transform', 'ex.motion'] as const; +export class MotionSystem extends System { public systemType = SystemType.Update; - public priority = -1; + public priority = SystemPriority.Higher; + query: Query; + constructor(public world: World) { + super(); + this.query = this.world.query([TransformComponent, MotionComponent]); + } - update(entities: Entity[], elapsedMs: number): void { + update(elapsedMs: number): void { let transform: TransformComponent; let motion: MotionComponent; + const entities = this.query.entities; for (let i = 0; i < entities.length; i++) { transform = entities[i].get(TransformComponent); motion = entities[i].get(MotionComponent); diff --git a/src/engine/Debug/DebugSystem.ts b/src/engine/Debug/DebugSystem.ts index 68b8d2190..384b6b822 100644 --- a/src/engine/Debug/DebugSystem.ts +++ b/src/engine/Debug/DebugSystem.ts @@ -3,7 +3,7 @@ import { Scene } from '../Scene'; import { Camera } from '../Camera'; import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; import { ColliderComponent } from '../Collision/ColliderComponent'; -import { Entity, TransformComponent } from '../EntityComponentSystem'; +import { Entity, Query, SystemPriority, TransformComponent, World } from '../EntityComponentSystem'; import { System, SystemType } from '../EntityComponentSystem/System'; import { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext'; import { vec, Vector } from '../Math/vector'; @@ -16,23 +16,28 @@ import { Particle } from '../Particles'; import { DebugGraphicsComponent } from '../Graphics/DebugGraphicsComponent'; import { CoordPlane } from '../Math/coord-plane'; -export class DebugSystem extends System { - public readonly types = ['ex.transform'] as const; +export class DebugSystem extends System { public readonly systemType = SystemType.Draw; - public priority = 999; // lowest priority + public priority = SystemPriority.Lowest; private _graphicsContext: ExcaliburGraphicsContext; private _collisionSystem: CollisionSystem; private _camera: Camera; private _engine: Engine; + query: Query; - public initialize(scene: Scene): void { + constructor(public world: World) { + super(); + this.query = this.world.query([TransformComponent]); + } + + public initialize(world: World, scene: Scene): void { this._graphicsContext = scene.engine.graphicsContext; this._camera = scene.camera; this._engine = scene.engine; - this._collisionSystem = scene.world.systemManager.get(CollisionSystem); + this._collisionSystem = world.systemManager.get(CollisionSystem); } - update(entities: Entity[], _delta: number): void { + update(): void { if (!this._engine.isDebug) { return; } @@ -63,7 +68,7 @@ export class DebugSystem extends System { const bodySettings = this._engine.debug.body; const cameraSettings = this._engine.debug.camera; - for (const entity of entities) { + for (const entity of this.query.entities) { if (entity.hasTag('offscreen')) { // skip offscreen entities continue; diff --git a/src/engine/EntityComponentSystem/Component.ts b/src/engine/EntityComponentSystem/Component.ts index 913bfdded..cbabd6c43 100644 --- a/src/engine/EntityComponentSystem/Component.ts +++ b/src/engine/EntityComponentSystem/Component.ts @@ -1,9 +1,16 @@ import { Entity } from './Entity'; /** - * Component Contructor Types + * Component Constructor Types */ -export declare type ComponentCtor = new (...args:any[]) => T; +export declare type ComponentCtor = new (...args:any[]) => TComponent; + +/** + * + */ +export function isComponentCtor(value: any): value is ComponentCtor { + return !!value && !!value.prototype && !!value.prototype.constructor; +} /** * Type guard to check if a component implements clone @@ -13,27 +20,21 @@ function hasClone(x: any): x is { clone(): any } { return !!x?.clone; } -export type ComponentType = ComponentToParse extends Component ? TypeName : never; - -/** - * Plucks the string type out of a component type - */ -export type ComponentStringType = T extends Component ? R : string; - /** * Components are containers for state in Excalibur, the are meant to convey capabilities that an Entity possesses * * Implementations of Component must have a zero-arg constructor to support dependencies * * ```typescript - * class MyComponent extends ex.Component<'my'> { - * public readonly type = 'my'; + * class MyComponent extends ex.Component { * // zero arg support required if you want to use component dependencies * constructor(public optionalPos?: ex.Vector) {} * } * ``` */ -export abstract class Component { +export abstract class Component { + // TODO maybe generate a unique id? + /** * Optionally list any component types this component depends on * If the owner entity does not have these components, new components will be added to the entity @@ -42,18 +43,10 @@ export abstract class Component { */ readonly dependencies?: ComponentCtor[]; - // todo implement optional - readonly optional?: ComponentCtor[]; - - /** - * Type of this component, must be a unique type among component types in you game. - */ - abstract readonly type: TypeName; - /** * Current owning [[Entity]], if any, of this component. Null if not added to any [[Entity]] */ - owner?: Entity = null; + owner?: Entity = undefined; /** * Clones any properties on this component, if that property value has a `clone()` method it will be called @@ -82,23 +75,4 @@ export abstract class Component { * Optional callback called when a component is added to an entity */ onRemove?(previousOwner: Entity): void; -} - -/** - * Tag components are a way of tagging a component with label and a simple value - * - * For example: - * - * ```typescript - * const isOffscreen = new TagComponent('offscreen'); - * entity.addComponent(isOffscreen); - * entity.tags.includes - * ``` - */ -export class TagComponent extends Component< -TypeName -> { - constructor(public readonly type: TypeName, public readonly value?: MaybeValueType) { - super(); - } -} +} \ No newline at end of file diff --git a/src/engine/EntityComponentSystem/Components/MotionComponent.ts b/src/engine/EntityComponentSystem/Components/MotionComponent.ts index 9e3c15e71..5cb5516b0 100644 --- a/src/engine/EntityComponentSystem/Components/MotionComponent.ts +++ b/src/engine/EntityComponentSystem/Components/MotionComponent.ts @@ -33,8 +33,7 @@ export interface Motion { inertia: number; } -export class MotionComponent extends Component<'ex.motion'> { - public readonly type = 'ex.motion'; +export class MotionComponent extends Component { /** * The velocity of an entity in pixels per second diff --git a/src/engine/EntityComponentSystem/Components/TagsComponent.ts b/src/engine/EntityComponentSystem/Components/TagsComponent.ts new file mode 100644 index 000000000..60a1a8d2b --- /dev/null +++ b/src/engine/EntityComponentSystem/Components/TagsComponent.ts @@ -0,0 +1,5 @@ +import { Component } from '../Component'; + +export class TagsComponent extends Component { + public tags = new Set(); +} \ No newline at end of file diff --git a/src/engine/EntityComponentSystem/Components/TransformComponent.ts b/src/engine/EntityComponentSystem/Components/TransformComponent.ts index 2604d8ae5..6e88d4436 100644 --- a/src/engine/EntityComponentSystem/Components/TransformComponent.ts +++ b/src/engine/EntityComponentSystem/Components/TransformComponent.ts @@ -6,8 +6,7 @@ import { Entity } from '../Entity'; import { Observable } from '../../Util/Observable'; -export class TransformComponent extends Component<'ex.transform'> { - public readonly type = 'ex.transform'; +export class TransformComponent extends Component { private _transform = new Transform(); public get() { diff --git a/src/engine/EntityComponentSystem/Entity.ts b/src/engine/EntityComponentSystem/Entity.ts index 470ae9772..7db1aa630 100644 --- a/src/engine/EntityComponentSystem/Entity.ts +++ b/src/engine/EntityComponentSystem/Entity.ts @@ -1,4 +1,4 @@ -import { Component, ComponentCtor, TagComponent } from './Component'; +import { Component, ComponentCtor, isComponentCtor } from './Component'; import { Observable, Message } from '../Util/Observable'; import { OnInitialize, OnPreUpdate, OnPostUpdate } from '../Interfaces/LifecycleEvents'; @@ -8,6 +8,7 @@ import { KillEvent } from '../Events'; import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter'; import { Scene } from '../Scene'; import { removeItemFromArray } from '../Util/Util'; +import { MaybeKnownComponent } from './Types'; /** * Interface holding an entity component pair @@ -22,7 +23,7 @@ export interface EntityComponent { */ export class AddedComponent implements Message { readonly type: 'Component Added' = 'Component Added'; - constructor(public data: EntityComponent) {} + constructor(public data: EntityComponent) { } } /** @@ -37,7 +38,7 @@ export function isAddedComponent(x: Message): x is AddedCompone */ export class RemovedComponent implements Message { readonly type: 'Component Removed' = 'Component Removed'; - constructor(public data: EntityComponent) {} + constructor(public data: EntityComponent) { } } /** @@ -64,6 +65,11 @@ export const EntityEvents = { Kill: 'kill' } as const; +export interface EntityOptions { + name?: string; + components: TComponents[]; +} + /** * An Entity is the base type of anything that can have behavior in Excalibur, they are part of the built in entity component system * @@ -75,48 +81,65 @@ export const EntityEvents = { * entity.components.b; // Type ComponentB * ``` */ -export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { +export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { private static _ID = 0; + /** + * The unique identifier for the entity + */ + public id: number = Entity._ID++; + + public name = `Entity#${this.id}`; /** * Listen to or emit events for an entity */ public events = new EventEmitter(); - - constructor(components?: Component[], name?: string) { - this._setName(name); - if (components) { - for (const component of components) { + private _tags = new Set(); + public componentAdded$ = new Observable; + public componentRemoved$ = new Observable; + public tagAdded$ = new Observable; + public tagRemoved$ = new Observable; + /** + * Current components on the entity + * + * **Do not modify** + * + * Use addComponent/removeComponent otherwise the ECS will not be notified of changes. + */ + public readonly components = new Map(); + private _componentsToRemove: ComponentCtor[] = []; + + private _instanceOfComponentCacheDirty = true; + private _instanceOfComponentCache = new Map(); + + constructor(options: EntityOptions); + constructor(components?: TKnownComponents[], name?: string); + constructor(componentsOrOptions?: TKnownComponents[] | EntityOptions, name?: string) { + let componentsToAdd!: TKnownComponents[]; + let nameToAdd: string | undefined; + if (Array.isArray(componentsOrOptions)) { + componentsToAdd = componentsOrOptions; + nameToAdd = name; + } else if (componentsOrOptions && typeof componentsOrOptions === 'object') { + const { components, name } = componentsOrOptions; + componentsToAdd = components; + nameToAdd = name; + } + if (nameToAdd) { + this.name = nameToAdd; + } + if (componentsToAdd) { + for (const component of componentsToAdd) { this.addComponent(component); } } + // this.addComponent(this.tagsComponent); } /** - * The unique identifier for the entity + * The current scene that the entity is in, if any */ - public id: number = Entity._ID++; - - /** - * The scene that the entity is in, if any - */ - public scene: Scene = null; - - private _name: string = 'anonymous'; - protected _setName(name: string) { - if (name) { - this._name = name; - } else { - this._name = `Entity#${this.id}`; - } - } - public get name(): string { - return this._name; - } - - public set name(name: string) { - this._setName(name); - } + public scene: Scene | null = null; /** * Whether this entity is active, if set to false it will be reclaimed @@ -140,10 +163,10 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { } /** - * Specifically get the tags on the entity from [[TagComponent]] + * Specifically get the tags on the entity from [[TagsComponent]] */ - public get tags(): readonly string[] { - return this._tagsMemo; + public get tags(): Set { + return this._tags; } /** @@ -151,16 +174,16 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { * @param tag name to check for */ public hasTag(tag: string): boolean { - return this.tags.includes(tag); + return this._tags.has(tag); } /** * Adds a tag to an entity * @param tag - * @returns Entity */ public addTag(tag: string) { - return this.addComponent(new TagComponent(tag)); + this._tags.add(tag); + this.tagAdded$.notifyAll(tag); } /** @@ -168,64 +191,79 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { * * Removals are deferred until the end of update * @param tag - * @param force Remove component immediately, no deferred */ - public removeTag(tag: string, force = false) { - return this.removeComponent(tag, force); + public removeTag(tag: string) { + this._tags.delete(tag); + this.tagRemoved$.notifyAll(tag); } /** * The types of the components on the Entity */ - public get types(): string[] { - return this._typesMemo; + public get types(): ComponentCtor[] { + return Array.from(this.components.keys()) as ComponentCtor[]; } /** - * Bucket to hold on to deferred removals + * Returns all component instances on entity */ - private _componentsToRemove: (Component | string)[] = []; - private _componentTypeToInstance = new Map(); - private _componentStringToInstance = new Map(); - - private _tagsMemo: string[] = []; - private _typesMemo: string[] = []; - private _rebuildMemos() { - this._tagsMemo = Array.from(this._componentStringToInstance.values()) - .filter((c) => c instanceof TagComponent) - .map((c) => c.type); - this._typesMemo = Array.from(this._componentStringToInstance.keys()); + public getComponents(): Component[] { + return Array.from(this.components.values()); } - public getComponents(): Component[] { - return Array.from(this._componentStringToInstance.values()); + /** + * Verifies that an entity has all the required types + * @param requiredTypes + */ + hasAll(requiredTypes: ComponentCtor[]): boolean { + for (let i = 0; i < requiredTypes.length; i++) { + if (!this.components.has(requiredTypes[i])) { + return false; + } + } + return true; } /** - * Observable that keeps track of component add or remove changes on the entity + * Verifies that an entity has all the required tags + * @param requiredTags */ - public componentAdded$ = new Observable(); - private _notifyAddComponent(component: Component) { - this._rebuildMemos(); - const added = new AddedComponent({ - component, - entity: this - }); - this.componentAdded$.notifyAll(added); + hasAllTags(requiredTags: string[]): boolean { + for (let i = 0; i < requiredTags.length; i++) { + if (!this.tags.has(requiredTags[i])) { + return false; + } + } + return true; + } + + private _getCachedInstanceOfType( + type: ComponentCtor): MaybeKnownComponent | undefined { + if (this._instanceOfComponentCacheDirty) { + this._instanceOfComponentCacheDirty = false; + this._instanceOfComponentCache.clear(); + } + + if (this._instanceOfComponentCache.has(type)) { + return this._instanceOfComponentCache.get(type) as MaybeKnownComponent; + } + + for (const instance of this.components.values()) { + if (instance instanceof type) { + this._instanceOfComponentCache.set(type, instance); + return instance as MaybeKnownComponent; + } + } + return undefined; } - public componentRemoved$ = new Observable(); - private _notifyRemoveComponent(component: Component) { - const removed = new RemovedComponent({ - component, - entity: this - }); - this.componentRemoved$.notifyAll(removed); - this._rebuildMemos(); + get(type: ComponentCtor): MaybeKnownComponent { + const maybeComponent = this._getCachedInstanceOfType(type); + return maybeComponent ?? this.components.get(type) as MaybeKnownComponent; } - private _parent: Entity = null; - public get parent(): Entity { + private _parent: Entity | null = null; + public get parent(): Entity | null { return this._parent; } @@ -313,8 +351,10 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { let queue: Entity[] = [this]; while (queue.length > 0) { const curr = queue.pop(); - queue = queue.concat(curr.children); - result = result.concat(curr.children); + if (curr) { + queue = queue.concat(curr.children); + result = result.concat(curr.children); + } } return result; } @@ -325,7 +365,10 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { public clone(): Entity { const newEntity = new Entity(); for (const c of this.types) { - newEntity.addComponent(this.get(c).clone()); + const componentInstance = this.get(c); + if (componentInstance) { + newEntity.addComponent(componentInstance.clone()); + } } for (const child of this.children) { newEntity.addChild(child.clone()); @@ -353,15 +396,16 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { * @param component Component or Entity to add copy of components from * @param force Optionally overwrite any existing components of the same type */ - public addComponent(component: T, force: boolean = false): Entity { + public addComponent(component: TComponent, force: boolean = false): Entity { + this._instanceOfComponentCacheDirty = true; // if component already exists, skip if not forced - if (this.has(component.type)) { + if (this.has(component.constructor as ComponentCtor)) { if (force) { // Remove existing component type if exists when forced - this.removeComponent(component, true); + this.removeComponent(component.constructor as ComponentCtor, true); } else { // early exit component exits - return this; + return this as Entity; } } @@ -373,67 +417,64 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { } component.owner = this; - const constuctorType = component.constructor as ComponentCtor; - this._componentTypeToInstance.set(constuctorType, component); - this._componentStringToInstance.set(component.type, component); + this.components.set(component.constructor, component); if (component.onAdd) { component.onAdd(this); } - this._notifyAddComponent(component); - return this; + this.componentAdded$.notifyAll(component); + return this as Entity; } /** * Removes a component from the entity, by default removals are deferred to the end of entity update to avoid consistency issues * * Components can be force removed with the `force` flag, the removal is not deferred and happens immediately - * @param componentOrType + * @param typeOrInstance * @param force */ - public removeComponent(componentOrType: ComponentOrType, force = false): Entity { + public removeComponent( + typeOrInstance: ComponentCtor | TComponent, force = false): Entity> { + + let type: ComponentCtor; + if (isComponentCtor(typeOrInstance)) { + type = typeOrInstance; + } else { + type = typeOrInstance.constructor as ComponentCtor; + } + if (force) { - if (typeof componentOrType === 'string') { - this._removeComponentByType(componentOrType); - } else if (componentOrType instanceof Component) { - this._removeComponentByType(componentOrType.type); + const componentToRemove = this.components.get(type); + if (componentToRemove) { + this.componentRemoved$.notifyAll(componentToRemove); + componentToRemove.owner = undefined; + if (componentToRemove.onRemove) { + componentToRemove.onRemove(this); + } } + this.components.delete(type); // remove after the notify to preserve typing + this._instanceOfComponentCacheDirty = true; } else { - this._componentsToRemove.push(componentOrType); + this._componentsToRemove.push(type); } return this as any; } public clearComponents() { - const components = this.getComponents(); + const components = this.types; for (const c of components) { this.removeComponent(c); } } - private _removeComponentByType(type: string) { - if (this.has(type)) { - const component = this.get(type); - component.owner = null; - if (component.onRemove) { - component.onRemove(this); - } - const ctor = component.constructor as ComponentCtor; - this._componentTypeToInstance.delete(ctor); - this._componentStringToInstance.delete(component.type); - this._notifyRemoveComponent(component); - } - } - /** * @hidden * @internal */ public processComponentRemoval() { - for (const componentOrType of this._componentsToRemove) { - const type = typeof componentOrType === 'string' ? componentOrType : componentOrType.type; - this._removeComponentByType(type); + for (const type of this._componentsToRemove) { + this.removeComponent(type, true); } this._componentsToRemove.length = 0; } @@ -442,30 +483,8 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { * Check if a component type exists * @param type */ - public has(type: ComponentCtor): boolean; - public has(type: string): boolean; - public has(type: ComponentCtor | string): boolean { - if (typeof type === 'string') { - return this._componentStringToInstance.has(type); - } else { - return this._componentTypeToInstance.has(type); - } - } - - /** - * Get a component by type with typecheck - * - * (Does not work on tag components, use .hasTag("mytag") instead) - * @param type - */ - public get(type: ComponentCtor): T | null; - public get(type: string): T | null; - public get(type: ComponentCtor | string): T | null { - if (typeof type === 'string') { - return this._componentStringToInstance.get(type) as T; - } else { - return this._componentTypeToInstance.get(type) as T; - } + public has(type: ComponentCtor): boolean { + return this.components.has(type); } private _isInitialized = false; @@ -579,6 +598,10 @@ export class Entity implements OnInitialize, OnPreUpdate, OnPostUpdate { public off(eventName: string, handler: Handler): void; public off(eventName: string): void; public off | string>(eventName: TEventName, handler?: Handler): void { - this.events.off(eventName, handler); + if (handler) { + this.events.off(eventName, handler); + } else { + this.events.off(eventName); + } } } diff --git a/src/engine/EntityComponentSystem/EntityManager.ts b/src/engine/EntityComponentSystem/EntityManager.ts index ee99d0478..11b41fab2 100644 --- a/src/engine/EntityComponentSystem/EntityManager.ts +++ b/src/engine/EntityComponentSystem/EntityManager.ts @@ -1,24 +1,24 @@ -import { Entity, RemovedComponent, AddedComponent, isAddedComponent, isRemovedComponent } from './Entity'; -import { Observer } from '../Util/Observable'; +import { Entity } from './Entity'; import { World } from './World'; import { removeItemFromArray } from '../Util/Util'; +import { Scene } from '../Scene'; // Add/Remove entities and components -export class EntityManager implements Observer { +export class EntityManager { public entities: Entity[] = []; public _entityIndex: { [entityId: string]: Entity } = {}; - constructor(private _world: World) {} + constructor(private _world: World) {} /** * Runs the entity lifecycle - * @param _context + * @param scene + * @param elapsed */ - public updateEntities(_context: ContextType, elapsed: number) { + public updateEntities(scene: Scene, elapsed: number) { for (const entity of this.entities) { - // TODO is this right? - entity.update((_context as any).engine, elapsed); + entity.update(scene.engine, elapsed); if (!entity.active) { this.removeEntity(entity); } @@ -33,34 +33,17 @@ export class EntityManager implements Observer { @@ -104,8 +87,6 @@ export class EntityManager implements Observer { @@ -116,8 +97,8 @@ export class EntityManager implements Observer = T extends ComponentCtor ? R : never; /** * Represents query for entities that match a list of types that is cached and observable @@ -13,100 +12,66 @@ import { AddedEntity, RemovedEntity } from './System'; * const queryAB = new ex.Query(['A', 'B']); * ``` */ -export class Query extends Observable { - public types: readonly string[]; - private _entities: Entity[] = []; - private _key: string; - public get key(): string { - if (this._key) { - return this._key; - } - return (this._key = buildTypeKey(this.types)); - } - - constructor(types: readonly string[]); - constructor(types: readonly ComponentCtor[]); - constructor(types: readonly string[] | readonly ComponentCtor[]) { - super(); - if (types[0] instanceof Function) { - this.types = (types as ComponentCtor[]).map(T => (new T).type); - } else { - this.types = types as string[]; - } - } - +export class Query = never> { + public readonly id: string; + public components = new Set(); + public entities: Entity>[] = []; /** - * Returns a list of entities that match the query - * @param sort Optional sorting function to sort entities returned from the query + * This fires right after the component is added */ - public getEntities(sort?: (a: Entity, b: Entity) => number): Entity[] { - if (sort) { - this._entities.sort(sort); - } - return this._entities; - } - + public entityAdded$ = new Observable>>(); /** - * Add an entity to the query, will only be added if the entity matches the query types - * @param entity + * This fires right before the component is actually removed from the entity, it will still be available for cleanup purposes */ - public addEntity(entity: Entity): void { - if (!contains(this._entities, entity) && this.matches(entity)) { - this._entities.push(entity); - this.notifyAll(new AddedEntity(entity)); + public entityRemoved$ = new Observable>>(); + + constructor(public readonly requiredComponents: TKnownComponentCtors[]) { + if (requiredComponents.length === 0) { + throw new Error('Cannot create query without components'); + } + for (const type of requiredComponents) { + this.components.add(type); } + + this.id = Query.createId(requiredComponents); + } + + static createId(requiredComponents: Function[]) { + // TODO what happens if a user defines the same type name as a built in type + // ! TODO this could be dangerous depending on the bundler's settings for names + // Maybe some kind of hash function is better here? + return requiredComponents.slice().map(c => c.name).sort().join('-'); } /** - * If the entity is part of the query it will be removed regardless of types + * Potentially adds an entity to a query index, returns true if added, false if not * @param entity */ - public removeEntity(entity: Entity): void { - if (removeItemFromArray(entity, this._entities)) { - this.notifyAll(new RemovedEntity(entity)); + checkAndAdd(entity: Entity) { + if (!this.entities.includes(entity) && entity.hasAll(Array.from(this.components))) { + this.entities.push(entity); + this.entityAdded$.notifyAll(entity); + return true; } + return false; } - /** - * Removes all entities and observers from the query - */ - public clear(): void { - this._entities.length = 0; - for (let i = this.observers.length - 1; i >= 0; i--) { - this.unregister(this.observers[i]); + removeEntity(entity: Entity) { + const index = this.entities.indexOf(entity); + if (index > -1) { + this.entities.splice(index, 1); + this.entityRemoved$.notifyAll(entity); } } /** - * Returns whether the entity's types match query - * @param entity - */ - public matches(entity: Entity): boolean; - - /** - * Returns whether the list of ComponentTypes have at least the same types as the query - * @param types + * Returns a list of entities that match the query + * @param sort Optional sorting function to sort entities returned from the query */ - public matches(types: string[]): boolean; - public matches(typesOrEntity: string[] | Entity): boolean { - let types: string[] = []; - if (typesOrEntity instanceof Entity) { - types = typesOrEntity.types; - } else { - types = typesOrEntity; - } - - let matches = true; - for (const type of this.types) { - matches = matches && types.indexOf(type) > -1; - if (!matches) { - return false; - } + public getEntities(sort?: (a: Entity, b: Entity) => number): Entity>[] { + if (sort) { + this.entities.sort(sort); } - return matches; - } - - public contain(type: string) { - return this.types.indexOf(type) > -1; + return this.entities; } -} +} \ No newline at end of file diff --git a/src/engine/EntityComponentSystem/QueryManager.ts b/src/engine/EntityComponentSystem/QueryManager.ts index 884910001..4f2b5c78e 100644 --- a/src/engine/EntityComponentSystem/QueryManager.ts +++ b/src/engine/EntityComponentSystem/QueryManager.ts @@ -1,99 +1,209 @@ import { Entity } from './Entity'; -import { buildTypeKey } from './Util'; import { Query } from './Query'; -import { Component } from './Component'; +import { Component, ComponentCtor } from './Component'; import { World } from './World'; +import { TagQuery } from './TagQuery'; /** * The query manager is responsible for updating all queries when entities/components change */ export class QueryManager { - private _queries: { [entityComponentKey: string]: Query } = {}; + private _queries = new Map>(); + private _addComponentHandlers = new Map any>(); + private _removeComponentHandlers = new Map any>(); + private _componentToQueriesIndex = new Map, Query[]>(); - constructor(private _world: World) {} + private _tagQueries = new Map>(); + private _addTagHandlers = new Map any>(); + private _removeTagHandlers = new Map any>(); + private _tagToQueriesIndex = new Map[]>(); - /** - * Adds a query to the manager and populates with any entities that match - * @param query - */ - private _addQuery(query: Query) { - this._queries[buildTypeKey(query.types)] = query; - for (const entity of this._world.entityManager.entities) { - query.addEntity(entity); + constructor(private _world: World) {} + + public createQuery>( + requiredComponents: TKnownComponentCtors[]): Query { + const id = Query.createId(requiredComponents); + if (this._queries.has(id)) { + // short circuit if query is already created + return this._queries.get(id) as Query; + } + + const query = new Query(requiredComponents); + + this._queries.set(query.id, query); + + // index maintenance + for (const component of requiredComponents) { + const queries = this._componentToQueriesIndex.get(component); + if (!queries) { + this._componentToQueriesIndex.set(component, [query]); + } else { + queries.push(query); + } + } + + for (const entity of this._world.entities) { + this.addEntity(entity); + } + + return query; + } + + public createTagQuery(requiredTags: TKnownTags[]): TagQuery { + const id = TagQuery.createId(requiredTags); + if (this._tagQueries.has(id)) { + // short circuit if query is already created + return this._tagQueries.get(id) as TagQuery; + } + + const query = new TagQuery(requiredTags); + + this._tagQueries.set(query.id, query); + + // index maintenance + for (const tag of requiredTags) { + const queries = this._tagToQueriesIndex.get(tag); + if (!queries) { + this._tagToQueriesIndex.set(tag, [query]); + } else { + queries.push(query); + } + } + + for (const entity of this._world.entities) { + this.addEntity(entity); } + + return query; } + private _createAddComponentHandler = (entity: Entity) => (c: Component) => { + this.addComponent(entity, c); + }; + + private _createRemoveComponentHandler = (entity: Entity) => (c: Component) => { + this.removeComponent(entity, c); + }; + + private _createAddTagHandler = (entity: Entity) => (tag: string) => { + this.addTag(entity, tag); + }; + + private _createRemoveTagHandler = (entity: Entity) => (tag: string) => { + this.removeTag(entity, tag); + }; + /** - * Removes the query if there are no observers left - * @param query + * Scans queries and locates any that need this entity added + * @param entity */ - public maybeRemoveQuery(query: Query): void { - if (query.observers.length === 0) { - query.clear(); - delete this._queries[buildTypeKey(query.types)]; + addEntity(entity: Entity) { + const maybeAddComponent = this._addComponentHandlers.get(entity); + const maybeRemoveComponent = this._removeComponentHandlers.get(entity); + const addComponent = maybeAddComponent ?? this._createAddComponentHandler(entity); + const removeComponent = maybeRemoveComponent ?? this._createRemoveComponentHandler(entity); + this._addComponentHandlers.set(entity, addComponent); + this._removeComponentHandlers.set(entity, removeComponent); + + const maybeAddTag = this._addTagHandlers.get(entity); + const maybeRemoveTag = this._removeTagHandlers.get(entity); + const addTag = maybeAddTag ?? this._createAddTagHandler(entity); + const removeTag = maybeRemoveTag ?? this._createRemoveTagHandler(entity); + this._addTagHandlers.set(entity, addTag); + this._removeTagHandlers.set(entity, removeTag); + + for (const query of this._queries.values()) { + query.checkAndAdd(entity); + } + for (const tagQuery of this._tagQueries.values()) { + tagQuery.checkAndAdd(entity); } + entity.componentAdded$.subscribe(addComponent); + entity.componentRemoved$.subscribe(removeComponent); + entity.tagAdded$.subscribe(addTag); + entity.tagRemoved$.subscribe(removeTag); } /** - * Adds the entity to any matching query in the query manage + * Scans queries and locates any that need this entity removed * @param entity */ - public addEntity(entity: Entity) { - for (const queryType in this._queries) { - if (this._queries[queryType]) { - this._queries[queryType].addEntity(entity); - } + removeEntity(entity: Entity) { + // Handle components + const addComponent = this._addComponentHandlers.get(entity); + const removeComponent = this._removeComponentHandlers.get(entity); + for (const query of this._queries.values()) { + query.removeEntity(entity); + } + if (addComponent) { + entity.componentAdded$.unsubscribe(addComponent); + } + if (removeComponent) { + entity.componentRemoved$.unsubscribe(removeComponent); + } + + // Handle tags + const addTag = this._addTagHandlers.get(entity); + const removeTag = this._removeTagHandlers.get(entity); + for (const tagQuery of this._tagQueries.values()) { + tagQuery.removeEntity(entity); + } + + if (addTag) { + entity.tagAdded$.unsubscribe(addTag); + } + if (removeTag) { + entity.tagRemoved$.unsubscribe(removeTag); } } /** - * Removes an entity from queries if the removed component disqualifies it + * Updates any queries when a component is added to an entity * @param entity * @param component */ - public removeComponent(entity: Entity, component: Component) { - for (const queryType in this._queries) { - // If the component being removed from an entity is a part of a query, - // it is now disqualified from that query, remove it - if (this._queries[queryType].contain(component.type)) { - this._queries[queryType].removeEntity(entity); - } + addComponent(entity: Entity, component: Component) { + const queries = this._componentToQueriesIndex.get(component.constructor as ComponentCtor) ?? []; + for (const query of queries) { + query.checkAndAdd(entity); } } /** - * Removes an entity from all queries it is currently a part of + * Updates any queries when a component is removed from an entity * @param entity + * @param component */ - public removeEntity(entity: Entity) { - for (const queryType in this._queries) { - this._queries[queryType].removeEntity(entity); + removeComponent(entity: Entity, component: Component) { + const queries = this._componentToQueriesIndex.get(component.constructor as ComponentCtor) ?? []; + for (const query of queries) { + query.removeEntity(entity); } } /** - * Creates a populated query and returns, if the query already exists that will be returned instead of a new instance - * @param types + * Updates any queries when a tag is added to an entity + * @param entity + * @param tag */ - public createQuery(types: readonly string[]): Query { - const maybeExistingQuery = this.getQuery(types); - if (maybeExistingQuery) { - return maybeExistingQuery; + addTag(entity: Entity, tag: string) { + const queries = this._tagToQueriesIndex.get(tag) ?? []; + for (const query of queries) { + query.checkAndAdd(entity); } - const query = new Query(types); - this._addQuery(query); - return query; } /** - * Retrieves an existing query by types if it exists otherwise returns null - * @param types + * Updates any queries when a component is removed from an entity + * @param entity + * @param tag */ - public getQuery(types: readonly string[]): Query { - const key = buildTypeKey(types); - if (this._queries[key]) { - return this._queries[key] as Query; + removeTag(entity: Entity, tag: string) { + const queries = this._tagToQueriesIndex.get(tag) ?? []; + for (const query of queries) { + query.removeEntity(entity); } - return null; } + + } diff --git a/src/engine/EntityComponentSystem/System.ts b/src/engine/EntityComponentSystem/System.ts index 786c5d33d..63384620e 100644 --- a/src/engine/EntityComponentSystem/System.ts +++ b/src/engine/EntityComponentSystem/System.ts @@ -1,7 +1,6 @@ -import { Entity } from './Entity'; -import { Message, Observer } from '../Util/Observable'; -import { Component } from './Component'; import { Scene } from '../Scene'; +import { SystemPriority } from './Priority'; +import { World } from './World'; /** * Enum that determines whether to run the system in the update or draw phase @@ -11,12 +10,12 @@ export enum SystemType { Draw = 'draw' } -export type SystemTypes = ComponentTypes extends Component ? TypeName : never; - /** * An Excalibur [[System]] that updates entities of certain types. * Systems are scene specific * + * + * * Excalibur Systems currently require at least 1 Component type to operated * * Multiple types are declared as a type union @@ -32,13 +31,7 @@ export type SystemTypes = ComponentTypes extends Component -implements Observer { - /** - * The types of entities that this system operates on - * For example ['transform', 'motion'] - */ - abstract readonly types: readonly SystemTypes[]; +export abstract class System { /** * Determine whether the system is called in the [[SystemType.Update]] or the [[SystemType.Draw]] phase. Update is first, then Draw. @@ -50,78 +43,32 @@ implements Observer { * For a system to execute before all other a lower priority value (-1 for example) must be set. * For a system to execute after all other a higher priority value (10 for example) must be set. */ - public priority: number = 0; - - /** - * Optionally specify a sort order for entities passed to the your system - * @param a The left entity - * @param b The right entity - */ - sort?(a: Entity, b: Entity): number; + public priority: number = SystemPriority.Average; /** * Optionally specify an initialize handler * @param scene */ - initialize?(engine: ContextType): void; + initialize?(world: World, scene: Scene): void; /** * Update all entities that match this system's types * @param entities Entities to update that match this system's types - * @param delta Time in milliseconds + * @param elapsedMs Time in milliseconds */ - abstract update(entities: Entity[], delta: number): void; + abstract update(elapsedMs: number): void; /** * Optionally run a preupdate before the system processes matching entities * @param engine * @param elapsedMs Time in milliseconds since the last frame */ - preupdate?(engine: ContextType, elapsedMs: number): void; + preupdate?(engine: Scene, elapsedMs: number): void; /** * Optionally run a postupdate after the system processes matching entities * @param engine * @param elapsedMs Time in milliseconds since the last frame */ - postupdate?(engine: ContextType, elapsedMs: number): void; - - /** - * Systems observe when entities match their types or no longer match their types, override - * @param _entityAddedOrRemoved - */ - public notify(_entityAddedOrRemoved: AddedEntity | RemovedEntity) { - // Override me - } -} - -/** - * An [[Entity]] with [[Component]] types that matches a [[System]] types exists in the current scene. - */ -export class AddedEntity implements Message { - readonly type: 'Entity Added' = 'Entity Added'; - constructor(public data: Entity) {} -} - -/** - * Type guard to check for AddedEntity messages - * @param x - */ -export function isAddedSystemEntity(x: Message): x is AddedEntity { - return !!x && x.type === 'Entity Added'; -} - -/** - * An [[Entity]] with [[Component]] types that no longer matches a [[System]] types exists in the current scene. - */ -export class RemovedEntity implements Message { - readonly type: 'Entity Removed' = 'Entity Removed'; - constructor(public data: Entity) {} -} - -/** - * type guard to check for the RemovedEntity message - */ -export function isRemoveSystemEntity(x: Message): x is RemovedEntity { - return !!x && x.type === 'Entity Removed'; + postupdate?(engine: Scene, elapsedMs: number): void; } diff --git a/src/engine/EntityComponentSystem/SystemManager.ts b/src/engine/EntityComponentSystem/SystemManager.ts index 02a8bac04..facbe846e 100644 --- a/src/engine/EntityComponentSystem/SystemManager.ts +++ b/src/engine/EntityComponentSystem/SystemManager.ts @@ -7,18 +7,24 @@ export interface SystemCtor { new (...args: any[]): T; } +/** + * + */ +export function isSystemConstructor(x: any): x is SystemCtor { + return !!x?.prototype && !!x?.prototype?.constructor?.name; +} + /** * The SystemManager is responsible for keeping track of all systems in a scene. * Systems are scene specific */ -export class SystemManager { +export class SystemManager { /** * List of systems, to add a new system call [[SystemManager.addSystem]] */ - public systems: System[] = []; - public _keyToSystem: { [key: string]: System }; + public systems: System[] = []; public initialized = false; - constructor(private _world: World) {} + constructor(private _world: World) {} /** * Get a system registered in the manager by type @@ -30,22 +36,22 @@ export class SystemManager { /** * Adds a system to the manager, it will now be updated every frame - * @param system + * @param systemOrCtor */ - public addSystem(system: System): void { - // validate system has types - if (!system.types || system.types.length === 0) { - throw new Error(`Attempted to add a System without any types`); + public addSystem(systemOrCtor: SystemCtor | System): void { + let system: System; + if (systemOrCtor instanceof System) { + system = systemOrCtor; + } else { + system = new systemOrCtor(this._world); } - const query = this._world.queryManager.createQuery(system.types); this.systems.push(system); this.systems.sort((a, b) => a.priority - b.priority); - query.register(system); // If systems are added and the manager has already been init'd // then immediately init the system if (this.initialized && system.initialize) { - system.initialize(this._world.context); + system.initialize(this._world, this._world.scene); } } @@ -53,13 +59,8 @@ export class SystemManager { * Removes a system from the manager, it will no longer be updated * @param system */ - public removeSystem(system: System) { + public removeSystem(system: System) { removeItemFromArray(system, this.systems); - const query = this._world.queryManager.getQuery(system.types); - if (query) { - query.unregister(system); - this._world.queryManager.maybeRemoveQuery(query); - } } /** @@ -72,7 +73,7 @@ export class SystemManager { this.initialized = true; for (const s of this.systems) { if (s.initialize) { - s.initialize(this._world.context); + s.initialize(this._world, this._world.scene); } } } @@ -81,32 +82,24 @@ export class SystemManager { /** * Updates all systems * @param type whether this is an update or draw system - * @param context context reference + * @param scene context reference * @param delta time in milliseconds */ - public updateSystems(type: SystemType, context: ContextType, delta: number) { + public updateSystems(type: SystemType, scene: Scene, delta: number) { const systems = this.systems.filter((s) => s.systemType === type); for (const s of systems) { if (s.preupdate) { - s.preupdate(context, delta); + s.preupdate(scene, delta); } } for (const s of systems) { - // Get entities that match the system types, pre-sort - const entities = this._world.queryManager.getQuery(s.types).getEntities(s.sort); - // Initialize entities if needed - if (context instanceof Scene) { - for (const entity of entities) { - entity._initialize(context?.engine); - } - } - s.update(entities, delta); + s.update(delta); } for (const s of systems) { if (s.postupdate) { - s.postupdate(context, delta); + s.postupdate(scene, delta); } } } diff --git a/src/engine/EntityComponentSystem/TagQuery.ts b/src/engine/EntityComponentSystem/TagQuery.ts new file mode 100644 index 000000000..bd9cedc34 --- /dev/null +++ b/src/engine/EntityComponentSystem/TagQuery.ts @@ -0,0 +1,60 @@ +import { Observable } from '../Util/Observable'; +import { Entity } from './Entity'; + + +export class TagQuery { + public readonly id: string; + public tags = new Set(); + public entities: Entity[] = []; + /** + * This fires right after the component is added + */ + public entityAdded$ = new Observable>(); + /** + * This fires right before the component is actually removed from the entity, it will still be available for cleanup purposes + */ + public entityRemoved$ = new Observable>(); + + constructor(public readonly requiredTags: TKnownTags[]) { + if (requiredTags.length === 0) { + throw new Error('Cannot create tag query without tags'); + } + for (const tag of requiredTags) { + this.tags.add(tag); + } + + this.id = TagQuery.createId(requiredTags); + } + + static createId(requiredComponents: string[]) { + return requiredComponents.slice().sort().join('-'); + } + + checkAndAdd(entity: Entity) { + if (!this.entities.includes(entity) && entity.hasAllTags(Array.from(this.tags))) { + this.entities.push(entity); + this.entityAdded$.notifyAll(entity); + return true; + } + return false; + } + + removeEntity(entity: Entity) { + const index = this.entities.indexOf(entity); + if (index > -1) { + this.entities.splice(index, 1); + this.entityRemoved$.notifyAll(entity); + } + } + + /** + * Returns a list of entities that match the query + * @param sort Optional sorting function to sort entities returned from the query + */ + public getEntities(sort?: (a: Entity, b: Entity) => number): Entity[] { + if (sort) { + this.entities.sort(sort); + } + return this.entities; + } +} \ No newline at end of file diff --git a/src/engine/EntityComponentSystem/Types.ts b/src/engine/EntityComponentSystem/Types.ts new file mode 100644 index 000000000..6e6da39c4 --- /dev/null +++ b/src/engine/EntityComponentSystem/Types.ts @@ -0,0 +1 @@ +export type MaybeKnownComponent = Component extends TKnownComponents ? Component : (Component | undefined); \ No newline at end of file diff --git a/src/engine/EntityComponentSystem/Util.ts b/src/engine/EntityComponentSystem/Util.ts deleted file mode 100644 index 6c695571a..000000000 --- a/src/engine/EntityComponentSystem/Util.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const buildTypeKey = (types: readonly string[]) => { - const key = [...types].sort((a, b) => a.localeCompare(b)).join('+'); - return key; -}; diff --git a/src/engine/EntityComponentSystem/World.ts b/src/engine/EntityComponentSystem/World.ts index f181d58d0..66e86fa20 100644 --- a/src/engine/EntityComponentSystem/World.ts +++ b/src/engine/EntityComponentSystem/World.ts @@ -1,31 +1,48 @@ +import { Scene } from '../Scene'; +import { Component, ComponentCtor } from './Component'; import { Entity } from './Entity'; import { EntityManager } from './EntityManager'; +import { Query } from './Query'; import { QueryManager } from './QueryManager'; import { System, SystemType } from './System'; -import { SystemManager } from './SystemManager'; +import { SystemCtor, SystemManager, isSystemConstructor } from './SystemManager'; +import { TagQuery } from './TagQuery'; /** * The World is a self-contained entity component system for a particular context. */ -export class World { +export class World { public queryManager: QueryManager = new QueryManager(this); - public entityManager: EntityManager = new EntityManager(this); - public systemManager: SystemManager = new SystemManager(this); + public entityManager: EntityManager = new EntityManager(this); + public systemManager: SystemManager = new SystemManager(this); /** * The context type is passed to the system updates - * @param context + * @param scene */ - constructor(public context: ContextType) {} + constructor(public scene: Scene) {} + + /** + * Query the ECS world for entities that match your components + * @param requiredTypes + */ + query>( + requiredTypes: TKnownComponentCtors[]): Query { + return this.queryManager.createQuery(requiredTypes); + } + + queryTags(requiredTags: TKnownTags[]): TagQuery { + return this.queryManager.createTagQuery(requiredTags); + } /** * Update systems by type and time elapsed in milliseconds */ update(type: SystemType, delta: number) { if (type === SystemType.Update) { - this.entityManager.updateEntities(this.context, delta); + this.entityManager.updateEntities(this.scene, delta); } - this.systemManager.updateSystems(type, this.context, delta); + this.systemManager.updateSystems(type, this.scene, delta); this.entityManager.findEntitiesForRemoval(); this.entityManager.processComponentRemovals(); this.entityManager.processEntityRemovals(); @@ -40,17 +57,25 @@ export class World { * Add a system to the ECS world * @param system */ - add(system: System): void; - add(entityOrSystem: Entity | System): void { + add(system: System): void; + add(system: SystemCtor): void; + add(entityOrSystem: Entity | System | SystemCtor): void { if (entityOrSystem instanceof Entity) { this.entityManager.addEntity(entityOrSystem); } - if (entityOrSystem instanceof System) { + if (entityOrSystem instanceof System || isSystemConstructor(entityOrSystem)) { this.systemManager.addSystem(entityOrSystem); } } + /** + * Get a system out of the ECS world + */ + get(system: SystemCtor) { + return this.systemManager.get(system); + } + /** * Remove an entity from the ECS world * @param entity @@ -60,8 +85,8 @@ export class World { * Remove a system from the ECS world * @param system */ - remove(system: System): void; - remove(entityOrSystem: Entity | System, deferred = true): void { + remove(system: System): void; + remove(entityOrSystem: Entity | System, deferred = true): void { if (entityOrSystem instanceof Entity) { this.entityManager.removeEntity(entityOrSystem, deferred); } @@ -71,6 +96,10 @@ export class World { } } + get entities() { + return this.entityManager.entities; + } + clearEntities(): void { this.entityManager.clear(); } diff --git a/src/engine/EntityComponentSystem/index.ts b/src/engine/EntityComponentSystem/index.ts index 9a0433f2b..c5ac43b95 100644 --- a/src/engine/EntityComponentSystem/index.ts +++ b/src/engine/EntityComponentSystem/index.ts @@ -2,10 +2,13 @@ export * from './Component'; export * from './Entity'; export * from './EntityManager'; export * from './Query'; +export * from './TagQuery'; export * from './QueryManager'; export * from './System'; export * from './SystemManager'; export * from './World'; +export * from './Types'; +export * from './Priority'; export * from './Components/TransformComponent'; export * from './Components/MotionComponent'; diff --git a/src/engine/EntityComponentSystem/tsconfig.json b/src/engine/EntityComponentSystem/tsconfig.json new file mode 100644 index 000000000..b2e9e0b8f --- /dev/null +++ b/src/engine/EntityComponentSystem/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strict": true + } +} \ No newline at end of file diff --git a/src/engine/Graphics/DebugGraphicsComponent.ts b/src/engine/Graphics/DebugGraphicsComponent.ts index 2d54f1a65..247081651 100644 --- a/src/engine/Graphics/DebugGraphicsComponent.ts +++ b/src/engine/Graphics/DebugGraphicsComponent.ts @@ -9,9 +9,10 @@ import { Component } from '../EntityComponentSystem/Component'; * Will only show when the Engine is set to debug mode [[Engine.showDebug]] or [[Engine.toggleDebug]] * */ -export class DebugGraphicsComponent extends Component<'ex.debuggraphics'> { - readonly type = 'ex.debuggraphics'; - constructor(public draw: (ctx: ExcaliburGraphicsContext, debugFlags: Debug) => void, public useTransform = true) { +export class DebugGraphicsComponent extends Component { + constructor( + public draw: (ctx: ExcaliburGraphicsContext, debugFlags: Debug) => void, + public useTransform = true) { super(); } } \ No newline at end of file diff --git a/src/engine/Graphics/GraphicsComponent.ts b/src/engine/Graphics/GraphicsComponent.ts index 8c3962019..d79773d29 100644 --- a/src/engine/Graphics/GraphicsComponent.ts +++ b/src/engine/Graphics/GraphicsComponent.ts @@ -70,9 +70,8 @@ export interface GraphicsComponentOptions { /** * Component to manage drawings, using with the position component */ -export class GraphicsComponent extends Component<'ex.graphics'> { +export class GraphicsComponent extends Component { private _logger = Logger.getInstance(); - readonly type = 'ex.graphics'; private _current: string = 'default'; private _graphics: Record = {}; diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts index 1fb0f8eb8..601ef6877 100644 --- a/src/engine/Graphics/GraphicsSystem.ts +++ b/src/engine/Graphics/GraphicsSystem.ts @@ -5,7 +5,7 @@ import { vec, Vector } from '../Math/vector'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { Entity } from '../EntityComponentSystem/Entity'; import { Camera } from '../Camera'; -import { AddedEntity, isAddedSystemEntity, RemovedEntity, System, SystemType } from '../EntityComponentSystem'; +import { Query, System, SystemPriority, SystemType, World } from '../EntityComponentSystem'; import { Engine } from '../Engine'; import { GraphicsGroup } from './GraphicsGroup'; import { Particle } from '../Particles'; // this import seems to bomb wallaby @@ -18,21 +18,39 @@ import { Transform } from '../Math/transform'; import { blendTransform } from './TransformInterpolation'; import { Graphic } from './Graphic'; -export class GraphicsSystem extends System { - public readonly types = ['ex.transform', 'ex.graphics'] as const; +export class GraphicsSystem extends System { public readonly systemType = SystemType.Draw; - public priority = 0; + public priority = SystemPriority.Average; private _token = 0; private _graphicsContext: ExcaliburGraphicsContext; private _camera: Camera; private _engine: Engine; - private _sortedTransforms: TransformComponent[] = []; + query: Query; public get sortedTransforms() { return this._sortedTransforms; } - public initialize(scene: Scene): void { + constructor(public world: World) { + super(); + this.query = this.world.query([TransformComponent, GraphicsComponent]); + this.query.entityAdded$.subscribe(e => { + const tx = e.get(TransformComponent); + this._sortedTransforms.push(tx); + tx.zIndexChanged$.subscribe(this._zIndexUpdate); + this._zHasChanged = true; + }); + this.query.entityRemoved$.subscribe(e => { + const tx = e.get(TransformComponent); + tx.zIndexChanged$.unsubscribe(this._zIndexUpdate); + const index = this._sortedTransforms.indexOf(tx); + if (index > -1) { + this._sortedTransforms.splice(index, 1); + } + }); + } + + public initialize(world: World, scene: Scene): void { this._camera = scene.camera; this._engine = scene.engine; } @@ -53,23 +71,7 @@ export class GraphicsSystem extends System -1) { - this._sortedTransforms.splice(index, 1); - } - } - } - - public update(_entities: Entity[], delta: number): void { + public update(delta: number): void { this._token++; let graphics: GraphicsComponent; FontCache.checkAndClearCache(); diff --git a/src/engine/Graphics/OffscreenSystem.ts b/src/engine/Graphics/OffscreenSystem.ts index 1d8614bd6..e10dc8201 100644 --- a/src/engine/Graphics/OffscreenSystem.ts +++ b/src/engine/Graphics/OffscreenSystem.ts @@ -2,7 +2,6 @@ import { GraphicsComponent } from './GraphicsComponent'; import { EnterViewPortEvent, ExitViewPortEvent } from '../Events'; import { Scene } from '../Scene'; import { Screen } from '../Screen'; -import { Entity } from '../EntityComponentSystem/Entity'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { Camera } from '../Camera'; import { System, SystemType } from '../EntityComponentSystem/System'; @@ -10,27 +9,34 @@ import { ParallaxComponent } from './ParallaxComponent'; import { Vector } from '../Math/vector'; import { CoordPlane } from '../Math/coord-plane'; import { BoundingBox } from '../Collision/BoundingBox'; +import { Query, SystemPriority, World } from '../EntityComponentSystem'; -export class OffscreenSystem extends System { - public readonly types = ['ex.transform', 'ex.graphics'] as const; +export class OffscreenSystem extends System { public systemType = SystemType.Draw; - priority: number = -1; + priority: number = SystemPriority.Higher; private _camera: Camera; private _screen: Screen; private _worldBounds: BoundingBox; + query: Query; - public initialize(scene: Scene): void { + + constructor(public world: World) { + super(); + this.query = this.world.query([TransformComponent, GraphicsComponent]); + } + + public initialize(world: World, scene: Scene): void { this._camera = scene.camera; this._screen = scene.engine.screen; } - update(entities: Entity[]): void { + update(): void { this._worldBounds = this._screen.getWorldBounds(); let transform: TransformComponent; let graphics: GraphicsComponent; let maybeParallax: ParallaxComponent; - for (const entity of entities) { + for (const entity of this.query.entities) { graphics = entity.get(GraphicsComponent); transform = entity.get(TransformComponent); maybeParallax = entity.get(ParallaxComponent); diff --git a/src/engine/Graphics/ParallaxComponent.ts b/src/engine/Graphics/ParallaxComponent.ts index c8fcb1cba..ac5559c75 100644 --- a/src/engine/Graphics/ParallaxComponent.ts +++ b/src/engine/Graphics/ParallaxComponent.ts @@ -1,8 +1,7 @@ import { Component } from '../EntityComponentSystem/Component'; import { vec, Vector } from '../Math/vector'; -export class ParallaxComponent extends Component<'ex.parallax'> { - readonly type = 'ex.parallax'; +export class ParallaxComponent extends Component { parallaxFactor = vec(1.0, 1.0); diff --git a/src/engine/Input/PointerComponent.ts b/src/engine/Input/PointerComponent.ts index c1b1c1af9..0c0bce7d6 100644 --- a/src/engine/Input/PointerComponent.ts +++ b/src/engine/Input/PointerComponent.ts @@ -9,8 +9,7 @@ import { Component } from '../EntityComponentSystem/Component'; * If both collider shape and graphics bounds are enabled it will fire events if either or * are intersecting the pointer. */ -export class PointerComponent extends Component<'ex.pointer'> { - public readonly type = 'ex.pointer'; +export class PointerComponent extends Component { /** * Use any existing Collider component geometry for pointer events. This is useful if you want * user pointer events only to trigger on the same collision geometry used in the collider component diff --git a/src/engine/Input/PointerEventReceiver.ts b/src/engine/Input/PointerEventReceiver.ts index bf130998e..c713b5457 100644 --- a/src/engine/Input/PointerEventReceiver.ts +++ b/src/engine/Input/PointerEventReceiver.ts @@ -495,10 +495,9 @@ export class PointerEventReceiver { } // Force update pointer system - const pointerSystem = this.engine.currentScene.world.systemManager.get(PointerSystem); - const transformEntities = this.engine.currentScene.world.queryManager.createQuery(pointerSystem.types); - pointerSystem.preupdate(); - pointerSystem.update(transformEntities.getEntities()); + const pointerSystem = this.engine.currentScene.world.get(PointerSystem); + pointerSystem.preupdate(this.engine.currentScene, 1); + pointerSystem.update(1); } private _nativeButtonToPointerButton(s: NativePointerButton): PointerButton { diff --git a/src/engine/Input/PointerSystem.ts b/src/engine/Input/PointerSystem.ts index 394e16ccd..e4d732f5b 100644 --- a/src/engine/Input/PointerSystem.ts +++ b/src/engine/Input/PointerSystem.ts @@ -5,9 +5,9 @@ import { TransformComponent, SystemType, Entity, - AddedEntity, - RemovedEntity, - isAddedSystemEntity + World, + Query, + SystemPriority } from '../EntityComponentSystem'; import { GraphicsComponent } from '../Graphics/GraphicsComponent'; import { Scene } from '../Scene'; @@ -23,13 +23,35 @@ import { CoordPlane } from '../Math/coord-plane'; * The PointerSystem can be optionally configured by the [[PointerComponent]], by default Entities use * the [[Collider]]'s shape for pointer events. */ -export class PointerSystem extends System { - public readonly types = ['ex.transform', 'ex.pointer'] as const; +export class PointerSystem extends System { public readonly systemType = SystemType.Update; - public priority = -1; + public priority = SystemPriority.Higher; private _engine: Engine; private _receiver: PointerEventReceiver; + query: Query; + + constructor(public world: World) { + super(); + this.query = this.world.query([TransformComponent, PointerComponent]); + this.query.entityAdded$.subscribe(e => { + const tx = e.get(TransformComponent); + this._sortedTransforms.push(tx); + this._sortedEntities.push(tx.owner); + tx.zIndexChanged$.subscribe(this._zIndexUpdate); + this._zHasChanged = true; + }); + + this.query.entityRemoved$.subscribe(e => { + const tx = e.get(TransformComponent); + tx.zIndexChanged$.unsubscribe(this._zIndexUpdate); + const index = this._sortedTransforms.indexOf(tx); + if (index > -1) { + this._sortedTransforms.splice(index, 1); + this._sortedEntities.splice(index, 1); + } + }); + } /** * Optionally override component configuration for all entities @@ -43,7 +65,7 @@ export class PointerSystem extends System public lastFrameEntityToPointers = new Map(); public currentFrameEntityToPointers = new Map(); - public initialize(scene: Scene): void { + public initialize(world: World, scene: Scene): void { this._engine = scene.engine; } @@ -67,23 +89,7 @@ export class PointerSystem extends System } } - public notify(entityAddedOrRemoved: AddedEntity | RemovedEntity): void { - if (isAddedSystemEntity(entityAddedOrRemoved)) { - const tx = entityAddedOrRemoved.data.get(TransformComponent); - this._sortedTransforms.push(tx); - this._sortedEntities.push(tx.owner); - tx.zIndexChanged$.subscribe(this._zIndexUpdate); - this._zHasChanged = true; - } else { - const tx = entityAddedOrRemoved.data.get(TransformComponent); - tx.zIndexChanged$.unsubscribe(this._zIndexUpdate); - const index = this._sortedTransforms.indexOf(tx); - if (index > -1) { - this._sortedTransforms.splice(index, 1); - this._sortedEntities.splice(index, 1); - } - } - } + public entityCurrentlyUnderPointer(entity: Entity, pointerId: number) { return this.currentFrameEntityToPointers.has(entity.id) && @@ -114,7 +120,7 @@ export class PointerSystem extends System this.currentFrameEntityToPointers.set(entity.id, pointers.concat(pointerId)); } - public update(_entities: Entity[]): void { + public update(): void { // Locate all the pointer/entity mappings this._processPointerToEntity(this._sortedEntities); diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts index a7105619f..346e4b44d 100644 --- a/src/engine/Scene.ts +++ b/src/engine/Scene.ts @@ -104,7 +104,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate /** * The ECS world for the scene */ - public world = new World(this); + public world: World = new World(this); /** * The Excalibur physics world for the scene. Used to interact @@ -118,7 +118,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate * The actors in the current scene */ public get actors(): readonly Actor[] { - return this.world.entityManager.entities.filter((e) => { + return this.world.entityManager.entities.filter((e: Entity) => { return e instanceof Actor; }) as Actor[]; } @@ -134,7 +134,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate * The triggers in the current scene */ public get triggers(): readonly Trigger[] { - return this.world.entityManager.entities.filter((e) => { + return this.world.entityManager.entities.filter((e: Entity) => { return e instanceof Trigger; }) as Trigger[]; } @@ -143,7 +143,7 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate * The [[TileMap]]s in the scene, if any */ public get tileMaps(): readonly TileMap[] { - return this.world.entityManager.entities.filter((e) => { + return this.world.entityManager.entities.filter((e: Entity) => { return e instanceof TileMap; }) as TileMap[]; } @@ -164,15 +164,15 @@ implements CanInitialize, CanActivate, CanDeactivate, CanUpdate // Initialize systems // Update - this.world.add(new ActionsSystem()); - this.world.add(new MotionSystem()); - this.world.add(new CollisionSystem(this.physics)); - this.world.add(new PointerSystem()); - this.world.add(new IsometricEntitySystem()); + this.world.add(ActionsSystem); + this.world.add(MotionSystem); + this.world.add(new CollisionSystem(this.world, this.physics)); + this.world.add(PointerSystem); + this.world.add(IsometricEntitySystem); // Draw - this.world.add(new OffscreenSystem()); - this.world.add(new GraphicsSystem()); - this.world.add(new DebugSystem()); + this.world.add(OffscreenSystem); + this.world.add(GraphicsSystem); + this.world.add(DebugSystem); } public emit>(eventName: TEventName, event: SceneEvents[TEventName]): void; diff --git a/src/engine/TileMap/IsometricEntityComponent.ts b/src/engine/TileMap/IsometricEntityComponent.ts index 5a6d69c64..5ce118663 100644 --- a/src/engine/TileMap/IsometricEntityComponent.ts +++ b/src/engine/TileMap/IsometricEntityComponent.ts @@ -8,8 +8,7 @@ export interface IsometricEntityComponentOptions { tileHeight: number; } -export class IsometricEntityComponent extends Component<'ex.isometricentity'> { - public readonly type = 'ex.isometricentity'; +export class IsometricEntityComponent extends Component { /** * Vertical "height" in the isometric world */ diff --git a/src/engine/TileMap/IsometricEntitySystem.ts b/src/engine/TileMap/IsometricEntitySystem.ts index 9395765c3..95368263f 100644 --- a/src/engine/TileMap/IsometricEntitySystem.ts +++ b/src/engine/TileMap/IsometricEntitySystem.ts @@ -1,17 +1,22 @@ import { System, SystemType } from '../EntityComponentSystem/System'; -import { Entity } from '../EntityComponentSystem/Entity'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { IsometricEntityComponent } from './IsometricEntityComponent'; +import { Query, SystemPriority, World } from '../EntityComponentSystem'; -export class IsometricEntitySystem extends System { - public readonly types = ['ex.transform', 'ex.isometricentity'] as const; +export class IsometricEntitySystem extends System { public readonly systemType = SystemType.Update; - priority: number = 99; - update(entities: Entity[], _delta: number): void { + priority: number = SystemPriority.Lower; + query: Query; + constructor(public world: World) { + super(); + this.query = this.world.query([TransformComponent, IsometricEntityComponent]); + } + + update(): void { let transform: TransformComponent; let iso: IsometricEntityComponent; - for (const entity of entities) { + for (const entity of this.query.entities) { transform = entity.get(TransformComponent); iso = entity.get(IsometricEntityComponent); diff --git a/src/engine/TileMap/TileMap.ts b/src/engine/TileMap/TileMap.ts index 043157696..312360020 100644 --- a/src/engine/TileMap/TileMap.ts +++ b/src/engine/TileMap/TileMap.ts @@ -206,7 +206,7 @@ export class TileMap extends Entity { * @param options */ constructor(options: TileMapOptions) { - super(null, options.name); + super([], options.name); this.addComponent(new TransformComponent()); this.addComponent(new MotionComponent()); this.addComponent( @@ -335,7 +335,7 @@ export class TileMap extends Entity { * If checkAndCombine returns true, the collider was successfully merged and should be thrown away * @param current current collider to test * @param colliders List of colliders to consider merging with - * @param maxLookBack The amount of colliders to look back for combindation + * @param maxLookBack The amount of colliders to look back for combination * @returns false when no combination found, true when successfully combined */ const checkAndCombine = (current: BoundingBox, colliders: BoundingBox[], maxLookBack = 10) => { diff --git a/src/spec/ActorSpec.ts b/src/spec/ActorSpec.ts index e18039108..5899a143c 100644 --- a/src/spec/ActorSpec.ts +++ b/src/spec/ActorSpec.ts @@ -21,10 +21,10 @@ describe('A game actor', () => { engine = TestUtils.engine({ width: 100, height: 100 }); actor = new ex.Actor({name: 'Default'}); actor.body.collisionType = ex.CollisionType.Active; - motionSystem = new ex.MotionSystem(); - collisionSystem = new ex.CollisionSystem(new PhysicsWorld()); - actionSystem = new ex.ActionsSystem(); scene = new ex.Scene(); + motionSystem = new ex.MotionSystem(scene.world); + collisionSystem = new ex.CollisionSystem(scene.world, new PhysicsWorld()); + actionSystem = new ex.ActionsSystem(scene.world); scene.add(actor); engine.addScene('test', scene); await engine.goToScene('test'); @@ -36,8 +36,8 @@ describe('A game actor', () => { await engine.start(); const clock = engine.clock as ex.TestClock; clock.step(1); - collisionSystem.initialize(scene); - scene.world.systemManager.get(ex.PointerSystem).initialize(scene); + collisionSystem.initialize(scene.world, scene); + scene.world.systemManager.get(ex.PointerSystem).initialize(scene.world, scene); ex.Physics.useArcadePhysics(); ex.Physics.acc.setTo(0, 0); @@ -213,7 +213,7 @@ describe('A game actor', () => { actor.pos = ex.vec(10, 10); actor.vel = ex.vec(10, 10); - motionSystem.update([actor], 1000); + motionSystem.update(1000); expect(actor.oldPos.x).toBe(10); expect(actor.oldPos.y).toBe(10); @@ -238,11 +238,11 @@ describe('A game actor', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); - motionSystem.update([actor], 1000); + motionSystem.update(1000); expect(actor.pos.x).toBe(-10); expect(actor.pos.y).toBe(10); - motionSystem.update([actor], 1000); + motionSystem.update(1000); expect(actor.pos.x).toBe(-20); expect(actor.pos.y).toBe(20); }); @@ -252,7 +252,7 @@ describe('A game actor', () => { actor.vel = ex.vec(10, 10); actor.acc = ex.vec(10, 10); - motionSystem.update([actor], 1000); + motionSystem.update(1000); expect(actor.oldVel.x).toBe(10); expect(actor.oldVel.y).toBe(10); @@ -448,8 +448,8 @@ describe('A game actor', () => { scene.add(actor); scene.add(other); - collisionSystem.update([actor, other], 20); - collisionSystem.update([actor, other], 20); + collisionSystem.update(20); + collisionSystem.update(20); scene.update(engine, 20); scene.update(engine, 20); @@ -466,7 +466,7 @@ describe('A game actor', () => { const child = new ex.Actor({ x: 10, y: 0, width: 10, height: 10 }); // (20, 10) actor.addChild(child); - motionSystem.update([actor], 100); + motionSystem.update(100); expect(child.getGlobalPos().x).toBeCloseTo(10, 0.001); expect(child.getGlobalPos().y).toBeCloseTo(20, 0.001); @@ -483,7 +483,7 @@ describe('A game actor', () => { actor.addChild(child); child.addChild(grandchild); - motionSystem.update([actor], 100); + motionSystem.update(100); expect(grandchild.getGlobalRotation()).toBe(rotation); expect(grandchild.getGlobalPos().x).toBeCloseTo(10, 0.001); @@ -498,7 +498,7 @@ describe('A game actor', () => { const child = new ex.Actor({ x: 10, y: 10, width: 10, height: 10 }); actor.addChild(child); - motionSystem.update([actor], 100); + motionSystem.update(100); expect(child.getGlobalPos().x).toBe(30); expect(child.getGlobalPos().y).toBe(30); @@ -514,7 +514,7 @@ describe('A game actor', () => { actor.addChild(child); child.addChild(grandchild); - motionSystem.update([actor], 100); + motionSystem.update(100); // Logic: // p = (10, 10) @@ -534,7 +534,7 @@ describe('A game actor', () => { const child = new ex.Actor({ x: 10, y: 0, width: 10, height: 10 }); // (30, 10) actor.addChild(child); - motionSystem.update([actor], 100); + motionSystem.update(100); expect(child.getGlobalPos().x).toBeCloseTo(10, 0.001); expect(child.getGlobalPos().y).toBeCloseTo(30, 0.001); @@ -552,7 +552,7 @@ describe('A game actor', () => { actor.addChild(child); child.addChild(grandchild); - motionSystem.update([actor], 100); + motionSystem.update(100); expect(grandchild.getGlobalPos().x).toBeCloseTo(10, 0.001); expect(grandchild.getGlobalPos().y).toBeCloseTo(50, 0.001); @@ -567,13 +567,13 @@ describe('A game actor', () => { expect(childActor.pos.y).toBe(50); actor.addChild(childActor); - actionSystem.notify(new ex.AddedEntity(actor)); + actionSystem.query.checkAndAdd(actor); actor.actions.moveTo(10, 15, 1000); - actionSystem.update([actor], 1000); - motionSystem.update([actor], 1000); - actionSystem.update([actor], 1); - motionSystem.update([actor], 1); + actionSystem.update(1000); + motionSystem.update(1000); + actionSystem.update(1); + motionSystem.update(1); expect(childActor.getGlobalPos().x).toBe(60); expect(childActor.getGlobalPos().y).toBe(65); @@ -635,8 +635,8 @@ describe('A game actor', () => { await TestUtils.runToReady(engine); actor = new ex.Actor(); actor.body.collisionType = ex.CollisionType.Active; - motionSystem = new ex.MotionSystem(); - collisionSystem = new ex.CollisionSystem(new PhysicsWorld()); + motionSystem = new ex.MotionSystem(engine.currentScene.world); + collisionSystem = new ex.CollisionSystem(engine.currentScene.world, new PhysicsWorld()); scene = new ex.Scene(); scene.add(actor); engine.addScene('test', scene); @@ -878,7 +878,7 @@ describe('A game actor', () => { // in world coordinates this should be true expect(child.contains(550.01, 50.01)).withContext('(550.1, 50.1) world should be top-left of of child').toBeTruthy(); - expect(child.contains(650, 150)).withContext('(650, 150) world should be bottom-rght of child').toBeTruthy(); + expect(child.contains(650, 150)).withContext('(650, 150) world should be bottom-right of child').toBeTruthy(); // second order child shifted to the origin expect(child2.contains(-49.99, -49.99)).withContext('(-50, -50) world should be top left of second order child').toBeTruthy(); diff --git a/src/spec/AnimationSpec.ts b/src/spec/AnimationSpec.ts index 71af1a51c..aed13d877 100644 --- a/src/spec/AnimationSpec.ts +++ b/src/spec/AnimationSpec.ts @@ -459,7 +459,7 @@ describe('A Graphics Animation', () => { anim.reset(); expect(anim.currentFrame).toBe(anim.frames[0]); anim.tick(100, 2); - // Reseting should re-fire the first tick frame event + // Reset should re-fire the first tick frame event expect(onFrame).toHaveBeenCalledTimes(2); }); diff --git a/src/spec/ColliderComponentSpec.ts b/src/spec/ColliderComponentSpec.ts index b18496b2f..daf1e779b 100644 --- a/src/spec/ColliderComponentSpec.ts +++ b/src/spec/ColliderComponentSpec.ts @@ -82,7 +82,7 @@ describe('A ColliderComponent', () => { const e = new ex.Entity(); e.addComponent(comp); - e.removeComponent(comp, true); + e.removeComponent(ex.ColliderComponent, true); expect(comp.events.clear).toHaveBeenCalled(); }); diff --git a/src/spec/ComponentSpec.ts b/src/spec/ComponentSpec.ts index 3c7a14453..5a3e4972e 100644 --- a/src/spec/ComponentSpec.ts +++ b/src/spec/ComponentSpec.ts @@ -1,12 +1,9 @@ import * as ex from '@excalibur'; -class Dep extends ex.Component<'dep'> { - public readonly type = 'dep'; -} +class Dep extends ex.Component {} -class ComponentImplementation extends ex.Component<'imp'> { +class ComponentImplementation extends ex.Component { public dependencies = [Dep]; - public readonly type = 'imp'; public otherprop = 'somestring'; constructor(public clonable: ex.Vector = ex.Vector.Zero) { super(); @@ -24,8 +21,7 @@ describe('A Component', () => { it('can be created', () => { const imp = new ComponentImplementation(); - expect(imp.type).toBe('imp'); - expect(imp.owner).toBe(null); + expect(imp.owner).toBe(undefined); expect(imp.onAdd).not.toHaveBeenCalled(); expect(imp.onRemove).not.toHaveBeenCalled(); }); @@ -35,14 +31,13 @@ describe('A Component', () => { const imp = new ComponentImplementation(); entity.addComponent(imp); - expect(entity.types).toEqual(['dep', 'imp']); - expect(imp.type).toBe('imp'); + expect(entity.types).toEqual([Dep, ComponentImplementation]); expect(imp.owner).toBe(entity); expect(imp.onAdd).toHaveBeenCalledWith(entity); expect(imp.onRemove).not.toHaveBeenCalled(); - entity.removeComponent(imp, true); - expect(imp.owner).toBe(null); + entity.removeComponent(ComponentImplementation, true); + expect(imp.owner).toBe(undefined); expect(imp.onRemove).toHaveBeenCalledWith(entity); }); diff --git a/src/spec/DebugSystemSpec.ts b/src/spec/DebugSystemSpec.ts index 6bb2df744..74d17a4d7 100644 --- a/src/spec/DebugSystemSpec.ts +++ b/src/spec/DebugSystemSpec.ts @@ -39,26 +39,29 @@ describe('DebugSystem', () => { }); it('does not crash with an empty collider', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); await (engine.graphicsContext.debug as any)._debugText.load(); const entity = new ex.Entity([new ex.TransformComponent(), new ex.ColliderComponent()]); + debugSystem.query.checkAndAdd(entity); engine.debug.collider.showAll = true; expect(() => { - debugSystem.update([entity], 100); + debugSystem.update(); }).not.toThrow(); }); it('can show transform and entity info', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); await (engine.graphicsContext.debug as any)._debugText.load(); @@ -67,7 +70,8 @@ describe('DebugSystem', () => { actor.id = 0; engine.debug.transform.showAll = true; engine.debug.entity.showAll = true; - debugSystem.update([actor], 100); + debugSystem.query.checkAndAdd(actor); + debugSystem.update(); engine.graphicsContext.flush(); @@ -75,9 +79,10 @@ describe('DebugSystem', () => { }); it('can show motion info', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); await (engine.graphicsContext.debug as any)._debugText.load(); @@ -86,10 +91,11 @@ describe('DebugSystem', () => { actor.id = 0; actor.vel = ex.vec(100, 0); actor.acc = ex.vec(100, -100); + debugSystem.query.checkAndAdd(actor); engine.debug.motion.showAll = true; engine.debug.collider.showGeometry = true; engine.debug.collider.geometryLineWidth = 2; - debugSystem.update([actor], 100); + debugSystem.update(); engine.graphicsContext.flush(); @@ -97,9 +103,10 @@ describe('DebugSystem', () => { }); it('can show body info', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); await (engine.graphicsContext.debug as any)._debugText.load(); @@ -108,8 +115,9 @@ describe('DebugSystem', () => { actor.id = 0; actor.vel = ex.vec(100, 0); actor.acc = ex.vec(100, -100); + debugSystem.query.checkAndAdd(actor); engine.debug.body.showAll = true; - debugSystem.update([actor], 100); + debugSystem.update(); engine.graphicsContext.flush(); @@ -117,18 +125,20 @@ describe('DebugSystem', () => { }); it('can show collider info', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); await (engine.graphicsContext.debug as any)._debugText.load(); const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); actor.id = 0; + debugSystem.query.checkAndAdd(actor); engine.debug.entity.showId = true; engine.debug.collider.showAll = true; - debugSystem.update([actor], 100); + debugSystem.update(); engine.graphicsContext.flush(); @@ -136,9 +146,10 @@ describe('DebugSystem', () => { }); it('can show composite collider info', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); await (engine.graphicsContext.debug as any)._debugText.load(); @@ -146,9 +157,10 @@ describe('DebugSystem', () => { const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); actor.collider.useCompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(150, 20), ex.Shape.Box(10, 150)]); actor.id = 0; + debugSystem.query.checkAndAdd(actor); engine.debug.collider.showAll = true; engine.debug.collider.geometryLineWidth = 3; - debugSystem.update([actor], 100); + debugSystem.update(); engine.graphicsContext.flush(); @@ -156,9 +168,10 @@ describe('DebugSystem', () => { }); it('can show graphics info', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); await (engine.graphicsContext.debug as any)._debugText.load(); @@ -166,11 +179,12 @@ describe('DebugSystem', () => { const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50 }); actor.graphics.use(new ex.Rectangle({ width: 200, height: 100, color: ex.Color.Red })); actor.id = 0; + debugSystem.query.checkAndAdd(actor); engine.debug.collider.showBounds = false; engine.debug.collider.showGeometry = false; engine.debug.collider.showOwner = false; engine.debug.graphics.showAll = true; - debugSystem.update([actor], 100); + debugSystem.update(); engine.graphicsContext.flush(); @@ -178,9 +192,10 @@ describe('DebugSystem', () => { }); it('can show DebugGraphicsComponent', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); @@ -189,7 +204,8 @@ describe('DebugSystem', () => { new ex.DebugGraphicsComponent(ctx => { ctx.drawCircle(ex.vec(250, 250), 100, ex.Color.Blue); })]); - debugSystem.update([entity], 100); + debugSystem.query.checkAndAdd(entity); + debugSystem.update(); engine.graphicsContext.flush(); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) @@ -197,9 +213,10 @@ describe('DebugSystem', () => { }); it('can debug draw a tilemap', async () => { - const debugSystem = new ex.DebugSystem(); + const world = engine.currentScene.world; + const debugSystem = new ex.DebugSystem(world); engine.currentScene.world.add(debugSystem); - debugSystem.initialize(engine.currentScene); + debugSystem.initialize(world, engine.currentScene); engine.graphicsContext.clear(); engine.debug.tilemap.showGrid = true; @@ -215,7 +232,8 @@ describe('DebugSystem', () => { }); tilemap.tiles[0].solid = true; tilemap.update(engine, 1); - debugSystem.update([tilemap], 100); + debugSystem.query.checkAndAdd(tilemap); + debugSystem.update(); engine.graphicsContext.flush(); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)).toEqualImage('src/spec/images/DebugSystemSpec/tilemap-debug.png'); diff --git a/src/spec/EntityManagerSpec.ts b/src/spec/EntityManagerSpec.ts index 1f6b00fff..1a6773005 100644 --- a/src/spec/EntityManagerSpec.ts +++ b/src/spec/EntityManagerSpec.ts @@ -39,40 +39,6 @@ describe('An EntityManager', () => { expect(entityManager.entities).toEqual([]); }); - it('will be notified when entity components are added', (done) => { - const entityManager = new ex.EntityManager(new ex.World(null)); - const entity = new ex.Entity(); - const componentA = new FakeComponent('A'); - entityManager.addEntity(entity); - - entityManager.notify = (message) => { - expect(message.type).toBe('Component Added'); - expect(message.data.entity).toBe(entity); - expect(message.data.component).toBe(componentA); - done(); - }; - - entity.addComponent(componentA); - }); - - it('will be notified when entity components are removed', (done) => { - const entityManager = new ex.EntityManager(new ex.World(null)); - const entity = new ex.Entity(); - const componentA = new FakeComponent('A'); - entity.addComponent(componentA); - entityManager.addEntity(entity); - - entityManager.notify = (message) => { - expect(message.type).toBe('Component Removed'); - expect(message.data.entity).toBe(entity); - expect(message.data.component).toBe(componentA); - done(); - }; - - entity.removeComponent(componentA); - entity.processComponentRemoval(); - }); - it('can find entities by name', () => { const entityManager = new ex.EntityManager(new ex.World(null)); const entity = new ex.Entity([], 'some-e'); diff --git a/src/spec/EntitySpec.ts b/src/spec/EntitySpec.ts index 6234cc831..1a6981e0f 100644 --- a/src/spec/EntitySpec.ts +++ b/src/spec/EntitySpec.ts @@ -1,11 +1,10 @@ import * as ex from '@excalibur'; -import { TagComponent } from '@excalibur'; -class FakeComponent extends ex.Component { - constructor(public type: string) { - super(); - } -} +class FakeComponentA extends ex.Component {} +class FakeComponentB extends ex.Component {} +class FakeComponentC extends ex.Component {} +class FakeComponentD extends ex.Component {} +class FakeComponentZ extends ex.Component {} describe('An entity', () => { it('exists', () => { @@ -13,22 +12,21 @@ describe('An entity', () => { }); it('can be constructed with a list of components', () => { - const e = new ex.Entity([new FakeComponent('A'), new FakeComponent('B'), new FakeComponent('C')]); + const e = new ex.Entity([new FakeComponentA, new FakeComponentB, new FakeComponentC]); - expect(e.has('A')).toBe(true); - expect(e.has('B')).toBe(true); - expect(e.has('C')).toBe(true); + expect(e.has(FakeComponentA)).toBe(true); + expect(e.has(FakeComponentB)).toBe(true); + expect(e.has(FakeComponentC)).toBe(true); }); it('can override existing components', () => { - const e = new ex.Entity([ - new FakeComponent('A') - ]); + const original = new FakeComponentA; + const e = new ex.Entity([original]); spyOn(e, 'removeComponent'); - const newComponent = new FakeComponent('A'); + const newComponent = new FakeComponentA; e.addComponent(newComponent, true); - expect(e.removeComponent).toHaveBeenCalledWith(newComponent, true); + expect(e.removeComponent).toHaveBeenCalledWith(FakeComponentA, true); }); it('has a unique id', () => { @@ -65,44 +63,44 @@ describe('An entity', () => { it('can have types by component', () => { const entity = new ex.Entity(); - const typeA = new FakeComponent('A'); - const typeB = new FakeComponent('B'); + const typeA = new FakeComponentA; + const typeB = new FakeComponentB; expect(entity.types).toEqual([]); entity.addComponent(typeA); - expect(entity.types).toEqual(['A']); + expect(entity.types).toEqual([FakeComponentA]); entity.addComponent(typeB); - expect(entity.types).toEqual(['A', 'B']); - entity.removeComponent(typeA, true); - expect(entity.types).toEqual(['B']); - entity.removeComponent(typeB, true); + expect(entity.types).toEqual([ FakeComponentA, FakeComponentB]); + entity.removeComponent(FakeComponentA, true); + expect(entity.types).toEqual([FakeComponentB]); + entity.removeComponent(FakeComponentB, true); expect(entity.types).toEqual([]); }); it('can get a list of components', () => { const entity = new ex.Entity(); - const typeA = new FakeComponent('A'); - const typeB = new FakeComponent('B'); - const typeC = new FakeComponent('C'); + const typeA = new FakeComponentA; + const typeB = new FakeComponentB; + const typeC = new FakeComponentC; expect(entity.getComponents()).toEqual([]); entity.addComponent(typeA).addComponent(typeB).addComponent(typeC); - expect(entity.getComponents().sort((a, b) => a.type.localeCompare(b.type))).toEqual([typeA, typeB, typeC]); + expect(entity.getComponents().sort((a, b) => a.constructor.name.localeCompare(b.constructor.name))).toEqual([typeA, typeB, typeC]); }); it('can have type from tag components', () => { const entity = new ex.Entity(); - const isOffscreen = new TagComponent('offscreen'); - const nonTag = new FakeComponent('A'); + // const isOffscreen = new TagComponent('offscreen'); + const nonTag = new FakeComponentA; expect(entity.types).toEqual([]); - entity.addComponent(isOffscreen); + entity.addTag('offscreen'); entity.addComponent(nonTag); - expect(entity.types).toEqual(['offscreen', 'A']); - expect(entity.tags).toEqual(['offscreen']); + expect(entity.types).toEqual([FakeComponentA]); + expect(entity.tags).toEqual(new Set(['offscreen'])); expect(entity.hasTag('offscreen')).toBeTrue(); }); @@ -112,27 +110,21 @@ describe('An entity', () => { entity.addTag('someTag2'); entity.addTag('someTag3'); - expect(entity.tags).toEqual(['someTag1', 'someTag2', 'someTag3']); - - // deferred removal + expect(entity.tags).toEqual(new Set(['someTag1', 'someTag2', 'someTag3'])); entity.removeTag('someTag3'); - expect(entity.tags).toEqual(['someTag1', 'someTag2', 'someTag3']); - entity.processComponentRemoval(); - expect(entity.tags).toEqual(['someTag1', 'someTag2']); - // immediate removal - entity.removeTag('someTag2', true); - expect(entity.tags).toEqual(['someTag1']); + expect(entity.tags).toEqual(new Set(['someTag1', 'someTag2'])); + entity.removeTag('someTag2'); + expect(entity.tags).toEqual(new Set(['someTag1'])); }); it('can be observed for added changes', (done) => { const entity = new ex.Entity(); - const typeA = new FakeComponent('A'); + const typeA = new FakeComponentA; entity.componentAdded$.register({ notify: (change) => { - expect(change.type).toBe('Component Added'); - expect(change.data.entity).toBe(entity); - expect(change.data.component).toBe(typeA); + expect(change.owner).toBe(entity); + expect(change).toBe(typeA); done(); } }); @@ -141,63 +133,62 @@ describe('An entity', () => { it('can be observed for removed changes', (done) => { const entity = new ex.Entity(); - const typeA = new FakeComponent('A'); + const typeA = new FakeComponentA; entity.addComponent(typeA); entity.componentRemoved$.register({ notify: (change) => { - expect(change.type).toBe('Component Removed'); - expect(change.data.entity).toBe(entity); - expect(change.data.component).toBe(typeA); + expect(change.owner).toBe(entity); + expect(change).toBe(typeA); done(); } }); - entity.removeComponent(typeA); + entity.removeComponent(FakeComponentA); entity.processComponentRemoval(); }); it('can be cloned', () => { const entity = new ex.Entity(); - const typeA = new FakeComponent('A'); - const typeB = new FakeComponent('B'); + const typeA = new FakeComponentA; + const typeB = new FakeComponentB; entity.addComponent(typeA); entity.addComponent(typeB); - entity.addChild(new ex.Entity([new FakeComponent('Z')])); + entity.addChild(new ex.Entity([new FakeComponentZ])); const clone = entity.clone(); expect(clone).not.toBe(entity); expect(clone.id).not.toBe(entity.id); - expect(clone.get('A')).not.toBe(entity.get('A')); - expect(clone.get('B')).not.toBe(entity.get('B')); - expect(clone.get('A').type).toBe(entity.get('A').type); - expect(clone.get('B').type).toBe(entity.get('B').type); + expect(clone.get(FakeComponentA)).not.toBe(entity.get(FakeComponentA)); + expect(clone.get(FakeComponentB)).not.toBe(entity.get(FakeComponentB)); + expect(clone.get(FakeComponentA).constructor).toBe(entity.get(FakeComponentA).constructor); + expect(clone.get(FakeComponentB).constructor).toBe(entity.get(FakeComponentB).constructor); expect(clone.children.length).toBe(1); - expect(clone.children[0].types).toEqual(['Z']); + expect(clone.children[0].types).toEqual([FakeComponentZ]); }); it('can be initialized with a template', () => { const entity = new ex.Entity(); - const template = new ex.Entity([new FakeComponent('A'), new FakeComponent('B')]).addChild( - new ex.Entity([new FakeComponent('C'), new FakeComponent('D')]).addChild(new ex.Entity([new FakeComponent('Z')])) + const template = new ex.Entity([new FakeComponentA, new FakeComponentB]).addChild( + new ex.Entity([new FakeComponentC, new FakeComponentD]).addChild(new ex.Entity([new FakeComponentZ])) ); expect(entity.getComponents()).toEqual([]); entity.addTemplate(template); - expect(entity.types.sort((a, b) => a.localeCompare(b))).toEqual(['A', 'B']); - expect(entity.children[0].types.sort((a, b) => a.localeCompare(b))).toEqual(['C', 'D']); - expect(entity.children[0].children[0].types).toEqual(['Z']); + expect(entity.types.sort((a, b) => a.name.localeCompare(b.name))).toEqual([FakeComponentA, FakeComponentB]); + expect(entity.children[0].types.sort((a, b) => a.name.localeCompare(b.name))).toEqual([FakeComponentC, FakeComponentD]); + expect(entity.children[0].children[0].types).toEqual([FakeComponentZ]); }); it('can be checked if it has a component', () => { const entity = new ex.Entity(); - const typeA = new FakeComponent('A'); - const typeB = new FakeComponent('B'); + const typeA = new FakeComponentA; + const typeB = new FakeComponentB; entity.addComponent(typeA); entity.addComponent(typeB); - expect(entity.has('A')).toBe(true); - expect(entity.has('B')).toBe(true); - expect(entity.has('C')).toBe(false); + expect(entity.has(FakeComponentA)).toBe(true); + expect(entity.has(FakeComponentB)).toBe(true); + expect(entity.has(FakeComponentC)).toBe(false); }); it('has an overridable initialize lifecycle handler', (done) => { @@ -305,11 +296,11 @@ describe('An entity', () => { it('can\'t parent if already parented', () => { const parent = new ex.Entity(); const child = new ex.Entity(); - const otherparent = new ex.Entity(); + const otherParent = new ex.Entity(); parent.addChild(child); expect(() => { - otherparent.addChild(child); + otherParent.addChild(child); }).toThrowError('Entity already has a parent, cannot add without unparenting'); }); @@ -319,7 +310,7 @@ describe('An entity', () => { e.componentAdded$.register({ notify: addedSpy }); - const component = new FakeComponent('A'); + const component = new FakeComponentA; e.addComponent(component); expect(addedSpy).toHaveBeenCalledTimes(1); }); @@ -330,9 +321,9 @@ describe('An entity', () => { e.componentRemoved$.register({ notify: removedSpy }); - const component = new FakeComponent('A'); + const component = new FakeComponentA; e.addComponent(component); - e.removeComponent(component); + e.removeComponent(FakeComponentA); e.processComponentRemoval(); expect(removedSpy).toHaveBeenCalledTimes(1); }); @@ -492,4 +483,20 @@ describe('An entity', () => { expect(child6.children.length).toBe(1); expect(grandChild.parent).toBe(child6); }); + + it('will return subclass instances of types', () => { + class MyBody extends ex.BodyComponent {} + const body = new MyBody; + const sut = new ex.Entity([body]); + + expect(sut.get(MyBody)).toBe(body); + expect(sut.get(ex.BodyComponent)).toBe(body); + + sut.removeComponent(body, true); + + const superBody = new ex.BodyComponent; + sut.addComponent(superBody); + expect(sut.get(MyBody)).toBe(undefined); + expect(sut.get(ex.BodyComponent)).toBe(superBody); + }); }); diff --git a/src/spec/GraphicsSystemSpec.ts b/src/spec/GraphicsSystemSpec.ts index 3cd6e3836..57cc6ec0e 100644 --- a/src/spec/GraphicsSystemSpec.ts +++ b/src/spec/GraphicsSystemSpec.ts @@ -27,23 +27,25 @@ describe('A Graphics ECS System', () => { }); it('sorts entities by transform.z', () => { - const sut = new ex.GraphicsSystem(); + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); engine.currentScene._initialize(engine); - sut.initialize(engine.currentScene); + sut.initialize(world, engine.currentScene); const es = [...entities]; - es.forEach(e => sut.notify(new ex.AddedEntity(e))); + es.forEach(e => sut.query.entityAdded$.notifyAll(e)); sut.preupdate(); expect(sut.sortedTransforms.map(t => t.owner)).toEqual(entities.reverse()); }); it('draws entities with transform and graphics components', async () => { - const sut = new ex.GraphicsSystem(); - const offscreenSystem = new ex.OffscreenSystem(); + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(engine.currentScene); - sut.initialize(engine.currentScene); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); const rect = new ex.Rectangle({ width: 25, @@ -85,12 +87,13 @@ describe('A Graphics ECS System', () => { entities.push(offscreen); engine.graphicsContext.clear(); - entities.forEach(e => sut.notify(new ex.AddedEntity(e))); + entities.forEach(e => offscreenSystem.query.checkAndAdd(e)); + entities.forEach(e => sut.query.checkAndAdd(e)); - offscreenSystem.update(entities); + offscreenSystem.update(); sut.preupdate(); - sut.update(entities, 1); + sut.update(1); expect(rect.draw).toHaveBeenCalled(); expect(circle.draw).toHaveBeenCalled(); @@ -121,13 +124,13 @@ describe('A Graphics ECS System', () => { spyOn(game.graphicsContext, 'rotate'); spyOn(game.graphicsContext, 'scale'); - const graphicsSystem = new ex.GraphicsSystem(); - graphicsSystem.initialize(game.currentScene); + const graphicsSystem = new ex.GraphicsSystem(game.currentScene.world); + graphicsSystem.initialize(game.currentScene.world, game.currentScene); graphicsSystem.preupdate(); - graphicsSystem.notify(new ex.AddedEntity(actor)); + graphicsSystem.query.checkAndAdd(actor); game.currentFrameLagMs = 8; // current lag in a 30 fps frame - graphicsSystem.update([actor], 30); + graphicsSystem.update(30); expect(game.graphicsContext.translate).toHaveBeenCalledWith(24, 24); }); @@ -158,16 +161,16 @@ describe('A Graphics ECS System', () => { spyOn(game.graphicsContext, 'rotate'); spyOn(game.graphicsContext, 'scale'); - const graphicsSystem = new ex.GraphicsSystem(); - graphicsSystem.initialize(game.currentScene); + const graphicsSystem = new ex.GraphicsSystem(game.currentScene.world); + graphicsSystem.initialize(game.currentScene.world, game.currentScene); graphicsSystem.preupdate(); - graphicsSystem.notify(new ex.AddedEntity(actor)); + graphicsSystem.query.checkAndAdd(actor); game.currentFrameLagMs = (1000 / 30) / 2; // current lag in a 30 fps frame - graphicsSystem.update([actor], 16); + graphicsSystem.update(16); expect(translateSpy.calls.argsFor(0)).toEqual([10, 10]); - expect(translateSpy.calls.argsFor(1)).toEqual([45, 45]); // 45 because the parent offets by (-10, -10) + expect(translateSpy.calls.argsFor(1)).toEqual([45, 45]); // 45 because the parent offsets by (-10, -10) }); it('will not interpolate body graphics if disabled', async () => { @@ -190,26 +193,27 @@ describe('A Graphics ECS System', () => { spyOn(game.graphicsContext, 'rotate'); spyOn(game.graphicsContext, 'scale'); - const graphicsSystem = new ex.GraphicsSystem(); - graphicsSystem.initialize(game.currentScene); + const graphicsSystem = new ex.GraphicsSystem(game.currentScene.world); + graphicsSystem.initialize(game.currentScene.world, game.currentScene); graphicsSystem.preupdate(); - graphicsSystem.notify(new ex.AddedEntity(actor)); + graphicsSystem.query.checkAndAdd(actor); actor.body.enableFixedUpdateInterpolate = false; game.currentFrameLagMs = 8; // current lag in a 30 fps frame - graphicsSystem.update([actor], 30); + graphicsSystem.update(30); expect(game.graphicsContext.translate).toHaveBeenCalledWith(100, 100); }); it('will multiply the opacity set on the context', async () => { - const sut = new ex.GraphicsSystem(); - const offscreenSystem = new ex.OffscreenSystem(); + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(engine.currentScene); - sut.initialize(engine.currentScene); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); @@ -224,13 +228,14 @@ describe('A Graphics ECS System', () => { }); actor.graphics.opacity = .5; - sut.notify(new ex.AddedEntity(actor)); + sut.query.checkAndAdd(actor); - offscreenSystem.update([actor]); + offscreenSystem.query.checkAndAdd(actor); + offscreenSystem.update(); engine.graphicsContext.clear(); sut.preupdate(); - sut.update([actor], 1); + sut.update(1); engine.graphicsContext.flush(); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) @@ -238,13 +243,14 @@ describe('A Graphics ECS System', () => { }); it('can flip graphics horizontally', async () => { - const sut = new ex.GraphicsSystem(); - const offscreenSystem = new ex.OffscreenSystem(); + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(engine.currentScene); - sut.initialize(engine.currentScene); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); const sword = new ex.ImageSource('src/spec/images/GraphicsSystemSpec/sword.png'); await sword.load(); @@ -258,13 +264,13 @@ describe('A Graphics ECS System', () => { actor.graphics.use(sword.toSprite()); actor.graphics.flipHorizontal = true; - sut.notify(new ex.AddedEntity(actor)); + sut.query.checkAndAdd(actor); - offscreenSystem.update([actor]); + offscreenSystem.update(); engine.graphicsContext.clear(); sut.preupdate(); - sut.update([actor], 1); + sut.update(1); engine.graphicsContext.flush(); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) @@ -272,13 +278,14 @@ describe('A Graphics ECS System', () => { }); it('can flip graphics vertically', async () => { - const sut = new ex.GraphicsSystem(); - const offscreenSystem = new ex.OffscreenSystem(); + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(engine.currentScene); - sut.initialize(engine.currentScene); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); const sword = new ex.ImageSource('src/spec/images/GraphicsSystemSpec/sword.png'); await sword.load(); @@ -292,13 +299,13 @@ describe('A Graphics ECS System', () => { actor.graphics.use(sword.toSprite()); actor.graphics.flipVertical = true; - sut.notify(new ex.AddedEntity(actor)); + sut.query.checkAndAdd(actor); - offscreenSystem.update([actor]); + offscreenSystem.update(); engine.graphicsContext.clear(); sut.preupdate(); - sut.update([actor], 1); + sut.update(1); engine.graphicsContext.flush(); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) @@ -306,13 +313,14 @@ describe('A Graphics ECS System', () => { }); it('can flip graphics both horizontally and vertically', async () => { - const sut = new ex.GraphicsSystem(); - const offscreenSystem = new ex.OffscreenSystem(); + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(engine.currentScene); - sut.initialize(engine.currentScene); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); const sword = new ex.ImageSource('src/spec/images/GraphicsSystemSpec/sword.png'); await sword.load(); @@ -327,13 +335,13 @@ describe('A Graphics ECS System', () => { actor.graphics.flipVertical = true; actor.graphics.flipHorizontal = true; - sut.notify(new ex.AddedEntity(actor)); + sut.query.checkAndAdd(actor); - offscreenSystem.update([actor]); + offscreenSystem.update(); engine.graphicsContext.clear(); sut.preupdate(); - sut.update([actor], 1); + sut.update(1); engine.graphicsContext.flush(); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) @@ -341,13 +349,14 @@ describe('A Graphics ECS System', () => { }); it('can flip graphics both horizontally and vertically with an offset', async () => { - const sut = new ex.GraphicsSystem(); - const offscreenSystem = new ex.OffscreenSystem(); + const world = engine.currentScene.world; + const sut = new ex.GraphicsSystem(world); + const offscreenSystem = new ex.OffscreenSystem(world); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); engine.screen.setCurrentCamera(engine.currentScene.camera); - offscreenSystem.initialize(engine.currentScene); - sut.initialize(engine.currentScene); + offscreenSystem.initialize(world, engine.currentScene); + sut.initialize(world, engine.currentScene); const sword = new ex.ImageSource('src/spec/images/GraphicsSystemSpec/sword.png'); await sword.load(); @@ -363,13 +372,13 @@ describe('A Graphics ECS System', () => { actor.graphics.flipHorizontal = true; actor.graphics.offset = ex.vec(25, 25); - sut.notify(new ex.AddedEntity(actor)); + sut.query.checkAndAdd(actor); - offscreenSystem.update([actor]); + offscreenSystem.update(); engine.graphicsContext.clear(); sut.preupdate(); - sut.update([actor], 1); + sut.update(1); engine.graphicsContext.flush(); await expectAsync(TestUtils.flushWebGLCanvasTo2D(engine.canvas)) diff --git a/src/spec/OffscreenSystemSpec.ts b/src/spec/OffscreenSystemSpec.ts index 02d343631..a23b56e1a 100644 --- a/src/spec/OffscreenSystemSpec.ts +++ b/src/spec/OffscreenSystemSpec.ts @@ -26,11 +26,11 @@ describe('The OffscreenSystem', () => { }); it('decorates offscreen entities with "offscreen" tag', () => { - const sut = new ex.OffscreenSystem(); + const sut = new ex.OffscreenSystem(engine.currentScene.world); engine.currentScene.camera.update(engine, 1); engine.screen.setCurrentCamera(engine.currentScene.camera); engine.currentScene._initialize(engine); - sut.initialize(engine.currentScene); + sut.initialize(engine.currentScene.world, engine.currentScene); const rect = new ex.Rectangle({ width: 25, @@ -48,9 +48,10 @@ describe('The OffscreenSystem', () => { offscreen.events.on('enterviewport', onscreenSpy); offscreen.events.on('exitviewport', offscreenSpy); + sut.query.checkAndAdd(offscreen); // Should be offscreen - sut.update([offscreen]); + sut.update(); expect(offscreenSpy).toHaveBeenCalled(); expect(onscreenSpy).not.toHaveBeenCalled(); expect(offscreen.hasTag('ex.offscreen')).toBeTrue(); @@ -59,7 +60,7 @@ describe('The OffscreenSystem', () => { // Should be onscreen offscreen.get(TransformComponent).pos = ex.vec(80, 80); - sut.update([offscreen]); + sut.update(); offscreen.processComponentRemoval(); expect(offscreenSpy).not.toHaveBeenCalled(); expect(onscreenSpy).toHaveBeenCalled(); diff --git a/src/spec/PairSpec.ts b/src/spec/PairSpec.ts index 69ab4a8c6..55e03e662 100644 --- a/src/spec/PairSpec.ts +++ b/src/spec/PairSpec.ts @@ -31,7 +31,7 @@ describe('A Collision Pair', () => { width: 20, height: 20 }); - actor2.removeComponent('ex.body', true); + actor2.removeComponent(ex.BodyComponent, true); const sut = ex.Pair.canCollide(actor1.collider.get(), actor2.collider.get()); diff --git a/src/spec/ParallaxSpec.ts b/src/spec/ParallaxSpec.ts index 85435ffc2..111af9e3f 100644 --- a/src/spec/ParallaxSpec.ts +++ b/src/spec/ParallaxSpec.ts @@ -74,6 +74,7 @@ describe('A Parallax Component', () => { tilemap.addComponent(new ex.ParallaxComponent(ex.vec(0.5, 0.5))); game.add(tilemap); + tilemap._initialize(game); clock.step(16); diff --git a/src/spec/QueryManagerSpec.ts b/src/spec/QueryManagerSpec.ts index a5bf29b74..7f328dcfa 100644 --- a/src/spec/QueryManagerSpec.ts +++ b/src/spec/QueryManagerSpec.ts @@ -1,10 +1,8 @@ import * as ex from '@excalibur'; -class FakeComponent extends ex.Component { - constructor(public type: T) { - super(); - } -} +class FakeComponentA extends ex.Component {} +class FakeComponentB extends ex.Component {} +class FakeComponentC extends ex.Component {} describe('A QueryManager', () => { it('exists', () => { @@ -19,84 +17,72 @@ describe('A QueryManager', () => { it('can create queries for entities', () => { const world = new ex.World(null); const entity1 = new ex.Entity(); - entity1.addComponent(new FakeComponent('A')); - entity1.addComponent(new FakeComponent('B')); + entity1.addComponent(new FakeComponentA); + entity1.addComponent(new FakeComponentB); const entity2 = new ex.Entity(); - entity2.addComponent(new FakeComponent('A')); + entity2.addComponent(new FakeComponentA); world.entityManager.addEntity(entity1); world.entityManager.addEntity(entity2); // Query for all entities that have type A components - const queryA = world.queryManager.createQuery>(['A']); + const queryA = world.query([FakeComponentA]); // Query for all entities that have type A & B components - const queryAB = world.queryManager.createQuery | FakeComponent<'B'>>(['A', 'B']); + const queryAB = world.query([FakeComponentA, FakeComponentB]); expect(queryA.getEntities()).toEqual([entity1, entity2], 'Both entities have component A'); expect(queryAB.getEntities()).toEqual([entity1], 'Only entity1 has both A+B'); // Queries update if component change - entity2.addComponent(new FakeComponent('B')); + entity2.addComponent(new FakeComponentB); expect(queryAB.getEntities()).toEqual( [entity1, entity2], 'Now both entities have A+B' ); // Queries update if components change - entity2.removeComponent('B', true); + entity2.removeComponent(FakeComponentB, true); expect(queryAB.getEntities()).toEqual([entity1], 'Component force removed from entity, only entity1 A+B'); // Queries are deferred by default, so queries will update after removals - entity1.removeComponent('B'); + entity1.removeComponent(FakeComponentB); expect(queryAB.getEntities()).toEqual([entity1], 'Deferred removal, component is still part of entity1'); entity1.processComponentRemoval(); expect(queryAB.getEntities()).toEqual([], 'No entities should match'); }); - it('can remove queries', () => { + it('can add entities to queries', () => { const world = new ex.World(null); const entity1 = new ex.Entity(); - entity1.addComponent(new FakeComponent('A')); - entity1.addComponent(new FakeComponent('B')); + entity1.addComponent(new FakeComponentA); + entity1.addComponent(new FakeComponentB); const entity2 = new ex.Entity(); - entity2.addComponent(new FakeComponent('A')); - entity2.addComponent(new FakeComponent('B')); - - world.entityManager.addEntity(entity1); - world.entityManager.addEntity(entity2); - - // Query for all entities that have type A components - const queryA = world.queryManager.createQuery(['A', 'B']); - - queryA.register({ - notify: () => {} // eslint-disable-line @typescript-eslint/no-empty-function - }); + entity2.addComponent(new FakeComponentA); + entity2.addComponent(new FakeComponentB); - expect(world.queryManager.getQuery(['A', 'B'])).toBe(queryA); + const queryAB = world.query([FakeComponentA, FakeComponentB]); + expect(queryAB.getEntities()).toEqual([]); - // Query will not be removed if there are observers - world.queryManager.maybeRemoveQuery(queryA); - expect(world.queryManager.getQuery(['A', 'B'])).toBe(queryA); + world.queryManager.addEntity(entity1); + expect(queryAB.getEntities()).toEqual([entity1]); - // Query will be removed if no observers - queryA.clear(); - world.queryManager.maybeRemoveQuery(queryA); - expect(world.queryManager.getQuery(['A', 'B'])).toBe(null); + world.queryManager.addEntity(entity2); + expect(queryAB.getEntities()).toEqual([entity1, entity2]); }); - it('can add entities to queries', () => { + it('can add entities to tag queries', () => { const world = new ex.World(null); const entity1 = new ex.Entity(); - entity1.addComponent(new FakeComponent('A')); - entity1.addComponent(new FakeComponent('B')); + entity1.addTag('A'); + entity1.addTag('B'); const entity2 = new ex.Entity(); - entity2.addComponent(new FakeComponent('A')); - entity2.addComponent(new FakeComponent('B')); + entity2.addTag('A'); + entity2.addTag('B'); - const queryAB = world.queryManager.createQuery(['A', 'B']); + const queryAB = world.queryTags(['A', 'B']); expect(queryAB.getEntities()).toEqual([]); world.queryManager.addEntity(entity1); @@ -109,14 +95,36 @@ describe('A QueryManager', () => { it('can remove entities from queries', () => { const world = new ex.World(null); const entity1 = new ex.Entity(); - entity1.addComponent(new FakeComponent('A')); - entity1.addComponent(new FakeComponent('B')); + entity1.addComponent(new FakeComponentA); + entity1.addComponent(new FakeComponentB); + + const entity2 = new ex.Entity(); + entity2.addComponent(new FakeComponentA); + entity2.addComponent(new FakeComponentB); + + const queryAB = world.query([FakeComponentA, FakeComponentB]); + world.queryManager.addEntity(entity1); + world.queryManager.addEntity(entity2); + expect(queryAB.getEntities()).toEqual([entity1, entity2]); + + world.queryManager.removeEntity(entity1); + expect(queryAB.getEntities()).toEqual([entity2]); + + world.queryManager.removeEntity(entity2); + expect(queryAB.getEntities()).toEqual([]); + }); + + it('can remove entities from tag queries', () => { + const world = new ex.World(null); + const entity1 = new ex.Entity(); + entity1.addTag('A'); + entity1.addTag('B'); const entity2 = new ex.Entity(); - entity2.addComponent(new FakeComponent('A')); - entity2.addComponent(new FakeComponent('B')); + entity2.addTag('A'); + entity2.addTag('B'); - const queryAB = world.queryManager.createQuery(['A', 'B']); + const queryAB = world.queryTags(['A', 'B']); world.queryManager.addEntity(entity1); world.queryManager.addEntity(entity2); expect(queryAB.getEntities()).toEqual([entity1, entity2]); @@ -131,47 +139,103 @@ describe('A QueryManager', () => { it('can update queries when a component is removed', () => { const world = new ex.World(null); const entity1 = new ex.Entity(); - entity1.addComponent(new FakeComponent('A')); - entity1.addComponent(new FakeComponent('B')); + entity1.addComponent(new FakeComponentA); + entity1.addComponent(new FakeComponentB); const entity2 = new ex.Entity(); - entity2.addComponent(new FakeComponent('A')); - entity2.addComponent(new FakeComponent('B')); + entity2.addComponent(new FakeComponentA); + entity2.addComponent(new FakeComponentB); - const queryAB = world.queryManager.createQuery(['A', 'B']); + const queryAB = world.query([FakeComponentA, FakeComponentB]); world.queryManager.addEntity(entity1); world.queryManager.addEntity(entity2); expect(queryAB.getEntities()).toEqual([entity1, entity2]); - const removed = entity1.get('A'); - entity1.removeComponent('A'); + const removed = entity1.get(FakeComponentA); + entity1.removeComponent(FakeComponentA); world.queryManager.removeComponent(entity1, removed); expect(queryAB.getEntities()).toEqual([entity2]); }); - it('removing components unrelated to the query doesnt remove the entity', () => { + it('can update tag queries when a component is removed', () => { const world = new ex.World(null); const entity1 = new ex.Entity(); - entity1.addComponent(new FakeComponent('A')); - entity1.addComponent(new FakeComponent('B')); - entity1.addComponent(new FakeComponent('C')); + entity1.addTag('A'); + entity1.addTag('B'); const entity2 = new ex.Entity(); - entity2.addComponent(new FakeComponent('A')); - entity2.addComponent(new FakeComponent('B')); + entity2.addTag('A'); + entity2.addTag('B'); - const queryAB = world.queryManager.createQuery(['A', 'B']); + const queryAB = world.queryTags(['A', 'B']); world.queryManager.addEntity(entity1); world.queryManager.addEntity(entity2); expect(queryAB.getEntities()).toEqual([entity1, entity2]); - const removed = entity1.get('C'); - entity1.removeComponent('C'); + entity1.removeTag('A'); + world.queryManager.removeTag(entity1, 'A'); + + expect(queryAB.getEntities()).toEqual([entity2]); + }); + + it('removing components unrelated to the query doesn\'t remove the entity', () => { + const world = new ex.World(null); + const entity1 = new ex.Entity(); + entity1.addComponent(new FakeComponentA); + entity1.addComponent(new FakeComponentB); + entity1.addComponent(new FakeComponentC); + + const entity2 = new ex.Entity(); + entity2.addComponent(new FakeComponentA); + entity2.addComponent(new FakeComponentB); + + const queryAB = world.query([FakeComponentA, FakeComponentB]); + world.queryManager.addEntity(entity1); + world.queryManager.addEntity(entity2); + + expect(queryAB.getEntities()).toEqual([entity1, entity2]); + + const removed = entity1.get(FakeComponentC); + entity1.removeComponent(FakeComponentC); world.queryManager.removeComponent(entity1, removed); expect(queryAB.getEntities()).toEqual([entity1, entity2]); }); + + it('will be notified when entity components are added', (done) => { + const world = new ex.World(null); + const entity = new ex.Entity(); + world.add(entity); + + const componentA = new FakeComponentA; + const query = world.query([FakeComponentA]); + query.entityAdded$.subscribe(e => { + expect(e).toBe(entity); + expect(e.get(FakeComponentA)).toBe(componentA); + done(); + }); + + entity.addComponent(componentA); + }); + + it('will be notified when entity components are removed', (done) => { + const world = new ex.World(null); + const entity = new ex.Entity(); + world.add(entity); + const componentA = new FakeComponentA; + entity.addComponent(componentA); + + const query = world.query([FakeComponentA]); + query.entityRemoved$.subscribe(e => { + expect(e).toBe(entity); + expect(e.get(FakeComponentA)).toBe(componentA); + done(); + }); + + entity.removeComponent(FakeComponentA); + entity.processComponentRemoval(); + }); }); diff --git a/src/spec/QuerySpec.ts b/src/spec/QuerySpec.ts index fb27948bf..cf34f77ad 100644 --- a/src/spec/QuerySpec.ts +++ b/src/spec/QuerySpec.ts @@ -1,10 +1,7 @@ import * as ex from '@excalibur'; -class FakeComponent extends ex.Component { - constructor(public type: T) { - super(); - } -} +class FakeComponentA extends ex.Component {} +class FakeComponentB extends ex.Component {} describe('A query', () => { it('should exist', () => { @@ -12,18 +9,18 @@ describe('A query', () => { }); it('can be created', () => { - expect(new ex.Query(['A', 'B'])).not.toBeNull(); + expect(new ex.Query([FakeComponentA, FakeComponentB])).not.toBeNull(); }); - it('has a key', () => { - const query = new ex.Query(['A', 'B']); - expect(query.key).toBe('A+B'); + it('has an id', () => { + const query = new ex.Query([FakeComponentA, FakeComponentB]); + expect(query.id).toEqual('FakeComponentA-FakeComponentB'); }); it('can match with entities', () => { - const queryAB = new ex.Query(['A', 'B']); - const compA = new FakeComponent('A'); - const compB = new FakeComponent('B'); + const queryAB = new ex.Query([FakeComponentA, FakeComponentB]); + const compA = new FakeComponentA; + const compB = new FakeComponentB; const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); @@ -31,16 +28,14 @@ describe('A query', () => { const entity2 = new ex.Entity(); entity2.addComponent(compA); - expect(queryAB.matches(entity1)).toBe(true, 'entity1 should match has both compontents A, B'); - expect(queryAB.matches(['A', 'B'])).toBe(true); - expect(queryAB.matches(entity2)).toBe(false, 'entity2 should not match, only has 1 component A'); - expect(queryAB.matches(['A'])).toBe(false); + expect(entity1.hasAll(queryAB.requiredComponents)).toBe(true, 'entity1 should match has both components A, B'); + expect(entity2.hasAll(queryAB.requiredComponents)).toBe(false, 'entity2 should not match, only has 1 component A'); }); it('can only add entities that match', () => { - const queryAB = new ex.Query(['A', 'B']); - const compA = new FakeComponent('A'); - const compB = new FakeComponent('B'); + const queryAB = new ex.Query([FakeComponentA, FakeComponentB]); + const compA = new FakeComponentA; + const compB = new FakeComponentB; const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); @@ -48,17 +43,17 @@ describe('A query', () => { const entity2 = new ex.Entity(); entity2.addComponent(compA); - queryAB.addEntity(entity1); + queryAB.checkAndAdd(entity1); expect(queryAB.getEntities()).toEqual([entity1]); - queryAB.addEntity(entity2); + queryAB.checkAndAdd(entity2); expect(queryAB.getEntities()).toEqual([entity1]); }); it('can remove entities', () => { - const queryAB = new ex.Query(['A', 'B']); - const compA = new FakeComponent('A'); - const compB = new FakeComponent('B'); + const queryAB = new ex.Query([FakeComponentA, FakeComponentB]); + const compA = new FakeComponentA; + const compB = new FakeComponentB; const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); @@ -66,7 +61,7 @@ describe('A query', () => { const entity2 = new ex.Entity(); entity2.addComponent(compA); - queryAB.addEntity(entity1); + queryAB.checkAndAdd(entity1); expect(queryAB.getEntities()).toEqual([entity1]); queryAB.removeEntity(entity2); @@ -76,61 +71,37 @@ describe('A query', () => { expect(queryAB.getEntities()).toEqual([]); }); - it('notifies obersvers of when something is added to the query', (done) => { - const queryAB = new ex.Query(['A', 'B']); - const compA = new FakeComponent('A'); - const compB = new FakeComponent('B'); + it('notifies observers of when something is added to the query', (done) => { + const queryAB = new ex.Query([FakeComponentA, FakeComponentB]); + const compA = new FakeComponentA; + const compB = new FakeComponentB; const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); - queryAB.register({ - notify: (message) => { - expect(message.type).toBe('Entity Added'); - expect(message.data).toBe(entity1); - done(); - } + queryAB.entityAdded$.subscribe(e => { + expect(e).toBe(entity1); + done(); }); - queryAB.addEntity(entity1); + queryAB.checkAndAdd(entity1); }); - it('notifies obersvers of when something is added to the query', (done) => { - const queryAB = new ex.Query(['A', 'B']); - const compA = new FakeComponent('A'); - const compB = new FakeComponent('B'); + it('notifies observers of when something is added to the query', (done) => { + const queryAB = new ex.Query([FakeComponentA, FakeComponentB]); + const compA = new FakeComponentA; + const compB = new FakeComponentB; const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); - queryAB.addEntity(entity1); - - queryAB.register({ - notify: (message) => { - expect(message.type).toBe('Entity Removed'); - expect(message.data).toBe(entity1); - done(); - } + queryAB.checkAndAdd(entity1); + + queryAB.entityRemoved$.subscribe(e => { + expect(e).toBe(entity1); + done(); }); queryAB.removeEntity(entity1); }); - it('can clear all observers and entities from the query', () => { - const queryAB = new ex.Query(['A', 'B']); - const compA = new FakeComponent('A'); - const compB = new FakeComponent('B'); - const entity1 = new ex.Entity(); - entity1.addComponent(compA); - entity1.addComponent(compB); - - queryAB.addEntity(entity1); - queryAB.register({ - notify: () => {} // eslint-disable-line @typescript-eslint/no-empty-function - }); - expect(queryAB.getEntities()).toEqual([entity1]); - expect(queryAB.observers.length).toBe(1); - queryAB.clear(); - expect(queryAB.getEntities()).toEqual([]); - expect(queryAB.observers.length).toBe(0); - }); }); diff --git a/src/spec/SceneSpec.ts b/src/spec/SceneSpec.ts index 7bedc3197..3f49c92b4 100644 --- a/src/spec/SceneSpec.ts +++ b/src/spec/SceneSpec.ts @@ -78,7 +78,7 @@ describe('A scene', () => { scene.clear(); expect(scene.entities.length).withContext('deferred entity removal means entities cleared at end of update').toBe(3); - expect(scene.timers.length).withContext('timers dont have deferred removal').toBe(0); + expect(scene.timers.length).withContext('timers don\'t have deferred removal').toBe(0); scene.update(engine, 100); expect(scene.entities.length).toBe(0); @@ -702,6 +702,7 @@ describe('A scene', () => { it('will update TileMaps that were added in a Timer callback', () => { let updated = false; const tilemap = new ex.TileMap({ pos: ex.vec(0, 0), tileWidth: 1, tileHeight: 1, columns: 1, rows: 1}); + tilemap._initialize(scene.engine); tilemap.on('postupdate', () => { updated = true; }); diff --git a/src/spec/SystemManagerSpec.ts b/src/spec/SystemManagerSpec.ts index a0f73e3a6..44d564586 100644 --- a/src/spec/SystemManagerSpec.ts +++ b/src/spec/SystemManagerSpec.ts @@ -1,17 +1,22 @@ import * as ex from '@excalibur'; import { SystemType } from '@excalibur'; -class FakeComponent extends ex.Component { - constructor(public type: T) { +class FakeComponentA extends ex.Component {} +class FakeComponentB extends ex.Component {} +class FakeComponentC extends ex.Component {} + +class FakeSystem extends ex.System { + query: ex.Query>; + constructor( + public world: ex.World, + public priority: number, + public name: string, + public types: ex.ComponentCtor[], + public systemType: ex.SystemType) { super(); + this.query = this.world.query(types); } -} - -class FakeSystem extends ex.System { - constructor(public priority: number, public name: string, public types: string[], public systemType: ex.SystemType) { - super(); - } - update(entities: ex.Entity[], delta: number): void { + update(delta: number): void { // fake } } @@ -26,14 +31,15 @@ describe('A SystemManager', () => { }); it('can add systems', () => { - const sm = new ex.World(null).systemManager; + const world = new ex.World(null); + const sm = world.systemManager; // Lower priority - const s3 = new FakeSystem(2, 'System3', ['C'], SystemType.Update); + const s3 = new FakeSystem(world, 2, 'System3', [FakeComponentC], SystemType.Update); sm.addSystem(s3); // Systems of equal priority should preserve order - const s1 = new FakeSystem(1, 'System1', ['A'], SystemType.Update); - const s2 = new FakeSystem(1, 'System2', ['C', 'B'], SystemType.Update); + const s1 = new FakeSystem(world, 1, 'System1', [FakeComponentA], SystemType.Update); + const s2 = new FakeSystem(world, 1, 'System2', [FakeComponentC, FakeComponentB], SystemType.Update); sm.addSystem(s1); sm.addSystem(s2); @@ -41,14 +47,15 @@ describe('A SystemManager', () => { }); it('can remove systems', () => { - const sm = new ex.World(null).systemManager; + const world = new ex.World(null); + const sm = world.systemManager; // Lower priority - const s3 = new FakeSystem(2, 'System3', ['C'], SystemType.Update); + const s3 = new FakeSystem(world, 2, 'System3', [FakeComponentC], SystemType.Update); sm.addSystem(s3); // Systems of equal priority should preserve order - const s1 = new FakeSystem(1, 'System1', ['A'], SystemType.Update); - const s2 = new FakeSystem(1, 'System2', ['C', 'B'], SystemType.Update); + const s1 = new FakeSystem(world, 1, 'System1', [FakeComponentA], SystemType.Update); + const s2 = new FakeSystem(world, 1, 'System2', [FakeComponentC, FakeComponentB], SystemType.Update); sm.addSystem(s1); sm.addSystem(s2); @@ -59,8 +66,9 @@ describe('A SystemManager', () => { }); it('can update systems', () => { - const sm = new ex.World(null).systemManager; - const system = new FakeSystem(2, 'System3', ['C'], SystemType.Update); + const world = new ex.World(null); + const sm = world.systemManager; + const system = new FakeSystem(world, 2, 'System3', [FakeComponentC], SystemType.Update); system.preupdate = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function system.postupdate = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function spyOn(system, 'preupdate'); @@ -78,31 +86,31 @@ describe('A SystemManager', () => { const sm = world.systemManager; const qm = world.queryManager; const em = world.entityManager; - const system = new FakeSystem(2, 'System3', ['A', 'C'], SystemType.Update); + const system = new FakeSystem(world, 2, 'System3', [FakeComponentA, FakeComponentC], SystemType.Update); spyOn(system, 'update').and.callThrough(); sm.addSystem(system); const e1 = new ex.Entity(); - e1.addComponent(new FakeComponent('A')); - e1.addComponent(new FakeComponent('C')); + e1.addComponent(new FakeComponentA); + e1.addComponent(new FakeComponentC); const e2 = new ex.Entity(); - e2.addComponent(new FakeComponent('B')); + e2.addComponent(new FakeComponentB); const e3 = new ex.Entity(); - e3.addComponent(new FakeComponent('C')); - e3.addComponent(new FakeComponent('A')); + e3.addComponent(new FakeComponentC); + e3.addComponent(new FakeComponentA); em.addEntity(e1); em.addEntity(e2); em.addEntity(e3); - const query = qm.getQuery(['A', 'C']); + const query = qm.createQuery([FakeComponentA, FakeComponentC]); expect(query.getEntities()).toEqual([e1, e3]); sm.updateSystems(SystemType.Update, null, 10); - expect(system.update).toHaveBeenCalledWith([e1, e3], 10); + expect(system.update).toHaveBeenCalledWith(10); }); it('only updates system of the specified system type', () => { @@ -110,33 +118,24 @@ describe('A SystemManager', () => { const sm = world.systemManager; const qm = world.queryManager; const em = world.entityManager; - const system1 = new FakeSystem(2, 'System1', ['A', 'C'], SystemType.Update); + const system1 = new FakeSystem(world, 2, 'System1', [FakeComponentA, FakeComponentC], SystemType.Update); spyOn(system1, 'update').and.callThrough(); sm.addSystem(system1); - const system2 = new FakeSystem(2, 'System1', ['A', 'C'], SystemType.Draw); + const system2 = new FakeSystem(world, 2, 'System1', [FakeComponentA, FakeComponentC], SystemType.Draw); spyOn(system2, 'update').and.callThrough(); sm.addSystem(system2); sm.updateSystems(SystemType.Draw, null, 10); expect(system1.update).not.toHaveBeenCalled(); - expect(system2.update).toHaveBeenCalledWith([], 10); + expect(system2.update).toHaveBeenCalledWith(10); }); it('should throw on invalid system', () => { - const sm = new ex.World(null).systemManager; + const world = new ex.World(null); + const sm = world.systemManager; expect(() => { - sm.addSystem(new FakeSystem(0, 'ErrorSystem', [], SystemType.Update)); - }).toThrow(new Error('Attempted to add a System without any types')); - }); - - it('type guards on messages should work', () => { - const add = new ex.AddedEntity(new ex.Entity()); - expect(ex.isAddedSystemEntity(add)).toBe(true); - expect(ex.isRemoveSystemEntity(add)).toBe(false); - - const remove = new ex.RemovedEntity(new ex.Entity()); - expect(ex.isRemoveSystemEntity(remove)).toBe(true); - expect(ex.isAddedSystemEntity(remove)).toBe(false); + sm.addSystem(new FakeSystem(world, 0, 'ErrorSystem', [], SystemType.Update)); + }).toThrow(new Error('Cannot create query without components')); }); }); diff --git a/src/spec/TagQuerySpec.ts b/src/spec/TagQuerySpec.ts new file mode 100644 index 000000000..dc0b47edf --- /dev/null +++ b/src/spec/TagQuerySpec.ts @@ -0,0 +1,94 @@ +import * as ex from '@excalibur'; + +describe('A tag query', () => { + it('should exist', () => { + expect(ex.TagQuery).toBeDefined(); + }); + + it('can be created', () => { + expect(new ex.TagQuery(['A', 'B'])).not.toBeNull(); + }); + + it('has an id', () => { + const query = new ex.TagQuery(['A', 'B']); + expect(query.id).toEqual('A-B'); + }); + + it('can match with entities', () => { + const queryAB = new ex.TagQuery(['A', 'B']); + const entity1 = new ex.Entity(); + entity1.addTag('A'); + entity1.addTag('B'); + + const entity2 = new ex.Entity(); + entity2.addTag('A'); + + expect(entity1.hasAllTags(queryAB.requiredTags)).toBe(true, 'entity1 should match has both components A, B'); + expect(entity2.hasAllTags(queryAB.requiredTags)).toBe(false, 'entity2 should not match, only has 1 component A'); + }); + + it('can only add entities that match', () => { + const queryAB = new ex.TagQuery(['A', 'B']); + const entity1 = new ex.Entity(); + entity1.addTag('A'); + entity1.addTag('B'); + + const entity2 = new ex.Entity(); + entity2.addTag('A'); + + queryAB.checkAndAdd(entity1); + expect(queryAB.getEntities()).toEqual([entity1]); + + queryAB.checkAndAdd(entity2); + expect(queryAB.getEntities()).toEqual([entity1]); + }); + + it('can remove entities', () => { + const queryAB = new ex.TagQuery(['A', 'B']); + const entity1 = new ex.Entity(); + entity1.addTag('A'); + entity1.addTag('B'); + + const entity2 = new ex.Entity(); + entity2.addTag('A'); + + queryAB.checkAndAdd(entity1); + expect(queryAB.getEntities()).toEqual([entity1]); + + queryAB.removeEntity(entity2); + expect(queryAB.getEntities()).toEqual([entity1]); + + queryAB.removeEntity(entity1); + expect(queryAB.getEntities()).toEqual([]); + }); + + it('notifies observers of when something is added to the query', (done) => { + const queryAB = new ex.TagQuery(['A', 'B']); + const entity1 = new ex.Entity(); + entity1.addTag('A'); + entity1.addTag('B'); + + queryAB.entityAdded$.subscribe(e => { + expect(e).toBe(entity1); + done(); + }); + + queryAB.checkAndAdd(entity1); + }); + + it('notifies observers of when something is added to the query', (done) => { + const queryAB = new ex.TagQuery(['A', 'B']); + const entity1 = new ex.Entity(); + entity1.addTag('A'); + entity1.addTag('B'); + queryAB.checkAndAdd(entity1); + + queryAB.entityRemoved$.subscribe(e => { + expect(e).toBe(entity1); + done(); + }); + + queryAB.removeEntity(entity1); + }); + +}); diff --git a/src/spec/WorldSpec.ts b/src/spec/WorldSpec.ts index 4e8accf85..0f1a1c96c 100644 --- a/src/spec/WorldSpec.ts +++ b/src/spec/WorldSpec.ts @@ -1,13 +1,16 @@ import * as ex from '@excalibur'; import { SystemType } from '@excalibur'; -class FakeComponent extends ex.Component<'A'> { - public readonly type = 'A'; +class FakeComponent extends ex.Component { } -class FakeSystem extends ex.System { - public readonly types = ['A'] as const; +class FakeSystem extends ex.System { public systemType = ex.SystemType.Update; - public update(entities) { + query: ex.Query; + constructor(public world: ex.World) { + super(); + this.query = world.query([FakeComponent]); + } + public update() { // nothing } } @@ -47,7 +50,7 @@ describe('A World', () => { const world = new ex.World(null); world.systemManager = jasmine.createSpyObj('SystemManager', ['addSystem']); - const system = new FakeSystem(); + const system = new FakeSystem(world); world.add(system); expect(world.systemManager.addSystem).toHaveBeenCalledWith(system); @@ -57,7 +60,7 @@ describe('A World', () => { const world = new ex.World(null); world.systemManager = jasmine.createSpyObj('SystemManager', ['addSystem', 'removeSystem']); - const system = new FakeSystem(); + const system = new FakeSystem(world); world.add(system); world.remove(system); @@ -77,14 +80,15 @@ describe('A World', () => { }); it('can update', () => { - const world = new ex.World('context'); + const scene = new ex.Scene; + const world = new ex.World(scene); world.entityManager = jasmine.createSpyObj('EntityManager', ['processEntityRemovals', 'findEntitiesForRemoval', 'processComponentRemovals', 'updateEntities']); world.systemManager = jasmine.createSpyObj('SystemManager', ['updateSystems']); world.update(SystemType.Update, 100); - expect(world.systemManager.updateSystems).toHaveBeenCalledWith(SystemType.Update, 'context', 100); + expect(world.systemManager.updateSystems).toHaveBeenCalledWith(SystemType.Update, scene, 100); expect(world.entityManager.processComponentRemovals).toHaveBeenCalled(); expect(world.entityManager.processEntityRemovals).toHaveBeenCalled(); });