finished implementing physics layers and added support for tilemap editing with code

This commit is contained in:
Joe Weaver 2020-11-16 11:02:45 -05:00
parent 9dc8cd29d1
commit 6149b983a5
18 changed files with 302 additions and 113 deletions

View File

@ -1,4 +1,3 @@
import GameEvent from "../../Events/GameEvent";
import Map from "../Map"; import Map from "../Map";
import AABB from "../Shapes/AABB"; import AABB from "../Shapes/AABB";
import Shape from "../Shapes/Shape"; import Shape from "../Shapes/Shape";
@ -77,6 +76,12 @@ export interface Physical {
/** The rectangle swept by the movement of this object, if dynamic */ /** The rectangle swept by the movement of this object, if dynamic */
sweptRect: AABB; 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; isPlayer: boolean;
/*---------- FUNCTIONS ----------*/ /*---------- FUNCTIONS ----------*/
@ -107,6 +112,12 @@ export interface Physical {
* @param eventType The name of the event to send when this trigger is activated * @param eventType The name of the event to send when this trigger is activated
*/ */
addTrigger: (group: string, eventType: string) => void; addTrigger: (group: string, eventType: string) => void;
/**
* Sets the physics layer of this node
* @param layer The name of the layer
*/
setPhysicsLayer: (layer: String) => void;
} }
/** /**

View File

@ -66,4 +66,12 @@ export default class Map<T> implements Collection {
clear(): void { clear(): void {
this.forEach(key => delete this.map[key]); this.forEach(key => delete this.map[key]);
} }
toString(): string {
let str = "";
this.forEach((key) => str += key + " -> " + this.get(key).toString() + "\n");
return str;
}
} }

View File

@ -234,6 +234,10 @@ export default class AABB extends Shape {
clone(): AABB { clone(): AABB {
return new AABB(this.center.clone(), this.halfSize.clone()); return new AABB(this.center.clone(), this.halfSize.clone());
} }
toString(): string {
return "(center: " + this.center.toString() + ", half-size: " + this.halfSize.toString() + ")"
}
} }
export class Hit { export class Hit {

View File

@ -74,6 +74,10 @@ export default class Tileset {
return this.numCols; return this.numCols;
} }
getTileCount(): number {
return this.endIndex - this.startIndex + 1;
}
hasTile(tileIndex: number): boolean { hasTile(tileIndex: number): boolean {
return tileIndex >= this.startIndex && tileIndex <= this.endIndex; 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 origin The viewport origin in the current layer
* @param scale The scale of the tilemap * @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); let image = ResourceManager.getInstance().getImage(this.imageKey);
// Get the true index // Get the true index
@ -102,8 +106,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 = Math.floor((dataIndex % worldSize.x) * width * scale.x); let x = Math.floor((dataIndex % maxCols) * width * scale.x);
let y = Math.floor(Math.floor(dataIndex / worldSize.x) * height * scale.y); 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)); 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));
} }
} }

View File

@ -286,29 +286,12 @@ export default class GameLoop {
class GameOptions { class GameOptions {
viewportSize: {x: number, y: number} viewportSize: {x: number, y: number}
physics: {
numPhysicsLayers: number,
physicsLayerNames: Array<string>,
physicsLayerCollisions: Array<Array<number>>;
}
static parse(options: Record<string, any>): GameOptions { static parse(options: Record<string, any>): GameOptions {
let gOpt = new GameOptions(); let gOpt = new GameOptions();
gOpt.viewportSize = options.viewportSize ? options.viewportSize : {x: 800, y: 600}; 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; return gOpt;
} }
} }

View File

@ -35,6 +35,8 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
triggers: Map<string>; triggers: Map<string>;
_velocity: Vec2; _velocity: Vec2;
sweptRect: AABB; sweptRect: AABB;
collidedWithTilemap: boolean;
physicsLayer: number;
isPlayer: boolean; isPlayer: boolean;
/*---------- ACTOR ----------*/ /*---------- ACTOR ----------*/
@ -124,6 +126,8 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
this.triggers = new Map(); this.triggers = new Map();
this._velocity = Vec2.ZERO; this._velocity = Vec2.ZERO;
this.sweptRect = new AABB(); this.sweptRect = new AABB();
this.collidedWithTilemap = false;
this.physicsLayer = -1;
if(collisionShape){ if(collisionShape){
this.collisionShape = collisionShape; this.collisionShape = collisionShape;
@ -147,6 +151,10 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
this.triggers.add(group, eventType); this.triggers.add(group, eventType);
}; };
setPhysicsLayer = (layer: string): void => {
this.scene.getPhysicsManager().setLayer(this, layer);
}
/*---------- ACTOR ----------*/ /*---------- ACTOR ----------*/
get ai(): AI { get ai(): AI {
return this._ai; return this._ai;

View File

@ -1,62 +1,83 @@
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import GameNode from "./GameNode";
import Tileset from "../DataTypes/Tilesets/Tileset"; import Tileset from "../DataTypes/Tilesets/Tileset";
import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledData" 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 * The representation of a tilemap - this can consist of a combination of tilesets in one layer
*/ */
export default abstract class Tilemap extends GameNode { export default abstract class Tilemap extends CanvasNode {
// A tileset represents the tiles within one specific image loaded from a file
protected tilesets: Array<Tileset>; protected tilesets: Array<Tileset>;
protected size: Vec2;
protected tileSize: Vec2; protected tileSize: Vec2;
protected scale: Vec2; protected data: Array<number>;
public data: Array<number>; protected collisionMap: Array<boolean>;
public visible: boolean; name: string;
// TODO: Make this no longer be specific to Tiled // TODO: Make this no longer be specific to Tiled
constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array<Tileset>, scale: Vec2) { constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array<Tileset>, scale: Vec2) {
super(); super();
this.tilesets = tilesets; this.tilesets = tilesets;
this.size = new Vec2(0, 0);
this.tileSize = 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 // 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.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[] { getTilesets(): Tileset[] {
return this.tilesets; 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 { 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 { /** Adds this tilemap to the physics system */
return this.scale;
}
setScale(scale: Vec2): void {
this.scale = scale;
}
isVisible(): boolean {
return this.visible;
}
/** Adds this tilemaps to the physics system */
addPhysics = (): void => { addPhysics = (): void => {
this.scene.getPhysicsManager().registerTilemap(this); 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 * Sets up the tileset using the data loaded from file

View File

@ -8,38 +8,84 @@ import Tileset from "../../DataTypes/Tilesets/Tileset";
*/ */
export default class OrthogonalTilemap extends Tilemap { 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 * 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 tilemapData
* @param layer * @param layer
*/ */
protected parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void { 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); 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.data = layer.data;
this.visible = layer.visible; this.visible = layer.visible;
// Whether the tilemap is collidable or not
this.isCollidable = false; this.isCollidable = false;
if(layer.properties){ if(layer.properties){
for(let item of layer.properties){ for(let item of layer.properties){
if(item.name === "Collidable"){ if(item.name === "Collidable"){
this.isCollidable = item.value; 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;
}
} }
} }
} }
} }
/** getTileAtWorldPosition(worldCoords: Vec2): number {
* Get the value of the tile at the coordinates in the vector worldCoords
* @param worldCoords
*/
getTileAt(worldCoords: Vec2): number {
let localCoords = this.getColRowAt(worldCoords); let localCoords = this.getColRowAt(worldCoords);
if(localCoords.x < 0 || localCoords.x >= this.size.x || localCoords.y < 0 || localCoords.y >= this.size.y){ return this.getTileAtRowCol(localCoords);
// There are no tiles in negative positions or out of bounds positions }
return 0;
/**
* 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 * @param row
*/ */
isTileCollidable(indexOrCol: number, row?: number): boolean { isTileCollidable(indexOrCol: number, row?: number): boolean {
let index = 0; // The value of the tile
let tile = 0;
if(row){ if(row){
if(indexOrCol < 0 || indexOrCol >= this.size.x || row < 0 || row >= this.size.y){ // We have a column and a row
// There are no tiles in negative positions or out of bounds positions tile = this.getTileAtRowCol(new Vec2(indexOrCol, row));
if(tile < 0){
return false; return false;
} }
index = row * this.size.x + indexOrCol;
} else { } else {
if(indexOrCol < 0 || indexOrCol >= this.data.length){ if(indexOrCol < 0 || indexOrCol >= this.data.length){
// Tiles that don't exist aren't collidable // Tiles that don't exist aren't collidable
return false; return false;
} }
index = indexOrCol; // We have an index
tile = this.getTile(indexOrCol);
} }
// TODO - Currently, all tiles in a collidable layer are collidable return this.collisionMap[tile];
return this.data[index] !== 0 && this.isCollidable;
} }
/** /**
* Takes in world coordinates and returns the row and column of the tile at that position * Takes in world coordinates and returns the row and column of the tile at that position
* @param worldCoords * @param worldCoords
*/ */
// TODO: Should this throw an error if someone tries to access an out of bounds value?
getColRowAt(worldCoords: Vec2): Vec2 { getColRowAt(worldCoords: Vec2): Vec2 {
let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x); let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x);
let row = Math.floor(worldCoords.y / this.tileSize.y / this.scale.y); let row = Math.floor(worldCoords.y / this.tileSize.y / this.scale.y);
return new Vec2(col, row); return new Vec2(col, row);
} }
@ -94,7 +143,7 @@ export default class OrthogonalTilemap extends Tilemap {
for(let tileset of this.tilesets){ for(let tileset of this.tilesets){
if(tileset.hasTile(tileIndex)){ 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);
} }
} }
} }

View File

@ -8,9 +8,7 @@ import SweepAndPrune from "./BroadPhaseAlgorithms/SweepAndPrune";
import Shape from "../DataTypes/Shapes/Shape"; import Shape from "../DataTypes/Shapes/Shape";
import MathUtils from "../Utils/MathUtils"; import MathUtils from "../Utils/MathUtils";
import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap";
import Debug from "../Debug/Debug";
import AABB from "../DataTypes/Shapes/AABB"; import AABB from "../DataTypes/Shapes/AABB";
import Map from "../DataTypes/Map";
export default class BasicPhysicsManager extends PhysicsManager { 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 */ /** The broad phase collision detection algorithm used by this physics system */
protected broadPhase: BroadPhase; protected broadPhase: BroadPhase;
protected layerMap: Map<number>; /** A 2D array that contains information about which layers interact with each other */
protected layerNames: Array<string>; protected layerMask: number[][];
constructor(physicsOptions: Record<string, any>){ constructor(physicsOptions: Record<string, any>){
super(); super();
@ -35,8 +33,6 @@ export default class BasicPhysicsManager extends PhysicsManager {
this.dynamicNodes = new Array(); this.dynamicNodes = new Array();
this.tilemaps = new Array(); this.tilemaps = new Array();
this.broadPhase = new SweepAndPrune(); this.broadPhase = new SweepAndPrune();
this.layerMap = new Map();
this.layerNames = new Array();
let i = 0; let i = 0;
if(physicsOptions.physicsLayerNames !== null){ if(physicsOptions.physicsLayerNames !== null){
@ -56,7 +52,7 @@ export default class BasicPhysicsManager extends PhysicsManager {
this.layerMap.add("" + i, i); 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 // TODO - This is problematic if a collision happens, but it is later learned that another collision happens before it
if(node1.triggers.has(group2)){ if(node1.triggers.has(group2)){
// Node1 should send an event // Node1 should send an event
console.log("Trigger")
let eventType = node1.triggers.get(group2); let eventType = node1.triggers.get(group2);
this.emitter.fireEvent(eventType, {node: node1, other: node2, collision: {firstContact: firstContact}}); this.emitter.fireEvent(eventType, {node: node1, other: node2, collision: {firstContact: firstContact}});
} }
if(node2.triggers.has(group1)){ if(node2.triggers.has(group1)){
// Node2 should send an event // Node2 should send an event
console.log("Trigger")
let eventType = node2.triggers.get(group1); let eventType = node2.triggers.get(group1);
this.emitter.fireEvent(eventType, {node: node2, other: node1, collision: {firstContact: firstContact}}); 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<TileCollisionData>(); let tilemapCollisions = new Array<TileCollisionData>();
let tileSize = tilemap.getTileSize(); let tileSize = tilemap.getTileSize();
Debug.log("tilemapCollision", "");
// Loop over all possible tiles (which isn't many in the scope of the velocity per frame) // 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 col = minIndex.x; col <= maxIndex.x; col++){
for(let row = minIndex.y; row <= maxIndex.y; row++){ for(let row = minIndex.y; row <= maxIndex.y; row++){
if(tilemap.isTileCollidable(col, row)){ if(tilemap.isTileCollidable(col, row)){
Debug.log("tilemapCollision", "Colliding with Tile");
// Get the position of this tile // Get the position of this tile
let tilePos = new Vec2(col * tileSize.x + tileSize.x/2, row * tileSize.y + tileSize.y/2); 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 // Now that we have all collisions, sort by collision area highest to lowest
tilemapCollisions = tilemapCollisions.sort((a, b) => a.overlapArea - b.overlapArea); 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) // 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 => { tilemapCollisions.forEach(collision => {
@ -233,6 +220,9 @@ export default class BasicPhysicsManager extends PhysicsManager {
// Handle collision // Handle collision
if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ 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(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 // 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 { } else {
@ -263,8 +253,7 @@ export default class BasicPhysicsManager extends PhysicsManager {
} }
} }
}) })
} }
update(deltaT: number): void { update(deltaT: number): void {
/*---------- INITIALIZATION PHASE ----------*/ /*---------- INITIALIZATION PHASE ----------*/
@ -273,6 +262,7 @@ export default class BasicPhysicsManager extends PhysicsManager {
node.onGround = false; node.onGround = false;
node.onCeiling = false; node.onCeiling = false;
node.onWall = false; node.onWall = false;
node.collidedWithTilemap = false;
// Update the swept shapes of each node // Update the swept shapes of each node
if(node.moving){ if(node.moving){
@ -303,17 +293,15 @@ export default class BasicPhysicsManager extends PhysicsManager {
continue; 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) // Get Collision (which may or may not happen)
let [firstContact, lastContact, collidingX, collidingY] = Shape.getTimeOfCollision(node1.collisionShape, node1._velocity, node2.collisionShape, node2._velocity); 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); this.resolveCollision(node1, node2, firstContact, lastContact, collidingX, collidingY);
} }
@ -322,7 +310,10 @@ export default class BasicPhysicsManager extends PhysicsManager {
if(node.moving && node.isCollidable){ if(node.moving && node.isCollidable){
// If a node is moving and can collide, check it against every tilemap // If a node is moving and can collide, check it against every tilemap
for(let tilemap of this.tilemaps){ 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);
}
} }
} }
} }

View File

@ -1,5 +1,4 @@
import { Physical } from "../DataTypes/Interfaces/Descriptors"; import { Physical } from "../DataTypes/Interfaces/Descriptors";
import AABB from "../DataTypes/Shapes/AABB";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
export class Collision { export class Collision {

View File

@ -4,21 +4,52 @@ import { Debug_Renderable, Updateable } from "../DataTypes/Interfaces/Descriptor
import Tilemap from "../Nodes/Tilemap"; import Tilemap from "../Nodes/Tilemap";
import Receiver from "../Events/Receiver"; import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter"; import Emitter from "../Events/Emitter";
import Map from "../DataTypes/Map";
export default abstract class PhysicsManager implements Updateable, Debug_Renderable { export default abstract class PhysicsManager implements Updateable, Debug_Renderable {
protected receiver: Receiver; protected receiver: Receiver;
protected emitter: Emitter; protected emitter: Emitter;
/** Layer names to numbers */
protected layerMap: Map<number>;
/** Layer numbers to names */
protected layerNames: Array<string>;
constructor(){ constructor(){
this.receiver = new Receiver(); this.receiver = new Receiver();
this.emitter = new Emitter(); 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; abstract registerObject(object: GameNode): void;
/**
* Registers a tilemap with this physics manager
* @param tilemap
*/
abstract registerTilemap(tilemap: Tilemap): void; abstract registerTilemap(tilemap: Tilemap): void;
/**
* Updates the physics
* @param deltaT
*/
abstract update(deltaT: number): void; abstract update(deltaT: number): void;
/**
* Renders any debug shapes or graphics
* @param ctx
*/
abstract debug_render(ctx: CanvasRenderingContext2D): void; abstract debug_render(ctx: CanvasRenderingContext2D): void;
setLayer(node: GameNode, layer: string): void {
node.physicsLayer = this.layerMap.get(layer);
}
} }

View File

@ -20,6 +20,7 @@ import ParallaxLayer from "./Layers/ParallaxLayer";
import UILayer from "./Layers/UILayer"; import UILayer from "./Layers/UILayer";
import CanvasNode from "../Nodes/CanvasNode"; import CanvasNode from "../Nodes/CanvasNode";
import GameNode from "../Nodes/GameNode"; import GameNode from "../Nodes/GameNode";
import ArrayUtils from "../Utils/ArrayUtils";
export default class Scene implements Updateable, Renderable { export default class Scene implements Updateable, Renderable {
/** The size of the game world. */ /** 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 */ /** An interface that allows the loading of different files for use in the scene */
public load: ResourceManager; 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<string, any>){
this.sceneOptions = SceneOptions.parse(options);
this.worldSize = new Vec2(500, 500); this.worldSize = new Vec2(500, 500);
this.viewport = viewport; this.viewport = viewport;
this.viewport.setBounds(0, 0, 2560, 1280); this.viewport.setBounds(0, 0, 2560, 1280);
@ -90,7 +96,7 @@ export default class Scene implements Updateable, Renderable {
this.uiLayers = new Map(); this.uiLayers = new Map();
this.parallaxLayers = 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.navManager = new NavigationManager();
this.aiManager = new AIManager(); this.aiManager = new AIManager();
@ -325,4 +331,40 @@ export default class Scene implements Updateable, Renderable {
generateId(): number { generateId(): number {
return this.sceneManager.generateId(); 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<string>,
physicsLayerCollisions: Array<Array<number>>;
}
static parse(options: Record<string, any>): 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;
}
} }

View File

@ -22,9 +22,8 @@ export default class SceneManager {
* Add a scene as the main scene * Add a scene as the main scene
* @param constr The constructor of the scene to add * @param constr The constructor of the scene to add
*/ */
public addScene<T extends Scene>(constr: new (...args: any) => T): void { public addScene<T extends Scene>(constr: new (...args: any) => T, options: Record<string, any>): void {
console.log("Adding Scene"); let scene = new constr(this.viewport, this, this.game, options);
let scene = new constr(this.viewport, this, this.game);
this.currentScene = scene; this.currentScene = scene;
// Enqueue all scene asset loads // Enqueue all scene asset loads
@ -43,7 +42,7 @@ export default class SceneManager {
* Change from the current scene to this new scene * Change from the current scene to this new scene
* @param constr The constructor of the scene to change to * @param constr The constructor of the scene to change to
*/ */
public changeScene<T extends Scene>(constr: new (...args: any) => T): void { public changeScene<T extends Scene>(constr: new (...args: any) => T, options: Record<string, any>): void {
// unload current scene // unload current scene
this.currentScene.unloadScene(); this.currentScene.unloadScene();
@ -51,7 +50,7 @@ export default class SceneManager {
this.viewport.setCenter(0, 0); this.viewport.setCenter(0, 0);
this.addScene(constr); this.addScene(constr, options);
} }
public generateId(): number { public generateId(): number {

View File

@ -7,6 +7,7 @@ import ParallaxLayer from "../../Scene/Layers/ParallaxLayer";
import Scene from "../../Scene/Scene"; import Scene from "../../Scene/Scene";
import PlayerController from "../Player/PlayerController"; import PlayerController from "../Player/PlayerController";
import GoombaController from "../Enemies/GoombaController"; import GoombaController from "../Enemies/GoombaController";
import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap";
export enum MarioEvents { export enum MarioEvents {
PLAYER_HIT_COIN = "PlayerHitCoin", PLAYER_HIT_COIN = "PlayerHitCoin",
@ -27,7 +28,11 @@ export default class Level1 extends Scene {
} }
startScene(): void { 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); this.viewport.setBounds(0, 0, 150*64, 20*64);
// Give parallax to the parallax layers // Give parallax to the parallax layers
@ -37,33 +42,34 @@ export default class Level1 extends Scene {
// Add the player (a rect for now) // 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.addPhysics(); 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 // Add triggers on colliding with coins or coinBlocks
this.player.addTrigger("coin", MarioEvents.PLAYER_HIT_COIN); this.player.addTrigger("coin", MarioEvents.PLAYER_HIT_COIN);
this.player.addTrigger("coinBlock", MarioEvents.PLAYER_HIT_COIN_BLOCK); 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.receiver.subscribe([MarioEvents.PLAYER_HIT_COIN, MarioEvents.PLAYER_HIT_COIN_BLOCK]);
this.viewport.follow(this.player); this.viewport.follow(this.player);
// Add enemies // 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}]){ 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"); let goomba = this.add.sprite("goomba", "Main");
goomba.position.set(pos.x*64, pos.y*64); goomba.position.set(pos.x*64, pos.y*64);
goomba.scale.set(2, 2); goomba.scale.set(2, 2);
goomba.addPhysics(); goomba.addPhysics();
goomba.addAI(GoombaController, {jumpy: false}); goomba.addAI(GoombaController, {jumpy: false});
goomba.setPhysicsLayer("enemy");
} }
for(let pos of [{x: 67, y: 18}, {x: 86, y: 21}, {x: 128, y: 18}]){ for(let pos of [{x: 67, y: 18}, {x: 86, y: 21}, {x: 128, y: 18}]){
let koopa = this.add.sprite("koopa", "Main"); let koopa = this.add.sprite("koopa", "Main");
koopa.position.set(pos.x*64, pos.y*64); koopa.position.set(pos.x*64, pos.y*64);
koopa.scale.set(2, 2); koopa.scale.set(2, 2);
koopa.addPhysics(); koopa.addPhysics();
koopa.addAI(GoombaController, {jumpy: true}); koopa.addAI(GoombaController, {jumpy: true});
koopa.setPhysicsLayer("enemy");
} }
// Add UI // Add UI

View File

@ -2,6 +2,7 @@ import StateMachineAI from "../../AI/StateMachineAI";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
import Debug from "../../Debug/Debug"; import Debug from "../../Debug/Debug";
import GameNode from "../../Nodes/GameNode"; import GameNode from "../../Nodes/GameNode";
import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap";
import IdleTopDown from "./PlayerStates/IdleTopDown"; import IdleTopDown from "./PlayerStates/IdleTopDown";
import MoveTopDown from "./PlayerStates/MoveTopDown"; import MoveTopDown from "./PlayerStates/MoveTopDown";
import Idle from "./PlayerStates/Platformer/Idle"; import Idle from "./PlayerStates/Platformer/Idle";
@ -28,7 +29,8 @@ export default class PlayerController extends StateMachineAI {
velocity: Vec2 = Vec2.ZERO; velocity: Vec2 = Vec2.ZERO;
speed: number = 400; speed: number = 400;
MIN_SPEED: number = 400; MIN_SPEED: number = 400;
MAX_SPEED: number = 1000; MAX_SPEED: number = 1000;
tilemap: OrthogonalTilemap;
initializeAI(owner: GameNode, options: Record<string, any>){ initializeAI(owner: GameNode, options: Record<string, any>){
this.owner = owner; this.owner = owner;
@ -38,6 +40,8 @@ export default class PlayerController extends StateMachineAI {
} else { } else {
this.initializePlatformer(); this.initializePlatformer();
} }
this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap;
} }
/** /**

View File

@ -14,6 +14,23 @@ export default class Jump extends PlayerState {
update(deltaT: number): void { update(deltaT: number): void {
super.update(deltaT); 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){ if(this.owner.onGround){
this.finished(PlayerStates.PREVIOUS); this.finished(PlayerStates.PREVIOUS);
} }

View File

@ -3,7 +3,7 @@ import StateMachine from "../../../../DataTypes/State/StateMachine";
import Vec2 from "../../../../DataTypes/Vec2"; import Vec2 from "../../../../DataTypes/Vec2";
import InputReceiver from "../../../../Input/InputReceiver"; import InputReceiver from "../../../../Input/InputReceiver";
import GameNode from "../../../../Nodes/GameNode"; import GameNode from "../../../../Nodes/GameNode";
import PlayerController from "./PlayerController"; import PlayerController from "../../PlayerController";
export default abstract class PlayerState extends State { export default abstract class PlayerState extends State {

View File

@ -6,15 +6,27 @@ function main(){
// Create the game object // Create the game object
let options = { let options = {
viewportSize: {x: 800, y: 600}, viewportSize: {x: 800, y: 600},
physics: {
physicsLayerNames: ["ground", "player", "enemy", "coin"]
}
} }
let game = new GameLoop(options); let game = new GameLoop(options);
game.start(); 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(); 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 { CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {