diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index f753570..675a578 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -11,6 +11,7 @@ import Stats from "../Debug/Stats"; import ArrayUtils from "../Utils/ArrayUtils"; import RenderingManager from "../Rendering/RenderingManager"; import CanvasRenderer from "../Rendering/CanvasRenderer"; +import Color from "../Utils/Color"; export default class GameLoop { gameOptions: GameOptions; @@ -63,6 +64,7 @@ export default class GameLoop { readonly HEIGHT: number; private viewport: Viewport; private ctx: CanvasRenderingContext2D; + private clearColor: Color; // All of the necessary subsystems that need to run here private eventQueue: EventQueue; @@ -107,6 +109,7 @@ export default class GameLoop { // For now, just hard code a canvas renderer. We can do this with options later this.renderingManager = new CanvasRenderer(); this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); + this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b); // Size the viewport to the game canvas this.viewport = new Viewport(); @@ -272,6 +275,8 @@ export default class GameLoop { */ render(): void { this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); + this.ctx.fillStyle = this.clearColor.toString(); + this.ctx.fillRect(0, 0, this.WIDTH, this.HEIGHT); this.sceneManager.render(); Debug.render(this.ctx); Stats.render(); @@ -279,12 +284,14 @@ export default class GameLoop { } class GameOptions { - viewportSize: {x: number, y: number} + viewportSize: {x: number, y: number}; + clearColor: {r: number, g: number, b: number} static parse(options: Record): GameOptions { let gOpt = new GameOptions(); gOpt.viewportSize = options.viewportSize ? options.viewportSize : {x: 800, y: 600}; + gOpt.clearColor = options.clearColor ? options.clearColor : {r: 255, g: 255, b: 255}; return gOpt; } diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index 8708167..84b8c34 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -45,6 +45,14 @@ export default abstract class CanvasNode extends GameNode implements Region { this.scaleChanged(); } + set scaleX(value: number) { + this.scale.x = value; + } + + set scaleY(value: number) { + this.scale.y = value; + } + protected positionChanged(): void { super.positionChanged(); this.updateBoundary(); diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 6592760..f25042f 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -9,11 +9,12 @@ import Shape from "../DataTypes/Shapes/Shape"; import Map from "../DataTypes/Map"; import AABB from "../DataTypes/Shapes/AABB"; import NavigationPath from "../Pathfinding/NavigationPath"; +import TweenManager from "../Rendering/Animations/TweenManager"; /** * The representation of an object in the game world */ -export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor, Debug_Renderable { +export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor { /*---------- POSITIONED ----------*/ private _position: Vec2; @@ -46,12 +47,14 @@ export default abstract class GameNode implements Positioned, Unique, Updateable path: NavigationPath; pathfinding: boolean = false; + /*---------- GENERAL ----------*/ protected input: InputReceiver; protected receiver: Receiver; protected emitter: Emitter; protected scene: Scene; protected layer: Layer; - + tweens: TweenManager; + rotation: number; constructor(){ this.input = InputReceiver.getInstance(); @@ -59,6 +62,8 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this._position.setOnChange(() => this.positionChanged()); this.receiver = new Receiver(); this.emitter = new Emitter(); + this.tweens = new TweenManager(this); + this.rotation = 0; } /*---------- POSITIONED ----------*/ @@ -190,6 +195,19 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.aiActive = active; } + /*---------- TWEENABLE PROPERTIES ----------*/ + set positionX(value: number) { + this.position.x = value; + } + + set positionY(value: number) { + this.position.y = value; + } + + abstract set scaleX(value: number); + + abstract set scaleY(value: number); + /*---------- GAME NODE ----------*/ /** * Sets the scene for this object. @@ -226,7 +244,15 @@ export default abstract class GameNode implements Positioned, Unique, Updateable } }; - abstract update(deltaT: number): void; + update(deltaT: number): void { + this.tweens.update(deltaT); + } +} - debug_render = (ctx: CanvasRenderingContext2D): void => {}; +export enum TweenableProperties{ + posX = "positionX", + posY = "positionY", + scaleX = "scaleX", + scaleY = "scaleY", + rotation = "rotation" } \ No newline at end of file diff --git a/src/Nodes/Graphics/Point.ts b/src/Nodes/Graphics/Point.ts index 0aef392..3a81e40 100644 --- a/src/Nodes/Graphics/Point.ts +++ b/src/Nodes/Graphics/Point.ts @@ -8,6 +8,4 @@ export default class Point extends Graphic { this.position = position; this.size.set(5, 5); } - - update(deltaT: number): void {} } \ No newline at end of file diff --git a/src/Nodes/Graphics/Rect.ts b/src/Nodes/Graphics/Rect.ts index 0c9130c..c97c6c9 100644 --- a/src/Nodes/Graphics/Rect.ts +++ b/src/Nodes/Graphics/Rect.ts @@ -38,6 +38,4 @@ export default class Rect extends Graphic { getBorderWidth(): number { return this.borderWidth; } - - update(deltaT: number): void {} } \ No newline at end of file diff --git a/src/Nodes/Sprites/AnimatedSprite.ts b/src/Nodes/Sprites/AnimatedSprite.ts index 7a20390..a5866d8 100644 --- a/src/Nodes/Sprites/AnimatedSprite.ts +++ b/src/Nodes/Sprites/AnimatedSprite.ts @@ -21,7 +21,7 @@ export default class AnimatedSprite extends Sprite { // 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(); + this.animation = new AnimationManager(this); // Add the animations to the animated sprite for(let animation of spritesheet.animations){ diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index f990070..73bc98d 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -8,6 +8,8 @@ import Vec2 from "../../DataTypes/Vec2"; export default class Sprite extends CanvasNode { imageId: string; imageOffset: Vec2; + invertX: boolean; + invertY: boolean; constructor(imageId: string){ super(); @@ -15,6 +17,8 @@ export default class Sprite extends CanvasNode { let image = ResourceManager.getInstance().getImage(this.imageId); this.size = new Vec2(image.width, image.height); this.imageOffset = Vec2.ZERO; + this.invertX = false; + this.invertY = false; } /** @@ -24,6 +28,4 @@ export default class Sprite extends CanvasNode { setImageOffset(offset: Vec2): void { this.imageOffset = offset; } - - update(deltaT: number): void {} } \ No newline at end of file diff --git a/src/Nodes/UIElement.ts b/src/Nodes/UIElement.ts index 28e3d4c..03256b3 100644 --- a/src/Nodes/UIElement.ts +++ b/src/Nodes/UIElement.ts @@ -59,6 +59,8 @@ export default abstract class UIElement extends CanvasNode { } update(deltaT: number): void { + super.update(deltaT); + // See of this object was just clicked if(this.input.isMouseJustPressed()){ let clickPos = this.input.getMousePressPosition(); diff --git a/src/Physics/BasicPhysicsManager.ts b/src/Physics/BasicPhysicsManager.ts index 71f34fd..a3d7587 100644 --- a/src/Physics/BasicPhysicsManager.ts +++ b/src/Physics/BasicPhysicsManager.ts @@ -9,6 +9,7 @@ import Shape from "../DataTypes/Shapes/Shape"; import MathUtils from "../Utils/MathUtils"; import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; import AABB from "../DataTypes/Shapes/AABB"; +import Debug from "../Debug/Debug"; export default class BasicPhysicsManager extends PhysicsManager { diff --git a/src/Rendering/Animations/AnimationManager.ts b/src/Rendering/Animations/AnimationManager.ts index 48b9f83..236147b 100644 --- a/src/Rendering/Animations/AnimationManager.ts +++ b/src/Rendering/Animations/AnimationManager.ts @@ -40,7 +40,8 @@ export default class AnimationManager { /** The onEnd event of a pending animation */ protected pendingOnEnd: string; - constructor(){ + constructor(owner: CanvasNode){ + this.owner = owner; this.animationState = AnimationState.STOPPED; this.currentAnimation = ""; this.currentFrame = 0; diff --git a/src/Rendering/Animations/AnimationTypes.ts b/src/Rendering/Animations/AnimationTypes.ts index 61f1751..c5316e2 100644 --- a/src/Rendering/Animations/AnimationTypes.ts +++ b/src/Rendering/Animations/AnimationTypes.ts @@ -1,3 +1,6 @@ +import { TweenableProperties } from "../../Nodes/GameNode"; +import { EaseFunctionType } from "../../Utils/EaseFunctions"; + export enum AnimationState { STOPPED = 0, PAUSED = 1, @@ -10,5 +13,33 @@ export class AnimationData { } export class TweenData { + // Members for initialization by the user + /** The amount of time in ms to wait before executing the tween */ + startDelay: number; + /** The duration of time over which the value with change from start to end */ + duration: number; + /** An array of the effects on the properties of the object */ + effects: [{ + property: TweenableProperties; + start: any; + end: any; + ease: EaseFunctionType; + }]; + /** Whether or not this tween should reverse from end to start for each property when it finishes */ + reverseOnComplete: boolean; + /** Whether or not this tween should loop when it completes */ + loop: boolean; + + // Members for management by the tween manager + /** The progress of this tween through its effects */ + progress: number; + /** The amount of time in ms that has passed from when this tween started running */ + elapsedTime: number; + + /** The state of this tween */ + animationState: AnimationState; + + /** Whether or not this tween is currently reversing */ + reversing: boolean; } \ No newline at end of file diff --git a/src/Rendering/Animations/TweenManager.ts b/src/Rendering/Animations/TweenManager.ts new file mode 100644 index 0000000..46286c6 --- /dev/null +++ b/src/Rendering/Animations/TweenManager.ts @@ -0,0 +1,141 @@ +import Map from "../../DataTypes/Map"; +import GameNode from "../../Nodes/GameNode"; +import { AnimationState, TweenData } from "./AnimationTypes"; +import EaseFunctions from "../../Utils/EaseFunctions"; +import MathUtils from "../../Utils/MathUtils"; + +export default class TweenManager { + protected owner: GameNode; + + protected tweens: Map; + + constructor(owner: GameNode){ + this.owner = owner; + this.tweens = new Map(); + } + + /** + * Add a tween to this game node + * @param key The name of the tween + * @param tween The data of the tween + */ + add(key: string, tween: Record | TweenData): void { + let typedTween = tween; + + // Initialize members that we need (and the user didn't provide) + typedTween.progress = 0; + typedTween.elapsedTime = 0; + typedTween.animationState = AnimationState.STOPPED; + + this.tweens.add(key, typedTween); + } + + /** + * Play a tween with a certain name + * @param key The name of the tween to play + * @param loop Whether or not the tween should loop + */ + play(key: string, loop?: boolean): void { + if(this.tweens.has(key)){ + let tween = this.tweens.get(key); + + // Set loop if needed + if(loop !== undefined){ + tween.loop = loop; + } + + // Start the tween running + tween.animationState = AnimationState.PLAYING; + tween.elapsedTime = 0; + tween.progress = 0; + tween.reversing = false; + } + } + + /** + * Pauses a playing tween. Does not affect tweens that are stopped. + * @param key The name of the tween to pause. + */ + pause(key: string): void { + if(this.tweens.has(key)){ + this.tweens.get(key).animationState = AnimationState.PAUSED; + } + } + + /** + * Resumes a paused tween. + * @param key The name of the tween to resume + */ + resume(key: string): void { + if(this.tweens.has(key)){ + let tween = this.tweens.get(key); + if(tween.animationState === AnimationState.PAUSED) + tween.animationState = AnimationState.PLAYING; + } + } + + /** + * Stops a currently playing tween + * @param key + */ + stop(key: string): void { + if(this.tweens.has(key)){ + this.tweens.get(key).animationState = AnimationState.STOPPED; + } + } + + update(deltaT: number): void { + this.tweens.forEach(key => { + let tween = this.tweens.get(key); + if(tween.animationState === AnimationState.PLAYING){ + // Update the progress of the tween + tween.elapsedTime += deltaT*1000; + + // If we're past the startDelay, do the tween + if(tween.elapsedTime >= tween.startDelay){ + if(!tween.reversing && tween.elapsedTime >= tween.startDelay + tween.duration){ + // If we're over time, stop the tween, loop, or reverse + if(tween.reverseOnComplete){ + // If we're over time and can reverse, do so + tween.reversing = true; + } else if(tween.loop){ + // If we can't reverse and can loop, do so + tween.elapsedTime -= tween.duration; + } else { + // We aren't looping and can't reverse, so stop + tween.animationState = AnimationState.STOPPED; + } + } + + // Check for the end of reversing + if(tween.reversing && tween.elapsedTime >= tween.startDelay + 2*tween.duration){ + if(tween.loop){ + tween.reversing = false; + tween.elapsedTime -= 2*tween.duration; + } else { + tween.animationState = AnimationState.STOPPED; + } + } + + // Update the progress, make sure it is between 0 and 1. Errors from this should never be large + if(tween.reversing){ + tween.progress = MathUtils.clamp01((2*tween.duration - (tween.elapsedTime- tween.startDelay))/tween.duration); + } else { + tween.progress = MathUtils.clamp01((tween.elapsedTime - tween.startDelay)/tween.duration); + } + + for(let effect of tween.effects){ + // Get the value from the ease function that corresponds to our progress + let ease = EaseFunctions[effect.ease](tween.progress); + + // Use the value to lerp the property + let value = MathUtils.lerp(effect.start, effect.end, ease); + + // Assign the value of the property + this.owner[effect.property] = value; + } + } + } + }); + } +} \ No newline at end of file diff --git a/src/Rendering/CanvasRenderer.ts b/src/Rendering/CanvasRenderer.ts index ba83abb..9dfc51a 100644 --- a/src/Rendering/CanvasRenderer.ts +++ b/src/Rendering/CanvasRenderer.ts @@ -18,6 +18,7 @@ import Button from "../Nodes/UIElements/Button"; import Slider from "../Nodes/UIElements/Slider"; import TextInput from "../Nodes/UIElements/TextInput"; import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite"; +import Vec2 from "../DataTypes/Vec2"; export default class CanvasRenderer extends RenderingManager { protected ctx: CanvasRenderingContext2D; @@ -25,8 +26,11 @@ export default class CanvasRenderer extends RenderingManager { protected tilemapRenderer: TilemapRenderer; protected uiElementRenderer: UIElementRenderer; + protected origin: Vec2; + protected zoom: number; + constructor(){ - super();; + super(); } setScene(scene: Scene){ @@ -79,6 +83,24 @@ export default class CanvasRenderer extends RenderingManager { } protected renderNode(node: CanvasNode): void { + // Calculate the origin of the viewport according to this sprite + this.origin = this.scene.getViewTranslation(node); + + // Get the zoom level of the scene + this.zoom = this.scene.getViewScale(); + + // Move the canvas to the position of the node and rotate + let xScale = 1; + let yScale = 1; + + if(node instanceof Sprite){ + xScale = node.invertX ? -1 : 1; + yScale = node.invertY ? -1 : 1; + } + + this.ctx.setTransform(xScale, 0, 0, yScale, (node.position.x - this.origin.x)*this.zoom, (node.position.y - this.origin.y)*this.zoom); + this.ctx.rotate(node.rotation); + if(node instanceof AnimatedSprite){ this.renderAnimatedSprite(node); } else if(node instanceof Sprite){ @@ -88,18 +110,14 @@ export default class CanvasRenderer extends RenderingManager { } else if(node instanceof UIElement){ this.renderUIElement(node); } + + this.ctx.setTransform(1, 0, 0, 1, 0, 0); } protected renderSprite(sprite: Sprite): 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(); - /* Coordinates in the space of the image: image crop start -> x, y @@ -111,15 +129,15 @@ export default class CanvasRenderer extends RenderingManager { this.ctx.drawImage(image, sprite.imageOffset.x, sprite.imageOffset.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); + (-sprite.size.x*sprite.scale.x/2)*this.zoom, (-sprite.size.y*sprite.scale.y/2)*this.zoom, + sprite.size.x * sprite.scale.x*this.zoom, sprite.size.y * sprite.scale.y*this.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); + this.ctx.strokeRect(-b.hw*this.zoom, -b.hh*this.zoom, b.hw*2*this.zoom, b.hh*2*this.zoom); } } @@ -127,12 +145,6 @@ export default class CanvasRenderer extends RenderingManager { // 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); @@ -141,30 +153,30 @@ export default class CanvasRenderer extends RenderingManager { 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 + Coordinates in the space of the world (given we moved) + image draw start -> -w/2, -h/2 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); + (-sprite.size.x*sprite.scale.x/2)*this.zoom, (-sprite.size.y*sprite.scale.y/2)*this.zoom, + sprite.size.x * sprite.scale.x*this.zoom, sprite.size.y * sprite.scale.y*this.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); + this.ctx.strokeRect(-b.hw*this.zoom, -b.hh*this.zoom, b.hw*2*this.zoom, b.hh*2*this.zoom); } } protected renderGraphic(graphic: Graphic): void { if(graphic instanceof Point){ - this.graphicRenderer.renderPoint(graphic); + this.graphicRenderer.renderPoint(graphic, this.origin, this.zoom); } else if(graphic instanceof Rect){ - this.graphicRenderer.renderRect(graphic); + this.graphicRenderer.renderRect(graphic, this.origin, this.zoom); } } @@ -176,13 +188,13 @@ export default class CanvasRenderer extends RenderingManager { protected renderUIElement(uiElement: UIElement): void { if(uiElement instanceof Label){ - this.uiElementRenderer.renderLabel(uiElement); + this.uiElementRenderer.renderLabel(uiElement, this.origin, this.zoom); } else if(uiElement instanceof Button){ - this.uiElementRenderer.renderButton(uiElement); + this.uiElementRenderer.renderButton(uiElement, this.origin, this.zoom); } else if(uiElement instanceof Slider){ - this.uiElementRenderer.renderSlider(uiElement); + this.uiElementRenderer.renderSlider(uiElement, this.origin, this.zoom); } else if(uiElement instanceof TextInput){ - this.uiElementRenderer.renderTextInput(uiElement); + this.uiElementRenderer.renderTextInput(uiElement, this.origin, this.zoom); } } } \ No newline at end of file diff --git a/src/Rendering/CanvasRendering/GraphicRenderer.ts b/src/Rendering/CanvasRendering/GraphicRenderer.ts index 12adff0..a18bce9 100644 --- a/src/Rendering/CanvasRendering/GraphicRenderer.ts +++ b/src/Rendering/CanvasRendering/GraphicRenderer.ts @@ -1,3 +1,4 @@ +import Vec2 from "../../DataTypes/Vec2"; import Point from "../../Nodes/Graphics/Point"; import Rect from "../../Nodes/Graphics/Rect"; import ResourceManager from "../../ResourceManager/ResourceManager"; @@ -17,28 +18,22 @@ export default class GraphicRenderer { this.scene = scene; } - renderPoint(point: Point): void { - let origin = this.scene.getViewTranslation(point); - let zoom = this.scene.getViewScale(); - + renderPoint(point: Point, origin: Vec2, zoom: number): void { this.ctx.fillStyle = point.color.toStringRGBA(); - this.ctx.fillRect((point.position.x - origin.x - point.size.x/2)*zoom, (point.position.y - origin.y - point.size.y/2)*zoom, + this.ctx.fillRect((-point.size.x/2)*zoom, (-point.size.y/2)*zoom, point.size.x*zoom, point.size.y*zoom); } - renderRect(rect: Rect): void { - let origin = this.scene.getViewTranslation(rect); - let zoom = this.scene.getViewScale(); - + renderRect(rect: Rect, origin: Vec2, zoom: number): void { // Draw the interior of the rect if(rect.color.a !== 0){ this.ctx.fillStyle = rect.color.toStringRGB(); - this.ctx.fillRect((rect.position.x - rect.size.x/2 - origin.x)*zoom, (rect.position.y - rect.size.y/2 - origin.y)*zoom, rect.size.x*zoom, rect.size.y*zoom); + this.ctx.fillRect((-rect.size.x/2)*zoom, (-rect.size.y/2)*zoom, rect.size.x*zoom, rect.size.y*zoom); } // Draw the border of the rect this.ctx.strokeStyle = rect.getBorderColor().toStringRGB(); this.ctx.lineWidth = rect.getBorderWidth(); - this.ctx.strokeRect((rect.position.x - rect.size.x/2 - origin.x)*zoom, (rect.position.y - rect.size.y/2 - origin.y)*zoom, rect.size.x*zoom, rect.size.y*zoom); + this.ctx.strokeRect((-rect.size.x/2)*zoom, (-rect.size.y/2)*zoom, rect.size.x*zoom, rect.size.y*zoom); } } \ No newline at end of file diff --git a/src/Rendering/CanvasRendering/UIElementRenderer.ts b/src/Rendering/CanvasRendering/UIElementRenderer.ts index f476750..6bb2bf1 100644 --- a/src/Rendering/CanvasRendering/UIElementRenderer.ts +++ b/src/Rendering/CanvasRendering/UIElementRenderer.ts @@ -21,16 +21,13 @@ export default class UIElementRenderer { this.scene = scene; } - renderLabel(label: Label): void { + renderLabel(label: Label, origin: Vec2, zoom: number): void { // If the size is unassigned (by the user or automatically) assign it label.handleInitialSizing(this.ctx); // Grab the global alpha so we can adjust it for this render let previousAlpha = this.ctx.globalAlpha; - // Get the origin of the viewport according to this label - let origin = this.scene.getViewTranslation(label); - // Get the font and text position in label this.ctx.font = label.getFontString(); let offset = label.calculateTextOffset(this.ctx); @@ -38,39 +35,37 @@ export default class UIElementRenderer { // Stroke and fill a rounded rect and give it text this.ctx.globalAlpha = label.backgroundColor.a; this.ctx.fillStyle = label.calculateBackgroundColor(); - this.ctx.fillRoundedRect(label.position.x - origin.x - label.size.x/2, label.position.y - origin.y - label.size.y/2, + this.ctx.fillRoundedRect(-label.size.x/2, -label.size.y/2, label.size.x, label.size.y, label.borderRadius); this.ctx.strokeStyle = label.calculateBorderColor(); this.ctx.globalAlpha = label.borderColor.a; this.ctx.lineWidth = label.borderWidth; - this.ctx.strokeRoundedRect(label.position.x - origin.x - label.size.x/2, label.position.y - origin.y - label.size.y/2, + this.ctx.strokeRoundedRect(-label.size.x/2, -label.size.y/2, label.size.x, label.size.y, label.borderRadius); this.ctx.fillStyle = label.calculateTextColor(); this.ctx.globalAlpha = label.textColor.a; - this.ctx.fillText(label.text, label.position.x + offset.x - origin.x - label.size.x/2, label.position.y + offset.y - origin.y - label.size.y/2); + this.ctx.fillText(label.text, offset.x - label.size.x/2, offset.y - label.size.y/2); this.ctx.globalAlpha = previousAlpha; } - renderButton(button: Button): void { - this.renderLabel(button); + renderButton(button: Button, origin: Vec2, zoom: number): void { + this.renderLabel(button, origin, zoom); } - renderSlider(slider: Slider): void { + renderSlider(slider: Slider, origin: Vec2, zoom: number): void { // Grab the global alpha so we can adjust it for this render let previousAlpha = this.ctx.globalAlpha; this.ctx.globalAlpha = slider.getLayer().getAlpha(); - let origin = this.scene.getViewTranslation(slider); - // Calcualate the slider size let sliderSize = new Vec2(slider.size.x, 2); // Draw the slider this.ctx.fillStyle = slider.sliderColor.toString(); - this.ctx.fillRoundedRect(slider.position.x - origin.x - sliderSize.x/2, slider.position.y - origin.y - sliderSize.y/2, + this.ctx.fillRoundedRect(-sliderSize.x/2, -sliderSize.y/2, sliderSize.x, sliderSize.y, slider.borderRadius); // Calculate the nib size and position @@ -80,20 +75,20 @@ export default class UIElementRenderer { // Draw the nib this.ctx.fillStyle = slider.nibColor.toString(); - this.ctx.fillRoundedRect(nibPosition.x - origin.x - nibSize.x/2, nibPosition.y - origin.y - nibSize.y/2, + this.ctx.fillRoundedRect(-nibSize.x/2, -nibSize.y/2, nibSize.x, nibSize.y, slider.borderRadius); // Reset the alpha this.ctx.globalAlpha = previousAlpha; } - renderTextInput(textInput: TextInput): void { + renderTextInput(textInput: TextInput, origin: Vec2, zoom: number): void { // Show a cursor sometimes if(textInput.focused && textInput.cursorCounter % 60 > 30){ textInput.text += "|"; } - this.renderLabel(textInput); + this.renderLabel(textInput, origin, zoom); if(textInput.focused){ if(textInput.cursorCounter % 60 > 30){ diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index a113d69..89d4516 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -120,6 +120,10 @@ export default class Viewport { } } + setZoomLevel(zoom: number): void { + this.view.halfSize.scale(1/zoom); + } + getZoomLevel(): number { return this.canvasSize.x/this.view.hw/2 } diff --git a/src/Utils/EaseFunctions.ts b/src/Utils/EaseFunctions.ts new file mode 100644 index 0000000..d81f340 --- /dev/null +++ b/src/Utils/EaseFunctions.ts @@ -0,0 +1,53 @@ +export default class EaseFunctions { + + static easeInOutSine(x: number): number { + return -(Math.cos(Math.PI * x) - 1) / 2; + } + + static easeOutInSine(x: number): number { + return x < 0.5 ? -Math.cos(Math.PI*(x + 0.5))/2 : -Math.cos(Math.PI*(x - 0.5))/2 + 1; + } + + static easeOutSine(x: number): number { + return Math.sin((x * Math.PI) / 2); + } + + static easeInSine(x: number): number { + return 1 - Math.cos((x * Math.PI) / 2); + } + + static easeInOutQuint(x: number): number { + return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2; + } + + static easeInOutQuad(x: number): number { + return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; + } + + static easeOutInQuad(x: number): number { + return x < 0.5 ? this.easeOutIn_OutPow(x, 2) : this.easeOutIn_InPow(x, 2); + } + + private static easeOutIn_OutPow(x: number, pow: number): number { + return 0.5 - Math.pow(-2 * x + 1, pow) / 2; + } + + private static easeOutIn_InPow(x: number, pow: number): number { + return 0.5 + Math.pow(2 * x - 1, pow) / 2; + } +} + +export enum EaseFunctionType { + // SINE + IN_OUT_SINE = "easeInOutSine", + OUT_IN_SINE = "easeOutInSine", + IN_SINE = "easeInSine", + OUT_SINE = "easeOutSine", + + // QUAD + IN_OUT_QUAD = "easeInOutQuad", + OUT_IN_QUAD = "easeOutInQuad", + + // QUINT + IN_OUT_QUINT = "easeInOutQuint" +} \ No newline at end of file diff --git a/src/_DemoClasses/Mario/Level1.ts b/src/_DemoClasses/Mario/Level1.ts index f2fc12f..8b061da 100644 --- a/src/_DemoClasses/Mario/Level1.ts +++ b/src/_DemoClasses/Mario/Level1.ts @@ -8,6 +8,9 @@ import Scene from "../../Scene/Scene"; import PlayerController from "../Player/PlayerController"; import GoombaController from "../Enemies/GoombaController"; import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap"; +import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite"; +import Debug from "../../Debug/Debug"; +import { EaseFunctionType } from "../../Utils/EaseFunctions"; export enum MarioEvents { PLAYER_HIT_COIN = "PlayerHitCoin", @@ -15,32 +18,30 @@ export enum MarioEvents { } export default class Level1 extends Scene { - player: GameNode; + player: AnimatedSprite; coinCount: number = 0; coinCountLabel: Label; livesCount: number = 3; livesCountLabel: Label; loadScene(): void { - this.load.tilemap("level1", "/assets/tilemaps/level1.json"); + this.load.tilemap("level1", "/assets/tilemaps/2bitlevel1.json"); this.load.image("goomba", "assets/sprites/Goomba.png"); this.load.image("koopa", "assets/sprites/Koopa.png"); + this.load.spritesheet("player", "assets/spritesheets/walking.json"); } startScene(): void { - let tilemap = this.add.tilemap("level1", new Vec2(2, 2))[0].getItems()[0]; - console.log(tilemap); - console.log((tilemap as OrthogonalTilemap).getTileAtRowCol(new Vec2(8, 17))); - (tilemap as OrthogonalTilemap).setTileAtRowCol(new Vec2(8, 17), 1); - console.log((tilemap as OrthogonalTilemap).getTileAtRowCol(new Vec2(8, 17))); - this.viewport.setBounds(0, 0, 150*64, 20*64); - - // Give parallax to the parallax layers - (this.getLayer("Clouds") as ParallaxLayer).parallax.set(0.5, 1); - (this.getLayer("Hills") as ParallaxLayer).parallax.set(0.8, 1); + let tilemap = this.add.tilemap("level1", new Vec2(2, 2))[0].getItems()[0]; + //tilemap.position.set(tilemap.size.x*tilemap.scale.x/2, tilemap.size.y*tilemap.scale.y/2); + tilemap.position.set(0, 0); + this.viewport.setBounds(0, 0, 128*32, 20*32); // Add the player (a rect for now) - this.player = this.add.graphic(GraphicType.RECT, "Main", {position: new Vec2(192, 1152), size: new Vec2(64, 64)}); + // this.player = this.add.graphic(GraphicType.RECT, "Main", {position: new Vec2(192, 1152), size: new Vec2(64, 64)}); + this.player = this.add.animatedSprite("player", "Main"); + this.player.scale.set(2, 2); + this.player.position.set(5*32, 18*32); this.player.addPhysics(); this.player.addAI(PlayerController, {playerType: "platformer", tilemap: "Main"}); @@ -49,28 +50,43 @@ export default class Level1 extends Scene { this.player.addTrigger("coinBlock", MarioEvents.PLAYER_HIT_COIN_BLOCK); this.player.setPhysicsLayer("player"); + this.player.tweens.add("flip", { + startDelay: 0, + duration: 500, + effects: [ + { + property: "rotation", + start: 0, + end: 2*Math.PI, + ease: EaseFunctionType.IN_OUT_QUAD + } + ] + }); + this.receiver.subscribe([MarioEvents.PLAYER_HIT_COIN, MarioEvents.PLAYER_HIT_COIN_BLOCK]); this.viewport.follow(this.player); + this.viewport.enableZoom(); + this.viewport.setZoomLevel(2); // Add enemies - for(let pos of [{x: 21, y: 18}, {x: 30, y: 18}, {x: 37, y: 18}, {x: 41, y: 18}, {x: 105, y: 8}, {x: 107, y: 8}, {x: 125, y: 18}]){ - let goomba = this.add.sprite("goomba", "Main"); - goomba.position.set(pos.x*64, pos.y*64); - goomba.scale.set(2, 2); - goomba.addPhysics(); - goomba.addAI(GoombaController, {jumpy: false}); - goomba.setPhysicsLayer("enemy"); - } + // for(let pos of [{x: 21, y: 18}, {x: 30, y: 18}, {x: 37, y: 18}, {x: 41, y: 18}, {x: 105, y: 8}, {x: 107, y: 8}, {x: 125, y: 18}]){ + // let goomba = this.add.sprite("goomba", "Main"); + // goomba.position.set(pos.x*64, pos.y*64); + // goomba.scale.set(2, 2); + // goomba.addPhysics(); + // goomba.addAI(GoombaController, {jumpy: false}); + // goomba.setPhysicsLayer("enemy"); + // } - for(let pos of [{x: 67, y: 18}, {x: 86, y: 21}, {x: 128, y: 18}]){ - let koopa = this.add.sprite("koopa", "Main"); - koopa.position.set(pos.x*64, pos.y*64); - koopa.scale.set(2, 2); - koopa.addPhysics(); - koopa.addAI(GoombaController, {jumpy: true}); - koopa.setPhysicsLayer("enemy"); - } + // for(let pos of [{x: 67, y: 18}, {x: 86, y: 21}, {x: 128, y: 18}]){ + // let koopa = this.add.sprite("koopa", "Main"); + // koopa.position.set(pos.x*64, pos.y*64); + // koopa.scale.set(2, 2); + // koopa.addPhysics(); + // koopa.addAI(GoombaController, {jumpy: true}); + // koopa.setPhysicsLayer("enemy"); + // } // Add UI this.addUILayer("UI"); @@ -106,9 +122,10 @@ export default class Level1 extends Scene { } } + Debug.log("playerpos", this.player.position.toString()); // If player falls into a pit, kill them off and reset their position - if(this.player.position.y > 21*64){ - this.player.position.set(192, 1152); + if(this.player.position.y > 100*64){ + this.player.position.set(5*32, 18*32); this.livesCount -= 1 this.livesCountLabel.setText("Lives: " + this.livesCount); } diff --git a/src/_DemoClasses/Mario/MainMenu.ts b/src/_DemoClasses/Mario/MainMenu.ts index 3bed050..d83ea1f 100644 --- a/src/_DemoClasses/Mario/MainMenu.ts +++ b/src/_DemoClasses/Mario/MainMenu.ts @@ -1,16 +1,18 @@ import Vec2 from "../../DataTypes/Vec2"; import Debug from "../../Debug/Debug"; import InputReceiver from "../../Input/InputReceiver"; +import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite"; import Button from "../../Nodes/UIElements/Button"; -import Label from "../../Nodes/UIElements/Label"; -import Slider from "../../Nodes/UIElements/Slider"; import { UIElementType } from "../../Nodes/UIElements/UIElementTypes"; import Scene from "../../Scene/Scene"; import Color from "../../Utils/Color"; +import { EaseFunctionType } from "../../Utils/EaseFunctions"; import Level1 from "./Level1"; export default class MainMenu extends Scene { + animatedSprite: AnimatedSprite; + loadScene(): void { this.load.spritesheet("walker", "assets/spritesheets/walking.json"); } @@ -47,6 +49,44 @@ export default class MainMenu extends Scene { animatedSprite.scale.set(4, 4); animatedSprite.animation.play("JUMP"); animatedSprite.animation.queue("WALK", true); + + animatedSprite.tweens.add("wiggle", { + startDelay: 0, + duration: 300, + effects: [{ + property: "rotation", + start: -0.1, + end: 0.1, + ease: EaseFunctionType.IN_OUT_SINE + }], + reverseOnComplete: true, + loop: true + }); + + animatedSprite.tweens.play("wiggle"); + + animatedSprite.tweens.add("scale", { + startDelay: 0, + duration: 1000, + effects: [{ + property: "scaleX", + start: 4, + end: 6, + ease: EaseFunctionType.IN_OUT_SINE + }, + { + property: "scaleY", + start: 4, + end: 6, + ease: EaseFunctionType.IN_OUT_SINE + }], + reverseOnComplete: true, + loop: true + }); + + animatedSprite.tweens.play("scale"); + + this.animatedSprite = animatedSprite; } updateScene(): void { diff --git a/src/_DemoClasses/Player/PlayerController.ts b/src/_DemoClasses/Player/PlayerController.ts index e601fd8..5164618 100644 --- a/src/_DemoClasses/Player/PlayerController.ts +++ b/src/_DemoClasses/Player/PlayerController.ts @@ -27,9 +27,9 @@ export enum PlayerStates { export default class PlayerController extends StateMachineAI { protected owner: GameNode; velocity: Vec2 = Vec2.ZERO; - speed: number = 400; - MIN_SPEED: number = 400; - MAX_SPEED: number = 1000; + speed: number = 200; + MIN_SPEED: number = 200; + MAX_SPEED: number = 500; tilemap: OrthogonalTilemap; initializeAI(owner: GameNode, options: Record){ diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts index d278084..ae9c08e 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Idle.ts @@ -1,10 +1,14 @@ +import AnimatedSprite from "../../../../Nodes/Sprites/AnimatedSprite"; import OnGround from "./OnGround"; import { PlayerStates } from "./PlayerController"; import PlayerState from "./PlayerState"; export default class Idle extends OnGround { + owner: AnimatedSprite; + onEnter(): void { this.parent.speed = this.parent.MIN_SPEED; + this.owner.animation.play("IDLE", true); } update(deltaT: number): void { @@ -24,4 +28,8 @@ export default class Idle extends OnGround { this.owner.move(this.parent.velocity.scaled(deltaT)); } + + onExit(): void { + this.owner.animation.stop(); + } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts index 18b3b83..4f3d182 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts @@ -1,5 +1,6 @@ import Vec2 from "../../../../DataTypes/Vec2"; import GameEvent from "../../../../Events/GameEvent"; +import AnimatedSprite from "../../../../Nodes/Sprites/AnimatedSprite"; import MathUtils from "../../../../Utils/MathUtils"; import { CustomGameEventType } from "../../../CustomGameEventType"; import Level1, { MarioEvents } from "../../../Mario/Level1"; @@ -7,8 +8,11 @@ import { PlayerStates } from "./PlayerController"; import PlayerState from "./PlayerState"; export default class Jump extends PlayerState { + owner: AnimatedSprite; - onEnter(): void {} + onEnter(): void { + this.owner.animation.play("JUMP", true); + } handleInput(event: GameEvent): void {} @@ -27,8 +31,8 @@ export default class Jump extends PlayerState { console.log("Hit tile: " + tile); // If coin block, change to empty coin block - if(tile === 4){ - this.parent.tilemap.setTileAtRowCol(pos, 12); + if(tile === 17){ + this.parent.tilemap.setTileAtRowCol(pos, 18); this.emitter.fireEvent(MarioEvents.PLAYER_HIT_COIN_BLOCK); } } @@ -49,5 +53,7 @@ export default class Jump extends PlayerState { this.owner.move(this.parent.velocity.scaled(deltaT)); } - onExit(): void {} + onExit(): void { + this.owner.animation.stop(); + } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts index 9197514..dcd4a1e 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/OnGround.ts @@ -1,4 +1,6 @@ import GameEvent from "../../../../Events/GameEvent"; +import Sprite from "../../../../Nodes/Sprites/Sprite"; +import MathUtils from "../../../../Utils/MathUtils"; import { CustomGameEventType } from "../../../CustomGameEventType"; import PlayerState from "./PlayerState"; @@ -13,9 +15,18 @@ export default class OnGround extends PlayerState { } super.update(deltaT); + let direction = this.getInputDirection(); + + if(direction.x !== 0){ + (this.owner).invertX = MathUtils.sign(direction.x) < 0; + } + if(this.input.isJustPressed("w") || this.input.isJustPressed("space")){ this.finished("jump"); - this.parent.velocity.y = -2000; + this.parent.velocity.y = -500; + if(this.parent.velocity.x !== 0){ + this.owner.tweens.play("flip"); + } this.emitter.fireEvent(CustomGameEventType.PLAYER_JUMP) } else if(!this.owner.onGround){ this.finished("jump"); diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts index 7522928..08e5d44 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts @@ -1,15 +1,19 @@ import State from "../../../../DataTypes/State/State"; import StateMachine from "../../../../DataTypes/State/StateMachine"; import Vec2 from "../../../../DataTypes/Vec2"; +import Debug from "../../../../Debug/Debug"; import InputReceiver from "../../../../Input/InputReceiver"; +import CanvasNode from "../../../../Nodes/CanvasNode"; import GameNode from "../../../../Nodes/GameNode"; +import Sprite from "../../../../Nodes/Sprites/Sprite"; +import MathUtils from "../../../../Utils/MathUtils"; import PlayerController from "../../PlayerController"; export default abstract class PlayerState extends State { input: InputReceiver = InputReceiver.getInstance(); owner: GameNode; - gravity: number = 7000; + gravity: number = 1000; parent: PlayerController; constructor(parent: StateMachine, owner: GameNode){ diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts index 022f0d1..df081a6 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Run.ts @@ -1,10 +1,15 @@ +import AnimatedSprite from "../../../../Nodes/Sprites/AnimatedSprite"; +import MathUtils from "../../../../Utils/MathUtils"; import { CustomGameEventType } from "../../../CustomGameEventType"; import OnGround from "./OnGround"; import { PlayerStates } from "./PlayerController"; export default class Run extends OnGround { + owner: AnimatedSprite; + onEnter(): void { this.parent.speed = this.parent.MAX_SPEED; + this.owner.animation.play("WALK", true); } update(deltaT: number): void { @@ -25,4 +30,8 @@ export default class Run extends OnGround { this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()}); this.owner.move(this.parent.velocity.scaled(deltaT)); } + + onExit(): void { + this.owner.animation.stop(); + } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts index b0ffa29..4ce8b91 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Walk.ts @@ -1,10 +1,14 @@ +import AnimatedSprite from "../../../../Nodes/Sprites/AnimatedSprite"; import { CustomGameEventType } from "../../../CustomGameEventType"; import OnGround from "./OnGround"; import { PlayerStates } from "./PlayerController"; export default class Walk extends OnGround { + owner: AnimatedSprite; + onEnter(): void { this.parent.speed = this.parent.MAX_SPEED/2; + this.owner.animation.play("WALK", true); } update(deltaT: number): void { @@ -25,4 +29,8 @@ export default class Walk extends OnGround { this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()}); this.owner.move(this.parent.velocity.scaled(deltaT)); } + + onExit(): void { + this.owner.animation.stop(); + } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 67c22a2..da87324 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ function main(){ // Create the game object let options = { viewportSize: {x: 800, y: 600}, + clearColor: {r: 34, g: 32, b: 52} } let game = new GameLoop(options); diff --git a/tsconfig.json b/tsconfig.json index 092b94a..4f2bfab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -59,6 +59,6 @@ ], "compilerOptions": { "noImplicitAny": true, - "target": "es5" + "target": "es2016" } } \ No newline at end of file