added stats

This commit is contained in:
Joe Weaver 2020-10-21 15:30:13 -04:00
parent 15dd74f45e
commit c77a947cc0
7 changed files with 307 additions and 19 deletions

View File

@ -32,7 +32,7 @@ export default class BoidDemo extends Scene {
this.viewport.enableZoom(); this.viewport.enableZoom();
// Create a bunch of boids // 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())); 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.setSize(5, 5); boid.setSize(5, 5);

View File

@ -3,6 +3,7 @@ import Collection from "./Collection";
import AABB from "./AABB" import AABB from "./AABB"
import { Region, Unique } from "./Interfaces/Descriptors"; import { Region, Unique } from "./Interfaces/Descriptors";
import Map from "./Map"; import Map from "./Map";
import Stats from "../Debug/Stats";
/** /**
* Primarily used to organize the scene graph * Primarily used to organize the scene graph
@ -144,7 +145,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
let results = new Array<T>(); let results = new Array<T>();
// A map to keep track of the items we've already found // A map to keep track of the items we've already found
let uniqueMap = new Map<T>(); let uniqueMap = new Array<boolean>();
// Query and return // Query and return
this._queryRegion(boundary, results, uniqueMap); this._queryRegion(boundary, results, uniqueMap);
@ -157,7 +158,7 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
* @param results The results matrix * @param results The results matrix
* @param uniqueMap A map that stores the unique ids of the results so we know what was already found * @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<T>, uniqueMap: Map<T>): void { protected _queryRegion(boundary: AABB, results: Array<T>, uniqueMap: Array<boolean>): void {
// Does this quadtree even contain the point? // Does this quadtree even contain the point?
if(!this.boundary.overlaps(boundary)) return; if(!this.boundary.overlaps(boundary)) return;
@ -170,12 +171,22 @@ export default class QuadTree<T extends Region & Unique> implements Collection {
} else { } else {
// Otherwise, return a set of the items // Otherwise, return a set of the items
for(let item of this.items){ for(let item of this.items){
let id = item.getId().toString(); // TODO - This is REALLY slow for some reason when we check for unique keys
// If the item hasn't been found yet and it contains the point
if(!uniqueMap.has(id) && item.getBoundary().overlaps(boundary)){ // let id = item.getId().toString();
// Add it to our found points // // If the item hasn't been found yet and it contains the point
uniqueMap.add(id, item); // 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); results.push(item);
uniqueMap[item.getId()] = true;
}
} }
} }
} }

243
src/Debug/Stats.ts Normal file
View File

@ -0,0 +1,243 @@
import Color from "../Utils/Color";
export default class Stats extends Object {
/** The fps of the game. */
private static prevfps: Array<number>;
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<number>;
private static SGClearTimes: Array<number>;
private static avgSGClearTime: number;
private static prevFillTimes: Array<number>;
private static SGFillTimes: Array<number>;
private static avgSGFillTime: number;
private static prevUpdateTimes: Array<number>;
private static SGUpdateTimes: Array<number>;
private static avgSGUpdateTime: number;
private static prevQueryTimes: Array<number>;
private static SGQueryTimes: Array<number>;
private static avgSGQueryTime: number;
static initStats(): void {
let canvas = <HTMLCanvasElement>document.getElementById("stats-canvas");
canvas.width = this.CANVAS_WIDTH;
canvas.height = this.CANVAS_HEIGHT;
this.ctx = canvas.getContext("2d");
this.statsDiv = <HTMLDivElement>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 = <HTMLSelectElement>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<number>, 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;
}
}

View File

