UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

544 lines (444 loc) • 17.4 kB
import Stats from "three/examples/jsm/libs/stats.module.js"; import { Color } from "../core/color/Color.js"; import { noop } from "../core/function/noop.js"; import Vector2 from "../core/geom/Vector2.js"; import Vector3 from "../core/geom/Vector3.js"; import EmptyView from "../view/elements/EmptyView.js"; import { JsonAssetLoader } from "./asset/loaders/JsonAssetLoader.js"; import { SerializationMetadata } from "./ecs/components/SerializationMetadata.js"; import Tag from "./ecs/components/Tag.js"; import Entity from "./ecs/Entity.js"; import { TerrainLayer } from "./ecs/terrain/ecs/layers/TerrainLayer.js"; import Terrain from "./ecs/terrain/ecs/Terrain.js"; import TerrainSystem from "./ecs/terrain/ecs/TerrainSystem.js"; import { Transform } from "./ecs/transform/Transform.js"; import Engine from "./Engine.js"; import { EngineConfiguration } from "./EngineConfiguration.js"; import { makeOrbitalCameraController } from "./graphics/camera/makeOrbitalCameraController.js"; import { Camera } from "./graphics/ecs/camera/Camera.js"; import { CameraSystem } from "./graphics/ecs/camera/CameraSystem.js"; import TopDownCameraController from "./graphics/ecs/camera/topdown/TopDownCameraController.js"; import TopDownCameraControllerSystem from "./graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js"; import { Light } from "./graphics/ecs/light/Light.js"; import LightSystem from "./graphics/ecs/light/LightSystem.js"; import Water from "./graphics/ecs/water/Water.js"; import WaterSystem from "./graphics/ecs/water/WaterSystem.js"; import { MouseEvents } from "./input/devices/events/MouseEvents.js"; import KeyboardCameraController from "./input/ecs/controllers/KeyboardCameraController.js"; import InputControllerSystem from "./input/ecs/systems/InputControllerSystem.js"; import { ConsoleLoggerBackend } from "./logging/ConsoleLoggerBackend.js"; import { logger } from "./logging/GlobalLogger.js"; import { getURLHash } from "./platform/GetURLHash.js"; import { WebEnginePlatform } from "./platform/WebEnginePlatform.js"; import SoundListener from "./sound/ecs/SoundListener.js"; import SoundListenerSystem from "./sound/ecs/SoundListenerSystem.js"; /** * * @param {Engine} engine * @returns {Promise} */ function setLocale(engine) { const urlHash = getURLHash(); let locale; if (urlHash.lang !== undefined) { locale = urlHash.lang; } else { locale = "en-gb"; } return engine.localization.loadLocale(locale); } /** * Equal to ~ 5500 K light color * @type {Readonly<Color>} */ const DEFAULT_SUNLIGHT_COLOR = Object.freeze(new Color(1, 0.93, 0.87)); export class EngineHarness { constructor() { /** * * @type {Engine} */ this.engine = new Engine(new WebEnginePlatform()); window.engine = this.engine; logger.addBackend(ConsoleLoggerBackend.INSTANCE); /** * @readonly * @type {Promise|null} */ this.p = null; } /** * Quick initialization of the engine in a sensible default configuration * @param {(config:EngineConfiguration,engine:Engine)=>*} [configuration] * @return {Promise<Engine>} */ static async bootstrap({ configuration } = {}) { const harness = new EngineHarness(); return harness.initialize({ configuration }); } /** * * @param {Engine} engine */ static addFpsCounter(engine) { const stats = new Stats(); engine.graphics.on.postRender.add(stats.update, stats); const view = new EmptyView({ el: stats.domElement }); engine.viewStack.addChild(view); } /** * @param {Object} [params] * @param {(config:EngineConfiguration,engine:Engine)=>*} [params.configuration] * @param {boolean} [params.enable_localization] Whether or not to load localization data * @returns {Promise<Engine>} */ initialize({ configuration = noop, enable_localization = false, } = {}) { if (this.p !== null) { return this.p; } const engine = this.engine; const promise = new Promise(async function (resolve, reject) { const config = new EngineConfiguration(); configuration(config, engine); if (!config.hasLoader('json')) { config.addLoader('json', new JsonAssetLoader()); } await config.apply(engine); await engine.start(); if (enable_localization) { try { await setLocale(engine); } catch (e) { console.warn('Failed to load localization:', e); } } engine.sceneManager.create("test"); engine.sceneManager.set("test"); document.body.appendChild(engine.viewStack.el); engine.viewStack.link(); // set css document.body.style.margin = "0"; document.body.style.overflow = "hidden"; function handleInput() { window.removeEventListener(MouseEvents.Down, handleInput); engine.sound.resumeContext(); } window.addEventListener(MouseEvents.Down, handleInput); resolve(engine); }); this.p = promise; return promise; } /** * @param {Object} param * @param {Engine} param.engine * @param {EntityComponentDataset} [param.ecd] * @param {Vector3} [param.target] * @param {number} [param.distance] * @param {number} [param.pitch] * @param {number} [param.yaw] * @param {boolean} [param.autoClip] * @param {number} [param.distanceMin] * @param {number} [param.distanceMax] * @param {number} [param.fieldOfView] in degrees * @returns {Entity} */ static buildCamera( { engine, ecd = engine.entityManager.dataset, target = new Vector3(), distance = 10, pitch = 1.4, yaw = 0, autoClip = false, distanceMin = 0.01, distanceMax = 1000, fieldOfView = 45 } ) { const em = engine.entityManager; if (em.getSystem(TopDownCameraControllerSystem) === null) { em.addSystem(new TopDownCameraControllerSystem()); } if (em.getSystem(CameraSystem) === null) { em.addSystem(new CameraSystem(engine.graphics)) } if (em.getSystem(SoundListenerSystem) === null) { em.addSystem(new SoundListenerSystem(engine.sound.context)); } if (!ecd.isComponentTypeRegistered(Tag)) { ecd.registerComponentType(Tag); } const transform = new Transform(); const cameraController = new TopDownCameraController(); cameraController.pitch = pitch; cameraController.yaw = yaw; cameraController.distance = distance; cameraController.distanceMin = distanceMin; cameraController.distanceMax = distanceMax; cameraController.target.copy(target); const camera = new Camera(); camera.active.set(true); camera.autoClip = autoClip; camera.clip_far = distanceMax; camera.fov.set(fieldOfView); const entityBuilder = new Entity(); entityBuilder .add(transform) .add(cameraController) .add(camera) .add(new SoundListener()) .add(Tag.fromJSON(["Camera"])) .build(ecd); console.log("build camera", entityBuilder); return entityBuilder; } /** * @param {Object} params * @param {Engine} params.engine * @param {{x:number,y:number,z:number}} [params.focus] * @param [params.heightMap] * @param [params.heightRange] * @param {number} [params.pitch] * @param {number} [params.yaw] * @param {number} [params.distance] * @param {{x:number, y:number}} [params.terrainSize] * @param {number} [params.terrainResolution] * @param {boolean} [params.enableWater] * @param {boolean} [params.enableTerrain] * @param {boolean} [params.enableLights=true] * @param {boolean} [params.enableShadows=true] if lights are enabled, enable shadows for sun * @param {number} [params.cameraFieldOfView] * @param {number} [params.cameraFarDistance] * @param {boolean} [params.cameraController=true] * @param {boolean} [params.cameraAutoClip] * @param {number} [params.shadowmapResolution] * @param {boolean} [params.showFps] */ static async buildBasics({ engine, focus = new Vector3(10, 0, 10), heightMap, heightRange, pitch = 0.7, yaw = -1.2, distance = 10, terrainSize = new Vector2(10, 10), terrainResolution = 10, enableWater = true, enableTerrain = true, enableLights = true, enableShadows = true, cameraFieldOfView, cameraFarDistance, cameraController = true, cameraAutoClip = false, shadowmapResolution, showFps = true }) { if (showFps) { EngineHarness.addFpsCounter(engine); } if (enableLights) { await EngineHarness.buildLights({ engine: engine, shadowmapResolution, castShadow: enableShadows, }); } const camera = EngineHarness.buildCamera({ engine, target: focus, pitch, yaw, distance, fieldOfView: cameraFieldOfView, distanceMax: cameraFarDistance, autoClip: cameraAutoClip }); const cameraEntity = camera.id; if (enableTerrain) { await EngineHarness.buildTerrain({ engine, heightMap, heightRange, resolution: terrainResolution, size: terrainSize, enableWater }); } if (cameraController) { EngineHarness.buildOrbitalCameraController({ engine, cameraEntity: cameraEntity }); const ecd = engine.entityManager.dataset; const keyboardCameraController = new KeyboardCameraController({ component: ecd.getComponent(cameraEntity, TopDownCameraController), entity: cameraEntity }, ecd); keyboardCameraController.build(ecd); } } /** * @param {Object} param * @param {Engine} param.engine * @param {EntityComponentDataset} [param.ecd] * @param {number} [param.shadowmapResolution] * @param {boolean} [param.castShadow] * @param {Color} [param.sun] * @param {number} [param.sunIntensity] * @param {Vector3} [param.sunDirection] * @param {Color} [param.ambient] * @param {number} [param.ambientIntensity] */ static async buildLights({ engine, ecd = engine.entityManager.dataset, shadowmapResolution = 1024, castShadow = true, sun = DEFAULT_SUNLIGHT_COLOR, sunIntensity = 0.9, sunDirection = new Vector3(0.1, -1, 0.1), ambient = Color.white, ambientIntensity = 0.1 }) { const em = engine.entityManager; if (!em.hasSystem(LightSystem)) { await em.addSystem(new LightSystem(engine, { shadowResolution: shadowmapResolution })); } if (!ecd.isComponentTypeRegistered(Tag)) { ecd.registerComponentType(Tag); } const key = new Light(); key.type.set(Light.Type.DIRECTION); key.color.copy(sun); key.intensity.set(sunIntensity); key.castShadow.set(castShadow); const transform = new Transform(); transform.position.set(30, 70, 30); transform.rotation.lookRotation(sunDirection); new Entity() .add(key) .add(transform) .add(Tag.fromJSON(["Light", "Key"])) .build(ecd); const fill = new Light(); fill.type.set(Light.Type.AMBIENT); fill.color.copy(ambient); fill.intensity.set(ambientIntensity); new Entity() .add(fill) .add(new Transform()) .add(Tag.fromJSON(["Light", "Ambient"])) .build(ecd); } /** * @param {Object} param * @param {number} [param.cameraEntity] * @param {Engine} param.engine * @param {number} [param.sensitivity] * @param {EntityComponentDataset} [param.ecd] * @returns {Promise<Entity>} */ static async buildOrbitalCameraController({ cameraEntity, engine, ecd = engine.entityManager.dataset, sensitivity = 1 }) { if (cameraEntity === undefined) { cameraEntity = ecd.getAnyComponent(Camera).entity; } const em = engine.entityManager; if (em.getSystem(InputControllerSystem) === null) { await em.addSystem(new InputControllerSystem(engine.devices)); } if (!ecd.isComponentTypeRegistered(SerializationMetadata)) { ecd.registerComponentType(SerializationMetadata); } const domElement = engine.graphics.domElement; domElement.addEventListener("contextmenu", (event) => { event.preventDefault(); }); const eb = makeOrbitalCameraController({ camera_entity: cameraEntity, ecd, dom_element: domElement, sensitivity }); eb.add(Tag.fromJSON(['CameraController'])); eb.add(SerializationMetadata.Transient); eb.build(ecd); return eb; } /** * * @param {Engine} engine * @param {Vector2} [size] * @param {String} [diffuse0] * @param heightMap * @param {number} [resolution] * @param {number} [waterLevel] * @param {boolean} [enableWater] * @returns {Terrain} */ static async buildTerrain( { engine, size = new Vector2(10, 10), diffuse0 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=", resolution = 10, waterLevel = 0, enableWater = true, } ) { const em = engine.entityManager; if (em.getSystem(TerrainSystem) === null) { await em.addSystem(new TerrainSystem(engine.graphics, engine.assetManager)); } const terrain = new Terrain(); terrain.size.copy(size); terrain.resolution = resolution; terrain.gridScale = 2; terrain.layers.addLayer(TerrainLayer.from( diffuse0, 5, 5 )); terrain.splat.resize(1, 1, 1); terrain.splat.fillLayerWeights(0, 255); const eb = new Entity(); eb.add(new Transform()) eb.add(terrain); if (enableWater) { if (em.getSystem(WaterSystem) === null) { em.addSystem(new WaterSystem(engine.graphics)); } const water = new Water(); water.level.set(waterLevel); eb.add(water); } eb.build(engine.entityManager.dataset); return terrain; } } let singleton = null; /** * * @returns {EngineHarness} */ EngineHarness.getSingleton = function () { if (singleton === null) { singleton = new EngineHarness(); } return singleton; };