UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

795 lines (622 loc) • 26 kB
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;