From 9dc8cd29d1dff19a0f4e1d3f8afb51ef61d8220b Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Sun, 15 Nov 2020 08:26:49 -0500 Subject: [PATCH] added demo level and started work on physics layer support --- src/DataTypes/Interfaces/Descriptors.ts | 2 +- src/Loop/GameLoop.ts | 40 ++++++- src/Nodes/CanvasNode.ts | 2 + src/Physics/BasicPhysicsManager.ts | 37 ++++++- src/Scene/Factories/TilemapFactory.ts | 22 +++- src/Scene/Scene.ts | 4 +- src/Utils/ArrayUtils.ts | 34 ++++++ src/_DemoClasses/Enemies/GoombaController.ts | 16 ++- src/_DemoClasses/Enemies/GoombaState.ts | 8 +- src/_DemoClasses/Enemies/OnGround.ts | 8 +- src/_DemoClasses/Enemies/Walk.ts | 11 ++ src/_DemoClasses/Mario/Level1.ts | 110 +++++++++++++++++++ src/_DemoClasses/Player/PlayerController.ts | 10 +- src/main.ts | 15 ++- 14 files changed, 272 insertions(+), 47 deletions(-) create mode 100644 src/Utils/ArrayUtils.ts create mode 100644 src/_DemoClasses/Mario/Level1.ts diff --git a/src/DataTypes/Interfaces/Descriptors.ts b/src/DataTypes/Interfaces/Descriptors.ts index de64dd0..61d4137 100644 --- a/src/DataTypes/Interfaces/Descriptors.ts +++ b/src/DataTypes/Interfaces/Descriptors.ts @@ -114,7 +114,7 @@ export interface Physical { */ export interface AI extends Updateable { /** Initializes the AI with the actor and any additional config */ - initializeAI: (owner: GameNode, config: Record) => void; + initializeAI: (owner: GameNode, options: Record) => void; } export interface Actor { diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index b7333b1..95f8594 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -8,8 +8,11 @@ import Viewport from "../SceneGraph/Viewport"; import SceneManager from "../Scene/SceneManager"; import AudioManager from "../Sound/AudioManager"; import Stats from "../Debug/Stats"; +import ArrayUtils from "../Utils/ArrayUtils"; export default class GameLoop { + gameOptions: GameOptions; + /** The max allowed update fps.*/ private maxUpdateFPS: number; @@ -68,9 +71,9 @@ export default class GameLoop { private sceneManager: SceneManager; private audioManager: AudioManager; - constructor(config?: object){ + constructor(options?: Record){ // Typecast the config object to a GameConfig object - let gameConfig = config ? config : new GameConfig(); + this.gameOptions = GameOptions.parse(options); this.maxUpdateFPS = 60; this.simulationTimestep = Math.floor(1000/this.maxUpdateFPS); @@ -95,8 +98,8 @@ export default class GameLoop { this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke"); // Give the canvas a size and get the rendering context - this.WIDTH = gameConfig.canvasSize ? gameConfig.canvasSize.x : 800; - this.HEIGHT = gameConfig.canvasSize ? gameConfig.canvasSize.y : 500; + this.WIDTH = this.gameOptions.viewportSize.x; + this.HEIGHT = this.gameOptions.viewportSize.y; this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); // Size the viewport to the game canvas @@ -281,6 +284,31 @@ export default class GameLoop { } } -class GameConfig { - canvasSize: {x: number, y: number} +class GameOptions { + viewportSize: {x: number, y: number} + physics: { + numPhysicsLayers: number, + physicsLayerNames: Array, + physicsLayerCollisions: Array>; + } + + static parse(options: Record): GameOptions { + let gOpt = new GameOptions(); + + gOpt.viewportSize = options.viewportSize ? options.viewportSize : {x: 800, y: 600}; + + gOpt.physics = { + numPhysicsLayers: 10, + physicsLayerNames: null, + physicsLayerCollisions: ArrayUtils.ones2d(10, 10) + }; + + if(options.physics){ + if(options.physics.numPhysicsLayers) gOpt.physics.numPhysicsLayers = options.physics.numPhysicsLayers; + if(options.physics.physicsLayerNames) gOpt.physics.physicsLayerNames = options.physics.physicsLayerNames; + if(options.physics.physicsLayerCollisions) gOpt.physics.physicsLayerCollisions = options.physics.physicsLayerCollisions; + } + + return gOpt; + } } \ No newline at end of file diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index f44cc1a..f4dfe5b 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -11,6 +11,8 @@ export default abstract class CanvasNode extends GameNode implements Region { private _scale: Vec2; private _boundary: AABB; + visible = true; + constructor(){ super(); this.position.setOnChange(this.positionChanged); diff --git a/src/Physics/BasicPhysicsManager.ts b/src/Physics/BasicPhysicsManager.ts index df346dd..cb72c54 100644 --- a/src/Physics/BasicPhysicsManager.ts +++ b/src/Physics/BasicPhysicsManager.ts @@ -10,6 +10,7 @@ import MathUtils from "../Utils/MathUtils"; import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; import Debug from "../Debug/Debug"; import AABB from "../DataTypes/Shapes/AABB"; +import Map from "../DataTypes/Map"; export default class BasicPhysicsManager extends PhysicsManager { @@ -25,12 +26,37 @@ export default class BasicPhysicsManager extends PhysicsManager { /** The broad phase collision detection algorithm used by this physics system */ protected broadPhase: BroadPhase; - constructor(){ + protected layerMap: Map; + protected layerNames: Array; + + constructor(physicsOptions: Record){ super(); this.staticNodes = new Array(); this.dynamicNodes = new Array(); this.tilemaps = new Array(); this.broadPhase = new SweepAndPrune(); + this.layerMap = new Map(); + this.layerNames = new Array(); + + let i = 0; + if(physicsOptions.physicsLayerNames !== null){ + for(let layer of physicsOptions.physicsLayerNames){ + if(i >= physicsOptions.numPhysicsLayers){ + // If we have too many string layers, don't add extras + } + + this.layerNames[i] = layer; + this.layerMap.add(layer, i); + i += 1; + } + } + + for(i; i < physicsOptions.numPhysicsLayers; i++){ + this.layerNames[i] = "" + i; + this.layerMap.add("" + i, i); + } + + console.log(this.layerNames); } /** @@ -271,14 +297,15 @@ export default class BasicPhysicsManager extends PhysicsManager { for(let pair of potentialCollidingPairs){ let node1 = pair[0]; let node2 = pair[1]; + + // Make sure both nodes are active + if(!node1.active || !node2.active){ + continue; + } // Get Collision (which may or may not happen) let [firstContact, lastContact, collidingX, collidingY] = Shape.getTimeOfCollision(node1.collisionShape, node1._velocity, node2.collisionShape, node2._velocity); - if(collidingX && collidingY){ - console.log("overlapping") - } - if(node1.isPlayer){ if(firstContact.x !== Infinity || firstContact.y !== Infinity) Debug.log("playercol", "First Contact: " + firstContact.toFixed(4)) diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index 425e530..f7bcf10 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -21,6 +21,10 @@ export default class TilemapFactory { this.resourceManager = ResourceManager.getInstance(); } + // TODO - This is specifically catered to Tiled tilemaps right now. In the future, + // it would be good to have a "parseTilemap" function that would convert the tilemap + // data into a standard format. This could allow for support from other programs + // or the development of an internal level builder tool /** * Adds a tilemap to the scene * @param key The key of the loaded tilemap to load @@ -63,8 +67,24 @@ export default class TilemapFactory { // Loop over the layers of the tilemap and create tiledlayers or object layers for(let layer of tilemapData.layers){ + + let sceneLayer; + let isParallaxLayer = false; - let sceneLayer = this.scene.addLayer(layer.name); + if(layer.properties){ + for(let prop of layer.properties){ + if(prop.name === "Parallax"){ + isParallaxLayer = prop.value; + } + } + } + + if(isParallaxLayer){ + console.log("Adding parallax layer: " + layer.name) + sceneLayer = this.scene.addParallaxLayer(layer.name, new Vec2(1, 1)); + } else { + sceneLayer = this.scene.addLayer(layer.name); + } if(layer.type === "tilelayer"){ // Create a new tilemap object for the layer diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index 3e52458..1d4b459 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -90,7 +90,7 @@ export default class Scene implements Updateable, Renderable { this.uiLayers = new Map(); this.parallaxLayers = new Map(); - this.physicsManager = new BasicPhysicsManager(); + this.physicsManager = new BasicPhysicsManager(this.game.gameOptions.physics); this.navManager = new NavigationManager(); this.aiManager = new AIManager(); @@ -170,7 +170,7 @@ export default class Scene implements Updateable, Renderable { }); // Render visible set - visibleSet.forEach(node => node.render(ctx)); + visibleSet.forEach(node => node.visible ? node.render(ctx) : ""); // Debug render the physicsManager this.physicsManager.debug_render(ctx); diff --git a/src/Utils/ArrayUtils.ts b/src/Utils/ArrayUtils.ts new file mode 100644 index 0000000..caa890f --- /dev/null +++ b/src/Utils/ArrayUtils.ts @@ -0,0 +1,34 @@ +export default class ArrayUtils { + /** + * Returns a 2d array of dim1 x dim2 filled with 1s + * @param dim1 + * @param dim2 + */ + static ones2d(dim1: number, dim2: number): number[][] { + let arr = new Array>(dim1); + + for(let i = 0; i < arr.length; i++){ + arr[i] = new Array(dim2); + + for(let j = 0; j < arr[i].length; j++){ + arr[i][j] = 1; + } + } + + return arr; + } + + static bool2d(dim1: number, dim2: number, flag: boolean): boolean[][] { + let arr = new Array>(dim1); + + for(let i = 0; i < arr.length; i++){ + arr[i] = new Array(dim2); + + for(let j = 0; j < arr[i].length; j++){ + arr[i][j] = flag; + } + } + + return arr; + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Enemies/GoombaController.ts b/src/_DemoClasses/Enemies/GoombaController.ts index 031280e..4369487 100644 --- a/src/_DemoClasses/Enemies/GoombaController.ts +++ b/src/_DemoClasses/Enemies/GoombaController.ts @@ -1,12 +1,12 @@ -import StateMachine from "../../DataTypes/State/StateMachine"; import { CustomGameEventType } from "../CustomGameEventType"; import Idle from "../Enemies/Idle"; import Jump from "../Enemies/Jump"; import Walk from "../Enemies/Walk"; import Afraid from "../Enemies/Afraid"; -import Debug from "../../Debug/Debug"; import GameNode from "../../Nodes/GameNode"; import Vec2 from "../../DataTypes/Vec2"; +import StateMachineAI from "../../AI/StateMachineAI"; +import GoombaState from "./GoombaState"; export enum GoombaStates { IDLE = "idle", @@ -16,18 +16,16 @@ export enum GoombaStates { AFRAID = "afraid" } -export default class GoombaController extends StateMachine { +export default class GoombaController extends StateMachineAI { owner: GameNode; jumpy: boolean; direction: Vec2 = Vec2.ZERO; velocity: Vec2 = Vec2.ZERO; speed: number = 200; - constructor(owner: GameNode, jumpy: boolean){ - super(); - + initializeAI(owner: GameNode, options: Record){ this.owner = owner; - this.jumpy = jumpy; + this.jumpy = options.jumpy ? options.jumpy : false; this.receiver.subscribe(CustomGameEventType.PLAYER_MOVE); this.receiver.subscribe("playerHitCoinBlock"); @@ -41,8 +39,8 @@ export default class GoombaController extends StateMachine { this.addState(GoombaStates.WALK, walk); let jump = new Jump(this, owner); this.addState(GoombaStates.JUMP, jump); - let afraid = new Afraid(this, owner); - this.addState(GoombaStates.AFRAID, afraid); + + this.initialize(GoombaStates.IDLE); } changeState(stateName: string): void { diff --git a/src/_DemoClasses/Enemies/GoombaState.ts b/src/_DemoClasses/Enemies/GoombaState.ts index ea1b135..32d2c42 100644 --- a/src/_DemoClasses/Enemies/GoombaState.ts +++ b/src/_DemoClasses/Enemies/GoombaState.ts @@ -15,13 +15,7 @@ export default abstract class GoombaState extends State { this.owner = owner; } - handleInput(event: GameEvent): void { - if(event.type === "playerHitCoinBlock") { - if(event.data.get("collision").firstContact.y < 1 && event.data.get("node").collisionShape.center.y > event.data.get("other").collisionShape.center.y){ - this.finished(GoombaStates.AFRAID); - } - } - } + handleInput(event: GameEvent): void {} update(deltaT: number): void { // Do gravity diff --git a/src/_DemoClasses/Enemies/OnGround.ts b/src/_DemoClasses/Enemies/OnGround.ts index c86a9fd..17be81c 100644 --- a/src/_DemoClasses/Enemies/OnGround.ts +++ b/src/_DemoClasses/Enemies/OnGround.ts @@ -1,17 +1,11 @@ import GameEvent from "../../Events/GameEvent"; -import { CustomGameEventType } from "../CustomGameEventType"; -import GoombaController, { GoombaStates } from "./GoombaController"; +import { 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 && (this.parent).jumpy){ - this.finished(GoombaStates.JUMP); - this.parent.velocity.y = -2000; - } - super.handleInput(event); } diff --git a/src/_DemoClasses/Enemies/Walk.ts b/src/_DemoClasses/Enemies/Walk.ts index 86667a5..aa7c5b4 100644 --- a/src/_DemoClasses/Enemies/Walk.ts +++ b/src/_DemoClasses/Enemies/Walk.ts @@ -1,11 +1,16 @@ import Vec2 from "../../DataTypes/Vec2"; +import { GoombaStates } from "./GoombaController"; import OnGround from "./OnGround"; export default class Walk extends OnGround { + time: number; + onEnter(): void { if(this.parent.direction.isZero()){ this.parent.direction = new Vec2(-1, 0); } + + this.time = Date.now(); } update(deltaT: number): void { @@ -16,6 +21,12 @@ export default class Walk extends OnGround { this.parent.direction.x *= -1; } + if(this.parent.jumpy && (Date.now() - this.time > 500)){ + console.log("Jump"); + this.finished(GoombaStates.JUMP); + this.parent.velocity.y = -2000; + } + this.parent.velocity.x = this.parent.direction.x * this.parent.speed; this.owner.move(this.parent.velocity.scaled(deltaT)); diff --git a/src/_DemoClasses/Mario/Level1.ts b/src/_DemoClasses/Mario/Level1.ts new file mode 100644 index 0000000..8946297 --- /dev/null +++ b/src/_DemoClasses/Mario/Level1.ts @@ -0,0 +1,110 @@ +import Vec2 from "../../DataTypes/Vec2"; +import GameNode from "../../Nodes/GameNode"; +import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; +import Label from "../../Nodes/UIElements/Label"; +import { UIElementType } from "../../Nodes/UIElements/UIElementTypes"; +import ParallaxLayer from "../../Scene/Layers/ParallaxLayer"; +import Scene from "../../Scene/Scene"; +import PlayerController from "../Player/PlayerController"; +import GoombaController from "../Enemies/GoombaController"; + +export enum MarioEvents { + PLAYER_HIT_COIN = "PlayerHitCoin", + PLAYER_HIT_COIN_BLOCK = "PlayerHitCoinBlock" +} + +export default class Level1 extends Scene { + player: GameNode; + coinCount: number = 0; + coinCountLabel: Label; + livesCount: number = 3; + livesCountLabel: Label; + + loadScene(): void { + this.load.tilemap("level1", "/assets/tilemaps/level1.json"); + this.load.image("goomba", "assets/sprites/Goomba.png"); + this.load.image("koopa", "assets/sprites/Koopa.png"); + } + + startScene(): void { + this.add.tilemap("level1", new Vec2(2, 2)); + 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); + + // 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.addPhysics(); + this.player.addAI(PlayerController, {playerType: "platformer"}); + + // Add triggers on colliding with coins or coinBlocks + this.player.addTrigger("coin", MarioEvents.PLAYER_HIT_COIN); + this.player.addTrigger("coinBlock", MarioEvents.PLAYER_HIT_COIN_BLOCK); + + this.receiver.subscribe([MarioEvents.PLAYER_HIT_COIN, MarioEvents.PLAYER_HIT_COIN_BLOCK]); + + this.viewport.follow(this.player); + + // Add enemies + // Goombas + 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}); + } + + + 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}); + } + + // Add UI + this.addUILayer("UI"); + + this.coinCountLabel = this.add.uiElement(UIElementType.LABEL, "UI", {position: new Vec2(80, 30), text: "Coins: 0"}); + this.livesCountLabel = this.add.uiElement(UIElementType.LABEL, "UI", {position: new Vec2(600, 30), text: "Lives: 3"}); + } + + updateScene(deltaT: number): void { + while(this.receiver.hasNextEvent()){ + let event = this.receiver.getNextEvent(); + + if(event.type === MarioEvents.PLAYER_HIT_COIN){ + let coin; + if(event.data.get("node") === this.player){ + // Other is coin, disable + coin = event.data.get("other"); + } else { + // Node is coin, disable + coin = event.data.get("node"); + } + + // Remove from physics and scene + coin.active = false; + coin.visible = false; + this.coinCount += 1; + + this.coinCountLabel.setText("Coins: " + this.coinCount); + + } else if(event.type === MarioEvents.PLAYER_HIT_COIN_BLOCK){ + console.log("Hit Coin Block") + console.log(event.data.get("node") === this.player); + } + } + + // 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); + this.livesCount -= 1 + this.livesCountLabel.setText("Lives: " + this.livesCount); + } + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerController.ts b/src/_DemoClasses/Player/PlayerController.ts index 3900c86..edcf826 100644 --- a/src/_DemoClasses/Player/PlayerController.ts +++ b/src/_DemoClasses/Player/PlayerController.ts @@ -30,11 +30,11 @@ export default class PlayerController extends StateMachineAI { MIN_SPEED: number = 400; MAX_SPEED: number = 1000; - initializeAI(owner: GameNode, config: Record){ + initializeAI(owner: GameNode, options: Record){ this.owner = owner; - if(config.playerType === PlayerType.TOPDOWN){ - this.initializeTopDown(config.speed); + if(options.playerType === PlayerType.TOPDOWN){ + this.initializeTopDown(options.speed); } else { this.initializePlatformer(); } @@ -65,7 +65,9 @@ export default class PlayerController extends StateMachineAI { let run = new Run(this, this.owner); this.addState(PlayerStates.RUN, run); let jump = new Jump(this, this.owner); - this.addState(PlayerStates.JUMP, jump); + this.addState(PlayerStates.JUMP, jump); + + this.initialize(PlayerStates.IDLE); } changeState(stateName: string): void { diff --git a/src/main.ts b/src/main.ts index 1ca22d0..74e5280 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,20 @@ import GameLoop from "./Loop/GameLoop"; import {} from "./index"; -import BoidDemo from "./BoidDemo"; -import MarioClone from "./_DemoClasses/MarioClone/MarioClone"; -import PathfindingScene from "./_DemoClasses/Pathfinding/PathfindingScene"; +import Level1 from "./_DemoClasses/Mario/Level1"; function main(){ // Create the game object - let game = new GameLoop({canvasSize: {x: 800, y: 600}}); + let options = { + viewportSize: {x: 800, y: 600}, + physics: { + physicsLayerNames: ["ground", "player", "enemy", "coin"] + } + } + + let game = new GameLoop(options); game.start(); let sm = game.getSceneManager(); - sm.addScene(PathfindingScene); + sm.addScene(Level1); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {