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