UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

357 lines (272 loc) • 9.73 kB
import { assert } from "../../../../core/assert.js"; import { Deque } from "../../../../core/collection/queue/Deque.js"; import Signal from "../../../../core/events/signal/Signal.js"; import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js"; import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js"; import { ParentEntity } from "../../../ecs/parent/ParentEntity.js"; import { AbstractContextSystem } from "../../../ecs/system/AbstractContextSystem.js"; import { SystemEntityContext } from "../../../ecs/system/SystemEntityContext.js"; import { Transform } from "../../../ecs/transform/Transform.js"; import Path from "../../../navigation/ecs/components/Path.js"; import { PathEvents } from "../../../navigation/ecs/components/PathEvents.js"; import { RibbonXPlugin } from "../../trail/x/RibbonXPlugin.js"; import { ShadedGeometry } from "../mesh-v2/ShadedGeometry.js"; import { EntityPath } from "./entity/EntityPath.js"; import { PathDisplay } from "./PathDisplay.js"; import { PathDisplayEvents } from "./PathDisplayEvents.js"; import { PathDisplayType } from "./PathDisplayType.js"; import { RibbonPathBuilder } from "./ribbon/RibbonPathBuilder.js"; import { TubePathBuilder } from "./tube/build/TubePathBuilder.js"; const builders = { [PathDisplayType.None]: function (style, path, result) { }, /** * * @param {EntityPathStyle} style * @param {Path} _p * @param {PathDisplaySystem} system * @param {Entity[]} result */ [PathDisplayType.Entity]: function (style, path, system, result) { const entity_path = new EntityPath(); entity_path.setPath(path); entity_path.setStyle(style); entity_path.build(); const markers = entity_path.markers; const n = markers.length; for (let i = 0; i < n; i++) { const marker = markers[i]; result.push(marker.entity); } }, /** * * @param {RibbonPathStyle} style * @param {Path} path * @param {PathDisplaySystem} system * @param {Entity[]} result */ [PathDisplayType.Ribbon]: function (style, path, system, result) { const builder = new RibbonPathBuilder(); builder.setPath(path); builder.setStyle(style); builder.setPlugin(system.plugin.getValue()); builder.build(result); }, /** * * @param {TubePathStyle} style * @param {Path} path * @param {PathDisplaySystem} system * @param {Entity[]} result */ [PathDisplayType.Tube]: function (style, path, system, result) { const builder = new TubePathBuilder(); builder.setPath(path); builder.setStyle(style); builder.setMaterials(system.engine.graphics.getMaterialManager()); builder.setAssetManager(system.engine.assetManager); builder.build(result); } }; /** * Maximum amount of time allowed per system tick to spend on processing update queue * in milliseconds * @readonly * @type {number} */ const UPDATE_PROCESSING_BUDGET_MS = 10; class PathDisplayContext extends SystemEntityContext { constructor() { super(); /** * * @type {Entity[]} * @private */ this.__owned_entities = []; } __build() { // clear out owned entities const ownedEntities = this.__owned_entities; ownedEntities.splice(0, ownedEntities.length); // todo check that the main entity still exists before we decide to spawn new entities /** * @type {PathDisplay} */ const display = this.components[0]; /** * @type {Path} */ const path = this.components[1]; const specs = display.specs; for (let i = 0; i < specs.length; i++) { const spec = specs[i]; const builder = builders[spec.type]; if (builder === undefined) { // builder not found console.warn(`No builder for type ${spec.type}`); return; } try { builder(spec.style, path, this.system, ownedEntities); } catch (e) { console.error(`Failed to build path of type '${spec.type}'`); console.error(e); } } // ensure that all entities are parented const n = ownedEntities.length; for (let i = 0; i < n; i++) { const e = ownedEntities[i]; e.add(ParentEntity.from(this.entity)); } this.__build_existing_entities(); const ecd = this.getDataset(); // signal completion of build ecd.sendEvent(this.entity, PathDisplayEvents.BuildComplete, ownedEntities); } __build_existing_entities() { const ecd = this.getDataset(); // attach all existing owned entities const owned_entities = this.__owned_entities; const owned_entity_count = owned_entities.length; for (let i = 0; i < owned_entity_count; i++) { const entity = owned_entities[i]; entity.build(ecd); } } __destroy_existing_entities() { const owned_entities = this.__owned_entities; const owned_entity_count = owned_entities.length; for (let i = 0; i < owned_entity_count; i++) { const entity = owned_entities[i]; entity.destroy(); } } rebuild() { this.__destroy_existing_entities(); this.__build(); } request_update() { /** * * @type {PathDisplaySystem} */ const system = this.system; system.request_entity_update(this.entity); } link() { super.link(); this.__build(); const ecd = this.getDataset(); const entity = this.entity; ecd.addEntityEventListener(entity, PathEvents.Changed, this.request_update, this); ecd.addEntityEventListener(entity, PathDisplayEvents.Changed, this.request_update, this); } unlink() { super.unlink(); const ecd = this.getDataset(); const entity = this.entity; ecd.removeEntityEventListener(entity, PathEvents.Changed, this.request_update, this); ecd.removeEntityEventListener(entity, PathDisplayEvents.Changed, this.request_update, this); /** * * @type {PathDisplaySystem} */ const system = this.system; system.cancel_entity_update(entity); // destroy existing owned entities this.__destroy_existing_entities(); } } export class PathDisplaySystem extends AbstractContextSystem { /** * * @param {Engine} engine */ constructor(engine) { super(PathDisplayContext); assert.defined(engine, "engine"); this.dependencies = [ PathDisplay, Path ]; this.components_used = [ ResourceAccessSpecification.from(ParentEntity, ResourceAccessKind.Create), ResourceAccessSpecification.from(ShadedGeometry, ResourceAccessKind.Create), ResourceAccessSpecification.from(Transform, ResourceAccessKind.Create), ]; /** * * @type {Engine} */ this.engine = engine; /** * * @type {Reference<RibbonXPlugin>} */ this.plugin = null; /** * Deferred queue of entities slated to be rebuilt * @type {Deque<number>} * @private */ this.__rebuild_queue = new Deque(); /** * * @type {Signal} */ this.onWorkDone = new Signal(); } /** * Request that path displays be rebuilt * @package * @param {number} entity */ request_entity_update(entity) { assert.isNonNegativeInteger(entity, 'entity'); if (!this.__rebuild_queue.has(entity)) { this.__rebuild_queue.add(entity); } } /** * Cancel any pending updates * @package * @param {number} entity */ cancel_entity_update(entity) { this.__rebuild_queue.remove(entity); } async startup(entityManager) { // bind plugin this.plugin = await this.engine.plugins.acquire(RibbonXPlugin); } async shutdown(entityManager) { this.plugin.release(); } update(time_delta) { // process update queue const queue = this.__rebuild_queue; if (queue.isEmpty()) { return; } const t0 = performance.now(); do { // note that we don't need to check if the queue is empty here for the first iteration, as we already checked earlier const entity = queue.pop(); /** * * @type {PathDisplayContext|undefined} */ const ctx = this.__getEntityContext(entity); // check that entity still exists if (ctx === undefined) { continue; } ctx.rebuild(); } while ((performance.now() - t0) < UPDATE_PROCESSING_BUDGET_MS && !queue.isEmpty()); // notify that some work was done this.onWorkDone.send0(); } }