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