const Game = require("../core/Game");
const CollisionShape = require("../physics/CollisionShape");
const { createElement } = require("../other/GameFunctions");
/**
* Extra functions for debugging, such as showing all wireframes, hitboxes, and collisions.
*
* ## Events
* | Name | Description | Arguments |
* | ---- | ----------- | --------- |
* | beforeSave | Before the DebugRender's canvas context is saved | None |
* | beforeRender | Before all debug tools are rendered | None |
* | afterRender | After all debug tools are rendered | None |
* | afterRestore | After the DebugRender's canvas contex is restored | None |
*
*/
class DebugRender {
// - Debug rendering
canvas = null;
ctx = null;
/**
* What is rendered
* - **enabled.wireframes** - Shows wireframes of all physics bodies
* - **enabled.collisions** - Shows collision points and normals
* - **enabled.boundingBox** - Shows AABB bounding boxes for physics bodies
* - **enabled.centers** - Shows center of mass of all physics bodies
* - **enabled.broadphase** - Shows active non-static broadphase grids cells
* @type {object}
* @todo Add methods for setting these, possibly also in Game
* @example
* // All options:
* game.DebugRender.enabled = {
* wireframes: true,
* centers: true,
* collisions: true,
* broadphase: true,
* boundingBox: true,
* }
*/
enabled = {
wireframes: false,
centers: false,
collisions: false,
broadphase: false,
boundingBox: false,
}
/**
* Creates a debug rendering context for the game.
* @param {Game} Game - Game to render debug info for
*/
constructor(Game) {
this.Game = Game;
let baseCanvas = Game.Render.app.view;
let canvas = this.canvas = createElement("canvas", {
parent: baseCanvas.parentNode,
width: baseCanvas.width,
height: baseCanvas.height,
style: {
position: "absolute",
zIndex: 1,
top: baseCanvas.offsetTop + "px",
left: baseCanvas.offsetLeft + "px",
width: baseCanvas.style.width,
height: baseCanvas.style.height,
background: "transparent",
pointerEvents: "none",
margin: 0,
}
})
this.ctx = canvas.getContext("2d");
Game.Render.app.renderer.on("resize", () => {
let view = Game.Render.app.view;
canvas.width = view.width;
canvas.height = view.height;
canvas.style.width = view.style.width;
canvas.style.height = view.style.height;
canvas.style.top = baseCanvas.offsetTop + "px";
canvas.style.left = baseCanvas.offsetLeft + "px";
});
this.update = this.update.bind(this);
Game.Render.app.ticker.add(this.update);
}
update() {
let { ctx, canvas, enabled, Game } = this;
const { Render } = Game;
const { camera, pixelRatio } = Render;
let canvWidth = canvas.width;
let canvHeight = canvas.height;
const { position:cameraPosition } = camera;
const scale = camera.scale * pixelRatio;
let translation = new vec({ x: -cameraPosition.x * scale + canvWidth/2, y: -cameraPosition.y * scale + canvHeight/2 });
ctx.clearRect(0, 0, canvWidth, canvHeight);
this.trigger("beforeSave");
ctx.save();
ctx.translate(translation.x, translation.y);
ctx.scale(scale, scale);
this.trigger("beforeRender");
for (let debugType in enabled) {
if (enabled[debugType] && typeof this[debugType] === "function") {
this[debugType]();
}
}
this.trigger("afterRender");
ctx.restore();
this.trigger("afterRestore");
}
wireframes() {
const { Game, ctx } = this;
const { camera, pixelRatio } = Game.Render;
const scale = camera.scale * pixelRatio;
function renderVertices(vertices) {
ctx.moveTo(vertices[0].x, vertices[0].y);
for (let j = 0; j < vertices.length; j++) {
if (j > 0) {
let vertice = vertices[j];
ctx.lineTo(vertice.x, vertice.y);
}
}
ctx.closePath();
}
ctx.beginPath();
let allBodies = Game.World.rigidBodies;
for (let body of allBodies) {
for (let child of body.children) {
if (child instanceof CollisionShape) {
renderVertices(child.vertices);
}
}
}
ctx.lineWidth = 2 / scale;
ctx.strokeStyle = "#DF7157";
ctx.stroke();
}
collisions() {
const { ctx, Game } = this;
const { globalPoints, globalVectors } = Game.World;
if (globalPoints.length > 0) { // Render globalPoints
ctx.beginPath();
for (let i = 0; i < globalPoints.length; i++) {
let point = globalPoints[i];
ctx.moveTo(point.x, point.y);
ctx.arc(point.x, point.y, 2.5 / camera.scale, 0, Math.PI*2);
ctx.fillStyle = "#e8e8e8";
}
ctx.fill();
}
if (globalVectors.length > 0) { // Render globalVectors
ctx.beginPath();
for (let i = 0; i < globalVectors.length; i++) {
let point = globalVectors[i].position;
let vector = globalVectors[i].vector;
ctx.moveTo(point.x, point.y);
ctx.lineTo(point.x + vector.x * 10 / camera.scale, point.y + vector.y * 10 / camera.scale);
ctx.strokeStyle = "#DF7157";
ctx.lineWidth = 3 / camera.scale;
}
ctx.stroke();
}
}
centers() {
const { ctx, Game } = this;
const { camera } = Game.Render;
ctx.fillStyle = "#DF7157";
let allBodies = Game.World.rigidBodies;
ctx.beginPath();
for (let body of allBodies) {
ctx.moveTo(body.position.x, body.position.y);
ctx.arc(body.position.x, body.position.y, 2 / camera.scale, 0, Math.PI*2);
}
ctx.fill();
}
boundingBox() {
const { ctx, Game } = this;
const { World, Render } = Game;
const { camera } = Render;
let allBodies = World.rigidBodies;
let allConstraints = World.constraints;
ctx.strokeStyle = "#66666680";
ctx.lineWidth = 1 / camera.scale;
for (let body of allBodies) {
for (let child of body.children) {
if (child instanceof CollisionShape) {
let bounds = child.bounds;
let width = bounds.max.x - bounds.min.x;
let height = bounds.max.y - bounds.min.y;
ctx.beginPath();
ctx.strokeRect(bounds.min.x, bounds.min.y, width, height);
}
}
}
ctx.strokeStyle = "#66666630";
for (let constraint of allConstraints) {
let bounds = constraint.bounds;
let width = bounds.max.x - bounds.min.x;
let height = bounds.max.y - bounds.min.y;
ctx.beginPath();
ctx.strokeRect(bounds.min.x, bounds.min.y, width, height);
}
}
broadphase(tree = this.Game.World.dynamicGrid) {
const { ctx, Game } = this;
const { camera } = Game.Render;
let size = tree.gridSize;
ctx.lineWidth = 0.4 / camera.scale;
ctx.strokeStyle = "#D0A356";
ctx.fillStyle = "#947849";
Object.keys(tree.grid).forEach(n => {
let node = tree.grid[n];
let pos = tree.unpair(n).mult(size);
ctx.strokeRect(pos.x, pos.y, size, size);
ctx.globalAlpha = 0.003 * node.length;
ctx.fillRect(pos.x, pos.y, size, size);
ctx.globalAlpha = 1;
});
}
// Random render functions
arrow(position, direction, size = 6) {
let ctx = this.ctx;
let endPos = new vec(position.x + direction.x, position.y + direction.y);
let sideA = direction.rotate(Math.PI * 3/4).normalize2().mult(size);
let sideB = sideA.reflect(direction.normalize());
ctx.moveTo(position.x, position.y);
ctx.lineTo(endPos.x, endPos.y);
ctx.lineTo(endPos.x + sideA.x, endPos.y + sideA.y);
ctx.moveTo(endPos.x, endPos.y);
ctx.lineTo(endPos.x + sideB.x, endPos.y + sideB.y);
}
#events = {
beforeSave: [],
beforeRender: [],
afterRender: [],
afterRestore: [],
}
/**
* Bind a callback to an event
* @param {string} event - Name of the event
* @param {Function} callback - Callback run when event is fired
*/
on(event, callback) {
if (this.#events[event]) {
this.#events[event].push(callback);
}
else {
console.warn(event + " is not a valid event");
}
}
/**
* Unbinds a callback from an event
* @param {string} event - Name of the event
* @param {Function} callback - Function to unbind
*/
off(event, callback) {
let events = this.#events[event];
if (events.includes(callback)) {
events.splice(events.indexOf(callback), 1);
}
}
/**
* Triggers an event, firing all bound callbacks
* @param {string} event - Name of the event
* @param {...*} args - Arguments passed to callbacks
*/
trigger(event, ...args) {
// Trigger each event
if (this.#events[event]) {
this.#events[event].forEach(callback => {
callback(...args);
});
}
}
}
module.exports = DebugRender;