added tween features

This commit is contained in:
Joe Weaver 2020-12-01 14:04:24 -05:00
parent 5bf6e96778
commit b8849b4c84
18 changed files with 191 additions and 49 deletions

View File

@ -55,6 +55,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
protected layer: Layer;
tweens: TweenManager;
rotation: number;
alpha: number;
constructor(){
this.input = InputReceiver.getInstance();
@ -64,6 +65,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
this.emitter = new Emitter();
this.tweens = new TweenManager(this);
this.rotation = 0;
this.alpha = 1;
}
/*---------- POSITIONED ----------*/
@ -254,5 +256,6 @@ export enum TweenableProperties{
posY = "positionY",
scaleX = "scaleX",
scaleY = "scaleY",
rotation = "rotation"
rotation = "rotation",
alpha = "alpha"
}

View File

@ -21,6 +21,8 @@ export class TweenData {
/** An array of the effects on the properties of the object */
effects: [{
property: TweenableProperties;
resetOnComplete: boolean;
initialValue: number;
start: any;
end: any;
ease: EaseFunctionType;

View File

@ -44,6 +44,13 @@ export default class TweenManager {
tween.loop = loop;
}
// Set the initial values
for(let effect of tween.effects){
if(effect.resetOnComplete){
effect.initialValue = this.owner[effect.property];
}
}
// Start the tween running
tween.animationState = AnimationState.PLAYING;
tween.elapsedTime = 0;
@ -80,7 +87,15 @@ export default class TweenManager {
*/
stop(key: string): void {
if(this.tweens.has(key)){
this.tweens.get(key).animationState = AnimationState.STOPPED;
let tween = this.tweens.get(key);
tween.animationState = AnimationState.STOPPED;
// Return to the initial values
for(let effect of tween.effects){
if(effect.resetOnComplete){
this.owner[effect.property] = effect.initialValue;
}
}
}
}
@ -103,7 +118,7 @@ export default class TweenManager {
tween.elapsedTime -= tween.duration;
} else {
// We aren't looping and can't reverse, so stop
tween.animationState = AnimationState.STOPPED;
this.stop(key);
}
}
@ -113,7 +128,7 @@ export default class TweenManager {
tween.reversing = false;
tween.elapsedTime -= 2*tween.duration;
} else {
tween.animationState = AnimationState.STOPPED;
this.stop(key);
}
}

View File

@ -66,19 +66,41 @@ export default class CanvasRenderer extends RenderingManager {
}
});
// Render tilemaps
tilemaps.forEach(tilemap => {
this.renderTilemap(tilemap);
});
let tilemapIndex = 0;
let tilemapLength = tilemaps.length;
// Render visible set
visibleSet.forEach(node => {
let visibleSetIndex = 0;
let visibleSetLength = visibleSet.length;
while(tilemapIndex < tilemapLength || visibleSetIndex < visibleSetLength){
// Check conditions where we've already reached the edge of one list
if(tilemapIndex >= tilemapLength){
// Only render the remaining visible set
let node = visibleSet[visibleSetIndex++];
if(node.visible){
this.renderNode(node);
}
});
continue;
}
// Render the uiLayers
if(visibleSetIndex >= visibleSetLength){
// Only render tilemaps
this.renderTilemap(tilemaps[tilemapIndex++]);
continue;
}
// Render whichever is further down
if(tilemaps[tilemapIndex].getLayer().getDepth() <= visibleSet[visibleSetIndex].getLayer().getDepth()){
this.renderTilemap(tilemaps[tilemapIndex++]);
} else {
let node = visibleSet[visibleSetIndex++];
if(node.visible){
this.renderNode(node);
}
}
}
// Render the uiLayers on top of everything else
uiLayers.forEach(key => uiLayers.get(key).getItems().forEach(node => this.renderNode(<CanvasNode>node)));
}
@ -100,6 +122,8 @@ export default class CanvasRenderer extends RenderingManager {
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);
let globalAlpha = this.ctx.globalAlpha;
this.ctx.globalAlpha = node.alpha;
if(node instanceof AnimatedSprite){
this.renderAnimatedSprite(<AnimatedSprite>node);
@ -111,6 +135,7 @@ export default class CanvasRenderer extends RenderingManager {
this.renderUIElement(<UIElement>node);
}
this.ctx.globalAlpha = globalAlpha;
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
}

