gibbon.js
Version:
Actor/Component system for use with pixi.js.
464 lines • 13.1 kB
JavaScript
import { Point, Container, utils } from 'pixi.js';
import { Game } from '../game';
import { Component } from './component';
import { Transform } from './transform';
import { EngineEvent } from '../events/engine-events';
/**
*
*/
export class Actor {
static NextId = 1000;
id;
/**
* @property {Game} game
*/
get game() { return Game.current; }
/**
* @property {Group} group - owning group of the actor, if any.
*/
get group() { return this._group; }
set group(v) { 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) {
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) { this._position.x = v; }
/**
* @property y
*/
get y() { return this._position.y; }
set y(v) { 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) {
if (this.clip) {
this.clip.interactive = v;
}
}
get sleeping() { return this._sleep; }
set sleep(v) { 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) {
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.
*/
clip;
_components = [];
/**
* Object was destroyed and should not be used any more.
*/
_destroyed = false;
/**
* If true, actor display object will match actor's rotation.
*/
_autoRotate = true;
/**
* Game object was added to engine.
*/
_isAdded = false;
emitter;
_sleep = false;
_name = '';
_destroyOpts;
_active = false;
_rotation = 0;
_position;
_group = null;
transform = new Transform();
flags = 0;
_compMap = new Map();
/**
* List of components waiting to be added next update.
* Components cannot add while actor is updating.
*/
_toAdd = [];
/**
*
* @param [clip=null]
* @param [pos=null]
*/
constructor(clip, pos) {
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() { }
/**
* 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, func, context) {
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, fn, context) {
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, ...args) {
this.emitter.emit(event, ...args);
}
/**
* Add an existing component to the Actor.
* @param {Component} inst
* @param {?Object} [cls=null]
* @returns {Component} Returns the instance.
*/
addInstance(inst, cls) {
const key = cls ?? 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(cls, ...args) {
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) {
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, y) {
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) {
return this._compMap.has(cls);
}
/**
*
* @param {*} cls
*/
get(cls) {
const inst = this._compMap.get(cls);
if (inst)
return inst;
for (const comp of this._compMap.values()) {
if (comp instanceof cls)
return comp;
}
/*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(cls, ...args) {
const inst = this._compMap.get(cls);
if (inst)
return inst;
for (let i = this._components.length - 1; i >= 0; i--) {
if (this._components[i] instanceof cls && !this._components[i].isDestroyed)
return this._components[i];
}
return this.add(cls, ...args);
}
/**
*
* @param {number} delta - Tick time in seconds.
*/
update(delta) {
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) {
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, destroy = 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, texture, baseTexture) {
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();
}
}
}
}
//# sourceMappingURL=actor.js.map