From 6149b983a5dadac4219cceb006dea602c3cf75db Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Mon, 16 Nov 2020 11:02:45 -0500 Subject: [PATCH] finished implementing physics layers and added support for tilemap editing with code --- src/DataTypes/Interfaces/Descriptors.ts | 13 ++- src/DataTypes/Map.ts | 8 ++ src/DataTypes/Shapes/AABB.ts | 4 + src/DataTypes/Tilesets/Tileset.ts | 10 ++- src/Loop/GameLoop.ts | 17 ---- src/Nodes/GameNode.ts | 8 ++ src/Nodes/Tilemap.ts | 77 ++++++++++------ src/Nodes/Tilemaps/OrthogonalTilemap.ts | 87 +++++++++++++++---- src/Physics/BasicPhysicsManager.ts | 45 ++++------ src/Physics/Collisions.ts | 1 - src/Physics/PhysicsManager.ts | 31 +++++++ src/Scene/Scene.ts | 46 +++++++++- src/Scene/SceneManager.ts | 9 +- src/_DemoClasses/Mario/Level1.ts | 14 ++- src/_DemoClasses/Player/PlayerController.ts | 6 +- .../Player/PlayerStates/Platformer/Jump.ts | 17 ++++ .../PlayerStates/Platformer/PlayerState.ts | 2 +- src/main.ts | 20 ++++- 18 files changed, 302 insertions(+), 113 deletions(-) diff --git a/src/DataTypes/Interfaces/Descriptors.ts b/src/DataTypes/Interfaces/Descriptors.ts index 61d4137..7f260bf 100644 --- a/src/DataTypes/Interfaces/Descriptors.ts +++ b/src/DataTypes/Interfaces/Descriptors.ts @@ -1,4 +1,3 @@ -import GameEvent from "../../Events/GameEvent"; import Map from "../Map"; import AABB from "../Shapes/AABB"; import Shape from "../Shapes/Shape"; @@ -77,6 +76,12 @@ export interface Physical { /** The rectangle swept by the movement of this object, if dynamic */ sweptRect: AABB; + /** A boolean representing whether or not the node just collided with the tilemap */ + collidedWithTilemap: boolean; + + /** The physics layer this node belongs to */ + physicsLayer: number; + isPlayer: boolean; /*---------- FUNCTIONS ----------*/ @@ -107,6 +112,12 @@ export interface Physical { * @param eventType The name of the event to send when this trigger is activated */ addTrigger: (group: string, eventType: string) => void; + + /** + * Sets the physics layer of this node + * @param layer The name of the layer + */ + setPhysicsLayer: (layer: String) => void; } /** diff --git a/src/DataTypes/Map.ts b/src/DataTypes/Map.ts index 4059bde..ddd8c17 100644 --- a/src/DataTypes/Map.ts +++ b/src/DataTypes/Map.ts @@ -66,4 +66,12 @@ export default class Map implements Collection { clear(): void { this.forEach(key => delete this.map[key]); } + + toString(): string { + let str = ""; + + this.forEach((key) => str += key + " -> " + this.get(key).toString() + "\n"); + + return str; + } } \ No newline at end of file diff --git a/src/DataTypes/Shapes/AABB.ts b/src/DataTypes/Shapes/AABB.ts index d34c5c2..2c2bee7 100644 --- a/src/DataTypes/Shapes/AABB.ts +++ b/src/DataTypes/Shapes/AABB.ts @@ -234,6 +234,10 @@ export default class AABB extends Shape { clone(): AABB { return new AABB(this.center.clone(), this.halfSize.clone()); } + + toString(): string { + return "(center: " + this.center.toString() + ", half-size: " + this.halfSize.toString() + ")" + } } export class Hit { diff --git a/src/DataTypes/Tilesets/Tileset.ts b/src/DataTypes/Tilesets/Tileset.ts index 1120acf..849b0d0 100644 --- a/src/DataTypes/Tilesets/Tileset.ts +++ b/src/DataTypes/Tilesets/Tileset.ts @@ -74,6 +74,10 @@ export default class Tileset { return this.numCols; } + getTileCount(): number { + return this.endIndex - this.startIndex + 1; + } + hasTile(tileIndex: number): boolean { return tileIndex >= this.startIndex && tileIndex <= this.endIndex; } @@ -87,7 +91,7 @@ export default class Tileset { * @param origin The viewport origin in the current layer * @param scale The scale of the tilemap */ - renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, worldSize: Vec2, origin: Vec2, scale: Vec2, zoom: number): void { + renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, maxCols: number, origin: Vec2, scale: Vec2, zoom: number): void { let image = ResourceManager.getInstance().getImage(this.imageKey); // Get the true index @@ -102,8 +106,8 @@ export default class Tileset { let top = row * height; // Calculate the position in the world to render the tile - let x = Math.floor((dataIndex % worldSize.x) * width * scale.x); - let y = Math.floor(Math.floor(dataIndex / worldSize.x) * height * scale.y); + let x = Math.floor((dataIndex % maxCols) * width * scale.x); + let y = Math.floor(Math.floor(dataIndex / maxCols) * height * scale.y); ctx.drawImage(image, left, top, width, height, Math.floor((x - origin.x)*zoom), Math.floor((y - origin.y)*zoom), Math.ceil(width * scale.x * zoom), Math.ceil(height * scale.y * zoom)); } } \ No newline at end of file diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 95f8594..cac769f 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -286,29 +286,12 @@ export default class GameLoop { 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/GameNode.ts b/src/Nodes/GameNode.ts index 064df9c..d3fc584 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -35,6 +35,8 @@ export default abstract class GameNode implements Positioned, Unique, Updateable triggers: Map; _velocity: Vec2; sweptRect: AABB; + collidedWithTilemap: boolean; + physicsLayer: number; isPlayer: boolean; /*---------- ACTOR ----------*/ @@ -124,6 +126,8 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.triggers = new Map(); this._velocity = Vec2.ZERO; this.sweptRect = new AABB(); + this.collidedWithTilemap = false; + this.physicsLayer = -1; if(collisionShape){ this.collisionShape = collisionShape; @@ -147,6 +151,10 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.triggers.add(group, eventType); }; + setPhysicsLayer = (layer: string): void => { + this.scene.getPhysicsManager().setLayer(this, layer); + } + /*---------- ACTOR ----------*/ get ai(): AI { return this._ai; diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts index 62683ee..e758b00 100644 --- a/src/Nodes/Tilemap.ts +++ b/src/Nodes/Tilemap.ts @@ -1,62 +1,83 @@ import Vec2 from "../DataTypes/Vec2"; -import GameNode from "./GameNode"; import Tileset from "../DataTypes/Tilesets/Tileset"; import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledData" +import CanvasNode from "./CanvasNode"; /** * The representation of a tilemap - this can consist of a combination of tilesets in one layer */ -export default abstract class Tilemap extends GameNode { - // A tileset represents the tiles within one specific image loaded from a file +export default abstract class Tilemap extends CanvasNode { protected tilesets: Array; - protected size: Vec2; protected tileSize: Vec2; - protected scale: Vec2; - public data: Array; - public visible: boolean; + protected data: Array; + protected collisionMap: Array; + name: string; // TODO: Make this no longer be specific to Tiled constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array, scale: Vec2) { super(); this.tilesets = tilesets; - this.size = new Vec2(0, 0); this.tileSize = new Vec2(0, 0); + this.name = layer.name; + + let tilecount = 0; + for(let tileset of tilesets){ + tilecount += tileset.getTileCount(); + } + + this.collisionMap = new Array(tilecount); + for(let i = 0; i < this.collisionMap.length; i++){ + this.collisionMap[i] = false; + } // Defer parsing of the data to child classes - this allows for isometric vs. orthographic tilemaps and handling of Tiled data or other data this.parseTilemapData(tilemapData, layer); - this.scale = scale.clone(); + this.scale.set(scale.x, scale.y); } + /** + * Returns an array of the tilesets associated with this tilemap + */ getTilesets(): Tileset[] { return this.tilesets; } - getsize(): Vec2 { - return this.size; - } - + /** + * Returns the size of tiles in this tilemap as they appear in the game world after scaling + */ getTileSize(): Vec2 { - return this.tileSize.clone().scale(this.scale.x, this.scale.y); + return this.tileSize.scaled(this.scale.x, this.scale.y); } - getScale(): Vec2 { - return this.scale; - } - - setScale(scale: Vec2): void { - this.scale = scale; - } - - isVisible(): boolean { - return this.visible; - } - - /** Adds this tilemaps to the physics system */ + /** Adds this tilemap to the physics system */ addPhysics = (): void => { this.scene.getPhysicsManager().registerTilemap(this); } - abstract getTileAt(worldCoords: Vec2): number; + /** + * Returns the value of the tile at the specified position + * @param worldCoords The position in world coordinates + */ + abstract getTileAtWorldPosition(worldCoords: Vec2): number; + + /** + * Returns the world position of the top left corner of the tile at the specified index + * @param index + */ + abstract getTileWorldPosition(index: number): Vec2; + + /** + * Returns the value of the tile at the specified index + * @param index + */ + abstract getTile(index: number): number; + + /** + * Sets the value of the tile at the specified index + * @param index + * @param type + */ + abstract setTile(index: number, type: number): void; /** * Sets up the tileset using the data loaded from file diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index 56a9d14..a67d300 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -8,38 +8,84 @@ import Tileset from "../../DataTypes/Tilesets/Tileset"; */ export default class OrthogonalTilemap extends Tilemap { + protected numCols: number; + protected numRows: number; + /** * Parses the tilemap data loaded from the json file. DOES NOT process images automatically - the ResourceManager class does this while loading tilemaps * @param tilemapData * @param layer */ protected parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void { - this.size.set(tilemapData.width, tilemapData.height); + // The size of the tilemap in local space + this.numCols = tilemapData.width; + this.numRows = tilemapData.height; + + // The size of tiles this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight); + + // The size of the tilemap on the canvas + this.size.set(this.numCols * this.tileSize.x, this.numRows * this.tileSize.y); + this.position.copy(this.size); this.data = layer.data; this.visible = layer.visible; + + // Whether the tilemap is collidable or not this.isCollidable = false; if(layer.properties){ for(let item of layer.properties){ if(item.name === "Collidable"){ this.isCollidable = item.value; + + // Set all tiles besides "empty: 0" to be collidable + for(let i = 1; i < this.collisionMap.length; i++){ + this.collisionMap[i] = true; + } } } } } - /** - * Get the value of the tile at the coordinates in the vector worldCoords - * @param worldCoords - */ - getTileAt(worldCoords: Vec2): number { + getTileAtWorldPosition(worldCoords: Vec2): number { let localCoords = this.getColRowAt(worldCoords); - if(localCoords.x < 0 || localCoords.x >= this.size.x || localCoords.y < 0 || localCoords.y >= this.size.y){ - // There are no tiles in negative positions or out of bounds positions - return 0; + return this.getTileAtRowCol(localCoords); + } + + /** + * Get the tile at the specified row and column + * @param rowCol + */ + getTileAtRowCol(rowCol: Vec2): number { + if(rowCol.x < 0 || rowCol.x >= this.numCols || rowCol.y < 0 || rowCol.y >= this.numRows){ + return -1; } - return this.data[localCoords.y * this.size.x + localCoords.x] + return this.data[rowCol.y * this.numCols + rowCol.x]; + } + + getTileWorldPosition(index: number): Vec2 { + // Get the local position + let col = index % this.numCols; + let row = Math.floor(index / this.numCols); + + // Get the world position + let x = col * this.tileSize.x; + let y = row * this.tileSize.y; + + return new Vec2(x, y); + } + + getTile(index: number): number { + return this.data[index]; + } + + setTile(index: number, type: number): void { + this.data[index] = type; + } + + setTileAtRowCol(rowCol: Vec2, type: number): void { + let index = rowCol.y * this.numCols + rowCol.x; + this.setTile(index, type); } /** @@ -48,33 +94,36 @@ export default class OrthogonalTilemap extends Tilemap { * @param row */ isTileCollidable(indexOrCol: number, row?: number): boolean { - let index = 0; + // The value of the tile + let tile = 0; + if(row){ - if(indexOrCol < 0 || indexOrCol >= this.size.x || row < 0 || row >= this.size.y){ - // There are no tiles in negative positions or out of bounds positions + // We have a column and a row + tile = this.getTileAtRowCol(new Vec2(indexOrCol, row)); + + if(tile < 0){ return false; } - index = row * this.size.x + indexOrCol; } else { if(indexOrCol < 0 || indexOrCol >= this.data.length){ // Tiles that don't exist aren't collidable return false; } - index = indexOrCol; + // We have an index + tile = this.getTile(indexOrCol); } - // TODO - Currently, all tiles in a collidable layer are collidable - return this.data[index] !== 0 && this.isCollidable; + return this.collisionMap[tile]; } /** * Takes in world coordinates and returns the row and column of the tile at that position * @param worldCoords */ - // TODO: Should this throw an error if someone tries to access an out of bounds value? getColRowAt(worldCoords: Vec2): Vec2 { let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x); let row = Math.floor(worldCoords.y / this.tileSize.y / this.scale.y); + return new Vec2(col, row); } @@ -94,7 +143,7 @@ export default class OrthogonalTilemap extends Tilemap { for(let tileset of this.tilesets){ if(tileset.hasTile(tileIndex)){ - tileset.renderTile(ctx, tileIndex, i, this.size, origin, this.scale, zoom); + tileset.renderTile(ctx, tileIndex, i, this.numCols, origin, this.scale, zoom); } } } diff --git a/src/Physics/BasicPhysicsManager.ts b/src/Physics/BasicPhysicsManager.ts index cb72c54..71f34fd 100644 --- a/src/Physics/BasicPhysicsManager.ts +++ b/src/Physics/BasicPhysicsManager.ts @@ -8,9 +8,7 @@ import SweepAndPrune from "./BroadPhaseAlgorithms/SweepAndPrune"; import Shape from "../DataTypes/Shapes/Shape"; 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 { @@ -26,8 +24,8 @@ export default class BasicPhysicsManager extends PhysicsManager { /** The broad phase collision detection algorithm used by this physics system */ protected broadPhase: BroadPhase; - protected layerMap: Map; - protected layerNames: Array; + /** A 2D array that contains information about which layers interact with each other */ + protected layerMask: number[][]; constructor(physicsOptions: Record){ super(); @@ -35,8 +33,6 @@ export default class BasicPhysicsManager extends PhysicsManager { 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){ @@ -56,7 +52,7 @@ export default class BasicPhysicsManager extends PhysicsManager { this.layerMap.add("" + i, i); } - console.log(this.layerNames); + this.layerMask = physicsOptions.physicsLayerCollisions; } /** @@ -101,14 +97,12 @@ export default class BasicPhysicsManager extends PhysicsManager { // TODO - This is problematic if a collision happens, but it is later learned that another collision happens before it if(node1.triggers.has(group2)){ // Node1 should send an event - console.log("Trigger") let eventType = node1.triggers.get(group2); this.emitter.fireEvent(eventType, {node: node1, other: node2, collision: {firstContact: firstContact}}); } if(node2.triggers.has(group1)){ // Node2 should send an event - console.log("Trigger") let eventType = node2.triggers.get(group1); this.emitter.fireEvent(eventType, {node: node2, other: node1, collision: {firstContact: firstContact}}); } @@ -191,13 +185,10 @@ export default class BasicPhysicsManager extends PhysicsManager { let tilemapCollisions = new Array(); let tileSize = tilemap.getTileSize(); - Debug.log("tilemapCollision", ""); - // Loop over all possible tiles (which isn't many in the scope of the velocity per frame) for(let col = minIndex.x; col <= maxIndex.x; col++){ for(let row = minIndex.y; row <= maxIndex.y; row++){ if(tilemap.isTileCollidable(col, row)){ - Debug.log("tilemapCollision", "Colliding with Tile"); // Get the position of this tile let tilePos = new Vec2(col * tileSize.x + tileSize.x/2, row * tileSize.y + tileSize.y/2); @@ -222,10 +213,6 @@ export default class BasicPhysicsManager extends PhysicsManager { // Now that we have all collisions, sort by collision area highest to lowest tilemapCollisions = tilemapCollisions.sort((a, b) => a.overlapArea - b.overlapArea); - - let areas = ""; - tilemapCollisions.forEach(col => areas += col.overlapArea + ", ") - Debug.log("cols", areas) // Resolve the collisions in order of collision area (i.e. "closest" tiles are collided with first, so we can slide along a surface of tiles) tilemapCollisions.forEach(collision => { @@ -233,6 +220,9 @@ export default class BasicPhysicsManager extends PhysicsManager { // Handle collision if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ + // We are definitely colliding, so add to this node's tilemap collision list + node.collidedWithTilemap = true; + if(collidingX && collidingY){ // If we're already intersecting, freak out I guess? Probably should handle this in some way for if nodes get spawned inside of tiles } else { @@ -263,8 +253,7 @@ export default class BasicPhysicsManager extends PhysicsManager { } } }) - } - + } update(deltaT: number): void { /*---------- INITIALIZATION PHASE ----------*/ @@ -273,6 +262,7 @@ export default class BasicPhysicsManager extends PhysicsManager { node.onGround = false; node.onCeiling = false; node.onWall = false; + node.collidedWithTilemap = false; // Update the swept shapes of each node if(node.moving){ @@ -303,17 +293,15 @@ export default class BasicPhysicsManager extends PhysicsManager { continue; } + // Make sure both nodes can collide with each other based on their physics layer + if(!(node1.physicsLayer === -1 || node2.physicsLayer === -1 || this.layerMask[node1.physicsLayer][node2.physicsLayer] === 1)){ + // Nodes do not collide. Continue onto the next pair + 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(node1.isPlayer){ - if(firstContact.x !== Infinity || firstContact.y !== Infinity) - Debug.log("playercol", "First Contact: " + firstContact.toFixed(4)) - } else if(node2.isPlayer) { - if(firstContact.x !== Infinity || firstContact.y !== Infinity) - Debug.log("playercol", "First Contact: " + firstContact.toFixed(4)) - } - this.resolveCollision(node1, node2, firstContact, lastContact, collidingX, collidingY); } @@ -322,7 +310,10 @@ export default class BasicPhysicsManager extends PhysicsManager { if(node.moving && node.isCollidable){ // If a node is moving and can collide, check it against every tilemap for(let tilemap of this.tilemaps){ - this.collideWithTilemap(node, tilemap, node._velocity); + // Check if there could even be a collision + if(node.sweptRect.overlaps(tilemap.boundary)){ + this.collideWithTilemap(node, tilemap, node._velocity); + } } } } diff --git a/src/Physics/Collisions.ts b/src/Physics/Collisions.ts index 7e5b8a9..a4a5417 100644 --- a/src/Physics/Collisions.ts +++ b/src/Physics/Collisions.ts @@ -1,5 +1,4 @@ import { Physical } from "../DataTypes/Interfaces/Descriptors"; -import AABB from "../DataTypes/Shapes/AABB"; import Vec2 from "../DataTypes/Vec2"; export class Collision { diff --git a/src/Physics/PhysicsManager.ts b/src/Physics/PhysicsManager.ts index af42473..0da7662 100644 --- a/src/Physics/PhysicsManager.ts +++ b/src/Physics/PhysicsManager.ts @@ -4,21 +4,52 @@ import { Debug_Renderable, Updateable } from "../DataTypes/Interfaces/Descriptor import Tilemap from "../Nodes/Tilemap"; import Receiver from "../Events/Receiver"; import Emitter from "../Events/Emitter"; +import Map from "../DataTypes/Map"; export default abstract class PhysicsManager implements Updateable, Debug_Renderable { protected receiver: Receiver; protected emitter: Emitter; + /** Layer names to numbers */ + protected layerMap: Map; + + /** Layer numbers to names */ + protected layerNames: Array; + constructor(){ this.receiver = new Receiver(); this.emitter = new Emitter(); + + // The creation and implementation of layers is deferred to the subclass + this.layerMap = new Map(); + this.layerNames = new Array(); } + /** + * Registers a gamenode with this physics manager + * @param object + */ abstract registerObject(object: GameNode): void; + /** + * Registers a tilemap with this physics manager + * @param tilemap + */ abstract registerTilemap(tilemap: Tilemap): void; + /** + * Updates the physics + * @param deltaT + */ abstract update(deltaT: number): void; + /** + * Renders any debug shapes or graphics + * @param ctx + */ abstract debug_render(ctx: CanvasRenderingContext2D): void; + + setLayer(node: GameNode, layer: string): void { + node.physicsLayer = this.layerMap.get(layer); + } } \ No newline at end of file diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index 1d4b459..ce3cb6d 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -20,6 +20,7 @@ import ParallaxLayer from "./Layers/ParallaxLayer"; import UILayer from "./Layers/UILayer"; import CanvasNode from "../Nodes/CanvasNode"; import GameNode from "../Nodes/GameNode"; +import ArrayUtils from "../Utils/ArrayUtils"; export default class Scene implements Updateable, Renderable { /** The size of the game world. */ @@ -73,7 +74,12 @@ export default class Scene implements Updateable, Renderable { /** An interface that allows the loading of different files for use in the scene */ public load: ResourceManager; - constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ + /** The configuration options for this scene */ + public sceneOptions: SceneOptions; + + constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop, options: Record){ + this.sceneOptions = SceneOptions.parse(options); + this.worldSize = new Vec2(500, 500); this.viewport = viewport; this.viewport.setBounds(0, 0, 2560, 1280); @@ -90,7 +96,7 @@ export default class Scene implements Updateable, Renderable { this.uiLayers = new Map(); this.parallaxLayers = new Map(); - this.physicsManager = new BasicPhysicsManager(this.game.gameOptions.physics); + this.physicsManager = new BasicPhysicsManager(this.sceneOptions.physics); this.navManager = new NavigationManager(); this.aiManager = new AIManager(); @@ -325,4 +331,40 @@ export default class Scene implements Updateable, Renderable { generateId(): number { return this.sceneManager.generateId(); } + + getTilemap(name: string): Tilemap { + for(let tilemap of this .tilemaps){ + if(tilemap.name === name){ + return tilemap; + } + } + + return null; + } +} + +class SceneOptions { + physics: { + numPhysicsLayers: number, + physicsLayerNames: Array, + physicsLayerCollisions: Array>; + } + + static parse(options: Record): SceneOptions{ + let sOpt = new SceneOptions(); + + sOpt.physics = { + numPhysicsLayers: 10, + physicsLayerNames: null, + physicsLayerCollisions: ArrayUtils.ones2d(10, 10) + }; + + if(options.physics){ + if(options.physics.numPhysicsLayers) sOpt.physics.numPhysicsLayers = options.physics.numPhysicsLayers; + if(options.physics.physicsLayerNames) sOpt.physics.physicsLayerNames = options.physics.physicsLayerNames; + if(options.physics.physicsLayerCollisions) sOpt.physics.physicsLayerCollisions = options.physics.physicsLayerCollisions; + } + + return sOpt; + } } \ No newline at end of file diff --git a/src/Scene/SceneManager.ts b/src/Scene/SceneManager.ts index 81e8da6..2b6425d 100644 --- a/src/Scene/SceneManager.ts +++ b/src/Scene/SceneManager.ts @@ -22,9 +22,8 @@ export default class SceneManager { * Add a scene as the main scene * @param constr The constructor of the scene to add */ - public addScene(constr: new (...args: any) => T): void { - console.log("Adding Scene"); - let scene = new constr(this.viewport, this, this.game); + public addScene(constr: new (...args: any) => T, options: Record): void { + let scene = new constr(this.viewport, this, this.game, options); this.currentScene = scene; // Enqueue all scene asset loads @@ -43,7 +42,7 @@ export default class SceneManager { * Change from the current scene to this new scene * @param constr The constructor of the scene to change to */ - public changeScene(constr: new (...args: any) => T): void { + public changeScene(constr: new (...args: any) => T, options: Record): void { // unload current scene this.currentScene.unloadScene(); @@ -51,7 +50,7 @@ export default class SceneManager { this.viewport.setCenter(0, 0); - this.addScene(constr); + this.addScene(constr, options); } public generateId(): number { diff --git a/src/_DemoClasses/Mario/Level1.ts b/src/_DemoClasses/Mario/Level1.ts index 8946297..a5441de 100644 --- a/src/_DemoClasses/Mario/Level1.ts +++ b/src/_DemoClasses/Mario/Level1.ts @@ -7,6 +7,7 @@ import ParallaxLayer from "../../Scene/Layers/ParallaxLayer"; import Scene from "../../Scene/Scene"; import PlayerController from "../Player/PlayerController"; import GoombaController from "../Enemies/GoombaController"; +import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap"; export enum MarioEvents { PLAYER_HIT_COIN = "PlayerHitCoin", @@ -27,7 +28,11 @@ export default class Level1 extends Scene { } startScene(): void { - this.add.tilemap("level1", new Vec2(2, 2)); + 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 @@ -37,33 +42,34 @@ export default class Level1 extends Scene { // 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"}); + this.player.addAI(PlayerController, {playerType: "platformer", tilemap: "Main"}); // 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.player.setPhysicsLayer("player"); 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}); + 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"); } // Add UI diff --git a/src/_DemoClasses/Player/PlayerController.ts b/src/_DemoClasses/Player/PlayerController.ts index edcf826..e601fd8 100644 --- a/src/_DemoClasses/Player/PlayerController.ts +++ b/src/_DemoClasses/Player/PlayerController.ts @@ -2,6 +2,7 @@ import StateMachineAI from "../../AI/StateMachineAI"; import Vec2 from "../../DataTypes/Vec2"; import Debug from "../../Debug/Debug"; import GameNode from "../../Nodes/GameNode"; +import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap"; import IdleTopDown from "./PlayerStates/IdleTopDown"; import MoveTopDown from "./PlayerStates/MoveTopDown"; import Idle from "./PlayerStates/Platformer/Idle"; @@ -28,7 +29,8 @@ export default class PlayerController extends StateMachineAI { velocity: Vec2 = Vec2.ZERO; speed: number = 400; MIN_SPEED: number = 400; - MAX_SPEED: number = 1000; + MAX_SPEED: number = 1000; + tilemap: OrthogonalTilemap; initializeAI(owner: GameNode, options: Record){ this.owner = owner; @@ -38,6 +40,8 @@ export default class PlayerController extends StateMachineAI { } else { this.initializePlatformer(); } + + this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap; } /** diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts index cb9932e..43ac0dc 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/Jump.ts @@ -14,6 +14,23 @@ export default class Jump extends PlayerState { update(deltaT: number): void { super.update(deltaT); + if(this.owner.collidedWithTilemap && this.owner.onCeiling){ + // We collided with a tilemap above us. First, get the tile right above us + let pos = this.owner.position.clone(); + + // Go up plus some extra + pos.y -= (this.owner.collisionShape.halfSize.y + 10); + 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 === 4){ + this.parent.tilemap.setTileAtRowCol(pos, 12); + } + } + if(this.owner.onGround){ this.finished(PlayerStates.PREVIOUS); } diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts index 4c64bdf..7522928 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerState.ts @@ -3,7 +3,7 @@ import StateMachine from "../../../../DataTypes/State/StateMachine"; import Vec2 from "../../../../DataTypes/Vec2"; import InputReceiver from "../../../../Input/InputReceiver"; import GameNode from "../../../../Nodes/GameNode"; -import PlayerController from "./PlayerController"; +import PlayerController from "../../PlayerController"; export default abstract class PlayerState extends State { diff --git a/src/main.ts b/src/main.ts index 74e5280..2857c45 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,15 +6,27 @@ function main(){ // Create the game object let options = { viewportSize: {x: 800, y: 600}, - physics: { - physicsLayerNames: ["ground", "player", "enemy", "coin"] - } } let game = new GameLoop(options); game.start(); + + let sceneOptions = { + physics: { + physicsLayerNames: ["ground", "player", "enemy", "coin"], + numPhyiscsLayers: 4, + physicsLayerCollisions: + [ + [0, 1, 1, 1], + [1, 0, 0, 1], + [1, 0, 0, 1], + [1, 1, 1, 0] + ] + } + } + let sm = game.getSceneManager(); - sm.addScene(Level1); + sm.addScene(Level1, sceneOptions); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {