import Map from "../DataTypes/Map"; import Queue from "../DataTypes/Queue"; import { TiledTilemapData } from "../DataTypes/Tilesets/TiledData"; import StringUtils from "../Utils/StringUtils"; import AudioManager from "../Sound/AudioManager"; import Spritesheet from "../DataTypes/Spritesheet"; import WebGLProgramType from "../DataTypes/Rendering/WebGLProgramType"; /** * The resource manager for the game engine. * The resource manager interfaces with the loadable assets of a game such as images, data files, * and sounds, which are all found in the dist folder. * This class controls loading and updates the @reference[Scene] with the loading progress, so that the scene does * not start before all necessary assets are loaded. */ export default class ResourceManager { // Instance for the singleton class private static instance: ResourceManager; // Booleans to keep track of whether or not the ResourceManager is currently loading something /** Whether or not any resources are loading */ private loading: boolean; /** A boolean to indicate that the assets just finished loading */ private justLoaded: boolean; // Functions to do something when loading progresses or is completed such as render a loading screen /** A function that is called when loading progresses */ public onLoadProgress: Function; /** A function that is called when loading completes */ public onLoadComplete: Function; /** Number to keep track of how many images need to be loaded*/ private loadonly_imagesLoaded: number; /** Number to keep track of how many images are loaded */ private loadonly_imagesToLoad: number; /** The queue of images we must load */ private loadonly_imageLoadingQueue: Queue; /** A map of the images that are currently loaded and being used by the scene. The reference to these images only exist here for easy cleanup. */ private images: Map; /** Number to keep track of how many tilemaps need to be loaded */ private loadonly_spritesheetsLoaded: number; /** Number to keep track of how many tilemaps are loaded */ private loadonly_spritesheetsToLoad: number; /** The queue of tilemaps we must load */ private loadonly_spritesheetLoadingQueue: Queue; /** A map of the tilemaps that are currently loaded and (presumably) being used by the scene */ private spritesheets: Map; /** Number to keep track of how many tilemaps need to be loaded */ private loadonly_tilemapsLoaded: number; /** Number to keep track of how many tilemaps are loaded */ private loadonly_tilemapsToLoad: number; /** The queue of tilemaps we must load */ private loadonly_tilemapLoadingQueue: Queue; /** A map of the tilemaps that are currently loaded and (presumably) being used by the scene */ private tilemaps: Map; /** Number to keep track of how many sounds need to be loaded */ private loadonly_audioLoaded: number; /** Number to keep track of how many sounds are loaded */ private loadonly_audioToLoad: number; /** The queue of sounds we must load */ private loadonly_audioLoadingQueue: Queue; /** A map of the sounds that are currently loaded and (presumably) being used by the scene */ private audioBuffers: Map; /** The total number of "types" of things that need to be loaded (i.e. images and tilemaps) */ private loadonly_typesToLoad: number; /* ########## INFORMATION SPECIAL TO WEBGL ########## */ private gl_WebGLActive: boolean; private loadonly_gl_ShaderProgramsLoaded: number; private loadonly_gl_ShaderProgramsToLoad: number; private loadonly_gl_ShaderLoadingQueue: Queue; private gl_DefaultShaderPrograms: Map; private gl_ShaderPrograms: Map; private gl_Textures: Map; private gl_NextTextureID: number; private gl_Buffers: Map; private gl: WebGLRenderingContext; private constructor(){ this.loading = false; this.justLoaded = false; this.loadonly_imagesLoaded = 0; this.loadonly_imagesToLoad = 0; this.loadonly_imageLoadingQueue = new Queue(); this.images = new Map(); this.loadonly_spritesheetsLoaded = 0; this.loadonly_spritesheetsToLoad = 0; this.loadonly_spritesheetLoadingQueue = new Queue(); this.spritesheets = new Map(); this.loadonly_tilemapsLoaded = 0; this.loadonly_tilemapsToLoad = 0; this.loadonly_tilemapLoadingQueue = new Queue(); this.tilemaps = new Map(); this.loadonly_audioLoaded = 0; this.loadonly_audioToLoad = 0; this.loadonly_audioLoadingQueue = new Queue(); this.audioBuffers = new Map(); this.loadonly_gl_ShaderProgramsLoaded = 0; this.loadonly_gl_ShaderProgramsToLoad = 0; this.loadonly_gl_ShaderLoadingQueue = new Queue(); this.gl_DefaultShaderPrograms = new Map(); this.gl_ShaderPrograms = new Map(); this.gl_Textures = new Map(); this.gl_NextTextureID = 0; this.gl_Buffers = new Map(); }; /** * Returns the current instance of this class or a new instance if none exist * @returns The resource manager */ static getInstance(): ResourceManager { if(!this.instance){ this.instance = new ResourceManager(); } return this.instance; } /** * Activates or deactivates the use of WebGL * @param flag True if WebGL should be used, false otherwise * @param gl The instance of the graphics context, if applicable */ public useWebGL(flag: boolean, gl: WebGLRenderingContext): void { this.gl_WebGLActive = flag; if(this.gl_WebGLActive){ this.gl = gl; } } /** * Loads an image from file * @param key The key to associate the loaded image with * @param path The path to the image to load */ public image(key: string, path: string): void { this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); } /** * Retrieves a loaded image * @param key The key of the loaded image * @returns The image element associated with this key */ public getImage(key: string): HTMLImageElement { let image = this.images.get(key); if(image === undefined){ throw `There is no image associated with key "${key}"` } return image; } /** * Loads a spritesheet from file * @param key The key to associate the loaded spritesheet with * @param path The path to the spritesheet to load */ public spritesheet(key: string, path: string): void { this.loadonly_spritesheetLoadingQueue.enqueue({key: key, path: path}); } /** * Retrieves a loaded spritesheet * @param key The key of the spritesheet to load * @returns The loaded Spritesheet */ public getSpritesheet(key: string): Spritesheet { return this.spritesheets.get(key); } /** * Loads an audio file * @param key The key to associate with the loaded audio file * @param path The path to the audio file to load */ public audio(key: string, path: string): void { this.loadonly_audioLoadingQueue.enqueue({key: key, path: path}); } /** * Retrieves a loaded audio file * @param key The key of the audio file to load * @returns The AudioBuffer created from the loaded audio fle */ public getAudio(key: string): AudioBuffer { return this.audioBuffers.get(key); } /** * Load a tilemap from a json file. Automatically loads related images * @param key The key to associate with the loaded tilemap * @param path The path to the tilemap to load */ public tilemap(key: string, path: string): void { this.loadonly_tilemapLoadingQueue.enqueue({key: key, path: path}); } /** * Retreives a loaded tilemap * @param key The key of the loaded tilemap * @returns The tilemap data associated with the key */ public getTilemap(key: string): TiledTilemapData { return this.tilemaps.get(key); } /** * Loads all resources currently in the queue * @param callback The function to cal when the resources are finished loading */ loadResourcesFromQueue(callback: Function): void { this.loadonly_typesToLoad = 5; 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(() => { console.log("Loaded Tilemaps"); this.loadSpritesheetsFromQueue(() => { console.log("Loaded Spritesheets"); this.loadImagesFromQueue(() => { console.log("Loaded Images"); this.loadAudioFromQueue(() => { console.log("Loaded Audio"); if(this.gl_WebGLActive){ this.gl_LoadShadersFromQueue(() => { console.log("Loaded Shaders"); this.finishLoading(callback); }); } else { this.finishLoading(callback); } }); }); }); }); } private finishLoading(callback: Function): void { // Done loading this.loading = false; this.justLoaded = true; callback(); } /** * Deletes references to all resources in the resource manager */ unloadAllResources(): void { this.loading = false; this.justLoaded = false; this.loadonly_imagesLoaded = 0; this.loadonly_imagesToLoad = 0; this.images.clear(); this.loadonly_spritesheetsLoaded = 0; this.loadonly_spritesheetsToLoad = 0; this.spritesheets.clear(); this.loadonly_tilemapsLoaded = 0; this.loadonly_tilemapsToLoad = 0; this.tilemaps.clear(); this.loadonly_audioLoaded = 0; this.loadonly_audioToLoad = 0; this.audioBuffers.clear(); // WebGL // Delete all programs through webGL this.gl_ShaderPrograms.forEach(key => this.gl_ShaderPrograms.get(key).delete(this.gl)); this.gl_ShaderPrograms.clear(); this.gl_Textures.clear(); } /** * Loads all tilemaps currently in the tilemap loading queue * @param onFinishLoading The function to call when loading is complete */ private loadTilemapsFromQueue(onFinishLoading: Function): void { this.loadonly_tilemapsToLoad = this.loadonly_tilemapLoadingQueue.getSize(); this.loadonly_tilemapsLoaded = 0; // If no items to load, we're finished if(this.loadonly_tilemapsToLoad === 0){ onFinishLoading(); } while(this.loadonly_tilemapLoadingQueue.hasItems()){ let tilemap = this.loadonly_tilemapLoadingQueue.dequeue(); this.loadTilemap(tilemap.key, tilemap.path, onFinishLoading); } } /** * Loads a singular tilemap * @param key The key of the tilemap * @param pathToTilemapJSON The path to the tilemap JSON file * @param callbackIfLast The function to call if this is the last tilemap to load */ private loadTilemap(key: string, pathToTilemapJSON: string, callbackIfLast: Function): void { this.loadTextFile(pathToTilemapJSON, (fileText: string) => { let tilemapObject = JSON.parse(fileText); // We can parse the object later - it's much faster than loading this.tilemaps.add(key, tilemapObject); // Grab the tileset images we need to load and add them to the imageloading queue for(let tileset of tilemapObject.tilesets){ if(tileset.image){ let key = tileset.image; let path = StringUtils.getPathFromFilePath(pathToTilemapJSON) + key; this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); } else if(tileset.tiles){ for(let tile of tileset.tiles){ let key = tile.image; let path = StringUtils.getPathFromFilePath(pathToTilemapJSON) + key; this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); } } } // Finish loading this.finishLoadingTilemap(callbackIfLast); }); } /** * Finish loading a tilemap. Calls the callback function if this is the last tilemap being loaded * @param callback The function to call if this is the last tilemap to load */ private finishLoadingTilemap(callback: Function): void { this.loadonly_tilemapsLoaded += 1; if(this.loadonly_tilemapsLoaded === this.loadonly_tilemapsToLoad){ // We're done loading tilemaps callback(); } } /** * Loads all spritesheets currently in the spritesheet loading queue * @param onFinishLoading The function to call when the spritesheets are done loading */ private loadSpritesheetsFromQueue(onFinishLoading: Function): void { this.loadonly_spritesheetsToLoad = this.loadonly_spritesheetLoadingQueue.getSize(); this.loadonly_spritesheetsLoaded = 0; // If no items to load, we're finished if(this.loadonly_spritesheetsToLoad === 0){ onFinishLoading(); } while(this.loadonly_spritesheetLoadingQueue.hasItems()){ let spritesheet = this.loadonly_spritesheetLoadingQueue.dequeue(); this.loadSpritesheet(spritesheet.key, spritesheet.path, onFinishLoading); } } /** * Loads a singular spritesheet * @param key The key of the spritesheet to load * @param pathToSpritesheetJSON The path to the spritesheet JSON file * @param callbackIfLast The function to call if this is the last spritesheet */ private loadSpritesheet(key: string, pathToSpritesheetJSON: string, callbackIfLast: Function): void { this.loadTextFile(pathToSpritesheetJSON, (fileText: string) => { let spritesheet = JSON.parse(fileText); // We can parse the object later - it's much faster than loading this.spritesheets.add(key, spritesheet); // Grab the image we need to load and add it to the imageloading queue let path = StringUtils.getPathFromFilePath(pathToSpritesheetJSON) + spritesheet.spriteSheetImage; this.loadonly_imageLoadingQueue.enqueue({key: spritesheet.name, path: path}); // Finish loading this.finishLoadingSpritesheet(callbackIfLast); }); } /** * Finish loading a spritesheet. Calls the callback function if this is the last spritesheet being loaded * @param callback The function to call if this is the last spritesheet to load */ private finishLoadingSpritesheet(callback: Function): void { this.loadonly_spritesheetsLoaded += 1; if(this.loadonly_spritesheetsLoaded === this.loadonly_spritesheetsToLoad){ // We're done loading spritesheets callback(); } } /** * Loads all images currently in the image loading queue * @param onFinishLoading The function to call when there are no more images to load */ private loadImagesFromQueue(onFinishLoading: Function): void { this.loadonly_imagesToLoad = this.loadonly_imageLoadingQueue.getSize(); this.loadonly_imagesLoaded = 0; // If no items to load, we're finished if(this.loadonly_imagesToLoad === 0){ onFinishLoading(); } while(this.loadonly_imageLoadingQueue.hasItems()){ let image = this.loadonly_imageLoadingQueue.dequeue(); this.loadImage(image.key, image.path, onFinishLoading); } } /** * Loads a singular image * @param key The key of the image to load * @param path The path to the image to load * @param callbackIfLast The function to call if this is the last image */ public loadImage(key: string, path: string, callbackIfLast: Function): void { var image = new Image(); image.onload = () => { // Add to loaded images this.images.add(key, image); // If WebGL is active, create a texture if(this.gl_WebGLActive){ this.createWebGLTexture(key, image); } // Finish image load this.finishLoadingImage(callbackIfLast); } image.src = path; } /** * Finish loading an image. If this is the last image, it calls the callback function * @param callback The function to call if this is the last image */ private finishLoadingImage(callback: Function): void { this.loadonly_imagesLoaded += 1; if(this.loadonly_imagesLoaded === this.loadonly_imagesToLoad ){ // We're done loading images callback(); } } /** * Loads all audio currently in the tilemap loading queue * @param onFinishLoading The function to call when tilemaps are done loading */ private loadAudioFromQueue(onFinishLoading: Function){ this.loadonly_audioToLoad = this.loadonly_audioLoadingQueue.getSize(); this.loadonly_audioLoaded = 0; // If no items to load, we're finished if(this.loadonly_audioToLoad === 0){ onFinishLoading(); } while(this.loadonly_audioLoadingQueue.hasItems()){ let audio = this.loadonly_audioLoadingQueue.dequeue(); this.loadAudio(audio.key, audio.path, onFinishLoading); } } /** * Load a singular audio file * @param key The key to the audio file to load * @param path The path to the audio file to load * @param callbackIfLast The function to call if this is the last audio file to load */ 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(); } /** * Finish loading an audio file. Calls the callback functon if this is the last audio sample being loaded. * @param callback The function to call if this is the last audio file to load */ private finishLoadingAudio(callback: Function): void { this.loadonly_audioLoaded += 1; if(this.loadonly_audioLoaded === this.loadonly_audioToLoad){ // We're done loading audio callback(); } } /* ########## WEBGL SPECIFIC FUNCTIONS ########## */ public getTexture(key: string): number { return this.gl_Textures.get(key); } public getShaderProgram(key: string): WebGLProgram { return this.gl_ShaderPrograms.get(key).program; } public getBuffer(key: string): WebGLBuffer { return this.gl_Buffers.get(key); } private createWebGLTexture(imageKey: string, image: HTMLImageElement): void { // Get the texture ID const textureID = this.getTextureID(this.gl_NextTextureID); // Create the texture const texture = this.gl.createTexture(); // Set up the texture // Enable texture0 this.gl.activeTexture(textureID); // Bind our texture to texture 0 this.gl.bindTexture(this.gl.TEXTURE_2D, texture); // Set the texture parameters this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); // Set the texture image this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image); // Add the texture to our map with the same key as the image this.gl_Textures.add(imageKey, this.gl_NextTextureID); // Increment the key this.gl_NextTextureID += 1; } private getTextureID(id: number): number { // Start with 9 cases - this can be expanded if needed, but for the best performance, // Textures should be stitched into an atlas switch(id){ case 0: return this.gl.TEXTURE0; case 1: return this.gl.TEXTURE1; case 2: return this.gl.TEXTURE2; case 3: return this.gl.TEXTURE3; case 4: return this.gl.TEXTURE4; case 5: return this.gl.TEXTURE5; case 6: return this.gl.TEXTURE6; case 7: return this.gl.TEXTURE7; case 8: return this.gl.TEXTURE8; default: return this.gl.TEXTURE9; } } public createBuffer(key: string): void { if(this.gl_WebGLActive){ let buffer = this.gl.createBuffer(); this.gl_Buffers.add(key, buffer); } } /** * Enqueues loading of a new shader program * @param key The key of the shader program * @param vShaderFilepath * @param fShaderFilepath */ public shader(key: string, vShaderFilepath: string, fShaderFilepath: string): void { let splitPath = vShaderFilepath.split("."); let end = splitPath[splitPath.length - 1]; if(end !== "vshader"){ throw `${vShaderFilepath} is not a valid vertex shader - must end in ".vshader`; } splitPath = fShaderFilepath.split("."); end = splitPath[splitPath.length - 1]; if(end !== "fshader"){ throw `${fShaderFilepath} is not a valid vertex shader - must end in ".fshader`; } let paths = new KeyPath_Shader(); paths.key = key; paths.vpath = vShaderFilepath; paths.fpath = fShaderFilepath; this.loadonly_gl_ShaderLoadingQueue.enqueue(paths); } private gl_LoadShadersFromQueue(onFinishLoading: Function): void { this.loadonly_gl_ShaderProgramsToLoad = this.loadonly_gl_ShaderLoadingQueue.getSize(); this.loadonly_gl_ShaderProgramsLoaded = 0; // If webGL isn'active or there are no items to load, we're finished if(!this.gl_WebGLActive || this.loadonly_gl_ShaderProgramsToLoad === 0){ onFinishLoading(); } while(this.loadonly_gl_ShaderLoadingQueue.hasItems()){ let shader = this.loadonly_gl_ShaderLoadingQueue.dequeue(); this.gl_LoadShader(shader.key, shader.vpath, shader.fpath, onFinishLoading); } } private gl_LoadShader(key: string, vpath: string, fpath: string, callbackIfLast: Function): void { this.loadTextFile(vpath, (vFileText: string) => { const vShader = vFileText; this.loadTextFile(fpath, (fFileText: string) => { const fShader = fFileText // Extract the program and shaders const [shaderProgram, vertexShader, fragmentShader] = this.createShaderProgram(vShader, fShader); // Create a wrapper type const programWrapper = new WebGLProgramType(); programWrapper.program = shaderProgram; programWrapper.vertexShader = vertexShader; programWrapper.fragmentShader = fragmentShader; // Add to our map this.gl_ShaderPrograms.add(key, programWrapper); // Finish loading this.gl_FinishLoadingShader(callbackIfLast); }); }); } private gl_FinishLoadingShader(callback: Function): void { this.loadonly_gl_ShaderProgramsLoaded += 1; if(this.loadonly_gl_ShaderProgramsLoaded === this.loadonly_gl_ShaderProgramsToLoad){ // We're done loading shaders callback(); } } private createShaderProgram(vShaderSource: string, fShaderSource: string){ const vertexShader = this.loadVertexShader(vShaderSource); const fragmentShader = this.loadFragmentShader(fShaderSource); if(vertexShader === null || fragmentShader === null){ // We had a problem intializing - error return null; } // Create a shader program const program = this.gl.createProgram(); if(!program) { // Error creating console.log("Failed to create program"); return null; } // Attach our vertex and fragment shader this.gl.attachShader(program, vertexShader); this.gl.attachShader(program, fragmentShader); // Link this.gl.linkProgram(program); if(!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)){ // Error linking const error = this.gl.getProgramInfoLog(program); console.log("Failed to link program: " + error); // Clean up this.gl.deleteProgram(program); this.gl.deleteShader(vertexShader); this.gl.deleteShader(fragmentShader); return null; } // We successfully create a program return [program, vertexShader, fragmentShader]; } private loadVertexShader(shaderSource: string): WebGLShader{ // Create a new vertex shader return this.loadShader(this.gl.VERTEX_SHADER, shaderSource); } private loadFragmentShader(shaderSource: string): WebGLShader{ // Create a new fragment shader return this.loadShader(this.gl.FRAGMENT_SHADER, shaderSource); } private loadShader(type: number, shaderSource: string): WebGLShader{ const shader = this.gl.createShader(type); // If we couldn't create the shader, error if(shader === null){ console.log("Unable to create shader"); return null; } // Add the source to the shader and compile this.gl.shaderSource(shader, shaderSource); this.gl.compileShader(shader); // Make sure there were no errors during this process if(!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)){ // Not compiled - error const error = this.gl.getShaderInfoLog(shader); console.log("Failed to compile shader: " + error); // Clean up this.gl.deleteShader(shader); return null; } // Sucess, so return the shader return shader; } /* ########## GENERAL LOADING FUNCTIONS ########## */ private loadTextFile(textFilePath: string, callback: Function): void { let xobj: XMLHttpRequest = new XMLHttpRequest(); xobj.overrideMimeType("application/json"); xobj.open('GET', textFilePath, true); xobj.onreadystatechange = function () { if ((xobj.readyState == 4) && (xobj.status == 200)) { callback(xobj.responseText); } }; xobj.send(null); } /* ########## LOADING BAR INFO ########## */ private getLoadPercent(): number { return (this.loadonly_tilemapsLoaded/this.loadonly_tilemapsToLoad + this.loadonly_spritesheetsLoaded/this.loadonly_spritesheetsToLoad + this.loadonly_imagesLoaded/this.loadonly_imagesToLoad + this.loadonly_audioLoaded/this.loadonly_audioToLoad) / this.loadonly_typesToLoad; } update(deltaT: number): void { if(this.loading){ if(this.onLoadProgress){ this.onLoadProgress(this.getLoadPercent()); } } else if(this.justLoaded){ this.justLoaded = false; if(this.onLoadComplete){ this.onLoadComplete(); } } } } class KeyPathPair { key: string; path: string; } class KeyPath_Shader { key: string; vpath: string; fpath: string; }