three-game-engine
Version:
Simple light-weight game engine using three.js, three-mesh-ui and rapier
323 lines (322 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const THREE = require("three");
const GameObject_1 = require("./GameObject");
const PhysicsHelpers = require("./physics/PhysicsHelpers");
const ThreeJSHelpers_1 = require("./util/ThreeJSHelpers");
const SoundAsset_1 = require("./assets/SoundAsset");
class Scene {
constructor(game, jsonAssetPath) {
this.game = game;
this.jsonAssetPath = jsonAssetPath;
this.sceneJSONAsset = null;
this.name = 'unnamed-scene';
this.lights = [];
this.gameObjects = [];
this.threeJSScene = null;
this.active = false;
}
getGameObjectClass(type) {
return this.game.getGameObjectClass(type);
}
async load() {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
if (this.jsonAssetPath) {
this.sceneJSONAsset = await this.game.loadAsset(this.jsonAssetPath);
}
this.threeJSScene = new THREE.Scene();
this.threeJSScene.name = this.name;
this.threeJSScene.background = ((_b = (_a = this.sceneJSONAsset) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.background) || new THREE.Color('lightblue');
this.setFog(((_d = (_c = this.sceneJSONAsset) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.fog) || null);
this.setLights(((_f = (_e = this.sceneJSONAsset) === null || _e === void 0 ? void 0 : _e.data) === null || _f === void 0 ? void 0 : _f.lights) || []);
await this.loadSounds(((_h = (_g = this.sceneJSONAsset) === null || _g === void 0 ? void 0 : _g.data) === null || _h === void 0 ? void 0 : _h.sounds) || []);
await PhysicsHelpers.initRAPIER();
this.initialGravity = {
x: ((_l = (_k = (_j = this.sceneJSONAsset) === null || _j === void 0 ? void 0 : _j.data) === null || _k === void 0 ? void 0 : _k.gravity) === null || _l === void 0 ? void 0 : _l.x) || 0,
y: ((_p = (_o = (_m = this.sceneJSONAsset) === null || _m === void 0 ? void 0 : _m.data) === null || _o === void 0 ? void 0 : _o.gravity) === null || _p === void 0 ? void 0 : _p.y) || -9.8,
z: ((_s = (_r = (_q = this.sceneJSONAsset) === null || _q === void 0 ? void 0 : _q.data) === null || _r === void 0 ? void 0 : _r.gravity) === null || _s === void 0 ? void 0 : _s.z) || 0,
};
this.rapierWorld = PhysicsHelpers.createRapierWorld(this.initialGravity);
this.gameObjects = [];
(((_u = (_t = this.sceneJSONAsset) === null || _t === void 0 ? void 0 : _t.data) === null || _u === void 0 ? void 0 : _u.gameObjects) || []).forEach((g, index) => this._createGameObject(this, g, [index]));
for (let i = 0; i < this.gameObjects.length; i++) {
const gameObject = this.gameObjects[i];
await gameObject.load();
}
}
setFog(fog) {
if (!this.threeJSScene) {
throw new Error('Cant set/change fog, this scene has not finished loading, thus no .threeJSScene exists yet');
}
if (fog instanceof THREE.Fog) {
this.threeJSScene.fog = fog;
}
else if (typeof fog === 'object') {
const fogDefaults = {
color: '#00000',
near: 1.0,
far: 1000.0
};
const fogSettings = Object.assign({}, fogDefaults, fog);
this.threeJSScene.fog = new THREE.Fog(fogSettings.color, fogSettings.near, fogSettings.far);
}
else if (fog === null) {
this.threeJSScene.fog = null;
}
else {
throw new Error(`scene.setFog(): invalid value ${fog}`);
}
}
setLights(lights) {
const existingLights = this.threeJSScene.children.filter(child => child instanceof THREE.Light);
existingLights.forEach(existingLight => this.threeJSScene.remove(existingLight));
lights.forEach((lightData) => {
const light = (0, ThreeJSHelpers_1.createLight)(lightData);
this.threeJSScene.add(light);
});
}
async loadSounds(sounds) {
const existingSounds = this.threeJSScene.children.filter(child => child instanceof THREE.Audio);
existingSounds.forEach(sound => this.threeJSScene.remove(sound));
for (let i = 0; i < sounds.length; i++) {
const soundData = sounds[i];
const asset = await this.game.loadAsset(soundData.assetPath);
if (!(asset instanceof SoundAsset_1.default)) {
throw new Error(`Scene: asset found at ${soundData.assetPath} in AssetStore should be a SoundAsset`);
}
const audioBuffer = asset.getData();
const audioListener = this.game.renderer.getCameraAudioListener();
const name = soundData.name || `sound_${i}`;
const audio = (0, ThreeJSHelpers_1.createAudio)(soundData, audioBuffer, audioListener, name);
this.threeJSScene.add(audio);
}
}
playSound(soundName, delayInSec = 0, detune = null) {
const audio = this.threeJSScene.children.find(c => c.name === soundName && c instanceof THREE.Audio);
if (audio) {
if (audio.isPlaying) {
audio.pause(); // elsewise nothing will happen
}
audio.play(delayInSec);
if (detune !== null) {
audio.setDetune(detune); // set this here, rather than when creating the audio as setDetune can't be called till playback (where audio.source is set)
}
}
else {
throw new Error(`scene.playSound(): scene: ${this.name} has no sound with name: ${soundName}`);
}
}
_createGameObject(parent, gameObjectJSON, indices) {
const options = { ...gameObjectJSON };
delete options.children;
options.userData = {
indices
};
let gameObject = null;
if (gameObjectJSON.type) {
const type = gameObjectJSON.type;
if (!this.game.getGameObjectTypeJSON(type)) {
throw new Error(`Scene: error creating game object: unknown game object type: ${type}. You must define this type in your game.json file`);
}
const RegisteredGameObjectClass = this.getGameObjectClass(type);
if (RegisteredGameObjectClass) {
// @ts-ignore
gameObject = new RegisteredGameObjectClass(parent, options);
}
else {
gameObject = new GameObject_1.default(parent, options);
}
}
else {
gameObject = new GameObject_1.default(parent, options);
}
if (!(gameObject instanceof GameObject_1.default)) {
throw new Error(`Error: GameObject class must be a sub-class of GameObject. Invalid class registered for type ${gameObjectJSON.type}`);
}
parent.addGameObject(gameObject);
this.threeJSScene.add(gameObject.threeJSGroup);
(gameObjectJSON.children || []).forEach((childData, index) => {
this._createGameObject(gameObject, childData, indices.concat(index));
});
}
advancePhysics() {
this.rapierWorld.step();
this.forEachGameObject(gameObject => {
gameObject.syncWithRigidBody();
});
}
isActive() {
return this.active;
}
addGameObject(gameObject) {
if (!this.gameObjects.some(g => g === gameObject)) {
gameObject.parent = this;
this.gameObjects.push(gameObject);
this.threeJSScene.add(gameObject.threeJSGroup);
if (this.isActive()) {
gameObject.load().then(() => gameObject.afterLoaded()); // asynchronous
}
}
}
removeGameObject(gameObject) {
if (this.gameObjects.some(g => g === gameObject)) {
// gameObject is indeed a child of this scene
this.gameObjects = this.gameObjects.filter(g => g !== gameObject);
gameObject.parent = null;
this.threeJSScene.remove(gameObject.threeJSGroup);
}
}
getRapierWorld() {
return this.rapierWorld;
}
getRootGameObjects() {
return this.gameObjects;
}
forEachGameObject(fn) {
for (let i = 0; i < this.gameObjects.length; i++) {
const obj = this.gameObjects[i];
fn(obj);
obj.gameObjects.forEach(child => {
child.forEachGameObject(fn);
});
}
}
getGameObject(fn) {
for (let i = 0; i < this.gameObjects.length; i++) {
const obj = this.gameObjects[i];
if (fn(obj)) {
return obj;
}
const child = obj.getGameObject(fn);
if (child) {
return child;
}
}
return null;
}
getGameObjects(fn) {
let results = [];
for (let i = 0; i < this.gameObjects.length; i++) {
const obj = this.gameObjects[i];
if (fn(obj)) {
results.push(obj);
}
const childResults = obj.getGameObjects(fn);
results = results.concat(childResults);
}
return results;
}
getGameObjectWithName(name) {
return this.getGameObject(g => g.name === name);
}
getGameObjectsWithTag(tag) {
return this.getGameObjects(g => g.hasTag(tag));
}
getGameObjectWithID(id) {
return this.getGameObject(g => g.id === id);
}
getGameObjectIndices(gameObject) {
return this._getGameObjectIndices(gameObject, this, []);
}
_getGameObjectIndices(gameObject, parent, indices) {
const currentIndices = [...indices];
currentIndices.push(0);
for (let i = 0; i < parent.gameObjects.length; i++) {
currentIndices[currentIndices.length - 1] = i;
const currentGameObject = parent.gameObjects[i];
if (currentGameObject === gameObject) {
return currentIndices;
}
// Now check all the children of this game object (recursively)
const result = this._getGameObjectIndices(gameObject, currentGameObject, currentIndices);
if (result) {
return result;
}
}
return null;
}
getGameObjectByIndices(indices) {
let parent = this;
for (let i = 0; i < indices.length; i++) {
const index = indices[i];
if (i == indices.length - 1) {
return parent.gameObjects[index];
}
else {
parent = parent.gameObjects[index];
}
if (!parent) {
return null;
}
}
}
getGameObjectWithThreeJSObject(object3D) {
if (object3D instanceof THREE.Group) {
const { gameObjectID } = object3D.userData;
if (gameObjectID) {
return this.getGameObjectWithID(gameObjectID);
}
else {
return this.getGameObjectWithThreeJSObject(object3D.parent);
}
}
else if (object3D.parent) {
return this.getGameObjectWithThreeJSObject(object3D.parent);
}
else {
return null;
}
}
afterLoaded() {
// Optional: override and handle this event
}
beforeRender({ deltaTimeInSec }) {
// Optional: override and handle this event
}
// Called on the scene and all its GameObjects just before
// a new scene is loaded. Use this to do teardown operations.
beforeUnloaded() {
// Optional: override and handle this event
}
showGrid(size = 100, divisions = 100, colorCenterLine = new THREE.Color(0x444444), colorGrid = new THREE.Color(0x888888)) {
this.hideGrid();
const gridHelper = new THREE.GridHelper(size, divisions, colorCenterLine, colorGrid);
gridHelper.name = 'GridHelper';
this.threeJSScene.add(gridHelper);
}
hideGrid() {
const gridHelper = this.threeJSScene.getObjectByName('GridHelper');
if (gridHelper) {
this.threeJSScene.remove(gridHelper);
}
}
showPhysics() {
let physicsRenderingLines = this.threeJSScene.getObjectByName('PhysicsRenderingLines');
if (!physicsRenderingLines) {
let material = new THREE.LineBasicMaterial({
color: 0xffffff,
vertexColors: true
});
let geometry = new THREE.BufferGeometry();
physicsRenderingLines = new THREE.LineSegments(geometry, material);
physicsRenderingLines.name = 'PhysicsRenderingLines';
this.threeJSScene.add(physicsRenderingLines);
}
}
hidePhysics() {
const physicsRenderingLines = this.threeJSScene.getObjectByName('PhysicsRenderingLines');
if (physicsRenderingLines) {
this.threeJSScene.remove(physicsRenderingLines);
}
}
updatePhysicsGraphics() {
var _a;
const physicsRenderingLines = (_a = this.threeJSScene) === null || _a === void 0 ? void 0 : _a.getObjectByName('PhysicsRenderingLines');
if (physicsRenderingLines) {
const buffers = this.rapierWorld.debugRender();
physicsRenderingLines.geometry.setAttribute('position', new THREE.BufferAttribute(buffers.vertices, 3));
physicsRenderingLines.geometry.setAttribute('color', new THREE.BufferAttribute(buffers.colors, 4));
}
}
}
exports.default = Scene;