@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
795 lines (622 loc) • 26 kB
JavaScript
import '../../../../css/editor/EditorView.scss';
import '../../../../css/editor/EntityEditorView.scss';
import { resetSoundEmitterTracks } from "../../model/game/options/resetSoundEmitterTracks.js";
import { assert } from "../src/core/assert.js";
import { downloadAsFile } from "../src/core/binary/downloadAsFile.js";
import { EncodingBinaryBuffer } from "../src/core/binary/EncodingBinaryBuffer.js";
import { EndianType } from "../src/core/binary/EndianType.js";
import List from '../src/core/collection/list/List.js';
import { Color } from "../src/core/color/Color.js";
import Signal from "../src/core/events/signal/Signal.js";
import Quaternion from "../src/core/geom/Quaternion.js";
import Vector1 from "../src/core/geom/Vector1.js";
import Vector2 from "../src/core/geom/Vector2.js";
import Vector3 from "../src/core/geom/Vector3.js";
import Vector4 from "../src/core/geom/Vector4.js";
import { NumericInterval } from "../src/core/math/interval/NumericInterval.js";
import ObservedBoolean from "../src/core/model/ObservedBoolean.js";
import ObservedEnum from "../src/core/model/ObservedEnum.js";
import ObservedInteger from "../src/core/model/ObservedInteger.js";
import ObservedString from "../src/core/model/ObservedString.js";
import { ActionProcessor } from '../src/core/process/undo/ActionProcessor.js';
import WorkerProxy from "../src/core/process/worker/WorkerProxy.js";
import Entity from "../src/engine/ecs/Entity.js";
import { EntityComponentDataset } from "../src/engine/ecs/EntityComponentDataset.js";
import BinaryBufferSerializer from "../src/engine/ecs/storage/BinaryBufferSerializer.js";
import { Transform } from "../src/engine/ecs/transform/Transform.js";
import { Camera } from "../src/engine/graphics/ecs/camera/Camera.js";
import TopDownCameraController from "../src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js";
import TopDownCameraControllerSystem, {
setCameraControllerFromTransform
} from "../src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js";
import { ParameterTrackSet } from "../src/engine/graphics/particles/particular/engine/parameter/ParameterTrackSet.js";
import { Sampler2D } from "../src/engine/graphics/texture/sampler/Sampler2D.js";
import { KeyCodes } from "../src/engine/input/devices/KeyCodes.js";
import InputControllerSystem from "../src/engine/input/ecs/systems/InputControllerSystem.js";
import ComponentAddAction from "./actions/concrete/ComponentAddAction.js";
import EntityCreateAction from "./actions/concrete/EntityCreateAction.js";
import EntityRemoveAction from "./actions/concrete/EntityRemoveAction.js";
import SelectionAddAction from "./actions/concrete/SelectionAddAction.js";
import SelectionClearAction from "./actions/concrete/SelectionClearAction.js";
import { ListEditor } from "./ecs/component/editors/collection/ListEditor.js";
import { ColorEditor } from "./ecs/component/editors/ColorEditor.js";
import { noEditor } from "./ecs/component/editors/common/noEditor.js";
import { ParameterTrackSetEditor } from "./ecs/component/editors/ecs/ParameterTrackSetEditor.js";
import { QuaternionEditor } from "./ecs/component/editors/geom/QuaternionEditor.js";
import { Vector1Editor } from "./ecs/component/editors/geom/Vector1Editor.js";
import { Vector2Editor } from "./ecs/component/editors/geom/Vector2Editor.js";
import { Vector3Editor } from "./ecs/component/editors/geom/Vector3Editor.js";
import { Vector4Editor } from "./ecs/component/editors/geom/Vector4Editor.js";
import { HTMLElementEditor } from "./ecs/component/editors/HTMLElementEditor.js";
import { NumericIntervalEditor } from "./ecs/component/editors/NumericIntervalEditor.js";
import { ObservedBooleanEditor } from "./ecs/component/editors/ObservedBooleanEditor.js";
import { ObservedEnumEditor } from "./ecs/component/editors/ObservedEnumEditor.js";
import { ObservedIntegerEditor } from "./ecs/component/editors/ObservedIntegerEditor.js";
import { ObservedStringEditor } from "./ecs/component/editors/ObservedStringEditor.js";
import { ArrayEditor } from "./ecs/component/editors/primitive/ArrayEditor.js";
import { BooleanEditor } from "./ecs/component/editors/primitive/BooleanEditor.js";
import { FunctionEditor } from "./ecs/component/editors/primitive/FunctionEditor.js";
import { NumberEditor } from "./ecs/component/editors/primitive/NumberEditor.js";
import { ObjectEditor } from "./ecs/component/editors/primitive/ObjectEditor.js";
import { StringEditor } from "./ecs/component/editors/primitive/StringEditor.js";
import { Sampler2DEditor } from "./ecs/component/editors/Sampler2DEditor.js";
import EditorEntity from "./ecs/EditorEntity.js";
import EditorEntitySystem from "./ecs/EditorEntitySystem.js";
import { MeshLibrary } from "./library/MeshLibrary.js";
import { ProcessEngine } from "./process/ProcessEngine.js";
import { SelectionVisualizer } from "./SelectionVisualizer.js";
import ToolEngine from './tools/engine/ToolEngine.js';
import { TransformerMode } from "./tools/TransformTool.js";
import EditorView from './view/EditorView.js';
/**
* @template T
* @param {T} original
* @param {EntityManager} em
* @returns {T}
*/
function cloneComponent(original, em) {
function newInstance() {
return new original.constructor();
}
const cloneViaMethod = {
name: 'Clone Method',
check: function (original) {
return typeof original.clone === "function";
},
execute: function (original) {
return original.clone();
}
};
const cloneViaSerialization = {
name: 'Serialization',
check: function (original) {
return typeof original.toJSON === "function" && typeof original.fromJSON === "function";
},
execute: function (original) {
//use serialization to create a clone
const json = original.toJSON();
const clone = newInstance();
const system = em.getOwnerSystemByComponentClass(original.constructor);
clone.fromJSON(json, system);
return clone;
}
};
const cloneViaInstantiate = {
name: 'New Instance',
check: function (original) {
return true;
},
execute: function (original) {
//no clone method, that's a bummer
const typeName = original.constructor.typeName;
console.error(`Component '${typeName}' has no 'clone' method, creating a new instance instead..`);
return newInstance();
}
};
const cloners = [cloneViaMethod, cloneViaSerialization, cloneViaInstantiate];
for (let i = 0; i < cloners.length; i++) {
const cloner = cloners[i];
const applicable = cloner.check(original);
if (!applicable) {
continue;
}
let clone;
try {
clone = cloner.execute(original);
} catch (e) {
continue;
}
if (original.constructor !== clone.constructor) {
console.error(`Cloner ${cloner.name} produced instance with different constructor`, 'original=', original, 'clone=', clone);
continue;
}
return clone;
}
throw new Error(`Failed to clone, no cloners worked`);
}
/**
*
* @param {Editor} editor
*/
function copySelectedEntities(editor) {
if (editor.selection.isEmpty()) {
//clear out copy buffer
editor.copyBuffer = [];
return;
}
/**
*
* @type {EntityManager}
*/
const em = editor.engine.entityManager;
const ecd = em.dataset;
editor.copyBuffer = editor.selection.map(function (entity) {
const components = ecd.getAllComponents(entity);
//clone all components to preserve their status
const clonedComponents = [];
components.forEach(function (c) {
if (c === undefined) {
//skip undefined
return;
}
let clone;
try {
clone = cloneComponent(c, em);
} catch (e) {
console.error(`Failed to clone component, omitting`, c, e);
return;
}
clonedComponents.push(clone);
});
return clonedComponents;
});
}
/**
*
* @param {Editor} editor
*/
function pasteEntities(editor) {
const copyBuffer = editor.copyBuffer;
if (copyBuffer.length === 0) {
//copy buffer is empty, nothing to do
return;
}
const actions = editor.actions;
actions.mark('paste entities');
const createdEntities = copyBuffer.map(function (components) {
const entityCreateAction = new EntityCreateAction();
actions.do(entityCreateAction);
//clone all components to preserve status of the buffer
const n = components.length;
for (let i = 0; i < n; i++) {
const c = components[i];
const clone = cloneComponent(c, editor.engine.entityManager);
const componentAddAction = new ComponentAddAction(entityCreateAction.entity, clone);
try {
actions.do(componentAddAction);
} catch (e) {
console.warn(`Failed to add component.`, e, 'The component might have been added as a result of some EntityObserver');
}
}
return entityCreateAction.entity;
});
//select newly created entities
actions.do(new SelectionClearAction());
actions.do(new SelectionAddAction(createdEntities));
}
function buildEditorCamera() {
const cameraEntity = new Entity();
const camera = new Camera();
camera.projectionType.set(Camera.ProjectionType.Perspective);
camera.autoClip = true;
camera.active.set(true);
cameraEntity.add(camera);
const topDownCameraController = new TopDownCameraController();
topDownCameraController.distanceMin = -Infinity;
topDownCameraController.distanceMax = Infinity;
cameraEntity.add(topDownCameraController);
cameraEntity.add(new Transform());
cameraEntity.add(new EditorEntity());
return cameraEntity;
}
/**
*
* @param {Entity} cameraEntity
* @param {EntityComponentDataset} dataset
* @param {Editor} editor
*/
function activateEditorCamera(cameraEntity, dataset, editor) {
const camera = cameraEntity.getComponent(Camera);
let activeCameraEntity = null;
//find currently active camera
dataset.traverseEntities([Camera], function (c, entity) {
if (dataset.getComponent(entity, EditorEntity) !== undefined) {
return true;
}
if (c.active.getValue()) {
c.active.set(false);
editor.cleanupTasks.push(function () {
//remember to restore active camera
c.active.set(true);
});
activeCameraEntity = entity;
}
});
if (activeCameraEntity !== null) {
/**
*
* @type {Transform}
*/
const acTransform = dataset.getComponent(activeCameraEntity, Transform);
/**
*
* @type {Transform}
*/
const cameraTransform = cameraEntity.getComponent(Transform);
if (acTransform !== undefined) {
cameraTransform.copy(acTransform);
}
/**
*
* @type {TopDownCameraController}
*/
const cameraController = cameraEntity.getComponent(TopDownCameraController);
const active_camera_controller = dataset.getComponent(activeCameraEntity, TopDownCameraController);
if (active_camera_controller !== undefined) {
cameraController.target.copy(active_camera_controller.target);
cameraController.distance = active_camera_controller.distance;
cameraController.pitch = active_camera_controller.pitch;
cameraController.yaw = active_camera_controller.yaw;
cameraController.roll = active_camera_controller.roll;
} else {
setCameraControllerFromTransform(cameraTransform, cameraController);
}
}
camera.active.set(true);
}
/**
*
* @param {Map<any, TypeEditor>} registry
*/
function initialize_basic_registry(registry) {
registry.set(Number, new NumberEditor());
registry.set(Boolean, new BooleanEditor());
registry.set(String, new StringEditor());
registry.set(Function, new FunctionEditor());
registry.set(Array, new ArrayEditor());
registry.set(Object, new ObjectEditor());
registry.set(ObservedInteger, new ObservedIntegerEditor());
registry.set(ObservedBoolean, new ObservedBooleanEditor());
registry.set(ObservedEnum, new ObservedEnumEditor());
registry.set(ObservedString, new ObservedStringEditor());
registry.set(NumericInterval, new NumericIntervalEditor());
registry.set(List, new ListEditor());
registry.set(Color, new ColorEditor());
registry.set(Vector4, new Vector4Editor());
registry.set(Vector3, new Vector3Editor());
registry.set(Vector2, new Vector2Editor());
registry.set(Vector1, new Vector1Editor());
registry.set(Quaternion, new QuaternionEditor());
registry.set(Sampler2D, new Sampler2DEditor());
registry.set(ParameterTrackSet, new ParameterTrackSetEditor());
registry.set(HTMLElement, new HTMLElementEditor());
registry.set(HTMLCanvasElement, new HTMLElementEditor());
registry.set(WorkerProxy, noEditor());
registry.set(Signal, noEditor());
registry.set(Uint8Array, noEditor());
registry.set(Uint16Array, noEditor());
registry.set(Uint32Array, noEditor());
registry.set(Int8Array, noEditor());
registry.set(Int16Array, noEditor());
registry.set(Int32Array, noEditor());
registry.set(Float32Array, noEditor());
registry.set(Float64Array, noEditor());
registry.set(CanvasRenderingContext2D, noEditor());
}
/**
*
* @constructor
* @property {List.<Number>} selection represents list of currently selected entities
* @property {List.<Object>} history list of applied actions
*/
function Editor() {
this.processEngine = new ProcessEngine();
this.toolEngine = new ToolEngine();
this.selection = new List();
this.actions = new ActionProcessor(this);
this.selectionVistualizer = new SelectionVisualizer(this);
this.meshLibrary = new MeshLibrary();
/**
*
* @type {Map<any, TypeEditor>}
*/
this.type_editor_registry = new Map();
initialize_basic_registry(this.type_editor_registry);
this.cameraEntity = buildEditorCamera();
this.editorEntitySystem = new EditorEntitySystem();
this.view = null;
this.copyBuffer = [];
const self = this;
/**
*
* @param keyCode
* @param {KeyboardEvent} event
*/
function processCtrlCombinations(keyCode, event) {
if (keyCode === KeyCodes.z) {
event.preventDefault();
event.stopPropagation();
self.actions.undo();
} else if (keyCode === KeyCodes.y) {
event.preventDefault();
event.stopPropagation();
self.actions.redo();
} else if (keyCode === KeyCodes.c) {
event.preventDefault();
event.stopPropagation();
//copy
copySelectedEntities(self);
} else if (keyCode === KeyCodes.v) {
event.preventDefault();
event.stopPropagation();
//paste
pasteEntities(self);
} else if (keyCode === KeyCodes.s) {
event.preventDefault();
event.stopPropagation();
const entityManager = self.engine.entityManager;
const currentDataset = entityManager.dataset;
//clone dataset
const dataset = new EntityComponentDataset();
dataset.setComponentTypeMap(currentDataset.getComponentTypeMap());
dataset.maskedCopy(currentDataset, currentDataset.getComponentTypeMap());
//remove all Editor entities
dataset.traverseComponents(EditorEntity, function (c, entity) {
dataset.removeEntity(entity);
});
//clone and re-enable the camera
dataset.traverseComponents(Camera, function (c, entity) {
const clone = c.clone();
clone.active.set(true);
dataset.removeComponentFromEntity(entity, Camera);
dataset.addComponentToEntity(entity, clone);
});
// Set music tracks back to time=0
resetSoundEmitterTracks(dataset);
const serializer = new BinaryBufferSerializer();
serializer.engine = self.engine;
serializer.registry = engine.binarySerializationRegistry;
const state = new EncodingBinaryBuffer();
state.endianness = EndianType.BigEndian;
try {
serializer.process(state, dataset);
} catch (e) {
//failed to serialize game state
console.error("Failed to serialize game state", e);
}
state.trim();
downloadAsFile(state.data, "level.bin");
}
}
/**
*
* @param keyCode
* @param {KeyboardEvent} event
*/
function processSingleKey(keyCode, event) {
if (keyCode === KeyCodes.delete || keyCode === KeyCodes.x) {
event.preventDefault();
event.stopPropagation();
self.actions.mark('delete selected entities');
const removeActions = self.selection.map(function (entity) {
return new EntityRemoveAction(entity);
});
self.actions.do(new SelectionClearAction());
removeActions.forEach(function (a) {
self.actions.do(a);
});
} else if (keyCode === KeyCodes.q) {
let activeTool = self.toolEngine.active.getValue();
if (activeTool === null || activeTool.name !== "marquee_selection") {
//activate selection tool
self.toolEngine.activate("marquee_selection");
} else {
//activate transform tool
self.toolEngine.activate("spatial_transform");
}
} else if (keyCode === KeyCodes.g) {
//activate camera tool
self.toolEngine.activate("camera_control");
} else if (keyCode === KeyCodes.d && event.shiftKey) {
if (!self.selection.isEmpty()) {
//copy
copySelectedEntities(self);
//paste
pasteEntities(self);
}
} else if (keyCode === KeyCodes.w) {
self.toolEngine.activate("spatial_transform");
self.toolEngine.active.getValue().mode.set(TransformerMode.Translation);
} else if (keyCode === KeyCodes.e) {
self.toolEngine.activate("spatial_transform");
self.toolEngine.active.getValue().mode.set(TransformerMode.Rotation);
} else if (keyCode === KeyCodes.r) {
self.toolEngine.activate("spatial_transform");
self.toolEngine.active.getValue().mode.set(TransformerMode.Scale);
}
}
function isViewFocused(view) {
function checkChild(el) {
if (el === view.el) {
return true;
} else {
const children = el.children;
const numChildren = children.length;
for (let i = 0; i < numChildren; i++) {
const child = children[i];
if (checkChild(child)) {
return true;
}
}
}
return false;
}
return checkChild(document.activeElement);
}
/**
*
* @param {KeyboardEvent} event
*/
function handleKeyDownEvent(event) {
//check that game view has focus
if (!isViewFocused(self.engine.gameView)) {
return;
}
const keyCode = event.keyCode;
if (event.ctrlKey) {
processCtrlCombinations(keyCode, event);
} else {
processSingleKey(keyCode, event)
}
const activeTool = self.toolEngine.active.getValue();
if (activeTool !== null && activeTool !== undefined) {
//pass event to active tool
activeTool.handleKeyboardEvent(event);
}
}
this.handlers = {
keyDown: handleKeyDownEvent
};
}
Editor.prototype.initialize = function () {
this.toolEngine.initialize();
this.processEngine.initialize(this);
this.view = new EditorView(this);
this.copyBuffer = [];
this.disabledSystems = [];
this.lastTool = "marquee_selection";
this.cleanupTasks = [];
};
/**
* Attempt to focus camera on the entity
* @param {int} entity
*/
Editor.prototype.focusEntity = function (entity) {
const em = this.engine.entityManager;
const ecd = em.dataset;
//try focus camera on it
const transform = ecd.getComponent(entity, ecd.getComponentClassByName("Transform"));
if (transform !== null) {
const TopDownCameraController = ecd.getComponentClassByName("TopDownCameraController");
const topDownCameraControllerSystem = em.getSystem(TopDownCameraControllerSystem);
ecd.traverseComponents(TopDownCameraController, function (camera) {
const target = camera.target;
target.set(transform.position.x, target.y, transform.position.z);
});
let originalValue = topDownCameraControllerSystem.enabled.get();
if (!originalValue) {
topDownCameraControllerSystem.enabled.set(true);
}
topDownCameraControllerSystem.update(0);
topDownCameraControllerSystem.enabled.set(originalValue);
}
};
/**
*
* @param {Engine} engine
*/
Editor.prototype.attach = async function (engine) {
assert.defined(engine, 'engine');
assert.ok(engine.isEngine, 'engine.isEngine');
this.engine = engine;
//validate selection
const missingEntities = this.selection.filter(function (entity) {
return !engine.entityManager.dataset.entityExists(entity);
});
//drop missing entities from selection
this.selection.removeAll(missingEntities);
//find interaction system and disable it
this.disableSystem(TopDownCameraControllerSystem);
this.disableSystem(InputControllerSystem);
//attach EditorEntity system
await engine.entityManager.addSystem(this.editorEntitySystem);
const dataset = engine.entityManager.dataset;
const cameraEntity = this.cameraEntity;
activateEditorCamera(this.cameraEntity, dataset, this);
cameraEntity.build(dataset);
this.toolEngine.startup(engine, this);
this.toolEngine.activate(this.lastTool);
this.processEngine.startup();
try {
this.selectionVistualizer.startup();
} catch (e) {
console.error("Failed to start selection visualizer:", e);
}
//attach view
engine.viewStack.push(this.view, "Editor");
//map keys
window.addEventListener('keydown', this.handlers.keyDown);
};
/**
*
* @param {Class} systemClass
*/
Editor.prototype.disableSystem = function (systemClass) {
const entityManager = this.engine.entityManager;
const system = entityManager.getSystem(systemClass);
if (system === null) {
console.log('System not found', systemClass);
return;
}
const originalState = system.enabled.get();
system.enabled.set(false);
this.disabledSystems.push({
name: name,
originalState: originalState,
system: system
});
};
Editor.prototype.restoreDisableSystem = function () {
this.disabledSystems.forEach(function (disabledSystem) {
disabledSystem.system.enabled.set(disabledSystem.originalState);
});
this.disabledSystems = [];
};
Editor.prototype.detach = function () {
this.selectionVistualizer.shutdown();
this.lastTool = this.toolEngine.active.get().name;
this.toolEngine.shutdown();
this.processEngine.shutdown();
//pop own view from the engine's view stack restoring the original state
this.engine.viewStack.pop();
//unmap keys
window.removeEventListener('keydown', this.handlers.keyDown);
//enable interactions system if it was disabled
this.restoreDisableSystem();
//remove all editor entities
const dataset = this.engine.entityManager.dataset;
dataset.traverseComponents(EditorEntity, function (component, entity) {
dataset.removeEntity(entity);
});
//remove system
this.engine.entityManager.removeSystem(this.editorEntitySystem);
this.cleanupTasks.forEach(t => t());
this.cleanupTasks = [];
};
/**
*
* @param {int} entity
* @returns {boolean}
*/
Editor.prototype.isEditorEntity = function (entity) {
return isEditorOwnedEntity(entity, this.engine.entityManager.dataset);
};
/**
*
* @param {int} entity
* @param {EntityComponentDataset} dataset
*/
function isEditorOwnedEntity(entity, dataset) {
return dataset.getComponent(entity, EditorEntity) !== undefined;
}
export default Editor;