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.

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 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
Preview