@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
223 lines (177 loc) • 5.56 kB
JavaScript
import { EventType } from "../EventType.js";
import { AbstractContextSystem } from "../system/AbstractContextSystem.js";
import { SystemEntityContext } from "../system/SystemEntityContext.js";
import { ParentEntity } from "./ParentEntity.js";
/**
*
* @type {number[]|Uint32Array}
*/
const loop_checker_array = new Uint32Array(1024);
/**
*
* @param {number} value
* @param {number} index_limit
* @returns {boolean}
*/
function check_loop(value, index_limit) {
for (let i = 0; i < index_limit; i++) {
if (loop_checker_array[i] === value) {
return true;
}
}
return false;
}
/**
*
* @param {number} limit
* @returns {string}
*/
function genLoopString(limit) {
let r = "";
for (let i = 0; i < limit; i++) {
if (i > 0) {
r += " -> ";
}
r += loop_checker_array[i];
}
return r;
}
class Context extends SystemEntityContext {
link() {
super.link();
const ecd = this.getDataset();
/**
*
* @type {ParentEntity}
*/
const cParent = this.components[0];
ecd.addEntityEventListener(cParent.entity, EventType.EntityRemoved, this.handleParentEntityRemoval, this);
}
handleParentEntityRemoval() {
// delete this entity as well
this.getDataset().removeEntity(this.entity);
}
unlink() {
super.unlink();
const ecd = this.getDataset();
/**
*
* @type {ParentEntity}
*/
const cParent = this.components[0];
ecd.removeEntityEventListener(cParent.entity, EventType.EntityRemoved, this.handleParentEntityRemoval, this);
}
}
export class ParentEntitySystem extends AbstractContextSystem {
constructor() {
super(Context);
this.dependencies = [ParentEntity];
}
/**
* Find root entity given some entity with a {@link ParentEntity} component attached
* @param {EntityComponentDataset} ecd
* @param {number} entity
* @returns {number}
*/
static findRoot(ecd, entity) {
let count = 0;
let p = entity;
while (true) {
loop_checker_array[count++] = p;
const parent = ParentEntitySystem.findParentEntity(ecd, p);
if (parent === -1) {
break;
}
if (check_loop(parent, count)) {
throw new Error(`Cyclic reference encountered: ${genLoopString(count)}`);
}
p = parent;
}
return p;
}
/**
* @param {EntityComponentDataset} ecd
* @param {number} child_entity
* @param {number} ancestor_entity
* @returns {boolean}
*/
static isAncestorOf(ecd, child_entity, ancestor_entity) {
let loop_count = 0;
let p = child_entity;
while (true) {
if (p === ancestor_entity) {
return true;
}
loop_checker_array[loop_count++] = p;
p = ParentEntitySystem.findParentEntity(ecd, p);
if (p === -1) {
break;
}
if (check_loop(p, loop_count)) {
throw new Error(`Cyclic reference encountered: ${genLoopString(loop_count)}`);
}
}
// reached the root, ancestor not encountered
return false;
}
/**
* @param {EntityComponentDataset} ecd
* @param {number} entity
* @returns {number}
*/
static findParentEntity(ecd, entity) {
const parent = ecd.getComponent(entity, ParentEntity);
if (parent === undefined) {
return -1;
}
return parent.entity;
}
/**
*
* @param {number[]} result
* @param {number} result_offset
* @param {EntityComponentDataset} ecd
* @param {number} entity
* @returns {number}
*/
static findChildrenOf(result, result_offset, ecd, entity) {
let result_cursor = result_offset;
ecd.traverseComponents(ParentEntity, (p, e) => {
if (p.entity === entity) {
result[result_cursor++] = e;
}
});
return result_cursor - result_offset;
}
/**
*
* @param {EntityComponentDataset} ecd
* @param {number} root_entity
* @param {function(number, EntityComponentDataset)} callback
* @param {*} [thisArg]
*/
static traverse(ecd, root_entity, callback, thisArg) {
traverse(ecd, root_entity, callback, thisArg);
}
}
/**
*
* @param {EntityComponentDataset} ecd
* @param {number} root_entity
* @param {function(number, EntityComponentDataset)} callback
* @param {*} [thisArg]
* @param {number[]} path keep track of where we came from, this is used to detect cycles
*/
function traverse(ecd, root_entity, callback, thisArg, path = []) {
callback.call(thisArg, root_entity, ecd);
const children = [];
const child_count = ParentEntitySystem.findChildrenOf(children, 0, ecd, root_entity);
for (let i = 0; i < child_count; i++) {
const entity = children[i];
if (path.includes(entity)) {
console.warn(`Loop in entity hierarchy detected: ${path.join('->')}`);
continue;
}
traverse(ecd, entity, callback, thisArg, path.concat(entity));
}
}