UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

391 lines (296 loc) • 10.6 kB
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js"; import { bvh_query_user_data_generic } from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_generic.js"; import { BVHQueryIntersectsSphere } from "../../../../core/bvh2/bvh3/query/BVHQueryIntersectsSphere.js"; import Vector3 from "../../../../core/geom/Vector3.js"; import { invokeObjectCompare } from "../../../../core/model/object/invokeObjectCompare.js"; import { GameAssetType } from "../../../asset/GameAssetType.js"; import { SoundAssetLoader } from "../../../asset/loaders/SoundAssetLoader.js"; import { System } from '../../../ecs/System.js'; import { Transform } from '../../../ecs/transform/Transform.js'; import { IncrementalDeltaSet } from "../../../graphics/render/visibility/IncrementalDeltaSet.js"; import SoundListener from "../SoundListener.js"; import { SoundEmitter } from './SoundEmitter.js'; import { SoundEmitterChannel } from "./SoundEmitterChannel.js"; import { SoundEmitterComponentContext } from "./SoundEmitterComponentContext.js"; import { SoundEmitterFlags } from "./SoundEmitterFlags.js"; import { SoundTrackFlags } from "./SoundTrackFlags.js"; /** * @readonly * @enum {string} */ export const SoundEmitterChannels = { Effects: 'effects', Music: 'music', Ambient: 'ambient' }; /** * * @type {number[]} */ const scratch_array = []; export class SoundEmitterSystem extends System { /** * * @param {AssetManager} assetManager * @param {AudioNode} destinationNode * @param {AudioContext} context * @constructor * @property {AssetManager} assetManager */ constructor(assetManager, destinationNode, context) { super(); this.dependencies = [SoundEmitter, Transform]; // this.destinationNode = destinationNode; /** * * @type {AudioContext} */ this.webAudioContext = context; this.assetManager = assetManager; /** * * @type {Object<SoundEmitterChannel>} */ this.channels = {}; this.addChannel(SoundEmitterChannels.Effects) .addChannel(SoundEmitterChannels.Music) .addChannel(SoundEmitterChannels.Ambient); this.channels[SoundEmitterChannels.Effects].volume = 1.2; this.channels[SoundEmitterChannels.Music].volume = 0.1; assetManager.registerLoader(GameAssetType.Sound, new SoundAssetLoader(context)); /** * * @type {SoundEmitterComponentContext[]} */ this.data = []; /** * * @type {IncrementalDeltaSet<SoundEmitterComponentContext>} */ this.activeSet = new IncrementalDeltaSet(invokeObjectCompare); /** * Spatial index * @type {BVH} * @private */ this.__bvh = new BVH(); /** * Number of currently linked entities * @type {number} * @private */ this.__linkedCount = 0; } async startup(entityManager) { this.activeSet.onAdded.add(this.handleContextActivation, this); this.activeSet.onRemoved.add(this.handleContextDeactivation, this); } async shutdown(entityManager) { this.activeSet.onAdded.remove(this.handleContextActivation, this); this.activeSet.onRemoved.remove(this.handleContextDeactivation, this); } /** * * @param {SoundEmitterComponentContext} ctx */ handleContextActivation(ctx) { ctx.connect(); } /** * * @param {SoundEmitterComponentContext} ctx */ handleContextDeactivation(ctx) { ctx.disconnect(); } /** * * @param {String} name * @returns {number} */ getChannelVolume(name) { return this.channels[name].volume; } /** * * @param {String} name * @param {number} value */ setChannelVolume(name, value) { this.channels[name].volume = value; } addChannel(name) { const channels = this.channels; if (!this.hasChannel(name)) { const channel = new SoundEmitterChannel(); channel.id = name; channel.sink = this.webAudioContext.createGain(); channel.sink.connect(this.destinationNode); channels[name] = channel; } else { console.error("Channel " + name + " already exists"); } return this; } /** * * @param {String} name * @returns {boolean} */ hasChannel(name) { return this.channels.hasOwnProperty(name); } /** * * @param {SoundEmitter} emitter * @param {Transform} transform * @param {number} entity */ link(emitter, transform, entity) { const context = this.webAudioContext; //what channel do we use? let channelName = emitter.channel; if (!this.hasChannel(channelName)) { console.error(`channel named '${channelName}' does not exist, defaulting to '${SoundEmitterChannels.Effects}'`); channelName = SoundEmitterChannels.Effects; } const channel = this.channels[channelName]; const targetNode = channel.sink; const nodes = emitter.nodes; if (nodes.volume === null) { emitter.buildNodes(context); } emitter.bvh.link(this.__bvh, entity); const ctx = new SoundEmitterComponentContext(); ctx.system = this; ctx.transform = transform; ctx.emitter = emitter; ctx.targetNode = targetNode; ctx.update(); ctx.link(); { // wire context if it's in hearing range const distance = ctx.transform.position.distanceTo(this.#cached_listener_position); if (distance <= ctx.emitter.distanceMax) { // add immediately this.activeSet.forceAdd(ctx); if (ctx.emitter.getFlag(SoundEmitterFlags.Attenuation)) { // is attenuated ctx.emitter.writeAttenuationVolume(distance); } ctx.connect(); } } this.data[entity] = ctx; this.__linkedCount++; } /** * * @param {SoundEmitter} emitter * @param {Transform} transform * @param {number} entity */ unlink(emitter, transform, entity) { //stop all tracks emitter.stopAllTracks(); const ctx = this.data[entity]; if (ctx !== undefined) { delete this.data[entity]; ctx.unlink(); } emitter.bvh.unlink(); this.__linkedCount--; } /** * * @param {SoundEmitterComponentContext} emitter * @returns {boolean} */ emitter_in_hearing_range(emitter) { } /** * Used to decide whether emitter is in hearing range when it is being added * @type {Vector3} */ #cached_listener_position = new Vector3(); update(timeDelta) { const entityManager = this.entityManager; const ecd = entityManager.dataset; if (ecd === null) { return; } const activeSet = this.activeSet; activeSet.initializeUpdate(); const soundListener = ecd.getAnyComponent(SoundListener); if (soundListener.entity !== -1) { const listenerTransform = ecd.getComponent(soundListener.entity, Transform); const listenerPosition = listenerTransform.position; this.#cached_listener_position.copy(listenerPosition); const query = BVHQueryIntersectsSphere.from([listenerPosition.x, listenerPosition.y, listenerPosition.z, 0]); const matchCount = bvh_query_user_data_generic( scratch_array, 0, this.__bvh, this.__bvh.root, query ); for (let i = 0; i < matchCount; i++) { const entity = scratch_array[i]; /** * @type {SoundEmitterComponentContext} */ const ctx = this.data[entity]; const emitter = ctx.emitter; if (emitter.tracks.isEmpty()) { // no tracks to play, don't render continue; } if (emitter.getFlag(SoundEmitterFlags.Attenuation)) { const distance = listenerPosition.distanceTo(ctx.transform.position); if (distance > emitter.distanceMax) { //emitter is too far away continue; } emitter.writeAttenuationVolume(distance); } activeSet.push(ctx); } } for (let entity in this.data) { /** * * @type {SoundEmitterComponentContext} */ const ctx = this.data[entity]; /** * * @type {SoundEmitter} */ const emitter = ctx.emitter; if (emitter === undefined) { console.error('Context.emitter is undefined. Context: ', ctx); } /** * * @type {List<SoundTrack>} */ const tracks = emitter.tracks; let trackCount = tracks.length; //update play time for (let i = 0; i < trackCount; i++) { const soundTrack = tracks.get(i); soundTrack.time += timeDelta; if ( soundTrack.setFlag(SoundTrackFlags.Suspended | SoundTrackFlags.Playing) && !soundTrack.getFlag(SoundTrackFlags.Loop) && soundTrack.time > soundTrack.duration ) { // track is suspended, but should have ended, trigger this state manually emitter.endTrack(soundTrack); // update cursors trackCount--; i--; } } } activeSet.finalizeUpdate(); } }