UNPKG

gibbon.js

Version:

Actor/Component system for use with pixi.js.

417 lines (306 loc) 7.98 kB
import { Actor } from "../.."; import type { Game } from '../game'; import type { Container } from 'pixi.js'; import { contains } from '../utils/array-utils'; import { EngineEvent } from '../events/engine-events'; import { Constructor } from '../utils/types'; /** * If a clip is supplied to the Group, it will act as the parent * of all Actor clips added to the group. */ export class Group<T extends Game = Game> { get actor() { return this._actor; } /** * @property Optional clip associated with group. */ readonly clip?: Container | null; /** * @property {string} name */ name?: string; /** * @property {boolean} enabled */ get enabled() { return this._enabled; } /** * Subgroups of this group. */ readonly subgroups: Group[] = []; /** * Objects in group. */ readonly objects: Actor[] = []; /** * Actor to hold group components. */ private readonly _actor?: Actor<Container>; private _game?: T; /** * Game group is added to, if any. */ public get game() { return this._game } get parent() { return this._parent } /** * Parent group, if any. */ private _parent?: Group; protected isDestroyed: boolean = false; private _enabled: boolean = false; /** * Whether group should enable when added to engine. * Used to track enable state before actually added. */ private _shouldEnable: boolean; /** * * @param actor -actor to assign to group, or container to use as group container, * or 'true' to create a group container. * @param enabled */ constructor(actor?: Container | boolean | undefined | null, enabled: boolean = true) { this._shouldEnable = enabled; if (actor) { this._actor = this.makeGroupActor(actor); this.clip = this._actor.clip; } } /** * Ensure the group has its own group Actor. */ private makeGroupActor(clip: Actor<Container> | Container | boolean): Actor<Container> { let actor: Actor<Container>; if (typeof clip === 'boolean') { actor = new Actor<Container>(); } else if (clip instanceof Actor) { actor = clip; } else { actor = new Actor<Container>(clip); } if (this._game) { this._game.addActor(actor); } return actor; } public enable() { this._shouldEnable = true; if (this._enabled === true) return; for (const g of this.subgroups) { g.enable(); } this._enabled = true; } public disable() { this._shouldEnable = false; if (this._enabled === false) return; this._enabled = false; for (const g of this.subgroups) { g.disable(); } } /** * Override in subclasses for notification of when * group is added to game. */ public onAdded() { } /** * Override in subclasses to be notified when group is removed. */ public onRemoved() { } /** * Internal message of group being added to game. * Do not call directly. * Override onAdded() in subclasses for the event. */ _onAdded(game: T) { if (this._game !== game) { this._game = game; if (this._actor && !this._actor.isAdded) { /// add actor to group. game.addActor(this._actor); } /// Add all objects in group. for (const a of this.objects) { game.addActor(a); } for (const s of this.subgroups) { game.addGroup(s); } this.onAdded(); if (this._shouldEnable) { this.enable(); } } } /** * Show all the objects in the group and subgroups. */ show() { if (this.actor) { this.actor.visible = true; } for (let i = this.subgroups.length - 1; i >= 0; i--) { this.subgroups[i].show(); } } /** * Hide all actors in this group and subgroups. */ hide() { if (this.actor) { this.actor.visible = false; } for (let i = this.subgroups.length - 1; i >= 0; i--) { this.subgroups[i].hide(); } } /** * Get group by class. Searches this group then * recurses up the parent chain, and then to the * current game, if Group has been added to game. * @param type * @returns */ getGroup<G extends Group>(type: Constructor<G>): G | undefined { for (let i = this.subgroups.length - 1; i >= 0; i--) { if (this.subgroups[i] instanceof type) { return this.subgroups[i] as G; } } if (this._parent) { return this._parent.getGroup(type) } else if (this._game) { return this._game.getGroup(type); } } /** * Find subgroup of this group. * @param gname * @returns */ findGroup(gname: string): Group | undefined { for (let i = this.subgroups.length - 1; i >= 0; i--) { if (this.subgroups[i].name == gname) return this.subgroups[i]; } return undefined; } /** * Return first subgroup found of type. */ get<GType extends Group<T> = Group<T>>(kind: Constructor<GType>) { for (let i = this.subgroups.length - 1; i >= 0; i--) { if (this.subgroups[i] instanceof kind) { return this.subgroups[i] as GType; } } return null; } /** * Add subgroup to this group. * @param {Group} sub */ addGroup(sub: Group) { if (!contains(this.subgroups, sub)) { if (sub._parent) { if (sub._parent === this) return; sub._parent.removeGroup(sub); } sub._parent = this; this.subgroups.push(sub); this.game?.addGroup(sub) } } /** * Remove Actor from group, but not Game or Engine. * @param {Actor} obj */ remove(obj: Actor) { const ind = this.objects.indexOf(obj); if (ind < 0) return; this.objects.splice(ind, 1); obj.off(EngineEvent.ActorDestroyed, this.remove, this); obj.group = null; } /** * * @param {Actor} obj * @returns {Actor} the object. */ add(obj: Actor): Actor { obj.group = this; obj.on(EngineEvent.ActorDestroyed, this.remove, this); this.objects.push(obj); this._game?.engine.add(obj); return obj; } /** * Internal message of group being removed from game. * Do not call directly. * Override onRemoved() in subclasses for the event. */ _onRemoved() { /// Save previous enabled state in case group is re-added. const curEnable = this._enabled; this.disable(); this._shouldEnable = curEnable; const game = this._game; if (game) { this.onRemoved(); this._game = undefined; for (const a of this.objects) { game.engine.remove(a); } for (const s of this.subgroups) { game.removeGroup(s); } } } /** * Remove subgroup from this group. * @param {Group} sub */ removeGroup(sub: Group) { if (sub._parent !== this) { return; } sub._parent = undefined; for (let i = this.subgroups.length - 1; i >= 0; i--) { if (this.subgroups[i] == sub) { this.subgroups.splice(i, 1); break; } } this.game?.removeGroup(sub); } /** * Override in subclasses to cleanup before group destroyed. */ onDestroy?(): void; destroy() { this.isDestroyed = true; for (let i = this.subgroups.length - 1; i >= 0; i--) { this.subgroups[i].destroy(); } for (let i = this.objects.length - 1; i >= 0; i--) { // Don't listen to the remove event since we're already looping. this.objects[i].off(EngineEvent.ActorDestroyed, this.remove, this); this.objects[i].destroy(); } this.objects.length = 0; this.subgroups.length = 0; /// workaround to ensure 'game' exists in the onDestroy() /// function since _onRemoved() clears it. const tempGame = this._game; if (this._parent && !this._parent.isDestroyed) { this._parent.removeGroup(this); } else { this._game?.removeGroup(this); } this._game = tempGame; this.onDestroy?.(); this._actor?.destroy(); this._parent = undefined; this._game = undefined; } }