Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444import type { Engine } from '../engine';import type { Scene } from '../scene';import type { Camera } from '../camera';import { MotionComponent } from '../entity-component-system/components/motion-component';import { ColliderComponent } from '../collision/collider-component';import type { Entity, Query, World } from '../entity-component-system';import { SystemPriority, TransformComponent } from '../entity-component-system';import { System, SystemType } from '../entity-component-system/system';import type { ExcaliburGraphicsContext } from '../graphics/context/excalibur-graphics-context';import { vec, Vector } from '../math/vector';import { toDegrees } from '../math/util';import { BodyComponent } from '../collision/body-component';import { CollisionSystem } from '../collision/collision-system';import { CompositeCollider } from '../collision/colliders/composite-collider';import { Particle } from '../particles/particles';import { DebugGraphicsComponent } from '../graphics/debug-graphics-component';import { CoordPlane } from '../math/coord-plane';import { DisplayMode } from '../screen';import { Debug } from '../graphics/debug';import { Color } from '../color';import type { BoundingBox } from '../collision/bounding-box';
export class DebugSystem extends System { static override priority = SystemPriority.Lowest;
public readonly systemType = SystemType.Draw; private _graphicsContext!: ExcaliburGraphicsContext; private _collisionSystem!: CollisionSystem; private _camera!: Camera; private _engine!: Engine; query: Query<typeof TransformComponent>;
constructor(public world: World) { super(); this.query = this.world.query([TransformComponent]); }
public override initialize(world: World, scene: Scene): void { this._graphicsContext = scene.engine.graphicsContext; this._camera = scene.camera; this._engine = scene.engine; this._collisionSystem = world.systemManager.get(CollisionSystem)!; }
update(): void { if (!this._engine.isDebug) { return; }
const filterSettings = this._engine.debug.filter;
let id: number; let name: string; const entitySettings = this._engine.debug.entity;
let tx: TransformComponent; const txSettings = this._engine.debug.transform;
let motion: MotionComponent; const motionSettings = this._engine.debug.motion;
let colliderComp: ColliderComponent; const colliderSettings = this._engine.debug.collider;
const physicsSettings = this._engine.debug.physics;
let debugDraw: DebugGraphicsComponent;
let body: BodyComponent; const bodySettings = this._engine.debug.body;
const cameraSettings = this._engine.debug.camera; for (let i = 0; i < this.query.entities.length; i++) { const entity = this.query.entities[i]; if (entity.hasTag('offscreen')) { // skip offscreen entities continue; } if (entity instanceof Particle) { // Particles crush the renderer :( continue; } if (filterSettings.useFilter) { const allIds = filterSettings.ids.length === 0; const idMatch = allIds || filterSettings.ids.includes(entity.id); if (!idMatch) { continue; } const allNames = filterSettings.nameQuery === ''; const nameMatch = allNames || entity.name.includes(filterSettings.nameQuery); if (!nameMatch) { continue; } }
let cursor = Vector.Zero; const lineHeight = vec(0, 16); id = entity.id; name = entity.name; tx = entity.get(TransformComponent);
// This optionally sets our camera based on the entity coord plan (world vs. screen) this._pushCameraTransform(tx);
this._graphicsContext.save(); if (tx.coordPlane === CoordPlane.Screen) { this._graphicsContext.translate(this._engine.screen.contentAreaOffset.x, this._engine.screen.contentAreaOffset.y); }
this._applyTransform(entity); if (tx) { if (txSettings.showAll || txSettings.showPosition) { this._graphicsContext.debug.drawPoint(Vector.Zero, { size: 4, color: txSettings.positionColor }); } if (txSettings.showAll || txSettings.showPositionLabel) { this._graphicsContext.debug.drawText(`pos${tx.pos.toString(2)}`, cursor); cursor = cursor.add(lineHeight); } if (txSettings.showAll || txSettings.showZIndex) { this._graphicsContext.debug.drawText(`z(${tx.z.toFixed(1)})`, cursor); cursor = cursor.add(lineHeight); }
if (entitySettings.showAll || entitySettings.showId) { this._graphicsContext.debug.drawText(`id(${id}) ${entity.parent ? 'child of id(' + entity.parent?.id + ')' : ''}`, cursor); cursor = cursor.add(lineHeight); }
if (entitySettings.showAll || entitySettings.showName) { this._graphicsContext.debug.drawText(`name(${name})`, cursor); cursor = cursor.add(lineHeight); }
if (txSettings.showAll || txSettings.showRotation) { this._graphicsContext.debug.drawLine(Vector.Zero, Vector.fromAngle(tx.rotation).scale(20).add(Vector.Zero), { color: txSettings.rotationColor, lineWidth: 2 }); this._graphicsContext.debug.drawText(`rot deg(${toDegrees(tx.rotation).toFixed(2)})`, cursor); cursor = cursor.add(lineHeight); }
if (txSettings.showAll || txSettings.showScale) { this._graphicsContext.debug.drawLine(Vector.Zero, tx.scale, { color: txSettings.scaleColor, lineWidth: 2 }); } }
debugDraw = entity.get(DebugGraphicsComponent)!; if (debugDraw) { if (!debugDraw.useTransform) { this._graphicsContext.restore(); } debugDraw.draw(this._graphicsContext, this._engine.debug); if (!debugDraw.useTransform) { this._graphicsContext.save(); this._applyTransform(entity); } }
body = entity.get(BodyComponent)!; if (body) { if (bodySettings.showAll || bodySettings.showCollisionGroup) { this._graphicsContext.debug.drawText(`collision group name(${body.group.name}))`, cursor); cursor = cursor.add(lineHeight); this._graphicsContext.debug.drawText(` mask(0x${(body.group.mask >>> 0).toString(16)})`, cursor); cursor = cursor.add(lineHeight); this._graphicsContext.debug.drawText(` category(0x${(body.group.category >>> 0).toString(16)})`, cursor); cursor = cursor.add(lineHeight); }
if (bodySettings.showAll || bodySettings.showCollisionType) { this._graphicsContext.debug.drawText(`collision type(${body.collisionType})`, cursor); cursor = cursor.add(lineHeight); }
if (bodySettings.showAll || bodySettings.showMass) { this._graphicsContext.debug.drawText(`mass(${body.mass})`, cursor); cursor = cursor.add(lineHeight); }
if (bodySettings.showAll || bodySettings.showMotion) { this._graphicsContext.debug.drawText(`motion(${body.sleepMotion.toFixed(3)})`, cursor); cursor = cursor.add(lineHeight); }
if (bodySettings.showAll || bodySettings.showSleeping) { this._graphicsContext.debug.drawText(`sleeping(${body.canSleep ? body.isSleeping : 'cant sleep'})`, cursor); cursor = cursor.add(lineHeight); } }
this._graphicsContext.restore();
// World space this._graphicsContext.save(); if (tx.coordPlane === CoordPlane.Screen) { this._graphicsContext.translate(this._engine.screen.contentAreaOffset.x, this._engine.screen.contentAreaOffset.y); } motion = entity.get(MotionComponent)!; if (motion) { if (motionSettings.showAll || motionSettings.showVelocity) { this._graphicsContext.debug.drawText(`vel${motion.vel.toString(2)}`, cursor.add(tx.globalPos)); this._graphicsContext.debug.drawLine(tx.globalPos, tx.globalPos.add(motion.vel), { color: motionSettings.velocityColor, lineWidth: 2 }); cursor = cursor.add(lineHeight); }
if (motionSettings.showAll || motionSettings.showAcceleration) { this._graphicsContext.debug.drawLine(tx.globalPos, tx.globalPos.add(motion.acc), { color: motionSettings.accelerationColor, lineWidth: 2 }); } }
// Colliders live in world space already so after the restore() colliderComp = entity.get(ColliderComponent)!; if (colliderComp) { const collider = colliderComp.get(); if ((colliderSettings.showAll || colliderSettings.showGeometry) && collider) { collider.debug(this._graphicsContext, colliderSettings.geometryColor, { lineWidth: colliderSettings.geometryLineWidth, pointSize: colliderSettings.geometryPointSize }); } if (colliderSettings.showAll || colliderSettings.showBounds) { if (collider instanceof CompositeCollider) { const colliders = collider.getColliders(); for (const collider of colliders) { const bounds = collider.bounds; const pos = vec(bounds.left, bounds.top); this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, { color: colliderSettings.boundsColor, dashed: true }); if (colliderSettings.showAll || colliderSettings.showOwner) { this._graphicsContext.debug.drawText(`owner id(${collider.owner!.id})`, pos); } } colliderComp.bounds.debug(this._graphicsContext, { color: colliderSettings.boundsColor, dashed: true }); } else if (collider) { const bounds = colliderComp.bounds; const pos = vec(bounds.left, bounds.top); this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, { color: colliderSettings.boundsColor, dashed: true }); if (colliderSettings.showAll || colliderSettings.showOwner) { this._graphicsContext.debug.drawText(`owner id(${colliderComp.owner!.id})`, pos); } } } }
this._graphicsContext.restore(); this._popCameraTransform(tx); }
this._graphicsContext.save(); this._camera.draw(this._graphicsContext); if (physicsSettings.showAll || physicsSettings.showBroadphaseSpacePartitionDebug) { this._collisionSystem.debug(this._graphicsContext); } if (physicsSettings.showAll || physicsSettings.showCollisionContacts || physicsSettings.showCollisionNormals) { for (const [_, contact] of this._engine.debug.stats.currFrame.physics.contacts) { if (physicsSettings.showAll || physicsSettings.showCollisionContacts) { for (const point of contact.points) { this._graphicsContext.debug.drawPoint(point, { size: physicsSettings.contactSize, color: physicsSettings.collisionContactColor }); } }
if (physicsSettings.showAll || physicsSettings.showCollisionNormals) { for (const point of contact.points) { this._graphicsContext.debug.drawLine(point, contact.normal.scale(30).add(point), { color: physicsSettings.collisionNormalColor }); } } } } this._graphicsContext.restore();
if (cameraSettings) { this._graphicsContext.save(); this._camera.draw(this._graphicsContext); if (cameraSettings.showAll || cameraSettings.showFocus) { this._graphicsContext.debug.drawCircle(this._camera.pos, 4, cameraSettings.focusColor); } if (cameraSettings.showAll || cameraSettings.showZoom) { this._graphicsContext.debug.drawText(`zoom(${this._camera.zoom})`, this._camera.pos); } this._graphicsContext.restore(); }
const screenSettings = this._engine.debug.screen; if (screenSettings && screenSettings.showAll) { const screen = this._engine.screen; this._graphicsContext.save(); this._graphicsContext.resetTransform(); this._graphicsContext.translate(screen.contentAreaOffset.x, screen.contentAreaOffset.y); this._graphicsContext.z = Debug.config.settings.z.solid;
const isZoomed = screen.displayMode === DisplayMode.FitScreenAndZoom || screen.displayMode === DisplayMode.FitContainerAndZoom; const visible = isZoomed ? screen.contentArea : screen.unsafeArea; const offsetX = screen.contentAreaOffset.x; const offsetY = screen.contentAreaOffset.y;
const drawClampedBox = (bb: BoundingBox, color: Color) => { const left = Math.max(visible.left, bb.left) + 1; const top = Math.max(visible.top, bb.top) + 1; const right = Math.min(visible.right, bb.right) - 1; const bottom = Math.min(visible.bottom, bb.bottom) - 1; const w = right - left; const h = bottom - top; if (w > 0 && h > 0) { this._graphicsContext.debug.drawRect(left, top, w, h, { color, dashed: true, lineWidth: 2 }); } };
if (screenSettings.showUnsafeArea) { drawClampedBox(screen.unsafeArea, screenSettings.unsafeAreaColor); } if (screenSettings.showContentArea) { drawClampedBox(screen.contentArea, screenSettings.contentAreaColor); }
this._graphicsContext.translate(-screen.contentAreaOffset.x, -screen.contentAreaOffset.y);
if (screenSettings.showLegend) { const textScale = 0.6; const swatchPad = 4; const swatchWidth = 10; const swatchHeight = 8; const lineHeight = 13; const labelPadX = swatchWidth + swatchPad; const legendPad = 4; const margin = 8;
const contentLabel = `contentArea ${Math.round(screen.contentArea.width)}x${Math.round(screen.contentArea.height)} (Screen)`; const unsafeLabel = `unsafeArea ${Math.round(screen.unsafeArea.width)}x${Math.round(screen.unsafeArea.height)} (Screen)`;
const contentSize = this._graphicsContext.debug.measureText(contentLabel, textScale); const unsafeSize = this._graphicsContext.debug.measureText(unsafeLabel, textScale); const maxLabelWidth = Math.max(contentSize.width, unsafeSize.width);
const legendWidth = labelPadX + Math.ceil(maxLabelWidth) + legendPad * 2; const legendHeight = lineHeight * 2 + legendPad * 2;
const legendX = visible.right + offsetX - legendWidth - margin; const legendY = visible.bottom + offsetY - legendHeight - margin;
this._graphicsContext.drawRectangle( vec(legendX, legendY), legendWidth, legendHeight, Color.fromRGB(0, 0, 0, 0.6), screenSettings.legendColor, 1 );
const row1 = vec(legendX + legendPad, legendY + legendPad); this._graphicsContext.drawRectangle(row1, swatchWidth, swatchHeight, screenSettings.contentAreaColor); this._graphicsContext.debug.drawText(contentLabel, row1.add(vec(labelPadX, 0)), { foreground: screenSettings.contentAreaColor, scale: textScale });
const row2 = row1.add(vec(0, lineHeight)); this._graphicsContext.drawRectangle(row2, swatchWidth, swatchHeight, screenSettings.unsafeAreaColor); this._graphicsContext.debug.drawText(unsafeLabel, row2.add(vec(labelPadX, 0)), { foreground: screenSettings.unsafeAreaColor, scale: textScale }); }
this._graphicsContext.restore(); } }
override postupdate(engine: Scene<unknown>, elapsed: number): void { if (this._engine.isDebug) { this._graphicsContext.save(); if (this._camera) { this._camera.draw(this._graphicsContext); } Debug.flush(this._graphicsContext); this._graphicsContext.restore(); } }
/** * This applies the current entity transform to the graphics context * @param entity */ private _applyTransform(entity: Entity): void { const ancestors = entity.getAncestors(); for (const ancestor of ancestors) { const transform = ancestor?.get(TransformComponent); if (transform) { this._graphicsContext.translate(transform.pos.x, transform.pos.y); this._graphicsContext.scale(transform.scale.x, transform.scale.y); this._graphicsContext.rotate(transform.rotation); } } }
/** * Applies the current camera transform if in world coordinates * @param transform */ private _pushCameraTransform(transform: TransformComponent) { // Establish camera offset per entity if (transform.coordPlane === CoordPlane.World) { this._graphicsContext.save(); if (this._camera) { this._camera.draw(this._graphicsContext); } } }
/** * Resets the current camera transform if in world coordinates * @param transform */ private _popCameraTransform(transform: TransformComponent) { if (transform.coordPlane === CoordPlane.World) { // Apply camera world offset this._graphicsContext.restore(); } }}