Sidequest

Visualize Spawn Points

Let's start by writing some code to visually display where the current pipe spawn point is, and within what range it can be generated.

We'll end up with something that looks like this:

Pipe spawn point visual aids
Pipe spawn point visual aids

The red line is the current spawn position - just like the visual aid we made in the previous chapter, and the purple lines represent the range a spawn position can be generated within.

Pipe Manager Config

Let's store some pipe manager config data:

1
export const config = {
2
/**
3
* In pixels
4
*/
5
gameWidth: 352,
6
7
/**
8
* In pixels
9
*/
10
gameHeight: 576,
11
12
pipe: {
13
collider: {
14
offsetX: 4,
15
width: 56,
16
},
17
},
18
+
pipeManager: {
+
pipeSpawnBuffer: 50,
+
},
22
};
23
24
export type Config = typeof config;

We'll use this next to configure some of the pipe manager's properties.

Pipe Manager Entity

Create the new pipe manager entity:

1
import { Vector2d } from "#/components/vector2d";
2
import { Game } from "#/game";
3
import { SpriteMap } from "#/sprite-map";
4
5
type PipeManagerOptions = {
6
game: Game;
7
spriteMap: SpriteMap;
8
};
9
10
export class PipeManager {
11
currentSpawnPosition: Vector2d;
12
game: Game;
13
spawnPositionYMinimum: number;
14
spawnPositionYMaximum: number;
15
spriteMap: SpriteMap;
16
17
constructor(options: PipeManagerOptions) {
18
this.game = options.game;
19
this.spriteMap = options.spriteMap;
20
21
// Account for ground height to make the distribution of pipes more even
22
const halfGameHeight =
23
(this.game.config.gameHeight - this.spriteMap.ground.height) / 2;
24
25
// Double the buffer for a wider distribution
26
const doubleBuffer = this.game.config.pipeManager.pipeSpawnBuffer * 2;
27
28
this.spawnPositionYMinimum = halfGameHeight - doubleBuffer;
29
this.spawnPositionYMaximum = halfGameHeight + doubleBuffer;
30
31
this.currentSpawnPosition = new Vector2d(
32
this.game.config.gameWidth,
33
halfGameHeight
34
);
35
}
36
37
public draw(context: CanvasRenderingContext2D) {
38
if (this.game.debug) {
39
context.fillStyle = "purple";
40
context.fillRect(
41
0,
42
this.spawnPositionYMinimum,
43
this.game.config.gameWidth,
44
1
45
);
46
context.fillRect(
47
0,
48
this.spawnPositionYMaximum,
49
this.game.config.gameWidth,
50
1
51
);
52
53
context.fillStyle = "red";
54
context.fillRect(
55
0,
56
this.currentSpawnPosition.y,
57
this.game.config.gameWidth,
58
1
59
);
60
}
61
}
62
}

This should look pretty standard by now.

Let's review a few standouts:

  • halfGameHeight is used to get the vertical center of the game screen minus the ground height. We don't want to include the space the ground sprite takes up when we calculate the spawn positions.
  • doubleBuffer is used to make the distribution of pipes double the size of our pipe buffer.
  • spawnPositionYMinimum is the minimum Y position for the spawn position. The top purple line.
  • spawnPositionYMaximum is the maximum Y position for the spawn position. The bottom purple line.
  • currentSpawnPosition is the current spawn position for the last pair of generated pipes.
  • currentSpawnPosition.x is set to the width of the game screen. This ensures that the pipes will spawn offscreen.
  • currentSpawnPosition.y is set to the center of the game screen minus the ground height.
  • draw() is rendering all three of our reference lines.

Go ahead and add an instance of the pipe manager to the game and call the draw method:

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 { PipeManager } from "#/entities/pipe-manager";
12
import { Game, GameState } from "#/game";
13
import { loadImage } from "#/lib/asset-loader";
14
import { circleRectangleIntersects } from "#/lib/collision";
15
import { spriteMap } from "#/sprite-map";
16
17
// ...
18
+
const pipeManager = new PipeManager({
+
game,
+
spriteMap,
+
});
23
24
let last = performance.now();
25
26
const frame = (hrt: DOMHighResTimeStamp) => {
27
// ...
28
29
bird.draw(context);
+
pipeManager.draw(context);
31
ground.draw(context);
32
33
last = hrt;
34
35
requestAnimationFrame(frame);
36
};

We'll draw the pipes between the bird and the ground. This way the pipes are always behind the ground but in front of the bird.

You can either set game.debug = true for now, or press the d key as needed to toggle debug mode. You should see the game looking like image above in the canvas.