diff --git a/src/BoidDemo.ts b/src/BoidDemo.ts index 57e116a..558959b 100644 --- a/src/BoidDemo.ts +++ b/src/BoidDemo.ts @@ -32,7 +32,7 @@ export default class BoidDemo extends Scene { this.viewport.enableZoom(); // Create a bunch of boids - for(let i = 0; i < 200; 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())); boid.fb = new FlockBehavior(this, boid, this.boids, 75, 50); boid.setSize(5, 5); diff --git a/src/DataTypes/RegionQuadTree.ts b/src/DataTypes/RegionQuadTree.ts index 711bcf8..bdf8451 100644 --- a/src/DataTypes/RegionQuadTree.ts +++ b/src/DataTypes/RegionQuadTree.ts @@ -3,6 +3,7 @@ import Collection from "./Collection"; import AABB from "./AABB" import { Region, Unique } from "./Interfaces/Descriptors"; import Map from "./Map"; +import Stats from "../Debug/Stats"; /** * Primarily used to organize the scene graph @@ -144,7 +145,7 @@ export default class QuadTree implements Collection { let results = new Array(); // A map to keep track of the items we've already found - let uniqueMap = new Map(); + let uniqueMap = new Array(); // Query and return this._queryRegion(boundary, results, uniqueMap); @@ -157,7 +158,7 @@ export default class QuadTree implements Collection { * @param results The results matrix * @param uniqueMap A map that stores the unique ids of the results so we know what was already found */ - protected _queryRegion(boundary: AABB, results: Array, uniqueMap: Map): void { + protected _queryRegion(boundary: AABB, results: Array, uniqueMap: Array): void { // Does this quadtree even contain the point? if(!this.boundary.overlaps(boundary)) return; @@ -170,12 +171,22 @@ export default class QuadTree implements Collection { } else { // Otherwise, return a set of the items for(let item of this.items){ - let id = item.getId().toString(); - // If the item hasn't been found yet and it contains the point - if(!uniqueMap.has(id) && item.getBoundary().overlaps(boundary)){ - // Add it to our found points - uniqueMap.add(id, item); - results.push(item); + // TODO - This is REALLY slow for some reason when we check for unique keys + + // let id = item.getId().toString(); + // // If the item hasn't been found yet and it contains the point + // if(!uniqueMap.has(id) && item.getBoundary().overlaps(boundary)){ + // // Add it to our found points + // uniqueMap.add(id, item); + // results.push(item); + // } + + // Maybe this is better? Just use a boolean array with no string nonsense? + if(item.getId() >= uniqueMap.length || !uniqueMap[item.getId()]){ + if(item.getBoundary().overlaps(boundary)){ + results.push(item); + uniqueMap[item.getId()] = true; + } } } } diff --git a/src/Debug/Stats.ts b/src/Debug/Stats.ts new file mode 100644 index 0000000..a7c1ecd --- /dev/null +++ b/src/Debug/Stats.ts @@ -0,0 +1,243 @@ +import Color from "../Utils/Color"; + +export default class Stats extends Object { + /** The fps of the game. */ + private static prevfps: Array; + private static readonly NUM_POINTS: number = 60; + private static ctx: CanvasRenderingContext2D; + private static CANVAS_WIDTH: number = 300; + private static CANVAS_HEIGHT: number = 300; + private static statsDiv: HTMLDivElement; + private static graphChoices: HTMLSelectElement; + + // Quadtree stats + private static prevClearTimes: Array; + private static SGClearTimes: Array; + private static avgSGClearTime: number; + + private static prevFillTimes: Array; + private static SGFillTimes: Array; + private static avgSGFillTime: number; + + private static prevUpdateTimes: Array; + private static SGUpdateTimes: Array; + private static avgSGUpdateTime: number; + + private static prevQueryTimes: Array; + private static SGQueryTimes: Array; + private static avgSGQueryTime: number; + + static initStats(): void { + let canvas = document.getElementById("stats-canvas"); + canvas.width = this.CANVAS_WIDTH; + canvas.height = this.CANVAS_HEIGHT; + this.ctx = canvas.getContext("2d"); + + this.statsDiv = document.getElementById("stats-display"); + + this.prevfps = new Array(); + + this.prevClearTimes = new Array(); + this.SGClearTimes = new Array(); + this.avgSGClearTime = 0; + + this.prevFillTimes = new Array(); + this.SGFillTimes = new Array(); + this.avgSGFillTime = 0; + + this.prevUpdateTimes = new Array(); + this.SGUpdateTimes = new Array(); + this.avgSGUpdateTime = 0; + + this.prevQueryTimes = new Array(); + this.SGQueryTimes = new Array(); + this.avgSGQueryTime = 0; + + let clearTime = document.createElement("span"); + clearTime.setAttribute("id", "sgclear"); + let fillTime = document.createElement("span"); + fillTime.setAttribute("id", "sgfill"); + let updateTime = document.createElement("span"); + updateTime.setAttribute("id", "sgupdate"); + let queryTime = document.createElement("span"); + queryTime.setAttribute("id", "sgquery"); + let br1 = document.createElement("br"); + let br2 = document.createElement("br"); + let br3 = document.createElement("br"); + + this.statsDiv.append(clearTime, br1, fillTime, br2, updateTime, br3, queryTime); + + this.graphChoices = document.getElementById("chart-option"); + let option1 = document.createElement("option"); + option1.value = "prevfps"; + option1.label = "FPS"; + let option2 = document.createElement("option"); + option2.value = "prevClearTimes"; + option2.label = "Clear Time"; + let option3 = document.createElement("option"); + option3.value = "prevFillTimes"; + option3.label = "Fill time"; + let option4 = document.createElement("option"); + option4.value = "prevUpdateTimes"; + option4.label = "Update time"; + let option5 = document.createElement("option"); + option5.value = "prevQueryTimes"; + option5.label = "Query Time"; + let optionAll = document.createElement("option"); + optionAll.value = "all"; + optionAll.label = "All"; + this.graphChoices.append(option1, option2, option3, option4, option5, optionAll); + } + + static updateFPS(fps: number): void { + this.prevfps.push(fps); + if(this.prevfps.length > Stats.NUM_POINTS){ + this.prevfps.shift(); + } + + if(this.SGClearTimes.length > 0){ + this.prevClearTimes.push(this.avgSGClearTime); + if(this.prevClearTimes.length > this.NUM_POINTS){ + this.prevClearTimes.shift(); + } + } + if(this.SGFillTimes.length > 0){ + this.prevFillTimes.push(this.avgSGFillTime); + if(this.prevFillTimes.length > this.NUM_POINTS){ + this.prevFillTimes.shift(); + } + } + if(this.SGUpdateTimes.length > 0){ + this.prevUpdateTimes.push(this.avgSGUpdateTime); + if(this.prevUpdateTimes.length > this.NUM_POINTS){ + this.prevUpdateTimes.shift(); + } + } + if(this.SGQueryTimes.length > 0){ + this.prevQueryTimes.push(this.avgSGQueryTime); + if(this.prevQueryTimes.length > this.NUM_POINTS){ + this.prevQueryTimes.shift(); + } + } + + this.updateSGStats(); + } + + static log(key: string, data: any): void { + if(key === "sgclear"){ + this.SGClearTimes.push(data); + if(this.SGClearTimes.length > 100){ + this.SGClearTimes.shift(); + } + } else if(key === "sgfill"){ + this.SGFillTimes.push(data); + if(this.SGFillTimes.length > 100){ + this.SGFillTimes.shift(); + } + } else if(key === "sgupdate"){ + this.SGUpdateTimes.push(data); + if(this.SGUpdateTimes.length > 100){ + this.SGUpdateTimes.shift(); + } + } else if(key === "sgquery"){ + this.SGQueryTimes.push(data); + if(this.SGQueryTimes.length > 1000){ + this.SGQueryTimes.shift(); + } + } + + } + + static render(): void { + // Display stats + this.drawCharts(); + } + + static drawCharts(){ + this.ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT); + + let paramString = this.graphChoices.value; + + if(paramString === "prevfps" || paramString === "all"){ + let param = this.prevfps; + let color = Color.BLUE.toString(); + this.drawChart(param, color); + } + if(paramString === "prevClearTimes" || paramString === "all"){ + let param = this.prevClearTimes; + let color = Color.RED.toString(); + this.drawChart(param, color); + } + if(paramString === "prevFillTimes" || paramString === "all"){ + let param = this.prevFillTimes; + let color = Color.GREEN.toString(); + this.drawChart(param, color); + } + if(paramString === "prevUpdateTimes" || paramString === "all"){ + let param = this.prevUpdateTimes; + let color = Color.CYAN.toString(); + this.drawChart(param, color); + } + if(paramString === "prevQueryTimes" || paramString === "all"){ + let param = this.prevQueryTimes; + let color = Color.ORANGE.toString(); + this.drawChart(param, color); + } + } + + static drawChart(param: Array, color: string){ + this.ctx.strokeStyle = Color.BLACK.toString(); + this.ctx.beginPath(); + this.ctx.moveTo(10, 10); + this.ctx.lineTo(10, this.CANVAS_HEIGHT - 10); + this.ctx.closePath(); + this.ctx.stroke(); + this.ctx.beginPath(); + this.ctx.moveTo(10, this.CANVAS_HEIGHT - 10); + this.ctx.lineTo(this.CANVAS_WIDTH - 10, this.CANVAS_HEIGHT - 10); + this.ctx.closePath(); + this.ctx.stroke(); + + let max = Math.max(...param); + let prevX = 10; + let prevY = this.CANVAS_HEIGHT - 10 - param[0]/max*(this.CANVAS_HEIGHT-20); + this.ctx.strokeStyle = color; + + for(let i = 1; i < param.length; i++){ + let fps = param[i]; + let x = 10 + i*(this.CANVAS_WIDTH - 20)/this.NUM_POINTS; + let y = this.CANVAS_HEIGHT - 10 - fps/max*(this.CANVAS_HEIGHT-20) + this.ctx.beginPath(); + this.ctx.moveTo(prevX, prevY); + this.ctx.lineTo(x, y); + this.ctx.closePath(); + this.ctx.stroke(); + + prevX = x; + prevY = y; + } + } + + static updateSGStats(){ + if(this.SGClearTimes.length > 0){ + this.avgSGClearTime = this.SGClearTimes.reduce((acc, val) => acc + val)/this.SGClearTimes.length; + } + + if(this.SGFillTimes.length > 0){ + this.avgSGFillTime = this.SGFillTimes.reduce((acc, val) => acc + val)/this.SGFillTimes.length; + } + + if(this.SGUpdateTimes.length > 0){ + this.avgSGUpdateTime = this.SGUpdateTimes.reduce((acc, val) => acc + val)/this.SGUpdateTimes.length; + } + + if(this.SGQueryTimes.length > 0){ + this.avgSGQueryTime = this.SGQueryTimes.reduce((acc, val) => acc + val)/this.SGQueryTimes.length; + } + + document.getElementById("sgclear").innerHTML = "Avg SG clear time: " + this.avgSGClearTime; + document.getElementById("sgfill").innerHTML = "Avg SG fill time: " + this.avgSGFillTime; + document.getElementById("sgupdate").innerHTML = "Avg SG update time: " + this.avgSGUpdateTime; + document.getElementById("sgquery").innerHTML = "Avg SG query time: " + this.avgSGQueryTime; + } +} \ No newline at end of file diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 06c415e..b7333b1 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -7,6 +7,7 @@ import ResourceManager from "../ResourceManager/ResourceManager"; import Viewport from "../SceneGraph/Viewport"; import SceneManager from "../Scene/SceneManager"; import AudioManager from "../Sound/AudioManager"; +import Stats from "../Debug/Stats"; export default class GameLoop { /** The max allowed update fps.*/ @@ -112,6 +113,8 @@ export default class GameLoop { this.resourceManager = ResourceManager.getInstance(); this.sceneManager = new SceneManager(this.viewport, this); this.audioManager = AudioManager.getInstance(); + + Stats.initStats(); } private initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D { @@ -153,6 +156,7 @@ export default class GameLoop { this.framesSinceLastFpsUpdate = 0; Debug.log("fps", "FPS: " + this.fps.toFixed(1)); + Stats.updateFPS(this.fps); } /** @@ -216,6 +220,7 @@ export default class GameLoop { this.numUpdateSteps++; if(this.numUpdateSteps > 100){ this.panic = true; + break; } } @@ -272,6 +277,7 @@ export default class GameLoop { this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); this.sceneManager.render(this.ctx); Debug.render(this.ctx); + Stats.render(); } } diff --git a/src/SceneGraph/SceneGraphArray.ts b/src/SceneGraph/SceneGraphArray.ts index d52a274..5b71f03 100644 --- a/src/SceneGraph/SceneGraphArray.ts +++ b/src/SceneGraph/SceneGraphArray.ts @@ -5,6 +5,7 @@ import Scene from "../Scene/Scene"; import Stack from "../DataTypes/Stack"; import Layer from "../Scene/Layer" import AABB from "../DataTypes/AABB"; +import Stats from "../Debug/Stats"; export default class SceneGraphArray extends SceneGraph{ private nodeList: Array; @@ -45,23 +46,29 @@ export default class SceneGraphArray extends SceneGraph{ } getNodesInRegion(boundary: AABB): Array { + let t0 = performance.now(); let results = []; for(let node of this.nodeList){ - if(boundary.overlapArea(node.getBoundary())){ + if(boundary.overlaps(node.getBoundary())){ results.push(node); } } + let t1 = performance.now(); + Stats.log("sgquery", (t1-t0)); return results; } update(deltaT: number): void { + let t0 = performance.now(); for(let node of this.nodeList){ if(!node.getLayer().isPaused()){ node.update(deltaT); } } + let t1 = performance.now(); + Stats.log("sgupdate", (t1-t0)); } render(ctx: CanvasRenderingContext2D): void {} diff --git a/src/SceneGraph/SceneGraphQuadTree.ts b/src/SceneGraph/SceneGraphQuadTree.ts index 44baf59..577d5b8 100644 --- a/src/SceneGraph/SceneGraphQuadTree.ts +++ b/src/SceneGraph/SceneGraphQuadTree.ts @@ -5,6 +5,7 @@ import Scene from "../Scene/Scene"; import RegionQuadTree from "../DataTypes/RegionQuadTree"; import Vec2 from "../DataTypes/Vec2"; import AABB from "../DataTypes/AABB"; +import Stats from "../Debug/Stats"; export default class SceneGraphQuadTree extends SceneGraph { private qt: RegionQuadTree; @@ -34,23 +35,35 @@ export default class SceneGraphQuadTree extends SceneGraph { } getNodesInRegion(boundary: AABB): Array { - return this.qt.queryRegion(boundary); + let t0 = performance.now(); + let res = this.qt.queryRegion(boundary); + let t1 = performance.now(); + + Stats.log("sgquery", (t1-t0)); + + return res; } update(deltaT: number): void { + let t0 = performance.now(); this.qt.clear(); + let t1 = performance.now(); + Stats.log("sgclear", (t1-t0)); + + t0 = performance.now(); for(let node of this.nodes){ this.qt.insert(node); } + t1 = performance.now(); + Stats.log("sgfill", (t1-t0)); + + t0 = performance.now(); this.nodes.forEach((node: CanvasNode) => node.update(deltaT)); - // TODO: forEach is buggy, some nodes are update multiple times - // this.qt.forEach((node: CanvasNode) => { - // if(!node.getLayer().isPaused()){ - // node.update(deltaT); - // } - // }); + t1 = performance.now(); + + Stats.log("sgupdate", (t1-t0)); } render(ctx: CanvasRenderingContext2D): void { diff --git a/src/index.html b/src/index.html index 1891d11..a20ad98 100644 --- a/src/index.html +++ b/src/index.html @@ -2,10 +2,18 @@ - Hello World! + Game - +
+ +
+ + +
+
+
\ No newline at end of file