Sidequest

Spawning Pipes

In this section we're going to:

  • Spawn a pair up pipes after we generate a new spawn position
  • Scroll the pipes across the screen
  • Remove pipes that are no long visible on screen

Pipe Manager Config

Let's store a new config value for our pipe scroll speed:

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
19
pipeManager: {
+
pipeScrollSpeed: 240,
21
pipeSpawnBuffer: 50,
22
spawnIntervalInSeconds: 1.5,
23
},
24
};
25
26
export type Config = typeof config;

Spawning Pipes

Update the pipe manager class to spawn a new pair of pipes after a certain amount of time has passed:

+
import { SpriteData } from "#/components/sprite-data";
2
import { Vector2d } from "#/components/vector2d";
+
import { Pipe } from "#/entities/pipe";
4
import { Game } from "#/game";
5
import { randomBetween } from "#/lib/random";
6
import { SpriteMap } from "#/sprite-map";
7
8
type PipeManagerOptions = {
9
game: Game;
10
spriteMap: SpriteMap;
+
spriteSheet: HTMLImageElement;
12
};
13
14
export class PipeManager {
15
currentSpawnPosition: Vector2d;
16
game: Game;
+
pipes: Pipe[] = [];
+
pipeScrollSpeed: number;
19
spawnTimerThreshold: number;
20
spawnTimerAccumulator = 0;
21
spawnPositionYMinimum: number;
22
spawnPositionYMaximum: number;
23
spriteMap: SpriteMap;
+
spriteSheet: HTMLImageElement;
25
26
constructor(options: PipeManagerOptions) {
27
this.game = options.game;
+
this.pipeScrollSpeed = options.game.config.pipeManager.pipeScrollSpeed;
+
this.spriteMap = options.spriteMap;
30
this.spriteSheet = options.spriteSheet;
31
this.spawnTimerThreshold =
32
this.game.config.pipeManager.spawnIntervalInSeconds;
33
34
// ...
35
}
36
37
public update(delta: number) {
38
this.spawnTimerAccumulator += delta;
39
40
if (this.spawnTimerAccumulator >= this.spawnTimerThreshold) {
41
this.spawnTimerAccumulator = 0;
42
43
this.currentSpawnPosition.y = Math.floor(
44
randomBetween(this.spawnPositionYMinimum, this.spawnPositionYMaximum)
45
);
46
+
this.pipes.push(
+
new Pipe({
+
game: this.game,
+
spriteSheet: this.spriteSheet,
+
position: new Vector2d(
+
this.currentSpawnPosition.x,
+
this.currentSpawnPosition.y -
+
this.game.config.pipeManager.pipeSpawnBuffer
+
),
+
rimSpriteData: new SpriteData(
+
this.spriteMap.pipes.green.bottom.sourceX,
+
this.spriteMap.pipes.green.bottom.sourceY,
+
this.spriteMap.pipes.green.bottom.width,
+
this.spriteMap.pipes.green.bottom.height
+
),
+
sliceSpriteData: new SpriteData(
+
this.spriteMap.pipes.green.slice.sourceX,
+
this.spriteMap.pipes.green.slice.sourceY,
+
this.spriteMap.pipes.green.slice.width,
+
this.spriteMap.pipes.green.slice.height
+
),
+
type: "top",
+
}),
+
+
new Pipe({
+
game: this.game,
+
spriteSheet: this.spriteSheet,
+
position: new Vector2d(
+
this.currentSpawnPosition.x,
+
this.currentSpawnPosition.y +
+
this.game.config.pipeManager.pipeSpawnBuffer
+
),
+
rimSpriteData: new SpriteData(
+
this.spriteMap.pipes.green.top.sourceX,
+
this.spriteMap.pipes.green.top.sourceY,
+
this.spriteMap.pipes.green.top.width,
+
this.spriteMap.pipes.green.top.height
+
),
+
sliceSpriteData: new SpriteData(
+
this.spriteMap.pipes.green.slice.sourceX,
+
this.spriteMap.pipes.green.slice.sourceY,
+
this.spriteMap.pipes.green.slice.width,
+
this.spriteMap.pipes.green.slice.height
+
),
+
type: "bottom",
+
})
+
);
94
}
95
}
96
}

We're storing the spriteSheet to pass along to the pipe instances. We're tracking our pipes in a pipes array, but spawing the pipes is pretty much exactly the same as before.

Update the pipe manager instance in main.ts to take the spriteSheet:

1
const pipeManager = new PipeManager({
2
game,
3
spriteMap,
+
spriteSheet,
5
});

Scrolling Pipes

Nothing is happening on the canvas yet. We need to loop over every pipe now and change their position, then draw them.

1
// ...
2
3
export class PipeManager {
4
// ...
5
6
public update(delta: number) {
7
this.spawnTimerAccumulator += delta;
8
9
if (this.spawnTimerAccumulator >= this.spawnTimerThreshold) {
10
// ...
11
}
12
+
for (const pipe of this.pipes) {
+
pipe.position.x -= this.pipeScrollSpeed * delta;
+
}
16
}
17
18
public draw(context: CanvasRenderingContext2D) {
+
for (const pipe of this.pipes) {
+
pipe.draw(context);
+
}
22
23
// ... debug draw calls
24
}
25
}

Perfect! Check out the canvas and you'll see our pipes scrolling across the screen.

There is one little snag though.

Cleaning Up Pipes

Our pipes array is constantly growing. This isn't necessarily a big deal in a game like Flappy Bird that has relatively short play sessions, but we'll be good citizens and clean up after ourselves nonetheless. If you like, you could add a console.log(this.pipes.length) call after we push new pipes in the update() method to see how many pipes are currently in the array. Just remember to remove it later.

Let's modify the update() method to tidy up when we can:

1
export class PipeManager {
2
// ...
3
4
public update(delta: number) {
5
// ...
6
+
let pipesToRemove = 0;
8
9
for (const pipe of this.pipes) {
10
pipe.position.x -= this.pipeScrollSpeed * delta;
11
+
if (pipe.position.x < -pipe.sliceSpriteData.width) {
+
++pipesToRemove;
+
}
15
}
16
+
while (pipesToRemove > 0) {
+
this.pipes.shift();
+
+
--pipesToRemove;
+
}
22
}
23
24
// ...
25
}

We're checking to see if the x position of any pipe is ever less than the width of the pipe (it's scrolled off screen). If it is, add it to the count, and remove it in the end. We're using shift() to remove and modify the array in place. shift() also removes elements from the front of the array, which is important because those will be the pipes already off screen.