added tween animations

This commit is contained in:
Joe Weaver 2020-11-29 19:49:04 -05:00
parent 32d63ea2bf
commit 5bf6e96778
28 changed files with 488 additions and 110 deletions

View File

@ -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<string, any>): 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;
}

View File

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

View File

@ -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;
debug_render = (ctx: CanvasRenderingContext2D): void => {};
update(deltaT: number): void {
this.tweens.update(deltaT);
}
}
export enum TweenableProperties{
posX = "positionX",
posY = "positionY",
scaleX = "scaleX",
scaleY = "scaleY",
rotation = "rotation"
}

View File

@ -8,6 +8,4 @@ export default class Point extends Graphic {
this.position = position;
this.size.set(5, 5);
}
update(deltaT: number): void {}
}

View File

@ -38,6 +38,4 @@ export default class Rect extends Graphic {
getBorderWidth(): number {
return this.borderWidth;
}
update(deltaT: number): void {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<TweenData>;
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<string, any> | TweenData): void {
let typedTween = <TweenData>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;
}
}
}
});
}
}

View File

@ -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(<AnimatedSprite>node);
} else if(node instanceof Sprite){
@ -88,18 +110,14 @@ export default class CanvasRenderer extends RenderingManager {
} else if(node instanceof UIElement){
this.renderUIElement(<UIElement>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(<Point>graphic);
this.graphicRenderer.renderPoint(<Point>graphic, this.origin, this.zoom);
} else if(graphic instanceof Rect){
this.graphicRenderer.renderRect(<Rect>graphic);
this.graphicRenderer.renderRect(<Rect>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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, any>){

View File

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

View File

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

View File

@ -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){
(<Sprite>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");

View File

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

View File

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

View File

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

View File

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

View File

@ -59,6 +59,6 @@
],
"compilerOptions": {
"noImplicitAny": true,
"target": "es5"
"target": "es2016"
}
}