finished implementing physics layers and added support for tilemap editing with code
This commit is contained in:
parent
9dc8cd29d1
commit
6149b983a5
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,4 +66,12 @@ export default class Map<T> 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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -286,29 +286,12 @@ export default class GameLoop {
|
|||
|
||||
class GameOptions {
|
||||
viewportSize: {x: number, y: number}
|
||||
physics: {
|
||||
numPhysicsLayers: number,
|
||||
physicsLayerNames: Array<string>,
|
||||
physicsLayerCollisions: Array<Array<number>>;
|
||||
}
|
||||
|
||||
static parse(options: Record<string, any>): 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;
|
||||
}
|
||||
}
|
|
@ -35,6 +35,8 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
|
|||
triggers: Map<string>;
|
||||
_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;
|
||||
|
|
|
@ -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<Tileset>;
|
||||
protected size: Vec2;
|
||||
protected tileSize: Vec2;
|
||||
protected scale: Vec2;
|
||||
public data: Array<number>;
|
||||
public visible: boolean;
|
||||
protected data: Array<number>;
|
||||
protected collisionMap: Array<boolean>;
|
||||
name: string;
|
||||
|
||||
// TODO: Make this no longer be specific to Tiled
|
||||
constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array<Tileset>, 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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<number>;
|
||||
protected layerNames: Array<string>;
|
||||
/** A 2D array that contains information about which layers interact with each other */
|
||||
protected layerMask: number[][];
|
||||
|
||||
constructor(physicsOptions: Record<string, any>){
|
||||
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<TileCollisionData>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Physical } from "../DataTypes/Interfaces/Descriptors";
|
||||
import AABB from "../DataTypes/Shapes/AABB";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
|
||||
export class Collision {
|
||||
|
|
|
@ -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<number>;
|
||||
|
||||
/** Layer numbers to names */
|
||||
protected layerNames: Array<string>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<string, any>){
|
||||
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<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;
|
||||
}
|
||||
}
|
|
@ -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<T extends Scene>(constr: new (...args: any) => T): void {
|
||||
console.log("Adding Scene");
|
||||
let scene = new constr(this.viewport, this, this.game);
|
||||
public addScene<T extends Scene>(constr: new (...args: any) => T, options: Record<string, any>): 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<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
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string, any>){
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
20
src/main.ts
20
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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user