16 - Score
Let's add a score to count how many pipes the bird managed to fly past.
In the Level
Sprite we add a score
field to the state and increment it whenever we pass a pipe. Then we render it in the top-left corner with a t.text
Texture.
Notice the align
field of the font
prop on the text Texture: by default, Sprites have an anchor point in their center, which defines how they're positioned and rotate. Setting align: "left"
moves the left edge of the text to the Sprite's anchor point, providing a left text align for the score. The x
position of the Texture is now the left side of it, not the center.
- 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 props.gameOver();
46 }
47
48 // Move pipes to the left
49 pipes = pipes.map((pipe) => {
50 let passed = pipe.passed;
51 if (!passed && pipe.x < birdX - birdWidth / 2 - pipeWidth / 2) {
52 // Mark pipe as having passed bird's x position
53 passed = true;
54 score++;
55 }
56 return { ...pipe, passed, x: pipe.x - speedX };
57 });
58
59 return {
60 birdGravity,
61 birdY,
62 pipes,
63 score,
64 };
65 },
66
67 render({ state, device }) {
68 const { size } = device;
69 return [
70 t.rectangle({
71 color: "#add8e6",
72 width: size.width + size.widthMargin * 2,
73 height: size.height + size.heightMargin * 2,
74 }),
75 Bird({
76 id: "bird",
77 x: birdX,
78 y: state.birdY,
79 rotation: Math.max(-30, state.birdGravity * 3 - 30),
80 }),
81 ...state.pipes.map((pipe, index) =>
82 Pipe({
83 id: `pipe-${index}`,
84 pipe,
85 x: pipe.x,
86 })
87 ),
88 t.text({
89 text: `Score: ${state.score}`,
90 color: "white",
91 x: -device.size.width / 2 + 10,
92 y: device.size.height / 2 + device.size.heightMargin - 80,
93 font: {
94 align: "left",
95 },
96 }),
97 ];
98 },
99});
100
101function newPipe(device) {
102 const height = device.size.height + device.size.heightMargin * 2;
103 const randomY = (height - pipeGap * 2) * (device.random() - 0.5);
104
105 return {
106 x: device.size.width + device.size.widthMargin + 50,
107 gapY: randomY,
108 passed: false,
109 };
110}
111
112function didHitPipe(birdY, size, pipes) {
113 if (
114 birdY - birdHeight / 2 < -(size.height / 2 + size.heightMargin) ||
115 birdY + birdHeight / 2 > size.height / 2 + size.heightMargin
116 ) {
117 // hit bottom or top
118 return true;
119 }
120 for (const pipe of pipes) {
121 if (
122 pipe.x > birdX + birdWidth / 2 + pipeWidth / 2 ||
123 pipe.x < birdX - birdWidth / 2 - pipeWidth / 2
124 ) {
125 // bird isn't at pipe
126 continue;
127 }
128 const {
129 yUpperTop,
130 yUpperBottom,
131 yLowerTop,
132 yLowerBottom,
133 } = getPipeYPositions(size, pipe.gapY);
134 const topPipeRect = {
135 x: pipe.x,
136 y: (yUpperTop + yUpperBottom) / 2,
137 width: pipeWidth,
138 height: yUpperTop - yUpperBottom,
139 };
140 const bottomPipeRect = {
141 x: pipe.x,
142 y: (yLowerTop + yLowerBottom) / 2,
143 width: pipeWidth,
144 height: yLowerTop - yLowerBottom,
145 };
146 // Check a few points at edges of bird
147 const birdPoints = [
148 { x: birdX + birdWidth / 2, y: birdY + birdHeight / 2 },
149 { x: birdX + birdWidth / 2, y: birdY - birdHeight / 2 },
150 { x: birdX, y: birdY + birdHeight / 2 },
151 { x: birdX, y: birdY - birdHeight / 2 },
152 { x: birdX - birdWidth / 2, y: birdY + birdHeight / 2 },
153 { x: birdX - birdWidth / 2, y: birdY - birdHeight / 2 },
154 ];
155 if (
156 birdPoints.some(
157 (point) =>
158 pointInRect(point, topPipeRect) || pointInRect(point, bottomPipeRect)
159 )
160 ) {
161 // Bird hit a pipe!
162 return true;
163 }
164 }
165 return false;
166}
167
168function pointInRect(point, rect) {
169 return (
170 point.x > rect.x - rect.width / 2 &&
171 point.x < rect.x + rect.width / 2 &&
172 point.y > rect.y - rect.height / 2 &&
173 point.y < rect.y + rect.height / 2
174 );
175}
176
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: () => 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 props.gameOver();
60 }
61
62 // Move pipes to the left
63 pipes = pipes.map((pipe) => {
64 let passed = pipe.passed;
65 if (!passed && pipe.x < birdX - birdWidth / 2 - pipeWidth / 2) {
66 // Mark pipe as having passed bird's x position
67 passed = true;
68 score++;
69 }
70 return { ...pipe, passed, x: pipe.x - speedX };
71 });
72
73 return {
74 birdGravity,
75 birdY,
76 pipes,
77 score,
78 };
79 },
80
81 render({ state, device }) {
82 const { size } = device;
83 return [
84 t.rectangle({
85 color: "#add8e6",
86 width: size.width + size.widthMargin * 2,
87 height: size.height + size.heightMargin * 2,
88 }),
89 Bird({
90 id: "bird",
91 x: birdX,
92 y: state.birdY,
93 rotation: Math.max(-30, state.birdGravity * 3 - 30),
94 }),
95 ...state.pipes.map((pipe, index) =>
96 Pipe({
97 id: `pipe-${index}`,
98 pipe,
99 x: pipe.x,
100 })
101 ),
102 t.text({
103 text: `Score: ${state.score}`,
104 color: "white",
105 x: -device.size.width / 2 + 10,
106 y: device.size.height / 2 + device.size.heightMargin - 80,
107 font: {
108 align: "left",
109 },
110 }),
111 ];
112 },
113});
114
115function newPipe(device: Device): PipeT {
116 const height = device.size.height + device.size.heightMargin * 2;
117 const randomY = (height - pipeGap * 2) * (device.random() - 0.5);
118
119 return {
120 x: device.size.width + device.size.widthMargin + 50,
121 gapY: randomY,
122 passed: false,
123 };
124}
125
126function didHitPipe(birdY: number, size: DeviceSize, pipes: PipeT[]) {
127 if (
128 birdY - birdHeight / 2 < -(size.height / 2 + size.heightMargin) ||
129 birdY + birdHeight / 2 > size.height / 2 + size.heightMargin
130 ) {
131 // hit bottom or top
132 return true;
133 }
134 for (const pipe of pipes) {
135 if (
136 pipe.x > birdX + birdWidth / 2 + pipeWidth / 2 ||
137 pipe.x < birdX - birdWidth / 2 - pipeWidth / 2
138 ) {
139 // bird isn't at pipe
140 continue;
141 }
142 const {
143 yUpperTop,
144 yUpperBottom,
145 yLowerTop,
146 yLowerBottom,
147 } = getPipeYPositions(size, pipe.gapY);
148 const topPipeRect = {
149 x: pipe.x,
150 y: (yUpperTop + yUpperBottom) / 2,
151 width: pipeWidth,
152 height: yUpperTop - yUpperBottom,
153 };
154 const bottomPipeRect = {
155 x: pipe.x,
156 y: (yLowerTop + yLowerBottom) / 2,
157 width: pipeWidth,
158 height: yLowerTop - yLowerBottom,
159 };
160 // Check a few points at edges of bird
161 const birdPoints = [
162 { x: birdX + birdWidth / 2, y: birdY + birdHeight / 2 },
163 { x: birdX + birdWidth / 2, y: birdY - birdHeight / 2 },
164 { x: birdX, y: birdY + birdHeight / 2 },
165 { x: birdX, y: birdY - birdHeight / 2 },
166 { x: birdX - birdWidth / 2, y: birdY + birdHeight / 2 },
167 { x: birdX - birdWidth / 2, y: birdY - birdHeight / 2 },
168 ];
169 if (
170 birdPoints.some(
171 (point) =>
172 pointInRect(point, topPipeRect) || pointInRect(point, bottomPipeRect)
173 )
174 ) {
175 // Bird hit a pipe!
176 return true;
177 }
178 }
179 return false;
180}
181
182function pointInRect(
183 point: { x: number; y: number },
184 rect: { x: number; y: number; height: number; width: number }
185) {
186 return (
187 point.x > rect.x - rect.width / 2 &&
188 point.x < rect.x + rect.width / 2 &&
189 point.y > rect.y - rect.height / 2 &&
190 point.y < rect.y + rect.height / 2
191 );
192}
193
Preview