gibbon.js
Version:
Actor/Component system for use with pixi.js.
601 lines (461 loc) • 12.7 kB
text/typescript
import { Point, DisplayObject, Container, utils, DisplayObjectEvents } from 'pixi.js';
import type { Group } from './group';
import { Game } from '../game';
import { Component } from './component';
import { Constructor } from '../utils/types';
import { Transform } from './transform';
import { EngineEvent } from '../events/engine-events';
import type { IPoint } from '@/data/geom';
export type ComponentKey = Component | Constructor<Component>;
/**
* Options for destroying a Actor
*/
export type DestroyOptions = {
children?: boolean,
texture?: boolean,
baseTexture?: boolean
};
/**
*
*/
export class Actor<T extends DisplayObject = DisplayObject, G extends Game = Game> {
private static NextId: number = 1000;
readonly id: number;
/**
* @property {Game} game
*/
get game() { return Game.current as G; }
/**
* @property {Group} group - owning group of the actor, if any.
*/
get group() { return this._group; }
set group(v: Group | null) { this._group = v; }
/**
* @property {string} name - Name of the Actor.
*/
get name() { return this._name; }
set name(v) { this._name = v; }
/**
* @property {boolean} active
*/
get active() { return this._active; }
set active(v: boolean) {
if (this._active != v) {
this._active = v;
if (v) {
for (const comp of this._components) {
comp.onActivate?.();
}
} else {
for (const comp of this._components) {
comp.onDeactivate?.();
}
}
}
}
/**
* @property {Point} position - Position of the object and Display clip.
*/
get position() { return this._position; }
set position(v) { this._position.set(v.x, v.y); }
/**
* @property x
*/
get x() { return this._position.x; }
set x(v: number) { this._position.x = v; }
/**
* @property y
*/
get y(): number { return this._position.y; }
set y(v: number) { this._position.y = v; }
/**
* @property rotation - Rotation in radians.
*/
get rotation() { return this._rotation; }
set rotation(v) {
if (v > 2 * Math.PI || v < -2 * Math.PI) v %= 2 * Math.PI;
this._rotation = v;
if (this.clip && this._autoRotate === true) {
this.clip.rotation = v;
}
}
get autoRotate() { return this._autoRotate }
set autoRotate(v) { this._autoRotate = v }
get width() { return this.clip?.getBounds().width ?? 0; }
get height() { return this.clip?.getBounds().height ?? 0; }
/**
* @property interactive - Set the interactivity for the Actor.
* The setting is ignored if the Actor has no clip.
*/
get interactive() { return this.clip?.interactive ?? false }
set interactive(v: boolean) {
if (this.clip) {
this.clip.interactive = v;
}
}
get sleeping() { return this._sleep; }
set sleep(v: boolean) { this._sleep = v; }
/**
* @property orient - returns the normalized orientation vector of the object.
*/
get orient() {
return new Point(Math.cos(this._rotation), Math.sin(this._rotation));
}
get visible() { return this.clip?.visible ?? false }
set visible(v: boolean) {
if (this.clip != null) {
this.clip.visible = v;
}
}
/**
* @property isAdded - true after Actor has been added to Engine.
*/
get isAdded() { return this._isAdded; }
/**
* @property destroyed
*/
get isDestroyed() { return this._destroyed }
/**
* @property clip - clip of the actor.d.
*/
readonly clip?: T;
readonly _components: Component[] = [];
/**
* Object was destroyed and should not be used any more.
*/
private _destroyed: boolean = false;
/**
* If true, actor display object will match actor's rotation.
*/
private _autoRotate: boolean = true;
/**
* Game object was added to engine.
*/
private _isAdded: boolean = false;
readonly emitter: utils.EventEmitter<DisplayObjectEvents>;
protected _sleep: boolean = false;
protected _name: string = '';
protected _destroyOpts?: DestroyOptions;
protected _active: boolean = false;
private _rotation: number = 0;
protected _position: Point;
protected _group: Group | null = null;
readonly transform: Transform = new Transform();
flags: number = 0;
private readonly _compMap: Map<Constructor<Component> | Function, Component> = new Map();
/**
* List of components waiting to be added next update.
* Components cannot add while actor is updating.
*/
private _toAdd: Component<any>[] = [];
/**
*
* @param [clip=null]
* @param [pos=null]
*/
constructor(clip?: T | null, pos?: IPoint | null) {
this.id = Actor.NextId++;
if (clip != null) {
if (pos) {
clip.position.set(pos.x, pos.y);
}
this._position = clip.position;
this._destroyOpts = {
children: true,
texture: false,
baseTexture: false
}
} else {
this._position = new Point(pos?.x, pos?.y);
}
this._active = true;
this.emitter = clip ?? new utils.EventEmitter();
this.clip = clip ?? undefined;
}
/**
* Override in subclass.
*/
added(): void { }
/**
* Called by Engine when Actor is added to engine.
* Calls init() on all components and self.added()
*/
_added() {
this._isAdded = true;
/// add components waiting.
this._addNew();
const len = this._components.length;
for (let i = 0; i < len; i++) {
this._components[i]._init(this);
}
if (this._active) {
for (let i = 0; i < len; i++) {
this._components[i].onActivate?.();
}
}
this.added();
}
/**
*
* @param evt
* @param func
* @returns
*/
on(evt: string, func: utils.EventEmitter.ListenerFn, context?: any) {
if (this.clip != null) {
return this.clip.on(evt, func, context);
}
else {
return this.emitter.on(evt, func, context);
}
}
/**
* Wrap emitter off()
* @param {...any} args
*/
off(e: string, fn?: utils.EventEmitter.ListenerFn, context?: any) {
this.emitter.off(e, fn, context);
}
/**
* Emit an event through the underlying actor clip. If the actor
* does not contain a clip, the event is emitted through a custom emitter.
* @param {*} args - First argument should be the {string} event name.
*/
emit(event: string, ...args: any[]) {
this.emitter.emit(event, ...args);
}
/**
* Add an existing component to the Actor.
* @param {Component} inst
* @param {?Object} [cls=null]
* @returns {Component} Returns the instance.
*/
public addInstance<C extends Component>(inst: C, cls?: Constructor<C>): C {
const key = cls ?? (<any>inst).constructor ?? Object.getPrototypeOf(inst).constructor ?? inst;
this._compMap.set(key, inst);
this._toAdd.push(inst);
if (this._isAdded) {
inst._init(this);
if (this._active) {
inst.onActivate?.();
}
}
return inst;
}
/**
* Instantiate and add a component to the Actor.
* @param {class} cls - component class to instantiate.
* @param args - arguments to pass to class constructor.
* @returns {Object}
*/
add<C extends Component>(cls: C | Constructor<C>, ...args: any[]): C {
if (cls instanceof Component) {
return this.addInstance(cls);
} else {
return this.addInstance(new cls(...args), cls);
}
}
/**
* Checks if the Object's clip contains a global point.
* Always false for objects without clips or clip.hitAreas.
* @param {Vector|Object} pt
* @param {number} pt.x
* @param {number} pt.y
* @returns {boolean}
*/
contains(pt: Point): boolean {
const clip = this.clip;
if (clip == null) return false;
if (!clip.hitArea) {
return clip.getBounds().contains(pt.x, pt.y);
}
pt = clip.toLocal(pt);
return clip.hitArea.contains(pt.x, pt.y);
}
/**
*
* @param {number} x
* @param {number} y
*/
translate(x: number, y: number) {
this._position.set(this._position.x + x, this._position.y + y);
}
/**
* Determine if Actor contains a Component entry
* under class or key cls.
* @param {*} cls - class or key of component.
*/
has(cls: Constructor<Component<T>>) {
return this._compMap.has(cls);
}
/**
*
* @param {*} cls
*/
get<C extends Component>(cls: Constructor<C>): C | undefined {
const inst = this._compMap.get(cls) as C;
if (inst) return inst;
for (const comp of this._compMap.values()) {
if (comp instanceof cls) return comp as C;
}
/*for (let i = this._components.length - 1; i >= 0; i--) {
if (this._components[i] instanceof cls) return this._components[i] as C;
}*/
return undefined;
}
/**
*
* @param {*} cls
*/
require<C extends Component>(cls: Constructor<C>, ...args: any[]): C {
const inst = this._compMap.get(cls);
if (inst) return inst as C;
for (let i = this._components.length - 1; i >= 0; i--) {
if (this._components[i] instanceof cls && !this._components[i].isDestroyed) return this._components[i] as C;
}
return this.add(cls, ...args);
}
/**
*
* @param {number} delta - Tick time in seconds.
*/
update(delta: number) {
const comps = this._components;
let destroyed = 0;
this._addNew();
for (let i = comps.length - 1; i >= 0; i--) {
const comp = comps[i];
if (comp._destroyed === true) {
destroyed++;
continue;
}
if (comp.update && comp.enabled === true) {
comp.update(delta);
}
}
this._removeDestroyed(destroyed);
}
/**
* Add waiting components to component list.
*/
_addNew() {
if (this._toAdd.length > 0) {
this._components.push.apply(this._components, this._toAdd);
this._components.sort((a, b) => a.priority - b.priority);
this._toAdd.length = 0;
}
}
/**
* Remove destroyed components at the end of update.
*/
_removeDestroyed(count: number) {
const comps = this._components;
const len = comps.length;
/// Slide down non-destroyed components over the destroyed ones.
/// Move destroyed components forward until they reach end.
for (let i = 0; i < len; i++) {
const c = comps[i];
if (!c._destroyed) {
/// slide down until a non-destroyed component is hit.
let j = i - 1;
while (j >= 0) {
if (!comps[j]._destroyed) {
break;
} else {
j--;
}
}
/// swap destroyed component ahead.
if (++j < i) {
comps[i] = comps[j];
comps[j] = c;
}
}
}
/// all destroyed components are now at the end of the array.
while (count--) {
comps.pop();
}
}
/**
*
* @param {Component} comp - the component to remove from the game object.
* @param {bool} [destroy=true] - whether the component should be destroyed.
*/
remove(comp: Component | Constructor<Component>, destroy: boolean = true) {
if (!(comp instanceof Component)) {
const val = this._compMap.get(comp);
if (val) {
comp = val;
} else {
return false;
}
}
if (destroy === true) comp.destroy();
this._compMap.delete(comp.constructor || comp);
return true;
}
show() {
if (this.clip != null) {
this.clip.visible = true;
}
}
hide() {
if (this.clip != null) {
this.clip.visible = false;
}
}
/**
* Set options for destroying the PIXI DisplayObject when
* the Actor is destroyed.
* @param {boolean} children
* @param {boolean} texture
* @param {boolean} baseTexture
*/
setDestroyOpts(children: boolean, texture: boolean, baseTexture: boolean) {
if (this._destroyOpts == null) {
this._destroyOpts = {
children: children,
texture: texture,
baseTexture: baseTexture
};
} else {
this._destroyOpts.children = children;
this._destroyOpts.texture = texture;
this._destroyOpts.baseTexture = baseTexture;
}
}
/**
* Call to destroy the game Object.
* Do not call _destroy() directly.
*/
destroy() {
if (this._destroyed === true) {
return;
}
this._destroyed = true;
this.emitter.emit(EngineEvent.ActorDestroyed, this);
const comps = this._components;
for (let i = comps.length - 1; i >= 0; i--) {
this.remove(comps[i]);
}
if (this._group) {
this._group?.remove(this);
this._group = null;
}
}
/**
* destroys all components and then the Actor itself.
*/
_destroy() {
this.emitter.removeAllListeners();
if (this.clip) {
if (this.clip instanceof Container && this._destroyOpts) {
this.clip.destroy(this._destroyOpts);
this._destroyOpts = undefined;
} else {
this.clip.destroy();
}
}
}
}