19 - Audio
The only thing our game is missing now is some sound! Let's play the boop sound that replay-starter comes with whenever the bird hits a pipe.
The device
parameter has an audio
method, which takes a file name and returns a sound object. We can then call play()
on it to play a sound.
Lastly we need to ensure the audio file is being loaded in our Game
Sprite.
- JavaScript
- TypeScript
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 score: 0,
15 };
16 },
17
18 loop({ props, state, getInputs, device }) {
19 if (props.paused) {
20 return state;
21 }
22
23 const inputs = getInputs();
24
25 let { birdGravity, birdY, pipes, score } = state;
26
27 birdGravity += 0.8;
28 birdY -= birdGravity;
29
30 if (inputs.pointer.justPressed || inputs.keysJustPressed[" "]) {
31 birdGravity = -12;
32 }
33
34 const lastPipe = pipes[pipes.length - 1];
35 if (lastPipe.x < 140) {
36 pipes = [...pipes, newPipe(device)]
37 // Remove the pipes off screen on left
38 .filter(
39 (pipe) =>
40 pipe.x > -(device.size.width + device.size.widthMargin + pipeWidth)
41 );
42 }
43
44 if (didHitPipe(birdY, device.size, pipes)) {
45 device.audio("boop.wav").play();
46 props.gameOver(state.score);
47 }
48
49 // Move pipes to the left
50 pipes = pipes.map((pipe) => {
51 let passed = pipe.passed;
52 if (!passed && pipe.x < birdX - birdWidth / 2 - pipeWidth / 2) {
53 // Mark pipe as having passed bird's x position
54 passed = true;
55 score++;
56 }
57 return { ...pipe, passed, x: pipe.x - speedX };
58 });
59
60 return {
61 birdGravity,
62 birdY,
63 pipes,
64 score,
65 };
66 },
67
68 render({ state, device }) {
69 const { size } = device;
70 return [
71 t.rectangle({
72 color: "#add8e6",
73 width: size.width + size.widthMargin * 2,
74 height: size.height + size.heightMargin * 2,
75 }),
76 Bird({
77 id: "bird",
78 x: birdX,
79 y: state.birdY,
80 rotation: Math.max(-30, state.birdGravity * 3 - 30),
81 }),
82 ...state.pipes.map((pipe, index) =>
83 Pipe({
84 id: `pipe-${index}`,
85 pipe,
86 x: pipe.x,
87 })
88 ),
89 t.text({
90 text: `Score: ${state.score}`,
91 color: "white",
92 x: -device.size.width / 2 + 10,
93 y: device.size.height / 2 + device.size.heightMargin - 80,
94 font: {
95 align: "left",
96 },
97 }),
98 ];
99 },
100});
101
102function newPipe(device) {
103 const height = device.size.height + device.size.heightMargin * 2;
104 const randomY = (height - pipeGap * 2) * (device.random() - 0.5);
105
106 return {
107 x: device.size.width + device.size.widthMargin + 50,
108 gapY: randomY,
109 passed: false,
110 };
111}
112
113function didHitPipe(birdY, size, pipes) {
114 if (
115 birdY - birdHeight / 2 < -(size.height / 2 + size.heightMargin) ||
116 birdY + birdHeight / 2 > size.height / 2 + size.heightMargin
117 ) {
118 // hit bottom or top
119 return true;
120 }
121 for (const pipe of pipes) {
122 if (
123 pipe.x > birdX + birdWidth / 2 + pipeWidth / 2 ||
124 pipe.x < birdX - birdWidth / 2 - pipeWidth / 2
125 ) {
126 // bird isn't at pipe
127 continue;
128 }
129 const {
130 yUpperTop,
131 yUpperBottom,
132 yLowerTop,
133 yLowerBottom,
134 } = getPipeYPositions(size, pipe.gapY);
135 const topPipeRect = {
136 x: pipe.x,
137 y: (yUpperTop + yUpperBottom) / 2,
138 width: pipeWidth,
139 height: yUpperTop - yUpperBottom,
140 };
141 const bottomPipeRect = {
142 x: pipe.x,
143 y: (yLowerTop + yLowerBottom) / 2,
144 width: pipeWidth,
145 height: yLowerTop - yLowerBottom,
146 };
147 // Check a few points at edges of bird
148 const birdPoints = [
149 { x: birdX + birdWidth / 2, y: birdY + birdHeight / 2 },
150 { x: birdX + birdWidth / 2, y: birdY - birdHeight / 2 },
151 { x: birdX, y: birdY + birdHeight / 2 },
152 { x: birdX, y: birdY - birdHeight / 2 },
153 { x: birdX - birdWidth / 2, y: birdY + birdHeight / 2 },
154 { x: birdX - birdWidth / 2, y: birdY - birdHeight / 2 },
155 ];
156 if (
157 birdPoints.some(
158 (point) =>
159 pointInRect(point, topPipeRect) || pointInRect(point, bottomPipeRect)
160 )
161 ) {
162 // Bird hit a pipe!
163 return true;
164 }
165 }
166 return false;
167}
168
169function pointInRect(point, rect) {
170 return (
171 point.x > rect.x - rect.width / 2 &&
172 point.x < rect.x + rect.width / 2 &&
173 point.y > rect.y - rect.height / 2 &&
174 point.y < rect.y + rect.height / 2
175 );
176}
177
1import { makeSprite, t, Device, DeviceSize } from "@replay/core";
2import { WebInputs } from "@replay/web";
3import { iOSInputs } from "@replay/swift";
4import { Bird, birdWidth, birdHeight } from "./bird";
5import { Pipe, PipeT, pipeWidth, pipeGap, getPipeYPositions } from "./pipe";
6
7const speedX = 2;
8const birdX = 0;
9
10type LevelProps = {
11 paused: boolean;
12 gameOver: (score: number) => void;
13};
14
15type LevelState = {
16 birdY: number;
17 birdGravity: number;
18 pipes: PipeT[];
19 score: number;
20};
21
22export const Level = makeSprite<LevelProps, LevelState, WebInputs | iOSInputs>({
23 init({ device, props }) {
24 return {
25 birdY: 10,
26 birdGravity: -12,
27 pipes: props.paused ? [] : [newPipe(device)],
28 score: 0,
29 };
30 },
31
32 loop({ props, state, getInputs, device }) {
33 if (props.paused) {
34 return state;
35 }
36
37 const inputs = getInputs();
38
39 let { birdGravity, birdY, pipes, score } = state;
40
41 birdGravity += 0.8;
42 birdY -= birdGravity;
43
44 if (inputs.pointer.justPressed || inputs.keysJustPressed[" "]) {
45 birdGravity = -12;
46 }
47
48 const lastPipe = pipes[pipes.length - 1];
49 if (lastPipe.x < 140) {
50 pipes = [...pipes, newPipe(device)]
51 // Remove the pipes off screen on left
52 .filter(
53 (pipe) =>
54 pipe.x > -(device.size.width + device.size.widthMargin + pipeWidth)
55 );
56 }
57
58 if (didHitPipe(birdY, device.size, pipes)) {
59 device.audio("boop.wav").play();
60 props.gameOver(state.score);
61 }
62
63 // Move pipes to the left
64 pipes = pipes.map((pipe) => {
65 let passed = pipe.passed;
66 if (!passed && pipe.x < birdX - birdWidth / 2 - pipeWidth / 2) {
67 // Mark pipe as having passed bird's x position
68 passed = true;
69 score++;
70 }
71 return { ...pipe, passed, x: pipe.x - speedX };
72 });
73
74 return {
75 birdGravity,
76 birdY,
77 pipes,
78 score,
79 };
80 },
81
82 render({ state, device }) {
83 const { size } = device;
84 return [
85 t.rectangle({
86 color: "#add8e6",
87 width: size.width + size.widthMargin * 2,
88 height: size.height + size.heightMargin * 2,
89 }),
90 Bird({
91 id: "bird",
92 x: birdX,
93 y: state.birdY,
94 rotation: Math.max(-30, state.birdGravity * 3 - 30),
95 }),
96 ...state.pipes.map((pipe, index) =>
97 Pipe({
98 id: `pipe-${index}`,
99 pipe,
100 x: pipe.x,
101 })
102 ),
103 t.text({
104 text: `Score: ${state.score}`,
105 color: "white",
106 x: -device.size.width / 2 + 10,
107 y: device.size.height / 2 + device.size.heightMargin - 80,
108 font: {
109 align: "left",
110 },
111 }),
112 ];
113 },
114});
115
116function newPipe(device: Device): PipeT {
117 const height = device.size.height + device.size.heightMargin * 2;
118 const randomY = (height - pipeGap * 2) * (device.random() - 0.5);
119
120 return {
121 x: device.size.width + device.size.widthMargin + 50,
122 gapY: randomY,
123 passed: false,
124 };
125}
126
127function didHitPipe(birdY: number, size: DeviceSize, pipes: PipeT[]) {
128 if (
129 birdY - birdHeight / 2 < -(size.height / 2 + size.heightMargin) ||
130 birdY + birdHeight / 2 > size.height / 2 + size.heightMargin
131 ) {
132 // hit bottom or top
133 return true;
134 }
135 for (const pipe of pipes) {
136 if (
137 pipe.x > birdX + birdWidth / 2 + pipeWidth / 2 ||
138 pipe.x < birdX - birdWidth / 2 - pipeWidth / 2
139 ) {
140 // bird isn't at pipe
141 continue;
142 }
143 const {
144 yUpperTop,
145 yUpperBottom,
146 yLowerTop,
147 yLowerBottom,
148 } = getPipeYPositions(size, pipe.gapY);
149 const topPipeRect = {
150 x: pipe.x,
151 y: (yUpperTop + yUpperBottom) / 2,
152 width: pipeWidth,
153 height: yUpperTop - yUpperBottom,
154 };
155 const bottomPipeRect = {
156 x: pipe.x,
157 y: (yLowerTop + yLowerBottom) / 2,
158 width: pipeWidth,
159 height: yLowerTop - yLowerBottom,
160 };
161 // Check a few points at edges of bird
162 const birdPoints = [
163 { x: birdX + birdWidth / 2, y: birdY + birdHeight / 2 },
164 { x: birdX + birdWidth / 2, y: birdY - birdHeight / 2 },
165 { x: birdX, y: birdY + birdHeight / 2 },
166 { x: birdX, y: birdY - birdHeight / 2 },
167 { x: birdX - birdWidth / 2, y: birdY + birdHeight / 2 },
168 { x: birdX - birdWidth / 2, y: birdY - birdHeight / 2 },
169 ];
170 if (
171 birdPoints.some(
172 (point) =>
173 pointInRect(point, topPipeRect) || pointInRect(point, bottomPipeRect)
174 )
175 ) {
176 // Bird hit a pipe!
177 return true;
178 }
179 }
180 return false;
181}
182
183function pointInRect(
184 point: { x: number; y: number },
185 rect: { x: number; y: number; height: number; width: number }
186) {
187 return (
188 point.x > rect.x - rect.width / 2 &&
189 point.x < rect.x + rect.width / 2 &&
190 point.y > rect.y - rect.height / 2 &&
191 point.y < rect.y + rect.height / 2
192 );
193}
194
Preview