@ -7,6 +7,7 @@ import ResourceManager from "../ResourceManager/ResourceManager";
import Viewport from "../SceneGraph/Viewport"; import Viewport from "../SceneGraph/Viewport";
import SceneManager from "../Scene/SceneManager"; import SceneManager from "../Scene/SceneManager";
import AudioManager from "../Sound/AudioManager"; import AudioManager from "../Sound/AudioManager";
import Stats from "../Debug/Stats";
export default class GameLoop { export default class GameLoop {
/** The max allowed update fps.*/ /** The max allowed update fps.*/
@ -112,6 +113,8 @@ export default class GameLoop {
this.resourceManager = ResourceManager.getInstance(); this.resourceManager = ResourceManager.getInstance();
this.sceneManager = new SceneManager(this.viewport, this); this.sceneManager = new SceneManager(this.viewport, this);
this.audioManager = AudioManager.getInstance(); this.audioManager = AudioManager.getInstance();
Stats.initStats();
} }
private initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D { private initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D {
@ -153,6 +156,7 @@ export default class GameLoop {
this.framesSinceLastFpsUpdate = 0; this.framesSinceLastFpsUpdate = 0;
Debug.log("fps", "FPS: " + this.fps.toFixed(1)); Debug.log("fps", "FPS: " + this.fps.toFixed(1));
Stats.updateFPS(this.fps);
} }
/** /**
@ -216,6 +220,7 @@ export default class GameLoop {
this.numUpdateSteps++; this.numUpdateSteps++;
if(this.numUpdateSteps > 100){ if(this.numUpdateSteps > 100){
this.panic = true; this.panic = true;
break;
} }
} }
@ -272,6 +277,7 @@ export default class GameLoop {
this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT);
this.sceneManager.render(this.ctx); this.sceneManager.render(this.ctx);
Debug.render(this.ctx); Debug.render(this.ctx);
Stats.render();
} }
} }

View File

@ -5,6 +5,7 @@ import Scene from "../Scene/Scene";
import Stack from "../DataTypes/Stack"; import Stack from "../DataTypes/Stack";
import Layer from "../Scene/Layer" import Layer from "../Scene/Layer"
import AABB from "../DataTypes/AABB"; import AABB from "../DataTypes/AABB";
import Stats from "../Debug/Stats";
export default class SceneGraphArray extends SceneGraph{ export default class SceneGraphArray extends SceneGraph{
private nodeList: Array<CanvasNode>; private nodeList: Array<CanvasNode>;
@ -45,23 +46,29 @@ export default class SceneGraphArray extends SceneGraph{
} }
getNodesInRegion(boundary: AABB): Array<CanvasNode> { getNodesInRegion(boundary: AABB): Array<CanvasNode> {
let t0 = performance.now();
let results = []; let results = [];
for(let node of this.nodeList){ for(let node of this.nodeList){
if(boundary.overlapArea(node.getBoundary())){ if(boundary.overlaps(node.getBoundary())){
results.push(node); results.push(node);
} }
} }
let t1 = performance.now();
Stats.log("sgquery", (t1-t0));
return results; return results;
} }
update(deltaT: number): void { update(deltaT: number): void {
let t0 = performance.now();
for(let node of this.nodeList){ for(let node of this.nodeList){
if(!node.getLayer().isPaused()){ if(!node.getLayer().isPaused()){
node.update(deltaT); node.update(deltaT);
} }
} }
let t1 = performance.now();
Stats.log("sgupdate", (t1-t0));
} }
render(ctx: CanvasRenderingContext2D): void {} render(ctx: CanvasRenderingContext2D): void {}

View File

@ -5,6 +5,7 @@ import Scene from "../Scene/Scene";
import RegionQuadTree from "../DataTypes/RegionQuadTree"; import RegionQuadTree from "../DataTypes/RegionQuadTree";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import AABB from "../DataTypes/AABB"; import AABB from "../DataTypes/AABB";
import Stats from "../Debug/Stats";
export default class SceneGraphQuadTree extends SceneGraph { export default class SceneGraphQuadTree extends SceneGraph {
private qt: RegionQuadTree<CanvasNode>; private qt: RegionQuadTree<CanvasNode>;
@ -34,23 +35,35 @@ export default class SceneGraphQuadTree extends SceneGraph {
} }
getNodesInRegion(boundary: AABB): Array<CanvasNode> { getNodesInRegion(boundary: AABB): Array<CanvasNode> {
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 { update(deltaT: number): void {
let t0 = performance.now();
this.qt.clear(); this.qt.clear();
let t1 = performance.now();
Stats.log("sgclear", (t1-t0));
t0 = performance.now();
for(let node of this.nodes){ for(let node of this.nodes){
this.qt.insert(node); this.qt.insert(node);
} }
t1 = performance.now();
Stats.log("sgfill", (t1-t0));
t0 = performance.now();
this.nodes.forEach((node: CanvasNode) => node.update(deltaT)); this.nodes.forEach((node: CanvasNode) => node.update(deltaT));
// TODO: forEach is buggy, some nodes are update multiple times t1 = performance.now();
// this.qt.forEach((node: CanvasNode) => {
// if(!node.getLayer().isPaused()){ Stats.log("sgupdate", (t1-t0));
// node.update(deltaT);
// }
// });
} }
render(ctx: CanvasRenderingContext2D): void { render(ctx: CanvasRenderingContext2D): void {

View File

@ -2,10 +2,18 @@
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Hello World!</title> <title>Game</title>
</head> </head>
<body> <body>
<div style="display: flex; flex-direction: row;">
<canvas id="game-canvas"></canvas> <canvas id="game-canvas"></canvas>
<div>
<canvas id="stats-canvas"></canvas>
<select name="Display" id="chart-option">
</select>
<div id="stats-display"></div>
</div>
</div>
<script src="bundle.js"></script> <script src="bundle.js"></script>
</body> </body>
</html> </html>