Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
Something went wrong. Try again.
20 kB · 692 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693import type { ComponentCtor } from './component';import { Component, isComponentCtor } from './component';
import type { Message } from '../util/observable';import { Observable } from '../util/observable';import type { OnInitialize, OnPreUpdate, OnPostUpdate, OnAdd, OnRemove } from '../interfaces/lifecycle-events';import type { Engine } from '../engine';import { InitializeEvent, PreUpdateEvent, PostUpdateEvent, AddEvent, RemoveEvent } from '../events';import { KillEvent } from '../events';import type { EventKey, Handler, Subscription } from '../event-emitter';import { EventEmitter } from '../event-emitter';import type { Scene } from '../scene';import { removeItemFromArray } from '../util/util';import type { MaybeKnownComponent } from './types';
/** * Interface holding an entity component pair */export interface EntityComponent { component: Component; entity: Entity;}
/** * AddedComponent message */export class AddedComponent implements Message<EntityComponent> { readonly type: 'Component Added' = 'Component Added'; constructor(public data: EntityComponent) {}}
/** * Type guard to know if message is f an Added Component */export function isAddedComponent(x: Message<EntityComponent>): x is AddedComponent { return !!x && x.type === 'Component Added';}
/** * RemovedComponent message */export class RemovedComponent implements Message<EntityComponent> { readonly type: 'Component Removed' = 'Component Removed'; constructor(public data: EntityComponent) {}}
/** * Type guard to know if message is for a Removed Component */export function isRemovedComponent(x: Message<EntityComponent>): x is RemovedComponent { return !!x && x.type === 'Component Removed';}
/** * Built in events supported by all entities */export interface EntityEvents { initialize: InitializeEvent; //@ts-ignore add: AddEvent; //@ts-ignore remove: RemoveEvent; preupdate: PreUpdateEvent; postupdate: PostUpdateEvent; kill: KillEvent;}
export const EntityEvents = { Add: 'add', Remove: 'remove', Initialize: 'initialize', PreUpdate: 'preupdate', PostUpdate: 'postupdate', Kill: 'kill'} as const;
export interface EntityOptions<TComponents extends Component> { 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 * * Entities can be strongly typed with the components they contain * * ```typescript * const entity = new Entity<ComponentA | ComponentB>(); * entity.components.a; // Type ComponentA * entity.components.b; // Type ComponentB * ``` */export class Entity<TKnownComponents extends Component = any> implements OnInitialize, OnPreUpdate, OnPostUpdate, OnAdd, OnRemove { 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<EntityEvents>(); private _tags = new Set<string>(); public componentAdded$ = new Observable<Component>(); public componentRemoved$ = new Observable<Component>(); public tagAdded$ = new Observable<string>(); public tagRemoved$ = new Observable<string>(); /** * 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<Function, Component>(); public componentValues: Component[] = []; public _componentsToRemove: ComponentCtor[] = [];
constructor(options: EntityOptions<TKnownComponents>); constructor(components?: TKnownComponents[], name?: string); constructor(componentsOrOptions?: TKnownComponents[] | EntityOptions<TKnownComponents>, 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); } } }
/** * The current scene that the entity is in, if any */ public scene: Scene | null = null;
/** * Whether this entity is active, if set to false it will be reclaimed */ public isActive: boolean = true;
/** * Kill the entity, means it will no longer be updated. Kills are deferred to the end of the update. * If parented it will be removed from the parent when killed. */ public kill() { if (this.isActive) { this.isActive = false; this.unparent(); } this.emit('kill', new KillEvent(this)); }
public isKilled() { return !this.isActive; }
/** * Specifically get the tags on the entity from {@apilink TagsComponent} */ public get tags(): Set<string> { return this._tags; }
/** * Check if a tag exists on the entity * @param tag name to check for */ public hasTag(tag: string): boolean { return this._tags.has(tag); }
/** * Adds a tag to an entity * @param tag */ public addTag(tag: string): Entity<TKnownComponents> { this._tags.add(tag); this.tagAdded$.notifyAll(tag); return this; }
/** * Removes a tag on the entity * * Removals are deferred until the end of update * @param tag */ public removeTag(tag: string): Entity<TKnownComponents> { this._tags.delete(tag); this.tagRemoved$.notifyAll(tag); return this; }
/** * The types of the components on the Entity */ public get types(): ComponentCtor[] { return Array.from(this.components.keys()) as ComponentCtor[]; }
/** * Returns all component instances on entity */ public getComponents(): Component[] { return Array.from(this.components.values()); }
/** * Verifies that an entity has all the required types * @param requiredTypes */ hasAll<TComponent extends Component>(requiredTypes: ComponentCtor<TComponent>[]): boolean { for (let i = 0; i < requiredTypes.length; i++) { if (!this.components.has(requiredTypes[i])) { return false; } } return true; }
/** * Verifies that an entity has all the required tags * @param requiredTags */ hasAllTags(requiredTags: string[]): boolean { for (let i = 0; i < requiredTags.length; i++) { if (!this.tags.has(requiredTags[i])) { return false; } } return true; }
get<TComponent extends Component>(type: ComponentCtor<TComponent>): MaybeKnownComponent<TComponent, TKnownComponents> { return this.components.get(type) as MaybeKnownComponent<TComponent, TKnownComponents>; }
private _parent: Entity | null = null; public get parent(): Entity | null { return this._parent; }
public childrenAdded$ = new Observable<Entity>(); public childrenRemoved$ = new Observable<Entity>();
private _children: Entity[] = []; /** * Get the direct children of this entity */ public get children(): readonly Entity[] { return this._children; }
/** * Unparents this entity, if there is a parent. Otherwise it does nothing. */ public unparent() { if (this._parent) { this._parent.removeChild(this); this._parent = null; } }
/** * Check if a child entity exists on the parent entity * @param child entity to check for * @param recursive whether to check recursively */ public hasChild(child: Entity, recursive = false): boolean { if (!recursive) { return child.parent === this; }
for (const c of this.children) { if (c === child) { return true; } if (recursive && c.hasChild(child, true)) { return true; } }
return false; }
/** * Adds an entity to be a child of this entity * @param entity */ public addChild(entity: Entity): Entity { if (entity.parent === null) { if (this.getAncestors().includes(entity)) { throw new Error('Cycle detected, cannot add entity'); } this._children.push(entity); entity._parent = this; this.childrenAdded$.notifyAll(entity); } else { throw new Error('Entity already has a parent, cannot add without unparenting'); } return this; }
/** * Remove an entity from children if it exists * @param entity */ public removeChild(entity: Entity): Entity { if (entity.parent === this) { removeItemFromArray(entity, this._children); entity._parent = null; this.childrenRemoved$.notifyAll(entity); } return this; }
/** * Removes all children from this entity */ public removeAllChildren(): Entity { // Avoid modifying the array issue by walking backwards for (let i = this.children.length - 1; i >= 0; i--) { this.removeChild(this.children[i]); } return this; }
/** * Returns a list of parent entities starting with the topmost parent. Includes the current entity. */ public getAncestors(): Entity[] { const result: Entity[] = [this]; let current = this.parent; while (current) { result.push(current); current = current.parent; } return result.reverse(); }
/** * Returns a list of all the entities that descend from this entity. Includes the current entity. */ public getDescendants(): Entity[] { let result: Entity[] = [this]; let queue: Entity[] = [this]; while (queue.length > 0) { const curr = queue.pop(); if (curr) { queue = queue.concat(curr.children); result = result.concat(curr.children); } } return result; }
/** * Creates a deep copy of the entity and a copy of all its components */ public clone(): Entity { const newEntity = new Entity(); for (const c of this.types) { const componentInstance = this.get(c); if (componentInstance) { newEntity.addComponent(componentInstance.clone()); } } for (const child of this.children) { newEntity.addChild(child.clone()); } return newEntity; }
/** * 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 already exists on the target 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; }
private _getClassHierarchyRoot(componentType: ComponentCtor): ComponentCtor { let current = componentType; let parent = Object.getPrototypeOf(current.prototype)?.constructor;
while (parent && parent !== Object && parent !== Component) { current = parent; parent = Object.getPrototypeOf(current.prototype)?.constructor; } return current; }
/** * 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<TComponent extends Component>(component: TComponent, force: boolean = false): Entity<TKnownComponents | TComponent> { // if component already exists, skip if not forced if (this.has(component.constructor as ComponentCtor)) { if (force) { // Remove existing component type if exists when forced this.removeComponent(component.constructor as ComponentCtor, true); } else { // early exit component exits return this as Entity<TKnownComponents | TComponent>; } }
// TODO circular dependencies will be a problem if (component.dependencies && component.dependencies.length) { for (const ctor of component.dependencies) { this.addComponent(new ctor()); } }
component.owner = this; const rootComponent = this._getClassHierarchyRoot(component.constructor as ComponentCtor); this.components.set(rootComponent, component); this.components.set(component.constructor, component); this.componentValues.push(component); if (component.onAdd) { component.onAdd(this); }
this.componentAdded$.notifyAll(component); return this as Entity<TKnownComponents | TComponent>; }
/** * 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 typeOrInstance * @param force */ public removeComponent<TComponent extends Component>( typeOrInstance: ComponentCtor<TComponent> | TComponent, force = false ): Entity<Exclude<TKnownComponents, TComponent>> { let type: ComponentCtor<TComponent>; if (isComponentCtor(typeOrInstance)) { type = typeOrInstance; } else { type = typeOrInstance.constructor as ComponentCtor<TComponent>; }
if (force) { const componentToRemove = this.components.get(type); if (componentToRemove) { this.componentRemoved$.notifyAll(componentToRemove); componentToRemove.owner = null; if (componentToRemove.onRemove) { componentToRemove.onRemove(this); } const componentIndex = this.componentValues.indexOf(componentToRemove); if (componentIndex > -1) { this.componentValues.splice(componentIndex, 1); } }
const rootComponent = this._getClassHierarchyRoot(type); this.components.delete(rootComponent); this.components.delete(type); // remove after the notify to preserve typing } else { this._componentsToRemove.push(type); }
return this as any; }
public clearComponents() { const components = this.types; for (const c of components) { this.removeComponent(c); } }
/** * @hidden * @internal */ public processComponentRemoval() { for (const type of this._componentsToRemove) { this.removeComponent(type, true); } this._componentsToRemove.length = 0; }
/** * Check if a component type exists * @param type */ public has<TComponent extends Component>(type: ComponentCtor<TComponent>): boolean { return this.components.has(type); }
private _isInitialized = false; private _isAdded = false;
/** * Gets whether the actor is Initialized */ public get isInitialized(): boolean { return this._isInitialized; }
public get isAdded(): boolean { return this._isAdded; }
/** * Initializes this entity, meant to be called by the Scene before first update not by users of Excalibur. * * It is not recommended that internal excalibur methods be overridden, do so at your own risk. * @internal */ public _initialize(engine: Engine) { if (!this.isInitialized) { this.onInitialize(engine); this.events.emit('initialize', new InitializeEvent(engine, this)); this._isInitialized = true; } }
/** * Adds this Actor, meant to be called by the Scene when Actor is added. * * It is not recommended that internal excalibur methods be overridden, do so at your own risk. * @internal */ public _add(engine: Engine) { if (!this.isAdded && this.isActive) { this.onAdd(engine); this.events.emit('add', new AddEvent(engine, this)); this._isAdded = true; } }
/** * Removes Actor, meant to be called by the Scene when Actor is removed. * * It is not recommended that internal excalibur methods be overridden, do so at your own risk. * @internal */ public _remove(engine: Engine) { if (this.isAdded && !this.isActive) { this.onRemove(engine); this.events.emit('remove', new RemoveEvent(engine, this)); this._isAdded = false; } }
/** * It is not recommended that internal excalibur methods be overridden, do so at your own risk. * * Internal _preupdate handler for {@apilink onPreUpdate} lifecycle event * @internal */ public _preupdate(engine: Engine, elapsed: number): void { this.events.emit('preupdate', new PreUpdateEvent(engine, elapsed, this)); this.onPreUpdate(engine, elapsed); }
/** * It is not recommended that internal excalibur methods be overridden, do so at your own risk. * * Internal _preupdate handler for {@apilink onPostUpdate} lifecycle event * @internal */ public _postupdate(engine: Engine, elapsed: number): void { this.events.emit('postupdate', new PostUpdateEvent(engine, elapsed, this)); this.onPostUpdate(engine, elapsed); }
/** * `onInitialize` is called before the first update of the entity. This method is meant to be * overridden. * * Synonymous with the event handler `.on('initialize', (evt) => {...})` */ public onInitialize(engine: Engine): void { // Override me }
/** * `onAdd` is called when Actor is added to scene. This method is meant to be * overridden. * * Synonymous with the event handler `.on('add', (evt) => {...})` */ public onAdd(engine: Engine): void { // Override me }
/** * `onRemove` is called when Actor is removed from a scene. This method is meant to be * overridden. * * Synonymous with the event handler `.on('remove', (evt) => {...})` */ public onRemove(engine: Engine): void { // Override me }
/** * Safe to override onPreUpdate lifecycle event handler. Synonymous with `.on('preupdate', (evt) =>{...})` * * `onPreUpdate` is called directly before an entity is updated. */ public onPreUpdate(engine: Engine, elapsed: number): void { // Override me }
/** * Safe to override onPostUpdate lifecycle event handler. Synonymous with `.on('postupdate', (evt) =>{...})` * * `onPostUpdate` is called directly after an entity is updated. */ public onPostUpdate(engine: Engine, elapsed: number): void { // Override me }
/** * * Entity update lifecycle, called internally * @internal * @param engine * @param elapsed */ public update(engine: Engine, elapsed: number): void { this._initialize(engine); this._add(engine); this._preupdate(engine, elapsed); for (const child of this.children) { child.update(engine, elapsed); } this._postupdate(engine, elapsed); this._remove(engine); }
public emit<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, event: EntityEvents[TEventName]): void; public emit(eventName: string, event?: any): void; public emit<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, event?: any): void { this.events.emit(eventName, event); }
public on<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, handler: Handler<EntityEvents[TEventName]>): Subscription; public on(eventName: string, handler: Handler<unknown>): Subscription; public on<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.on(eventName, handler); }
public once<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, handler: Handler<EntityEvents[TEventName]>): Subscription; public once(eventName: string, handler: Handler<unknown>): Subscription; public once<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription { return this.events.once(eventName, handler); }
public off<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, handler: Handler<EntityEvents[TEventName]>): void; public off(eventName: string, handler: Handler<unknown>): void; public off(eventName: string): void; public off<TEventName extends EventKey<EntityEvents> | string>(eventName: TEventName, handler?: Handler<any>): void { if (handler) { this.events.off(eventName, handler); } else { this.events.off(eventName); } }}