From ff5a2896feb3e7348b542923e17598019045db60 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Wed, 4 Nov 2020 14:03:52 -0500 Subject: [PATCH] added pathfinding and ai factories, split layers, and made fixes to other factories --- src/AI/AIManager.ts | 39 ++++ src/AI/StateMachineAI.ts | 9 + src/BoidDemo.ts | 40 ++-- src/DataTypes/Graphs/Graph.ts | 45 ++++- src/DataTypes/Graphs/PositionGraph.ts | 15 +- src/DataTypes/Interfaces/Descriptors.ts | 33 ++++ src/DataTypes/Navmesh.ts | 27 --- src/DataTypes/Stack.ts | 21 ++- src/DataTypes/Vec2.ts | 50 ++++- src/Debug/Debug.ts | 6 +- src/Nodes/GameNode.ts | 61 ++++-- src/Nodes/Graphics/GraphicTypes.ts | 4 + src/Nodes/Graphics/Point.ts | 7 +- src/Nodes/Graphics/Rect.ts | 4 +- src/Nodes/Sprites/Sprite.ts | 4 +- src/Nodes/Tilemaps/OrthogonalTilemap.ts | 4 +- src/Nodes/UIElement.ts | 7 +- src/Nodes/UIElements/Button.ts | 6 +- src/Nodes/UIElements/Label.ts | 5 +- src/Nodes/UIElements/UIElementTypes.ts | 4 + src/Pathfinding/NavigationManager.ts | 22 +++ src/Pathfinding/NavigationPath.ts | 35 ++++ src/Pathfinding/Navmesh.ts | 53 ++++++ src/Physics/Collisions.ts | 12 ++ src/Scene/Factories/CanvasNodeFactory.ts | 123 +++++++++++-- src/Scene/Factories/TilemapFactory.ts | 30 ++- src/Scene/Layer.ts | 37 ++-- src/Scene/Layers/ParallaxLayer.ts | 12 ++ src/Scene/Layers/UILayer.ts | 9 + src/Scene/Scene.ts | 174 +++++++++++++++++- src/SceneGraph/SceneGraph.ts | 16 -- src/SceneGraph/SceneGraphArray.ts | 9 - src/SceneGraph/SceneGraphQuadTree.ts | 9 - src/SceneGraph/Viewport.ts | 12 +- src/Utils/GraphUtils.ts | 43 ++++- src/Utils/MathUtils.ts | 10 +- src/_DemoClasses/Boids/Boid.ts | 8 +- .../Boids/BoidStates/BoidBehavior.ts | 12 -- src/_DemoClasses/Enemies/GoombaController.ts | 8 - src/_DemoClasses/MarioClone/MarioClone.ts | 11 +- .../Pathfinder/PathfinderController.ts | 22 +++ .../Pathfinding/Pathfinder/PathfinderIdle.ts | 17 ++ .../Pathfinding/Pathfinder/PathfinderNav.ts | 41 +++++ .../Pathfinding/PathfindingScene.ts | 32 +++- src/_DemoClasses/Player/Player.ts | 4 +- src/_DemoClasses/Player/PlayerController.ts | 19 +- .../Platformer/PlayerController.ts | 10 +- 47 files changed, 956 insertions(+), 225 deletions(-) create mode 100644 src/AI/AIManager.ts create mode 100644 src/AI/StateMachineAI.ts delete mode 100644 src/DataTypes/Navmesh.ts create mode 100644 src/Nodes/Graphics/GraphicTypes.ts create mode 100644 src/Nodes/UIElements/UIElementTypes.ts create mode 100644 src/Pathfinding/NavigationManager.ts create mode 100644 src/Pathfinding/NavigationPath.ts create mode 100644 src/Pathfinding/Navmesh.ts create mode 100644 src/Physics/Collisions.ts create mode 100644 src/Scene/Layers/ParallaxLayer.ts create mode 100644 src/Scene/Layers/UILayer.ts create mode 100644 src/_DemoClasses/Pathfinding/Pathfinder/PathfinderController.ts create mode 100644 src/_DemoClasses/Pathfinding/Pathfinder/PathfinderIdle.ts create mode 100644 src/_DemoClasses/Pathfinding/Pathfinder/PathfinderNav.ts diff --git a/src/AI/AIManager.ts b/src/AI/AIManager.ts new file mode 100644 index 0000000..afb8664 --- /dev/null +++ b/src/AI/AIManager.ts @@ -0,0 +1,39 @@ +import { Actor, AI, Updateable } from "../DataTypes/Interfaces/Descriptors"; +import Map from "../DataTypes/Map"; + +export default class AIManager implements Updateable { + actors: Array; + + registeredAI: Map() => T>; + + constructor(){ + this.actors = new Array(); + this.registeredAI = new Map(); + } + + /** + * Registers an actor with the AIManager + * @param actor The actor to register + */ + registerActor(actor: Actor): void { + actor.actorId = this.actors.length; + this.actors.push(actor); + } + + registerAI(name: string, constr: new () => T ): void { + this.registeredAI.add(name, constr); + } + + generateAI(name: string): AI { + if(this.registeredAI.has(name)){ + return new (this.registeredAI.get(name))(); + } else { + throw `Cannot create AI with name ${name}, no AI with that name is registered`; + } + } + + update(deltaT: number): void { + // Run the ai for every active actor + this.actors.forEach(actor => { if(actor.aiActive) actor.ai.update(deltaT) }); + } +} \ No newline at end of file diff --git a/src/AI/StateMachineAI.ts b/src/AI/StateMachineAI.ts new file mode 100644 index 0000000..0e4f464 --- /dev/null +++ b/src/AI/StateMachineAI.ts @@ -0,0 +1,9 @@ +import { AI } from "../DataTypes/Interfaces/Descriptors"; +import StateMachine from "../DataTypes/State/StateMachine"; +import GameNode from "../Nodes/GameNode"; + +export default class StateMachineAI extends StateMachine implements AI { + protected owner: GameNode; + + initializeAI(owner: GameNode, config: Record): void {} +} \ No newline at end of file diff --git a/src/BoidDemo.ts b/src/BoidDemo.ts index 4878d9d..5289092 100644 --- a/src/BoidDemo.ts +++ b/src/BoidDemo.ts @@ -18,29 +18,29 @@ export default class BoidDemo extends Scene { startScene(){ // Set the world size - this.worldSize = new Vec2(800, 600); - this.sceneGraph = new SceneGraphQuadTree(this.viewport, this); - this.viewport.setBounds(0, 0, 800, 600) - this.viewport.setCenter(400, 300); + // this.worldSize = new Vec2(800, 600); + // this.sceneGraph = new SceneGraphQuadTree(this.viewport, this); + // this.viewport.setBounds(0, 0, 800, 600) + // this.viewport.setCenter(400, 300); - let layer = this.addLayer(); - this.boids = new Array(); + // let layer = this.addLayer(); + // this.boids = new Array(); - // Add the player - let player = this.add.graphic(Player, layer, new Vec2(0, 0)); - player.addPhysics(); - let ai = new PlayerController(player, "topdown"); - player.update = (deltaT: number) => {ai.update(deltaT)} - this.viewport.follow(player); - this.viewport.enableZoom(); + // // Add the player + // let player = this.add.graphic(Player, layer, new Vec2(0, 0)); + // player.addPhysics(); + // let ai = new PlayerController(player, "topdown"); + // player.update = (deltaT: number) => {ai.update(deltaT)} + // this.viewport.follow(player); + // this.viewport.enableZoom(); - // Create a bunch of boids - for(let i = 0; i < 150; i++){ - let boid = this.add.graphic(Boid, layer, new Vec2(this.worldSize.x*Math.random(), this.worldSize.y*Math.random())); - boid.fb = new FlockBehavior(this, boid, this.boids, 75, 50); - boid.size.set(5, 5); - this.boids.push(boid); - } + // // Create a bunch of boids + // for(let i = 0; i < 150; i++){ + // let boid = this.add.graphic(Boid, layer, new Vec2(this.worldSize.x*Math.random(), this.worldSize.y*Math.random())); + // boid.fb = new FlockBehavior(this, boid, this.boids, 75, 50); + // boid.size.set(5, 5); + // this.boids.push(boid); + // } } updateScene(deltaT: number): void { diff --git a/src/DataTypes/Graphs/Graph.ts b/src/DataTypes/Graphs/Graph.ts index 9105b7e..802c63b 100644 --- a/src/DataTypes/Graphs/Graph.ts +++ b/src/DataTypes/Graphs/Graph.ts @@ -27,9 +27,22 @@ export default class Graph { addEdge(x: number, y: number, weight?: number){ let edge = new EdgeNode(y, weight); - edge.next = this.edges[x]; - + if(this.edges[x]){ + edge.next = this.edges[x]; + } + this.edges[x] = edge; + + if(!this.directed){ + edge = new EdgeNode(x, weight); + + if(this.edges[y]){ + edge.next = this.edges[y]; + } + + this.edges[y] = edge; + } + this.numEdges += 1; } @@ -40,6 +53,34 @@ export default class Graph { getDegree(x: number): number { return this.degree[x]; } + + protected nodeToString(index: number): string { + return "Node " + index; + } + + toString(): string { + let retval = ""; + + for(let i = 0; i < this.numVertices; i++){ + let edge = this.edges[i]; + let edgeStr = ""; + while(edge !== null){ + edgeStr += edge.y.toString(); + if(this.weighted){ + edgeStr += " (" + edge.weight + ")"; + } + if(edge.next !== null){ + edgeStr += ", "; + } + + edge = edge.next; + } + + retval += this.nodeToString(i) + ": " + edgeStr + "\n"; + } + + return retval; + } } export class EdgeNode { diff --git a/src/DataTypes/Graphs/PositionGraph.ts b/src/DataTypes/Graphs/PositionGraph.ts index bc73695..adbc64a 100644 --- a/src/DataTypes/Graphs/PositionGraph.ts +++ b/src/DataTypes/Graphs/PositionGraph.ts @@ -10,21 +10,34 @@ export default class PositionGraph extends Graph implements Debug_Renderable{ this.positions = new Array(MAX_V); } + addPositionedNode(position: Vec2){ + this.positions[this.numVertices] = position; + this.addNode(); + } + setNodePosition(index: number, position: Vec2): void { this.positions[index] = position; } + getNodePosition(index: number): Vec2 { + return this.positions[index]; + } + addEdge(x: number, y: number): void { if(!this.positions[x] || !this.positions[y]){ throw "Can't add edge to un-positioned node!"; } // Weight is the distance between the nodes - let weight = this.positions[x].distanceSqTo(this.positions[y]); + let weight = this.positions[x].distanceTo(this.positions[y]); super.addEdge(x, y, weight); } + protected nodeToString(index: number): string { + return "Node " + index + " - " + this.positions[index].toString(); + } + debug_render(ctx: CanvasRenderingContext2D, origin: Vec2, zoom: number): void { for(let point of this.positions){ ctx.fillRect((point.x - origin.x - 4)*zoom, (point.y - origin.y - 4)*zoom, 8, 8); diff --git a/src/DataTypes/Interfaces/Descriptors.ts b/src/DataTypes/Interfaces/Descriptors.ts index 7ad3394..de64dd0 100644 --- a/src/DataTypes/Interfaces/Descriptors.ts +++ b/src/DataTypes/Interfaces/Descriptors.ts @@ -3,6 +3,8 @@ import Map from "../Map"; import AABB from "../Shapes/AABB"; import Shape from "../Shapes/Shape"; import Vec2 from "../Vec2"; +import NavigationPath from "../../Pathfinding/NavigationPath"; +import GameNode from "../../Nodes/GameNode"; export interface Unique { /** The unique id of this object. */ @@ -107,6 +109,37 @@ export interface Physical { addTrigger: (group: string, eventType: string) => void; } +/** + * Defines a controller for a bot or a human. Must be able to update + */ +export interface AI extends Updateable { + /** Initializes the AI with the actor and any additional config */ + initializeAI: (owner: GameNode, config: Record) => void; +} + +export interface Actor { + /** The AI of the actor */ + ai: AI; + + /** The activity status of the actor */ + aiActive: boolean; + + /** The id of the actor according to the AIManager */ + actorId: number; + + path: NavigationPath; + + pathfinding: boolean; + + addAI: (ai: string | (new () => T), options: Record) => void; + + setAIActive: (active: boolean) => void; +} + +export interface Navigable { + getNavigationPath: (fromPosition: Vec2, toPosition: Vec2) => NavigationPath; +} + export interface Updateable { /** Updates this object. */ update: (deltaT: number) => void; diff --git a/src/DataTypes/Navmesh.ts b/src/DataTypes/Navmesh.ts deleted file mode 100644 index b748970..0000000 --- a/src/DataTypes/Navmesh.ts +++ /dev/null @@ -1,27 +0,0 @@ -import PositionGraph from "./Graphs/PositionGraph" -import Vec2 from "./Vec2"; - -export default class Navmesh { - protected graph: PositionGraph; - - getNavigationPath(fromPosition: Vec2, toPosition: Vec2): Array { - return []; - } - - getClosestNode(position: Vec2): number { - let n = this.graph.numVertices; - let i = 1; - let index = 0; - let dist = position.distanceSqTo(this.graph.positions[0]); - while(i < n){ - let d = position.distanceSqTo(this.graph.positions[i]); - if(d < dist){ - dist = d; - index = i; - } - i++; - } - - return index; - } -} \ No newline at end of file diff --git a/src/DataTypes/Stack.ts b/src/DataTypes/Stack.ts index 338cdee..7c05771 100644 --- a/src/DataTypes/Stack.ts +++ b/src/DataTypes/Stack.ts @@ -47,6 +47,11 @@ export default class Stack implements Collection { return this.stack[this.head]; } + /** Returns true if this stack is empty */ + isEmpty(): boolean { + return this.head === -1; + } + clear(): void { this.forEach((item, index) => delete this.stack[index]); this.head = -1; @@ -62,8 +67,22 @@ export default class Stack implements Collection { forEach(func: (item: T, index?: number) => void): void{ let i = 0; while(i <= this.head){ - func(this.stack[i]); + func(this.stack[i], i); i += 1; } } + + toString(): string { + let retval = ""; + + this.forEach( (item, index) => { + let str = item.toString() + if(index !== 0){ + str += " -> " + } + retval = str + retval; + }); + + return "Top -> " + retval; + } } \ No newline at end of file diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index 08c49e0..69ae54f 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -1,3 +1,5 @@ +import MathUtils from "../Utils/MathUtils"; + /** * A two-dimensional vector (x, y) */ @@ -99,10 +101,11 @@ export default class Vec2 { /** * Sets the vector's x and y based on the angle provided. Goes counter clockwise. * @param angle The angle in radians + * @param radius The magnitude of the vector at the specified angle */ - setToAngle(angle: number): Vec2 { - this.x = Math.cos(angle); - this.y = Math.sin(angle); + setToAngle(angle: number, radius: number = 1): Vec2 { + this.x = MathUtils.floorToPlace(Math.cos(angle)*radius, 5); + this.y = MathUtils.floorToPlace(-Math.sin(angle)*radius, 5); return this; } @@ -113,6 +116,14 @@ export default class Vec2 { vecTo(other: Vec2): Vec2 { return new Vec2(other.x - this.x, other.y - this.y); } + + /** + * Returns a vector containing the direction from this vector to another + * @param other + */ + dirTo(other: Vec2): Vec2 { + return this.vecTo(other).normalize(); + } /** * Keeps the vector's direction, but sets its magnitude to be the provided magnitude @@ -245,6 +256,22 @@ export default class Vec2 { return this.x*other.x + this.y*other.y; } + /** + * Returns the angle counter-clockwise in radians from this vector to another vector + * @param other + */ + angleToCCW(other: Vec2): number { + let dot = this.dot(other); + let det = this.x*other.y - this.y*other.x; + let angle = -Math.atan2(det, dot); + + if(angle < 0){ + angle += 2*Math.PI; + } + + return angle; + } + /** * Returns a string representation of this vector rounded to 1 decimal point */ @@ -267,12 +294,23 @@ export default class Vec2 { return new Vec2(this.x, this.y); } + /** + * Returns true if this vector and other have the EXACT same x and y (not assured to be safe for floats) + * @param other The vector to check against + */ + strictEquals(other: Vec2): boolean { + return this.x === other.x && this.y === other.y; + } + /** * Returns true if this vector and other have the same x and y * @param other The vector to check against */ equals(other: Vec2): boolean { - return this.x === other.x && this.y === other.y; + let xEq = Math.abs(this.x - other.x) < 0.00000001; + let yEq = Math.abs(this.y - other.y) < 0.00000001; + + return xEq && yEq; } /** @@ -293,4 +331,8 @@ export default class Vec2 { getOnChange(): string { return this.onChange.toString(); } + + static lerp(a: Vec2, b: Vec2, t: number): Vec2 { + return new Vec2(MathUtils.lerp(a.x, b.x, t), MathUtils.lerp(a.y, b.y, t)); + } } \ No newline at end of file diff --git a/src/Debug/Debug.ts b/src/Debug/Debug.ts index f986e38..db56392 100644 --- a/src/Debug/Debug.ts +++ b/src/Debug/Debug.ts @@ -5,7 +5,11 @@ export default class Debug { // A map of log messages to display on the screen private static logMessages: Map = new Map(); - static log(id: string, message: string): void { + static log(id: string, ...messages: any): void { + let message = ""; + for(let i = 0; i < messages.length; i++){ + message += messages[i].toString(); + } this.logMessages.add(id, message); } diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index f27709e..064df9c 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -4,17 +4,16 @@ import Receiver from "../Events/Receiver"; import Emitter from "../Events/Emitter"; import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; -import { Physical, Positioned, isRegion, Unique, Updateable, Region } from "../DataTypes/Interfaces/Descriptors" +import { Physical, Positioned, isRegion, Unique, Updateable, Actor, AI } from "../DataTypes/Interfaces/Descriptors" import Shape from "../DataTypes/Shapes/Shape"; -import GameEvent from "../Events/GameEvent"; import Map from "../DataTypes/Map"; import AABB from "../DataTypes/Shapes/AABB"; -import Debug from "../Debug/Debug"; +import NavigationPath from "../Pathfinding/NavigationPath"; /** * The representation of an object in the game world */ -export default abstract class GameNode implements Positioned, Unique, Updateable, Physical { +export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor { /*---------- POSITIONED ----------*/ private _position: Vec2; @@ -38,6 +37,13 @@ export default abstract class GameNode implements Positioned, Unique, Updateable sweptRect: AABB; isPlayer: boolean; + /*---------- ACTOR ----------*/ + _ai: AI; + aiActive: boolean; + actorId: number; + path: NavigationPath; + pathfinding: boolean = false; + protected input: InputReceiver; protected receiver: Receiver; protected emitter: Emitter; @@ -93,6 +99,9 @@ export default abstract class GameNode implements Positioned, Unique, Updateable finishMove = (): void => { this.moving = false; this.position.add(this._velocity); + if(this.pathfinding){ + this.path.handlePathProgress(this); + } } /** @@ -138,6 +147,41 @@ export default abstract class GameNode implements Positioned, Unique, Updateable this.triggers.add(group, eventType); }; + /*---------- ACTOR ----------*/ + get ai(): AI { + return this._ai; + } + + set ai(ai: AI) { + if(!this._ai){ + // If we haven't been previously had an ai, register us with the ai manager + this.scene.getAIManager().registerActor(this); + } + + this._ai = ai; + this.aiActive = true; + } + + addAI(ai: string | (new () => T), options?: Record): void { + if(!this._ai){ + this.scene.getAIManager().registerActor(this); + } + + if(typeof ai === "string"){ + this._ai = this.scene.getAIManager().generateAI(ai); + } else { + this._ai = new ai(); + } + + this._ai.initializeAI(this, options); + + this.aiActive = true; + } + + setAIActive(active: boolean): void { + this.aiActive = active; + } + /*---------- GAME NODE ----------*/ /** * Sets the scene for this object. @@ -175,14 +219,5 @@ export default abstract class GameNode implements Positioned, Unique, Updateable } }; - // TODO - This doesn't seem ideal. Is there a better way to do this? - getViewportOriginWithParallax(): Vec2 { - return this.scene.getViewport().getOrigin().mult(this.layer.getParallax()); - } - - getViewportScale(): number { - return this.scene.getViewport().getZoomLevel(); - } - abstract update(deltaT: number): void; } \ No newline at end of file diff --git a/src/Nodes/Graphics/GraphicTypes.ts b/src/Nodes/Graphics/GraphicTypes.ts new file mode 100644 index 0000000..4ed20f9 --- /dev/null +++ b/src/Nodes/Graphics/GraphicTypes.ts @@ -0,0 +1,4 @@ +export enum GraphicType { + POINT = "POINT", + RECT = "RECT", +} \ No newline at end of file diff --git a/src/Nodes/Graphics/Point.ts b/src/Nodes/Graphics/Point.ts index e9366f8..3b30729 100644 --- a/src/Nodes/Graphics/Point.ts +++ b/src/Nodes/Graphics/Point.ts @@ -12,11 +12,12 @@ export default class Point extends Graphic { update(deltaT: number): void {} render(ctx: CanvasRenderingContext2D): void { - let origin = this.getViewportOriginWithParallax(); + let origin = this.scene.getViewTranslation(this); + let zoom = this.scene.getViewScale(); ctx.fillStyle = this.color.toStringRGBA(); - ctx.fillRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, - this.size.x, this.size.y); + ctx.fillRect((this.position.x - origin.x - this.size.x/2)*zoom, (this.position.y - origin.y - this.size.y/2)*zoom, + this.size.x*zoom, this.size.y*zoom); } } \ No newline at end of file diff --git a/src/Nodes/Graphics/Rect.ts b/src/Nodes/Graphics/Rect.ts index cb6c09a..e0dbd0d 100644 --- a/src/Nodes/Graphics/Rect.ts +++ b/src/Nodes/Graphics/Rect.ts @@ -34,8 +34,8 @@ export default class Rect extends Graphic { update(deltaT: number): void {} render(ctx: CanvasRenderingContext2D): void { - let origin = this.getViewportOriginWithParallax(); - let zoom = this.getViewportScale(); + let origin = this.scene.getViewTranslation(this); + let zoom = this.scene.getViewScale(); if(this.color.a !== 0){ ctx.fillStyle = this.color.toStringRGB(); diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index 98d6c6b..dc285e0 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -29,8 +29,8 @@ export default class Sprite extends CanvasNode { render(ctx: CanvasRenderingContext2D): void { let image = ResourceManager.getInstance().getImage(this.imageId); - let origin = this.getViewportOriginWithParallax(); - let zoom = this.getViewportScale(); + let origin = this.scene.getViewTranslation(this); + let zoom = this.scene.getViewScale(); ctx.drawImage(image, this.imageOffset.x, this.imageOffset.y, this.size.x, this.size.y, diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index 44911c6..56a9d14 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -85,8 +85,8 @@ export default class OrthogonalTilemap extends Tilemap { let previousAlpha = ctx.globalAlpha; ctx.globalAlpha = this.getLayer().getAlpha(); - let origin = this.getViewportOriginWithParallax(); - let zoom = this.getViewportScale(); + let origin = this.scene.getViewTranslation(this); + let zoom = this.scene.getViewScale(); if(this.visible){ for(let i = 0; i < this.data.length; i++){ diff --git a/src/Nodes/UIElement.ts b/src/Nodes/UIElement.ts index c3c4bd1..51f4dc8 100644 --- a/src/Nodes/UIElement.ts +++ b/src/Nodes/UIElement.ts @@ -31,8 +31,10 @@ export default class UIElement extends CanvasNode { protected isClicked: boolean; protected isEntered: boolean; - constructor(){ + constructor(position: Vec2){ super(); + this.position = position; + this.textColor = new Color(0, 0, 0, 1); this.backgroundColor = new Color(0, 0, 0, 0); this.borderColor = new Color(0, 0, 0, 0); @@ -82,6 +84,7 @@ export default class UIElement extends CanvasNode { } if(this.onClickEventId !== null){ let data = {}; + console.log("Click event: " + this.onClickEventId) this.emitter.fireEvent(this.onClickEventId, data); } } @@ -179,7 +182,7 @@ export default class UIElement extends CanvasNode { let previousAlpha = ctx.globalAlpha; ctx.globalAlpha = this.getLayer().getAlpha(); - let origin = this.getViewportOriginWithParallax(); + let origin = this.scene.getViewTranslation(this); ctx.font = this.fontSize + "px " + this.font; let offset = this.calculateOffset(ctx); diff --git a/src/Nodes/UIElements/Button.ts b/src/Nodes/UIElements/Button.ts index e56b0d5..30887af 100644 --- a/src/Nodes/UIElements/Button.ts +++ b/src/Nodes/UIElements/Button.ts @@ -4,8 +4,10 @@ import Vec2 from "../../DataTypes/Vec2"; export default class Button extends UIElement{ - constructor(){ - super(); + constructor(position: Vec2, text: string){ + super(position); + this.text = text; + this.backgroundColor = new Color(150, 75, 203); this.borderColor = new Color(41, 46, 30); this.textColor = new Color(255, 255, 255); diff --git a/src/Nodes/UIElements/Label.ts b/src/Nodes/UIElements/Label.ts index fbadd7e..2b9376c 100644 --- a/src/Nodes/UIElements/Label.ts +++ b/src/Nodes/UIElements/Label.ts @@ -1,8 +1,9 @@ +import Vec2 from "../../DataTypes/Vec2"; import UIElement from "../UIElement"; export default class Label extends UIElement{ - constructor(text: string){ - super(); + constructor(position: Vec2, text: string){ + super(position); this.text = text; } } \ No newline at end of file diff --git a/src/Nodes/UIElements/UIElementTypes.ts b/src/Nodes/UIElements/UIElementTypes.ts new file mode 100644 index 0000000..cf97070 --- /dev/null +++ b/src/Nodes/UIElements/UIElementTypes.ts @@ -0,0 +1,4 @@ +export enum UIElementType { + BUTTON = "BUTTON", + LABEL = "LABEL", +} \ No newline at end of file diff --git a/src/Pathfinding/NavigationManager.ts b/src/Pathfinding/NavigationManager.ts new file mode 100644 index 0000000..6c363da --- /dev/null +++ b/src/Pathfinding/NavigationManager.ts @@ -0,0 +1,22 @@ +import { Navigable } from "../DataTypes/Interfaces/Descriptors" +import Map from "../DataTypes/Map"; +import Vec2 from "../DataTypes/Vec2"; +import NavigationPath from "./NavigationPath"; + +export default class NavigationManager { + + protected navigableEntities: Map; + + constructor(){ + this.navigableEntities = new Map(); + } + + addNavigableEntity(navName: string, nav: Navigable){ + this.navigableEntities.add(navName, nav); + } + + getPath(navName: string, fromPosition: Vec2, toPosition: Vec2): NavigationPath { + let nav = this.navigableEntities.get(navName); + return nav.getNavigationPath(fromPosition.clone(), toPosition.clone()); + } +} \ No newline at end of file diff --git a/src/Pathfinding/NavigationPath.ts b/src/Pathfinding/NavigationPath.ts new file mode 100644 index 0000000..9277322 --- /dev/null +++ b/src/Pathfinding/NavigationPath.ts @@ -0,0 +1,35 @@ +import Stack from "../DataTypes/Stack"; +import Vec2 from "../DataTypes/Vec2"; +import GameNode from "../Nodes/GameNode"; + +/** + * A path that AIs can follow. Uses finishMove() in Physical to determine progress on the route + */ +export default class NavigationPath { + protected path: Stack + protected currentMoveDirection: Vec2; + protected distanceThreshold: number; + + constructor(path: Stack){ + this.path = path; + console.log(path.toString()) + this.currentMoveDirection = Vec2.ZERO; + this.distanceThreshold = 20; + } + + isDone(): boolean { + return this.path.isEmpty(); + } + + getMoveDirection(node: GameNode): Vec2 { + // Return direction to next point in the nav + return node.position.dirTo(this.path.peek()); + } + + handlePathProgress(node: GameNode): void { + if(node.position.distanceSqTo(this.path.peek()) < this.distanceThreshold*this.distanceThreshold){ + // We've reached our node, move on to the next destination + this.path.pop(); + } + } +} \ No newline at end of file diff --git a/src/Pathfinding/Navmesh.ts b/src/Pathfinding/Navmesh.ts new file mode 100644 index 0000000..d6e19e2 --- /dev/null +++ b/src/Pathfinding/Navmesh.ts @@ -0,0 +1,53 @@ +import PositionGraph from "../DataTypes/Graphs/PositionGraph"; +import { Navigable } from "../DataTypes/Interfaces/Descriptors"; +import Stack from "../DataTypes/Stack"; +import Vec2 from "../DataTypes/Vec2"; +import GraphUtils from "../Utils/GraphUtils"; +import NavigationPath from "./NavigationPath"; + +export default class Navmesh implements Navigable { + protected graph: PositionGraph; + + constructor(graph: PositionGraph){ + this.graph = graph; + } + + getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath { + let start = this.getClosestNode(fromPosition); + let end = this.getClosestNode(toPosition); + + let parent = GraphUtils.djikstra(this.graph, start); + + let pathStack = new Stack(this.graph.numVertices); + + // Push the final position and the final position in the graph + pathStack.push(toPosition.clone()); + pathStack.push(this.graph.positions[end]); + + // Add all parents along the path + let i = end; + while(parent[i] !== -1){ + pathStack.push(this.graph.positions[parent[i]]); + i = parent[i]; + } + + return new NavigationPath(pathStack); + } + + getClosestNode(position: Vec2): number { + let n = this.graph.numVertices; + let i = 1; + let index = 0; + let dist = position.distanceSqTo(this.graph.positions[0]); + while(i < n){ + let d = position.distanceSqTo(this.graph.positions[i]); + if(d < dist){ + dist = d; + index = i; + } + i++; + } + + return index; + } +} \ No newline at end of file diff --git a/src/Physics/Collisions.ts b/src/Physics/Collisions.ts new file mode 100644 index 0000000..7e5b8a9 --- /dev/null +++ b/src/Physics/Collisions.ts @@ -0,0 +1,12 @@ +import { Physical } from "../DataTypes/Interfaces/Descriptors"; +import AABB from "../DataTypes/Shapes/AABB"; +import Vec2 from "../DataTypes/Vec2"; + +export class Collision { + firstContact: Vec2; + lastContact: Vec2; + collidingX: boolean; + collidingY: boolean; + node1: Physical; + node2: Physical; +} \ No newline at end of file diff --git a/src/Scene/Factories/CanvasNodeFactory.ts b/src/Scene/Factories/CanvasNodeFactory.ts index baaa374..c1b0da2 100644 --- a/src/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Scene/Factories/CanvasNodeFactory.ts @@ -1,9 +1,16 @@ import Scene from "../Scene"; -import SceneGraph from "../../SceneGraph/SceneGraph"; import UIElement from "../../Nodes/UIElement"; import Layer from "../Layer"; import Graphic from "../../Nodes/Graphic"; import Sprite from "../../Nodes/Sprites/Sprite"; +import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; +import { UIElementType } from "../../Nodes/UIElements/UIElementTypes"; +import Point from "../../Nodes/Graphics/Point"; +import Vec2 from "../../DataTypes/Vec2"; +import Shape from "../../DataTypes/Shapes/Shape"; +import Button from "../../Nodes/UIElements/Button"; +import Label from "../../Nodes/UIElements/Label"; +import Rect from "../../Nodes/Graphics/Rect"; export default class CanvasNodeFactory { private scene: Scene; @@ -14,20 +21,33 @@ export default class CanvasNodeFactory { /** * Adds an instance of a UIElement to the current scene - i.e. any class that extends UIElement - * @param constr The constructor of the UIElement to be created - * @param layer The layer to add the UIElement to - * @param args Any additional arguments to feed to the constructor + * @param type The type of UIElement to add + * @param layerName The layer to add the UIElement to + * @param options Any additional arguments to feed to the constructor */ - addUIElement = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { - let instance = new constr(...args); + addUIElement = (type: string | UIElementType, layerName: string, options?: Record): UIElement => { + // Get the layer + let layer = this.scene.getLayer(layerName); + + let instance: UIElement; + + switch(type){ + case UIElementType.BUTTON: + instance = this.buildButton(options); + break; + case UIElementType.LABEL: + instance = this.buildLabel(options); + break; + default: + throw `UIElementType '${type}' does not exist, or is registered incorrectly.` + } - // Add instance to scene instance.setScene(this.scene); instance.id = this.scene.generateId(); this.scene.getSceneGraph().addNode(instance); // Add instance to layer - layer.addNode(instance); + layer.addNode(instance) return instance; } @@ -35,9 +55,11 @@ export default class CanvasNodeFactory { /** * Adds a sprite to the current scene * @param key The key of the image the sprite will represent - * @param layer The layer on which to add the sprite + * @param layerName The layer on which to add the sprite */ - addSprite = (key: string, layer: Layer): Sprite => { + addSprite = (key: string, layerName: string): Sprite => { + let layer = this.scene.getLayer(layerName); + let instance = new Sprite(key); // Add instance to scene @@ -53,21 +75,90 @@ export default class CanvasNodeFactory { /** * Adds a new graphic element to the current Scene - * @param constr The constructor of the graphic element to add - * @param layer The layer on which to add the graphic - * @param args Any additional arguments to send to the graphic constructor + * @param type The type of graphic to add + * @param layerName The layer on which to add the graphic + * @param options Any additional arguments to send to the graphic constructor */ - addGraphic = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { - let instance = new constr(...args); + addGraphic = (type: GraphicType | string, layerName: string, options?: Record): Graphic => { + // Get the layer + let layer = this.scene.getLayer(layerName); + + let instance: Graphic; + + switch(type){ + case GraphicType.POINT: + instance = this.buildPoint(options); + break; + case GraphicType.RECT: + instance = this.buildRect(options); + break; + default: + throw `GraphicType '${type}' does not exist, or is registered incorrectly.` + } // Add instance to scene instance.setScene(this.scene); instance.id = this.scene.generateId(); - this.scene.getSceneGraph().addNode(instance); + + if(!(this.scene.isParallaxLayer(layerName) || this.scene.isUILayer(layerName))){ + this.scene.getSceneGraph().addNode(instance); + } // Add instance to layer layer.addNode(instance); return instance; } + + /* ---------- BUILDERS ---------- */ + + buildButton(options?: Record): Button { + this.checkIfPropExists("Button", options, "position", Vec2, "Vec2"); + this.checkIfPropExists("Button", options, "text", "string"); + + return new Button(options.position, options.text); + } + + buildLabel(options?: Record): Label { + this.checkIfPropExists("Label", options, "position", Vec2, "Vec2"); + this.checkIfPropExists("Label", options, "text", "string"); + + return new Label(options.position, options.text) + } + + buildPoint(options?: Record): Point { + this.checkIfPropExists("Point", options, "position", Vec2, "Vec2"); + + return new Point(options.position); + } + + buildRect(options?: Record): Rect { + this.checkIfPropExists("Rect", options, "position", Vec2, "Vec2"); + this.checkIfPropExists("Rect", options, "size", Vec2, "Vec2"); + + return new Rect(options.position, options.size); + } + + /* ---------- ERROR HANDLING ---------- */ + + checkIfPropExists(objectName: string, options: Record, prop: string, type: (new (...args: any) => T) | string, typeName?: string){ + if(!options || !options[prop]){ + // Check that the options object has the property + throw `${objectName} object requires argument ${prop} of type ${typeName}, but none was provided.`; + } else { + // Check that the property has the correct type + if((typeof type) === "string"){ + if(!(typeof options[prop] === type)){ + throw `${objectName} object requires argument ${prop} of type ${type}, but provided ${prop} was not of type ${type}.`; + } + } else if(type instanceof Function){ + // If type is a constructor, check against that + if(!(options[prop] instanceof type)){ + throw `${objectName} object requires argument ${prop} of type ${typeName}, but provided ${prop} was not of type ${typeName}.`; + } + } else { + throw `${objectName} object requires argument ${prop} of type ${typeName}, but provided ${prop} was not of type ${typeName}.`; + } + } + } } \ No newline at end of file diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index 27ec0de..425e530 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -1,6 +1,5 @@ import Scene from "../Scene"; import Tilemap from "../../Nodes/Tilemap"; -import PhysicsManager from "../../Physics/PhysicsManager"; import ResourceManager from "../../ResourceManager/ResourceManager"; import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap"; import Layer from "../Layer"; @@ -8,6 +7,8 @@ import Tileset from "../../DataTypes/Tilesets/Tileset"; import Vec2 from "../../DataTypes/Vec2"; import { TiledCollectionTile } from "../../DataTypes/Tilesets/TiledData"; import Sprite from "../../Nodes/Sprites/Sprite"; +import PositionGraph from "../../DataTypes/Graphs/PositionGraph"; +import Navmesh from "../../Pathfinding/Navmesh"; export default class TilemapFactory { private scene: Scene; @@ -63,7 +64,7 @@ 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 = this.scene.addLayer(); + let sceneLayer = this.scene.addLayer(layer.name); if(layer.type === "tilelayer"){ // Create a new tilemap object for the layer @@ -82,17 +83,34 @@ export default class TilemapFactory { } } else { - let isNavmeshPoints = false + let isNavmeshPoints = false; + let navmeshName; + let edges; if(layer.properties){ for(let prop of layer.properties){ if(prop.name === "NavmeshPoints"){ isNavmeshPoints = true; + } else if(prop.name === "name"){ + navmeshName = prop.value; + } else if(prop.name === "edges"){ + edges = prop.value } } } if(isNavmeshPoints){ - console.log("Parsing NavmeshPoints") + let g = new PositionGraph(); + + for(let obj of layer.objects){ + g.addPositionedNode(new Vec2(obj.x, obj.y)); + } + + for(let edge of edges){ + g.addEdge(edge.from, edge.to); + } + + this.scene.getNavigationManager().addNavigableEntity(navmeshName, new Navmesh(g)); + continue; } @@ -126,7 +144,7 @@ export default class TilemapFactory { // The object is a tile from this set let imageKey = tileset.getImageKey(); let offset = tileset.getImageOffsetForTile(obj.gid); - sprite = this.scene.add.sprite(imageKey, sceneLayer); + sprite = this.scene.add.sprite(imageKey, layer.name); let size = tileset.getTileSize().clone(); sprite.position.set((obj.x + size.x/2)*scale.x, (obj.y - size.y/2)*scale.y); sprite.setImageOffset(offset); @@ -140,7 +158,7 @@ export default class TilemapFactory { for(let tile of collectionTiles){ if(obj.gid === tile.id){ let imageKey = tile.image; - sprite = this.scene.add.sprite(imageKey, sceneLayer); + sprite = this.scene.add.sprite(imageKey, layer.name); sprite.position.set((obj.x + tile.imagewidth/2)*scale.x, (obj.y - tile.imageheight/2)*scale.y); sprite.scale.set(scale.x, scale.y); } diff --git a/src/Scene/Layer.ts b/src/Scene/Layer.ts index 5ac5e6d..3ac3e7b 100644 --- a/src/Scene/Layer.ts +++ b/src/Scene/Layer.ts @@ -1,24 +1,39 @@ -import Vec2 from "../DataTypes/Vec2"; import Scene from "./Scene"; import MathUtils from "../Utils/MathUtils"; import GameNode from "../Nodes/GameNode"; + /** * A layer in the scene. Has its own alpha value and parallax. */ export default class Layer { + /** The scene this layer belongs to */ protected scene: Scene; - protected parallax: Vec2; + + /** The name of this layer */ + protected name: string; + + /** Whether this layer is paused or not */ protected paused: boolean; + + /** Whether this layer is hidden from being rendered or not */ protected hidden: boolean; + + /** The global alpha level of this layer */ protected alpha: number; + + /** An array of the GameNodes that belong to this layer */ protected items: Array; + + /** Whether or not this layer should be ysorted */ protected ySort: boolean; + + /** The depth of this layer compared to other layers */ protected depth: number; - constructor(scene: Scene){ + constructor(scene: Scene, name: string){ this.scene = scene; - this.parallax = new Vec2(1, 1); + this.name = name; this.paused = false; this.hidden = false; this.alpha = 1; @@ -51,24 +66,18 @@ export default class Layer { return this.hidden; } + /** Pauses this scene and hides it */ disable(): void { this.paused = true; this.hidden = true; } + /** Unpauses this layer and makes it visible */ enable(): void { this.paused = false; this.hidden = false; } - setParallax(x: number, y: number): void { - this.parallax.set(x, y); - } - - getParallax(): Vec2 { - return this.parallax; - } - setYSort(ySort: boolean): void { this.ySort = ySort; } @@ -89,4 +98,8 @@ export default class Layer { this.items.push(node); node.setLayer(this); } + + getItems(): Array { + return this.items; + } } \ No newline at end of file diff --git a/src/Scene/Layers/ParallaxLayer.ts b/src/Scene/Layers/ParallaxLayer.ts new file mode 100644 index 0000000..580f3df --- /dev/null +++ b/src/Scene/Layers/ParallaxLayer.ts @@ -0,0 +1,12 @@ +import Layer from "../Layer"; +import Vec2 from "../../DataTypes/Vec2"; +import Scene from "../Scene"; + +export default class ParallaxLayer extends Layer { + parallax: Vec2; + + constructor(scene: Scene, name: string, parallax: Vec2){ + super(scene, name); + this.parallax = parallax; + } +} \ No newline at end of file diff --git a/src/Scene/Layers/UILayer.ts b/src/Scene/Layers/UILayer.ts new file mode 100644 index 0000000..5a2b072 --- /dev/null +++ b/src/Scene/Layers/UILayer.ts @@ -0,0 +1,9 @@ +import Vec2 from "../../DataTypes/Vec2"; +import Scene from "../Scene"; +import ParallaxLayer from "./ParallaxLayer"; + +export default class UILayer extends ParallaxLayer { + constructor(scene: Scene, name: string){ + super(scene, name, Vec2.ZERO); + } +} \ No newline at end of file diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index 27fd4ef..3e52458 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -13,7 +13,13 @@ import SceneManager from "./SceneManager"; import Receiver from "../Events/Receiver"; import Emitter from "../Events/Emitter"; import { Renderable, Updateable } from "../DataTypes/Interfaces/Descriptors"; -import Navmesh from "../DataTypes/Navmesh"; +import NavigationManager from "../Pathfinding/NavigationManager"; +import AIManager from "../AI/AIManager"; +import Map from "../DataTypes/Map"; +import ParallaxLayer from "./Layers/ParallaxLayer"; +import UILayer from "./Layers/UILayer"; +import CanvasNode from "../Nodes/CanvasNode"; +import GameNode from "../Nodes/GameNode"; export default class Scene implements Updateable, Renderable { /** The size of the game world. */ @@ -40,18 +46,33 @@ export default class Scene implements Updateable, Renderable { /** This list of tilemaps in this scene. */ protected tilemaps: Array; + /** A map from layer names to the layers themselves */ + protected layers: Map; + + /** A map from parallax layer names to the parallax layers themselves */ + protected parallaxLayers: Map; + + /** A map from uiLayer names to the uiLayers themselves */ + protected uiLayers: Map; + /** The scene graph of the Scene*/ protected sceneGraph: SceneGraph; + + /** The physics manager of the Scene */ protected physicsManager: PhysicsManager; + /** The navigation manager of the Scene */ + protected navManager: NavigationManager; + + /** The AI manager of the Scene */ + protected aiManager: AIManager; + /** An interface that allows the adding of different nodes to the scene */ public add: FactoryManager; /** An interface that allows the loading of different files for use in the scene */ public load: ResourceManager; - protected navmeshes: Array; - constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ this.worldSize = new Vec2(500, 500); this.viewport = viewport; @@ -64,7 +85,14 @@ export default class Scene implements Updateable, Renderable { this.tilemaps = new Array(); this.sceneGraph = new SceneGraphArray(this.viewport, this); + + this.layers = new Map(); + this.uiLayers = new Map(); + this.parallaxLayers = new Map(); + this.physicsManager = new BasicPhysicsManager(); + this.navManager = new NavigationManager(); + this.aiManager = new AIManager(); this.add = new FactoryManager(this, this.tilemaps); @@ -89,6 +117,9 @@ export default class Scene implements Updateable, Renderable { update(deltaT: number): void { this.updateScene(deltaT); + // Do all AI updates + this.aiManager.update(deltaT); + // Update all physics objects this.physicsManager.update(deltaT); @@ -111,6 +142,25 @@ export default class Scene implements Updateable, Renderable { // We need to keep track of the order of things. let visibleSet = this.sceneGraph.getVisibleSet(); + // Add parallax layer items to the visible set (we're rendering them all for now) + this.parallaxLayers.forEach(key => { + let pLayer = this.parallaxLayers.get(key); + for(let node of pLayer.getItems()){ + if(node instanceof CanvasNode){ + visibleSet.push(node); + } + } + }); + + // Sort by depth, then by visible set by y-value + visibleSet.sort((a, b) => { + if(a.getLayer().getDepth() === b.getLayer().getDepth()){ + return (a.boundary.bottom) - (b.boundary.bottom); + } else { + return a.getLayer().getDepth() - b.getLayer().getDepth(); + } + }); + // Render scene graph for demo this.sceneGraph.render(ctx); @@ -124,6 +174,9 @@ export default class Scene implements Updateable, Renderable { // Debug render the physicsManager this.physicsManager.debug_render(ctx); + + // Render the uiLayers + this.uiLayers.forEach(key => this.uiLayers.get(key).getItems().forEach(node => (node).render(ctx))); } setRunning(running: boolean): void { @@ -136,11 +189,114 @@ export default class Scene implements Updateable, Renderable { /** * Adds a new layer to the scene and returns it + * @param name The name of the new layer + * @param depth The depth of the layer */ - addLayer(): Layer { - return this.sceneGraph.addLayer(); + addLayer(name: string, depth?: number): Layer { + if(this.layers.has(name) || this.uiLayers.has(name)){ + throw `Layer with name ${name} already exists`; + } + + let layer = new Layer(this, name); + + this.layers.add(name, layer); + + if(depth){ + layer.setDepth(depth); + } + + return layer; } + /** + * Adds a new parallax layer to this scene and returns it + * @param name The name of the parallax layer + * @param parallax The parallax level + * @param depth The depth of the layer + */ + addParallaxLayer(name: string, parallax: Vec2, depth?: number): ParallaxLayer { + if(this.layers.has(name) || this.uiLayers.has(name)){ + throw `Layer with name ${name} already exists`; + } + + let layer = new ParallaxLayer(this, name, parallax); + + this.layers.add(name, layer); + + if(depth){ + layer.setDepth(depth); + } + + return layer; + } + + /** + * Adds a new UILayer to the scene + * @param name The name of the new UIlayer + */ + addUILayer(name: string): UILayer { + if(this.layers.has(name) || this.uiLayers.has(name)){ + throw `Layer with name ${name} already exists`; + } + + let layer = new UILayer(this, name); + + this.uiLayers.add(name, layer); + + return layer; + } + + /** + * Gets a layer from the scene by name if it exists + * @param name The name of the layer + */ + getLayer(name: string): Layer { + if(this.layers.has(name)){ + return this.layers.get(name); + } else if(this.parallaxLayers.has(name)){ + return this.parallaxLayers.get(name); + } else if(this.uiLayers.has(name)){ + return this.uiLayers.get(name); + } else { + throw `Requested layer ${name} does not exist.`; + } + } + + /** + * Returns true if this layer is a ParallaxLayer + * @param name + */ + isParallaxLayer(name: string): boolean { + return this.parallaxLayers.has(name); + } + + /** + * Returns true if this layer is a UILayer + * @param name + */ + isUILayer(name: string): boolean { + return this.uiLayers.has(name); + } + + /** + * Returns the translation of this node with respect to camera space (due to the viewport moving); + * @param node + */ + getViewTranslation(node: GameNode): Vec2 { + let layer = node.getLayer(); + + if(layer instanceof ParallaxLayer || layer instanceof UILayer){ + return this.viewport.getOrigin().mult(layer.parallax); + } else { + return this.viewport.getOrigin(); + } + } + + /** Returns the scale level of the view */ + getViewScale(): number { + return this.viewport.getZoomLevel(); + } + /** Returns the viewport associated with this scene */ getViewport(): Viewport { return this.viewport; @@ -158,6 +314,14 @@ export default class Scene implements Updateable, Renderable { return this.physicsManager; } + getNavigationManager(): NavigationManager { + return this.navManager; + } + + getAIManager(): AIManager { + return this.aiManager; + } + generateId(): number { return this.sceneManager.generateId(); } diff --git a/src/SceneGraph/SceneGraph.ts b/src/SceneGraph/SceneGraph.ts index 7b18342..e71e004 100644 --- a/src/SceneGraph/SceneGraph.ts +++ b/src/SceneGraph/SceneGraph.ts @@ -3,8 +3,6 @@ import CanvasNode from "../Nodes/CanvasNode"; import Map from "../DataTypes/Map"; import Vec2 from "../DataTypes/Vec2"; import Scene from "../Scene/Scene"; -import Layer from "../Scene/Layer"; -import Stack from "../DataTypes/Stack"; import AABB from "../DataTypes/Shapes/AABB"; /** @@ -15,14 +13,12 @@ export default abstract class SceneGraph { protected nodeMap: Map; protected idCounter: number; protected scene: Scene; - protected layers: Stack; constructor(viewport: Viewport, scene: Scene){ this.viewport = viewport; this.scene = scene; this.nodeMap = new Map(); this.idCounter = 0; - this.layers = new Stack(10); } /** @@ -93,18 +89,6 @@ export default abstract class SceneGraph { * @param y */ protected abstract getNodesAtCoords(x: number, y: number): Array; - - addLayer(): Layer { - let layer = new Layer(this.scene); - let depth = this.layers.size(); - layer.setDepth(depth); - this.layers.push(layer); - return layer; - } - - getLayers(): Stack { - return this.layers; - } abstract update(deltaT: number): void; diff --git a/src/SceneGraph/SceneGraphArray.ts b/src/SceneGraph/SceneGraphArray.ts index c4eab36..aaf192c 100644 --- a/src/SceneGraph/SceneGraphArray.ts +++ b/src/SceneGraph/SceneGraphArray.ts @@ -91,15 +91,6 @@ export default class SceneGraphArray extends SceneGraph{ } } - // Sort by depth, then by visible set by y-value - visibleSet.sort((a, b) => { - if(a.getLayer().getDepth() === b.getLayer().getDepth()){ - return (a.boundary.bottom) - (b.boundary.bottom); - } else { - return a.getLayer().getDepth() - b.getLayer().getDepth(); - } - }); - return visibleSet; } } \ No newline at end of file diff --git a/src/SceneGraph/SceneGraphQuadTree.ts b/src/SceneGraph/SceneGraphQuadTree.ts index 5956bab..5f42c71 100644 --- a/src/SceneGraph/SceneGraphQuadTree.ts +++ b/src/SceneGraph/SceneGraphQuadTree.ts @@ -77,15 +77,6 @@ export default class SceneGraphQuadTree extends SceneGraph { visibleSet = visibleSet.filter(node => !node.getLayer().isHidden()); - // Sort by depth, then by visible set by y-value - visibleSet.sort((a, b) => { - if(a.getLayer().getDepth() === b.getLayer().getDepth()){ - return (a.boundary.bottom) - (b.boundary.bottom); - } else { - return a.getLayer().getDepth() - b.getLayer().getDepth(); - } - }); - return visibleSet; } } \ No newline at end of file diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index fa6bd9f..df8c15b 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -6,6 +6,8 @@ import Queue from "../DataTypes/Queue"; import AABB from "../DataTypes/Shapes/AABB"; import Debug from "../Debug/Debug"; import InputReceiver from "../Input/InputReceiver"; +import ParallaxLayer from "../Scene/Layers/ParallaxLayer"; +import UILayer from "../Scene/Layers/UILayer"; export default class Viewport { private view: AABB; @@ -46,8 +48,9 @@ export default class Viewport { return this.view.center; } + /** Returns a new Vec2 with the origin of the viewport */ getOrigin(): Vec2 { - return this.view.center.clone().sub(this.view.halfSize) + return new Vec2(this.view.left, this.view.top); } /** @@ -133,7 +136,7 @@ export default class Viewport { * @param node */ includes(node: CanvasNode): boolean { - let parallax = node.getLayer().getParallax(); + let parallax = node.getLayer() instanceof ParallaxLayer || node.getLayer() instanceof UILayer ? (node.getLayer()).parallax : new Vec2(1, 1); let center = this.view.center.clone(); this.view.center.mult(parallax); let overlaps = this.view.overlaps(node.boundary); @@ -197,8 +200,6 @@ export default class Viewport { } } - Debug.log("vpzoom", "View size: " + this.view.getHalfSize()); - // If viewport is following an object if(this.following){ // Update our list of previous positions @@ -219,8 +220,6 @@ export default class Viewport { // Assure there are no lines in the tilemap pos.x = Math.floor(pos.x); pos.y = Math.floor(pos.y); - - Debug.log("vp", "Viewport pos: " + pos.toString()) this.view.center.copy(pos); } else { @@ -240,7 +239,6 @@ export default class Viewport { pos.x = Math.floor(pos.x); pos.y = Math.floor(pos.y); - Debug.log("vp", "Viewport pos: " + pos.toString()) this.view.center.copy(pos); } } diff --git a/src/Utils/GraphUtils.ts b/src/Utils/GraphUtils.ts index cf87f56..c8283bc 100644 --- a/src/Utils/GraphUtils.ts +++ b/src/Utils/GraphUtils.ts @@ -2,11 +2,12 @@ import Graph, { EdgeNode } from "../DataTypes/Graphs/Graph"; export default class GraphUtils { - static djikstra(g: Graph, start: number): void { + static djikstra(g: Graph, start: number): Array { let i: number; // Counter let p: EdgeNode; // Pointer to edgenode - let inTree: Array - let distance: number; + let inTree: Array = new Array(g.numVertices); + let distance: Array = new Array(g.numVertices); + let parent: Array = new Array(g.numVertices); let v: number; // Current vertex to process let w: number; // Candidate for next vertex let weight: number; // Edge weight @@ -15,7 +16,41 @@ export default class GraphUtils { for(i = 0; i < g.numVertices; i++){ inTree[i] = false; distance[i] = Infinity; - // parent[i] = -1; + parent[i] = -1; } + + distance[start] = 0; + v = start; + + while(!inTree[v]){ + inTree[v] = true; + p = g.edges[v]; + + while(p !== null){ + w = p.y; + weight = p.weight; + + if(distance[w] > distance[v] + weight){ + distance[w] = distance[v] + weight; + parent[w] = v; + } + + p = p.next; + } + + v = 0; + + dist = Infinity; + + for(i = 0; i <= g.numVertices; i++){ + if(!inTree[i] && dist > distance[i]){ + dist = distance; + v = i; + } + } + } + + return parent; + } } \ No newline at end of file diff --git a/src/Utils/MathUtils.ts b/src/Utils/MathUtils.ts index be2bf83..cd3a7f4 100644 --- a/src/Utils/MathUtils.ts +++ b/src/Utils/MathUtils.ts @@ -41,10 +41,14 @@ export default class MathUtils { * Linear Interpolation * @param a The first value for the interpolation bound * @param b The second value for the interpolation bound - * @param x The value we are interpolating + * @param t The time we are interpolating to */ - static lerp(a: number, b: number, x: number){ - return a + x * (b - a); + static lerp(a: number, b: number, t: number): number { + return a + t * (b - a); + } + + static invLerp(a: number, b: number, value: number){ + return (value - a)/(b - a); } /** diff --git a/src/_DemoClasses/Boids/Boid.ts b/src/_DemoClasses/Boids/Boid.ts index 29d462a..3d892ca 100644 --- a/src/_DemoClasses/Boids/Boid.ts +++ b/src/_DemoClasses/Boids/Boid.ts @@ -8,13 +8,13 @@ export default class Boid extends Graphic { acceleration: Vec2 = Vec2.ZERO; velocity: Vec2 = Vec2.ZERO; - ai: BoidController; + //ai: BoidController; fb: FlockBehavior; constructor(position: Vec2){ super(); this.position = position; - this.ai = new BoidController(this); + //this.ai = new BoidController(this); } update(deltaT: number){ @@ -22,8 +22,8 @@ export default class Boid extends Graphic { } render(ctx: CanvasRenderingContext2D): void { - let origin = this.getViewportOriginWithParallax(); - let zoom = this.getViewportScale(); + let origin = this.scene.getViewTranslation(this); + let zoom = this.scene.getViewScale(); let dirVec = this.direction.scaled(this.size.x, this.size.y); let finVec1 = this.direction.clone().rotateCCW(Math.PI/2).scale(this.size.x/2, this.size.y/2).sub(this.direction.scaled(this.size.x/1.5, this.size.y/1.5)); diff --git a/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts b/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts index 0337a71..834f8b2 100644 --- a/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts +++ b/src/_DemoClasses/Boids/BoidStates/BoidBehavior.ts @@ -1,7 +1,6 @@ import State from "../../../DataTypes/State/State"; import StateMachine from "../../../DataTypes/State/StateMachine"; import Vec2 from "../../../DataTypes/Vec2"; -import Debug from "../../../Debug/Debug"; import GameEvent from "../../../Events/GameEvent"; import MathUtils from "../../../Utils/MathUtils"; import { CustomGameEventType } from "../../CustomGameEventType"; @@ -68,17 +67,6 @@ export default class BoidBehavior extends State { this.actor.direction = this.actor.velocity.clone(); speed = MathUtils.clamp(speed, BoidBehavior.MIN_SPEED, BoidBehavior.MAX_SPEED); this.actor.velocity.scale(speed); - - if(this.actor.id < 1){ - Debug.log("BoidSep", "Separation: " + separationForce.toString()); - Debug.log("BoidAl", "Alignment: " + alignmentForce.toString()); - Debug.log("BoidCo", "Cohesion: " + cohesionForce.toString()); - Debug.log("BoidSpd", "Speed: " + speed); - } - } - - if(this.actor.id < 1){ - Debug.log("BoidDir", "Velocity: " + this.actor.velocity.toString()); } // Update the position diff --git a/src/_DemoClasses/Enemies/GoombaController.ts b/src/_DemoClasses/Enemies/GoombaController.ts index 398c108..031280e 100644 --- a/src/_DemoClasses/Enemies/GoombaController.ts +++ b/src/_DemoClasses/Enemies/GoombaController.ts @@ -55,13 +55,5 @@ export default class GoombaController extends StateMachine { update(deltaT: number): void { super.update(deltaT); - - if(this.currentState instanceof Jump){ - Debug.log("goombastate", "GoombaState: Jump"); - } else if (this.currentState instanceof Walk){ - Debug.log("goombastate", "GoombaState: Walk"); - } else { - Debug.log("goombastate", "GoombaState: Idle"); - } } } \ No newline at end of file diff --git a/src/_DemoClasses/MarioClone/MarioClone.ts b/src/_DemoClasses/MarioClone/MarioClone.ts index 2d6530e..6203767 100644 --- a/src/_DemoClasses/MarioClone/MarioClone.ts +++ b/src/_DemoClasses/MarioClone/MarioClone.ts @@ -6,6 +6,7 @@ import PlayerController from "../Player/PlayerStates/Platformer/PlayerController import { PlayerStates } from "../Player/PlayerStates/Platformer/PlayerController"; import GoombaController from "../Enemies/GoombaController"; import InputReceiver from "../../Input/InputReceiver"; +import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; export default class MarioClone extends Scene { @@ -15,24 +16,22 @@ export default class MarioClone extends Scene { } startScene(): void { - let layer = this.addLayer(); + this.addLayer("main"); this.add.tilemap("level", new Vec2(2, 2)); - let player = this.add.graphic(Rect, layer, new Vec2(0, 0), new Vec2(64, 64)); + let player = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(0, 0), size: new Vec2(64, 64)}); player.setColor(Color.BLUE); player.addPhysics(); player.isPlayer = true; this.viewport.follow(player); this.viewport.setBounds(0, 0, 5120, 1280); - let ai = new PlayerController(player); - ai.initialize(PlayerStates.IDLE); - player.update = (deltaT: number) => {ai.update(deltaT)}; + player.ai = new PlayerController(); player.addTrigger("CoinBlock", "playerHitCoinBlock"); for(let xPos of [14, 20, 25, 30, 33, 37, 49, 55, 58, 70, 74]){ - let goomba = this.add.sprite("goomba", layer); + let goomba = this.add.sprite("goomba", "main"); let ai = new GoombaController(goomba, false); ai.initialize("idle"); goomba.update = (deltaT: number) => {ai.update(deltaT)}; diff --git a/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderController.ts b/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderController.ts new file mode 100644 index 0000000..902fb94 --- /dev/null +++ b/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderController.ts @@ -0,0 +1,22 @@ +import StateMachineAI from "../../../AI/StateMachineAI"; +import GameNode from "../../../Nodes/GameNode"; +import PathfinderIdle from "./PathfinderIdle"; +import PathfinderNav from "./PathfinderNav"; + +export default class PathfinderController extends StateMachineAI { + protected owner: GameNode; + + initializeAI(owner: GameNode, config: Record): void { + this.owner = owner; + + let idle = new PathfinderIdle(this); + this.addState("idle", idle); + + let nav = new PathfinderNav(this, owner, config.player); + this.addState("nav", nav); + + this.receiver.subscribe("navigate"); + + this.initialize("idle"); + } +} \ No newline at end of file diff --git a/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderIdle.ts b/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderIdle.ts new file mode 100644 index 0000000..0088043 --- /dev/null +++ b/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderIdle.ts @@ -0,0 +1,17 @@ +import State from "../../../DataTypes/State/State"; +import GameEvent from "../../../Events/GameEvent"; + +export default class PathfinderIdle extends State { + onEnter(): void {} + + handleInput(event: GameEvent): void { + if(event.type === "navigate"){ + this.finished("nav"); + } + } + + update(deltaT: number): void {} + + onExit(): void {} + +} \ No newline at end of file diff --git a/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderNav.ts b/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderNav.ts new file mode 100644 index 0000000..309a869 --- /dev/null +++ b/src/_DemoClasses/Pathfinding/Pathfinder/PathfinderNav.ts @@ -0,0 +1,41 @@ +import State from "../../../DataTypes/State/State"; +import GameEvent from "../../../Events/GameEvent"; +import GameNode from "../../../Nodes/GameNode"; +import NavigationPath from "../../../Pathfinding/NavigationPath"; +import PathfinderController from "./PathfinderController"; + +export default class PathfinderNav extends State { + parent: PathfinderController; + owner: GameNode; + player: GameNode; + + constructor(parent: PathfinderController, owner: GameNode, player: GameNode){ + super(parent); + this.owner = owner; + this.player = player; + } + + onEnter(): void { + // Request a path + this.owner.path = this.owner.getScene().getNavigationManager().getPath("main", this.owner.position, this.player.position); + this.owner.pathfinding = true; + } + + handleInput(event: GameEvent): void {} + + update(deltaT: number): void { + if(this.owner.path.isDone()){ + this.finished("idle"); + return; + } + + let dir = this.owner.path.getMoveDirection(this.owner); + + this.owner.move(dir.scale(200 * deltaT)); + } + + onExit(): void { + this.owner.pathfinding = false; + } + +} \ No newline at end of file diff --git a/src/_DemoClasses/Pathfinding/PathfindingScene.ts b/src/_DemoClasses/Pathfinding/PathfindingScene.ts index e89f0b9..e52dcf4 100644 --- a/src/_DemoClasses/Pathfinding/PathfindingScene.ts +++ b/src/_DemoClasses/Pathfinding/PathfindingScene.ts @@ -1,7 +1,10 @@ import Scene from "../../Scene/Scene"; -import Rect from "../../Nodes/Graphics/Rect"; import Vec2 from "../../DataTypes/Vec2"; import PlayerController from "../Player/PlayerController"; +import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; +import { UIElementType } from "../../Nodes/UIElements/UIElementTypes"; +import Color from "../../Utils/Color"; +import PathfinderController from "./Pathfinder/PathfinderController"; export default class PathfindingScene extends Scene { @@ -12,15 +15,32 @@ export default class PathfindingScene extends Scene { startScene(){ this.add.tilemap("interior"); - let layer = this.addLayer(); + // Add a layer for the game objects + this.addLayer("main"); - let player = this.add.graphic(Rect, layer, new Vec2(500, 500), new Vec2(64, 64)); + // Add the player + let player = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(500, 500), size: new Vec2(64, 64)}); player.addPhysics(); - let ai = new PlayerController(player, "topdown"); - ai.speed = 400; - player.update = (deltaT: number) => {ai.update(deltaT)} + player.addAI(PlayerController, {playerType: "topdown", speed: 400}); + + // Set the viewport to follow the player this.viewport.setBounds(0, 0, 40*64, 40*64); this.viewport.follow(player); this.viewport.enableZoom(); + + // Add a navigator + let nav = this.add.graphic(GraphicType.RECT, "main", {position: new Vec2(700, 400), size: new Vec2(64, 64)}); + nav.setColor(Color.BLUE); + nav.addPhysics(); + nav.addAI(PathfinderController, {player: player}); + + // Add a layer for the ui + this.addUILayer("uiLayer"); + + // Add a button that triggers the navigator + let btn = this.add.uiElement(UIElementType.BUTTON, "uiLayer", {position: new Vec2(400, 20), text: "Navigate"}); + btn.size = new Vec2(120, 35); + btn.setBackgroundColor(Color.BLUE); + btn.onClickEventId = "navigate"; } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/Player.ts b/src/_DemoClasses/Player/Player.ts index a010fd5..87faa0e 100644 --- a/src/_DemoClasses/Player/Player.ts +++ b/src/_DemoClasses/Player/Player.ts @@ -8,10 +8,10 @@ export default class Player extends Rect { constructor(position: Vec2){ super(position, new Vec2(20, 20)); - this.controller = new PlayerController(this, PlayerType.TOPDOWN); + //this.controller = new PlayerController(this, PlayerType.TOPDOWN); } update(deltaT: number): void { - this.controller.update(deltaT); + //this.controller.update(deltaT); } } \ No newline at end of file diff --git a/src/_DemoClasses/Player/PlayerController.ts b/src/_DemoClasses/Player/PlayerController.ts index 20560ca..3900c86 100644 --- a/src/_DemoClasses/Player/PlayerController.ts +++ b/src/_DemoClasses/Player/PlayerController.ts @@ -1,4 +1,4 @@ -import StateMachine from "../../DataTypes/State/StateMachine"; +import StateMachineAI from "../../AI/StateMachineAI"; import Vec2 from "../../DataTypes/Vec2"; import Debug from "../../Debug/Debug"; import GameNode from "../../Nodes/GameNode"; @@ -23,21 +23,18 @@ export enum PlayerStates { PREVIOUS = "previous" } -export default class PlayerController extends StateMachine { +export default class PlayerController extends StateMachineAI { protected owner: GameNode; velocity: Vec2 = Vec2.ZERO; - speed: number; + speed: number = 400; MIN_SPEED: number = 400; MAX_SPEED: number = 1000; - - constructor(owner: GameNode, playerType: string){ - super(); - + initializeAI(owner: GameNode, config: Record){ this.owner = owner; - if(playerType === PlayerType.TOPDOWN){ - this.initializeTopDown(); + if(config.playerType === PlayerType.TOPDOWN){ + this.initializeTopDown(config.speed); } else { this.initializePlatformer(); } @@ -46,11 +43,11 @@ export default class PlayerController extends StateMachine { /** * Initializes the player controller for a top down player */ - initializeTopDown(): void { + initializeTopDown(speed: number): void { let idle = new IdleTopDown(this); let move = new MoveTopDown(this, this.owner); - this.speed = 150; + this.speed = speed ? speed : 150; this.addState(PlayerStates.IDLE, idle); this.addState(PlayerStates.MOVE, move); diff --git a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts index 2f1fb96..ac0ecbe 100644 --- a/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts +++ b/src/_DemoClasses/Player/PlayerStates/Platformer/PlayerController.ts @@ -1,4 +1,3 @@ -import StateMachine from "../../../../DataTypes/State/StateMachine"; import Debug from "../../../../Debug/Debug"; import Idle from "./Idle"; import Jump from "./Jump"; @@ -6,6 +5,7 @@ import Walk from "./Walk"; import Run from "./Run"; import GameNode from "../../../../Nodes/GameNode"; import Vec2 from "../../../../DataTypes/Vec2"; +import StateMachineAI from "../../../../AI/StateMachineAI"; export enum PlayerStates { WALK = "walk", @@ -15,16 +15,14 @@ export enum PlayerStates { PREVIOUS = "previous" } -export default class PlayerController extends StateMachine { +export default class PlayerController extends StateMachineAI { protected owner: GameNode; velocity: Vec2 = Vec2.ZERO; speed: number = 400; MIN_SPEED: number = 400; MAX_SPEED: number = 1000; - constructor(owner: GameNode){ - super(); - + initializeAI(owner: GameNode, config: Record): void { this.owner = owner; let idle = new Idle(this, owner); @@ -35,6 +33,8 @@ export default class PlayerController extends StateMachine { this.addState(PlayerStates.RUN, run); let jump = new Jump(this, owner); this.addState(PlayerStates.JUMP, jump); + + this.initialize(PlayerStates.IDLE); } currentStateString: string = "";