View File

@ -107,7 +107,11 @@ export default class ResourceManager {
* @param key The key of the loaded image
*/
public getImage(key: string): HTMLImageElement {
return this.images.get(key);
let image = this.images.get(key);
if(image === undefined){
throw `There is no image associated with key "${key}"`
}
return image;
}
public spritesheet(key: string, path: string): void {

View File

@ -13,6 +13,8 @@ import Slider from "../../Nodes/UIElements/Slider";
import TextInput from "../../Nodes/UIElements/TextInput";
import Rect from "../../Nodes/Graphics/Rect";
import ResourceManager from "../../ResourceManager/ResourceManager";
import UILayer from "../Layers/UILayer";
import ParallaxLayer from "../Layers/ParallaxLayer";
export default class CanvasNodeFactory {
protected scene: Scene;
@ -75,7 +77,10 @@ export default class CanvasNodeFactory {
// Add instance to scene
instance.setScene(this.scene);
instance.id = this.scene.generateId();
if(!(this.scene.isParallaxLayer(layerName) || this.scene.isUILayer(layerName))){
this.scene.getSceneGraph().addNode(instance);
}
// Add instance to layer
layer.addNode(instance);
@ -91,7 +96,10 @@ export default class CanvasNodeFactory {
// Add instance fo scene
instance.setScene(this.scene);
instance.id = this.scene.generateId();
if(!(this.scene.isParallaxLayer(layerName) || this.scene.isUILayer(layerName))){
this.scene.getSceneGraph().addNode(instance);
}
// Add instance to layer
layer.addNode(instance);

View File

@ -180,7 +180,7 @@ export default class Scene implements Updateable, Renderable {
* @param depth The depth of the layer
*/
addLayer(name: string, depth?: number): Layer {
if(this.layers.has(name) || this.uiLayers.has(name)){
if(this.layers.has(name) || this.parallaxLayers.has(name) || this.uiLayers.has(name)){
throw `Layer with name ${name} already exists`;
}
@ -202,13 +202,13 @@ export default class Scene implements Updateable, Renderable {
* @param depth The depth of the layer
*/
addParallaxLayer(name: string, parallax: Vec2, depth?: number): ParallaxLayer {
if(this.layers.has(name) || this.uiLayers.has(name)){
if(this.layers.has(name) || this.parallaxLayers.has(name) || this.uiLayers.has(name)){
throw `Layer with name ${name} already exists`;
}
let layer = new ParallaxLayer(this, name, parallax);
this.layers.add(name, layer);
this.parallaxLayers.add(name, layer);
if(depth){
layer.setDepth(depth);
@ -222,7 +222,7 @@ export default class Scene implements Updateable, Renderable {
* @param name The name of the new UIlayer
*/
addUILayer(name: string): UILayer {
if(this.layers.has(name) || this.uiLayers.has(name)){
if(this.layers.has(name) || this.parallaxLayers.has(name) || this.uiLayers.has(name)){
throw `Layer with name ${name} already exists`;
}

View File

@ -82,7 +82,7 @@ export default class MathUtils {
factor *= 16;
}
let hexStr = "";
while(num > 0){
while(factor >= 1){
let digit = Math.floor(num/factor);
hexStr += MathUtils.toHexDigit(digit);
num -= digit * factor;

View File

@ -31,6 +31,7 @@ export default class GoombaController extends StateMachineAI {
this.receiver.subscribe("playerHitCoinBlock");
if(this.jumpy){
this.receiver.subscribe(CustomGameEventType.PLAYER_JUMP);
this.speed = 100;
}
let idle = new Idle(this, owner);

View File

@ -6,7 +6,7 @@ import GoombaController, { GoombaStates } from "./GoombaController";
export default abstract class GoombaState extends State {
owner: GameNode;
gravity: number = 7000;
gravity: number = 1000;
parent: GoombaController
constructor(parent: StateMachine, owner: GameNode){

View File

@ -1,5 +1,6 @@
import Vec2 from "../../DataTypes/Vec2";
import GameEvent from "../../Events/GameEvent";
import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite";
import { CustomGameEventType } from "../CustomGameEventType";
import { GoombaStates } from "./GoombaController";
import OnGround from "./OnGround";
@ -7,6 +8,11 @@ import OnGround from "./OnGround";
export default class Idle extends OnGround {
onEnter(): void {
this.parent.speed = this.parent.speed;
(<AnimatedSprite>this.owner).animation.play("IDLE", true);
}
onExit(): void {
(<AnimatedSprite>this.owner).animation.stop();
}
handleInput(event: GameEvent) {

View File

@ -1,10 +1,15 @@
import GameEvent from "../../Events/GameEvent";
import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite";
import { GoombaStates } from "./GoombaController";
import GoombaState from "./GoombaState";
export default class Jump extends GoombaState {
onEnter(): void {}
onEnter(): void {
(<AnimatedSprite>this.owner).animation.play("JUMP", true);
(<AnimatedSprite>this.owner).tweens.play("jump", true);
this.gravity = 500;
}
update(deltaT: number): void {
super.update(deltaT);
@ -22,5 +27,8 @@ export default class Jump extends GoombaState {
this.owner.move(this.parent.velocity.scaled(deltaT));
}
onExit(): void {}
onExit(): void {
(<AnimatedSprite>this.owner).animation.stop();
(<AnimatedSprite>this.owner).tweens.stop("jump");
}
}

View File

@ -1,4 +1,5 @@
import Vec2 from "../../DataTypes/Vec2";
import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite";
import { GoombaStates } from "./GoombaController";
import OnGround from "./OnGround";
@ -8,8 +9,11 @@ export default class Walk extends OnGround {
onEnter(): void {
if(this.parent.direction.isZero()){
this.parent.direction = new Vec2(-1, 0);
(<AnimatedSprite>this.owner).invertX = true;
}
(<AnimatedSprite>this.owner).animation.play("WALK", true);
this.time = Date.now();
}
@ -19,16 +23,22 @@ export default class Walk extends OnGround {
if(this.owner.onWall){
// Flip around
this.parent.direction.x *= -1;
(<AnimatedSprite>this.owner).invertX = !(<AnimatedSprite>this.owner).invertX;
}
if(this.parent.jumpy && (Date.now() - this.time > 500)){
console.log("Jump");
this.finished(GoombaStates.JUMP);
this.parent.velocity.y = -2000;
this.parent.velocity.y = -300;
}
this.parent.velocity.x = this.parent.direction.x * this.parent.speed;
this.owner.move(this.parent.velocity.scaled(deltaT));
}
onExit(): void {
(<AnimatedSprite>this.owner).animation.stop();
}
}

View File

@ -11,6 +11,7 @@ import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap";
import AnimatedSprite from "../../Nodes/Sprites/AnimatedSprite";
import Debug from "../../Debug/Debug";
import { EaseFunctionType } from "../../Utils/EaseFunctions";
import Sprite from "../../Nodes/Sprites/Sprite";
export enum MarioEvents {
PLAYER_HIT_COIN = "PlayerHitCoin",
@ -23,20 +24,34 @@ export default class Level1 extends Scene {
coinCountLabel: Label;
livesCount: number = 3;
livesCountLabel: Label;
bg: Sprite;
loadScene(): void {
this.load.image("background", "/assets/sprites/2bitbackground.png");
this.load.image("coin", "/assets/sprites/coin.png");
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");
this.load.spritesheet("hopper", "assets/spritesheets/hopper.json");
this.load.spritesheet("bunny", "assets/spritesheets/ghostBunny.json");
}
startScene(): void {
// Add a background layer and set the background image on it
this.addParallaxLayer("bg", new Vec2(0.25, 0), -100);
let bg = this.add.sprite("background", "bg");
bg.scale.set(2, 2);
bg.position.set(bg.boundary.halfSize.x, 16);
this.bg = bg;
this.bg.toString = () => "BackgroundImage";
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 a layer behind the tilemap for coin animation
this.addLayer("coinLayer", -50);
// 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.animatedSprite("player", "Main");
@ -70,23 +85,37 @@ export default class Level1 extends Scene {
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 bunny = this.add.animatedSprite("bunny", "Main");
bunny.position.set(pos.x*32, pos.y*32);
bunny.scale.set(2, 2);
bunny.addPhysics();
bunny.addAI(GoombaController, {jumpy: false});
bunny.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 hopper = this.add.animatedSprite("hopper", "Main");
hopper.position.set(pos.x*32, pos.y*32);
hopper.scale.set(2, 2);
hopper.addPhysics();
hopper.addAI(GoombaController, {jumpy: true});
hopper.setPhysicsLayer("enemy");
hopper.tweens.add("jump", {
startDelay: 0,
duration: 300,
effects: [
{
property: "rotation",
resetOnComplete: true,
start: -3.14/8,
end: 3.14/8,
ease: EaseFunctionType.IN_OUT_SINE
}
],
reverseOnComplete: true,
});
}
// Add UI
this.addUILayer("UI");
@ -96,6 +125,8 @@ export default class Level1 extends Scene {
}
updateScene(deltaT: number): void {
Debug.log("pos", this.bg.position);
while(this.receiver.hasNextEvent()){
let event = this.receiver.getNextEvent();

View File

@ -2,7 +2,9 @@ import StateMachineAI from "../../AI/StateMachineAI";
import Vec2 from "../../DataTypes/Vec2";
import Debug from "../../Debug/Debug";
import GameNode from "../../Nodes/GameNode";
import Sprite from "../../Nodes/Sprites/Sprite";
import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap";
import { TweenData } from "../../Rendering/Animations/AnimationTypes";
import IdleTopDown from "./PlayerStates/IdleTopDown";
import MoveTopDown from "./PlayerStates/MoveTopDown";
import Idle from "./PlayerStates/Platformer/Idle";
@ -29,8 +31,9 @@ export default class PlayerController extends StateMachineAI {
velocity: Vec2 = Vec2.ZERO;
speed: number = 200;
MIN_SPEED: number = 200;
MAX_SPEED: number = 500;
MAX_SPEED: number = 300;
tilemap: OrthogonalTilemap;
coin: Sprite;
initializeAI(owner: GameNode, options: Record<string, any>){
this.owner = owner;
@ -42,6 +45,8 @@ export default class PlayerController extends StateMachineAI {
}
this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap;
this.coin = this.owner.getScene().add.sprite("coin", "coinLayer");
this.coin.scale.set(2, 2);
}
/**

View File

@ -1,6 +1,7 @@
import Vec2 from "../../../../DataTypes/Vec2";
import GameEvent from "../../../../Events/GameEvent";
import AnimatedSprite from "../../../../Nodes/Sprites/AnimatedSprite";
import { EaseFunctionType } from "../../../../Utils/EaseFunctions";
import MathUtils from "../../../../Utils/MathUtils";
import { CustomGameEventType } from "../../../CustomGameEventType";
import Level1, { MarioEvents } from "../../../Mario/Level1";
@ -28,12 +29,35 @@ export default class Jump extends PlayerState {
pos = this.parent.tilemap.getColRowAt(pos);
let tile = this.parent.tilemap.getTileAtRowCol(pos);
console.log("Hit tile: " + tile);
// If coin block, change to empty coin block
if(tile === 17){
this.parent.tilemap.setTileAtRowCol(pos, 18);
this.emitter.fireEvent(MarioEvents.PLAYER_HIT_COIN_BLOCK);
let tileSize = this.parent.tilemap.getTileSize();
this.parent.coin.position.copy(pos.scale(tileSize.x, tileSize.y).add(tileSize.scaled(0.5)));
// Animate collision
this.parent.coin.tweens.add("coin", {
startDelay: 0,
duration: 300,
effects: [{
property: "positionY",
resetOnComplete: false,
start: this.parent.coin.position.y,
end: this.parent.coin.position.y - 2*tileSize.y,
ease: EaseFunctionType.OUT_SINE
},
{
property: "alpha",
resetOnComplete: false,
start: 1,
end: 0,
ease: EaseFunctionType.OUT_SINE
}]
});
this.parent.coin.tweens.play("coin");
}
}

View File

@ -20,7 +20,7 @@ export default class PlayerController extends StateMachineAI {
velocity: Vec2 = Vec2.ZERO;
speed: number = 400;
MIN_SPEED: number = 400;
MAX_SPEED: number = 1000;
MAX_SPEED: number = 400;
initializeAI(owner: GameNode, config: Record<string, any>): void {
this.owner = owner;

View File

@ -7,7 +7,7 @@ export default class Walk extends OnGround {
owner: AnimatedSprite;
onEnter(): void {
this.parent.speed = this.parent.MAX_SPEED/2;
this.parent.speed = this.parent.MIN_SPEED;
this.owner.animation.play("WALK", true);
}