import { Renderer } from '../core/renderer'; import { EngineComponent } from '../types/engine-component'; import { ChangeEvent, EngineEvents, InstanceEvent, RemoveEvent, ReparentEvent, } from '../types/events'; import { EventEmitter } from '../utils/events'; import { Environment } from '../gameobjects/environment.object'; import { assetManager } from '../assets/manager'; import { WorldFile } from '../types/world-file'; import { World } from '../gameobjects/world.object'; import { instancableGameObjects } from '../gameobjects'; import { Object3D } from 'three'; import { GameObject, SerializedObject } from '../types/game-object'; import { environmentDefaults } from '../defaults/environment'; /** * Game level management component. This component provides the World. * * EnvironmentComponent must be initialized before this component. * @listens change Applies changes to objects * @listens remove Removes objects from scene * @listens reparent Reparents object */ export class LevelComponent extends EngineComponent { public name = LevelComponent.name; public world = new World(); private environment!: Environment; private cleanUpEvents?: Function; constructor( protected renderer: Renderer, protected events: EventEmitter ) { super(renderer, events); } initialize(): void { this.renderer.scene.add(this.world); this.environment = this.renderer.scene.getObjectByName( 'Environment' ) as Environment; this.cleanUpEvents = this.bindEvents(); } update(delta: number): void {} dispose(): void { this.cleanUpEvents?.call(this); } public getSceneTree() { return { world: this.world, environment: this.environment, }; } public createObject(object: string, setParent?: Object3D, skipJoin = false) { const parent = setParent || this.world; const ObjectType = instancableGameObjects[object]; if (!ObjectType) return; const newObject = new ObjectType(); parent.add(newObject); if (!skipJoin) this.events.emit('sceneJoin', newObject); return newObject; } public deserializeObject(root: SerializedObject, setParent?: Object3D) { const parent = setParent || this.world; if (root.objectType === 'World') { root.children.forEach((entry) => this.recursiveCreate(entry, parent)); return parent; } return this.recursiveCreate(root, parent); } public serializeLevelSave(name: string): WorldFile { const world = this.world.serialize(); const environment = this.environment.serialize(); const assets = assetManager.serialize(); return { name, world, environment, assets, }; } public async deserializeLevelSave(save: WorldFile) { // Reset the world this.events.emit('reset'); // Load all assets await assetManager.loadAll(save.assets); // Load environment this.environment.deserialize(save.environment); this.events.emit('setEnvironment', this.environment); // Load world this.deserializeObject(save.world); this.world.deserialize(save.world); this.events.emit('loadComplete'); } private recursiveCreate(entry: SerializedObject, setParent?: Object3D) { const parent = setParent || this.world; const newObject = this.createObject(entry.objectType, parent, true); newObject?.deserialize(entry); entry.children.forEach((child) => this.recursiveCreate(child, newObject)); return newObject; } private bindEvents() { const changeEvent = (event: ChangeEvent) => { if (event.applied || !event.object) return; const prop = (event.object as any)[event.property]; if (prop?.copy) prop.copy(event.value); else (event.object as any)[event.property] = event.value; }; const removeEvent = (event: RemoveEvent) => { if (event.applied || !event.object) return; if ((event.object as GameObject).virtual) return; if (Array.isArray(event.object)) { event.object.forEach((object) => object.removeFromParent()); return; } event.object.removeFromParent(); this.events.emit('sceneLeave', event.object); }; const reparentEvent = (event: ReparentEvent) => { if (event.applied || !event.object || !event.parent) return; if (Array.isArray(event.object)) { event.object.forEach((object) => { event.parent.attach(object); this.events.emit('change', { object: object, property: 'parent', value: event.parent, applied: true, }); }); return; } event.parent.attach(event.object); this.events.emit('change', { object: event.object, property: 'parent', value: event.parent, applied: true, }); }; const instanceEvent = (event: InstanceEvent) => { const parent = typeof event.parent === 'string' ? this.world.getObjectByProperty('uuid', event.parent) : event.parent; const object = this.createObject(event.type, parent, !!event.data); if (event.data && object) { object.deserialize(event.data); this.events.emit('sceneJoin', object); } }; const resetEvent = () => { this.world.clear(); assetManager.freeAll(); this.events.emit('setEnvironment', environmentDefaults); Object.assign(this.environment, environmentDefaults); }; this.events.addListener('change', changeEvent); this.events.addListener('remove', removeEvent); this.events.addListener('reparent', reparentEvent); this.events.addListener('instance', instanceEvent); this.events.addListener('reset', resetEvent); return () => { this.events.removeEventListener('change', changeEvent); this.events.removeEventListener('remove', removeEvent); this.events.removeEventListener('reparent', reparentEvent); this.events.removeEventListener('instance', instanceEvent); this.events.removeEventListener('reset', resetEvent); }; } }