added demo level and started work on physics layer support

This commit is contained in:
Joe Weaver 2020-11-15 08:26:49 -05:00
parent ff5a2896fe
commit 9dc8cd29d1
14 changed files with 272 additions and 47 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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