added animated sprites

This commit is contained in:
Joe Weaver 2020-11-24 16:51:13 -05:00
parent b6a0aa569a
commit 32d63ea2bf
10 changed files with 410 additions and 61 deletions

View File

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

View File

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

View File

@ -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<AnimationData>;
/** 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;
}
}

View File

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

View File

@ -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(<AnimatedSprite>node);
} else if(node instanceof Sprite){
this.renderSprite(<Sprite>node);
} else if(node instanceof Graphic){
this.renderGraphic(<Graphic>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 {

View File

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

View File

@ -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<HTMLImageElement>;
/**
* 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<Spritesheet>;
/** 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<TiledTilemapData>;
/**
* 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<AudioBuffer>;
/**
* 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 = <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;

View File

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

View File

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

View File

@ -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 = <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) => (<Label>label).setText(value.toString());
this.add.uiElement(UIElementType.TEXT_INPUT, "Main", {position: new Vec2(size.x, size.y*1.7)});
let animatedSprite = this.add.animatedSprite("walker", "Sprite");
animatedSprite.position.set(100, 100);
animatedSprite.scale.set(4, 4);
animatedSprite.animation.play("JUMP");
animatedSprite.animation.queue("WALK", true);
}
updateScene(): void {