Sidequest

Debug Collider Rendering

Tracking Debug Mode

Let's toggle drawing the colliders when we press the d key.

We're going to create a new file: game.ts, to track whether or not we're in debug mode.

1
export class Game {
2
debug = false;
3
}

Very minimal for now, but we'll add more by the end of this section.

Now create a new instance of the Game class, and add a keypress listener beneath our canvas click listener.

1
import spriteSheetUrl from "#/assets/image/spritesheet.png";
2
import { BoxCollider } from "#/components/box-collider";
3
import { CircleCollider } from "#/components/circle-collider";
4
import { SpriteAnimation } from "#/components/sprite-animation";
5
import { SpriteAnimationDetails } from "#/components/sprite-animation-details";
6
import { SpriteData } from "#/components/sprite-data";
7
import { Vector2d } from "#/components/vector2d";
8
import { config } from "#/config";
9
import { Bird } from "#/entities/bird";
10
import { Ground } from "#/entities/ground";
+
import { Game } from "#/game";
12
import { loadImage } from "#/lib/asset-loader";
13
import { circleRectangleIntersects } from "#/lib/collision";
14
import { spriteMap } from "#/sprite-map";
15
16
const spriteSheet = await loadImage(spriteSheetUrl);
+
const game = new Game();
18
19
// ...
20
21
// Add event listener to trigger bird flap
22
canvas.addEventListener("click", () => {
23
bird.flap();
24
});
25
+
window.addEventListener("keypress", (event) => {
+
if (event.code === "KeyD") {
+
game.debug = !game.debug;
+
}
+
});
31
32
// ...

Now that we're tracking this, we need to react to it in the Ground and Bird classes.

Debug Rendering in the Bird

Let's make a few changes to the Bird class to support debug rendering.

1
import { CircleCollider } from "#/components/circle-collider";
2
import { SpriteAnimation } from "#/components/sprite-animation";
3
import { SpriteData } from "#/components/sprite-data";
4
import { Vector2d } from "#/components/vector2d";
+
import { Game } from "#/game";
6
7
type BirdOptions = {
+
game: Game;
9
spriteSheet: HTMLImageElement;
10
spriteData: SpriteData;
11
position: Vector2d;
12
flapAnimation: SpriteAnimation;
13
circlCollider: CircleCollider;
14
};
15
16
// ...
17
18
export class Bird {
19
state = BirdState.Idle;
20
spriteSheet: HTMLImageElement;
21
spriteData: SpriteData;
22
position: Vector2d;
23
flapAnimation: SpriteAnimation;
24
velocity = new Vector2d();
25
circleCollider: CircleCollider;
+
game: Game;
27
28
// ...
29
30
constructor(options: BirdOptions) {
31
this.spriteSheet = options.spriteSheet;
32
this.spriteData = options.spriteData;
33
this.position = options.position;
34
this.flapAnimation = options.flapAnimation;
35
this.circleCollider = options.circlCollider;
+
this.game = options.game;
37
}
38
39
public draw(context: CanvasRenderingContext2D) {
40
context.translate(this.position.x, this.position.y);
41
const rotation = (this.rotation * Math.PI) / 180;
42
context.rotate(rotation);
43
44
const currentFrame = this.flapAnimation.getCurrentFrame();
45
46
context.drawImage(
47
this.spriteSheet,
48
currentFrame.sourceX,
49
currentFrame.sourceY,
50
currentFrame.width,
51
currentFrame.height,
52
-currentFrame.width / 2,
53
-currentFrame.height / 2,
54
currentFrame.width,
55
currentFrame.height
56
);
57
+
if (this.game.debug) {
+
context.fillStyle = "red";
+
context.globalAlpha = 0.5;
+
context.beginPath();
+
context.arc(0, 0, this.circleCollider.radius, 0, 2 * Math.PI);
+
context.fill();
+
context.globalAlpha = 1;
+
}
66
67
context.resetTransform();
68
}
69
}

Then update the instance in main.ts:

