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 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;
}
/**

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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

View 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;
}
}
}
}
}
getTileAtWorldPosition(worldCoords: Vec2): number {
let localCoords = this.getColRowAt(worldCoords);
return this.getTileAtRowCol(localCoords);
}
/**
* Get the value of the tile at the coordinates in the vector worldCoords
* @param worldCoords
* Get the tile at the specified row and column
* @param rowCol
*/
getTileAt(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;
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);
}
}
}

View File

@ -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);
@ -223,16 +214,15 @@ 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 => {
let [firstContact, _, collidingX, collidingY] = Shape.getTimeOfCollision(node.collisionShape, velocity, collision.collider, Vec2.ZERO);
// 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 {
@ -265,7 +255,6 @@ export default class BasicPhysicsManager extends PhysicsManager {
})
}
update(deltaT: number): void {
/*---------- INITIALIZATION PHASE ----------*/
for(let node of this.dynamicNodes){
@ -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,10 +310,13 @@ 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){
// Check if there could even be a collision
if(node.sweptRect.overlaps(tilemap.boundary)){
this.collideWithTilemap(node, tilemap, node._velocity);
}
}
}
}
/*---------- ENDING PHASE ----------*/
for(let node of this.dynamicNodes){

View File

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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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";
@ -29,6 +30,7 @@ export default class PlayerController extends StateMachineAI {
speed: number = 400;
MIN_SPEED: number = 400;
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;
}
/**

View File

@ -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);
}

View File

@ -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 {

View File

@ -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 {