• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 19716547001

26 Nov 2025 08:27PM UTC coverage: 88.647% (+0.08%) from 88.57%
19716547001

push

github

web-flow
feat!: debug draw improvements (#3585)

<!--
Hi, and thanks for contributing to Excalibur!
Before you go any further, please read our contributing guide: https://github.com/excaliburjs/Excalibur/blob/main/.github/CONTRIBUTING.md
especially the "Submitting Changes" section:
https://github.com/excaliburjs/Excalibur/blob/main/.github/CONTRIBUTING.md#submitting-changes
---
A quick summary checklist is included below for convenience:
-->

===:clipboard: PR Checklist :clipboard:===

- [ ] :pushpin: issue exists in github for these changes
- [x] :microscope: existing tests still pass
- [x] :see_no_evil: code conforms to the [style guide](https://github.com/excaliburjs/Excalibur/blob/main/STYLEGUIDE.md)
- [x] :triangular_ruler: new tests written and passing / old tests updated with new scenario(s)
- [x] :page_facing_up: changelog entry added (or not needed)

==================

Discussed in our core contributor group, we are wanting to switch the "design language" of debug draw to help users more clearly identify problems.

<img width="1107" height="851" alt="image" src="https://github.com/user-attachments/assets/76c5fd59-2ede-4847-8138-ecd7cca5e210" />


1. "Bounds" type drawings will now be dashed boxes
2. Debug Graphics for line, point, point are no longer zoom coupled, so they'll always render at X pixels on screen regardless of zoom to make it easier to zoom in/out and debug things.
3. Breaking change! in bounds debug drawing to allow specifying more props
4. Fixed dupe graphics bounds drawing in DebugDrawSystem
5. Improved performance of debug drawing!


TODOS:
* [x] Debug Specific Circle Renderer
* [x] Uncouple Debug Text from Camera zoom
* [x] Add configuration option for dashed

5288 of 7219 branches covered (73.25%)

280 of 306 new or added lines in 18 files covered. (91.5%)

3 existing lines in 1 file now uncovered.

14656 of 16533 relevant lines covered (88.65%)

24557.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.27
/src/engine/Debug/DebugSystem.ts
1
import type { Engine } from '../Engine';
2
import type { Scene } from '../Scene';
3
import type { Camera } from '../Camera';
4
import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent';
5
import { ColliderComponent } from '../Collision/ColliderComponent';
6
import type { Entity, Query, World } from '../EntityComponentSystem';
7
import { SystemPriority, TransformComponent } from '../EntityComponentSystem';
8
import { System, SystemType } from '../EntityComponentSystem/System';
9
import type { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext';
10
import { vec, Vector } from '../Math/vector';
11
import { toDegrees } from '../Math/util';
12
import { BodyComponent } from '../Collision/BodyComponent';
13
import { CollisionSystem } from '../Collision/CollisionSystem';
14
import { CompositeCollider } from '../Collision/Colliders/CompositeCollider';
15
import { Particle } from '../Particles/Particles';
16
import { DebugGraphicsComponent } from '../Graphics/DebugGraphicsComponent';
17
import { CoordPlane } from '../Math/coord-plane';
18
import { Debug } from '../Graphics/Debug';
19

20
export class DebugSystem extends System {
248✔
21
  static priority = SystemPriority.Lowest;
22

23
  public readonly systemType = SystemType.Draw;
1,304✔
24
  private _graphicsContext: ExcaliburGraphicsContext;
25
  private _collisionSystem: CollisionSystem;
26
  private _camera: Camera;
27
  private _engine: Engine;
28
  query: Query<typeof TransformComponent>;
29

30
  constructor(public world: World) {
1,304✔
31
    super();
1,304✔
32
    this.query = this.world.query([TransformComponent]);
1,304✔
33
  }
34

35
  public initialize(world: World, scene: Scene): void {
36
    this._graphicsContext = scene.engine.graphicsContext;
690✔
37
    this._camera = scene.camera;
690✔
38
    this._engine = scene.engine;
690✔
39
    this._collisionSystem = world.systemManager.get(CollisionSystem);
690✔
40
  }
41

42
  update(): void {
43
    if (!this._engine.isDebug) {
944✔
44
      return;
934✔
45
    }
46

47
    const filterSettings = this._engine.debug.filter;
10✔
48

49
    let id: number;
50
    let name: string;
51
    const entitySettings = this._engine.debug.entity;
10✔
52

53
    let tx: TransformComponent;
54
    const txSettings = this._engine.debug.transform;
10✔
55

56
    let motion: MotionComponent;
57
    const motionSettings = this._engine.debug.motion;
10✔
58

59
    let colliderComp: ColliderComponent;
60
    const colliderSettings = this._engine.debug.collider;
10✔
61

62
    const physicsSettings = this._engine.debug.physics;
10✔
63

64
    let debugDraw: DebugGraphicsComponent;
65

66
    let body: BodyComponent;
67
    const bodySettings = this._engine.debug.body;
10✔
68

69
    const cameraSettings = this._engine.debug.camera;
10✔
70
    for (let i = 0; i < this.query.entities.length; i++) {
10✔
71
      const entity = this.query.entities[i];
235✔
72
      if (entity.hasTag('offscreen')) {
235!
73
        // skip offscreen entities
74
        continue;
×
75
      }
76
      if (entity instanceof Particle) {
235!
77
        // Particles crush the renderer :(
78
        continue;
×
79
      }
80
      if (filterSettings.useFilter) {
235!
81
        const allIds = filterSettings.ids.length === 0;
×
82
        const idMatch = allIds || filterSettings.ids.includes(entity.id);
×
83
        if (!idMatch) {
×
84
          continue;
×
85
        }
86
        const allNames = filterSettings.nameQuery === '';
×
87
        const nameMatch = allNames || entity.name.includes(filterSettings.nameQuery);
×
88
        if (!nameMatch) {
×
89
          continue;
×
90
        }
91
      }
92

93
      let cursor = Vector.Zero;
235✔
94
      const lineHeight = vec(0, 16);
235✔
95
      id = entity.id;
235✔
96
      name = entity.name;
235✔
97
      tx = entity.get(TransformComponent);
235✔
98

99
      // This optionally sets our camera based on the entity coord plan (world vs. screen)
100
      this._pushCameraTransform(tx);
235✔
101

102
      this._graphicsContext.save();
235✔
103
      if (tx.coordPlane === CoordPlane.Screen) {
235!
104
        this._graphicsContext.translate(this._engine.screen.contentArea.left, this._engine.screen.contentArea.top);
×
105
      }
106

107
      this._applyTransform(entity);
235✔
108
      if (tx) {
235!
109
        if (txSettings.showAll || txSettings.showPosition) {
235✔
110
          this._graphicsContext.debug.drawPoint(Vector.Zero, { size: 4, color: txSettings.positionColor });
227✔
111
        }
112
        if (txSettings.showAll || txSettings.showPositionLabel) {
235✔
113
          this._graphicsContext.debug.drawText(`pos${tx.pos.toString(2)}`, cursor);
1✔
114
          cursor = cursor.add(lineHeight);
1✔
115
        }
116
        if (txSettings.showAll || txSettings.showZIndex) {
235✔
117
          this._graphicsContext.debug.drawText(`z(${tx.z.toFixed(1)})`, cursor);
1✔
118
          cursor = cursor.add(lineHeight);
1✔
119
        }
120

121
        if (entitySettings.showAll || entitySettings.showId) {
235✔
122
          this._graphicsContext.debug.drawText(`id(${id}) ${entity.parent ? 'child of id(' + entity.parent?.id + ')' : ''}`, cursor);
2!
123
          cursor = cursor.add(lineHeight);
2✔
124
        }
125

126
        if (entitySettings.showAll || entitySettings.showName) {
235✔
127
          this._graphicsContext.debug.drawText(`name(${name})`, cursor);
1✔
128
          cursor = cursor.add(lineHeight);
1✔
129
        }
130

131
        if (txSettings.showAll || txSettings.showRotation) {
235✔
132
          this._graphicsContext.drawLine(
1✔
133
            Vector.Zero,
134
            Vector.fromAngle(tx.rotation).scale(50).add(Vector.Zero),
135
            txSettings.rotationColor,
136
            2
137
          );
138
          this._graphicsContext.debug.drawText(`rot deg(${toDegrees(tx.rotation).toFixed(2)})`, cursor);
1✔
139
          cursor = cursor.add(lineHeight);
1✔
140
        }
141

142
        if (txSettings.showAll || txSettings.showScale) {
235✔
143
          this._graphicsContext.drawLine(Vector.Zero, tx.scale.add(Vector.Zero), txSettings.scaleColor, 2);
1✔
144
        }
145
      }
146

147
      debugDraw = entity.get(DebugGraphicsComponent);
235✔
148
      if (debugDraw) {
235✔
149
        if (!debugDraw.useTransform) {
3✔
150
          this._graphicsContext.restore();
2✔
151
        }
152
        debugDraw.draw(this._graphicsContext, this._engine.debug);
3✔
153
        if (!debugDraw.useTransform) {
3✔
154
          this._graphicsContext.save();
2✔
155
          this._applyTransform(entity);
2✔
156
        }
157
      }
158

159
      body = entity.get(BodyComponent);
235✔
160
      if (body) {
235✔
161
        if (bodySettings.showAll || bodySettings.showCollisionGroup) {
8✔
162
          this._graphicsContext.debug.drawText(`collision group(${body.group.name})`, cursor);
1✔
163
          cursor = cursor.add(lineHeight);
1✔
164
        }
165

166
        if (bodySettings.showAll || bodySettings.showCollisionType) {
8✔
167
          this._graphicsContext.debug.drawText(`collision type(${body.collisionType})`, cursor);
1✔
168
          cursor = cursor.add(lineHeight);
1✔
169
        }
170

171
        if (bodySettings.showAll || bodySettings.showMass) {
8✔
172
          this._graphicsContext.debug.drawText(`mass(${body.mass})`, cursor);
1✔
173
          cursor = cursor.add(lineHeight);
1✔
174
        }
175

176
        if (bodySettings.showAll || bodySettings.showMotion) {
8✔
177
          this._graphicsContext.debug.drawText(`motion(${body.sleepMotion})`, cursor);
1✔
178
          cursor = cursor.add(lineHeight);
1✔
179
        }
180

181
        if (bodySettings.showAll || bodySettings.showSleeping) {
8✔
182
          this._graphicsContext.debug.drawText(`sleeping(${body.canSleep ? body.isSleeping : 'cant sleep'})`, cursor);
1!
183
          cursor = cursor.add(lineHeight);
1✔
184
        }
185
      }
186

187
      this._graphicsContext.restore();
235✔
188

189
      // World space
190
      this._graphicsContext.save();
235✔
191
      if (tx.coordPlane === CoordPlane.Screen) {
235!
192
        this._graphicsContext.translate(this._engine.screen.contentArea.left, this._engine.screen.contentArea.top);
×
193
      }
194
      motion = entity.get(MotionComponent);
235✔
195
      if (motion) {
235✔
196
        if (motionSettings.showAll || motionSettings.showVelocity) {
8✔
197
          this._graphicsContext.debug.drawText(`vel${motion.vel.toString(2)}`, cursor.add(tx.globalPos));
1✔
198
          this._graphicsContext.drawLine(tx.globalPos, tx.globalPos.add(motion.vel), motionSettings.velocityColor, 2);
1✔
199
          cursor = cursor.add(lineHeight);
1✔
200
        }
201

202
        if (motionSettings.showAll || motionSettings.showAcceleration) {
8✔
203
          this._graphicsContext.drawLine(tx.globalPos, tx.globalPos.add(motion.acc), motionSettings.accelerationColor, 2);
1✔
204
        }
205
      }
206

207
      // Colliders live in world space already so after the restore()
208
      colliderComp = entity.get(ColliderComponent);
235✔
209
      if (colliderComp) {
235✔
210
        const collider = colliderComp.get();
9✔
211
        if ((colliderSettings.showAll || colliderSettings.showGeometry) && collider) {
9✔
212
          collider.debug(this._graphicsContext, colliderSettings.geometryColor, {
7✔
213
            lineWidth: colliderSettings.geometryLineWidth,
214
            pointSize: colliderSettings.geometryPointSize
215
          });
216
        }
217
        if (colliderSettings.showAll || colliderSettings.showBounds) {
9✔
218
          if (collider instanceof CompositeCollider) {
3✔
219
            const colliders = collider.getColliders();
1✔
220
            for (const collider of colliders) {
1✔
221
              const bounds = collider.bounds;
3✔
222
              const pos = vec(bounds.left, bounds.top);
3✔
223
              this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, {
3✔
224
                color: colliderSettings.boundsColor,
225
                dashed: true
226
              });
227
              if (colliderSettings.showAll || colliderSettings.showOwner) {
3!
228
                this._graphicsContext.debug.drawText(`owner id(${collider.owner.id})`, pos);
3✔
229
              }
230
            }
231
            colliderComp.bounds.debug(this._graphicsContext, {
1✔
232
              color: colliderSettings.boundsColor,
233
              dashed: true
234
            });
235
          } else if (collider) {
2✔
236
            const bounds = colliderComp.bounds;
1✔
237
            const pos = vec(bounds.left, bounds.top);
1✔
238
            this._graphicsContext.debug.drawRect(pos.x, pos.y, bounds.width, bounds.height, {
1✔
239
              color: colliderSettings.boundsColor,
240
              dashed: true
241
            });
242
            if (colliderSettings.showAll || colliderSettings.showOwner) {
1!
243
              this._graphicsContext.debug.drawText(`owner id(${colliderComp.owner.id})`, pos);
1✔
244
            }
245
          }
246
        }
247
      }
248

249
      this._graphicsContext.restore();
235✔
250
      this._popCameraTransform(tx);
235✔
251
    }
252

253
    this._graphicsContext.save();
10✔
254
    this._camera.draw(this._graphicsContext);
10✔
255
    if (physicsSettings.showAll || physicsSettings.showBroadphaseSpacePartitionDebug) {
10!
256
      this._collisionSystem.debug(this._graphicsContext);
×
257
    }
258
    if (physicsSettings.showAll || physicsSettings.showCollisionContacts || physicsSettings.showCollisionNormals) {
10!
259
      for (const [_, contact] of this._engine.debug.stats.currFrame.physics.contacts) {
10✔
260
        if (physicsSettings.showAll || physicsSettings.showCollisionContacts) {
×
261
          for (const point of contact.points) {
×
262
            this._graphicsContext.debug.drawPoint(point, {
×
263
              size: physicsSettings.contactSize,
264
              color: physicsSettings.collisionContactColor
265
            });
266
          }
267
        }
268

269
        if (physicsSettings.showAll || physicsSettings.showCollisionNormals) {
×
270
          for (const point of contact.points) {
×
271
            this._graphicsContext.debug.drawLine(point, contact.normal.scale(30).add(point), {
×
272
              color: physicsSettings.collisionNormalColor
273
            });
274
          }
275
        }
276
      }
277
    }
278
    this._graphicsContext.restore();
10✔
279

280
    if (cameraSettings) {
10!
281
      this._graphicsContext.save();
10✔
282
      this._camera.draw(this._graphicsContext);
10✔
283
      if (cameraSettings.showAll || cameraSettings.showFocus) {
10!
NEW
284
        this._graphicsContext.debug.drawCircle(this._camera.pos, 4, cameraSettings.focusColor);
×
285
      }
286
      if (cameraSettings.showAll || cameraSettings.showZoom) {
10!
287
        this._graphicsContext.debug.drawText(`zoom(${this._camera.zoom})`, this._camera.pos);
×
288
      }
289
      this._graphicsContext.restore();
10✔
290
    }
291
  }
292

293
  postupdate(engine: Scene<unknown>, elapsed: number): void {
294
    if (this._engine.isDebug) {
935✔
295
      this._graphicsContext.save();
1✔
296
      if (this._camera) {
1!
297
        this._camera.draw(this._graphicsContext);
1✔
298
      }
299
      Debug.flush(this._graphicsContext);
1✔
300
      this._graphicsContext.restore();
1✔
301
    }
302
  }
303

304
  /**
305
   * This applies the current entity transform to the graphics context
306
   * @param entity
307
   */
308
  private _applyTransform(entity: Entity): void {
309
    const ancestors = entity.getAncestors();
237✔
310
    for (const ancestor of ancestors) {
237✔
311
      const transform = ancestor?.get(TransformComponent);
462!
312
      if (transform) {
462!
313
        this._graphicsContext.translate(transform.pos.x, transform.pos.y);
462✔
314
        this._graphicsContext.scale(transform.scale.x, transform.scale.y);
462✔
315
        this._graphicsContext.rotate(transform.rotation);
462✔
316
      }
317
    }
318
  }
319

320
  /**
321
   * Applies the current camera transform if in world coordinates
322
   * @param transform
323
   */
324
  private _pushCameraTransform(transform: TransformComponent) {
325
    // Establish camera offset per entity
326
    if (transform.coordPlane === CoordPlane.World) {
235!
327
      this._graphicsContext.save();
235✔
328
      if (this._camera) {
235!
329
        this._camera.draw(this._graphicsContext);
235✔
330
      }
331
    }
332
  }
333

334
  /**
335
   * Resets the current camera transform if in world coordinates
336
   * @param transform
337
   */
338
  private _popCameraTransform(transform: TransformComponent) {
339
    if (transform.coordPlane === CoordPlane.World) {
235!
340
      // Apply camera world offset
341
      this._graphicsContext.restore();
235✔
342
    }
343
  }
344
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc