UNPKG

gibbon.js

Version:

Actor/Component system for use with pixi.js.

464 lines 13.1 kB
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