diff --git a/src/DataTypes/Spritesheet.ts b/src/DataTypes/Spritesheet.ts new file mode 100644 index 0000000..31b3798 --- /dev/null +++ b/src/DataTypes/Spritesheet.ts @@ -0,0 +1,11 @@ +import { AnimationData } from "../Rendering/Animations/AnimationTypes"; + +export default class Spritesheet { + name: string; + spriteSheetImage: string; + spriteWidth: number; + spriteHeight: number; + columns: number; + rows: number; + animations: Array; +} \ No newline at end of file diff --git a/src/Nodes/Sprites/AnimatedSprite.ts b/src/Nodes/Sprites/AnimatedSprite.ts new file mode 100644 index 0000000..7a20390 --- /dev/null +++ b/src/Nodes/Sprites/AnimatedSprite.ts @@ -0,0 +1,35 @@ +import Sprite from "./Sprite"; +import AnimationManager from "../../Rendering/Animations/AnimationManager"; +import Spritesheet from "../../DataTypes/Spritesheet"; +import Vec2 from "../../DataTypes/Vec2"; + +export default class AnimatedSprite extends Sprite { + /** The number of columns in this sprite sheet */ + protected numCols: number; + + /** The number of rows in this sprite sheet */ + protected numRows: number; + + /** The animationManager for this sprite */ + animation: AnimationManager; + + constructor(spritesheet: Spritesheet){ + super(spritesheet.name); + this.numCols = spritesheet.columns; + this.numRows = spritesheet.rows; + + // Set the size of the sprite to the sprite size specified by the spritesheet + this.size.set(spritesheet.spriteWidth, spritesheet.spriteHeight); + + this.animation = new AnimationManager(); + + // Add the animations to the animated sprite + for(let animation of spritesheet.animations){ + this.animation.add(animation.name, animation); + } + } + + getAnimationOffset(index: number): Vec2 { + return new Vec2((index % this.numCols) * this.size.x, Math.floor(index / this.numCols) * this.size.y); + } +} \ No newline at end of file diff --git a/src/Rendering/Animations/AnimationManager.ts b/src/Rendering/Animations/AnimationManager.ts new file mode 100644 index 0000000..48b9f83 --- /dev/null +++ b/src/Rendering/Animations/AnimationManager.ts @@ -0,0 +1,173 @@ +import Map from "../../DataTypes/Map"; +import Emitter from "../../Events/Emitter"; +import CanvasNode from "../../Nodes/CanvasNode"; +import { AnimationData, AnimationState } from "./AnimationTypes"; + +export default class AnimationManager { + /** The owner of this animation manager */ + protected owner: CanvasNode; + + /** The current animation state of this sprite */ + protected animationState: AnimationState; + + /** The name of the current animation of this sprite */ + protected currentAnimation: string; + + /** The current frame of this animation */ + protected currentFrame: number; + + /** The progress of the current animation through the current frame */ + protected frameProgress: number; + + /** Whether the current animation is looping or not */ + protected loop: boolean; + + /** The map of animations */ + protected animations: Map; + + /** The name of the event (if any) to send when the current animation stops playing. */ + protected onEndEvent: string; + + /** The event emitter for this animation manager */ + protected emitter: Emitter; + + /** A queued animation */ + protected pendingAnimation: string; + + /** The loop status of a pending animation */ + protected pendingLoop: boolean; + + /** The onEnd event of a pending animation */ + protected pendingOnEnd: string; + + constructor(){ + this.animationState = AnimationState.STOPPED; + this.currentAnimation = ""; + this.currentFrame = 0; + this.frameProgress = 0; + this.loop = false; + this.animations = new Map(); + this.onEndEvent = null; + this.emitter = new Emitter(); + } + + /** + * Add an animation to this sprite + * @param key The unique key of the animation + * @param animation The animation data + */ + add(key: string, animation: AnimationData): void { + this.animations.add(key, animation); + } + + /** Gets the index specified by the current animation and current frame */ + getIndex(): number { + if(this.animations.has(this.currentAnimation)){ + return this.animations.get(this.currentAnimation).frames[this.currentFrame].index; + } else { + // No current animation, warn the user + console.warn("Animation index was requested, but the current animation was invalid"); + return 0; + } + } + + getIndexAndAdvanceAnimation(): number { + // If we aren't playing, we won't be advancing the animation + if(!(this.animationState === AnimationState.PLAYING)){ + return this.getIndex(); + } + + if(this.animations.has(this.currentAnimation)){ + let currentAnimation = this.animations.get(this.currentAnimation); + let index = currentAnimation.frames[this.currentFrame].index; + + // Advance the animation + this.frameProgress += 1; + if(this.frameProgress >= currentAnimation.frames[this.currentFrame].duration){ + // We have been on this frame for its whole duration, go to the next one + this.frameProgress = 0; + this.currentFrame += 1; + + if(this.currentFrame >= currentAnimation.frames.length){ + // We have reached the end of this animation + if(this.loop){ + this.currentFrame = 0; + this.frameProgress = 0; + } else { + this.endCurrentAnimation(); + } + } + } + + // Return the current index + return index; + } else { + // No current animation, can't advance. Warn the user + console.warn("Animation index and advance was requested, but the current animation was invalid"); + return 0; + } + } + + protected endCurrentAnimation(): void { + this.currentFrame = 0; + this.animationState = AnimationState.STOPPED; + + if(this.onEndEvent !== null){ + this.emitter.fireEvent(this.onEndEvent, {owner: this.owner, animation: this.currentAnimation}); + } + + // If there is a pending animation, play it + if(this.pendingAnimation !== null){ + this.play(this.pendingAnimation, this.pendingLoop, this.pendingOnEnd); + } + } + + /** + * Plays the specified animation + * @param animation The name of the animation to play + * @param loop Whether or not to loop the animation. False by default + * @param onEnd The name of an event to send when this animation naturally stops playing. This only matters if loop is false. + */ + play(animation: string, loop: boolean = false, onEnd?: string): void { + this.currentAnimation = animation; + this.currentFrame = 0; + this.frameProgress = 0; + this.loop = loop; + this.animationState = AnimationState.PLAYING; + if(onEnd !== undefined){ + this.onEndEvent = onEnd; + } else { + this.onEndEvent = null; + } + + this.pendingAnimation = null; + } + + /** Queues a single animation to be played after the current one. Does NOT stack */ + queue(animation: string, loop: boolean = false, onEnd?: string): void { + this.pendingAnimation = animation; + this.pendingLoop = loop; + if(onEnd !== undefined){ + this.pendingOnEnd = onEnd; + } else { + this.pendingOnEnd = null; + } + } + + /** Pauses the current animation */ + pause(): void { + this.animationState = AnimationState.PAUSED; + } + + /** Resumes the current animation if possible */ + resume(): void { + if(this.animationState === AnimationState.PAUSED){ + this.animationState = AnimationState.PLAYING; + } + } + + /** Stops the current animation. The animation cannot be resumed after this. */ + stop(): void { + this.animationState = AnimationState.STOPPED; + } +} \ No newline at end of file diff --git a/src/Rendering/Animations/AnimationTypes.ts b/src/Rendering/Animations/AnimationTypes.ts new file mode 100644 index 0000000..61f1751 --- /dev/null +++ b/src/Rendering/Animations/AnimationTypes.ts @@ -0,0 +1,14 @@ +export enum AnimationState { + STOPPED = 0, + PAUSED = 1, + PLAYING = 2, +} + +export class AnimationData { + name: string; + frames: Array<{index: number, duration: number}>; +} + +export class TweenData { + +} \ No newline at end of file diff --git a/src/Rendering/CanvasRenderer.ts b/src/Rendering/CanvasRenderer.ts index d44c08a..ba83abb 100644 --- a/src/Rendering/CanvasRenderer.ts +++ b/src/Rendering/CanvasRenderer.ts @@ -17,6 +17,7 @@ import Label from "../Nodes/UIElements/Label"; import Button from "../Nodes/UIElements/Button"; import Slider from "../Nodes/UIElements/Slider"; import TextInput from "../Nodes/UIElements/TextInput"; +import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite"; export default class CanvasRenderer extends RenderingManager { protected ctx: CanvasRenderingContext2D; @@ -78,7 +79,9 @@ export default class CanvasRenderer extends RenderingManager { } protected renderNode(node: CanvasNode): void { - if(node instanceof Sprite){ + if(node instanceof AnimatedSprite){ + this.renderAnimatedSprite(node); + } else if(node instanceof Sprite){ this.renderSprite(node); } else if(node instanceof Graphic){ this.renderGraphic(node); @@ -120,8 +123,41 @@ export default class CanvasRenderer extends RenderingManager { } } - protected renderAnimatedSprite(): void { - throw new Error("Method not implemented."); + protected renderAnimatedSprite(sprite: AnimatedSprite): void { + // Get the image from the resource manager + let image = this.resourceManager.getImage(sprite.imageId); + + // Calculate the origin of the viewport according to this sprite + let origin = this.scene.getViewTranslation(sprite); + + // Get the zoom level of the scene + let zoom = this.scene.getViewScale(); + + let animationIndex = sprite.animation.getIndexAndAdvanceAnimation(); + + let animationOffset = sprite.getAnimationOffset(animationIndex); + + /* + Coordinates in the space of the image: + image crop start -> x, y + image crop size -> w, h + Coordinates in the space of the world + image draw start -> x, y + image draw size -> w, h + */ + this.ctx.drawImage(image, + sprite.imageOffset.x + animationOffset.x, sprite.imageOffset.y + animationOffset.y, + sprite.size.x, sprite.size.y, + (sprite.position.x - origin.x - sprite.size.x*sprite.scale.x/2)*zoom, (sprite.position.y - origin.y - sprite.size.y*sprite.scale.y/2)*zoom, + sprite.size.x * sprite.scale.x*zoom, sprite.size.y * sprite.scale.y*zoom); + + // Debug mode + if(this.debug){ + this.ctx.lineWidth = 4; + this.ctx.strokeStyle = "#00FF00" + let b = sprite.boundary; + this.ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2*zoom, b.hh*2*zoom); + } } protected renderGraphic(graphic: Graphic): void { diff --git a/src/Rendering/RenderingManager.ts b/src/Rendering/RenderingManager.ts index 7a8cffd..9f84c6a 100644 --- a/src/Rendering/RenderingManager.ts +++ b/src/Rendering/RenderingManager.ts @@ -1,6 +1,7 @@ import Map from "../DataTypes/Map"; import CanvasNode from "../Nodes/CanvasNode"; import Graphic from "../Nodes/Graphic"; +import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite"; import Sprite from "../Nodes/Sprites/Sprite"; import Tilemap from "../Nodes/Tilemap"; import UIElement from "../Nodes/UIElement"; @@ -29,7 +30,7 @@ export default abstract class RenderingManager { protected abstract renderSprite(sprite: Sprite): void; - protected abstract renderAnimatedSprite(): void; + protected abstract renderAnimatedSprite(sprite: AnimatedSprite): void; protected abstract renderGraphic(graphic: Graphic): void; diff --git a/src/ResourceManager/ResourceManager.ts b/src/ResourceManager/ResourceManager.ts index 47dd2b8..2ae9cea 100644 --- a/src/ResourceManager/ResourceManager.ts +++ b/src/ResourceManager/ResourceManager.ts @@ -1,9 +1,9 @@ import Map from "../DataTypes/Map"; -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"; +import Spritesheet from "../DataTypes/Spritesheet"; export default class ResourceManager { // Instance for the singleton class @@ -18,60 +18,43 @@ export default class ResourceManager { public onLoadComplete: Function; - /** - * Number to keep track of how many images need to be loaded - */ + /** 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 - */ + /** Number to keep track of how many images are loaded */ private loadonly_imagesToLoad: number; - /** - * The queue of images we must load - */ + /** The queue of images we must load */ private loadonly_imageLoadingQueue: Queue<{key: string, path: string}>; - /** - * A map of the images that are currently loaded and (presumably) being used by the scene - */ + /** A map of the images that are currently loaded and (presumably) being used by the scene */ private images: Map; - /** - * Number to keep track of how many tilemaps need to be loaded - */ + /** 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<{key: string, path: string}>; + /** 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 - */ + /** Number to keep track of how many tilemaps are loaded */ private loadonly_tilemapsToLoad: number; - /** - * The queue of tilemaps we must load - */ + /** The queue of tilemaps we must load */ private loadonly_tilemapLoadingQueue: Queue<{key: string, path: string}>; - /** - * A map of the tilemaps that are currently loaded and (presumably) being used by the scene - */ + /** 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 - */ + /** 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 - */ + /** Number to keep track of how many sounds are loaded */ private loadonly_audioToLoad: number; - /** - * The queue of sounds we must load - */ + /** The queue of sounds we must load */ private loadonly_audioLoadingQueue: Queue<{key: string, path: string}>; - /** - * A map of the sounds that are currently loaded and (presumably) being used by the scene - */ + /** 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) - */ + /** The total number of "types" of things that need to be loaded (i.e. images and tilemaps) */ private loadonly_typesToLoad: number; private constructor(){ @@ -83,6 +66,11 @@ export default class ResourceManager { 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(); @@ -122,8 +110,12 @@ export default class ResourceManager { return this.images.get(key); } - public spritesheet(key: string, path: string, frames: {hFrames: number, vFrames: number}): void { + public spritesheet(key: string, path: string): void { + this.loadonly_spritesheetLoadingQueue.enqueue({key: key, path: path}); + } + public getSpritesheet(key: string): Spritesheet { + return this.spritesheets.get(key); } /** @@ -160,7 +152,6 @@ export default class ResourceManager { return this.tilemaps.get(key); } - // TODO - Should everything be loaded in order, one file at a time? /** * Loads all resources currently in the queue * @param callback @@ -173,14 +164,17 @@ export default class ResourceManager { // 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.loadImagesFromQueue(() => { - console.log("Loaded Images"); - this.loadAudioFromQueue(() => { - console.log("Loaded Audio"); - // Done loading - this.loading = false; - this.justLoaded = true; - callback(); + this.loadSpritesheetsFromQueue(() => { + console.log("Loaded Spritesheets"); + this.loadImagesFromQueue(() => { + console.log("Loaded Images"); + this.loadAudioFromQueue(() => { + console.log("Loaded Audio"); + // Done loading + this.loading = false; + this.justLoaded = true; + callback(); + }); }); }); }); @@ -198,6 +192,10 @@ export default class ResourceManager { 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(); @@ -252,7 +250,6 @@ export default class ResourceManager { this.loadonly_imageLoadingQueue.enqueue({key: key, path: path}); } } - } // Finish loading @@ -273,8 +270,62 @@ export default class ResourceManager { } } + /** + * Loads all spritesheets currently in the spritesheet loading queue + * @param onFinishLoading + */ + 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 all images currently in the tilemap loading queue + * Loads a singular spritesheet + * @param key + * @param pathToSpritesheetJSON + * @param callbackIfLast + */ + 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 + */ + 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 */ private loadImagesFromQueue(onFinishLoading: Function): void { @@ -398,6 +449,7 @@ export default class ResourceManager { 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; diff --git a/src/Scene/Factories/CanvasNodeFactory.ts b/src/Scene/Factories/CanvasNodeFactory.ts index f7bf4c4..b126bf4 100644 --- a/src/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Scene/Factories/CanvasNodeFactory.ts @@ -2,6 +2,7 @@ import Scene from "../Scene"; import UIElement from "../../Nodes/UIElement"; import Graphic from "../../Nodes/Graphic"; import Sprite from "../../Nodes/Sprites/Sprite"; +import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite"; import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; import { UIElementType } from "../../Nodes/UIElements/UIElementTypes"; import Point from "../../Nodes/Graphics/Point"; @@ -11,12 +12,15 @@ import Label from "../../Nodes/UIElements/Label"; import Slider from "../../Nodes/UIElements/Slider"; import TextInput from "../../Nodes/UIElements/TextInput"; import Rect from "../../Nodes/Graphics/Rect"; +import ResourceManager from "../../ResourceManager/ResourceManager"; export default class CanvasNodeFactory { - private scene: Scene; + protected scene: Scene; + protected resourceManager: ResourceManager; init(scene: Scene): void { this.scene = scene; + this.resourceManager = ResourceManager.getInstance(); } /** @@ -79,6 +83,22 @@ export default class CanvasNodeFactory { return instance; } + addAnimatedSprite = (key: string, layerName: string): AnimatedSprite => { + let layer = this.scene.getLayer(layerName); + let spritesheet = this.resourceManager.getSpritesheet(key); + let instance = new AnimatedSprite(spritesheet); + + // Add instance fo scene + instance.setScene(this.scene); + instance.id = this.scene.generateId(); + this.scene.getSceneGraph().addNode(instance); + + // Add instance to layer + layer.addNode(instance); + + return instance; + } + /** * Adds a new graphic element to the current Scene * @param type The type of graphic to add diff --git a/src/Scene/Factories/FactoryManager.ts b/src/Scene/Factories/FactoryManager.ts index 53afa49..6089f48 100644 --- a/src/Scene/Factories/FactoryManager.ts +++ b/src/Scene/Factories/FactoryManager.ts @@ -17,6 +17,7 @@ export default class FactoryManager { // Expose all of the factories through the factory manager uiElement = this.canvasNodeFactory.addUIElement; sprite = this.canvasNodeFactory.addSprite; + animatedSprite = this.canvasNodeFactory.addAnimatedSprite; graphic = this.canvasNodeFactory.addGraphic; tilemap = this.tilemapFactory.add; } \ No newline at end of file diff --git a/src/_DemoClasses/Mario/MainMenu.ts b/src/_DemoClasses/Mario/MainMenu.ts index e81de4a..3bed050 100644 --- a/src/_DemoClasses/Mario/MainMenu.ts +++ b/src/_DemoClasses/Mario/MainMenu.ts @@ -11,8 +11,13 @@ import Level1 from "./Level1"; export default class MainMenu extends Scene { + loadScene(): void { + this.load.spritesheet("walker", "assets/spritesheets/walking.json"); + } + startScene(): void { this.addUILayer("Main"); + this.addLayer("Sprite"); let size = this.viewport.getHalfSize(); this.viewport.setFocus(size); @@ -37,10 +42,11 @@ export default class MainMenu extends Scene { this.sceneManager.changeScene(Level1, sceneOptions); } - let slider = this.add.uiElement(UIElementType.SLIDER, "Main", {position: new Vec2(size.x, size.y*1.5)}); - let label = this.add.uiElement(UIElementType.LABEL, "Main", {position: new Vec2(size.x + 150, size.y*1.5), text: ""}); - slider.onValueChange = (value) => (