From b08561290861ae30d5bad97d46ef567a7d8d2bce Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Fri, 19 Mar 2021 15:44:11 -0400 Subject: [PATCH] added resource culling and resource saving in the ResourceManager --- src/Wolfie2D/AI/StateMachineAI.ts | 2 +- src/Wolfie2D/DataTypes/Interfaces/Physical.ts | 3 + src/Wolfie2D/DataTypes/Queue.ts | 4 +- src/Wolfie2D/DataTypes/Stack.ts | 2 +- src/Wolfie2D/Events/EventQueue.ts | 2 +- src/Wolfie2D/Events/Receiver.ts | 5 + src/Wolfie2D/Nodes/GameNode.ts | 17 +- src/Wolfie2D/Nodes/UIElement.ts | 1 - src/Wolfie2D/Physics/BasicPhysicsManager.ts | 2 +- src/Wolfie2D/Physics/PhysicsManager.ts | 4 + .../ResourceManager/ResourceManager.ts | 216 +++++++++++++++--- .../Scene/Factories/TilemapFactory.ts | 1 - src/Wolfie2D/Scene/Scene.ts | 20 +- src/Wolfie2D/Scene/SceneManager.ts | 47 +++- src/Wolfie2D/SceneGraph/Viewport.ts | 2 +- src/Wolfie2D/Sound/AudioManager.ts | 2 +- src/Wolfie2D/Utils/MemoryUtils.ts | 46 ++++ 17 files changed, 316 insertions(+), 60 deletions(-) create mode 100644 src/Wolfie2D/Utils/MemoryUtils.ts diff --git a/src/Wolfie2D/AI/StateMachineAI.ts b/src/Wolfie2D/AI/StateMachineAI.ts index ab59c70..b3bcd9f 100644 --- a/src/Wolfie2D/AI/StateMachineAI.ts +++ b/src/Wolfie2D/AI/StateMachineAI.ts @@ -1,6 +1,5 @@ import AI from "../DataTypes/Interfaces/AI"; import StateMachine from "../DataTypes/State/StateMachine"; -import GameEvent from "../Events/GameEvent"; import GameNode from "../Nodes/GameNode"; /** @@ -17,6 +16,7 @@ export default class StateMachineAI extends StateMachine implements AI { destroy(){ // Get rid of our reference to the owner delete this.owner; + this.receiver.destroy(); } // @implemented diff --git a/src/Wolfie2D/DataTypes/Interfaces/Physical.ts b/src/Wolfie2D/DataTypes/Interfaces/Physical.ts index 9b2d4f2..989a2f5 100644 --- a/src/Wolfie2D/DataTypes/Interfaces/Physical.ts +++ b/src/Wolfie2D/DataTypes/Interfaces/Physical.ts @@ -13,6 +13,9 @@ export default interface Physical { /** Represents whether the object is moving or not. */ moving: boolean; + /** Represent whether the object is frozen from moving or not. */ + frozen: boolean; + /** Represents whether the object is on the ground or not. */ onGround: boolean; diff --git a/src/Wolfie2D/DataTypes/Queue.ts b/src/Wolfie2D/DataTypes/Queue.ts index 89ec40e..ad47adb 100644 --- a/src/Wolfie2D/DataTypes/Queue.ts +++ b/src/Wolfie2D/DataTypes/Queue.ts @@ -37,7 +37,7 @@ export default class Queue implements Collection { */ enqueue(item: T): void{ if((this.tail + 1) % this.MAX_ELEMENTS === this.head){ - throw "Queue full - cannot add element" + throw new Error("Queue full - cannot add element"); } this.size += 1; @@ -51,7 +51,7 @@ export default class Queue implements Collection { */ dequeue(): T { if(this.head === this.tail){ - throw "Queue empty - cannot remove element" + throw new Error("Queue empty - cannot remove element"); } diff --git a/src/Wolfie2D/DataTypes/Stack.ts b/src/Wolfie2D/DataTypes/Stack.ts index 362f310..d45d5b5 100644 --- a/src/Wolfie2D/DataTypes/Stack.ts +++ b/src/Wolfie2D/DataTypes/Stack.ts @@ -5,7 +5,7 @@ import Collection from "./Collection"; */ export default class Stack implements Collection { /** The maximum number of elements in the Stack */ - private readonly MAX_ELEMENTS: number; + private MAX_ELEMENTS: number; /** The internal representation of the stack */ private stack: Array; diff --git a/src/Wolfie2D/Events/EventQueue.ts b/src/Wolfie2D/Events/EventQueue.ts index b7c066d..f6718dc 100644 --- a/src/Wolfie2D/Events/EventQueue.ts +++ b/src/Wolfie2D/Events/EventQueue.ts @@ -81,7 +81,7 @@ export default class EventQueue { unsubscribe(receiver: Receiver, ...events: Array): void { this.receivers.forEach(eventName => { // If keys were provided, only continue if this key is one of them - if(events !== undefined && events.indexOf(eventName) === -1) return; + if(events.length > 0 && events.indexOf(eventName) === -1) return; // Find the index of our receiver for this key let index = this.receivers.get(eventName).indexOf(receiver); diff --git a/src/Wolfie2D/Events/Receiver.ts b/src/Wolfie2D/Events/Receiver.ts index cce73b4..3b3f680 100644 --- a/src/Wolfie2D/Events/Receiver.ts +++ b/src/Wolfie2D/Events/Receiver.ts @@ -36,7 +36,12 @@ export default class Receiver { * @param event The event to receive */ receive(event: GameEvent): void { + try{ this.q.enqueue(event); + } catch(e){ + console.warn("Receiver overflow for event " + event.toString()); + throw e; + } } /** diff --git a/src/Wolfie2D/Nodes/GameNode.ts b/src/Wolfie2D/Nodes/GameNode.ts index 4e7b1b6..6ae3794 100644 --- a/src/Wolfie2D/Nodes/GameNode.ts +++ b/src/Wolfie2D/Nodes/GameNode.ts @@ -33,6 +33,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable /*---------- PHYSICAL ----------*/ hasPhysics: boolean = false; moving: boolean = false; + frozen: boolean = false; onGround: boolean = false; onWall: boolean = false; onCeiling: boolean = false; @@ -151,11 +152,13 @@ export default abstract class GameNode implements Positioned, Unique, Updateable * @param velocity The velocity with which to move the object. */ move(velocity: Vec2): void { + if(this.frozen) return; this.moving = true; this._velocity = velocity; }; moveOnPath(speed: number, path: NavigationPath): void { + if(this.frozen) return; this.path = path; let dir = path.getMoveDirection(this); this.moving = true; @@ -230,6 +233,9 @@ export default abstract class GameNode implements Positioned, Unique, Updateable /** Removes this object from the physics system */ removePhysics(): void { + // Remove this from the physics manager + this.scene.getPhysicsManager().deregisterObject(this); + // Nullify all physics fields this.hasPhysics = false; this.moving = false; @@ -250,9 +256,16 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.collisionShape = null; this.colliderOffset = Vec2.ZERO; this.sweptRect = null; + } - // Remove this from the physics manager - this.scene.getPhysicsManager().deregisterObject(this); + /** Disables physics movement for this node */ + freeze(): void { + this.frozen = true; + } + + /** Reenables physics movement for this node */ + unfreeze(): void { + this.frozen = false; } /** Prevents this object from participating in all collisions and triggers. It can still move. */ diff --git a/src/Wolfie2D/Nodes/UIElement.ts b/src/Wolfie2D/Nodes/UIElement.ts index 02aec9a..f6cb36b 100644 --- a/src/Wolfie2D/Nodes/UIElement.ts +++ b/src/Wolfie2D/Nodes/UIElement.ts @@ -90,7 +90,6 @@ export default abstract class UIElement extends CanvasNode { } if(this.onClickEventId !== null){ let data = {}; - console.log("Click event: " + this.onClickEventId) this.emitter.fireEvent(this.onClickEventId, data); } } diff --git a/src/Wolfie2D/Physics/BasicPhysicsManager.ts b/src/Wolfie2D/Physics/BasicPhysicsManager.ts index f7ad5cf..81e74cf 100644 --- a/src/Wolfie2D/Physics/BasicPhysicsManager.ts +++ b/src/Wolfie2D/Physics/BasicPhysicsManager.ts @@ -72,7 +72,6 @@ export default class BasicPhysicsManager extends PhysicsManager { * @param options A record of options */ protected parseOptions(options: Record): void { - console.log("Parsing physics options: ", options); if(options.groupNames !== undefined && options.collisions !== undefined){ for(let i = 0; i < options.groupNames.length; i++){ let group = options.groupNames[i]; @@ -108,6 +107,7 @@ export default class BasicPhysicsManager extends PhysicsManager { // @override deregisterObject(node: Physical): void { + console.log("Deregistering physics object"); if(node.isStatic){ // Remove the node from the static list const index = this.staticNodes.indexOf(node); diff --git a/src/Wolfie2D/Physics/PhysicsManager.ts b/src/Wolfie2D/Physics/PhysicsManager.ts index 16eeeb5..b35bccd 100644 --- a/src/Wolfie2D/Physics/PhysicsManager.ts +++ b/src/Wolfie2D/Physics/PhysicsManager.ts @@ -33,6 +33,10 @@ export default abstract class PhysicsManager implements Updateable { this.groupNames = new Array(); } + destroy(): void { + this.receiver.destroy(); + } + /** * Registers a gamenode with this physics manager * @param object The object to register diff --git a/src/Wolfie2D/ResourceManager/ResourceManager.ts b/src/Wolfie2D/ResourceManager/ResourceManager.ts index 0fe454b..0581663 100644 --- a/src/Wolfie2D/ResourceManager/ResourceManager.ts +++ b/src/Wolfie2D/ResourceManager/ResourceManager.ts @@ -81,7 +81,6 @@ export default class ResourceManager { private loadonly_gl_ShaderProgramsToLoad: number; private loadonly_gl_ShaderLoadingQueue: Queue; - private gl_DefaultShaderPrograms: Map; private gl_ShaderPrograms: Map; private gl_Textures: Map; @@ -90,6 +89,13 @@ export default class ResourceManager { private gl: WebGLRenderingContext; + /* ########## UNLOADING AND EXCLUSION LIST ########## */ + /** A list of resources that will be unloaded at the end of the current scene */ + private resourcesToUnload: Array; + + /** A list of resources to keep until further notice */ + private resourcesToKeep: Array; + private constructor(){ this.loading = false; this.justLoaded = false; @@ -123,14 +129,17 @@ export default class ResourceManager { 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(); + + this.resourcesToUnload = new Array(); + this.resourcesToKeep = new Array(); }; + /* ######################################## SINGLETON ########################################*/ /** * Returns the current instance of this class or a new instance if none exist * @returns The resource manager @@ -143,6 +152,7 @@ export default class ResourceManager { return this.instance; } + /* ######################################## PUBLIC FUNCTION ########################################*/ /** * Activates or deactivates the use of WebGL * @param flag True if WebGL should be used, false otherwise @@ -165,6 +175,14 @@ export default class ResourceManager { this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); } + /** + * Tells the resource manager to keep this resource + * @param key The key of the resource + */ + public keepImage(key: string): void { + this.keepResource(key, ResourceType.IMAGE); + } + /** * Retrieves a loaded image * @param key The key of the loaded image @@ -187,6 +205,14 @@ export default class ResourceManager { this.loadonly_spritesheetLoadingQueue.enqueue({key: key, path: path}); } + /** + * Tells the resource manager to keep this resource + * @param key The key of the resource + */ + public keepSpritesheet(key: string): void { + this.keepResource(key, ResourceType.SPRITESHEET); + } + /** * Retrieves a loaded spritesheet * @param key The key of the spritesheet to load @@ -205,6 +231,14 @@ export default class ResourceManager { this.loadonly_audioLoadingQueue.enqueue({key: key, path: path}); } + /** + * Tells the resource manager to keep this resource + * @param key The key of the resource + */ + public keepAudio(key: string): void { + this.keepResource(key, ResourceType.AUDIO); + } + /** * Retrieves a loaded audio file * @param key The key of the audio file to load @@ -223,6 +257,14 @@ export default class ResourceManager { this.loadonly_tilemapLoadingQueue.enqueue({key: key, path: path}); } + /** + * Tells the resource manager to keep this resource + * @param key The key of the resource + */ + public keepTilemap(key: string): void { + this.keepResource(key, ResourceType.TILEMAP); + } + /** * Retreives a loaded tilemap * @param key The key of the loaded tilemap @@ -241,6 +283,14 @@ export default class ResourceManager { this.loadonly_jsonLoadingQueue.enqueue({key: key, path: path}); } + /** + * Tells the resource manager to keep this resource + * @param key The key of the resource + */ + public keepObject(key: string): void { + this.keepResource(key, ResourceType.JSON); + } + /** * Retreives a loaded object * @param key The key of the loaded object @@ -250,6 +300,7 @@ export default class ResourceManager { return this.jsonObjects.get(key); } + /* ######################################## LOAD FUNCTION ########################################*/ /** * Loads all resources currently in the queue * @param callback The function to cal when the resources are finished loading @@ -293,6 +344,21 @@ export default class ResourceManager { callback(); } + /* ######################################## UNLOAD FUNCTION ########################################*/ + + private keepResource(key: string, type: ResourceType): void { + console.log("Keep resource..."); + for(let i = 0; i < this.resourcesToUnload.length; i++){ + let resource = this.resourcesToUnload[i]; + if(resource.key === key && resource.resourceType === type){ + console.log("Found resource " + key + " of type " + type + ". Keeping."); + let resourceToMove = this.resourcesToUnload.splice(i, 1); + this.resourcesToKeep.push(...resourceToMove); + return; + } + } + } + /** * Deletes references to all resources in the resource manager */ @@ -300,29 +366,46 @@ export default class ResourceManager { 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(); + for(let resource of this.resourcesToUnload){ + // Unload the resource + this.unloadResource(resource); + } } + private unloadResource(resource: ResourceReference): void { + // Delete the resource itself + switch(resource.resourceType){ + case ResourceType.IMAGE: + this.images.delete(resource.key); + if(this.gl_WebGLActive){ + this.gl_Textures.delete(resource.key); + } + break; + case ResourceType.TILEMAP: + this.tilemaps.delete(resource.key); + break; + case ResourceType.SPRITESHEET: + this.spritesheets.delete(resource.key); + break; + case ResourceType.AUDIO: + this.audioBuffers.delete(resource.key); + break; + case ResourceType.JSON: + this.jsonObjects.delete(resource.key); + break; + case ResourceType.SHADER: + this.gl_ShaderPrograms.get(resource.key).delete(this.gl); + this.gl_ShaderPrograms.delete(resource.key); + break; + } + + // Delete any dependencies + for(let dependency of resource.dependencies){ + this.unloadResource(dependency); + } + } + + /* ######################################## WORK FUNCTIONS ########################################*/ /** * Loads all tilemaps currently in the tilemap loading queue * @param onFinishLoading The function to call when loading is complete @@ -355,22 +438,32 @@ export default class ResourceManager { // We can parse the object later - it's much faster than loading this.tilemaps.add(key, tilemapObject); + let resource = new ResourceReference(key, ResourceType.TILEMAP); // 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}); + this.loadonly_imageLoadingQueue.enqueue({key: key, path: path, isDependency: true}); + + // Add this image as a dependency to the tilemap + resource.addDependency(new ResourceReference(key, ResourceType.IMAGE)); } 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}); + this.loadonly_imageLoadingQueue.enqueue({key: key, path: path, isDependency: true}); + + // Add this image as a dependency to the tilemap + resource.addDependency(new ResourceReference(key, ResourceType.IMAGE)); } } } + // Add the resource reference to the list of resource to unload + this.resourcesToUnload.push(resource); + // Finish loading this.finishLoadingTilemap(callbackIfLast); }); @@ -422,9 +515,14 @@ export default class ResourceManager { // We can parse the object later - it's much faster than loading this.spritesheets.add(key, spritesheet); + let resource = new ResourceReference(key, ResourceType.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}); + this.loadonly_imageLoadingQueue.enqueue({key: spritesheet.name, path: path, isDependency: true}); + + resource.addDependency(new ResourceReference(spritesheet.name, ResourceType.IMAGE)); + this.resourcesToUnload.push(resource); // Finish loading this.finishLoadingSpritesheet(callbackIfLast); @@ -460,7 +558,7 @@ export default class ResourceManager { while(this.loadonly_imageLoadingQueue.hasItems()){ let image = this.loadonly_imageLoadingQueue.dequeue(); - this.loadImage(image.key, image.path, onFinishLoading); + this.loadImage(image.key, image.path, image.isDependency, onFinishLoading); } } @@ -470,13 +568,18 @@ export default class ResourceManager { * @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 { + public loadImage(key: string, path: string, isDependency: boolean, callbackIfLast: Function): void { var image = new Image(); image.onload = () => { // Add to loaded images this.images.add(key, image); + // If not a dependency, push it to the unload list. Otherwise it's managed by something else + if(!isDependency){ + this.resourcesToUnload.push(new ResourceReference(key, ResourceType.IMAGE)); + } + // If WebGL is active, create a texture if(this.gl_WebGLActive){ this.createWebGLTexture(key, image); @@ -539,6 +642,7 @@ export default class ResourceManager { audioCtx.decodeAudioData(request.response, (buffer) => { // Add to list of audio buffers this.audioBuffers.add(key, buffer); + this.resourcesToUnload.push(new ResourceReference(key, ResourceType.AUDIO)); // Finish loading sound this.finishLoadingAudio(callbackIfLast); @@ -592,6 +696,9 @@ export default class ResourceManager { this.loadTextFile(path, (fileText: string) => { let obj = JSON.parse(fileText); this.jsonObjects.add(key, obj); + + this.resourcesToUnload.push(new ResourceReference(key, ResourceType.JSON)); + this.finishLoadingObject(callbackIfLast); }); } @@ -706,6 +813,14 @@ export default class ResourceManager { this.loadonly_gl_ShaderLoadingQueue.enqueue(paths); } + /** + * Tells the resource manager to keep this resource + * @param key The key of the resource + */ + public keepShader(key: string): void { + this.keepResource(key, ResourceType.IMAGE); + } + private gl_LoadShadersFromQueue(onFinishLoading: Function): void { this.loadonly_gl_ShaderProgramsToLoad = this.loadonly_gl_ShaderLoadingQueue.getSize(); this.loadonly_gl_ShaderProgramsLoaded = 0; @@ -741,6 +856,8 @@ export default class ResourceManager { // Add to our map this.gl_ShaderPrograms.add(key, programWrapper); + this.resourcesToUnload.push(new ResourceReference(key, ResourceType.SHADER)); + // Finish loading this.gl_FinishLoadingShader(callbackIfLast); }); @@ -769,7 +886,7 @@ export default class ResourceManager { const program = this.gl.createProgram(); if(!program) { // Error creating - console.log("Failed to create program"); + console.warn("Failed to create program"); return null; } @@ -782,7 +899,7 @@ export default class ResourceManager { if(!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)){ // Error linking const error = this.gl.getProgramInfoLog(program); - console.log("Failed to link program: " + error); + console.warn("Failed to link program: " + error); // Clean up this.gl.deleteProgram(program); @@ -810,7 +927,7 @@ export default class ResourceManager { // If we couldn't create the shader, error if(shader === null){ - console.log("Unable to create shader"); + console.warn("Unable to create shader"); return null; } @@ -822,7 +939,7 @@ export default class ResourceManager { 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); + console.warn("Failed to compile shader: " + error); // Clean up this.gl.deleteShader(shader); @@ -871,9 +988,44 @@ export default class ResourceManager { } } +/** + * A class representing a reference to a resource. + * This is used for the exemption list to assure assets and their dependencies don't get + * destroyed if they are still needed. + */ +class ResourceReference { + key: string; + resourceType: ResourceType; + dependencies: Array; + + constructor(key: string, resourceType: ResourceType){ + this.key = key; + this.resourceType = resourceType; + this. dependencies = new Array(); + } + + addDependency(resource: ResourceReference): void { + this.dependencies.push(resource); + } +} + + +enum ResourceType { + IMAGE = "IMAGE", + TILEMAP = "TILEMAP", + SPRITESHEET = "SPRITESHEET", + AUDIO = "AUDIO", + JSON = "JSON", + SHADER = "SHADER" +} + +/** + * A pair representing a key and the path of the resource to load + */ class KeyPathPair { key: string; path: string; + isDependency?: boolean = false; } class KeyPath_Shader { diff --git a/src/Wolfie2D/Scene/Factories/TilemapFactory.ts b/src/Wolfie2D/Scene/Factories/TilemapFactory.ts index 79ce3cc..d41f7a1 100644 --- a/src/Wolfie2D/Scene/Factories/TilemapFactory.ts +++ b/src/Wolfie2D/Scene/Factories/TilemapFactory.ts @@ -90,7 +90,6 @@ export default class TilemapFactory { } if(isParallaxLayer){ - console.log("Adding parallax layer: " + layer.name) sceneLayer = this.scene.addParallaxLayer(layer.name, new Vec2(1, 1), depth); } else { sceneLayer = this.scene.addLayer(layer.name, depth); diff --git a/src/Wolfie2D/Scene/Scene.ts b/src/Wolfie2D/Scene/Scene.ts index 7a7801b..ee7f5a3 100644 --- a/src/Wolfie2D/Scene/Scene.ts +++ b/src/Wolfie2D/Scene/Scene.ts @@ -81,9 +81,12 @@ export default class Scene implements Updateable { /** An interface that allows the adding of different nodes to the scene */ public add: FactoryManager; - /** An interface that allows the loading of different files for use in the scene */ + /** An interface that allows the loading of different files for use in the scene. An alias for resourceManager */ public load: ResourceManager; + /** An interface that allows the loading and unloading of different files for use in the scene */ + public resourceManager: ResourceManager; + /** The configuration options for this scene */ public sceneOptions: SceneOptions; @@ -120,7 +123,8 @@ export default class Scene implements Updateable { this.add = new FactoryManager(this, this.tilemaps); - this.load = ResourceManager.getInstance(); + this.load = ResourceManager.getInstance() + this.resourceManager = this.load; // Get the timer manager and clear any existing timers TimerManager.getInstance().clearTimers(); @@ -132,9 +136,6 @@ export default class Scene implements Updateable { /** A lifecycle method that gets called when a new scene is created. Load all files you wish to access in the scene here. */ loadScene(): void {} - /** A lifecycle method that gets called on scene destruction. Specify which files you no longer need for garbage collection. */ - unloadScene(): void {} - /** A lifecycle method called strictly after loadScene(). Create any game objects you wish to use in the scene here. */ startScene(): void {} @@ -144,6 +145,9 @@ export default class Scene implements Updateable { */ updateScene(deltaT: number): void {} + /** A lifecycle method that gets called on scene destruction. Specify which files you no longer need for garbage collection. */ + unloadScene(): void {} + update(deltaT: number): void { this.updateScene(deltaT); @@ -232,6 +236,12 @@ export default class Scene implements Updateable { node.destroy(); } + for(let tilemap of this.tilemaps){ + tilemap.destroy(); + } + + this.receiver.destroy(); + delete this.sceneGraph; delete this.physicsManager; delete this.navManager; diff --git a/src/Wolfie2D/Scene/SceneManager.ts b/src/Wolfie2D/Scene/SceneManager.ts index a411c79..0809a30 100644 --- a/src/Wolfie2D/Scene/SceneManager.ts +++ b/src/Wolfie2D/Scene/SceneManager.ts @@ -2,6 +2,7 @@ import Scene from "./Scene"; import ResourceManager from "../ResourceManager/ResourceManager"; import Viewport from "../SceneGraph/Viewport"; import RenderingManager from "../Rendering/RenderingManager"; +import MemoryUtils from "../Utils/MemoryUtils"; /** * The SceneManager acts as an interface to create Scenes, and handles the lifecycle methods of Scenes. @@ -25,6 +26,7 @@ export default class SceneManager { /** For consistency, only change scenes at the beginning of the update cycle */ protected pendingScene: Scene; + protected pendingSceneInit: Record; /** * Creates a new SceneManager @@ -37,6 +39,7 @@ export default class SceneManager { this.viewport = viewport; this.renderingManager = renderingManager; this.idCounter = 0; + this.pendingScene = null; } /** @@ -46,31 +49,47 @@ export default class SceneManager { * @param init An object to pass to the init function of the new scene */ public changeToScene(constr: new (...args: any) => T, init?: Record, options?: Record): void { - this.viewport.setCenter(this.viewport.getHalfSize().x, this.viewport.getHalfSize().y); + console.log("Creating the new scene - change is pending until next update"); + this.pendingScene = new constr(this.viewport, this, this.renderingManager, options); + this.pendingSceneInit = init; + } - let scene = new constr(this.viewport, this, this.renderingManager, options); + protected doSceneChange(){ + console.log("Performing scene change"); + this.viewport.setCenter(this.viewport.getHalfSize().x, this.viewport.getHalfSize().y); if(this.currentScene){ - console.log("Destroying Old Scene"); + console.log("Unloading old scene") + this.currentScene.unloadScene(); + + console.log("Destroying old scene"); this.currentScene.destroy(); } - this.currentScene = scene; + console.log("Unloading old resources..."); + this.resourceManager.unloadAllResources(); - scene.initScene(init); + // Make the pending scene the current one + this.currentScene = this.pendingScene; + + // Make the pending scene null + this.pendingScene = null; + + // Init the scene + this.currentScene.initScene(this.pendingSceneInit); // Enqueue all scene asset loads - scene.loadScene(); + this.currentScene.loadScene(); // Load all assets console.log("Starting Scene Load"); this.resourceManager.loadResourcesFromQueue(() => { console.log("Starting Scene"); - scene.startScene(); - scene.setRunning(true); + this.currentScene.startScene(); + this.currentScene.setRunning(true); }); - this.renderingManager.setScene(scene); + this.renderingManager.setScene(this.currentScene); } /** @@ -85,7 +104,9 @@ export default class SceneManager { * Renders the current Scene */ public render(): void { - this.currentScene.render(); + if(this.currentScene){ + this.currentScene.render(); + } } /** @@ -93,7 +114,11 @@ export default class SceneManager { * @param deltaT The timestep of the Scene */ public update(deltaT: number){ - if(this.currentScene.isRunning()){ + if(this.pendingScene !== null){ + this.doSceneChange(); + } + + if(this.currentScene && this.currentScene.isRunning()){ this.currentScene.update(deltaT); } } diff --git a/src/Wolfie2D/SceneGraph/Viewport.ts b/src/Wolfie2D/SceneGraph/Viewport.ts index 6e85aee..d2eceba 100644 --- a/src/Wolfie2D/SceneGraph/Viewport.ts +++ b/src/Wolfie2D/SceneGraph/Viewport.ts @@ -155,7 +155,7 @@ export default class Viewport { * @param zoom The zoom level */ setZoomLevel(zoom: number): void { - this.view.halfSize.scale(1/zoom); + this.view.halfSize.copy(this.canvasSize.scaled(1/zoom/2)); } /** diff --git a/src/Wolfie2D/Sound/AudioManager.ts b/src/Wolfie2D/Sound/AudioManager.ts index c3af555..4319edd 100644 --- a/src/Wolfie2D/Sound/AudioManager.ts +++ b/src/Wolfie2D/Sound/AudioManager.ts @@ -46,7 +46,7 @@ export default class AudioManager { this.audioCtx = new AudioContext(); console.log('Web Audio API successfully loaded'); } catch(e) { - console.log('Web Audio API is not supported in this browser'); + console.warn('Web Audio API is not supported in this browser'); } } diff --git a/src/Wolfie2D/Utils/MemoryUtils.ts b/src/Wolfie2D/Utils/MemoryUtils.ts new file mode 100644 index 0000000..be5e011 --- /dev/null +++ b/src/Wolfie2D/Utils/MemoryUtils.ts @@ -0,0 +1,46 @@ +import Stack from "../DataTypes/Stack"; + +export default class MemoryUtils { + /** + * Returns an approximate size in bytes of any object + * @param obj The object to get the size of + * @returns An approximation of the number of bytes in the object provided + */ + static approxSizeOf(obj: any): number { + let objectList = new Array(); + let stack = new Stack(10000); + stack.push(obj); + let bytes = 0; + + while(!stack.isEmpty()){ + let item = stack.pop(); + + // We aren't interested in counting window and document + if(item === window || item === document) continue; + + if((typeof item) === "boolean"){ + bytes += 4; + } else if((typeof item) === "number"){ + bytes += 8; + } else if((typeof item) === "string"){ + bytes += item.length; + } else if((typeof item) === "object" && objectList.indexOf(item) === -1){ + // We haven't seen this object before - log it + objectList.push(item); + + // Get the number of bytes in all of its fields + for(let field in item){ + try { + stack.push(item[field]); + } catch(e){ + console.log("Pushing item: ", + item[field]); + console.warn(`Ran out of room counting memory (Stack has size ${stack.size()}) - returning current number of bytes`); + return bytes; + } + } + } + } + + return bytes; + } +} \ No newline at end of file