From 9e86192bb05e04f159ec9f04f917c818a6e4351b Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Mon, 7 Sep 2020 15:38:10 -0400 Subject: [PATCH] added audio and sprite loading --- src/Loop/GameLoop.ts | 4 + src/MainScene.ts | 26 +++++- src/Nodes/Sprites/Sprite.ts | 34 ++++++++ src/Player.ts | 6 ++ src/ResourceManager/ResourceManager.ts | 103 ++++++++++++++++++++--- src/Scene/Factories/AudioFactory.ts | 21 +++++ src/Scene/Factories/CanvasNodeFactory.ts | 6 +- src/Scene/Factories/FactoryManager.ts | 10 ++- src/Sound/Audio.ts | 26 ++++++ src/Sound/AudioManager.ts | 50 +++++++++++ tsconfig.json | 5 +- 11 files changed, 271 insertions(+), 20 deletions(-) create mode 100644 src/Nodes/Sprites/Sprite.ts create mode 100644 src/Scene/Factories/AudioFactory.ts create mode 100644 src/Sound/Audio.ts create mode 100644 src/Sound/AudioManager.ts diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 7db6974..2c1a6a5 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -6,6 +6,7 @@ import Debug from "../Debug/Debug"; import ResourceManager from "../ResourceManager/ResourceManager"; import Viewport from "../SceneGraph/Viewport"; import SceneManager from "../Scene/SceneManager"; +import AudioManager from "../Sound/AudioManager"; export default class GameLoop{ // The amount of time to spend on a physics step @@ -39,6 +40,7 @@ export default class GameLoop{ private recorder: Recorder; private resourceManager: ResourceManager; private sceneManager: SceneManager; + private audioManager: AudioManager; constructor(){ this.maxFPS = 60; @@ -68,6 +70,7 @@ export default class GameLoop{ this.recorder = new Recorder(); this.resourceManager = ResourceManager.getInstance(); this.sceneManager = new SceneManager(this.viewport, this); + this.audioManager = AudioManager.getInstance(); } private initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D { @@ -150,6 +153,7 @@ export default class GameLoop{ this.inputReceiver.update(deltaT); this.recorder.update(deltaT); this.sceneManager.update(deltaT); + this.resourceManager.update(deltaT); } render(): void { diff --git a/src/MainScene.ts b/src/MainScene.ts index 06d318d..0373fed 100644 --- a/src/MainScene.ts +++ b/src/MainScene.ts @@ -13,6 +13,23 @@ export default class MainScene extends Scene { loadScene(){ this.load.tilemap("platformer", "assets/tilemaps/Platformer.json"); this.load.tilemap("background", "assets/tilemaps/Background.json"); + this.load.image("player", "assets/sprites/player.png"); + this.load.audio("player_jump", "assets/sounds/jump-3.wav"); + this.load.audio("level_music", "assets/sounds/level.wav"); + + let loadingScreen = this.addLayer(); + let box = this.add.graphic(Rect, loadingScreen, new Vec2(200, 300), new Vec2(400, 60)); + box.setColor(new Color(0, 0, 0)); + let bar = this.add.graphic(Rect, loadingScreen, new Vec2(205, 305), new Vec2(0, 50)); + bar.setColor(new Color(0, 200, 200)); + + this.load.onLoadProgress = (percentProgress: number) => { + bar.setSize(295 * percentProgress, bar.getSize().y); + } + + this.load.onLoadComplete = () => { + loadingScreen.disable(); + } } startScene(){ @@ -22,6 +39,9 @@ export default class MainScene extends Scene { backgroundTilemap.getLayer().setParallax(0.5, 0.8); backgroundTilemap.getLayer().setAlpha(0.5); + // Add the music and start playing it on a loop + this.add.audio("level_music").play(true); + // Add the tilemap this.add.tilemap("platformer", OrthogonalTilemap); @@ -30,10 +50,12 @@ export default class MainScene extends Scene { // Add a player let player = this.add.physics(Player, mainLayer, "platformer"); - let playerSprite = this.add.graphic(Rect, mainLayer, new Vec2(0, 0), new Vec2(50, 50)); - playerSprite.setColor(new Color(255, 0, 0)); + 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); diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts new file mode 100644 index 0000000..02cc481 --- /dev/null +++ b/src/Nodes/Sprites/Sprite.ts @@ -0,0 +1,34 @@ +import CanvasNode from "../CanvasNode"; +import ResourceManager from "../../ResourceManager/ResourceManager"; +import Vec2 from "../../DataTypes/Vec2"; + +export default class Sprite extends CanvasNode { + private imageId: string; + private scale: Vec2; + + constructor(imageId: string){ + super(); + this.imageId = imageId; + let image = ResourceManager.getInstance().getImage(this.imageId); + this.size = new Vec2(image.width, image.height); + this.scale = new Vec2(1, 1); + } + + getScale(): Vec2 { + return this.scale; + } + + setScale(scale: Vec2): void { + this.scale = scale; + } + + update(deltaT: number): void {} + + render(ctx: CanvasRenderingContext2D): void { + let image = ResourceManager.getInstance().getImage(this.imageId); + let origin = this.getViewportOriginWithParallax(); + ctx.drawImage(image, + 0, 0, this.size.x, this.size.y, + this.position.x - origin.x, this.position.y - origin.y, this.size.x * this.scale.x, this.size.y * this.scale.y); + } +} \ No newline at end of file diff --git a/src/Player.ts b/src/Player.ts index 912f5c4..3fb828c 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -3,6 +3,7 @@ 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; @@ -11,6 +12,7 @@ export default class Player extends PhysicsNode { size: Vec2; gravity: number = 7000; type: string; + jumpSound: Audio; constructor(type: string){ super(); @@ -81,6 +83,10 @@ export default class Player extends PhysicsNode { let vel = new Vec2(0, this.velocity.y); if(this.grounded){ + if(dir.y === -1){ + // Jumping + this.jumpSound.play(); + } vel.y = dir.y*1800; } diff --git a/src/ResourceManager/ResourceManager.ts b/src/ResourceManager/ResourceManager.ts index d82e865..58336cd 100644 --- a/src/ResourceManager/ResourceManager.ts +++ b/src/ResourceManager/ResourceManager.ts @@ -3,11 +3,16 @@ import Tilemap from "../Nodes/Tilemap"; import Queue from "../DataTypes/Queue"; import { TiledTilemapData } from "../DataTypes/Tilesets/TiledData"; import StringUtils from "../Utils/StringUtils"; +import AudioManager from "../Sound/AudioManager"; export default class ResourceManager { private static instance: ResourceManager; private loading: boolean; + private justLoaded: boolean; + + public onLoadProgress: Function; + public onLoadComplete: Function; private imagesLoaded: number; private imagesToLoad: number; @@ -19,8 +24,17 @@ export default class ResourceManager { private tilemapLoadingQueue: Queue<{key: string, path: string}>; private tilemaps: Map; + private audioLoaded: number; + private audioToLoad: number; + private audioLoadingQueue: Queue<{key: string, path: string}>; + private audioBuffers: Map; + + // The number of different types of things to load + private typesToLoad: number; + private constructor(){ this.loading = false; + this.justLoaded = false; this.imagesLoaded = 0; this.imagesToLoad = 0; @@ -31,6 +45,11 @@ export default class ResourceManager { this.tilemapsToLoad = 0; this.tilemapLoadingQueue = new Queue(); this.tilemaps = new Map(); + + this.audioLoaded = 0; + this.audioToLoad = 0; + this.audioLoadingQueue = new Queue(); + this.audioBuffers = new Map(); }; static getInstance(): ResourceManager { @@ -45,7 +64,7 @@ export default class ResourceManager { this.imageLoadingQueue.enqueue({key: key, path: path}); } - public getImage(key: string){ + public getImage(key: string): HTMLImageElement{ return this.images.get(key); } @@ -54,28 +73,36 @@ export default class ResourceManager { } public audio(key: string, path: string): void { - + this.audioLoadingQueue.enqueue({key: key, path: path}); + } + + public getAudio(key: string): AudioBuffer { + return this.audioBuffers.get(key); } - // This one is trickier than the others because we first have to load the json file, then we have to load the images public tilemap(key: string, path: string): void { - // Add a function that loads the tilemap to the queue this.tilemapLoadingQueue.enqueue({key: key, path: path}); } - public getTilemap(key: string): TiledTilemapData{ + public getTilemap(key: string): TiledTilemapData { return this.tilemaps.get(key); } + // TODO - Should everything be loaded in order, one file at a time? loadResourcesFromQueue(callback: Function): void { + this.typesToLoad = 3; + this.loading = true; // Load everything in the queues. Tilemaps have to come before images because they will add new images to the queue this.loadTilemapsFromQueue(() => { this.loadImagesFromQueue(() => { - // Done loading - this.loading = false; - callback(); + this.loadAudioFromQueue(() => { + // Done loading + this.loading = false; + this.justLoaded = true; + callback(); + }); }); }); @@ -121,7 +148,7 @@ export default class ResourceManager { private loadImagesFromQueue(onFinishLoading: Function): void { this.imagesToLoad = this.imageLoadingQueue.getSize(); - this.tilemapsLoaded = 0; + this.imagesLoaded = 0; while(this.imageLoadingQueue.hasItems()){ let image = this.imageLoadingQueue.dequeue(); @@ -148,7 +175,47 @@ export default class ResourceManager { this.imagesLoaded += 1; if(this.imagesLoaded === this.imagesToLoad ){ - // We're done loading tilemaps + // We're done loading images + callback(); + } + } + + private loadAudioFromQueue(onFinishLoading: Function){ + this.audioToLoad = this.audioLoadingQueue.getSize(); + this.audioLoaded = 0; + + while(this.audioLoadingQueue.hasItems()){ + let audio = this.audioLoadingQueue.dequeue(); + this.loadAudio(audio.key, audio.path, onFinishLoading); + } + } + + private loadAudio(key: string, path: string, callbackIfLast: Function): void { + let audioCtx = AudioManager.getInstance().getAudioContext(); + + let request = new XMLHttpRequest(); + request.open('GET', path, true); + request.responseType = 'arraybuffer'; + + request.onload = () => { + audioCtx.decodeAudioData(request.response, (buffer) => { + // Add to list of audio buffers + this.audioBuffers.add(key, buffer); + + // Finish loading sound + this.finishLoadingAudio(callbackIfLast); + }, (error) =>{ + throw "Error loading sound"; + }); + } + request.send(); + } + + private finishLoadingAudio(callback: Function): void { + this.audioLoaded += 1; + + if(this.audioLoaded === this.audioToLoad){ + // We're done loading audio callback(); } } @@ -164,4 +231,20 @@ export default class ResourceManager { }; xobj.send(null); } + + private getLoadPercent(): number { + return (this.tilemapsLoaded/this.tilemapsToLoad + + this.imagesLoaded/this.imagesToLoad + + this.audioLoaded/this.audioToLoad) + / this.typesToLoad; + } + + public update(deltaT: number): void { + if(this.loading){ + this.onLoadProgress(this.getLoadPercent()); + } else if(this.justLoaded){ + this.justLoaded = false; + this.onLoadComplete(); + } + } } \ No newline at end of file diff --git a/src/Scene/Factories/AudioFactory.ts b/src/Scene/Factories/AudioFactory.ts new file mode 100644 index 0000000..a14226b --- /dev/null +++ b/src/Scene/Factories/AudioFactory.ts @@ -0,0 +1,21 @@ +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(); + } + + addAudio = (key: string, ...args: any): Audio => { + let audio = new Audio(key); + return audio; + } +} \ No newline at end of file diff --git a/src/Scene/Factories/CanvasNodeFactory.ts b/src/Scene/Factories/CanvasNodeFactory.ts index add97e5..bc5c8db 100644 --- a/src/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Scene/Factories/CanvasNodeFactory.ts @@ -1,9 +1,9 @@ import Scene from "../Scene"; -import CanvasItem from "../../Nodes/CanvasNode" import SceneGraph from "../../SceneGraph/SceneGraph"; import UIElement from "../../Nodes/UIElement"; import Layer from "../Layer"; import Graphic from "../../Nodes/Graphic"; +import Sprite from "../../Nodes/Sprites/Sprite"; export default class CanvasNodeFactory { private scene: Scene; @@ -27,8 +27,8 @@ export default class CanvasNodeFactory { return instance; } - addSprite = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { - let instance = new constr(...args); + addSprite = (imageId: string, layer: Layer, ...args: any): Sprite => { + let instance = new Sprite(imageId); // Add instance to scene instance.setScene(this.scene); diff --git a/src/Scene/Factories/FactoryManager.ts b/src/Scene/Factories/FactoryManager.ts index 8b420aa..39dea85 100644 --- a/src/Scene/Factories/FactoryManager.ts +++ b/src/Scene/Factories/FactoryManager.ts @@ -2,20 +2,23 @@ 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"; export default class FactoryManager { - private canvasNodeFactory: CanvasNodeFactory = new CanvasNodeFactory();; - private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory();; - private tilemapFactory: TilemapFactory = new TilemapFactory();; + 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); } uiElement = this.canvasNodeFactory.addUIElement; @@ -23,4 +26,5 @@ 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/Sound/Audio.ts b/src/Sound/Audio.ts new file mode 100644 index 0000000..34a7730 --- /dev/null +++ b/src/Sound/Audio.ts @@ -0,0 +1,26 @@ +import AudioManager from "./AudioManager"; + +export default class Audio { + private key: string; + private sound: AudioBufferSourceNode; + + constructor(key: string){ + this.key = key; + } + + play(loop?: boolean){ + this.sound = AudioManager.getInstance().createSound(this.key); + + if(loop){ + this.sound.loop = true; + } + + this.sound.start(); + } + + stop(){ + if(this.sound){ + this.sound.stop(); + } + } +} \ No newline at end of file diff --git a/src/Sound/AudioManager.ts b/src/Sound/AudioManager.ts new file mode 100644 index 0000000..0fe51ff --- /dev/null +++ b/src/Sound/AudioManager.ts @@ -0,0 +1,50 @@ +import ResourceManager from "../ResourceManager/ResourceManager"; + +export default class AudioManager { + private static instance: AudioManager; + + private audioCtx: AudioContext; + + private constructor(){ + this.initAudio(); + } + + public static getInstance(): AudioManager { + if(!this.instance){ + this.instance = new AudioManager(); + } + return this.instance; + } + + private initAudio(): void { + try { + window.AudioContext = window.AudioContext;// || window.webkitAudioContext; + this.audioCtx = new AudioContext(); + console.log('Web Audio API successfully loaded'); + } catch(e) { + console.log('Web Audio API is not supported in this browser'); + } + } + + public getAudioContext(): AudioContext { + return this.audioCtx; + } + + createSound(key: string): AudioBufferSourceNode { + // Get audio buffer + let buffer = ResourceManager.getInstance().getAudio(key); + + // creates a sound source + var source = this.audioCtx.createBufferSource(); + + // tell the source which sound to play + source.buffer = buffer; + + // connect the source to the context's destination + // i.e. the speakers + source.connect(this.audioCtx.destination); + + return source; + } + +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 745a1c8..bcc77d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "files": [ "src/main.ts", "src/Player.ts", + "src/PlayerSprite.ts", "src/DataTypes/Tilesets/TiledData.ts", "src/DataTypes/Tilesets/Tileset.ts", @@ -33,9 +34,7 @@ "src/Nodes/UIElements/Button.ts", "src/Nodes/UIElements/Label.ts", "src/Nodes/CanvasNode.ts", - "src/ColoredCircle.ts", "src/Nodes/GameNode.ts", - "src/PlayerSprite.ts", "src/Nodes/Tilemap.ts", "src/Nodes/UIElement.ts", @@ -51,6 +50,8 @@ "src/SceneGraph/SceneGraph.ts", "src/SceneGraph/SceneGraphArray.ts", "src/SceneGraph/Viewport.ts", + + "src/Sound/AudioManager.ts", "src/Utils/Color.ts", "src/Utils/MathUtils.ts",