Skip to content

Commit

Permalink
feat: Entity onAdd and onRemove event handlers and lifecycle (#3143)
Browse files Browse the repository at this point in the history
* first cut at onAdd and onRemove

* updated transfer method

* complete

* added on('event') for add and remove

* added test that doesn't work

* fix test

---------

Co-authored-by: Erik Onarheim <[email protected]>
  • Loading branch information
jyoung4242 and eonarheim authored Sep 19, 2024
1 parent 0415293 commit 81495b2
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 5 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/engine/Actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -984,8 +984,10 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
*/
public update(engine: Engine, elapsedMs: number) {
this._initialize(engine);
this._add(engine);
this._preupdate(engine, elapsedMs);
this._postupdate(engine, elapsedMs);
this._remove(engine);
}

/**
Expand Down
67 changes: 64 additions & 3 deletions src/engine/EntityComponentSystem/Entity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, ComponentCtor, isComponentCtor } from './Component';

import { Observable, Message } from '../Util/Observable';
import { OnInitialize, OnPreUpdate, OnPostUpdate } from '../Interfaces/LifecycleEvents';
import { OnInitialize, OnPreUpdate, OnPostUpdate, OnAdd, OnRemove } from '../Interfaces/LifecycleEvents';
import { Engine } from '../Engine';
import { InitializeEvent, PreUpdateEvent, PostUpdateEvent } from '../Events';
import { InitializeEvent, PreUpdateEvent, PostUpdateEvent, AddEvent, RemoveEvent } from '../Events';
import { KillEvent } from '../Events';
import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter';
import { Scene } from '../Scene';
Expand Down Expand Up @@ -54,12 +54,18 @@ export function isRemovedComponent(x: Message<EntityComponent>): x is RemovedCom
*/
export type 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',
Expand All @@ -83,7 +89,7 @@ export interface EntityOptions<TComponents extends Component> {
* entity.components.b; // Type ComponentB
* ```
*/
export class Entity<TKnownComponents extends Component = any> implements OnInitialize, OnPreUpdate, OnPostUpdate {
export class Entity<TKnownComponents extends Component = any> implements OnInitialize, OnPreUpdate, OnPostUpdate, OnAdd, OnRemove {
private static _ID = 0;
/**
* The unique identifier for the entity
Expand Down Expand Up @@ -513,6 +519,7 @@ export class Entity<TKnownComponents extends Component = any> implements OnIniti
}

private _isInitialized = false;
private _isAdded = false;

/**
* Gets whether the actor is Initialized
Expand All @@ -521,6 +528,10 @@ export class Entity<TKnownComponents extends Component = any> implements OnIniti
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.
*
Expand All @@ -535,6 +546,34 @@ export class Entity<TKnownComponents extends Component = any> implements OnIniti
}
}

/**
* 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.active) {
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 added.
*
* 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.active) {
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.
*
Expand Down Expand Up @@ -567,6 +606,26 @@ export class Entity<TKnownComponents extends Component = any> implements OnIniti
// 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 added to 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) =>{...})`
*
Expand Down Expand Up @@ -594,11 +653,13 @@ export class Entity<TKnownComponents extends Component = any> implements OnIniti
*/
public update(engine: Engine, elapsedMs: number): void {
this._initialize(engine);
this._add(engine);
this._preupdate(engine, elapsedMs);
for (const child of this.children) {
child.update(engine, elapsedMs);
}
this._postupdate(engine, elapsedMs);
this._remove(engine);
}

public emit<TEventName extends EventKey<EntityEvents>>(eventName: TEventName, event: EntityEvents[TEventName]): void;
Expand Down
34 changes: 32 additions & 2 deletions src/engine/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Side } from './Collision/Side';
import { CollisionContact } from './Collision/Detection/CollisionContact';
import { Collider } from './Collision/Colliders/Collider';
import { Entity } from './EntityComponentSystem/Entity';
import { OnInitialize, OnPreUpdate, OnPostUpdate, SceneActivationContext } from './Interfaces/LifecycleEvents';
import { OnInitialize, OnPreUpdate, OnPostUpdate, SceneActivationContext, OnAdd, OnRemove } from './Interfaces/LifecycleEvents';
import { BodyComponent } from './Collision/BodyComponent';
import { ExcaliburGraphicsContext } from './Graphics';
import { Axes, Buttons, Gamepad } from './Input/Gamepad';
Expand Down Expand Up @@ -84,7 +84,10 @@ export enum EventTypes {
PointerDragMove = 'pointerdragmove',

ActionStart = 'actionstart',
ActionComplete = 'actioncomplete'
ActionComplete = 'actioncomplete',

Add = 'add',
Remove = 'remove'
}

/* istanbul ignore next */
Expand Down Expand Up @@ -159,6 +162,9 @@ export type pointerdragenter = 'pointerdragenter';
export type pointerdragleave = 'pointerdragleave';
export type pointerdragmove = 'pointerdragmove';

export type add = 'add';
export type remove = 'remove';

/**
* Base event type in Excalibur that all other event types derive from. Not all event types are thrown on all Excalibur game objects,
* some events are unique to a type, others are not.
Expand Down Expand Up @@ -706,3 +712,27 @@ export class ActionCompleteEvent extends GameEvent<Entity> {
super();
}
}

/**
* Event thrown on an [[Actor]] when an Actor added to scene.
*/
export class AddEvent<T extends OnAdd> extends GameEvent<T> {
constructor(
public engine: Engine,
public target: T
) {
super();
}
}

/**
* Event thrown on an [[Actor]] when an Actor removed from scene.
*/
export class RemoveEvent<T extends OnRemove> extends GameEvent<T> {
constructor(
public engine: Engine,
public target: T
) {
super();
}
}
60 changes: 60 additions & 0 deletions src/engine/Interfaces/LifecycleEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { ExcaliburGraphicsContext } from '../Graphics';
export interface _initialize {
_initialize(engine: Engine): void;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export interface _add {
onAdd(scene: Scene): void;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export interface _remove {
onRemove(engine: Engine): void;
}

/**
* Type guard checking for internal initialize method
Expand All @@ -17,6 +25,20 @@ export function has_initialize(a: any): a is _initialize {
return !!a._initialize;
}

/**
*
*/
export function has_add(a: any): a is _add {
return !!a.onAdd;
}

/**
*
*/
export function has_remove(a: any): a is _remove {
return !!a.onRemove;
}

export interface OnInitialize {
onInitialize(engine: Engine): void;
}
Expand Down Expand Up @@ -88,6 +110,44 @@ export interface CanInitialize {
off(eventName: Events.initialize, handler?: (event: Events.InitializeEvent<any>) => void): void;
}

export interface OnAdd {
onAdd(engine: Engine): void;
}

/**
*
*/
export function hasOnAdd(a: any): a is OnAdd {
return !!a.onAdd;
}

export interface CanAdd {
onAdd(engine: Engine): void;

on(eventName: Events.add, handler: (event: Events.AddEvent<any>) => void): void;
once(eventName: Events.add, handler: (event: Events.AddEvent<any>) => void): void;
off(eventName: Events.add, handler?: (event: Events.AddEvent<any>) => void): void;
}

export interface OnRemove {
onRemove(engine: Engine): void;
}

/**
*
*/
export function hasOnRemove(a: any): a is OnRemove {
return !!a.onRemove;
}

export interface CanRemove {
onRemove(engine: Engine): void;

on(eventName: Events.remove, handler: (event: Events.RemoveEvent<any>) => void): void;
once(eventName: Events.remove, handler: (event: Events.RemoveEvent<any>) => void): void;
off(eventName: Events.remove, handler?: (event: Events.RemoveEvent<any>) => void): void;
}

export interface SceneActivationContext<TData = undefined> {
data?: TData;
previousScene: Scene;
Expand Down
1 change: 1 addition & 0 deletions src/engine/Scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ export class Scene<TActivationData = unknown> implements CanInitialize, CanActiv
scene = entity.scene;
entity.scene.removeTimer(entity);
}

scene?.emit('entityremoved', { target: entity } as any);
this.add(entity);
}
Expand Down
23 changes: 23 additions & 0 deletions src/spec/ActorSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,5 +1439,28 @@ describe('A game actor', () => {
expect(actor._postkill).toHaveBeenCalledTimes(1);
expect(actor.onPostKill).toHaveBeenCalledTimes(1);
});

it('can run onAdd and onRemove', () => {
actor.onRemove = (engine: ex.Engine) => {
expect(engine).not.toBe(null);
};

actor.onAdd = (engine: ex.Engine) => {
expect(engine).not.toBe(null);
};

spyOn(actor, 'onAdd').and.callThrough();
spyOn(actor, 'onRemove').and.callThrough();
engine.add(actor);
engine.currentScene.update(engine, 100);
engine.remove(actor);
engine.currentScene.update(engine, 100);
expect(actor.onAdd).toHaveBeenCalledTimes(1);
expect(actor.onRemove).toHaveBeenCalledTimes(1);
engine.add(actor);
engine.currentScene.update(engine, 100);
expect(actor.onAdd).toHaveBeenCalledTimes(2);
expect(actor.onRemove).toHaveBeenCalledTimes(1);
});
});
});

0 comments on commit 81495b2

Please sign in to comment.