made sounds play via EventQueue and flagged vars in ResourceManager

This commit is contained in:
Joe Weaver 2020-09-16 14:45:37 -04:00
parent 3a57a1acab
commit 90996115f1
12 changed files with 163 additions and 131 deletions

View File

@ -55,6 +55,14 @@ export default class Map<T> 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]);
}

View File

@ -1,4 +1,5 @@
import Queue from "../DataTypes/Queue";
import EventQueue from "./EventQueue";
import GameEvent from "./GameEvent";
/**
@ -13,6 +14,14 @@ export default class Receiver{
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<string>): void {
EventQueue.getInstance().subscribe(this, eventTypes);
}
/**
* Adds an event to the queue of this reciever
*/

View File

@ -196,6 +196,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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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<Tilemap>){
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;
}

View File

@ -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<Layer>;
@ -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<any> | Record<string, any> = null): void {
let event = new GameEvent(eventType, data);
EventQueue.getInstance().addEvent(event);
}
}

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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<AudioBufferSourceNode>;
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);
}
}
}
}