@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
357 lines (272 loc) • 9.73 kB
JavaScript
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();
}
}