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