added pathfinding and ai factories, split layers, and made fixes to other factories

This commit is contained in:
Joe Weaver 2020-11-04 14:03:52 -05:00
parent 1047e31c70
commit ff5a2896fe
47 changed files with 956 additions and 225 deletions

39
src/AI/AIManager.ts Normal file
View File

@ -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<Actor>;
registeredAI: Map<new <T extends AI>() => 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 extends AI>() => 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) });
}
}

9
src/AI/StateMachineAI.ts Normal file
View File

@ -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<string, any>): void {}
}

View File

@ -18,29 +18,29 @@ export default class BoidDemo extends Scene {
startScene(){ startScene(){
// Set the world size // Set the world size
this.worldSize = new Vec2(800, 600); // this.worldSize = new Vec2(800, 600);
this.sceneGraph = new SceneGraphQuadTree(this.viewport, this); // this.sceneGraph = new SceneGraphQuadTree(this.viewport, this);
this.viewport.setBounds(0, 0, 800, 600) // this.viewport.setBounds(0, 0, 800, 600)
this.viewport.setCenter(400, 300); // this.viewport.setCenter(400, 300);
let layer = this.addLayer(); // let layer = this.addLayer();
this.boids = new Array(); // this.boids = new Array();
// Add the player // // Add the player
let player = this.add.graphic(Player, layer, new Vec2(0, 0)); // let player = this.add.graphic(Player, layer, new Vec2(0, 0));
player.addPhysics(); // player.addPhysics();
let ai = new PlayerController(player, "topdown"); // let ai = new PlayerController(player, "topdown");
player.update = (deltaT: number) => {ai.update(deltaT)} // player.update = (deltaT: number) => {ai.update(deltaT)}
this.viewport.follow(player); // this.viewport.follow(player);
this.viewport.enableZoom(); // this.viewport.enableZoom();
// Create a bunch of boids // // Create a bunch of boids
for(let i = 0; i < 150; i++){ // 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())); // 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.fb = new FlockBehavior(this, boid, this.boids, 75, 50);
boid.size.set(5, 5); // boid.size.set(5, 5);
this.boids.push(boid); // this.boids.push(boid);
} // }
} }
updateScene(deltaT: number): void { updateScene(deltaT: number): void {

View File

@ -27,9 +27,22 @@ export default class Graph {
addEdge(x: number, y: number, weight?: number){ addEdge(x: number, y: number, weight?: number){
let edge = new EdgeNode(y, weight); let edge = new EdgeNode(y, weight);
if(this.edges[x]){
edge.next = this.edges[x]; edge.next = this.edges[x];
}
this.edges[x] = edge; 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; this.numEdges += 1;
} }
@ -40,6 +53,34 @@ export default class Graph {
getDegree(x: number): number { getDegree(x: number): number {
return this.degree[x]; 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 { export class EdgeNode {

View File

@ -10,21 +10,34 @@ export default class PositionGraph extends Graph implements Debug_Renderable{
this.positions = new Array(MAX_V); this.positions = new Array(MAX_V);
} }
addPositionedNode(position: Vec2){
this.positions[this.numVertices] = position;
this.addNode();
}
setNodePosition(index: number, position: Vec2): void { setNodePosition(index: number, position: Vec2): void {
this.positions[index] = position; this.positions[index] = position;
} }
getNodePosition(index: number): Vec2 {
return this.positions[index];
}
addEdge(x: number, y: number): void { addEdge(x: number, y: number): void {
if(!this.positions[x] || !this.positions[y]){ if(!this.positions[x] || !this.positions[y]){
throw "Can't add edge to un-positioned node!"; throw "Can't add edge to un-positioned node!";
} }
// Weight is the distance between the nodes // 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); 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 { debug_render(ctx: CanvasRenderingContext2D, origin: Vec2, zoom: number): void {
for(let point of this.positions){ for(let point of this.positions){
ctx.fillRect((point.x - origin.x - 4)*zoom, (point.y - origin.y - 4)*zoom, 8, 8); ctx.fillRect((point.x - origin.x - 4)*zoom, (point.y - origin.y - 4)*zoom, 8, 8);

View File

@ -3,6 +3,8 @@ import Map from "../Map";
import AABB from "../Shapes/AABB"; import AABB from "../Shapes/AABB";
import Shape from "../Shapes/Shape"; import Shape from "../Shapes/Shape";
import Vec2 from "../Vec2"; import Vec2 from "../Vec2";
import NavigationPath from "../../Pathfinding/NavigationPath";
import GameNode from "../../Nodes/GameNode";
export interface Unique { export interface Unique {
/** The unique id of this object. */ /** The unique id of this object. */
@ -107,6 +109,37 @@ export interface Physical {
addTrigger: (group: string, eventType: string) => void; 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<string, any>) => 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: <T extends AI>(ai: string | (new () => T), options: Record<string, any>) => void;
setAIActive: (active: boolean) => void;
}
export interface Navigable {
getNavigationPath: (fromPosition: Vec2, toPosition: Vec2) => NavigationPath;
}
export interface Updateable { export interface Updateable {
/** Updates this object. */ /** Updates this object. */
update: (deltaT: number) => void; update: (deltaT: number) => void;

View File

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

View File

@ -47,6 +47,11 @@ export default class Stack<T> implements Collection {
return this.stack[this.head]; return this.stack[this.head];
} }
/** Returns true if this stack is empty */
isEmpty(): boolean {
return this.head === -1;
}
clear(): void { clear(): void {
this.forEach((item, index) => delete this.stack[index]); this.forEach((item, index) => delete this.stack[index]);
this.head = -1; this.head = -1;
@ -62,8 +67,22 @@ export default class Stack<T> implements Collection {
forEach(func: (item: T, index?: number) => void): void{ forEach(func: (item: T, index?: number) => void): void{
let i = 0; let i = 0;
while(i <= this.head){ while(i <= this.head){
func(this.stack[i]); func(this.stack[i], i);
i += 1; i += 1;
} }
} }
toString(): string {
let retval = "";
this.forEach( (item, index) => {
let str = item.toString()
if(index !== 0){
str += " -> "
}
retval = str + retval;
});
return "Top -> " + retval;
}
} }

View File

@ -1,3 +1,5 @@
import MathUtils from "../Utils/MathUtils";
/** /**
* A two-dimensional vector (x, y) * 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. * Sets the vector's x and y based on the angle provided. Goes counter clockwise.
* @param angle The angle in radians * @param angle The angle in radians
* @param radius The magnitude of the vector at the specified angle
*/ */
setToAngle(angle: number): Vec2 { setToAngle(angle: number, radius: number = 1): Vec2 {
this.x = Math.cos(angle); this.x = MathUtils.floorToPlace(Math.cos(angle)*radius, 5);
this.y = Math.sin(angle); this.y = MathUtils.floorToPlace(-Math.sin(angle)*radius, 5);
return this; return this;
} }
@ -114,6 +117,14 @@ export default class Vec2 {
return new Vec2(other.x - this.x, other.y - this.y); 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 * Keeps the vector's direction, but sets its magnitude to be the provided magnitude
* @param magnitude * @param magnitude
@ -245,6 +256,22 @@ export default class Vec2 {
return this.x*other.x + this.y*other.y; 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 * 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); 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 * Returns true if this vector and other have the same x and y
* @param other The vector to check against * @param other The vector to check against
*/ */
equals(other: Vec2): boolean { 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 { getOnChange(): string {
return this.onChange.toString(); 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));
}
} }

View File

@ -5,7 +5,11 @@ export default class Debug {
// A map of log messages to display on the screen // A map of log messages to display on the screen
private static logMessages: Map<string> = new Map(); private static logMessages: Map<string> = 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); this.logMessages.add(id, message);
} }

View File

@ -4,17 +4,16 @@ import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter"; import Emitter from "../Events/Emitter";
import Scene from "../Scene/Scene"; import Scene from "../Scene/Scene";
import Layer from "../Scene/Layer"; 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 Shape from "../DataTypes/Shapes/Shape";
import GameEvent from "../Events/GameEvent";
import Map from "../DataTypes/Map"; import Map from "../DataTypes/Map";
import AABB from "../DataTypes/Shapes/AABB"; 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 * 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 ----------*/ /*---------- POSITIONED ----------*/
private _position: Vec2; private _position: Vec2;
@ -38,6 +37,13 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
sweptRect: AABB; sweptRect: AABB;
isPlayer: boolean; isPlayer: boolean;
/*---------- ACTOR ----------*/
_ai: AI;
aiActive: boolean;
actorId: number;
path: NavigationPath;
pathfinding: boolean = false;
protected input: InputReceiver; protected input: InputReceiver;
protected receiver: Receiver; protected receiver: Receiver;
protected emitter: Emitter; protected emitter: Emitter;
@ -93,6 +99,9 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
finishMove = (): void => { finishMove = (): void => {
this.moving = false; this.moving = false;
this.position.add(this._velocity); 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); 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<T extends AI>(ai: string | (new () => T), options?: Record<string, any>): 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 ----------*/ /*---------- GAME NODE ----------*/
/** /**
* Sets the scene for this object. * 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; abstract update(deltaT: number): void;
} }

View File

@ -0,0 +1,4 @@
export enum GraphicType {
POINT = "POINT",
RECT = "RECT",
}

View File

@ -12,11 +12,12 @@ export default class Point extends Graphic {
update(deltaT: number): void {} update(deltaT: number): void {}
render(ctx: CanvasRenderingContext2D): 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.fillStyle = this.color.toStringRGBA();
ctx.fillRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, 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, this.size.y); this.size.x*zoom, this.size.y*zoom);
} }
} }

View File

@ -34,8 +34,8 @@ export default class Rect extends Graphic {
update(deltaT: number): void {} update(deltaT: number): void {}
render(ctx: CanvasRenderingContext2D): void { render(ctx: CanvasRenderingContext2D): void {
let origin = this.getViewportOriginWithParallax(); let origin = this.scene.getViewTranslation(this);
let zoom = this.getViewportScale(); let zoom = this.scene.getViewScale();
if(this.color.a !== 0){ if(this.color.a !== 0){
ctx.fillStyle = this.color.toStringRGB(); ctx.fillStyle = this.color.toStringRGB();

View File

@ -29,8 +29,8 @@ export default class Sprite extends CanvasNode {
render(ctx: CanvasRenderingContext2D): void { render(ctx: CanvasRenderingContext2D): void {
let image = ResourceManager.getInstance().getImage(this.imageId); let image = ResourceManager.getInstance().getImage(this.imageId);
let origin = this.getViewportOriginWithParallax(); let origin = this.scene.getViewTranslation(this);
let zoom = this.getViewportScale(); let zoom = this.scene.getViewScale();
ctx.drawImage(image, ctx.drawImage(image,
this.imageOffset.x, this.imageOffset.y, this.size.x, this.size.y, this.imageOffset.x, this.imageOffset.y, this.size.x, this.size.y,

View File

@ -85,8 +85,8 @@ export default class OrthogonalTilemap extends Tilemap {
let previousAlpha = ctx.globalAlpha; let previousAlpha = ctx.globalAlpha;
ctx.globalAlpha = this.getLayer().getAlpha(); ctx.globalAlpha = this.getLayer().getAlpha();
let origin = this.getViewportOriginWithParallax(); let origin = this.scene.getViewTranslation(this);
let zoom = this.getViewportScale(); let zoom = this.scene.getViewScale();
if(this.visible){ if(this.visible){
for(let i = 0; i < this.data.length; i++){ for(let i = 0; i < this.data.length; i++){

View File

@ -31,8 +31,10 @@ export default class UIElement extends CanvasNode {
protected isClicked: boolean; protected isClicked: boolean;
protected isEntered: boolean; protected isEntered: boolean;
constructor(){ constructor(position: Vec2){
super(); super();
this.position = position;
this.textColor = new Color(0, 0, 0, 1); this.textColor = new Color(0, 0, 0, 1);
this.backgroundColor = new Color(0, 0, 0, 0); this.backgroundColor = new Color(0, 0, 0, 0);
this.borderColor = 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){ if(this.onClickEventId !== null){
let data = {}; let data = {};
console.log("Click event: " + this.onClickEventId)
this.emitter.fireEvent(this.onClickEventId, data); this.emitter.fireEvent(this.onClickEventId, data);
} }
} }
@ -179,7 +182,7 @@ export default class UIElement extends CanvasNode {
let previousAlpha = ctx.globalAlpha; let previousAlpha = ctx.globalAlpha;
ctx.globalAlpha = this.getLayer().getAlpha(); ctx.globalAlpha = this.getLayer().getAlpha();
let origin = this.getViewportOriginWithParallax(); let origin = this.scene.getViewTranslation(this);
ctx.font = this.fontSize + "px " + this.font; ctx.font = this.fontSize + "px " + this.font;
let offset = this.calculateOffset(ctx); let offset = this.calculateOffset(ctx);

View File

@ -4,8 +4,10 @@ import Vec2 from "../../DataTypes/Vec2";
export default class Button extends UIElement{ export default class Button extends UIElement{
constructor(){ constructor(position: Vec2, text: string){
super(); super(position);
this.text = text;
this.backgroundColor = new Color(150, 75, 203); this.backgroundColor = new Color(150, 75, 203);
this.borderColor = new Color(41, 46, 30); this.borderColor = new Color(41, 46, 30);
this.textColor = new Color(255, 255, 255); this.textColor = new Color(255, 255, 255);

View File

@ -1,8 +1,9 @@
import Vec2 from "../../DataTypes/Vec2";
import UIElement from "../UIElement"; import UIElement from "../UIElement";
export default class Label extends UIElement{ export default class Label extends UIElement{
constructor(text: string){ constructor(position: Vec2, text: string){
super(); super(position);
this.text = text; this.text = text;
} }
} }

View File

@ -0,0 +1,4 @@
export enum UIElementType {
BUTTON = "BUTTON",
LABEL = "LABEL",
}

View File

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

View File

@ -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<Vec2>
protected currentMoveDirection: Vec2;
protected distanceThreshold: number;
constructor(path: Stack<Vec2>){
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();
}
}
}

View File

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

12
src/Physics/Collisions.ts Normal file
View File

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

View File

@ -1,9 +1,16 @@
import Scene from "../Scene"; import Scene from "../Scene";
import SceneGraph from "../../SceneGraph/SceneGraph";
import UIElement from "../../Nodes/UIElement"; import UIElement from "../../Nodes/UIElement";
import Layer from "../Layer"; import Layer from "../Layer";
import Graphic from "../../Nodes/Graphic"; import Graphic from "../../Nodes/Graphic";
import Sprite from "../../Nodes/Sprites/Sprite"; 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 { export default class CanvasNodeFactory {
private scene: Scene; 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 * 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 type The type of UIElement to add
* @param layer The layer to add the UIElement to * @param layerName The layer to add the UIElement to
* @param args Any additional arguments to feed to the constructor * @param options Any additional arguments to feed to the constructor
*/ */
addUIElement = <T extends UIElement>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => { addUIElement = (type: string | UIElementType, layerName: string, options?: Record<string, any>): UIElement => {
let instance = new constr(...args); // 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.setScene(this.scene);
instance.id = this.scene.generateId(); instance.id = this.scene.generateId();
this.scene.getSceneGraph().addNode(instance); this.scene.getSceneGraph().addNode(instance);
// Add instance to layer // Add instance to layer
layer.addNode(instance); layer.addNode(instance)
return instance; return instance;
} }
@ -35,9 +55,11 @@ export default class CanvasNodeFactory {
/** /**
* Adds a sprite to the current scene * Adds a sprite to the current scene
* @param key The key of the image the sprite will represent * @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); let instance = new Sprite(key);
// Add instance to scene // Add instance to scene
@ -53,21 +75,90 @@ export default class CanvasNodeFactory {
/** /**
* Adds a new graphic element to the current Scene * Adds a new graphic element to the current Scene
* @param constr The constructor of the graphic element to add * @param type The type of graphic to add
* @param layer The layer on which to add the graphic * @param layerName The layer on which to add the graphic
* @param args Any additional arguments to send to the graphic constructor * @param options Any additional arguments to send to the graphic constructor
*/ */
addGraphic = <T extends Graphic>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => { addGraphic = (type: GraphicType | string, layerName: string, options?: Record<string, any>): Graphic => {
let instance = new constr(...args); // 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 // Add instance to scene
instance.setScene(this.scene); instance.setScene(this.scene);
instance.id = this.scene.generateId(); instance.id = this.scene.generateId();
if(!(this.scene.isParallaxLayer(layerName) || this.scene.isUILayer(layerName))){
this.scene.getSceneGraph().addNode(instance); this.scene.getSceneGraph().addNode(instance);
}
// Add instance to layer // Add instance to layer
layer.addNode(instance); layer.addNode(instance);
return instance; return instance;
} }
/* ---------- BUILDERS ---------- */
buildButton(options?: Record<string, any>): Button {
this.checkIfPropExists("Button", options, "position", Vec2, "Vec2");
this.checkIfPropExists("Button", options, "text", "string");
return new Button(options.position, options.text);
}
buildLabel(options?: Record<string, any>): Label {
this.checkIfPropExists("Label", options, "position", Vec2, "Vec2");
this.checkIfPropExists("Label", options, "text", "string");
return new Label(options.position, options.text)
}
buildPoint(options?: Record<string, any>): Point {
this.checkIfPropExists("Point", options, "position", Vec2, "Vec2");
return new Point(options.position);
}
buildRect(options?: Record<string, any>): 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<T>(objectName: string, options: Record<string, any>, 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}.`;
}
}
}
} }

View File

@ -1,6 +1,5 @@
import Scene from "../Scene"; import Scene from "../Scene";
import Tilemap from "../../Nodes/Tilemap"; import Tilemap from "../../Nodes/Tilemap";
import PhysicsManager from "../../Physics/PhysicsManager";
import ResourceManager from "../../ResourceManager/ResourceManager"; import ResourceManager from "../../ResourceManager/ResourceManager";
import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap"; import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap";
import Layer from "../Layer"; import Layer from "../Layer";
@ -8,6 +7,8 @@ import Tileset from "../../DataTypes/Tilesets/Tileset";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
import { TiledCollectionTile } from "../../DataTypes/Tilesets/TiledData"; import { TiledCollectionTile } from "../../DataTypes/Tilesets/TiledData";
import Sprite from "../../Nodes/Sprites/Sprite"; import Sprite from "../../Nodes/Sprites/Sprite";
import PositionGraph from "../../DataTypes/Graphs/PositionGraph";
import Navmesh from "../../Pathfinding/Navmesh";
export default class TilemapFactory { export default class TilemapFactory {
private scene: Scene; private scene: Scene;
@ -63,7 +64,7 @@ export default class TilemapFactory {
// Loop over the layers of the tilemap and create tiledlayers or object layers // Loop over the layers of the tilemap and create tiledlayers or object layers
for(let layer of tilemapData.layers){ for(let layer of tilemapData.layers){
let sceneLayer = this.scene.addLayer(); let sceneLayer = this.scene.addLayer(layer.name);
if(layer.type === "tilelayer"){ if(layer.type === "tilelayer"){
// Create a new tilemap object for the layer // Create a new tilemap object for the layer
@ -82,17 +83,34 @@ export default class TilemapFactory {
} }
} else { } else {
let isNavmeshPoints = false let isNavmeshPoints = false;
let navmeshName;
let edges;
if(layer.properties){ if(layer.properties){
for(let prop of layer.properties){ for(let prop of layer.properties){
if(prop.name === "NavmeshPoints"){ if(prop.name === "NavmeshPoints"){
isNavmeshPoints = true; isNavmeshPoints = true;
} else if(prop.name === "name"){
navmeshName = prop.value;
} else if(prop.name === "edges"){
edges = prop.value
} }
} }
} }
if(isNavmeshPoints){ 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; continue;
} }
@ -126,7 +144,7 @@ export default class TilemapFactory {
// The object is a tile from this set // The object is a tile from this set
let imageKey = tileset.getImageKey(); let imageKey = tileset.getImageKey();
let offset = tileset.getImageOffsetForTile(obj.gid); 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(); let size = tileset.getTileSize().clone();
sprite.position.set((obj.x + size.x/2)*scale.x, (obj.y - size.y/2)*scale.y); sprite.position.set((obj.x + size.x/2)*scale.x, (obj.y - size.y/2)*scale.y);
sprite.setImageOffset(offset); sprite.setImageOffset(offset);
@ -140,7 +158,7 @@ export default class TilemapFactory {
for(let tile of collectionTiles){ for(let tile of collectionTiles){
if(obj.gid === tile.id){ if(obj.gid === tile.id){
let imageKey = tile.image; 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.position.set((obj.x + tile.imagewidth/2)*scale.x, (obj.y - tile.imageheight/2)*scale.y);
sprite.scale.set(scale.x, scale.y); sprite.scale.set(scale.x, scale.y);
} }

View File

@ -1,24 +1,39 @@
import Vec2 from "../DataTypes/Vec2";
import Scene from "./Scene"; import Scene from "./Scene";
import MathUtils from "../Utils/MathUtils"; import MathUtils from "../Utils/MathUtils";
import GameNode from "../Nodes/GameNode"; import GameNode from "../Nodes/GameNode";
/** /**
* A layer in the scene. Has its own alpha value and parallax. * A layer in the scene. Has its own alpha value and parallax.
*/ */
export default class Layer { export default class Layer {
/** The scene this layer belongs to */
protected scene: Scene; protected scene: Scene;
protected parallax: Vec2;
/** The name of this layer */
protected name: string;
/** Whether this layer is paused or not */
protected paused: boolean; protected paused: boolean;
/** Whether this layer is hidden from being rendered or not */
protected hidden: boolean; protected hidden: boolean;
/** The global alpha level of this layer */
protected alpha: number; protected alpha: number;
/** An array of the GameNodes that belong to this layer */
protected items: Array<GameNode>; protected items: Array<GameNode>;
/** Whether or not this layer should be ysorted */
protected ySort: boolean; protected ySort: boolean;
/** The depth of this layer compared to other layers */
protected depth: number; protected depth: number;
constructor(scene: Scene){ constructor(scene: Scene, name: string){
this.scene = scene; this.scene = scene;
this.parallax = new Vec2(1, 1); this.name = name;
this.paused = false; this.paused = false;
this.hidden = false; this.hidden = false;
this.alpha = 1; this.alpha = 1;
@ -51,24 +66,18 @@ export default class Layer {
return this.hidden; return this.hidden;
} }
/** Pauses this scene and hides it */
disable(): void { disable(): void {
this.paused = true; this.paused = true;
this.hidden = true; this.hidden = true;
} }
/** Unpauses this layer and makes it visible */
enable(): void { enable(): void {
this.paused = false; this.paused = false;
this.hidden = false; this.hidden = false;
} }
setParallax(x: number, y: number): void {
this.parallax.set(x, y);
}
getParallax(): Vec2 {
return this.parallax;
}
setYSort(ySort: boolean): void { setYSort(ySort: boolean): void {
this.ySort = ySort; this.ySort = ySort;
} }
@ -89,4 +98,8 @@ export default class Layer {
this.items.push(node); this.items.push(node);
node.setLayer(this); node.setLayer(this);
} }
getItems(): Array<GameNode> {
return this.items;
}
} }

View File

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

View File

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

View File

@ -13,7 +13,13 @@ import SceneManager from "./SceneManager";
import Receiver from "../Events/Receiver"; import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter"; import Emitter from "../Events/Emitter";
import { Renderable, Updateable } from "../DataTypes/Interfaces/Descriptors"; 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 { export default class Scene implements Updateable, Renderable {
/** The size of the game world. */ /** The size of the game world. */
@ -40,18 +46,33 @@ export default class Scene implements Updateable, Renderable {
/** This list of tilemaps in this scene. */ /** This list of tilemaps in this scene. */
protected tilemaps: Array<Tilemap>; protected tilemaps: Array<Tilemap>;
/** A map from layer names to the layers themselves */
protected layers: Map<Layer>;
/** A map from parallax layer names to the parallax layers themselves */
protected parallaxLayers: Map<ParallaxLayer>;
/** A map from uiLayer names to the uiLayers themselves */
protected uiLayers: Map<UILayer>;
/** The scene graph of the Scene*/ /** The scene graph of the Scene*/
protected sceneGraph: SceneGraph; protected sceneGraph: SceneGraph;
/** The physics manager of the Scene */
protected physicsManager: PhysicsManager; 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 */ /** An interface that allows the adding of different nodes to the scene */
public add: FactoryManager; public add: FactoryManager;
/** An interface that allows the loading of different files for use in the scene */ /** An interface that allows the loading of different files for use in the scene */
public load: ResourceManager; public load: ResourceManager;
protected navmeshes: Array<Navmesh>;
constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){
this.worldSize = new Vec2(500, 500); this.worldSize = new Vec2(500, 500);
this.viewport = viewport; this.viewport = viewport;
@ -64,7 +85,14 @@ export default class Scene implements Updateable, Renderable {
this.tilemaps = new Array(); this.tilemaps = new Array();
this.sceneGraph = new SceneGraphArray(this.viewport, this); this.sceneGraph = new SceneGraphArray(this.viewport, this);
this.layers = new Map();
this.uiLayers = new Map();
this.parallaxLayers = new Map();
this.physicsManager = new BasicPhysicsManager(); this.physicsManager = new BasicPhysicsManager();
this.navManager = new NavigationManager();
this.aiManager = new AIManager();
this.add = new FactoryManager(this, this.tilemaps); this.add = new FactoryManager(this, this.tilemaps);
@ -89,6 +117,9 @@ export default class Scene implements Updateable, Renderable {
update(deltaT: number): void { update(deltaT: number): void {
this.updateScene(deltaT); this.updateScene(deltaT);
// Do all AI updates
this.aiManager.update(deltaT);
// Update all physics objects // Update all physics objects
this.physicsManager.update(deltaT); 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. // We need to keep track of the order of things.
let visibleSet = this.sceneGraph.getVisibleSet(); 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 // Render scene graph for demo
this.sceneGraph.render(ctx); this.sceneGraph.render(ctx);
@ -124,6 +174,9 @@ export default class Scene implements Updateable, Renderable {
// Debug render the physicsManager // Debug render the physicsManager
this.physicsManager.debug_render(ctx); this.physicsManager.debug_render(ctx);
// Render the uiLayers
this.uiLayers.forEach(key => this.uiLayers.get(key).getItems().forEach(node => (<CanvasNode>node).render(ctx)));
} }
setRunning(running: boolean): void { setRunning(running: boolean): void {
@ -136,9 +189,112 @@ export default class Scene implements Updateable, Renderable {
/** /**
* Adds a new layer to the scene and returns it * 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 { addLayer(name: string, depth?: number): Layer {
return this.sceneGraph.addLayer(); 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 */ /** Returns the viewport associated with this scene */
@ -158,6 +314,14 @@ export default class Scene implements Updateable, Renderable {
return this.physicsManager; return this.physicsManager;
} }
getNavigationManager(): NavigationManager {
return this.navManager;
}
getAIManager(): AIManager {
return this.aiManager;
}
generateId(): number { generateId(): number {
return this.sceneManager.generateId(); return this.sceneManager.generateId();
} }

View File

@ -3,8 +3,6 @@ import CanvasNode from "../Nodes/CanvasNode";
import Map from "../DataTypes/Map"; import Map from "../DataTypes/Map";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import Scene from "../Scene/Scene"; import Scene from "../Scene/Scene";
import Layer from "../Scene/Layer";
import Stack from "../DataTypes/Stack";
import AABB from "../DataTypes/Shapes/AABB"; import AABB from "../DataTypes/Shapes/AABB";
/** /**
@ -15,14 +13,12 @@ export default abstract class SceneGraph {
protected nodeMap: Map<CanvasNode>; protected nodeMap: Map<CanvasNode>;
protected idCounter: number; protected idCounter: number;
protected scene: Scene; protected scene: Scene;
protected layers: Stack<Layer>;
constructor(viewport: Viewport, scene: Scene){ constructor(viewport: Viewport, scene: Scene){
this.viewport = viewport; this.viewport = viewport;
this.scene = scene; this.scene = scene;
this.nodeMap = new Map<CanvasNode>(); this.nodeMap = new Map<CanvasNode>();
this.idCounter = 0; this.idCounter = 0;
this.layers = new Stack(10);
} }
/** /**
@ -94,18 +90,6 @@ export default abstract class SceneGraph {
*/ */
protected abstract getNodesAtCoords(x: number, y: number): Array<CanvasNode>; protected abstract getNodesAtCoords(x: number, y: number): Array<CanvasNode>;
addLayer(): Layer {
let layer = new Layer(this.scene);
let depth = this.layers.size();
layer.setDepth(depth);
this.layers.push(layer);
return layer;
}
getLayers(): Stack<Layer> {
return this.layers;
}
abstract update(deltaT: number): void; abstract update(deltaT: number): void;
abstract render(ctx: CanvasRenderingContext2D): void; abstract render(ctx: CanvasRenderingContext2D): void;

View File

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

View File

@ -77,15 +77,6 @@ export default class SceneGraphQuadTree extends SceneGraph {
visibleSet = visibleSet.filter(node => !node.getLayer().isHidden()); 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; return visibleSet;
} }
} }

View File

@ -6,6 +6,8 @@ import Queue from "../DataTypes/Queue";
import AABB from "../DataTypes/Shapes/AABB"; import AABB from "../DataTypes/Shapes/AABB";
import Debug from "../Debug/Debug"; import Debug from "../Debug/Debug";
import InputReceiver from "../Input/InputReceiver"; import InputReceiver from "../Input/InputReceiver";
import ParallaxLayer from "../Scene/Layers/ParallaxLayer";
import UILayer from "../Scene/Layers/UILayer";
export default class Viewport { export default class Viewport {
private view: AABB; private view: AABB;
@ -46,8 +48,9 @@ export default class Viewport {
return this.view.center; return this.view.center;
} }
/** Returns a new Vec2 with the origin of the viewport */
getOrigin(): Vec2 { 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 * @param node
*/ */
includes(node: CanvasNode): boolean { includes(node: CanvasNode): boolean {
let parallax = node.getLayer().getParallax(); let parallax = node.getLayer() instanceof ParallaxLayer || node.getLayer() instanceof UILayer ? (<ParallaxLayer>node.getLayer()).parallax : new Vec2(1, 1);
let center = this.view.center.clone(); let center = this.view.center.clone();
this.view.center.mult(parallax); this.view.center.mult(parallax);
let overlaps = this.view.overlaps(node.boundary); 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 viewport is following an object
if(this.following){ if(this.following){
// Update our list of previous positions // Update our list of previous positions
@ -220,8 +221,6 @@ export default class Viewport {
pos.x = Math.floor(pos.x); pos.x = Math.floor(pos.x);
pos.y = Math.floor(pos.y); pos.y = Math.floor(pos.y);
Debug.log("vp", "Viewport pos: " + pos.toString())
this.view.center.copy(pos); this.view.center.copy(pos);
} else { } else {
if(this.lastPositions.getSize() > this.smoothingFactor){ if(this.lastPositions.getSize() > this.smoothingFactor){
@ -240,7 +239,6 @@ export default class Viewport {
pos.x = Math.floor(pos.x); pos.x = Math.floor(pos.x);
pos.y = Math.floor(pos.y); pos.y = Math.floor(pos.y);
Debug.log("vp", "Viewport pos: " + pos.toString())
this.view.center.copy(pos); this.view.center.copy(pos);
} }
} }

View File

@ -2,11 +2,12 @@ import Graph, { EdgeNode } from "../DataTypes/Graphs/Graph";
export default class GraphUtils { export default class GraphUtils {
static djikstra(g: Graph, start: number): void { static djikstra(g: Graph, start: number): Array<number> {
let i: number; // Counter let i: number; // Counter
let p: EdgeNode; // Pointer to edgenode let p: EdgeNode; // Pointer to edgenode
let inTree: Array<boolean> let inTree: Array<boolean> = new Array(g.numVertices);
let distance: number; let distance: Array<number> = new Array(g.numVertices);
let parent: Array<number> = new Array(g.numVertices);
let v: number; // Current vertex to process let v: number; // Current vertex to process
let w: number; // Candidate for next vertex let w: number; // Candidate for next vertex
let weight: number; // Edge weight let weight: number; // Edge weight
@ -15,7 +16,41 @@ export default class GraphUtils {
for(i = 0; i < g.numVertices; i++){ for(i = 0; i < g.numVertices; i++){
inTree[i] = false; inTree[i] = false;
distance[i] = Infinity; 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;
}
}

View File

@ -41,10 +41,14 @@ export default class MathUtils {
* Linear Interpolation * Linear Interpolation
* @param a The first value for the interpolation bound * @param a The first value for the interpolation bound
* @param b The second 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){ static lerp(a: number, b: number, t: number): number {
return a + x * (b - a); return a + t * (b - a);
}
static invLerp(a: number, b: number, value: number){
return (value - a)/(b - a);
} }
/** /**

View File

@ -8,13 +8,13 @@ export default class Boid extends Graphic {
acceleration: Vec2 = Vec2.ZERO; acceleration: Vec2 = Vec2.ZERO;
velocity: Vec2 = Vec2.ZERO; velocity: Vec2 = Vec2.ZERO;
ai: BoidController; //ai: BoidController;
fb: FlockBehavior; fb: FlockBehavior;
constructor(position: Vec2){ constructor(position: Vec2){
super(); super();
this.position = position; this.position = position;
this.ai = new BoidController(this); //this.ai = new BoidController(this);
} }
update(deltaT: number){ update(deltaT: number){
@ -22,8 +22,8 @@ export default class Boid extends Graphic {
} }
render(ctx: CanvasRenderingContext2D): void { render(ctx: CanvasRenderingContext2D): void {
let origin = this.getViewportOriginWithParallax(); let origin = this.scene.getViewTranslation(this);
let zoom = this.getViewportScale(); let zoom = this.scene.getViewScale();
let dirVec = this.direction.scaled(this.size.x, this.size.y); 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)); 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));

View File

@ -1,7 +1,6 @@
import State from "../../../DataTypes/State/State"; import State from "../../../DataTypes/State/State";
import StateMachine from "../../../DataTypes/State/StateMachine"; import StateMachine from "../../../DataTypes/State/StateMachine";
import Vec2 from "../../../DataTypes/Vec2"; import Vec2 from "../../../DataTypes/Vec2";
import Debug from "../../../Debug/Debug";
import GameEvent from "../../../Events/GameEvent"; import GameEvent from "../../../Events/GameEvent";
import MathUtils from "../../../Utils/MathUtils"; import MathUtils from "../../../Utils/MathUtils";
import { CustomGameEventType } from "../../CustomGameEventType"; import { CustomGameEventType } from "../../CustomGameEventType";
@ -68,17 +67,6 @@ export default class BoidBehavior extends State {
this.actor.direction = this.actor.velocity.clone(); this.actor.direction = this.actor.velocity.clone();
speed = MathUtils.clamp(speed, BoidBehavior.MIN_SPEED, BoidBehavior.MAX_SPEED); speed = MathUtils.clamp(speed, BoidBehavior.MIN_SPEED, BoidBehavior.MAX_SPEED);
this.actor.velocity.scale(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 // Update the position

View File

@ -55,13 +55,5 @@ export default class GoombaController extends StateMachine {
update(deltaT: number): void { update(deltaT: number): void {
super.update(deltaT); 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");
}
} }
} }

View File

@ -6,6 +6,7 @@ import PlayerController from "../Player/PlayerStates/Platformer/PlayerController
import { PlayerStates } from "../Player/PlayerStates/Platformer/PlayerController"; import { PlayerStates } from "../Player/PlayerStates/Platformer/PlayerController";
import GoombaController from "../Enemies/GoombaController"; import GoombaController from "../Enemies/GoombaController";
import InputReceiver from "../../Input/InputReceiver"; import InputReceiver from "../../Input/InputReceiver";
import { GraphicType } from "../../Nodes/Graphics/GraphicTypes";
export default class MarioClone extends Scene { export default class MarioClone extends Scene {
@ -15,24 +16,22 @@ export default class MarioClone extends Scene {
} }
startScene(): void { startScene(): void {
let layer = this.addLayer(); this.addLayer("main");
this.add.tilemap("level", new Vec2(2, 2)); 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.setColor(Color.BLUE);
player.addPhysics(); player.addPhysics();
player.isPlayer = true; player.isPlayer = true;
this.viewport.follow(player); this.viewport.follow(player);
this.viewport.setBounds(0, 0, 5120, 1280); this.viewport.setBounds(0, 0, 5120, 1280);
let ai = new PlayerController(player); player.ai = new PlayerController();
ai.initialize(PlayerStates.IDLE);
player.update = (deltaT: number) => {ai.update(deltaT)};
player.addTrigger("CoinBlock", "playerHitCoinBlock"); player.addTrigger("CoinBlock", "playerHitCoinBlock");
for(let xPos of [14, 20, 25, 30, 33, 37, 49, 55, 58, 70, 74]){ 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); let ai = new GoombaController(goomba, false);
ai.initialize("idle"); ai.initialize("idle");
goomba.update = (deltaT: number) => {ai.update(deltaT)}; goomba.update = (deltaT: number) => {ai.update(deltaT)};

View File

@ -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<string, any>): 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");
}
}

View File

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

View File

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

View File

@ -1,7 +1,10 @@
import Scene from "../../Scene/Scene"; import Scene from "../../Scene/Scene";
import Rect from "../../Nodes/Graphics/Rect";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
import PlayerController from "../Player/PlayerController"; 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 { export default class PathfindingScene extends Scene {
@ -12,15 +15,32 @@ export default class PathfindingScene extends Scene {
startScene(){ startScene(){
this.add.tilemap("interior"); 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(); player.addPhysics();
let ai = new PlayerController(player, "topdown"); player.addAI(PlayerController, {playerType: "topdown", speed: 400});
ai.speed = 400;
player.update = (deltaT: number) => {ai.update(deltaT)} // Set the viewport to follow the player
this.viewport.setBounds(0, 0, 40*64, 40*64); this.viewport.setBounds(0, 0, 40*64, 40*64);
this.viewport.follow(player); this.viewport.follow(player);
this.viewport.enableZoom(); 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";
} }
} }

View File

@ -8,10 +8,10 @@ export default class Player extends Rect {
constructor(position: Vec2){ constructor(position: Vec2){
super(position, new Vec2(20, 20)); super(position, new Vec2(20, 20));
this.controller = new PlayerController(this, PlayerType.TOPDOWN); //this.controller = new PlayerController(this, PlayerType.TOPDOWN);
} }
update(deltaT: number): void { update(deltaT: number): void {
this.controller.update(deltaT); //this.controller.update(deltaT);
} }
} }

View File

@ -1,4 +1,4 @@
import StateMachine from "../../DataTypes/State/StateMachine"; import StateMachineAI from "../../AI/StateMachineAI";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
import Debug from "../../Debug/Debug"; import Debug from "../../Debug/Debug";
import GameNode from "../../Nodes/GameNode"; import GameNode from "../../Nodes/GameNode";
@ -23,21 +23,18 @@ export enum PlayerStates {
PREVIOUS = "previous" PREVIOUS = "previous"
} }
export default class PlayerController extends StateMachine { export default class PlayerController extends StateMachineAI {
protected owner: GameNode; protected owner: GameNode;
velocity: Vec2 = Vec2.ZERO; velocity: Vec2 = Vec2.ZERO;
speed: number; speed: number = 400;
MIN_SPEED: number = 400; MIN_SPEED: number = 400;
MAX_SPEED: number = 1000; MAX_SPEED: number = 1000;
initializeAI(owner: GameNode, config: Record<string, any>){
constructor(owner: GameNode, playerType: string){
super();
this.owner = owner; this.owner = owner;
if(playerType === PlayerType.TOPDOWN){ if(config.playerType === PlayerType.TOPDOWN){
this.initializeTopDown(); this.initializeTopDown(config.speed);
} else { } else {
this.initializePlatformer(); this.initializePlatformer();
} }
@ -46,11 +43,11 @@ export default class PlayerController extends StateMachine {
/** /**
* Initializes the player controller for a top down player * Initializes the player controller for a top down player
*/ */
initializeTopDown(): void { initializeTopDown(speed: number): void {
let idle = new IdleTopDown(this); let idle = new IdleTopDown(this);
let move = new MoveTopDown(this, this.owner); let move = new MoveTopDown(this, this.owner);
this.speed = 150; this.speed = speed ? speed : 150;
this.addState(PlayerStates.IDLE, idle); this.addState(PlayerStates.IDLE, idle);
this.addState(PlayerStates.MOVE, move); this.addState(PlayerStates.MOVE, move);

View File

@ -1,4 +1,3 @@
import StateMachine from "../../../../DataTypes/State/StateMachine";
import Debug from "../../../../Debug/Debug"; import Debug from "../../../../Debug/Debug";
import Idle from "./Idle"; import Idle from "./Idle";
import Jump from "./Jump"; import Jump from "./Jump";
@ -6,6 +5,7 @@ import Walk from "./Walk";
import Run from "./Run"; import Run from "./Run";
import GameNode from "../../../../Nodes/GameNode"; import GameNode from "../../../../Nodes/GameNode";
import Vec2 from "../../../../DataTypes/Vec2"; import Vec2 from "../../../../DataTypes/Vec2";
import StateMachineAI from "../../../../AI/StateMachineAI";
export enum PlayerStates { export enum PlayerStates {
WALK = "walk", WALK = "walk",
@ -15,16 +15,14 @@ export enum PlayerStates {
PREVIOUS = "previous" PREVIOUS = "previous"
} }
export default class PlayerController extends StateMachine { export default class PlayerController extends StateMachineAI {
protected owner: GameNode; protected owner: GameNode;
velocity: Vec2 = Vec2.ZERO; velocity: Vec2 = Vec2.ZERO;
speed: number = 400; speed: number = 400;
MIN_SPEED: number = 400; MIN_SPEED: number = 400;
MAX_SPEED: number = 1000; MAX_SPEED: number = 1000;
constructor(owner: GameNode){ initializeAI(owner: GameNode, config: Record<string, any>): void {
super();
this.owner = owner; this.owner = owner;
let idle = new Idle(this, owner); let idle = new Idle(this, owner);
@ -35,6 +33,8 @@ export default class PlayerController extends StateMachine {
this.addState(PlayerStates.RUN, run); this.addState(PlayerStates.RUN, run);
let jump = new Jump(this, owner); let jump = new Jump(this, owner);
this.addState(PlayerStates.JUMP, jump); this.addState(PlayerStates.JUMP, jump);
this.initialize(PlayerStates.IDLE);
} }
currentStateString: string = ""; currentStateString: string = "";