added state machine demo and added some features

This commit is contained in:
Joe Weaver 2020-10-18 12:03:40 -04:00
parent 25e0b8a39e
commit 7a0f9e5c95
25 changed files with 598 additions and 8 deletions

View File

@ -102,8 +102,8 @@ export default class Tileset {
let top = row * height; let top = row * height;
// Calculate the position in the world to render the tile // Calculate the position in the world to render the tile
let x = (dataIndex % worldSize.x) * width * scale.x; let x = Math.floor((dataIndex % worldSize.x) * width * scale.x);
let y = Math.floor(dataIndex / worldSize.x) * height * scale.y; let y = Math.floor(Math.floor(dataIndex / worldSize.x) * height * scale.y);
ctx.drawImage(image, left, top, width, height, x - origin.x, y - origin.y, width * scale.x, height * scale.y); ctx.drawImage(image, left, top, width, height, x - origin.x, y - origin.y, width * scale.x, height * scale.y);
} }
} }

View File

@ -26,6 +26,10 @@ export default class GameEvent {
this.time = Date.now(); this.time = Date.now();
} }
isType(type: string): boolean {
return this.type === type;
}
toString(): string { toString(): string {
return this.type + ": @" + this.time; return this.type + ": @" + this.time;
} }

View File

@ -68,7 +68,11 @@ export default class InputReceiver{
} }
if(event.type === GameEventType.KEY_DOWN){ if(event.type === GameEventType.KEY_DOWN){
let key = event.data.get("key") let key = event.data.get("key");
// Handle space bar
if(key === " "){
key = "space";
}
if(!this.keyPressed.get(key)){ if(!this.keyPressed.get(key)){
this.keyJustPressed.set(key, true); this.keyJustPressed.set(key, true);
this.keyPressed.set(key, true); this.keyPressed.set(key, true);
@ -76,7 +80,11 @@ export default class InputReceiver{
} }
if(event.type === GameEventType.KEY_UP){ if(event.type === GameEventType.KEY_UP){
let key = event.data.get("key") let key = event.data.get("key");
// Handle space bar
if(key === " "){
key = "space";
}
this.keyPressed.set(key, false); this.keyPressed.set(key, false);
} }

View File

@ -61,6 +61,8 @@ export default class MainScene extends Scene {
let player = this.add.physics(Player, mainLayer, "platformer"); let player = this.add.physics(Player, mainLayer, "platformer");
let playerSprite = this.add.sprite("player", mainLayer) let playerSprite = this.add.sprite("player", mainLayer)
player.setSprite(playerSprite); player.setSprite(playerSprite);
playerSprite.position = player.position.clone();
playerSprite.setSize(new Vec2(64, 64));
this.viewport.follow(player); this.viewport.follow(player);

View File

@ -144,7 +144,17 @@ export default class PhysicsManager {
// TODO - This is a bug, check to make sure our velocity is going downwards // TODO - This is a bug, check to make sure our velocity is going downwards
// Maybe feed in a downward direction to check to be sure // Maybe feed in a downward direction to check to be sure
if(yScale !== 1){ if(yScale !== 1){
node.setGrounded(true); // If the collider is below us
if(collision.collider.getPosition().y > node.position.y){
node.setGrounded(true);
} else {
console.log("On ceiling")
node.setOnCeiling(true);
}
}
if(xScale !== 1){
node.setOnWall(true);
} }
// Scale the velocity of the node // Scale the velocity of the node
@ -175,7 +185,16 @@ export default class PhysicsManager {
// TODO - This is a bug, check to make sure our velocity is going downwards // TODO - This is a bug, check to make sure our velocity is going downwards
// Maybe feed in a downward direction to check to be sure // Maybe feed in a downward direction to check to be sure
if(yScale !== 1){ if(yScale !== 1){
movingNode.setGrounded(true); // If the collider is below us
if(staticNode.position.y > movingNode.position.y){
movingNode.setGrounded(true);
} else {
movingNode.setOnCeiling(true);
}
}
if(xScale !== 1){
movingNode.setOnWall(true);
} }
// Scale the velocity of the node // Scale the velocity of the node
@ -207,6 +226,8 @@ export default class PhysicsManager {
// For now, we will only have the moving player, don't bother checking for collisions with other moving things // For now, we will only have the moving player, don't bother checking for collisions with other moving things
for(let movingNode of dynamicSet){ for(let movingNode of dynamicSet){
movingNode.setGrounded(false); movingNode.setGrounded(false);
movingNode.setOnCeiling(false);
movingNode.setOnWall(false);
// Get velocity of node // Get velocity of node
let velocity = null; let velocity = null;
for(let data of this.movements){ for(let data of this.movements){

View File

@ -14,11 +14,15 @@ export default abstract class PhysicsNode extends GameNode {
private manager: PhysicsManager; private manager: PhysicsManager;
protected moving: boolean; protected moving: boolean;
protected grounded: boolean; protected grounded: boolean;
protected onCeiling: boolean;
protected onWall: boolean;
constructor(){ constructor(){
super(); super();
this.children = new Array(); this.children = new Array();
this.grounded = false; this.grounded = false;
this.onCeiling = false;
this.onWall = false;
this.moving = false; this.moving = false;
} }
@ -26,6 +30,26 @@ export default abstract class PhysicsNode extends GameNode {
this.grounded = grounded; this.grounded = grounded;
} }
isGrounded(): boolean {
return this.grounded;
}
setOnCeiling(onCeiling: boolean): void {
this.onCeiling = onCeiling;
}
isOnCeiling(): boolean {
return this.onCeiling;
}
setOnWall(onWall: boolean): void {
this.onWall = onWall;
}
isOnWall(): boolean {
return this.onWall;
}
addManager(manager: PhysicsManager): void { addManager(manager: PhysicsManager): void {
this.manager = manager; this.manager = manager;
} }
@ -54,7 +78,7 @@ export default abstract class PhysicsNode extends GameNode {
* Register a movement to the physics manager that can be handled at the end of the frame * Register a movement to the physics manager that can be handled at the end of the frame
* @param velocity * @param velocity
*/ */
protected move(velocity: Vec2): void { move(velocity: Vec2): void {
this.moving = true; this.moving = true;
this.manager.addMovement(this, velocity); this.manager.addMovement(this, velocity);
} }

View File

@ -158,6 +158,12 @@ export default class Viewport {
pos.x = MathUtils.clamp(pos.x, this.boundary.left + this.view.hw, this.boundary.right - this.view.hw); pos.x = MathUtils.clamp(pos.x, this.boundary.left + this.view.hw, this.boundary.right - this.view.hw);
pos.y = MathUtils.clamp(pos.y, this.boundary.top + this.view.hh, this.boundary.bottom - this.view.hh); pos.y = MathUtils.clamp(pos.y, this.boundary.top + this.view.hh, this.boundary.bottom - this.view.hh);
// Assure there are no lines in the tilemap
pos.x = Math.floor(pos.x);
pos.y = Math.floor(pos.y);
Debug.log("vp", "Viewport pos: " + pos.toString())
this.view.setCenter(pos); this.view.setCenter(pos);
} else { } else {
if(this.lastPositions.getSize() > this.smoothingFactor){ if(this.lastPositions.getSize() > this.smoothingFactor){
@ -172,6 +178,11 @@ export default class Viewport {
pos.x = MathUtils.clamp(pos.x, this.boundary.left + this.view.hw, this.boundary.right - this.view.hw); pos.x = MathUtils.clamp(pos.x, this.boundary.left + this.view.hw, this.boundary.right - this.view.hw);
pos.y = MathUtils.clamp(pos.y, this.boundary.top + this.view.hh, this.boundary.bottom - this.view.hh); pos.y = MathUtils.clamp(pos.y, this.boundary.top + this.view.hh, this.boundary.bottom - this.view.hh);
// Assure there are no lines in the tilemap
pos.x = Math.floor(pos.x);
pos.y = Math.floor(pos.y);
Debug.log("vp", "Viewport pos: " + pos.toString())
this.view.setCenter(pos); this.view.setCenter(pos);
} }
} }

View File

@ -1,3 +1,4 @@
export enum CustomGameEventType { export enum CustomGameEventType {
PLAYER_MOVE = "player_move", PLAYER_MOVE = "player_move",
PLAYER_JUMP = "player_jump",
} }

View File

@ -0,0 +1,58 @@
import StateMachine from "../../DataTypes/State/StateMachine";
import { CustomGameEventType } from "../CustomGameEventType";
import Goomba from "../MarioClone/Goomba";
import Idle from "../Enemies/Idle";
import Jump from "../Enemies/Jump";
import Walk from "../Enemies/Walk";
import Debug from "../../Debug/Debug";
export enum GoombaStates {
IDLE = "idle",
WALK = "walk",
JUMP = "jump",
PREVIOUS = "previous"
}
export default class GoombaController extends StateMachine {
owner: Goomba;
jumpy: boolean;
constructor(owner: Goomba, jumpy: boolean){
super();
this.owner = owner;
this.jumpy = jumpy;
this.receiver.subscribe(CustomGameEventType.PLAYER_MOVE);
if(this.jumpy){
this.receiver.subscribe(CustomGameEventType.PLAYER_JUMP);
}
let idle = new Idle(this, owner);
this.addState(GoombaStates.IDLE, idle);
let walk = new Walk(this, owner);
this.addState(GoombaStates.WALK, walk);
let jump = new Jump(this, owner);
this.addState(GoombaStates.JUMP, jump);
}
changeState(stateName: string): void {
if(stateName === GoombaStates.JUMP){
this.stack.push(this.stateMap.get(stateName));
}
super.changeState(stateName);
}
update(deltaT: number): void {
super.update(deltaT);
if(this.currentState instanceof Jump){
Debug.log("goombastate", "GoombaState: Jump");
} else if (this.currentState instanceof Walk){
Debug.log("goombastate", "GoombaState: Walk");
} else {
Debug.log("goombastate", "GoombaState: Idle");
}
}
}

View File

@ -0,0 +1,19 @@
import State from "../../DataTypes/State/State";
import StateMachine from "../../DataTypes/State/StateMachine";
import Goomba from "../MarioClone/Goomba";
export default abstract class GoombaState extends State {
owner: Goomba;
gravity: number = 7000;
constructor(parent: StateMachine, owner: Goomba){
super(parent);
this.owner = owner;
}
update(deltaT: number): void {
// Do gravity;
this.owner.velocity.y += this.gravity*deltaT;
}
}

View File

@ -0,0 +1,29 @@
import Vec2 from "../../DataTypes/Vec2";
import GameEvent from "../../Events/GameEvent";
import { CustomGameEventType } from "../CustomGameEventType";
import { GoombaStates } from "./GoombaController";
import OnGround from "./OnGround";
export default class Idle extends OnGround {
onEnter(): void {
this.owner.speed = this.owner.speed;
}
handleInput(event: GameEvent) {
if(event.type === CustomGameEventType.PLAYER_MOVE){
let pos = event.data.get("position");
if(this.owner.position.x - pos.x < (64*10)){
this.finished(GoombaStates.WALK);
}
}
super.handleInput(event);
}
update(deltaT: number): void {
super.update(deltaT);
this.owner.velocity.x = 0;
this.owner.move(this.owner.velocity.scaled(deltaT));
}
}

View File

@ -0,0 +1,28 @@
import GameEvent from "../../Events/GameEvent";
import { GoombaStates } from "./GoombaController";
import GoombaState from "./GoombaState";
export default class Jump extends GoombaState {
onEnter(): void {}
handleInput(event: GameEvent): void {}
update(deltaT: number): void {
super.update(deltaT);
if(this.owner.isGrounded()){
this.finished(GoombaStates.PREVIOUS);
}
if(this.owner.isOnCeiling()){
this.owner.velocity.y = 0;
}
this.owner.velocity.x += this.owner.direction.x * this.owner.speed/3.5 - 0.3*this.owner.velocity.x;
this.owner.move(this.owner.velocity.scaled(deltaT));
}
onExit(): void {}
}

View File

@ -0,0 +1,28 @@
import GameEvent from "../../Events/GameEvent";
import { CustomGameEventType } from "../CustomGameEventType";
import GoombaController, { GoombaStates } from "./GoombaController";
import GoombaState from "./GoombaState";
export default class OnGround extends GoombaState {
onEnter(): void {}
handleInput(event: GameEvent): void {
if(event.type === CustomGameEventType.PLAYER_JUMP && (<GoombaController>this.parentStateMachine).jumpy){
this.finished(GoombaStates.JUMP);
this.owner.velocity.y = -2000;
}
}
update(deltaT: number): void {
if(this.owner.velocity.y > 0){
this.owner.velocity.y = 0;
}
super.update(deltaT);
if(!this.owner.isGrounded()){
this.finished(GoombaStates.JUMP);
}
}
onExit(): void {}
}

View File

@ -0,0 +1,23 @@
import Vec2 from "../../DataTypes/Vec2";
import OnGround from "./OnGround";
export default class Walk extends OnGround {
onEnter(): void {
if(this.owner.direction.isZero()){
this.owner.direction = new Vec2(-1, 0);
}
}
update(deltaT: number): void {
super.update(deltaT);
if(this.owner.isOnWall()){
// Flip around
this.owner.direction.x *= -1;
}
this.owner.velocity.x = this.owner.direction.x * this.owner.speed;
this.owner.move(this.owner.velocity.scaled(deltaT));
}
}

View File

@ -0,0 +1,30 @@
import AABB from "../../DataTypes/AABB";
import Vec2 from "../../DataTypes/Vec2";
import Sprite from "../../Nodes/Sprites/Sprite";
import Collider from "../../Physics/Colliders/Collider";
import PhysicsNode from "../../Physics/PhysicsNode";
import GoombaController, { GoombaStates } from "../Enemies/GoombaController";
export default class Goomba extends PhysicsNode {
controller: GoombaController;
velocity: Vec2 = Vec2.ZERO;
speed: number = 200;
direction: Vec2 = Vec2.ZERO;
constructor(position: Vec2, canJump: boolean){
super();
this.position.copy(position);
this.velocity = Vec2.ZERO;
this.controller = new GoombaController(this, canJump);
this.controller.initialize(GoombaStates.IDLE);
this.collider = new Collider(new AABB(position, new Vec2(32, 32)))
}
create(): void {
}
update(deltaT: number): void {
this.controller.update(deltaT);
}
}

View File

@ -0,0 +1,35 @@
import Scene from "../../Scene/Scene";
import Rect from "../../Nodes/Graphics/Rect";
import Vec2 from "../../DataTypes/Vec2";
import Player from "./Player";
import Color from "../../Utils/Color";
import Goomba from "./Goomba";
export default class MarioClone extends Scene {
loadScene(): void {
this.load.tilemap("level", "assets/tilemaps/MarioClone.json");
this.load.image("goomba", "assets/sprites/Goomba.png");
}
startScene(): void {
let layer = this.addLayer();
this.add.tilemap("level", new Vec2(2, 2));
let player = this.add.physics(Player, layer, new Vec2(0, 0));
let playerSprite = this.add.graphic(Rect, layer, new Vec2(0, 0), new Vec2(64, 64));
playerSprite.setColor(Color.BLUE);
player.addChild(playerSprite);
this.viewport.follow(playerSprite);
this.viewport.setBounds(0, 0, 5120, 1280);
for(let xPos of [14, 20, 25, 30, 33, 37, 49, 55, 58, 70, 74]){
let goomba = this.add.physics(Goomba, layer, new Vec2(64*xPos, 0), true);
let goombaSprite = this.add.sprite("goomba", layer);
goombaSprite.setPosition(64*xPos, 0);
goombaSprite.setScale(new Vec2(2, 2));
goomba.addChild(goombaSprite);
}
}
}

View File

@ -0,0 +1,33 @@
import AABB from "../../DataTypes/AABB";
import Vec2 from "../../DataTypes/Vec2";
import Debug from "../../Debug/Debug";
import Collider from "../../Physics/Colliders/Collider";
import PhysicsNode from "../../Physics/PhysicsNode";
import PlayerController from "../Player/PlayerStates/Platformer/PlayerController";
import { PlayerStates } from "../Player/PlayerStates/Platformer/PlayerController";
export default class Player extends PhysicsNode {
protected controller: PlayerController
velocity: Vec2;
speed: number = 400;
MIN_SPEED: number = 400;
MAX_SPEED: number = 1000;
constructor(position: Vec2){
super();
this.position.copy(position);
this.velocity = Vec2.ZERO;
this.controller = new PlayerController(this);
this.controller.initialize(PlayerStates.IDLE);
this.collider = new Collider(new AABB(Vec2.ZERO, new Vec2(32, 32)))
}
create(): void {
}
update(deltaT: number): void {
this.controller.update(deltaT);
Debug.log("playerVel", "Pos: " + this.position.toString() + ", Vel: " + this.velocity.toString())
}
}

View File

@ -0,0 +1,27 @@
import OnGround from "./OnGround";
import { PlayerStates } from "./PlayerController";
import PlayerState from "./PlayerState";
export default class Idle extends OnGround {
onEnter(): void {
this.owner.speed = this.owner.MIN_SPEED;
}
update(deltaT: number): void {
super.update(deltaT);
let dir = this.getInputDirection();
if(!dir.isZero() && dir.y === 0){
if(this.input.isPressed("shift")){
this.finished(PlayerStates.RUN);
} else {
this.finished(PlayerStates.WALK);
}
}
this.owner.velocity.x = 0;
this.owner.move(this.owner.velocity.scaled(deltaT));
}
}

View File

@ -0,0 +1,34 @@
import Vec2 from "../../../../DataTypes/Vec2";
import GameEvent from "../../../../Events/GameEvent";
import MathUtils from "../../../../Utils/MathUtils";
import { CustomGameEventType } from "../../../CustomGameEventType";
import { PlayerStates } from "./PlayerController";
import PlayerState from "./PlayerState";
export default class Jump extends PlayerState {
onEnter(): void {}
handleInput(event: GameEvent): void {}
update(deltaT: number): void {
super.update(deltaT);
if(this.owner.isGrounded()){
this.finished(PlayerStates.PREVIOUS);
}
if(this.owner.isOnCeiling()){
this.owner.velocity.y = 0;
}
let dir = this.getInputDirection();
this.owner.velocity.x += dir.x * this.owner.speed/3.5 - 0.3*this.owner.velocity.x;
this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()});
this.owner.move(this.owner.velocity.scaled(deltaT));
}
onExit(): void {}
}

View File

@ -0,0 +1,26 @@
import GameEvent from "../../../../Events/GameEvent";
import { CustomGameEventType } from "../../../CustomGameEventType";
import PlayerState from "./PlayerState";
export default class OnGround extends PlayerState {
onEnter(): void {}
handleInput(event: GameEvent): void {}
update(deltaT: number): void {
if(this.owner.velocity.y > 0){
this.owner.velocity.y = 0;
}
super.update(deltaT);
if(this.input.isJustPressed("w") || this.input.isJustPressed("space")){
this.finished("jump");
this.owner.velocity.y = -2000;
this.emitter.fireEvent(CustomGameEventType.PLAYER_JUMP)
} else if(!this.owner.isGrounded()){
this.finished("jump");
}
}
onExit(): void {}
}

View File

@ -0,0 +1,59 @@
import StateMachine from "../../../../DataTypes/State/StateMachine";
import Debug from "../../../../Debug/Debug";
import Player from "../../../MarioClone/Player";
import Idle from "./Idle";
import Jump from "./Jump";
import Walk from "./Walk";
import Run from "./Run";
export enum PlayerStates {
WALK = "walk",
RUN = "run",
IDLE = "idle",
JUMP = "jump",
PREVIOUS = "previous"
}
export default class PlayerController extends StateMachine {
protected owner: Player;
constructor(owner: Player){
super();
this.owner = owner;
let idle = new Idle(this, owner);
this.addState(PlayerStates.IDLE, idle);
let walk = new Walk(this, owner);
this.addState(PlayerStates.WALK, walk);
let run = new Run(this, owner);
this.addState(PlayerStates.RUN, run);
let jump = new Jump(this, owner);
this.addState(PlayerStates.JUMP, jump);
}
currentStateString: string = "";
changeState(stateName: string): void {
this.currentStateString = stateName;
if(stateName === PlayerStates.JUMP){
this.stack.push(this.stateMap.get(stateName));
}
super.changeState(stateName);
}
update(deltaT: number): void {
super.update(deltaT);
if(this.currentState instanceof Jump){
Debug.log("playerstate", "Player State: Jump");
} else if (this.currentState instanceof Walk){
Debug.log("playerstate", "Player State: Walk");
} else if (this.currentState instanceof Run){
Debug.log("playerstate", "Player State: Run");
} else {
Debug.log("playerstate", "Player State: Idle");
}
}
}

View File

@ -0,0 +1,33 @@
import State from "../../../../DataTypes/State/State";
import StateMachine from "../../../../DataTypes/State/StateMachine";
import Vec2 from "../../../../DataTypes/Vec2";
import InputReceiver from "../../../../Input/InputReceiver";
import CanvasNode from "../../../../Nodes/CanvasNode";
import Player from "../../../MarioClone/Player";
export default abstract class PlayerState extends State {
input: InputReceiver = InputReceiver.getInstance();
owner: Player;
gravity: number = 7000;
constructor(parent: StateMachine, owner: Player){
super(parent);
this.owner = owner;
}
getInputDirection(): Vec2 {
let direction = Vec2.ZERO;
direction.x = (this.input.isPressed("a") ? -1 : 0) + (this.input.isPressed("d") ? 1 : 0);
direction.y = (this.input.isJustPressed("w") ? -1 : 0);
return direction;
}
updateLookDirection(direction: Vec2): void {
// Update the owners look direction
}
update(deltaT: number): void {
// Do gravity;
this.owner.velocity.y += this.gravity*deltaT;
}
}

View File

@ -0,0 +1,28 @@
import { CustomGameEventType } from "../../../CustomGameEventType";
import OnGround from "./OnGround";
import { PlayerStates } from "./PlayerController";
export default class Run extends OnGround {
onEnter(): void {
this.owner.speed = this.owner.MAX_SPEED;
}
update(deltaT: number): void {
super.update(deltaT);
let dir = this.getInputDirection();
if(dir.isZero()){
this.finished(PlayerStates.IDLE);
} else {
if(!this.input.isPressed("shift")){
this.finished(PlayerStates.WALK);
}
}
this.owner.velocity.x = dir.x * this.owner.speed
this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()});
this.owner.move(this.owner.velocity.scaled(deltaT));
}
}

View File

@ -0,0 +1,28 @@
import { CustomGameEventType } from "../../../CustomGameEventType";
import OnGround from "./OnGround";
import { PlayerStates } from "./PlayerController";
export default class Walk extends OnGround {
onEnter(): void {
this.owner.speed = this.owner.MAX_SPEED/2;
}
update(deltaT: number): void {
super.update(deltaT);
let dir = this.getInputDirection();
if(dir.isZero()){
this.finished(PlayerStates.IDLE);
} else {
if(this.input.isPressed("shift")){
this.finished(PlayerStates.RUN);
}
}
this.owner.velocity.x = dir.x * this.owner.speed
this.emitter.fireEvent(CustomGameEventType.PLAYER_MOVE, {position: this.owner.position.clone()});
this.owner.move(this.owner.velocity.scaled(deltaT));
}
}

View File

@ -3,13 +3,14 @@ import {} from "./index";
import MainScene from "./MainScene" import MainScene from "./MainScene"
import QuadTreeScene from "./QuadTreeScene"; import QuadTreeScene from "./QuadTreeScene";
import BoidDemo from "./BoidDemo"; import BoidDemo from "./BoidDemo";
import MarioClone from "./_DemoClasses/MarioClone/MarioClone";
function main(){ function main(){
// Create the game object // Create the game object
let game = new GameLoop({viewportSize: {x: 800, y: 600}}); let game = new GameLoop({viewportSize: {x: 800, y: 600}});
game.start(); game.start();
let sm = game.getSceneManager(); let sm = game.getSceneManager();
sm.addScene(BoidDemo); sm.addScene(MarioClone);
} }
CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void { CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {