15 - Collision Detection

Next we need to add some collision detection to see if the bird hit a pipe or fell off the bottom of the screen.

We'll add a callback prop gameOver in the Level Sprite which can return us to the main menu. Then in the loop method we call it when the didHitPipe function returns true.

In Replay one way of performing collision detection is by checking if a point is within a rectangle, like the pointInRect function defined at the bottom of the level file does.

In didHitPipe we essentially choose a few points around the shape of the bird, and see if any of those are inside one of the pipe rectangles - if so, it's a hit! For simplicity, this function doesn't account for the bird's rotation; as an exercise, feel free to try to add that for a more accurate hit detection.

We update our top-level Game Sprite to change its view state to "menu" in the gameOver callback prop.

You'll also see an attempt count added in the state. Even though we don't display this, it's a neat hack to force the Level to reset its state whenever we start again by changing its id prop. Replay uses the id to track state between Sprites (which is why it has to be unique locally), but we can also use that to reset a Sprite's state.

BackNext
1import { makeSprite, t } from "@replay/core";
2import { Bird, birdWidth, birdHeight } from "./bird";
3import { Pipe, pipeWidth, pipeGap, getPipeYPositions } from "./pipe";
4
5const speedX = 2;
6const birdX = 0;
7
8export const Level = makeSprite({
9 init({ device, props }) {
10 return {
11 birdY: 10,
12 birdGravity: -12,
13 pipes: props.paused ? [] : [newPipe(device)],
14 };
15 },
16
17 loop({ props, state, getInputs, device }) {
18 if (props.paused) {
19 return state;
20 }
21
22 const inputs = getInputs();
23
24 let { birdGravity, birdY, pipes } = state;
25
26 birdGravity += 0.8;
27 birdY -= birdGravity;
28
29 if (inputs.pointer.justPressed || inputs.keysJustPressed[" "]) {
30 birdGravity = -12;
31 }
32
33 const lastPipe = pipes[pipes.length - 1];
34 if (lastPipe.x < 140) {
35 pipes = [...pipes, newPipe(device)]
36 // Remove the pipes off screen on left
37 .filter(
38 (pipe) =>
39 pipe.x > -(device.size.width + device.size.widthMargin + pipeWidth)
40 );
41 }
42
43 if (didHitPipe(birdY, device.size, pipes)) {
44 props.gameOver();
45 }
46
47 // Move pipes to the left
48 pipes = pipes.map((pipe) => {
49 let passed = pipe.passed;
50 if (!passed && pipe.x < birdX - birdWidth / 2 - pipeWidth / 2) {
51 // Mark pipe as having passed bird's x position
52 passed = true;
53 }
54 return { ...pipe, passed, x: pipe.x - speedX };
55 });
56
57 return {
58 birdGravity,
59 birdY,
60 pipes,
61 };
62 },
63
64 render({ state, device }) {
65 const { size } = device;
66 return [
67 t.rectangle({
68 color: "#add8e6",
69 width: size.width + size.widthMargin * 2,
70 height: size.height + size.heightMargin * 2,
71 }),
72 Bird({
73 id: "bird",
74 x: birdX,
75 y: state.birdY,
76 rotation: Math.max(-30, state.birdGravity * 3 - 30),
77 }),
78 ...state.pipes.map((pipe, index) =>
79 Pipe({
80 id: `pipe-${index}`,
81 pipe,
82 x: pipe.x,
83 })
84 ),
85 ];
86 },
87});
88
89function newPipe(device) {
90 const height = device.size.height + device.size.heightMargin * 2;
91 const randomY = (height - pipeGap * 2) * (device.random() - 0.5);
92
93 return {
94 x: device.size.width + device.size.widthMargin + 50,
95 gapY: randomY,
96 passed: false,
97 };
98}
99
100function didHitPipe(birdY, size, pipes) {
101 if (
102 birdY - birdHeight / 2 < -(size.height / 2 + size.heightMargin) ||
103 birdY + birdHeight / 2 > size.height / 2 + size.heightMargin
104 ) {
105 // hit bottom or top
106 return true;
107 }
108 for (const pipe of pipes) {
109 if (
110 pipe.x > birdX + birdWidth / 2 + pipeWidth / 2 ||
111 pipe.x < birdX - birdWidth / 2 - pipeWidth / 2
112 ) {
113 // bird isn't at pipe
114 continue;
115 }
116 const {
117 yUpperTop,
118 yUpperBottom,
119 yLowerTop,
120 yLowerBottom,
121 } = getPipeYPositions(size, pipe.gapY);
122 const topPipeRect = {
123 x: pipe.x,
124 y: (yUpperTop + yUpperBottom) / 2,
125 width: pipeWidth,
126 height: yUpperTop - yUpperBottom,
127 };
128 const bottomPipeRect = {
129 x: pipe.x,
130 y: (yLowerTop + yLowerBottom) / 2,
131 width: pipeWidth,
132 height: yLowerTop - yLowerBottom,
133 };
134 // Check a few points at edges of bird
135 const birdPoints = [
136 { x: birdX + birdWidth / 2, y: birdY + birdHeight / 2 },
137 { x: birdX + birdWidth / 2, y: birdY - birdHeight / 2 },
138 { x: birdX, y: birdY + birdHeight / 2 },
139 { x: birdX, y: birdY - birdHeight / 2 },
140 { x: birdX - birdWidth / 2, y: birdY + birdHeight / 2 },
141 { x: birdX - birdWidth / 2, y: birdY - birdHeight / 2 },
142 ];
143 if (
144 birdPoints.some(
145 (point) =>
146 pointInRect(point, topPipeRect) || pointInRect(point, bottomPipeRect)
147 )
148 ) {
149 // Bird hit a pipe!
150 return true;
151 }
152 }
153 return false;
154}
155
156function pointInRect(point, rect) {
157 return (
158 point.x > rect.x - rect.width / 2 &&
159 point.x < rect.x + rect.width / 2 &&
160 point.y > rect.y - rect.height / 2 &&
161 point.y < rect.y + rect.height / 2
162 );
163}
164
Preview