diff --git a/src/DataTypes/Map.ts b/src/DataTypes/Map.ts index eab373a..4059bde 100644 --- a/src/DataTypes/Map.ts +++ b/src/DataTypes/Map.ts @@ -55,6 +55,14 @@ export default class Map implements Collection { Object.keys(this.map).forEach(key => func(key)); } + /** + * Deletes an item associated with a key + * @param key The key at which to delete an item + */ + delete(key: string): void { + delete this.map[key]; + } + clear(): void { this.forEach(key => delete this.map[key]); } diff --git a/src/Events/Receiver.ts b/src/Events/Receiver.ts index 47ed8ce..4970f7c 100644 --- a/src/Events/Receiver.ts +++ b/src/Events/Receiver.ts @@ -1,4 +1,5 @@ import Queue from "../DataTypes/Queue"; +import EventQueue from "./EventQueue"; import GameEvent from "./GameEvent"; /** @@ -12,6 +13,14 @@ export default class Receiver{ this.MAX_SIZE = 100; this.q = new Queue(this.MAX_SIZE); } + + /** + * Adds these types of events to this receiver's queue every update. + * @param eventTypes The types of events this receiver will be subscribed to + */ + subscribe(eventTypes: string | Array): void { + EventQueue.getInstance().subscribe(this, eventTypes); + } /** * Adds an event to the queue of this reciever diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 28ecc22..e8931fb 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -195,6 +195,9 @@ export default class GameLoop{ // Update all scenes this.sceneManager.update(deltaT); + + // Update all sounds + this.audioManager.update(deltaT); // Load or unload any resources if needed this.resourceManager.update(deltaT); diff --git a/src/MainScene.ts b/src/MainScene.ts index f1b2cfe..db2f9c2 100644 --- a/src/MainScene.ts +++ b/src/MainScene.ts @@ -8,6 +8,7 @@ import UIElement from "./Nodes/UIElement"; import Button from "./Nodes/UIElements/Button"; import Layer from "./Scene/Layer"; import SecondScene from "./SecondScene"; +import GameEvent from "./Events/GameEvent"; export default class MainScene extends Scene { @@ -41,8 +42,7 @@ export default class MainScene extends Scene { backgroundTilemap.getLayer().setAlpha(0.5); // Add the music and start playing it on a loop - let music = this.add.audio("level_music"); - music.play(true); + this.emit("play_sound", {key: "level_music", loop: true, holdReference: true}); // Add the tilemap this.add.tilemap("platformer", OrthogonalTilemap); @@ -55,10 +55,6 @@ export default class MainScene extends Scene { let playerSprite = this.add.sprite("player", mainLayer) player.setSprite(playerSprite); - // TODO - Should sound playing be handled with events? - let playerJumpSound = this.add.audio("player_jump"); - player.jumpSound = playerJumpSound; - this.viewport.follow(player); // Initialize UI @@ -127,7 +123,7 @@ export default class MainScene extends Scene { switchButton.setText("Change Scene"); switchButton.setPosition(340, 190); switchButton.onClick = () => { - music.stop(); + this.emit("stop_sound", {key: "level_music"}); this.sceneManager.changeScene(SecondScene); } } diff --git a/src/Player.ts b/src/Player.ts index 3fb828c..e47a317 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -3,7 +3,6 @@ import Vec2 from "./DataTypes/Vec2"; import Debug from "./Debug/Debug"; import AABB from "./Physics/Colliders/AABB"; import CanvasNode from "./Nodes/CanvasNode"; -import Audio from "./Sound/Audio"; export default class Player extends PhysicsNode { velocity: Vec2; @@ -12,7 +11,6 @@ export default class Player extends PhysicsNode { size: Vec2; gravity: number = 7000; type: string; - jumpSound: Audio; constructor(type: string){ super(); @@ -85,7 +83,7 @@ export default class Player extends PhysicsNode { if(this.grounded){ if(dir.y === -1){ // Jumping - this.jumpSound.play(); + this.emit("play_sound", {key: "player_jump"}); } vel.y = dir.y*1800; } diff --git a/src/ResourceManager/ResourceManager.ts b/src/ResourceManager/ResourceManager.ts index 530ce2d..f5db6c1 100644 --- a/src/ResourceManager/ResourceManager.ts +++ b/src/ResourceManager/ResourceManager.ts @@ -21,15 +21,15 @@ export default class ResourceManager { /** * Number to keep track of how many images need to be loaded */ - private imagesLoaded: number; + private loadonly_imagesLoaded: number; /** * Number to keep track of how many images are loaded */ - private imagesToLoad: number; + private loadonly_imagesToLoad: number; /** * The queue of images we must load */ - private imageLoadingQueue: Queue<{key: string, path: string}>; + private loadonly_imageLoadingQueue: Queue<{key: string, path: string}>; /** * A map of the images that are currently loaded and (presumably) being used by the scene */ @@ -38,15 +38,15 @@ export default class ResourceManager { /** * Number to keep track of how many tilemaps need to be loaded */ - private tilemapsLoaded: number; + private loadonly_tilemapsLoaded: number; /** * Number to keep track of how many tilemaps are loaded */ - private tilemapsToLoad: number; + private loadonly_tilemapsToLoad: number; /** * The queue of tilemaps we must load */ - private tilemapLoadingQueue: Queue<{key: string, path: string}>; + private loadonly_tilemapLoadingQueue: Queue<{key: string, path: string}>; /** * A map of the tilemaps that are currently loaded and (presumably) being used by the scene */ @@ -55,15 +55,15 @@ export default class ResourceManager { /** * Number to keep track of how many sounds need to be loaded */ - private audioLoaded: number; + private loadonly_audioLoaded: number; /** * Number to keep track of how many sounds are loaded */ - private audioToLoad: number; + private loadonly_audioToLoad: number; /** * The queue of sounds we must load */ - private audioLoadingQueue: Queue<{key: string, path: string}>; + private loadonly_audioLoadingQueue: Queue<{key: string, path: string}>; /** * A map of the sounds that are currently loaded and (presumably) being used by the scene */ @@ -72,25 +72,25 @@ export default class ResourceManager { /** * The total number of "types" of things that need to be loaded (i.e. images and tilemaps) */ - private typesToLoad: number; + private loadonly_typesToLoad: number; private constructor(){ this.loading = false; this.justLoaded = false; - this.imagesLoaded = 0; - this.imagesToLoad = 0; - this.imageLoadingQueue = new Queue(); + this.loadonly_imagesLoaded = 0; + this.loadonly_imagesToLoad = 0; + this.loadonly_imageLoadingQueue = new Queue(); this.images = new Map(); - this.tilemapsLoaded = 0; - this.tilemapsToLoad = 0; - this.tilemapLoadingQueue = new Queue(); + this.loadonly_tilemapsLoaded = 0; + this.loadonly_tilemapsToLoad = 0; + this.loadonly_tilemapLoadingQueue = new Queue(); this.tilemaps = new Map(); - this.audioLoaded = 0; - this.audioToLoad = 0; - this.audioLoadingQueue = new Queue(); + this.loadonly_audioLoaded = 0; + this.loadonly_audioToLoad = 0; + this.loadonly_audioLoadingQueue = new Queue(); this.audioBuffers = new Map(); }; @@ -111,7 +111,7 @@ export default class ResourceManager { * @param path The path to the image to load */ public image(key: string, path: string): void { - this.imageLoadingQueue.enqueue({key: key, path: path}); + this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); } /** @@ -132,7 +132,7 @@ export default class ResourceManager { * @param path */ public audio(key: string, path: string): void { - this.audioLoadingQueue.enqueue({key: key, path: path}); + this.loadonly_audioLoadingQueue.enqueue({key: key, path: path}); } /** @@ -149,7 +149,7 @@ export default class ResourceManager { * @param path */ public tilemap(key: string, path: string): void { - this.tilemapLoadingQueue.enqueue({key: key, path: path}); + this.loadonly_tilemapLoadingQueue.enqueue({key: key, path: path}); } /** @@ -166,7 +166,7 @@ export default class ResourceManager { * @param callback */ loadResourcesFromQueue(callback: Function): void { - this.typesToLoad = 3; + this.loadonly_typesToLoad = 3; this.loading = true; @@ -191,16 +191,16 @@ export default class ResourceManager { this.loading = false; this.justLoaded = false; - this.imagesLoaded = 0; - this.imagesToLoad = 0; + this.loadonly_imagesLoaded = 0; + this.loadonly_imagesToLoad = 0; this.images.clear(); - this.tilemapsLoaded = 0; - this.tilemapsToLoad = 0; + this.loadonly_tilemapsLoaded = 0; + this.loadonly_tilemapsToLoad = 0; this.tilemaps.clear(); - this.audioLoaded = 0; - this.audioToLoad = 0; + this.loadonly_audioLoaded = 0; + this.loadonly_audioToLoad = 0; this.audioBuffers.clear(); } @@ -209,11 +209,11 @@ export default class ResourceManager { * @param onFinishLoading */ private loadTilemapsFromQueue(onFinishLoading: Function): void { - this.tilemapsToLoad = this.tilemapLoadingQueue.getSize(); - this.tilemapsLoaded = 0; + this.loadonly_tilemapsToLoad = this.loadonly_tilemapLoadingQueue.getSize(); + this.loadonly_tilemapsLoaded = 0; - while(this.tilemapLoadingQueue.hasItems()){ - let tilemap = this.tilemapLoadingQueue.dequeue(); + while(this.loadonly_tilemapLoadingQueue.hasItems()){ + let tilemap = this.loadonly_tilemapLoadingQueue.dequeue(); this.loadTilemap(tilemap.key, tilemap.path, onFinishLoading); } } @@ -235,7 +235,7 @@ export default class ResourceManager { for(let tileset of tilemapObject.tilesets){ let key = tileset.image; let path = StringUtils.getPathFromFilePath(pathToTilemapJSON) + key; - this.imageLoadingQueue.enqueue({key: key, path: path}); + this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); } // Finish loading @@ -248,9 +248,9 @@ export default class ResourceManager { * @param callback */ private finishLoadingTilemap(callback: Function): void { - this.tilemapsLoaded += 1; + this.loadonly_tilemapsLoaded += 1; - if(this.tilemapsLoaded === this.tilemapsToLoad){ + if(this.loadonly_tilemapsLoaded === this.loadonly_tilemapsToLoad){ // We're done loading tilemaps callback(); } @@ -261,11 +261,11 @@ export default class ResourceManager { * @param onFinishLoading */ private loadImagesFromQueue(onFinishLoading: Function): void { - this.imagesToLoad = this.imageLoadingQueue.getSize(); - this.imagesLoaded = 0; + this.loadonly_imagesToLoad = this.loadonly_imageLoadingQueue.getSize(); + this.loadonly_imagesLoaded = 0; - while(this.imageLoadingQueue.hasItems()){ - let image = this.imageLoadingQueue.dequeue(); + while(this.loadonly_imageLoadingQueue.hasItems()){ + let image = this.loadonly_imageLoadingQueue.dequeue(); this.loadImage(image.key, image.path, onFinishLoading); } } @@ -295,9 +295,9 @@ export default class ResourceManager { * @param callback */ private finishLoadingImage(callback: Function): void { - this.imagesLoaded += 1; + this.loadonly_imagesLoaded += 1; - if(this.imagesLoaded === this.imagesToLoad ){ + if(this.loadonly_imagesLoaded === this.loadonly_imagesToLoad ){ // We're done loading images callback(); } @@ -308,11 +308,11 @@ export default class ResourceManager { * @param onFinishLoading */ private loadAudioFromQueue(onFinishLoading: Function){ - this.audioToLoad = this.audioLoadingQueue.getSize(); - this.audioLoaded = 0; + this.loadonly_audioToLoad = this.loadonly_audioLoadingQueue.getSize(); + this.loadonly_audioLoaded = 0; - while(this.audioLoadingQueue.hasItems()){ - let audio = this.audioLoadingQueue.dequeue(); + while(this.loadonly_audioLoadingQueue.hasItems()){ + let audio = this.loadonly_audioLoadingQueue.dequeue(); this.loadAudio(audio.key, audio.path, onFinishLoading); } } @@ -349,9 +349,9 @@ export default class ResourceManager { * @param callback */ private finishLoadingAudio(callback: Function): void { - this.audioLoaded += 1; + this.loadonly_audioLoaded += 1; - if(this.audioLoaded === this.audioToLoad){ + if(this.loadonly_audioLoaded === this.loadonly_audioToLoad){ // We're done loading audio callback(); } @@ -370,10 +370,10 @@ export default class ResourceManager { } private getLoadPercent(): number { - return (this.tilemapsLoaded/this.tilemapsToLoad - + this.imagesLoaded/this.imagesToLoad - + this.audioLoaded/this.audioToLoad) - / this.typesToLoad; + return (this.loadonly_tilemapsLoaded/this.loadonly_tilemapsToLoad + + this.loadonly_imagesLoaded/this.loadonly_imagesToLoad + + this.loadonly_audioLoaded/this.loadonly_audioToLoad) + / this.loadonly_typesToLoad; } public update(deltaT: number): void { diff --git a/src/Scene/Factories/AudioFactory.ts b/src/Scene/Factories/AudioFactory.ts deleted file mode 100644 index ff39e9b..0000000 --- a/src/Scene/Factories/AudioFactory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ResourceManager from "../../ResourceManager/ResourceManager"; -import AudioManager from "../../Sound/AudioManager"; -import Scene from "../Scene"; -import Audio from "../../Sound/Audio"; - -export default class AudioFactory { - private scene: Scene; - private resourceManager: ResourceManager; - private audioManager: AudioManager; - - init(scene: Scene){ - this.scene = scene; - this.resourceManager = ResourceManager.getInstance(); - this.audioManager = AudioManager.getInstance(); - } - - /** - * Returns an audio element created using the previously loaded audio file specified by the key. - * @param key The key of the loaded audio file - */ - addAudio = (key: string): Audio => { - let audio = new Audio(key); - return audio; - } -} \ No newline at end of file diff --git a/src/Scene/Factories/FactoryManager.ts b/src/Scene/Factories/FactoryManager.ts index 7aa2228..1f42d2f 100644 --- a/src/Scene/Factories/FactoryManager.ts +++ b/src/Scene/Factories/FactoryManager.ts @@ -2,7 +2,6 @@ import Scene from "../Scene"; import PhysicsNodeFactory from "./PhysicsNodeFactory"; import CanvasNodeFactory from "./CanvasNodeFactory"; import TilemapFactory from "./TilemapFactory"; -import AudioFactory from "./AudioFactory"; import PhysicsManager from "../../Physics/PhysicsManager"; import SceneGraph from "../../SceneGraph/SceneGraph"; import Tilemap from "../../Nodes/Tilemap"; @@ -13,13 +12,11 @@ export default class FactoryManager { private canvasNodeFactory: CanvasNodeFactory = new CanvasNodeFactory(); private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory(); private tilemapFactory: TilemapFactory = new TilemapFactory(); - private audioFactory: AudioFactory = new AudioFactory(); constructor(scene: Scene, sceneGraph: SceneGraph, physicsManager: PhysicsManager, tilemaps: Array){ this.canvasNodeFactory.init(scene, sceneGraph); this.physicsNodeFactory.init(scene, physicsManager); this.tilemapFactory.init(scene, tilemaps, physicsManager); - this.audioFactory.init(scene); } // Expose all of the factories through the factory manager @@ -28,5 +25,4 @@ export default class FactoryManager { graphic = this.canvasNodeFactory.addGraphic; physics = this.physicsNodeFactory.add; tilemap = this.tilemapFactory.add; - audio = this.audioFactory.addAudio; } \ No newline at end of file diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index fcfca28..ec2f0f9 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -10,6 +10,9 @@ import Tilemap from "../Nodes/Tilemap"; import ResourceManager from "../ResourceManager/ResourceManager"; import GameLoop from "../Loop/GameLoop"; import SceneManager from "./SceneManager"; +import EventQueue from "../Events/EventQueue"; +import GameEvent from "../Events/GameEvent"; +import Map from "../DataTypes/Map"; export default class Scene{ protected layers: Stack; @@ -143,4 +146,14 @@ export default class Scene{ getViewport(): Viewport { return this.viewport; } + + /** + * Emit and event of type eventType with the data packet data + * @param eventType + * @param data + */ + emit(eventType: string, data: Map | Record = null): void { + let event = new GameEvent(eventType, data); + EventQueue.getInstance().addEvent(event); + } } \ No newline at end of file diff --git a/src/SecondScene.ts b/src/SecondScene.ts index 79e47b1..bb9f623 100644 --- a/src/SecondScene.ts +++ b/src/SecondScene.ts @@ -40,7 +40,7 @@ export default class SecondScene extends Scene { backgroundTilemap.getLayer().setAlpha(0.2); // Add the music and start playing it on a loop - this.add.audio("level_music").play(true); + this.emit("play_sound", {key: "level_music", loop: true, holdReference: true}); // Add the tilemap this.add.tilemap("level2", OrthogonalTilemap); @@ -53,10 +53,6 @@ export default class SecondScene extends Scene { let playerSprite = this.add.sprite("player", mainLayer); player.setSprite(playerSprite); - // TODO - Should sound playing be handled with events? - let playerJumpSound = this.add.audio("player_jump"); - player.jumpSound = playerJumpSound; - this.viewport.follow(player); // Initialize UI diff --git a/src/Sound/Audio.ts b/src/Sound/Audio.ts deleted file mode 100644 index 9e3cb2b..0000000 --- a/src/Sound/Audio.ts +++ /dev/null @@ -1,33 +0,0 @@ -import AudioManager from "./AudioManager"; - -export default class Audio { - private key: string; - private sound: AudioBufferSourceNode; - - constructor(key: string){ - this.key = key; - } - - /** - * Play the sound this audio represents - * @param loop A boolean for whether or not to loop the sound - */ - play(loop?: boolean): void { - this.sound = AudioManager.getInstance().createSound(this.key); - - if(loop){ - this.sound.loop = true; - } - - this.sound.start(); - } - - /** - * Stop the sound this audio represents - */ - stop(): void { - if(this.sound){ - this.sound.stop(); - } - } -} \ No newline at end of file diff --git a/src/Sound/AudioManager.ts b/src/Sound/AudioManager.ts index 0bd110a..4006f3a 100644 --- a/src/Sound/AudioManager.ts +++ b/src/Sound/AudioManager.ts @@ -1,12 +1,19 @@ +import Map from "../DataTypes/Map"; +import Receiver from "../Events/Receiver"; import ResourceManager from "../ResourceManager/ResourceManager"; export default class AudioManager { private static instance: AudioManager; + private receiver: Receiver; + private currentSounds: Map; private audioCtx: AudioContext; private constructor(){ this.initAudio(); + this.receiver = new Receiver(); + this.receiver.subscribe(["play_sound", "stop_sound"]); + this.currentSounds = new Map(); } /** @@ -43,7 +50,17 @@ export default class AudioManager { * Creates a new sound from the key of a loaded audio file * @param key The key of the loaded audio file to create a new sound for */ - createSound(key: string): AudioBufferSourceNode { + /* + According to the MDN, create a new sound for every call: + + An AudioBufferSourceNode can only be played once; after each call to start(), you have to create a new node + if you want to play the same sound again. Fortunately, these nodes are very inexpensive to create, and the + actual AudioBuffers can be reused for multiple plays of the sound. Indeed, you can use these nodes in a + "fire and forget" manner: create the node, call start() to begin playing the sound, and don't even bother to + hold a reference to it. It will automatically be garbage-collected at an appropriate time, which won't be + until sometime after the sound has finished playing. + */ + protected createSound(key: string): AudioBufferSourceNode { // Get audio buffer let buffer = ResourceManager.getInstance().getAudio(key); @@ -57,6 +74,60 @@ export default class AudioManager { source.connect(this.audioCtx.destination); return source; - } + } + + /** + * Play the sound specified by the key + * @param key The key of the sound to play + * @param loop A boolean for whether or not to loop the sound + * @param holdReference A boolean for whether or not we want to hold on to a reference of the audio node. This is good for playing music on a loop that will eventually need to be stopped. + */ + protected playSound(key: string, loop: boolean, holdReference: boolean): void { + let sound = this.createSound(key); + + if(loop){ + sound.loop = true; + } + + // Add a reference of the new sound to a map. This will allow us to stop a looping or long sound at a later time + if(holdReference){ + this.currentSounds.add(key, sound); + } + + sound.start(); + } + + /** + * Stop the sound specified by the key + */ + protected stopSound(key: string): void { + let sound = this.currentSounds.get(key); + if(sound){ + sound.stop(); + this.currentSounds.delete(key); + } + } + /** + * Updates the AudioManager + * @param deltaT + */ + update(deltaT: number): void { + // Play each audio clip requested + // TODO - Add logic to merge sounds if there are multiple of the same key + while(this.receiver.hasNextEvent()){ + let event = this.receiver.getNextEvent(); + if(event.type === "play_sound"){ + let soundKey = event.data.get("key"); + let loop = event.data.get("loop"); + let holdReference = event.data.get("holdReference"); + this.playSound(soundKey, loop, holdReference); + } + + if(event.type === "stop_sound"){ + let soundKey = event.data.get("key"); + this.stopSound(soundKey); + } + } + } } \ No newline at end of file