diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f2ed95..0cc138cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes +- Removes `Entity.components` as a way to access, add, and remove components - Camera.z has been renamed to property `zoom` which is the zoom factor - Camera.zoom(...) has been renamed to function `zoomOverTime()` - TileMap no longer needs registered SpriteSheets, `Sprite`'s can be added directly to `Cell`'s with `addSprite` @@ -27,7 +28,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Tag `offscreen` is now added to entities that are offscreen - Added `Entity.componentAdded$` and `Entity.componentRemoved$` for observing component changes on an entity. - For child/parent entities: - - Added `Entity.add(entity: Entity)`, `Entity.remove(entity: Entity)`, `Entity.removeAll()` for managing child entities + - Added `Entity.addChild(entity: Entity)`, `Entity.removeChild(entity: Entity)`, `Entity.removeAllChildren()` for managing child entities + - Added `Entity.addTemplate(templateEntity: Entity)` for adding template entities or "prefab". - Added `Entity.parent` readonly accessor to the parent (if exists), and `Entity.unparent()` to unparent an entity. - Added `Entity.getAncestors()` is a sorted list of parents starting with the topmost parent. - Added `Entity.children` readonly accessor to the list of children. diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index 3adef1bb..60b9e8f7 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -441,7 +441,7 @@ player.rotation = 0; // Health bar example var healthbar = new ex.Actor(0, -70, 140, 5, new ex.Color(0, 255, 0)); -player.add(healthbar); +player.addChild(healthbar); // player.onPostDraw = (ctx: CanvasRenderingContext2D) => { // ctx.fillStyle = 'red'; // ctx.fillRect(0, 0, 100, 100); diff --git a/sandbox/tests/coordinates/coordinates.ts b/sandbox/tests/coordinates/coordinates.ts index 42352638..ab2bf7cf 100644 --- a/sandbox/tests/coordinates/coordinates.ts +++ b/sandbox/tests/coordinates/coordinates.ts @@ -46,11 +46,11 @@ class PointLabel extends ex.Actor { this._expectLabel = new ex.Label('', 5, 10); this._expectLabel.color = ex.Color.fromHex('#249111'); - this.add(this._expectLabel); + this.addChild(this._expectLabel); this._actualLabel = new ex.Label('', 5, 20); this._actualLabel.color = ex.Color.Blue; - this.add(this._actualLabel); + this.addChild(this._actualLabel); } update(engine: ex.Engine, delta: number) { diff --git a/sandbox/tests/debug/boundingbox.ts b/sandbox/tests/debug/boundingbox.ts index 2e5fcfef..7c16655a 100644 --- a/sandbox/tests/debug/boundingbox.ts +++ b/sandbox/tests/debug/boundingbox.ts @@ -7,6 +7,6 @@ game.showDebug(true); game.start().then(() => { var parent = new ex.Actor(100, 100, 100 * 1.5, 100 * 1.5, ex.Color.Red); var child = new ex.Actor(150, 150, 100, 100, ex.Color.White); - parent.add(child); + parent.addChild(child); game.add(parent); }); diff --git a/sandbox/tests/input/pointer.ts b/sandbox/tests/input/pointer.ts index 90e44b10..8e6da78a 100644 --- a/sandbox/tests/input/pointer.ts +++ b/sandbox/tests/input/pointer.ts @@ -18,7 +18,7 @@ uiElement.on('pointerdown', (p: ex.Input.PointerEvent) => { uiElement.color = ex.Color.Red; }); -box.add(box2); +box.addChild(box2); // Change color of box when clicked box.on('pointerdown', (pe: ex.Input.PointerEvent) => { console.log('box clicked'); diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 97d31c8c..4d577ee9 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -86,7 +86,7 @@ export interface ActorDefaults { */ export class ActorImpl - extends Entity + extends Entity implements Actionable, Eventable, PointerEvents, CanInitialize, CanUpdate, CanDraw, CanBeKilled { // #region Properties @@ -982,7 +982,7 @@ export class ActorImpl alternateMethod: 'Use Actor.transform.z or Actor.z' }) public getZIndex(): number { - return this.components.transform.z; + return this.get(TransformComponent).z; } /** @@ -997,7 +997,7 @@ export class ActorImpl alternateMethod: 'Use Actor.transform.z or Actor.z' }) public setZIndex(newIndex: number) { - this.components.transform.z = newIndex; + this.get(TransformComponent).z = newIndex; } /** diff --git a/src/engine/Drawing/CanvasDrawingSystem.ts b/src/engine/Drawing/CanvasDrawingSystem.ts index 75bf7fe6..5aeabf28 100644 --- a/src/engine/Drawing/CanvasDrawingSystem.ts +++ b/src/engine/Drawing/CanvasDrawingSystem.ts @@ -25,11 +25,11 @@ export class CanvasDrawingSystem extends System, b: Entity) { - return a.components.transform.z - b.components.transform.z; + public sort(a: Entity, b: Entity) { + return a.get(TransformComponent).z - b.get(TransformComponent).z; } - public update(entities: Entity[], delta: number) { + public update(entities: Entity[], delta: number) { this._clearScreen(); let transform: TransformComponent; @@ -37,8 +37,8 @@ export class CanvasDrawingSystem extends System) { + private _applyTransform(entity: Entity) { const ancestors = entity.getAncestors(); for (const ancestor of ancestors) { const transform = ancestor?.get(TransformComponent); diff --git a/src/engine/EntityComponentSystem/Entity.ts b/src/engine/EntityComponentSystem/Entity.ts index cb68b3f9..56843e87 100644 --- a/src/engine/EntityComponentSystem/Entity.ts +++ b/src/engine/EntityComponentSystem/Entity.ts @@ -45,23 +45,6 @@ export function isRemovedComponent(x: Message): x is RemovedCom return !!x && x.type === 'Component Removed'; } -export type ComponentMap = { [type: string]: Component }; - -// Given a TypeName string (Component.type), find the ComponentType that goes with that type name -export type MapTypeNameToComponent = - // If the ComponentType is a Component with type = TypeName then that's the type we are looking for - ComponentType extends Component ? ComponentType : never; - -// Given a type union of PossibleComponentTypes, create a dictionary that maps that type name string to those individual types -export type ComponentMapper = { - [TypeName in PossibleComponentTypes['type']]: MapTypeNameToComponent; -} & -ComponentMap; - -export type ExcludeType = TypeNameOrType extends string - ? Exclude> - : Exclude; - /** * An Entity is the base type of anything that can have behavior in Excalibur, they are part of the built in entity component system * @@ -73,7 +56,7 @@ export type ExcludeType = TypeNameOrType extends stri * entity.components.b; // Type ComponentB * ``` */ -export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUpdate { +export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUpdate { private static _ID = 0; constructor(components?: Component[]) { @@ -137,63 +120,48 @@ export class Entity extends Class imp return this._typesMemo; } + /** + * Bucket to hold on to deferred removals + */ + private _componentsToRemove: (Component | string)[] = []; + private _componentTypeToInstance = new Map(); + private _componentStringToInstance = new Map(); + private _tagsMemo: string[] = []; private _typesMemo: string[] = []; private _rebuildMemos() { - this._tagsMemo = Object.values(this.components) + this._tagsMemo = Array.from(this._componentStringToInstance.values()) .filter((c) => c instanceof TagComponent) .map((c) => c.type); - this._typesMemo = Object.keys(this.components); + this._typesMemo = Array.from(this._componentStringToInstance.keys()); } - /** - * Bucket to hold on to deferred removals - */ - private _componentsToRemove: (Component | string)[] = []; - - /** - * Proxy handler for component changes, responsible for notifying observers - */ - private _handleChanges = { - defineProperty: (obj: any, prop: any, descriptor: PropertyDescriptor) => { - obj[prop] = descriptor.value; - this._rebuildMemos(); - const added = new AddedComponent({ - component: descriptor.value as Component, - entity: this - }); - this.changes.notifyAll(added); - this.componentAdded$.notifyAll(added); - return true; - }, - deleteProperty: (obj: any, prop: any) => { - if (prop in obj) { - const removed = new RemovedComponent({ - component: obj[prop] as Component, - entity: this - }); - this.changes.notifyAll(removed); - this.componentRemoved$.notifyAll(removed); - delete obj[prop]; - this._rebuildMemos(); - return true; - } - return false; - } - }; - - /** - * Dictionary that holds entity components - */ - public components = new Proxy>({} as any, this._handleChanges); - private _componentMap = new Map(); + public getComponents(): Component[] { + return Array.from(this._componentStringToInstance.values()); + } /** * Observable that keeps track of component add or remove changes on the entity */ - public changes = new Observable(); public componentAdded$ = new Observable(); + private _notifyAddComponent(component: Component) { + this._rebuildMemos(); + const added = new AddedComponent({ + component, + entity: this + }); + this.componentAdded$.notifyAll(added); + } + public componentRemoved$ = new Observable(); + private _notifyRemoveComponent(component: Component) { + const removed = new RemovedComponent({ + component, + entity: this + }); + this.componentRemoved$.notifyAll(removed); + this._rebuildMemos(); + } private _parent: Entity = null; public get parent(): Entity { @@ -216,7 +184,7 @@ export class Entity extends Class imp */ public unparent() { if (this._parent) { - this._parent.remove(this); + this._parent.removeChild(this); this._parent = null; } } @@ -225,7 +193,7 @@ export class Entity extends Class imp * Adds an entity to be a child of this entity * @param entity */ - public add(entity: Entity): Entity { + public addChild(entity: Entity): Entity { if (entity.parent === null) { if (this.getAncestors().includes(entity)) { throw new Error('Cycle detected, cannot add entity'); @@ -243,7 +211,7 @@ export class Entity extends Class imp * Remove an entity from children if it exists * @param entity */ - public remove(entity: Entity): Entity { + public removeChild(entity: Entity): Entity { if (entity.parent === this) { Util.removeItemFromArray(entity, this._children); entity._parent = null; @@ -255,9 +223,9 @@ export class Entity extends Class imp /** * Removes all children from this entity */ - public removeAll(): Entity { + public removeAllChildren(): Entity { this.children.forEach((c) => { - this.remove(c); + this.removeChild(c); }); return this; } @@ -295,50 +263,63 @@ export class Entity extends Class imp public clone(): Entity { const newEntity = new Entity(); for (const c of this.types) { - newEntity.addComponent(this.components[c].clone()); + newEntity.addComponent(this.get(c).clone()); + } + for (const child of this.children) { + newEntity.addChild(child.clone()); } return newEntity; } /** - * Adds a component to the entity, or adds a copy of all the components from another entity as a "prefab" - * @param componentOrEntity Component or Entity to add copy of components from - * @param force Optionally overwrite any existing components of the same type + * Adds a copy of all the components from another template entity as a "prefab" + * @param templateEntity Entity to use as a template + * @param force Force component replacement if it aleady exists on the target entity */ - public addComponent(componentOrEntity: T | Entity, force: boolean = false): Entity { - // If you use an entity as a "prefab" or template - if (componentOrEntity instanceof Entity) { - for (const c in componentOrEntity.components) { - this.addComponent(componentOrEntity.components[c].clone(), force); - } - // Normal component case - } else { - // if component already exists, skip if not forced - if (this.components[componentOrEntity.type] && !force) { - return this as Entity; - } + public addTemplate(templateEntity: Entity, force: boolean = false): Entity { + for (const c of templateEntity.getComponents()) { + this.addComponent(c.clone(), force); + } + for (const child of templateEntity.children) { + this.addChild(child.clone().addTemplate(child)); + } + return this; + } - // Remove existing component type if exists when forced - if (this.components[componentOrEntity.type] && force) { - this.removeComponent(componentOrEntity); + /** + * Adds a component to the entity + * @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 { + // if component already exists, skip if not forced + if (this.has(component.type)) { + if (force) { + // Remove existing component type if exists when forced + this.removeComponent(component); + } else { + // early exit component exiss + return this; } + } - // todo circular dependencies will be a problem - if (componentOrEntity.dependencies && componentOrEntity.dependencies.length) { - for (const ctor of componentOrEntity.dependencies) { - this.addComponent(new ctor()); - } + // TODO circular dependencies will be a problem + if (component.dependencies && component.dependencies.length) { + for (const ctor of component.dependencies) { + this.addComponent(new ctor()); } + } - componentOrEntity.owner = this; - (this.components as ComponentMap)[componentOrEntity.type] = componentOrEntity; - const type = componentOrEntity.constructor as ComponentCtor; - this._componentMap.set(type, componentOrEntity); - if (componentOrEntity.onAdd) { - componentOrEntity.onAdd(this); - } + component.owner = this; + const constuctorType = component.constructor as ComponentCtor; + this._componentTypeToInstance.set(constuctorType, component); + this._componentStringToInstance.set(component.type, component); + if (component.onAdd) { + component.onAdd(this); } - return this as Entity; + this._notifyAddComponent(component); + + return this; } /** @@ -351,7 +332,7 @@ export class Entity extends Class imp public removeComponent( componentOrType: ComponentOrType, force = false - ): Entity> { + ): Entity { if (force) { if (typeof componentOrType === 'string') { this._removeComponentByType(componentOrType); @@ -366,14 +347,16 @@ export class Entity extends Class imp } private _removeComponentByType(type: string) { - if (this.components[type]) { - this.components[type].owner = null; - if (this.components[type].onRemove) { - this.components[type].onRemove(this); + if (this.has(type)) { + const component = this.get(type); + component.owner = null; + if (component.onRemove) { + component.onRemove(this); } - const ctor = this.components[type].constructor as ComponentCtor; - this._componentMap.delete(ctor); - delete this.components[type]; + const ctor = component.constructor as ComponentCtor; + this._componentTypeToInstance.delete(ctor); + this._componentStringToInstance.delete(component.type); + this._notifyRemoveComponent(component); } } @@ -397,9 +380,9 @@ export class Entity extends Class imp public has(type: string): boolean; public has(type: ComponentCtor | string): boolean { if (typeof type === 'string') { - return !!this.components[type]; + return this._componentStringToInstance.has(type); } else { - return this._componentMap.has(type); + return this._componentTypeToInstance.has(type); } } @@ -409,8 +392,14 @@ export class Entity extends Class imp * (Does not work on tag components, use .hasTag("mytag") instead) * @param type */ - public get(type: ComponentCtor): T { - return this._componentMap.get(type) as T; + 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; + } } private _isInitialized = false; diff --git a/src/engine/EntityComponentSystem/EntityManager.ts b/src/engine/EntityComponentSystem/EntityManager.ts index 6d5c89bd..85deb799 100644 --- a/src/engine/EntityComponentSystem/EntityManager.ts +++ b/src/engine/EntityComponentSystem/EntityManager.ts @@ -35,7 +35,8 @@ export class EntityManager implements Observer this.addEntity(c)); @@ -66,7 +67,8 @@ export class EntityManager implements Observer this.removeEntity(c)); diff --git a/src/engine/EntityComponentSystem/Query.ts b/src/engine/EntityComponentSystem/Query.ts index 8d37fad5..55a97884 100644 --- a/src/engine/EntityComponentSystem/Query.ts +++ b/src/engine/EntityComponentSystem/Query.ts @@ -14,7 +14,7 @@ import { AddedEntity, RemovedEntity } from './System'; */ export class Query extends Observable { public types: readonly string[]; - private _entities: Entity[] = []; + private _entities: Entity[] = []; private _key: string; public get key(): string { if (this._key) { @@ -39,7 +39,7 @@ export class Query extends Observable, b: Entity) => number): Entity[] { + public getEntities(sort?: (a: Entity, b: Entity) => number): Entity[] { if (sort) { this._entities.sort(sort); } @@ -50,7 +50,7 @@ export class Query extends Observable): void { + public addEntity(entity: Entity): void { if (!Util.contains(this._entities, entity) && this.matches(entity)) { this._entities.push(entity); this.notifyAll(new AddedEntity(entity)); @@ -61,7 +61,7 @@ export class Query extends Observable): void { + public removeEntity(entity: Entity): void { if (Util.removeItemFromArray(entity, this._entities)) { this.notifyAll(new RemovedEntity(entity)); } diff --git a/src/engine/EntityComponentSystem/System.ts b/src/engine/EntityComponentSystem/System.ts index b56c6371..953bd773 100644 --- a/src/engine/EntityComponentSystem/System.ts +++ b/src/engine/EntityComponentSystem/System.ts @@ -57,7 +57,7 @@ implements Observer { * @param a The left entity * @param b The right entity */ - sort?(a: Entity, b: Entity): number; + sort?(a: Entity, b: Entity): number; /** * Optionally specify an initialize handler @@ -67,10 +67,10 @@ implements Observer { /** * Update all entities that match this system's types - * @param entities Entities to update that match this system's typse + * @param entities Entities to update that match this system's types * @param delta Time in milliseconds */ - abstract update(entities: Entity[], delta: number): void; + abstract update(entities: Entity[], delta: number): void; /** * Optionally run a preupdate before the system processes matching entities diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts index 7add6872..ad4c82c5 100644 --- a/src/engine/Graphics/GraphicsSystem.ts +++ b/src/engine/Graphics/GraphicsSystem.ts @@ -27,19 +27,19 @@ export class GraphicsSystem extends System, b: Entity) { - return a.components.transform.z - b.components.transform.z; + public sort(a: Entity, b: Entity) { + return a.get(TransformComponent).z - b.get(TransformComponent).z; } - public update(entities: Entity[], delta: number): void { + public update(entities: Entity[], delta: number): void { this._clearScreen(); this._token++; let transform: TransformComponent; let graphics: GraphicsComponent; for (const entity of entities) { - transform = entity.components.transform; - graphics = entity.components.graphics; + transform = entity.get(TransformComponent); + graphics = entity.get(GraphicsComponent); // Figure out if entities are offscreen const entityOffscreen = this._isOffscreen(transform, graphics); @@ -190,7 +190,7 @@ export class GraphicsSystem extends System, + entity: Entity, _transform: TransformComponent, _graphics: GraphicsComponent ) { diff --git a/src/engine/Label.ts b/src/engine/Label.ts index bdebf88d..6690f68c 100644 --- a/src/engine/Label.ts +++ b/src/engine/Label.ts @@ -230,12 +230,13 @@ export class LabelImpl extends Actor { } this.addComponent(new TransformComponent); - this.components.transform.pos = pos; + this.get(TransformComponent).pos = pos; this.addComponent(new CanvasDrawComponent((ctx, delta) => this.draw(ctx, delta))); this.addComponent(new GraphicsComponent); - this.components.graphics.anchor = Vector.Zero; - this.components.graphics.use(this._text); + const gfx = this.get(GraphicsComponent); + gfx.anchor = Vector.Zero; + gfx.use(this._text); this.text = text || ''; this.color = Color.Black; diff --git a/src/engine/Particles.ts b/src/engine/Particles.ts index 157af509..14e8137b 100644 --- a/src/engine/Particles.ts +++ b/src/engine/Particles.ts @@ -32,7 +32,7 @@ export enum EmitterType { /** * @hidden */ -export class ParticleImpl extends Entity { +export class ParticleImpl extends Entity { public position: Vector = new Vector(0, 0); public velocity: Vector = new Vector(0, 0); public acceleration: Vector = new Vector(0, 0); @@ -404,7 +404,7 @@ export class ParticleEmitterImpl extends Actor { this._particlesToEmit = 0; this.body.collider.type = CollisionType.PreventCollision; this.random = new Random(); - this.removeComponent(this.components.canvas); + this.removeComponent('canvas'); // Remove offscreen culling from particle emitters for (let i = 0; i < this.traits.length; i++) { diff --git a/src/engine/TileMap.ts b/src/engine/TileMap.ts index 64d4dae1..0e5f7004 100644 --- a/src/engine/TileMap.ts +++ b/src/engine/TileMap.ts @@ -19,7 +19,7 @@ import { obsolete } from './Util/Decorators'; /** * @hidden */ -export class TileMapImpl extends Entity { +export class TileMapImpl extends Entity { private _collidingX: number = -1; private _collidingY: number = -1; private _onScreenXStart: number = 0; @@ -38,61 +38,63 @@ export class TileMapImpl extends Entity public rows: number; public cols: number; + private _transform: TransformComponent; + public get x(): number { - return this.components?.transform?.pos.x ?? 0; + return this._transform.pos.x ?? 0; } public set x(val: number) { - if (this.components?.transform?.pos) { - this.components.transform.pos = vec(val, this.y); + if (this._transform?.pos) { + this.get(TransformComponent).pos = vec(val, this.y); } } public get y(): number { - return this.components?.transform?.pos.y ?? 0; + return this._transform?.pos.y ?? 0; } public set y(val: number) { - if (this.components?.transform?.pos) { - this.components.transform.pos = vec(this.x, val); + if (this._transform?.pos) { + this._transform.pos = vec(this.x, val); } } public get z(): number { - return this.components?.transform.z ?? 0; + return this._transform.z ?? 0; } public set z(val: number) { - if (this.components?.transform?.z) { - this.components.transform.z = val; + if (this._transform?.z) { + this._transform.z = val; } } public get rotation(): number { - return this.components?.transform?.rotation ?? 0; + return this._transform?.rotation ?? 0; } public set rotation(val: number) { - if (this.components?.transform?.rotation) { - this.components.transform.rotation = val; + if (this._transform?.rotation) { + this._transform.rotation = val; } } public get scale(): Vector { - return this.components?.transform?.scale ?? Vector.One; + return this._transform?.scale ?? Vector.One; } public set scale(val: Vector) { - if (this.components?.transform?.scale) { - this.components.transform.scale = val; + if (this._transform?.scale) { + this._transform.scale = val; } } public get pos(): Vector { - return this.components.transform.pos; + return this._transform.pos; } public set pos(val: Vector) { - this.components.transform.pos = val; + this._transform.pos = val; } public on(eventName: Events.preupdate, handler: (event: Events.PreUpdateEvent) => void): void; @@ -145,8 +147,8 @@ export class TileMapImpl extends Entity })(); } } - - this.components.graphics.localBounds = new BoundingBox({ + this._transform = this.get(TransformComponent); + this.get(GraphicsComponent).localBounds = new BoundingBox({ left: 0, top: 0, right: this.cols * this.cellWidth, @@ -267,7 +269,7 @@ export class TileMapImpl extends Entity this._onScreenYStart = Math.max(Math.floor((worldCoordsUpperLeft.y - this.y) / this.cellHeight) - 2, 0); this._onScreenXEnd = Math.max(Math.floor((worldCoordsLowerRight.x - this.x) / this.cellWidth) + 2, 0); this._onScreenYEnd = Math.max(Math.floor((worldCoordsLowerRight.y - this.y) / this.cellHeight) + 2, 0); - this.components.transform.pos = vec(this.x, this.y); + this._transform.pos = vec(this.x, this.y); this.onPostUpdate(engine, delta); this.emit('postupdate', new Events.PostUpdateEvent(engine, delta, this)); @@ -383,7 +385,7 @@ export class TileMap extends Configurable(TileMapImpl) { /** * @hidden */ -export class CellImpl extends Entity { +export class CellImpl extends Entity { private _bounds: BoundingBox; public x: number; public y: number; diff --git a/src/spec/ActorSpec.ts b/src/spec/ActorSpec.ts index a7e93703..bf6e58f4 100644 --- a/src/spec/ActorSpec.ts +++ b/src/spec/ActorSpec.ts @@ -187,7 +187,7 @@ describe('A game actor', () => { const child = new ex.Actor(0, 0, 50, 50); - actor.add(child); + actor.addChild(child); expect(child.width).toBe(100); expect(child.height).toBe(100); @@ -316,7 +316,7 @@ describe('A game actor', () => { const child = new ex.Actor(10, 0, 10, 10); // (20, 10) - actor.add(child); + actor.addChild(child); actor.update(engine, 100); expect(child.getGlobalPos().x).toBeCloseTo(10, 0.001); @@ -332,8 +332,8 @@ describe('A game actor', () => { const child = new ex.Actor(10, 0, 10, 10); // (20, 10) const grandchild = new ex.Actor(10, 0, 10, 10); // (30, 10) - actor.add(child); - child.add(grandchild); + actor.addChild(child); + child.addChild(grandchild); actor.update(engine, 100); expect(grandchild.getGlobalRotation()).toBe(rotation); @@ -348,7 +348,7 @@ describe('A game actor', () => { const child = new ex.Actor(10, 10, 10, 10); - actor.add(child); + actor.addChild(child); actor.update(engine, 100); expect(child.getGlobalPos().x).toBe(30); @@ -363,8 +363,8 @@ describe('A game actor', () => { const child = new ex.Actor(10, 10, 10, 10); const grandchild = new ex.Actor(10, 10, 10, 10); - actor.add(child); - child.add(grandchild); + actor.addChild(child); + child.addChild(grandchild); actor.update(engine, 100); // Logic: @@ -384,7 +384,7 @@ describe('A game actor', () => { const child = new ex.Actor(10, 0, 10, 10); // (30, 10) - actor.add(child); + actor.addChild(child); actor.update(engine, 100); expect(child.getGlobalPos().x).toBeCloseTo(10, 0.001); @@ -401,8 +401,8 @@ describe('A game actor', () => { const child = new ex.Actor(10, 0, 10, 10); // (30, 10) const grandchild = new ex.Actor(10, 0, 10, 10); // (50, 10) - actor.add(child); - child.add(grandchild); + actor.addChild(child); + child.addChild(grandchild); actor.update(engine, 100); expect(grandchild.getGlobalPos().x).toBeCloseTo(10, 0.001); @@ -417,7 +417,7 @@ describe('A game actor', () => { expect(childActor.pos.x).toBe(50); expect(childActor.pos.y).toBe(50); - actor.add(childActor); + actor.addChild(childActor); actor.actions.moveTo(10, 15, 1000); actor.update(engine, 1000); @@ -434,8 +434,8 @@ describe('A game actor', () => { const childActor = new ex.Actor(50, 50); const grandChildActor = new ex.Actor(10, 10); - actor.add(childActor); - childActor.add(grandChildActor); + actor.addChild(childActor); + childActor.addChild(grandChildActor); actor.actions.moveBy(10, 15, 1000); actor.update(engine, 1000); @@ -690,8 +690,8 @@ describe('A game actor', () => { const child = new ex.Actor(0, 0, 100, 100); const child2 = new ex.Actor(-600, -100, 100, 100); - parent.add(child); - child.add(child2); + parent.addChild(child); + child.addChild(child2); // check reality expect(parent.contains(550, 50)).toBeTruthy(); @@ -713,14 +713,14 @@ describe('A game actor', () => { const parent = new ex.Actor(0, 0, 100, 100); const child = new ex.Actor(100, 100, 100, 100); const child2 = new ex.Actor(100, 100, 100, 100); - parent.add(child); + parent.addChild(child); expect(parent.contains(150, 150)).toBeFalsy(); expect(child.contains(150, 150)).toBeTruthy(); expect(parent.contains(150, 150, true)).toBeTruthy(); expect(parent.contains(200, 200, true)).toBeFalsy(); - child.add(child2); + child.addChild(child2); expect(parent.contains(250, 250, true)).toBeTruthy(); }); @@ -794,7 +794,7 @@ describe('A game actor', () => { const parentActor = new ex.Actor(); const childActor = new ex.Actor(); scene.add(parentActor); - parentActor.add(childActor); + parentActor.addChild(childActor); spyOn(childActor, 'update'); @@ -807,7 +807,7 @@ describe('A game actor', () => { const parentActor = new ex.Actor(); const childActor = new ex.Actor(); scene.add(parentActor); - parentActor.add(childActor); + parentActor.addChild(childActor); spyOn(childActor, 'draw'); @@ -820,7 +820,7 @@ describe('A game actor', () => { const parentActor = new ex.Actor(); const childActor = new ex.Actor(); scene.add(parentActor); - parentActor.add(childActor); + parentActor.addChild(childActor); spyOn(childActor, 'draw'); @@ -899,8 +899,8 @@ describe('A game actor', () => { const child = new ex.Actor(); const grandchild = new ex.Actor(); let initializeCount = 0; - actor.add(child); - child.add(grandchild); + actor.addChild(child); + child.addChild(grandchild); actor.on('initialize', () => { initializeCount++; }); diff --git a/src/spec/EntitySpec.ts b/src/spec/EntitySpec.ts index 778bf7af..bd3b3725 100644 --- a/src/spec/EntitySpec.ts +++ b/src/spec/EntitySpec.ts @@ -56,6 +56,21 @@ describe('An entity', () => { 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'); + + 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]); + }); + it('can have type from tag components', () => { const entity = new ex.Entity(); const isOffscreen = new TagComponent('offscreen'); @@ -73,7 +88,7 @@ describe('An entity', () => { it('can be observed for added changes', (done) => { const entity = new ex.Entity(); const typeA = new FakeComponent('A'); - entity.changes.register({ + entity.componentAdded$.register({ notify: (change) => { expect(change.type).toBe('Component Added'); expect(change.data.entity).toBe(entity); @@ -89,7 +104,7 @@ describe('An entity', () => { const typeA = new FakeComponent('A'); entity.addComponent(typeA); - entity.changes.register({ + entity.componentRemoved$.register({ notify: (change) => { expect(change.type).toBe('Component Removed'); expect(change.data.entity).toBe(entity); @@ -107,14 +122,44 @@ describe('An entity', () => { const typeB = new FakeComponent('B'); entity.addComponent(typeA); entity.addComponent(typeB); + entity.addChild( + new ex.Entity([ + new FakeComponent('Z') + ]) + ); const clone = entity.clone(); expect(clone).not.toBe(entity); expect(clone.id).not.toBe(entity.id); - expect(clone.components.A).not.toBe(entity.components.A); - expect(clone.components.B).not.toBe(entity.components.B); - expect(clone.components.A.type).toBe(entity.components.A.type); - expect(clone.components.B.type).toBe(entity.components.B.type); + 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.children.length).toBe(1); + expect(clone.children[0].types).toEqual(['Z']); + }); + + 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') + ]) + ) + ); + + 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']); }); it('can be checked if it has a component', () => { @@ -186,7 +231,7 @@ describe('An entity', () => { it('can be parented', () => { const parent = new ex.Entity(); const child = new ex.Entity(); - parent.add(child); + parent.addChild(child); expect(child.parent).toBe(parent); expect(parent.children).toEqual([child]); @@ -197,7 +242,7 @@ describe('An entity', () => { const parent = new ex.Entity(); const child = new ex.Entity(); const grandchild = new ex.Entity(); - parent.add(child.add(grandchild)); + parent.addChild(child.addChild(grandchild)); expect(grandchild.parent).toBe(child); expect(child.parent).toBe(parent); @@ -211,7 +256,7 @@ describe('An entity', () => { const parent = new ex.Entity(); const child = new ex.Entity(); const grandchild = new ex.Entity(); - parent.add(child.add(grandchild)); + parent.addChild(child.addChild(grandchild)); expect(child.parent).toBe(parent); @@ -225,10 +270,10 @@ describe('An entity', () => { const parent = new ex.Entity(); const child = new ex.Entity(); - parent.add(child); + parent.addChild(child); expect(() => { - child.add(parent); + child.addChild(parent); }).toThrowError('Cycle detected, cannot add entity'); }); @@ -237,9 +282,9 @@ describe('An entity', () => { const child = new ex.Entity(); const otherparent = new ex.Entity(); - parent.add(child); + parent.addChild(child); expect(() => { - otherparent.add(child); + otherparent.addChild(child); }).toThrowError('Entity already has a parent, cannot add without unparenting'); }); @@ -271,7 +316,7 @@ describe('An entity', () => { const e = new ex.Entity(); const child = new ex.Entity(); const grandchild = new ex.Entity(); - e.add(child.add(grandchild)); + e.addChild(child.addChild(grandchild)); const world = new ex.World(new ex.Scene()); @@ -279,11 +324,11 @@ describe('An entity', () => { expect(world.entityManager.entities.length).toBe(3); - grandchild.add(new ex.Entity()); + grandchild.addChild(new ex.Entity()); expect(world.entityManager.entities.length).toBe(4); - child.remove(grandchild); + child.removeChild(grandchild); expect(world.entityManager.entities.length).toBe(2); }); diff --git a/src/spec/GraphicsSystemSpec.ts b/src/spec/GraphicsSystemSpec.ts index ff6e66be..713b959a 100644 --- a/src/spec/GraphicsSystemSpec.ts +++ b/src/spec/GraphicsSystemSpec.ts @@ -5,7 +5,7 @@ import { GraphicsComponent } from '../engine/Graphics'; import { TestUtils } from './util/TestUtils'; describe('A Graphics ECS System', () => { - let entities: ex.Entity[]; + let entities: ex.Entity[]; let engine: ex.Engine; beforeEach(() => { jasmine.addMatchers(ExcaliburMatchers); @@ -17,9 +17,9 @@ describe('A Graphics ECS System', () => { new ex.Entity().addComponent(new ex.TransformComponent()).addComponent(new ex.Graphics.GraphicsComponent()), new ex.Entity().addComponent(new ex.TransformComponent()).addComponent(new ex.Graphics.GraphicsComponent()) ]; - entities[0].components.transform.z = 10; - entities[1].components.transform.z = 5; - entities[2].components.transform.z = 1; + entities[0].get(TransformComponent).z = 10; + entities[1].get(TransformComponent).z = 5; + entities[2].get(TransformComponent).z = 1; }); it('exists', () => { @@ -45,9 +45,7 @@ describe('A Graphics ECS System', () => { color: ex.Color.Yellow }); - const offscreen = new ex.Entity([new TransformComponent(), new GraphicsComponent()]) as ex.Entity< - TransformComponent | GraphicsComponent - >; + const offscreen = new ex.Entity([new TransformComponent(), new GraphicsComponent()]); offscreen.get(GraphicsComponent).show(rect); offscreen.get(TransformComponent).pos = ex.vec(112.5, 112.5); @@ -97,21 +95,21 @@ describe('A Graphics ECS System', () => { color: ex.Color.Red }); - entities[0].components.transform.pos = ex.vec(25, 25); - entities[0].components.transform.rotation = Math.PI / 4; - entities[0].components.graphics.show(rect); + entities[0].get(TransformComponent).pos = ex.vec(25, 25); + entities[0].get(TransformComponent).rotation = Math.PI / 4; + entities[0].get(GraphicsComponent).show(rect); - entities[1].components.transform.pos = ex.vec(75, 75); - entities[1].components.graphics.show(circle); + entities[1].get(TransformComponent).pos = ex.vec(75, 75); + entities[1].get(GraphicsComponent).show(circle); - entities[2].components.transform.pos = ex.vec(75, 25); - entities[2].components.transform.scale = ex.vec(2, 2); - entities[2].components.graphics.show(rect2); + entities[2].get(TransformComponent).pos = ex.vec(75, 25); + entities[2].get(TransformComponent).scale = ex.vec(2, 2); + entities[2].get(GraphicsComponent).show(rect2); const offscreenRect = rect.clone(); const offscreen = new ex.Entity().addComponent(new TransformComponent()).addComponent(new GraphicsComponent()); - offscreen.components.transform.pos = ex.vec(112.5, 112.5); - offscreen.components.graphics.show(offscreenRect); + offscreen.get(TransformComponent).pos = ex.vec(112.5, 112.5); + offscreen.get(GraphicsComponent).show(offscreenRect); spyOn(rect, 'draw').and.callThrough(); spyOn(circle, 'draw').and.callThrough(); diff --git a/src/spec/QueryManagerSpec.ts b/src/spec/QueryManagerSpec.ts index 2fe9e297..a5bf29b7 100644 --- a/src/spec/QueryManagerSpec.ts +++ b/src/spec/QueryManagerSpec.ts @@ -18,11 +18,11 @@ describe('A QueryManager', () => { it('can create queries for entities', () => { const world = new ex.World(null); - const entity1 = new ex.Entity | FakeComponent<'B'>>(); + const entity1 = new ex.Entity(); entity1.addComponent(new FakeComponent('A')); entity1.addComponent(new FakeComponent('B')); - const entity2 = new ex.Entity>(); + const entity2 = new ex.Entity(); entity2.addComponent(new FakeComponent('A')); world.entityManager.addEntity(entity1); @@ -39,7 +39,7 @@ describe('A QueryManager', () => { // Queries update if component change entity2.addComponent(new FakeComponent('B')); expect(queryAB.getEntities()).toEqual( - [entity1, entity2 as ex.Entity | FakeComponent<'B'>>], + [entity1, entity2], 'Now both entities have A+B' ); @@ -144,7 +144,7 @@ describe('A QueryManager', () => { expect(queryAB.getEntities()).toEqual([entity1, entity2]); - const removed = entity1.components.A; + const removed = entity1.get('A'); entity1.removeComponent('A'); world.queryManager.removeComponent(entity1, removed); @@ -168,7 +168,7 @@ describe('A QueryManager', () => { expect(queryAB.getEntities()).toEqual([entity1, entity2]); - const removed = entity1.components.C; + const removed = entity1.get('C'); entity1.removeComponent('C'); world.queryManager.removeComponent(entity1, removed); diff --git a/src/spec/QuerySpec.ts b/src/spec/QuerySpec.ts index 306bd657..fb27948b 100644 --- a/src/spec/QuerySpec.ts +++ b/src/spec/QuerySpec.ts @@ -41,11 +41,11 @@ describe('A query', () => { const queryAB = new ex.Query(['A', 'B']); const compA = new FakeComponent('A'); const compB = new FakeComponent('B'); - const entity1 = new ex.Entity | FakeComponent<'B'>>(); + const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); - const entity2 = new ex.Entity>(); + const entity2 = new ex.Entity(); entity2.addComponent(compA); queryAB.addEntity(entity1); @@ -59,11 +59,11 @@ describe('A query', () => { const queryAB = new ex.Query(['A', 'B']); const compA = new FakeComponent('A'); const compB = new FakeComponent('B'); - const entity1 = new ex.Entity | FakeComponent<'B'>>(); + const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); - const entity2 = new ex.Entity>(); + const entity2 = new ex.Entity(); entity2.addComponent(compA); queryAB.addEntity(entity1); @@ -80,7 +80,7 @@ describe('A query', () => { const queryAB = new ex.Query(['A', 'B']); const compA = new FakeComponent('A'); const compB = new FakeComponent('B'); - const entity1 = new ex.Entity | FakeComponent<'B'>>(); + const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); @@ -99,7 +99,7 @@ describe('A query', () => { const queryAB = new ex.Query(['A', 'B']); const compA = new FakeComponent('A'); const compB = new FakeComponent('B'); - const entity1 = new ex.Entity | FakeComponent<'B'>>(); + const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); queryAB.addEntity(entity1); @@ -119,7 +119,7 @@ describe('A query', () => { const queryAB = new ex.Query(['A', 'B']); const compA = new FakeComponent('A'); const compB = new FakeComponent('B'); - const entity1 = new ex.Entity | FakeComponent<'B'>>(); + const entity1 = new ex.Entity(); entity1.addComponent(compA); entity1.addComponent(compB); diff --git a/src/spec/TransformComponentSpec.ts b/src/spec/TransformComponentSpec.ts index ea512fdb..600a480f 100644 --- a/src/spec/TransformComponentSpec.ts +++ b/src/spec/TransformComponentSpec.ts @@ -62,7 +62,7 @@ describe('A TransformComponent', () => { it('can have parent/child relationships with position', () => { const parent = new ex.Entity([new ex.TransformComponent()]); const child = new ex.Entity([new ex.TransformComponent()]); - parent.add(child); + parent.addChild(child); const parentTx = parent.get(ex.TransformComponent); const childTx = child.get(ex.TransformComponent); @@ -87,7 +87,7 @@ describe('A TransformComponent', () => { it('can have parent/child relationships with scale', () => { const parent = new ex.Entity([new ex.TransformComponent()]); const child = new ex.Entity([new ex.TransformComponent()]); - parent.add(child); + parent.addChild(child); const parentTx = parent.get(ex.TransformComponent); const childTx = child.get(ex.TransformComponent); @@ -112,7 +112,7 @@ describe('A TransformComponent', () => { it('can have parent/child relations with rotation', () => { const parent = new ex.Entity([new ex.TransformComponent()]); const child = new ex.Entity([new ex.TransformComponent()]); - parent.add(child); + parent.addChild(child); const parentTx = parent.get(ex.TransformComponent); const childTx = child.get(ex.TransformComponent);