1
const bird = new Bird({
+
game,
3
spriteSheet: spriteSheet,
4
position: new Vector2d(config.gameWidth / 4, config.gameHeight / 2),
5
spriteData: new SpriteData(
6
spriteMap.bird.idle.sourceX,
7
spriteMap.bird.idle.sourceY,
8
spriteMap.bird.idle.width,
9
spriteMap.bird.idle.height
10
),
11
flapAnimation: new SpriteAnimation(
12
new SpriteAnimationDetails(
13
spriteMap.bird.animations.flap.sourceX,
14
spriteMap.bird.animations.flap.sourceY,
15
spriteMap.bird.animations.flap.width,
16
spriteMap.bird.animations.flap.height,
17
spriteMap.bird.animations.flap.frameWidth,
18
spriteMap.bird.animations.flap.frameHeight
19
),
20
0.3
21
),
22
circlCollider: new CircleCollider(0, 0, 12),
23
});

Once the window has reloaded, go ahead and press the d key to toggle the debug rendering. Looking pretty good 😎

Debug Rendering in the Ground

Let's make a few changes to the Ground class to support debug rendering.

1
import { BoxCollider } from "#/components/box-collider";
2
import { SpriteData } from "#/components/sprite-data";
3
import { Vector2d } from "#/components/vector2d";
+
import { Game } from "#/game";
5
6
type GroundOptions = {
7
boxCollider: BoxCollider;
+
game: Game;
9
position: Vector2d;
10
spriteData: SpriteData;
11
spriteSheet: HTMLImageElement;
12
13
/**
14
* This is in pixels **per frame**.
15
*/
16
scrollSpeed: number;
17
};
18
19
export class Ground {
20
boxCollider: BoxCollider;
+
game: Game;
22
position: Vector2d;
23
spriteData: SpriteData;
24
spriteSheet: HTMLImageElement;
25
scrollSpeed: number;
26
27
// Track the current scroll position separately from the position.
28
public scrollPositionX = 0;
29
30
constructor(options: GroundOptions) {
31
this.boxCollider = options.boxCollider;
+
this.game = options.game;
33
this.position = options.position;
34
this.spriteData = options.spriteData;
35
this.spriteSheet = options.spriteSheet;
36
this.scrollSpeed = options.scrollSpeed;
37
}
38
39
public draw(context: CanvasRenderingContext2D) {
40
// First track how far the image is offscreen.
41
const diff = Math.abs(this.scrollPositionX);
42
43
// Draw the visible portion of the image.
44
context.drawImage(
45
this.spriteSheet,
46
this.spriteData.sourceX + diff,
47
this.spriteData.sourceY,
48
this.spriteData.width - diff,
49
this.spriteData.height,
50
this.position.x,
51
this.position.y,
52
this.spriteData.width - diff,
53
this.spriteData.height
54
);
55
56
// Draw the remaining portion of the image (what is currently offscreen).
57
context.drawImage(
58
this.spriteSheet,
59
this.spriteData.sourceX,
60
this.spriteData.sourceY,
61
diff,
62
this.spriteData.height,
63
context.canvas.width - diff,
64
this.position.y,
65
diff,
66
this.spriteData.height
67
);
68
+
if (this.game.debug) {
+
context.fillStyle = "red";
+
context.globalAlpha = 0.5;
+
context.fillRect(
+
this.position.x,
+
this.position.y,
+
this.boxCollider.width,
+
this.boxCollider.height
+
);
+
+
context.globalAlpha = 1;
+
}
81
}
82
}

Then update the instance in main.ts:

1
const ground = new Ground({
2
boxCollider: new BoxCollider(
3
0,
4
0,
5
spriteMap.ground.width,
6
spriteMap.ground.height
7
),
+
game,
9
position: new Vector2d(0, config.gameHeight - spriteMap.ground.height),
10
spriteData: spriteMap.ground,
11
spriteSheet: spriteSheet,
12
scrollSpeed: 120,
13
});

On window reload, press the d key and you should also see the ground collider rendering. Sweet stuff!

Next, let's finally detect collision between the bird and the ground.