Global Game Jam 2025, Day 2
It's 8:30. Today I want to establish the input and rules for the game.
Tasks to do:
- Allow rendering the playfield at a different position
- Render the field in 3d. I will need this to handle the bubble rendering
- Always only spawn bubbles at the bottom of the field
- Preview next bubble level that spawns
Render position and 3d
Both of this is connected. I will add an orthographic camera The input gets mapped to the field camera functions. Field rendering itself is done around the origin, so I can move the camera around to render the field at a different position.
1 #include "bpop_main.h"
2
3
4 GameTime gameTime = {
5 .fixedDeltaTime = 1.0f / 60.0f,
6 };
7
8 Model bubbleModel;
9
10 Color ColorFromHex(int hexValue)
11 {
12 Color color = {0};
13 color.r = (hexValue >> 16) & 0xFF;
14 color.g = (hexValue >> 8) & 0xFF;
15 color.b = hexValue & 0xFF;
16 color.a = 0xFF;
17 return color;
18 }
19
20 float BubbleLevelRadius(int level)
21 {
22 return powf((level + 1) * 30, .75f);
23 }
24
25 void PlayfieldFixedUpdate(Playfield *playfield)
26 {
27 for (int i = 0; i < MAX_BUBBLES; i++)
28 {
29 Bubble *bubble = &playfield->bubbles[i];
30 bubble->sameLevelContact = 0;
31 }
32
33 for (int i = 0; i < MAX_BUBBLES; i++)
34 {
35 Bubble *bubble = &playfield->bubbles[i];
36 if (!bubble->flagIsActive) continue;
37 float r = bubble->radius;
38
39 for (int j = i + 1; j < MAX_BUBBLES; j++)
40 {
41 Bubble *other = &playfield->bubbles[j];
42 if (!other->flagIsActive) continue;
43 float otherR = other->radius;
44 float sumR2 = (r + otherR) * (r + otherR);
45 float d2 = Vector2DistanceSqr(bubble->position, other->position);
46 int isSameLevel = bubble->bubbleLevel == other->bubbleLevel;
47 if (d2 < sumR2 * 1.05f)
48 {
49 if (isSameLevel)
50 {
51 bubble->sameLevelContact = 1;
52 other->sameLevelContact = 1;
53 }
54 if (isSameLevel && bubble->bubbleMergeCooldown <= 0.0f
55 && other->bubbleMergeCooldown <= 0.0f)
56 {
57 // merge bubbles
58 bubble->bubbleLevel++;
59 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
60 bubble->bubbleMergeCooldown = 1.0f;
61 other->flagIsActive = 0;
62 }
63 }
64
65 if (d2 < sumR2)
66 {
67 float overlap = r + otherR - sqrtf(d2);
68 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
69 // resolve overlap by moving the bubbles apart
70 const float errorCorrection = 0.25f;
71 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
72 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
73
74 // bounce off each other
75 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
76 float dot = Vector2DotProduct(relativeVelocity, normal);
77 if (dot > 0.0f)
78 {
79 // DrawLineV(bubble->position, other->position, ColorFromHex(0xff0000));
80 float impulse = -dot * 0.85f;
81 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
82 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
83 }
84 }
85 }
86
87 if (!bubble->sameLevelContact)
88 {
89 bubble->bubbleMergeCooldown = 1.0f;
90 }
91 else
92 {
93 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
94 }
95 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, 20.0f});
96 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
97 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
98 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
99 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
100 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
101 {
102 bubble->velocity.x *= -0.9f;
103 }
104 if ((bubble->position.y < r && bubble->velocity.y < 0.0f) ||
105 (bubble->position.y > playfield->fieldSize.y - r && bubble->velocity.y > 0.0f))
106 {
107 bubble->velocity.y *= -0.9f;
108 }
109
110 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
111 bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
112
113 // debug velocity
114 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), ColorFromHex(0xff0000));
115 }
116 }
117
118 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
119 {
120 for (int i = 0; i < MAX_BUBBLES; i++)
121 {
122 Bubble *bubble = &playfield->bubbles[i];
123 if (!bubble->flagIsActive)
124 {
125 bubble->flagIsActive = 1;
126 bubble->position = position;
127 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
128 bubble->bubbleLevel = playfield->nextBubbleLevel;
129 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
130 break;
131 }
132 }
133
134 playfield->nextBubbleLevel = GetRandomValue(0, 2);
135 }
136
137 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
138 {
139 Vector2 spawnPosition = position;
140 spawnPosition.y = BubbleLevelRadius(5);
141 return spawnPosition;
142 }
143 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
144 {
145 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
146 cursorPosition.x += playfield->fieldSize.x / 2;
147 cursorPosition.y += playfield->fieldSize.y / 2;
148
149 Vector2 pos = {cursorPosition.x, cursorPosition.y};
150 return PlayfieldPositionToSpawnPosition(playfield, pos);
151 }
152
153 void PlayfieldDraw(Playfield *playfield, Camera3D camera)
154 {
155 float bubbleExtraRadius = 5.0f;
156 Vector2 mousePos = GetMousePosition();
157 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
158 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, ColorFromHex(0x225588));
159
160 DrawModel(bubbleModel,
161 (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0},
162 BubbleLevelRadius(playfield->nextBubbleLevel) + bubbleExtraRadius, ColorFromHex(0xaaccff));
163 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, ColorFromHex(0x225588));
164 rlPushMatrix();
165 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
166 for (int i = 0; i < MAX_BUBBLES; i++)
167 {
168 Bubble *bubble = &playfield->bubbles[i];
169 if (!bubble->flagIsActive) continue;
170 Vector3 position = {bubble->position.x, bubble->position.y, 0};
171 DrawModel(bubbleModel, position, bubble->radius + bubbleExtraRadius, ColorFromHex(0xaaccff));
172 // DrawCircleLinesV(bubble->position, bubble->radius, ColorFromHex(0xaaccff));
173 // const char* bubbleLevel = TextFormat("%d:%.1f", bubble->bubbleLevel, bubble->bubbleMergeCooldown);
174 // float width = MeasureText(bubbleLevel, 20);
175 // DrawText(bubbleLevel, bubble->position.x - width / 2, bubble->position.y - 10, 20, ColorFromHex(0xaaccff));
176 }
177 rlPopMatrix();
178 }
179
180 int main(void)
181 {
182 // Initialization
183 //--------------------------------------------------------------------------------------
184 const int screenWidth = 800;
185 const int screenHeight = 450;
186
187 InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window");
188
189 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
190 //--------------------------------------------------------------------------------------
191
192 Camera3D camera = {
193 .position = { 0.0f, 0.0f, -10.0f },
194 .target = { 0.0f, 0.0f, 0.0f },
195 .up = { 0.0f, 1.0f, 0.0f },
196 .projection = CAMERA_ORTHOGRAPHIC,
197 .fovy = 350.0f,
198 };
199 Playfield playfield = {
200 .fieldSize = {300, 300}
201 };
202
203 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
204 // Main game loop
205 while (!WindowShouldClose()) // Detect window close button or ESC key
206 {
207 float dt = GetFrameTime();
208 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
209 if (dt > 0.2f) dt = 0.2f;
210 gameTime.time += dt;
211 gameTime.deltaTime = dt;
212
213
214 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
215 {
216 PlayfieldTryAddBubble(&playfield, PlayfieldScreenToSpawnPosition(&playfield, camera,
217 GetMousePosition()));
218 }
219 // Draw
220 //----------------------------------------------------------------------------------
221 BeginDrawing();
222
223 ClearBackground(ColorFromHex(0x4488cc));
224
225 BeginMode3D(camera);
226 PlayfieldDraw(&playfield, camera);
227
228 // fixed step update in post draw to allow debug drawing
229 while (gameTime.fixedTime < gameTime.time)
230 {
231 gameTime.fixedTime += gameTime.fixedDeltaTime;
232 PlayfieldFixedUpdate(&playfield);
233 }
234
235 EndMode3D();
236
237 EndDrawing();
238 //----------------------------------------------------------------------------------
239 }
240
241 // De-Initialization
242 //--------------------------------------------------------------------------------------
243 CloseWindow(); // Close window and OpenGL context
244 //--------------------------------------------------------------------------------------
245
246 return 0;
247 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8
9 typedef struct Bubble
10 {
11 uint8_t flagIsActive:1;
12 uint8_t sameLevelContact:1;
13 uint8_t bubbleLevel;
14 Vector2 position;
15 Vector2 velocity;
16 float bubbleMergeCooldown;
17 float radius;
18 } Bubble;
19
20 #define MAX_BUBBLES 64
21
22 typedef struct Playfield
23 {
24 Bubble bubbles[MAX_BUBBLES];
25 Matrix transform;
26 Vector2 fieldSize;
27 uint8_t nextBubbleLevel;
28 } Playfield;
29
30 typedef struct GameTime
31 {
32 float time;
33 float deltaTime;
34 float fixedTime;
35 float fixedDeltaTime;
36 } GameTime;
37
38 Color ColorFromHex(int hexValue);
39 #endif
Ok, got the camera working. The bubbles are spheres, but due to a happy little accident, they look like circular outlines. This is due to the clipping plane of the camera that cuts off the front of the sphere, making it lookthrough. I am render the spheres with and increased radius so they overlap a little. I have some ideas how to later make the rendering make the bubbles look less like spheres and more like bubbles that connect to each other. But that is for later.
The input is now also limited to the bottom of the screen.
One thing how to make this game a little different from my inspirational game is to add a water line where buyancy changes and the bubbles could form smaller towers.
Let's try it out:
1 #include "bpop_main.h"
2
3
4 GameTime gameTime = {
5 .fixedDeltaTime = 1.0f / 60.0f,
6 };
7
8 Model bubbleModel;
9
10 Color ColorFromHex(int hexValue)
11 {
12 Color color = {0};
13 color.r = (hexValue >> 16) & 0xFF;
14 color.g = (hexValue >> 8) & 0xFF;
15 color.b = hexValue & 0xFF;
16 color.a = 0xFF;
17 return color;
18 }
19
20 float BubbleLevelRadius(int level)
21 {
22 return powf((level + 1) * 30, .65f);
23 }
24
25 void PlayfieldFixedUpdate(Playfield *playfield)
26 {
27 for (int i = 0; i < MAX_BUBBLES; i++)
28 {
29 Bubble *bubble = &playfield->bubbles[i];
30 bubble->sameLevelContact = 0;
31 }
32
33 for (int i = 0; i < MAX_BUBBLES; i++)
34 {
35 Bubble *bubble = &playfield->bubbles[i];
36 if (!bubble->flagIsActive) continue;
37 float r = bubble->radius;
38
39 for (int j = i + 1; j < MAX_BUBBLES; j++)
40 {
41 Bubble *other = &playfield->bubbles[j];
42 if (!other->flagIsActive) continue;
43 float otherR = other->radius;
44 float sumR2 = (r + otherR) * (r + otherR);
45 float d2 = Vector2DistanceSqr(bubble->position, other->position);
46 int isSameLevel = bubble->bubbleLevel == other->bubbleLevel;
47 if (d2 < sumR2 * 1.05f)
48 {
49 if (isSameLevel)
50 {
51 bubble->sameLevelContact = 1;
52 other->sameLevelContact = 1;
53 }
54 if (isSameLevel && bubble->bubbleMergeCooldown <= 0.0f
55 && other->bubbleMergeCooldown <= 0.0f)
56 {
57 // merge bubbles
58 bubble->bubbleLevel++;
59 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
60 bubble->bubbleMergeCooldown = 1.0f;
61 other->flagIsActive = 0;
62 }
63 }
64
65 if (d2 < sumR2)
66 {
67 float overlap = r + otherR - sqrtf(d2);
68 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
69 // resolve overlap by moving the bubbles apart
70 const float errorCorrection = 0.25f;
71 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
72 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
73
74 // bounce off each other
75 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
76 float dot = Vector2DotProduct(relativeVelocity, normal);
77 if (dot > 0.0f)
78 {
79 // DrawLineV(bubble->position, other->position, ColorFromHex(0xff0000));
80 float impulse = -dot * 0.85f;
81 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
82 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
83 }
84 }
85 }
86
87 if (!bubble->sameLevelContact)
88 {
89 bubble->bubbleMergeCooldown = 1.0f;
90 }
91 else
92 {
93 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
94 }
95
96 float buoyancy = -20.0f;
97 if (bubble->position.y < playfield->waterLevel)
98 {
99 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
100 }
101 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, buoyancy});
102 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
103 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
104 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
105 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
106 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
107 {
108 bubble->velocity.x *= -0.9f;
109 }
110
111 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
112 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
113
114 // debug velocity
115 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), ColorFromHex(0xff0000));
116 }
117 }
118
119 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
120 {
121 for (int i = 0; i < MAX_BUBBLES; i++)
122 {
123 Bubble *bubble = &playfield->bubbles[i];
124 if (!bubble->flagIsActive)
125 {
126 bubble->flagIsActive = 1;
127 bubble->position = position;
128 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
129 bubble->bubbleLevel = playfield->nextBubbleLevel;
130 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
131 break;
132 }
133 }
134
135 playfield->nextBubbleLevel = GetRandomValue(0, 5);
136 }
137
138 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
139 {
140 Vector2 spawnPosition = position;
141 spawnPosition.y = BubbleLevelRadius(5);
142 return spawnPosition;
143 }
144 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
145 {
146 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
147 cursorPosition.x += playfield->fieldSize.x / 2;
148 cursorPosition.y += playfield->fieldSize.y / 2;
149
150 Vector2 pos = {cursorPosition.x, cursorPosition.y};
151 return PlayfieldPositionToSpawnPosition(playfield, pos);
152 }
153
154 void PlayfieldDraw(Playfield *playfield, Camera3D camera)
155 {
156 float bubbleExtraRadius = 5.0f;
157 Vector2 mousePos = GetMousePosition();
158 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
159 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, ColorFromHex(0xaabbee));
160 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
161 playfield->waterLevel, 0, ColorFromHex(0x225588));
162
163 DrawModel(bubbleModel,
164 (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0},
165 BubbleLevelRadius(playfield->nextBubbleLevel) + bubbleExtraRadius, ColorFromHex(0xaaccff));
166 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, ColorFromHex(0x225588));
167 rlPushMatrix();
168 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
169 for (int i = 0; i < MAX_BUBBLES; i++)
170 {
171 Bubble *bubble = &playfield->bubbles[i];
172 if (!bubble->flagIsActive) continue;
173 Vector3 position = {bubble->position.x, bubble->position.y, 0};
174 DrawModel(bubbleModel, position, bubble->radius + bubbleExtraRadius, ColorFromHex(0xaaccff));
175 // DrawCircleLinesV(bubble->position, bubble->radius, ColorFromHex(0xaaccff));
176 // const char* bubbleLevel = TextFormat("%d:%.1f", bubble->bubbleLevel, bubble->bubbleMergeCooldown);
177 // float width = MeasureText(bubbleLevel, 20);
178 // DrawText(bubbleLevel, bubble->position.x - width / 2, bubble->position.y - 10, 20, ColorFromHex(0xaaccff));
179 }
180 rlPopMatrix();
181 }
182
183 int main(void)
184 {
185 // Initialization
186 //--------------------------------------------------------------------------------------
187 const int screenWidth = 800;
188 const int screenHeight = 450;
189
190 InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window");
191
192 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
193 //--------------------------------------------------------------------------------------
194
195 Camera3D camera = {
196 .position = { 0.0f, 0.0f, -10.0f },
197 .target = { 0.0f, 0.0f, 0.0f },
198 .up = { 0.0f, 1.0f, 0.0f },
199 .projection = CAMERA_ORTHOGRAPHIC,
200 .fovy = 350.0f,
201 };
202 Playfield playfield = {
203 .fieldSize = {250, 300},
204 .waterLevel = 170.0f,
205 };
206
207 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
208 // Main game loop
209 while (!WindowShouldClose()) // Detect window close button or ESC key
210 {
211 float dt = GetFrameTime();
212 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
213 if (dt > 0.2f) dt = 0.2f;
214 gameTime.time += dt;
215 gameTime.deltaTime = dt;
216
217
218 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
219 {
220 PlayfieldTryAddBubble(&playfield, PlayfieldScreenToSpawnPosition(&playfield, camera,
221 GetMousePosition()));
222 }
223 // Draw
224 //----------------------------------------------------------------------------------
225 BeginDrawing();
226
227 ClearBackground(ColorFromHex(0x4488cc));
228
229 BeginMode3D(camera);
230 PlayfieldDraw(&playfield, camera);
231
232 // fixed step update in post draw to allow debug drawing
233 while (gameTime.fixedTime < gameTime.time)
234 {
235 gameTime.fixedTime += gameTime.fixedDeltaTime;
236 PlayfieldFixedUpdate(&playfield);
237 }
238
239 EndMode3D();
240
241 EndDrawing();
242 //----------------------------------------------------------------------------------
243 }
244
245 // De-Initialization
246 //--------------------------------------------------------------------------------------
247 CloseWindow(); // Close window and OpenGL context
248 //--------------------------------------------------------------------------------------
249
250 return 0;
251 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8
9 typedef struct Bubble
10 {
11 uint8_t flagIsActive:1;
12 uint8_t sameLevelContact:1;
13 uint8_t bubbleLevel;
14 Vector2 position;
15 Vector2 velocity;
16 float bubbleMergeCooldown;
17 float radius;
18 } Bubble;
19
20 #define MAX_BUBBLES 64
21
22 typedef struct Playfield
23 {
24 Bubble bubbles[MAX_BUBBLES];
25 Matrix transform;
26 Vector2 fieldSize;
27 float waterLevel;
28 uint8_t nextBubbleLevel;
29 } Playfield;
30
31 typedef struct GameTime
32 {
33 float time;
34 float deltaTime;
35 float fixedTime;
36 float fixedDeltaTime;
37 } GameTime;
38
39 Color ColorFromHex(int hexValue);
40 #endif
The buyancy is interesting. But somehow, the game feels too easy - I can spawn enormous amounts of bubbles and I can't see a way how this could end up as a loosing game. Even though I increased the range of bubble levels I am spawning, the field quickly cleans up by constantly merging bubbles. There needs to be more of a challenge.
I think I have to try to use different bubble colors instead of having only one color and a range of sizes. It is 11:00 btw and I had a break for breakfast.
1 #include "bpop_main.h"
2
3
4 GameTime gameTime = {
5 .fixedDeltaTime = 1.0f / 60.0f,
6 };
7
8 Color bubbleTypeColors[] = {
9 COLOR_FROM_HEX(0xaaccff),
10 COLOR_FROM_HEX(0xffaa00),
11 COLOR_FROM_HEX(0x00ffaa),
12 COLOR_FROM_HEX(0xff00aa),
13 COLOR_FROM_HEX(0xaa00ff),
14 };
15
16 Model bubbleModel;
17
18 float BubbleLevelRadius(int level)
19 {
20 return powf((level + 1) * 20, .75f);
21 }
22
23 void PlayfieldFixedUpdate(Playfield *playfield)
24 {
25 for (int i = 0; i < MAX_BUBBLES; i++)
26 {
27 Bubble *bubble = &playfield->bubbles[i];
28 bubble->sameLevelContact = 0;
29 }
30
31 for (int i = 0; i < MAX_BUBBLES; i++)
32 {
33 Bubble *bubble = &playfield->bubbles[i];
34 if (!bubble->flagIsActive) continue;
35 float r = bubble->radius;
36
37 for (int j = i + 1; j < MAX_BUBBLES; j++)
38 {
39 Bubble *other = &playfield->bubbles[j];
40 if (!other->flagIsActive) continue;
41 float otherR = other->radius;
42 float sumR2 = (r + otherR) * (r + otherR);
43 float d2 = Vector2DistanceSqr(bubble->position, other->position);
44 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType;
45 if (d2 < sumR2 * 1.05f)
46 {
47 if (canMerge)
48 {
49 bubble->sameLevelContact = 1;
50 other->sameLevelContact = 1;
51 }
52 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f
53 && other->bubbleMergeCooldown <= 0.0f)
54 {
55 // merge bubbles
56 bubble->bubbleLevel++;
57 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
58 bubble->bubbleMergeCooldown = 1.0f;
59 other->flagIsActive = 0;
60 }
61 }
62
63 if (d2 < sumR2)
64 {
65 float overlap = r + otherR - sqrtf(d2);
66 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
67 // resolve overlap by moving the bubbles apart
68 const float errorCorrection = 0.25f;
69 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
70 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
71
72 // bounce off each other
73 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
74 float dot = Vector2DotProduct(relativeVelocity, normal);
75 if (dot > 0.0f)
76 {
77 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000));
78 float impulse = -dot * 0.85f;
79 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
80 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
81 }
82 }
83 }
84
85 if (!bubble->sameLevelContact)
86 {
87 bubble->bubbleMergeCooldown = 1.0f;
88 }
89 else
90 {
91 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
92 }
93
94 float buoyancy = -20.0f;
95 if (bubble->position.y < playfield->waterLevel)
96 {
97 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
98 }
99 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, buoyancy});
100 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
101 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
102 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
103 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
104 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
105 {
106 bubble->velocity.x *= -0.9f;
107 }
108
109 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
110 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
111
112 // debug velocity
113 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000));
114 }
115 }
116
117 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
118 {
119 for (int i = 0; i < MAX_BUBBLES; i++)
120 {
121 Bubble *bubble = &playfield->bubbles[i];
122 if (!bubble->flagIsActive)
123 {
124 bubble->flagIsActive = 1;
125 bubble->position = position;
126 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
127 bubble->bubbleType = playfield->nextBubbleType;
128 bubble->bubbleLevel = playfield->nextBubbleLevel;
129 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
130 break;
131 }
132 }
133
134 playfield->nextBubbleType = GetRandomValue(0, MAX_BUBBLE_TYPES - 1);
135 playfield->nextBubbleLevel = GetRandomValue(0, 3);
136 }
137
138 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
139 {
140 Vector2 spawnPosition = position;
141 spawnPosition.y = BubbleLevelRadius(5);
142 return spawnPosition;
143 }
144 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
145 {
146 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
147 cursorPosition.x += playfield->fieldSize.x / 2;
148 cursorPosition.y += playfield->fieldSize.y / 2;
149
150 Vector2 pos = {cursorPosition.x, cursorPosition.y};
151 return PlayfieldPositionToSpawnPosition(playfield, pos);
152 }
153
154 void PlayfieldDraw(Playfield *playfield, Camera3D camera)
155 {
156 float bubbleExtraRadius = 5.0f;
157 Vector2 mousePos = GetMousePosition();
158 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
159 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xaabbee));
160 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
161 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588));
162
163 DrawModel(bubbleModel,
164 (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0},
165 BubbleLevelRadius(playfield->nextBubbleLevel) + bubbleExtraRadius, bubbleTypeColors[playfield->nextBubbleType]);
166 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588));
167 rlPushMatrix();
168 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
169 for (int i = 0; i < MAX_BUBBLES; i++)
170 {
171 Bubble *bubble = &playfield->bubbles[i];
172 if (!bubble->flagIsActive) continue;
173 Vector3 position = {bubble->position.x, bubble->position.y, 0};
174 DrawModel(bubbleModel, position, bubble->radius + bubbleExtraRadius, bubbleTypeColors[bubble->bubbleType]);
175 // DrawCircleLinesV(bubble->position, bubble->radius, COLOR_FROM_HEX(0xaaccff));
176 // const char* bubbleLevel = TextFormat("%d:%.1f", bubble->bubbleLevel, bubble->bubbleMergeCooldown);
177 // float width = MeasureText(bubbleLevel, 20);
178 // DrawText(bubbleLevel, bubble->position.x - width / 2, bubble->position.y - 10, 20, COLOR_FROM_HEX(0xaaccff));
179 }
180 rlPopMatrix();
181 }
182
183 int main(void)
184 {
185 // Initialization
186 //--------------------------------------------------------------------------------------
187 const int screenWidth = 800;
188 const int screenHeight = 450;
189
190 InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window");
191
192 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
193 //--------------------------------------------------------------------------------------
194
195 Camera3D camera = {
196 .position = { 0.0f, 0.0f, -10.0f },
197 .target = { 0.0f, 0.0f, 0.0f },
198 .up = { 0.0f, 1.0f, 0.0f },
199 .projection = CAMERA_ORTHOGRAPHIC,
200 .fovy = 350.0f,
201 };
202 Playfield playfield = {
203 .fieldSize = {250, 300},
204 .waterLevel = 170.0f,
205 };
206
207 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
208 // Main game loop
209 while (!WindowShouldClose()) // Detect window close button or ESC key
210 {
211 float dt = GetFrameTime();
212 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
213 if (dt > 0.2f) dt = 0.2f;
214 gameTime.time += dt;
215 gameTime.deltaTime = dt;
216
217
218 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
219 {
220 PlayfieldTryAddBubble(&playfield, PlayfieldScreenToSpawnPosition(&playfield, camera,
221 GetMousePosition()));
222 }
223 // Draw
224 //----------------------------------------------------------------------------------
225 BeginDrawing();
226
227 ClearBackground(COLOR_FROM_HEX(0x4488cc));
228
229 BeginMode3D(camera);
230 PlayfieldDraw(&playfield, camera);
231
232 // fixed step update in post draw to allow debug drawing
233 while (gameTime.fixedTime < gameTime.time)
234 {
235 gameTime.fixedTime += gameTime.fixedDeltaTime;
236 PlayfieldFixedUpdate(&playfield);
237 }
238
239 EndMode3D();
240
241 EndDrawing();
242 //----------------------------------------------------------------------------------
243 }
244
245 // De-Initialization
246 //--------------------------------------------------------------------------------------
247 CloseWindow(); // Close window and OpenGL context
248 //--------------------------------------------------------------------------------------
249
250 return 0;
251 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8
9 #define MAX_BUBBLE_TYPES 3
10
11 typedef struct Bubble
12 {
13 uint8_t flagIsActive:1;
14 uint8_t sameLevelContact:1;
15 uint8_t bubbleType:3;
16 uint8_t bubbleLevel;
17
18 Vector2 position;
19 Vector2 velocity;
20 float bubbleMergeCooldown;
21 float radius;
22 } Bubble;
23
24 #define MAX_BUBBLES 64
25
26 typedef struct Playfield
27 {
28 Bubble bubbles[MAX_BUBBLES];
29 Matrix transform;
30 Vector2 fieldSize;
31 float waterLevel;
32 uint8_t nextBubbleLevel;
33 uint8_t nextBubbleType;
34 } Playfield;
35
36 typedef struct GameTime
37 {
38 float time;
39 float deltaTime;
40 float fixedTime;
41 float fixedDeltaTime;
42 } GameTime;
43
44 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF}
45 #endif
Different types makes it certainly more difficult to keep going. How much? I am not sure. I need a score and some statistic counter displays.
1 #include "bpop_main.h"
2
3 GameTime gameTime = {
4 .fixedDeltaTime = 1.0f / 60.0f,
5 };
6
7 Color bubbleTypeColors[] = {
8 COLOR_FROM_HEX(0xaaccff),
9 COLOR_FROM_HEX(0xffaa33),
10 COLOR_FROM_HEX(0x33ffaa),
11 COLOR_FROM_HEX(0xff33aa),
12 COLOR_FROM_HEX(0xaa33ff),
13 };
14
15 Model bubbleModel;
16 Shader bubbleOutlineShader;
17 RenderTexture2D bubbleFieldTexture;
18
19 float BubbleLevelRadius(int level)
20 {
21 return powf((level + 1) * 20, .75f);
22 }
23
24 void PlayfieldFixedUpdate(Playfield *playfield)
25 {
26 for (int i = 0; i < MAX_BUBBLES; i++)
27 {
28 Bubble *bubble = &playfield->bubbles[i];
29 bubble->sameLevelContact = 0;
30 }
31
32 for (int i = 0; i < MAX_BUBBLES; i++)
33 {
34 Bubble *bubble = &playfield->bubbles[i];
35 if (!bubble->flagIsActive) continue;
36 float r = bubble->radius;
37
38 for (int j = i + 1; j < MAX_BUBBLES; j++)
39 {
40 Bubble *other = &playfield->bubbles[j];
41 if (!other->flagIsActive) continue;
42 float otherR = other->radius;
43 float sumR2 = (r + otherR) * (r + otherR);
44 float d2 = Vector2DistanceSqr(bubble->position, other->position);
45 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType;
46 if (d2 < sumR2 * 1.05f)
47 {
48 if (canMerge)
49 {
50 bubble->sameLevelContact = 1;
51 other->sameLevelContact = 1;
52 }
53 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f
54 && other->bubbleMergeCooldown <= 0.0f)
55 {
56 // merge bubbles
57 bubble->bubbleLevel++;
58 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
59 bubble->bubbleMergeCooldown = 1.0f;
60 other->flagIsActive = 0;
61 }
62 }
63
64 if (d2 < sumR2)
65 {
66 float overlap = r + otherR - sqrtf(d2);
67 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
68 // resolve overlap by moving the bubbles apart
69 const float errorCorrection = 0.25f;
70 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
71 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
72
73 // bounce off each other
74 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
75 float dot = Vector2DotProduct(relativeVelocity, normal);
76 if (dot > 0.0f)
77 {
78 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000));
79 float impulse = -dot * 0.85f;
80 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
81 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
82 }
83 }
84 }
85
86 if (!bubble->sameLevelContact)
87 {
88 bubble->bubbleMergeCooldown = 1.0f;
89 }
90 else
91 {
92 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
93 }
94
95 float buoyancy = -20.0f;
96 if (bubble->position.y < playfield->waterLevel)
97 {
98 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
99 }
100
101 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius ||
102 bubble->position.y > playfield->maxHeight - bubble->radius;
103 bubble->lifeTime += gameTime.fixedDeltaTime;
104 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f;
105
106 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, buoyancy});
107 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
108 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
109 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
110 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
111 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
112 {
113 bubble->velocity.x *= -0.9f;
114 }
115
116 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
117 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
118
119 // debug velocity
120 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000));
121 }
122 }
123
124 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
125 {
126 for (int i = 0; i < MAX_BUBBLES; i++)
127 {
128 Bubble *bubble = &playfield->bubbles[i];
129 if (!bubble->flagIsActive)
130 {
131 bubble->flagIsActive = 1;
132 bubble->position = position;
133 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
134 bubble->bubbleType = playfield->nextBubbleType;
135 bubble->bubbleLevel = playfield->nextBubbleLevel;
136 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
137 bubble->lifeTime = 0.0f;
138 playfield->spawnedBubbleCount++;
139 break;
140 }
141 }
142
143 playfield->nextBubbleType = GetRandomValue(0, MAX_BUBBLE_TYPES - 1);
144 playfield->nextBubbleLevel = GetRandomValue(0, 3);
145 }
146
147 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield)
148 {
149 PlayfieldScores scores = {0};
150 for (int i = 0; i < MAX_BUBBLES; i++)
151 {
152 Bubble *bubble = &playfield->bubbles[i];
153 if (bubble->flagIsActive)
154 {
155 scores.bubbleCount++;
156 uint32_t bubbleScore = 1 << bubble->bubbleLevel;
157 scores.score += bubbleScore;
158
159 if (bubble->isOutsideZone)
160 {
161 scores.outsideBubbleCount++;
162 }
163 }
164 }
165 scores.score += playfield->spawnedBubbleCount;
166 return scores;
167 }
168
169 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
170 {
171 Vector2 spawnPosition = position;
172 spawnPosition.y = BubbleLevelRadius(5);
173 return spawnPosition;
174 }
175
176 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
177 {
178 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
179 cursorPosition.x += playfield->fieldSize.x / 2;
180 cursorPosition.y += playfield->fieldSize.y / 2;
181
182 Vector2 pos = {cursorPosition.x, cursorPosition.y};
183 return PlayfieldPositionToSpawnPosition(playfield, pos);
184 }
185
186 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera)
187 {
188 float bubbleExtraRadius = 5.0f;
189 Vector2 mousePos = GetMousePosition();
190 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
191 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff));
192 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
193 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588));
194
195 DrawModel(bubbleModel,
196 (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0},
197 BubbleLevelRadius(playfield->nextBubbleLevel) + bubbleExtraRadius, bubbleTypeColors[playfield->nextBubbleType]);
198 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588));
199 rlPushMatrix();
200 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
201 // draw bubbles into playfield space
202 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f;
203 for (int i = 0; i < MAX_BUBBLES; i++)
204 {
205 Bubble *bubble = &playfield->bubbles[i];
206 if (!bubble->flagIsActive) continue;
207 Vector3 position = {bubble->position.x, bubble->position.y, 0};
208 Color bubbleColor = bubbleTypeColors[bubble->bubbleType];
209 int isOutsideZone = bubble->isOutsideZone;
210
211 if (isOutsideZone)
212 {
213 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink);
214 }
215 // lazy: encode id into rgb values
216 bubbleColor.r = bubbleColor.r - i % 8;
217 bubbleColor.g = bubbleColor.g - (i / 8) % 8;
218 bubbleColor.b = bubbleColor.b - (i / 64) % 8;
219 bubbleColor.a = 255;
220 DrawModel(bubbleModel, position, bubble->radius + bubbleExtraRadius, bubbleColor);
221 }
222 rlPopMatrix();
223 }
224
225 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera)
226 {
227 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400);
228 int divides = 10;
229 float divWidth = playfield->fieldSize.x / divides;
230 for (int i = 0; i < divides; i+=2)
231 {
232 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f;
233 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
234 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
235 }
236 }
237 // const char* bubbleLevel = TextFormat("%d:%.1f", bubble->bubbleLevel, bubble->bubbleMergeCooldown);
238 // float width = MeasureText(bubbleLevel, 20);
239 // DrawText(bubbleLevel, bubble->position.x - width / 2, bubble->position.y - 10, 20, COLOR_FROM_HEX(0xaaccff));
240
241 int main(void)
242 {
243 // Initialization
244 //--------------------------------------------------------------------------------------
245 const int screenWidth = 800;
246 const int screenHeight = 450;
247
248 InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window");
249
250 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
251 //--------------------------------------------------------------------------------------
252
253 Camera3D camera = {
254 .position = { 0.0f, 0.0f, -10.0f },
255 .target = { 0.0f, 0.0f, 0.0f },
256 .up = { 0.0f, 1.0f, 0.0f },
257 .projection = CAMERA_ORTHOGRAPHIC,
258 .fovy = 350.0f,
259 };
260 Playfield playfield = {
261 .fieldSize = {250, 300},
262 .waterLevel = 200.0f,
263 .minHeight = 90.0f,
264 .maxHeight = 280.0f,
265 };
266
267 TraceLog(LOG_INFO, "loading shaders");
268 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs");
269 // Get shader locations
270 int outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize");
271 int outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor");
272 int textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize");
273
274
275
276 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
277 // Main game loop
278 while (!WindowShouldClose()) // Detect window close button or ESC key
279 {
280 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight())
281 {
282 UnloadRenderTexture(bubbleFieldTexture);
283 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
284 }
285
286 float dt = GetFrameTime();
287 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
288 if (dt > 0.2f) dt = 0.2f;
289 gameTime.time += dt;
290 gameTime.deltaTime = dt;
291
292
293 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
294 {
295 PlayfieldTryAddBubble(&playfield, PlayfieldScreenToSpawnPosition(&playfield, camera,
296 GetMousePosition()));
297 }
298 // Draw
299 //----------------------------------------------------------------------------------
300
301 BeginDrawing();
302
303 ClearBackground(COLOR_FROM_HEX(0x4488cc));
304
305
306 BeginTextureMode(bubbleFieldTexture);
307 rlSetClipPlanes(-128.0f, 128.0f);
308 BeginMode3D(camera);
309
310 ClearBackground(COLOR_FROM_HEX(0));
311 PlayfieldDrawBubbles(&playfield, camera);
312 EndMode3D();
313 EndTextureMode();
314
315 // fixed step update in post draw to allow debug drawing
316 while (gameTime.fixedTime < gameTime.time)
317 {
318 gameTime.fixedTime += gameTime.fixedDeltaTime;
319 PlayfieldFixedUpdate(&playfield);
320 }
321
322 // Set shader values (they can be changed later)
323 float outlineSize = 1.0f;
324 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; // Normalized RED color
325 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height };
326
327 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT);
328 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4);
329 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2);
330
331 rlDisableDepthMask();
332 BeginShaderMode(bubbleOutlineShader);
333 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width,
334 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()},
335 (Vector2){0, 0}, 0.0f, WHITE);
336 EndShaderMode();
337 rlEnableDepthMask();
338
339 BeginMode3D(camera);
340 PlayfieldDrawRange(&playfield, camera);
341 EndMode3D();
342
343 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
344 DrawText(TextFormat("Score: %d", scores.score), 10, 10, 20, WHITE);
345 DrawText(TextFormat("Bubbles: %d", scores.bubbleCount), 10, 40, 20, WHITE);
346 DrawText(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, 70, 20, WHITE);
347 DrawText(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, 100, 20, WHITE);
348
349 EndDrawing();
350 //----------------------------------------------------------------------------------
351 }
352
353 // De-Initialization
354 //--------------------------------------------------------------------------------------
355 CloseWindow(); // Close window and OpenGL context
356 //--------------------------------------------------------------------------------------
357
358 return 0;
359 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8 #include <math.h>
9
10 #define MAX_BUBBLE_TYPES 3
11
12 typedef struct Bubble
13 {
14 uint8_t flagIsActive:1;
15 uint8_t sameLevelContact:1;
16 uint8_t isOutsideZone:1;
17 uint8_t bubbleType:3;
18 uint8_t bubbleLevel;
19
20 Vector2 position;
21 Vector2 velocity;
22 float bubbleMergeCooldown;
23 float radius;
24 float lifeTime;
25 } Bubble;
26
27 #define MAX_BUBBLES 64
28
29 typedef struct PlayfieldScores
30 {
31 uint8_t bubbleCount;
32 uint8_t outsideBubbleCount;
33 uint32_t score;
34 } PlayfieldScores;
35
36 typedef struct Playfield
37 {
38 Bubble bubbles[MAX_BUBBLES];
39 Matrix transform;
40 Vector2 fieldSize;
41 float waterLevel;
42 float maxHeight;
43 float minHeight;
44 uint8_t nextBubbleLevel;
45 uint8_t nextBubbleType;
46 uint32_t spawnedBubbleCount;
47 } Playfield;
48
49 typedef struct GameTime
50 {
51 float time;
52 float deltaTime;
53 float fixedTime;
54 float fixedDeltaTime;
55 } GameTime;
56
57 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF}
58 #endif
it's nearly 16:00. I have a score counter and am detecting when bubbles are outside the allowed area. I guess it's time to think about game states...
- Start screen
- Difficulty selection screen
- Game screen
- Game over screen
- Highscore screen
1 #include "bpop_main.h"
2
3 GameScene currentScene = GAME_SCENE_MENU;
4 GameScene nextScene = GAME_SCENE_NONE;
5
6 GameDifficulty gameDifficulty = GAME_DIFFICULTY_NORMAL;
7
8 int outlineSizeLoc = 0;
9 int outlineColorLoc = 0;
10 int textureSizeLoc = 0;
11
12 Camera3D camera = {
13 .position = { 0.0f, 0.0f, -10.0f },
14 .target = { 0.0f, 0.0f, 0.0f },
15 .up = { 0.0f, 1.0f, 0.0f },
16 .projection = CAMERA_ORTHOGRAPHIC,
17 .fovy = 350.0f,
18 };
19
20
21 Playfield playfield = {
22 .fieldSize = {250, 300},
23 .waterLevel = 200.0f,
24 .minHeight = 90.0f,
25 .maxHeight = 280.0f,
26 };
27
28 GameTime gameTime = {
29 .fixedDeltaTime = 1.0f / 60.0f,
30 };
31
32 Color bubbleTypeColors[] = {
33 COLOR_FROM_HEX(0xaaccff),
34 COLOR_FROM_HEX(0xaa7733),
35 COLOR_FROM_HEX(0x33ffaa),
36 COLOR_FROM_HEX(0xff33aa),
37 COLOR_FROM_HEX(0xaa33ff),
38 };
39
40 Model bubbleModel;
41 Shader bubbleOutlineShader;
42 RenderTexture2D bubbleFieldTexture;
43
44 float BubbleLevelRadius(int level)
45 {
46 return powf((level + 1) * 20, .75f);
47 }
48
49 int Button(const char *text, Vector2 position, Vector2 size)
50 {
51 int result = 0;
52 Rectangle rect = {position.x, position.y, size.x, size.y};
53 if (CheckCollisionPointRec(GetMousePosition(), rect))
54 {
55 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
56 {
57 result = 1;
58 }
59 }
60 DrawRectangleRec(rect, (Color){200, 200, 200, 255});
61 DrawRectangleLinesEx(rect, 2, BLACK);
62 float width = MeasureText(text, 20);
63 DrawText(text, position.x + size.x * 0.5f - width * 0.5f, position.y + 10, 20, BLACK);
64 return result;
65 }
66
67 void PlayfieldFixedUpdate(Playfield *playfield)
68 {
69 for (int i = 0; i < MAX_BUBBLES; i++)
70 {
71 Bubble *bubble = &playfield->bubbles[i];
72 bubble->sameLevelContact = 0;
73 }
74
75 for (int i = 0; i < MAX_BUBBLES; i++)
76 {
77 Bubble *bubble = &playfield->bubbles[i];
78 if (!bubble->flagIsActive) continue;
79 float r = bubble->radius;
80
81 for (int j = i + 1; j < MAX_BUBBLES; j++)
82 {
83 Bubble *other = &playfield->bubbles[j];
84 if (!other->flagIsActive) continue;
85 float otherR = other->radius;
86 float sumR2 = (r + otherR) * (r + otherR);
87 float d2 = Vector2DistanceSqr(bubble->position, other->position);
88 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType;
89 if (d2 < sumR2 * 1.05f)
90 {
91 if (canMerge)
92 {
93 bubble->sameLevelContact = 1;
94 other->sameLevelContact = 1;
95 }
96 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f
97 && other->bubbleMergeCooldown <= 0.0f)
98 {
99 // merge bubbles
100 bubble->bubbleLevel++;
101 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
102 bubble->bubbleMergeCooldown = 1.0f;
103 other->flagIsActive = 0;
104 }
105 }
106
107 if (d2 < sumR2)
108 {
109 float overlap = r + otherR - sqrtf(d2);
110 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
111 // resolve overlap by moving the bubbles apart
112 const float errorCorrection = 0.25f;
113 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
114 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
115
116 // bounce off each other
117 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
118 float dot = Vector2DotProduct(relativeVelocity, normal);
119 if (dot > 0.0f)
120 {
121 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000));
122 float impulse = -dot * 0.85f;
123 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
124 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
125 }
126 }
127 }
128
129 if (!bubble->sameLevelContact)
130 {
131 bubble->bubbleMergeCooldown = 1.0f;
132 }
133 else
134 {
135 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
136 }
137
138 float buoyancy = -20.0f;
139 if (bubble->position.y < playfield->waterLevel)
140 {
141 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
142 }
143
144 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius ||
145 bubble->position.y > playfield->maxHeight - bubble->radius;
146 bubble->lifeTime += gameTime.fixedDeltaTime;
147 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f;
148
149 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, buoyancy});
150 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
151 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
152 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
153 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
154 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
155 {
156 bubble->velocity.x *= -0.9f;
157 }
158
159 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
160 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
161
162 // debug velocity
163 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000));
164 }
165
166 int outsideCount = 0;
167 for (int i = 0; i < MAX_BUBBLES; i++)
168 {
169 Bubble *bubble = &playfield->bubbles[i];
170 if (bubble->isOutsideZone)
171 {
172 outsideCount++;
173 }
174 }
175
176 if (outsideCount == 0)
177 {
178 playfield->strikes = 0;
179 playfield->gameoverCooldown = 0.0f;
180 }
181 if (playfield->strikes >= MAX_STRIKES)
182 {
183 playfield->gameoverCooldown += gameTime.fixedDeltaTime;
184 if (playfield->gameoverCooldown > GAMEOVER_COOLDOWN)
185 {
186 nextScene = GAME_SCENE_GAMEOVER;
187 }
188 }
189 }
190
191 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
192 {
193 if (playfield->strikes >= MAX_STRIKES) return;
194
195 for (int i = 0; i < MAX_BUBBLES; i++)
196 {
197 Bubble *bubble = &playfield->bubbles[i];
198 if (!bubble->flagIsActive)
199 {
200 bubble->flagIsActive = 1;
201 bubble->position = position;
202 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
203 bubble->bubbleType = playfield->nextBubbleType;
204 bubble->bubbleLevel = playfield->nextBubbleLevel;
205 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
206 bubble->lifeTime = 0.0f;
207 playfield->spawnedBubbleCount++;
208
209 for (int j = 0; j < MAX_BUBBLES; j += 1)
210 {
211 Bubble *other = &playfield->bubbles[j];
212 if (!other->flagIsActive) continue;
213 if (other->isOutsideZone)
214 {
215 playfield->strikes++;
216 break;
217 }
218 }
219
220 playfield->nextBubbleType = GetRandomValue(0, gameDifficulty);
221 playfield->nextBubbleLevel = GetRandomValue(0, 3);
222 break;
223 }
224 }
225
226 }
227
228 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield)
229 {
230 PlayfieldScores scores = {0};
231 for (int i = 0; i < MAX_BUBBLES; i++)
232 {
233 Bubble *bubble = &playfield->bubbles[i];
234 if (bubble->flagIsActive)
235 {
236 scores.bubbleCount++;
237 uint32_t bubbleScore = 1 << bubble->bubbleLevel;
238 scores.score += bubbleScore;
239
240 if (bubble->isOutsideZone)
241 {
242 scores.outsideBubbleCount++;
243 }
244 }
245 }
246 scores.score += playfield->spawnedBubbleCount;
247 return scores;
248 }
249
250 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
251 {
252 Vector2 spawnPosition = position;
253 spawnPosition.y = BubbleLevelRadius(5);
254 return spawnPosition;
255 }
256
257 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
258 {
259 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
260 cursorPosition.x += playfield->fieldSize.x / 2;
261 cursorPosition.y += playfield->fieldSize.y / 2;
262
263 Vector2 pos = {cursorPosition.x, cursorPosition.y};
264 return PlayfieldPositionToSpawnPosition(playfield, pos);
265 }
266
267 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera)
268 {
269 float bubbleExtraRadius = 5.0f;
270 Vector2 mousePos = GetMousePosition();
271 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
272 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff));
273 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
274 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588));
275
276 DrawModel(bubbleModel,
277 (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0},
278 BubbleLevelRadius(playfield->nextBubbleLevel) + bubbleExtraRadius, bubbleTypeColors[playfield->nextBubbleType]);
279 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588));
280 rlPushMatrix();
281 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
282 // draw bubbles into playfield space
283 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f;
284 for (int i = 0; i < MAX_BUBBLES; i++)
285 {
286 Bubble *bubble = &playfield->bubbles[i];
287 if (!bubble->flagIsActive) continue;
288 Vector3 position = {bubble->position.x, bubble->position.y, 0};
289 Color bubbleColor = bubbleTypeColors[bubble->bubbleType];
290 int isOutsideZone = bubble->isOutsideZone;
291
292 if (isOutsideZone)
293 {
294 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink);
295 }
296 // lazy: encode id into rgb values
297 bubbleColor.r = bubbleColor.r - i * 8 % 32;
298 bubbleColor.g = bubbleColor.g - (i * 8 / 32) % 32;
299 bubbleColor.b = bubbleColor.b - (i * 8 / 512) % 32;
300 bubbleColor.a = 255;
301 DrawModel(bubbleModel, position, bubble->radius + bubbleExtraRadius, bubbleColor);
302 }
303 rlPopMatrix();
304 }
305
306 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera)
307 {
308 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400);
309 int divides = 10;
310 float divWidth = playfield->fieldSize.x / divides;
311 for (int i = 0; i < divides; i+=2)
312 {
313 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f;
314 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
315 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
316 }
317 }
318
319 void PlayfieldFullDraw(Playfield *playfield, Camera3D camera)
320 {
321 BeginDrawing();
322
323 ClearBackground(COLOR_FROM_HEX(0x4488cc));
324
325 BeginTextureMode(bubbleFieldTexture);
326 rlSetClipPlanes(-128.0f, 128.0f);
327 BeginMode3D(camera);
328
329 ClearBackground(BLANK);
330 PlayfieldDrawBubbles(playfield, camera);
331 EndMode3D();
332 EndTextureMode();
333
334 float outlineSize = 1.0f;
335 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
336 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height };
337
338 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT);
339 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4);
340 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2);
341
342 rlDisableDepthMask();
343 BeginShaderMode(bubbleOutlineShader);
344 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width,
345 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()},
346 (Vector2){0, 0}, 0.0f, WHITE);
347 EndShaderMode();
348 rlEnableDepthMask();
349
350 BeginMode3D(camera);
351 PlayfieldDrawRange(playfield, camera);
352 EndMode3D();
353 }
354
355 void UpdateSceneGameOver()
356 {
357 if (IsKeyPressed(KEY_ENTER))
358 {
359 nextScene = GAME_SCENE_MENU;
360 }
361 // Draw
362 //----------------------------------------------------------------------------------
363
364 BeginDrawing();
365
366 ClearBackground(COLOR_FROM_HEX(0x4488cc));
367
368 PlayfieldFullDraw(&playfield, camera);
369
370 DrawText("Game Over", 30, 10, 40, WHITE);
371 DrawText("Press [ENTER] to restart", 30, 50, 20, WHITE);
372
373 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
374 DrawText(TextFormat("Final Score: %d", scores.score), 30, 90, 20, WHITE);
375
376 if (Button("Restart", (Vector2){30, 130}, (Vector2){100, 50}))
377 {
378 nextScene = GAME_SCENE_MENU;
379 }
380 EndDrawing();
381 }
382
383 void UpdateScenePlay()
384 {
385 if (IsKeyPressed(KEY_ESCAPE))
386 {
387 nextScene = GAME_SCENE_MENU;
388 }
389
390 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && playfield.strikes < MAX_STRIKES)
391 {
392 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
393 GetMousePosition());
394 if (pos.y >= 0.0f && pos.y <= playfield.fieldSize.y
395 && pos.x >= 0.0f && pos.x <= playfield.fieldSize.x)
396 {
397 PlayfieldTryAddBubble(&playfield, pos);
398 }
399 }
400
401 while (gameTime.fixedTime < gameTime.time)
402 {
403 gameTime.fixedTime += gameTime.fixedDeltaTime;
404 PlayfieldFixedUpdate(&playfield);
405 }
406
407 // Draw
408 //----------------------------------------------------------------------------------
409
410 PlayfieldFullDraw(&playfield, camera);
411
412 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
413 DrawText(TextFormat("Score: %d", scores.score), 10, 10, 20, WHITE);
414 DrawText(TextFormat("Bubbles: %d", scores.bubbleCount), 10, 40, 20, WHITE);
415 DrawText(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, 70, 20, WHITE);
416 DrawText(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, 100, 20, WHITE);
417 DrawText(TextFormat("Strikes: %d", playfield.strikes), 10, 130, 20, WHITE);
418 DrawText(TextFormat("Gameover in: %.1f", GAMEOVER_COOLDOWN - playfield.gameoverCooldown), 10, 160, 20, WHITE);
419
420 EndDrawing();
421 }
422
423 void UpdateSceneMenu()
424 {
425 // Draw
426 //----------------------------------------------------------------------------------
427
428 BeginDrawing();
429
430 ClearBackground(COLOR_FROM_HEX(0x4488cc));
431
432 DrawText("Bubble Pop", GetScreenWidth() / 2 - MeasureText("Bubble Pop", 40) / 2, 10, 40, WHITE);
433
434 if (Button("Play easy", (Vector2){GetScreenWidth() / 2 - 100, 100}, (Vector2){200, 50}))
435 {
436 gameDifficulty = GAME_DIFFICULTY_EASY;
437 nextScene = GAME_SCENE_PLAY;
438 }
439
440 if (Button("Play normal", (Vector2){GetScreenWidth() / 2 - 100, 160}, (Vector2){200, 50}))
441 {
442 gameDifficulty = GAME_DIFFICULTY_NORMAL;
443 nextScene = GAME_SCENE_PLAY;
444 }
445
446 if (Button("Play hard", (Vector2){GetScreenWidth() / 2 - 100, 220}, (Vector2){200, 50}))
447 {
448 gameDifficulty = GAME_DIFFICULTY_HARD;
449 nextScene = GAME_SCENE_PLAY;
450 }
451
452 EndDrawing();
453 }
454
455 int main(void)
456 {
457 // Initialization
458 //--------------------------------------------------------------------------------------
459 const int screenWidth = 800;
460 const int screenHeight = 450;
461
462 InitWindow(screenWidth, screenHeight, "GGJ25 - Bubble Pop");
463
464 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
465 //--------------------------------------------------------------------------------------
466
467 TraceLog(LOG_INFO, "loading shaders");
468 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs");
469 // Get shader locations
470 outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize");
471 outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor");
472 textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize");
473
474
475
476 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
477 // Main game loop
478 while (!WindowShouldClose()) // Detect window close button or ESC key
479 {
480 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight())
481 {
482 UnloadRenderTexture(bubbleFieldTexture);
483 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
484 }
485
486 float dt = GetFrameTime();
487 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
488 if (dt > 0.2f) dt = 0.2f;
489 gameTime.time += dt;
490 gameTime.deltaTime = dt;
491
492 switch (currentScene)
493 {
494 default: UpdateSceneMenu(); break;
495 case GAME_SCENE_PLAY: UpdateScenePlay(); break;
496 case GAME_SCENE_GAMEOVER: UpdateSceneGameOver(); break;
497 }
498
499 switch (nextScene)
500 {
501 case GAME_SCENE_NONE: break;
502 default: currentScene = nextScene; break;
503 case GAME_SCENE_PLAY:
504 playfield = (Playfield){
505 .fieldSize = {250, 300},
506 .waterLevel = 200.0f,
507 .minHeight = 90.0f,
508 .maxHeight = 280.0f,
509 };
510 currentScene = GAME_SCENE_PLAY; break;
511 }
512
513 nextScene = GAME_SCENE_NONE;
514 }
515
516 // De-Initialization
517 //--------------------------------------------------------------------------------------
518 CloseWindow(); // Close window and OpenGL context
519 //--------------------------------------------------------------------------------------
520
521 return 0;
522 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8 #include <math.h>
9
10
11 typedef enum GameScene
12 {
13 GAME_SCENE_NONE,
14 GAME_SCENE_MENU,
15 GAME_SCENE_DIFFICULTY_SELECT,
16 GAME_SCENE_PLAY,
17 GAME_SCENE_GAMEOVER,
18 GAME_SCENE_HIGHSCORES,
19 } GameScene;
20
21 typedef enum GameDifficulty
22 {
23 GAME_DIFFICULTY_EASY,
24 GAME_DIFFICULTY_NORMAL,
25 GAME_DIFFICULTY_HARD,
26 } GameDifficulty;
27
28 #define MAX_BUBBLE_TYPES 3
29
30 typedef struct Bubble
31 {
32 uint8_t flagIsActive:1;
33 uint8_t sameLevelContact:1;
34 uint8_t isOutsideZone:1;
35 uint8_t bubbleType:3;
36 uint8_t bubbleLevel;
37
38 Vector2 position;
39 Vector2 velocity;
40 float bubbleMergeCooldown;
41 float radius;
42 float lifeTime;
43 } Bubble;
44
45 #define MAX_BUBBLES 64
46 #define MAX_STRIKES 3
47 #define GAMEOVER_COOLDOWN 2.0f
48
49 typedef struct PlayfieldScores
50 {
51 uint8_t bubbleCount;
52 uint8_t outsideBubbleCount;
53 uint32_t score;
54 } PlayfieldScores;
55
56 typedef struct Playfield
57 {
58 Bubble bubbles[MAX_BUBBLES];
59 Matrix transform;
60 Vector2 fieldSize;
61 float waterLevel;
62 float maxHeight;
63 float minHeight;
64 uint8_t nextBubbleLevel;
65 uint8_t nextBubbleType;
66 // every placed bubble when a bubble is outside the zone counts
67 // as a strike, 3 strikes and it's game over. Strikes are
68 // reset when all bubbles are inside the zone.
69 uint8_t strikes;
70 float gameoverCooldown;
71 uint32_t spawnedBubbleCount;
72 } Playfield;
73
74 typedef struct GameTime
75 {
76 float time;
77 float deltaTime;
78 float fixedTime;
79 float fixedDeltaTime;
80 } GameTime;
81
82 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF}
83 #endif
Technically, it's now a game: menu, game, game over, highscore. It's 17:00. I am now deploying, testing and thinking how to proceed.
Tuturial
It's 18:45. A few people here tested the game and it's obvious that I need a tutorial. Raises a few more questions:
- How can I store the game state? Would be useful to track if the tutorial was done or storing the high scores.
- How do I present the tutorial?
For the storage, adding some highscores is probably most important... It's now 22:00 already and I wasn't very focused. This is the current version:
1 #include "bpop_main.h"
2 #include <string.h>
3 #include <stdlib.h>
4 #include <stdio.h>
5
6 GameScene currentScene = GAME_SCENE_MENU;
7 GameScene nextScene = GAME_SCENE_NONE;
8
9 GameDifficulty gameDifficulty = GAME_DIFFICULTY_NORMAL;
10
11 Storage storage = {
12 .version = 1,
13 };
14
15 int outlineSizeLoc = 0;
16 int outlineColorLoc = 0;
17 int textureSizeLoc = 0;
18
19 Camera3D camera = {
20 .position = { 0.0f, 0.0f, -10.0f },
21 .target = { 0.0f, 0.0f, 0.0f },
22 .up = { 0.0f, 1.0f, 0.0f },
23 .projection = CAMERA_ORTHOGRAPHIC,
24 .fovy = 320.0f,
25 };
26
27
28 Playfield playfield = {
29 .fieldSize = {250, 300},
30 .waterLevel = 200.0f,
31 .minHeight = 90.0f,
32 .maxHeight = 280.0f,
33 };
34
35 GameTime gameTime = {
36 .fixedDeltaTime = 1.0f / 60.0f,
37 };
38
39 Color bubbleTypeColors[] = {
40 COLOR_FROM_HEX(0xaaccff),
41 COLOR_FROM_HEX(0xaa7733),
42 COLOR_FROM_HEX(0x33ffaa),
43 COLOR_FROM_HEX(0xff33aa),
44 COLOR_FROM_HEX(0xaa33ff),
45 };
46
47 Model bubbleModel;
48 Shader bubbleOutlineShader;
49 RenderTexture2D bubbleFieldTexture;
50
51 float BubbleLevelRadius(int level)
52 {
53 return powf((level + 1) * 20, .75f);
54 }
55
56 int Button(const char *text, Vector2 position, Vector2 size)
57 {
58 int result = 0;
59 Rectangle rect = {position.x, position.y, size.x, size.y};
60 if (CheckCollisionPointRec(GetMousePosition(), rect))
61 {
62 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON))
63 {
64 result = 1;
65 }
66 }
67 DrawRectangleRec(rect, (Color){200, 200, 200, 255});
68 DrawRectangleLinesEx(rect, 2, BLACK);
69 float width = MeasureText(text, 20);
70 DrawText(text, position.x + size.x * 0.5f - width * 0.5f, position.y + 10, 20, BLACK);
71 return result;
72 }
73
74 void PlayfieldFixedUpdate(Playfield *playfield)
75 {
76 for (int i = 0; i < MAX_BUBBLES; i++)
77 {
78 Bubble *bubble = &playfield->bubbles[i];
79 bubble->sameLevelContact = 0;
80 }
81
82 for (int i = 0; i < MAX_BUBBLES; i++)
83 {
84 Bubble *bubble = &playfield->bubbles[i];
85 if (!bubble->flagIsActive) continue;
86 float r = bubble->radius;
87
88 for (int j = i + 1; j < MAX_BUBBLES; j++)
89 {
90 Bubble *other = &playfield->bubbles[j];
91 if (!other->flagIsActive) continue;
92 float otherR = other->radius;
93 float sumR2 = (r + otherR) * (r + otherR);
94 float d2 = Vector2DistanceSqr(bubble->position, other->position);
95 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType;
96 if (d2 < sumR2 * 1.05f)
97 {
98 if (canMerge)
99 {
100 bubble->sameLevelContact = 1;
101 other->sameLevelContact = 1;
102 }
103 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f
104 && other->bubbleMergeCooldown <= 0.0f)
105 {
106 // merge bubbles
107 bubble->bubbleLevel++;
108 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
109 bubble->bubbleMergeCooldown = 1.0f;
110 other->flagIsActive = 0;
111 }
112 }
113
114 if (d2 < sumR2)
115 {
116 float overlap = r + otherR - sqrtf(d2);
117 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
118 // resolve overlap by moving the bubbles apart
119 const float errorCorrection = 0.25f;
120 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
121 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
122
123 // bounce off each other
124 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
125 float dot = Vector2DotProduct(relativeVelocity, normal);
126 if (dot > 0.0f)
127 {
128 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000));
129 float impulse = -dot * 0.85f;
130 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
131 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
132 }
133 }
134 }
135
136 if (!bubble->sameLevelContact)
137 {
138 bubble->bubbleMergeCooldown = 1.0f;
139 }
140 else
141 {
142 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
143 }
144
145 float buoyancy = -20.0f;
146 if (bubble->position.y < playfield->waterLevel)
147 {
148 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
149 }
150
151 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius ||
152 bubble->position.y > playfield->maxHeight - bubble->radius;
153 bubble->lifeTime += gameTime.fixedDeltaTime;
154 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f;
155
156 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, buoyancy});
157 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
158 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
159 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
160 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
161 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
162 {
163 bubble->velocity.x *= -0.9f;
164 }
165
166 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
167 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
168
169 // debug velocity
170 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000));
171 }
172
173 int outsideCount = 0;
174 for (int i = 0; i < MAX_BUBBLES; i++)
175 {
176 Bubble *bubble = &playfield->bubbles[i];
177 if (bubble->isOutsideZone)
178 {
179 outsideCount++;
180 }
181 }
182
183 if (outsideCount == 0)
184 {
185 playfield->strikes = 0;
186 playfield->gameoverCooldown = 0.0f;
187 }
188 if (playfield->strikes >= MAX_STRIKES)
189 {
190 playfield->gameoverCooldown += gameTime.fixedDeltaTime;
191 if (playfield->gameoverCooldown > GAMEOVER_COOLDOWN)
192 {
193 nextScene = GAME_SCENE_GAMEOVER;
194 }
195 }
196 }
197
198 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
199 {
200 if (playfield->strikes >= MAX_STRIKES) return;
201
202 for (int i = 0; i < MAX_BUBBLES; i++)
203 {
204 Bubble *bubble = &playfield->bubbles[i];
205 if (!bubble->flagIsActive)
206 {
207 bubble->flagIsActive = 1;
208 bubble->position = position;
209 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
210 bubble->bubbleType = playfield->nextBubbleType;
211 bubble->bubbleLevel = playfield->nextBubbleLevel;
212 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
213 bubble->lifeTime = 0.0f;
214 playfield->spawnedBubbleCount++;
215
216 for (int j = 0; j < MAX_BUBBLES; j += 1)
217 {
218 Bubble *other = &playfield->bubbles[j];
219 if (!other->flagIsActive) continue;
220 if (other->isOutsideZone)
221 {
222 playfield->strikes++;
223 break;
224 }
225 }
226
227 playfield->nextBubbleType = GetRandomValue(0, gameDifficulty);
228 playfield->nextBubbleLevel = GetRandomValue(0, 3);
229 break;
230 }
231 }
232
233 }
234
235 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield)
236 {
237 PlayfieldScores scores = {0};
238 for (int i = 0; i < MAX_BUBBLES; i++)
239 {
240 Bubble *bubble = &playfield->bubbles[i];
241 if (bubble->flagIsActive)
242 {
243 scores.bubbleCount++;
244 uint32_t bubbleScore = 1 << bubble->bubbleLevel;
245 scores.score += bubbleScore;
246
247 if (bubble->isOutsideZone)
248 {
249 scores.outsideBubbleCount++;
250 }
251 }
252 }
253 scores.score += playfield->spawnedBubbleCount;
254 return scores;
255 }
256
257 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
258 {
259 Vector2 spawnPosition = position;
260 spawnPosition.y = BubbleLevelRadius(5);
261 return spawnPosition;
262 }
263
264 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
265 {
266 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
267 cursorPosition.x += playfield->fieldSize.x / 2;
268 cursorPosition.y += playfield->fieldSize.y / 2;
269
270 Vector2 pos = {cursorPosition.x, cursorPosition.y};
271 return PlayfieldPositionToSpawnPosition(playfield, pos);
272 }
273
274 void DrawBubble(Vector3 position, int level, Color color)
275 {
276 float bubbleExtraRadius = 5.0f;
277 float r = BubbleLevelRadius(level) + bubbleExtraRadius;
278 DrawModel(bubbleModel, position, r, color);
279 if (level < 1) return;
280 position.z -= r;
281 float tinyR = level < 6 ? 2 : 4;
282 int count = level < 6 ? level : level - 5;
283 for (int i = 0; i < count; i++)
284 {
285 float ang = (i * 25.0f + 30.0f) * DEG2RAD;
286 float offsetR = i % 2 == 0 ? 0.4f : 0.7f;
287 Vector3 offset = {cosf(ang) * offsetR * r, sinf(ang) * offsetR * r, 0};
288 DrawModel(bubbleModel, Vector3Add(position, offset), tinyR, WHITE);
289 }
290 }
291
292 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera)
293 {
294 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff));
295 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
296 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588));
297
298 // cursor bubble
299 if (currentScene == GAME_SCENE_PLAY)
300 {
301 Vector2 mousePos = GetMousePosition();
302 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
303 Vector3 drawPos = (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0};
304 if (playfield->strikes < MAX_STRIKES && drawPos.x >= -playfield->fieldSize.x * 0.5f && drawPos.x <= playfield->fieldSize.x * 0.5f)
305 {
306 DrawBubble(drawPos, playfield->nextBubbleLevel, bubbleTypeColors[playfield->nextBubbleType]);
307 }
308 }
309
310 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588));
311 rlPushMatrix();
312 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
313 // draw bubbles into playfield space
314 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f;
315 for (int i = 0; i < MAX_BUBBLES; i++)
316 {
317 Bubble *bubble = &playfield->bubbles[i];
318 if (!bubble->flagIsActive) continue;
319 Vector3 position = {bubble->position.x, bubble->position.y, 0};
320 Color bubbleColor = bubbleTypeColors[bubble->bubbleType];
321 int isOutsideZone = bubble->isOutsideZone;
322
323 if (isOutsideZone)
324 {
325 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink);
326 }
327 // lazy: encode id into rgb values
328 bubbleColor.r = bubbleColor.r - i * 8 % 32;
329 bubbleColor.g = bubbleColor.g - (i * 8 / 32) % 32;
330 bubbleColor.b = bubbleColor.b - (i * 8 / 512) % 32;
331 bubbleColor.a = 255;
332 DrawBubble(position, bubble->bubbleLevel, bubbleColor);
333 }
334 rlPopMatrix();
335 }
336
337 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera)
338 {
339 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400);
340 int divides = 10;
341 float divWidth = playfield->fieldSize.x / divides;
342 for (int i = 0; i < divides; i+=2)
343 {
344 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f;
345 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
346 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
347 }
348 }
349
350 void PlayfieldFullDraw(Playfield *playfield, Camera3D camera)
351 {
352 BeginDrawing();
353
354 ClearBackground(COLOR_FROM_HEX(0x4488cc));
355
356 BeginTextureMode(bubbleFieldTexture);
357 rlSetClipPlanes(-128.0f, 128.0f);
358 BeginMode3D(camera);
359
360 ClearBackground(BLANK);
361 PlayfieldDrawBubbles(playfield, camera);
362 EndMode3D();
363 EndTextureMode();
364
365 float outlineSize = 1.0f;
366 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
367 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height };
368
369 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT);
370 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4);
371 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2);
372
373 rlDisableDepthMask();
374 BeginShaderMode(bubbleOutlineShader);
375 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width,
376 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()},
377 (Vector2){0, 0}, 0.0f, WHITE);
378 EndShaderMode();
379 rlEnableDepthMask();
380
381 BeginMode3D(camera);
382 PlayfieldDrawRange(playfield, camera);
383 EndMode3D();
384
385 const char *difficultyText = "Tutorial";
386 switch (gameDifficulty)
387 {
388 case GAME_DIFFICULTY_EASY: difficultyText = "Easy"; break;
389 case GAME_DIFFICULTY_NORMAL: difficultyText = "Normal"; break;
390 case GAME_DIFFICULTY_HARD: difficultyText = "Hard"; break;
391 default:
392 break;
393 }
394 const char *modeText = TextFormat("Mode: %s", difficultyText);
395 int screenWidth = GetScreenWidth();
396 int x = screenWidth - 215;
397 DrawText(modeText, x, 10, 20, WHITE);
398 DifficultyScores table = storage.scores[gameDifficulty];
399 for (int i = 0; i < 8; i++)
400 {
401 HighscoreEntry entry = table.highscores[i];
402 if (entry.score == 0) break;
403 char buffer[64];
404 sprintf(buffer, "%d:", i + 1);
405 int y = 40 + i * 30;
406 DrawText(buffer, x + 18 - MeasureText(buffer, 20), y, 20, WHITE);
407 sprintf(buffer, "%d", entry.score);
408 DrawText(buffer, x + 55 - MeasureText(buffer, 20) / 2, y, 20, WHITE);
409 sprintf(buffer, "%s", entry.date);
410 DrawText(buffer, screenWidth - 15 - MeasureText(buffer, 20), y, 20, WHITE);
411 }
412 }
413
414 void UpdateSceneGameOver()
415 {
416 if (IsKeyPressed(KEY_ENTER))
417 {
418 nextScene = GAME_SCENE_MENU;
419 }
420 // Draw
421 //----------------------------------------------------------------------------------
422
423 BeginDrawing();
424
425 ClearBackground(COLOR_FROM_HEX(0x4488cc));
426
427 PlayfieldFullDraw(&playfield, camera);
428
429 DrawText("Game Over", 20, 20, 35, WHITE);
430
431 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
432 DrawText(TextFormat("Final Score: %d", scores.score), 20, 90, 20, WHITE);
433
434 if (Button("Restart", (Vector2){20, 130}, (Vector2){100, 50}))
435 {
436 nextScene = GAME_SCENE_MENU;
437 }
438 EndDrawing();
439 }
440
441 void UpdateScenePlay()
442 {
443 if (IsKeyPressed(KEY_ESCAPE))
444 {
445 nextScene = GAME_SCENE_MENU;
446 }
447
448 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && playfield.strikes < MAX_STRIKES)
449 {
450 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
451 GetMousePosition());
452 if (pos.y >= 0.0f && pos.y <= playfield.fieldSize.y
453 && pos.x >= 0.0f && pos.x <= playfield.fieldSize.x)
454 {
455 PlayfieldTryAddBubble(&playfield, pos);
456 }
457 }
458
459 while (gameTime.fixedTime < gameTime.time)
460 {
461 gameTime.fixedTime += gameTime.fixedDeltaTime;
462 PlayfieldFixedUpdate(&playfield);
463 }
464
465 // Draw
466 //----------------------------------------------------------------------------------
467
468 PlayfieldFullDraw(&playfield, camera);
469
470 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
471 DrawText(TextFormat("Score: %d", scores.score), 10, 10, 20, WHITE);
472 DrawText(TextFormat("Bubbles: %d", scores.bubbleCount), 10, 40, 20, WHITE);
473 DrawText(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, 70, 20, WHITE);
474 DrawText(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, 100, 20, WHITE);
475 DrawText(TextFormat("Strikes: %d", playfield.strikes), 10, 130, 20, WHITE);
476 DrawText(TextFormat("Gameover in: %.1f", GAMEOVER_COOLDOWN - playfield.gameoverCooldown), 10, 160, 20, WHITE);
477
478 EndDrawing();
479 }
480
481 void UpdateSceneMenu()
482 {
483 // Draw
484 //----------------------------------------------------------------------------------
485
486 BeginDrawing();
487
488 ClearBackground(COLOR_FROM_HEX(0x4488cc));
489
490 DrawText("Bubble Pop", GetScreenWidth() / 2 - MeasureText("Bubble Pop", 40) / 2, 10, 40, WHITE);
491
492 if (Button("Play easy", (Vector2){GetScreenWidth() / 2 - 100, 100}, (Vector2){200, 50}))
493 {
494 gameDifficulty = GAME_DIFFICULTY_EASY;
495 nextScene = GAME_SCENE_PLAY;
496 }
497
498 if (Button("Play normal", (Vector2){GetScreenWidth() / 2 - 100, 160}, (Vector2){200, 50}))
499 {
500 gameDifficulty = GAME_DIFFICULTY_NORMAL;
501 nextScene = GAME_SCENE_PLAY;
502 }
503
504 if (Button("Play hard", (Vector2){GetScreenWidth() / 2 - 100, 220}, (Vector2){200, 50}))
505 {
506 gameDifficulty = GAME_DIFFICULTY_HARD;
507 nextScene = GAME_SCENE_PLAY;
508 }
509
510 EndDrawing();
511 }
512 #if defined(PLATFORM_WEB)
513 #include <emscripten.h>
514
515 // Function to store data in Local Storage
516 void StoreData(const char *key, const char *value) {
517 EM_ASM_({
518 localStorage.setItem(UTF8ToString($0), UTF8ToString($1));
519 }, key, value);
520 }
521
522 // Function to retrieve data from Local Storage
523 const char* RetrieveData(const char *key) {
524 return (const char*)EM_ASM_INT({
525 var value = localStorage.getItem(UTF8ToString($0));
526 if (value === null) {
527 return 0;
528 }
529 var lengthBytes = lengthBytesUTF8(value) + 1;
530 var stringOnWasmHeap = _malloc(lengthBytes);
531 stringToUTF8(value, stringOnWasmHeap, lengthBytes);
532 return stringOnWasmHeap;
533 }, key);
534 }
535 #else
536 void StoreData(const char *key, const char *value) {}
537 const char* RetrieveData(const char *key) { return 0; }
538 #endif
539
540 uint32_t RetrieveUInt32(const char *key)
541 {
542 const char *value = RetrieveData(key);
543 if (value)
544 {
545 uint32_t result = atoi(value);
546 free((void*)value);
547 return result;
548 }
549 return 0;
550 }
551
552 void StoreUInt32(const char *key, uint32_t value)
553 {
554 char buffer[16];
555 sprintf(buffer, "%d", value);
556 StoreData(key, buffer);
557 }
558
559 void RetrieveFixedChar(const char *key, char *buffer, int size)
560 {
561 const char *value = RetrieveData(key);
562 if (value)
563 {
564 strncpy(buffer, value, size);
565 free((void*)value);
566 }
567 else
568 {
569 buffer[0] = '\0';
570 }
571 }
572
573 void StoreFixedChar(const char *key, const char *value)
574 {
575 StoreData(key, value);
576 }
577
578
579 void LoadStorage()
580 {
581 // ignore version as this is first version. Upgrades need to be backwards compatible
582 for (int i = 0; i < 4; i++)
583 {
584 for (int j = 0; j < 8; j++)
585 {
586 char key[64];
587 sprintf(key, "storage.highscore_%d_%d", i, j);
588 storage.scores[i].highscores[j].score = RetrieveUInt32(key);
589 sprintf(key, "storage.name_%d_%d", i, j);
590 RetrieveFixedChar(key, storage.scores[i].highscores[j].name, 16);
591 sprintf(key, "storage.date_%d_%d", i, j);
592 RetrieveFixedChar(key, storage.scores[i].highscores[j].date, 16);
593 }
594 }
595 storage.tutorialStep = RetrieveUInt32("storage.tutorialStep");
596 storage.tutorialStepCount = RetrieveUInt32("storage.tutorialStepCount");
597 storage.tutorialCompleted = RetrieveUInt32("storage.tutorialCompleted");
598 storage.startups = RetrieveUInt32("storage.startups");
599 StoreUInt32("storage.startups", storage.startups + 1);
600 }
601
602 void SaveStorage()
603 {
604 StoreUInt32("storage.version", 1);
605 for (int i = 0; i < 4; i++)
606 {
607 for (int j = 0; j < 8; j++)
608 {
609 char key[64];
610 sprintf(key, "storage.highscore_%d_%d", i, j);
611 StoreUInt32(key, storage.scores[i].highscores[j].score);
612 sprintf(key, "storage.name_%d_%d", i, j);
613 StoreFixedChar(key, storage.scores[i].highscores[j].name);
614 sprintf(key, "storage.date_%d_%d", i, j);
615 StoreFixedChar(key, storage.scores[i].highscores[j].date);
616 }
617 }
618 StoreUInt32("storage.tutorialStep", storage.tutorialStep);
619 StoreUInt32("storage.tutorialStepCount", storage.tutorialStepCount);
620 StoreUInt32("storage.tutorialCompleted", storage.tutorialCompleted);
621 }
622
623 void LogStorage()
624 {
625 TraceLog(LOG_INFO, "[storage] Storage log");
626 for (int i = 0; i < 4; i++)
627 {
628 for (int j = 0; j < 8; j++)
629 {
630 TraceLog(LOG_INFO, "[storage] Highscore %d %d: %d %s %s", i, j, storage.scores[i].highscores[j].score,
631 storage.scores[i].highscores[j].name, storage.scores[i].highscores[j].date);
632 }
633 }
634 TraceLog(LOG_INFO, "[storage] Tutorial: %d %d %d", storage.tutorialStep, storage.tutorialStepCount, storage.tutorialCompleted);
635 TraceLog(LOG_INFO, "[storage] Startups: %d", storage.startups);
636 }
637
638 #include <time.h>
639 void StoreHighScore(PlayfieldScores scores)
640 {
641 DifficultyScores *difficultyScores = &storage.scores[gameDifficulty];
642
643 for (int i = 0; i < 8; i++)
644 {
645 if (scores.score > difficultyScores->highscores[i].score)
646 {
647 for (int j = 7; j > i; j--)
648 {
649 difficultyScores->highscores[j] = difficultyScores->highscores[j - 1];
650 }
651 difficultyScores->highscores[i].score = scores.score;
652 time_t now = time(0);
653 struct tm *tm = localtime(&now);
654 strftime(difficultyScores->highscores[i].date, 16, "%Y-%m-%d", tm);
655 difficultyScores->highscores[i].name[0] = '\0';
656 SaveStorage();
657 break;
658 }
659 }
660 }
661
662 int main(void)
663 {
664 // Initialization
665 //--------------------------------------------------------------------------------------
666 const int screenWidth = 800;
667 const int screenHeight = 450;
668
669 InitWindow(screenWidth, screenHeight, "GGJ25 - Bubble Pop");
670
671 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
672 //--------------------------------------------------------------------------------------
673
674 TraceLog(LOG_INFO, "loading shaders");
675 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs");
676 // Get shader locations
677 outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize");
678 outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor");
679 textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize");
680
681 LoadStorage();
682 LogStorage();
683
684 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
685 // Main game loop
686 while (!WindowShouldClose()) // Detect window close button or ESC key
687 {
688 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight())
689 {
690 UnloadRenderTexture(bubbleFieldTexture);
691 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
692 }
693
694 float dt = GetFrameTime();
695 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
696 if (dt > 0.2f) dt = 0.2f;
697 gameTime.time += dt;
698 gameTime.deltaTime = dt;
699
700 switch (currentScene)
701 {
702 default: UpdateSceneMenu(); break;
703 case GAME_SCENE_PLAY: UpdateScenePlay(); break;
704 case GAME_SCENE_GAMEOVER: UpdateSceneGameOver(); break;
705 }
706
707 switch (nextScene)
708 {
709 case GAME_SCENE_NONE: break;
710 default: currentScene = nextScene; break;
711 case GAME_SCENE_GAMEOVER:
712 StoreHighScore(CalculatePlayfieldScores(&playfield));
713 currentScene = GAME_SCENE_GAMEOVER;
714 break;
715 case GAME_SCENE_PLAY:
716 playfield = (Playfield){
717 .fieldSize = {230, 300},
718 .waterLevel = 200.0f,
719 .minHeight = 90.0f,
720 .maxHeight = 280.0f,
721 };
722 currentScene = GAME_SCENE_PLAY; break;
723 }
724
725 nextScene = GAME_SCENE_NONE;
726 }
727
728 // De-Initialization
729 //--------------------------------------------------------------------------------------
730 CloseWindow(); // Close window and OpenGL context
731 //--------------------------------------------------------------------------------------
732
733 return 0;
734 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8 #include <math.h>
9
10 typedef enum GameDifficulty
11 {
12 GAME_DIFFICULTY_TUTORIAL,
13 GAME_DIFFICULTY_EASY,
14 GAME_DIFFICULTY_NORMAL,
15 GAME_DIFFICULTY_HARD,
16 } GameDifficulty;
17
18 typedef struct HighscoreEntry
19 {
20 uint32_t score;
21 char name[16];
22 char date[16];
23 } HighscoreEntry;
24
25 typedef struct DifficultyScores
26 {
27 HighscoreEntry highscores[8];
28 } DifficultyScores;
29
30
31 typedef struct Storage
32 {
33 uint32_t version;
34 uint32_t startups;
35 DifficultyScores scores[4];
36 uint32_t tutorialStep;
37 uint32_t tutorialStepCount;
38 uint32_t tutorialCompleted;
39 } Storage;
40
41 typedef enum GameScene
42 {
43 GAME_SCENE_NONE,
44 GAME_SCENE_MENU,
45 GAME_SCENE_DIFFICULTY_SELECT,
46 GAME_SCENE_PLAY,
47 GAME_SCENE_GAMEOVER,
48 GAME_SCENE_HIGHSCORES,
49 } GameScene;
50
51
52 #define MAX_BUBBLE_TYPES 3
53
54 typedef struct Bubble
55 {
56 uint8_t flagIsActive:1;
57 uint8_t sameLevelContact:1;
58 uint8_t isOutsideZone:1;
59 uint8_t bubbleType:3;
60 uint8_t bubbleLevel;
61
62 Vector2 position;
63 Vector2 velocity;
64 float bubbleMergeCooldown;
65 float radius;
66 float lifeTime;
67 } Bubble;
68
69 #define MAX_BUBBLES 64
70 #define MAX_STRIKES 3
71 #define GAMEOVER_COOLDOWN 2.0f
72
73 typedef struct PlayfieldScores
74 {
75 uint8_t bubbleCount;
76 uint8_t outsideBubbleCount;
77 uint32_t score;
78 } PlayfieldScores;
79
80 typedef struct Playfield
81 {
82 Bubble bubbles[MAX_BUBBLES];
83 Matrix transform;
84 Vector2 fieldSize;
85 float waterLevel;
86 float maxHeight;
87 float minHeight;
88 uint8_t nextBubbleLevel;
89 uint8_t nextBubbleType;
90 // every placed bubble when a bubble is outside the zone counts
91 // as a strike, 3 strikes and it's game over. Strikes are
92 // reset when all bubbles are inside the zone.
93 uint8_t strikes;
94 float gameoverCooldown;
95 uint32_t spawnedBubbleCount;
96 } Playfield;
97
98 typedef struct GameTime
99 {
100 float time;
101 float deltaTime;
102 float fixedTime;
103 float fixedDeltaTime;
104 } GameTime;
105
106 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF}
107 #endif
TODO collection of ideas:
- Fish character that has created this game out of boredom
- Bubbles pop on game over screen and fish is sobbing
- Display highscore on the right during game play
1 #include "bpop_main.h"
2 #include <string.h>
3 #include <stdlib.h>
4 #include <stdio.h>
5
6 GameScene currentScene = GAME_SCENE_MENU;
7 GameScene nextScene = GAME_SCENE_NONE;
8
9 GameDifficulty gameDifficulty = GAME_DIFFICULTY_NORMAL;
10
11 Storage storage = {
12 .version = 1,
13 };
14
15 int outlineSizeLoc = 0;
16 int outlineColorLoc = 0;
17 int textureSizeLoc = 0;
18
19 Camera3D camera = {
20 .position = { 0.0f, 0.0f, -10.0f },
21 .target = { 0.0f, 0.0f, 0.0f },
22 .up = { 0.0f, 1.0f, 0.0f },
23 .projection = CAMERA_ORTHOGRAPHIC,
24 .fovy = 320.0f,
25 };
26
27
28 Playfield playfield = {
29 .fieldSize = {250, 300},
30 .waterLevel = 200.0f,
31 .minHeight = 90.0f,
32 .maxHeight = 280.0f,
33 };
34
35 GameTime gameTime = {
36 .fixedDeltaTime = 1.0f / 60.0f,
37 };
38
39 Color bubbleTypeColors[] = {
40 COLOR_FROM_HEX(0xaaccff),
41 COLOR_FROM_HEX(0xaa7733),
42 COLOR_FROM_HEX(0x33ffaa),
43 COLOR_FROM_HEX(0xff33aa),
44 COLOR_FROM_HEX(0xaa33ff),
45 };
46
47 Model bubbleModel;
48 Shader bubbleOutlineShader;
49 RenderTexture2D bubbleFieldTexture;
50
51 int isClickActionBlocked = 0;
52
53 float BubbleLevelRadius(int level)
54 {
55 return powf((level + 1) * 120, .5f);
56 }
57
58 int IsClickActioned()
59 {
60 int result = 0;
61 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !isClickActionBlocked)
62 {
63 result = 1;
64 }
65 return result;
66 }
67
68 int Button(const char *text, Vector2 position, Vector2 size)
69 {
70 int result = 0;
71 Rectangle rect = {position.x, position.y, size.x, size.y};
72 if (CheckCollisionPointRec(GetMousePosition(), rect))
73 {
74 if (IsClickActioned())
75 {
76 result = 1;
77 }
78 }
79 DrawRectangleRec(rect, (Color){200, 200, 200, 255});
80 DrawRectangleLinesEx(rect, 2, BLACK);
81 float width = MeasureText(text, 20);
82 DrawText(text, position.x + size.x * 0.5f - width * 0.5f, position.y + 10, 20, BLACK);
83 return result;
84 }
85
86 void PlayfieldFixedUpdate(Playfield *playfield)
87 {
88 for (int i = 0; i < MAX_BUBBLES; i++)
89 {
90 Bubble *bubble = &playfield->bubbles[i];
91 bubble->sameLevelContact = 0;
92 }
93
94 for (int i = 0; i < MAX_BUBBLES; i++)
95 {
96 Bubble *bubble = &playfield->bubbles[i];
97 if (!bubble->flagIsActive) continue;
98 float r = bubble->radius;
99
100 for (int j = i + 1; j < MAX_BUBBLES; j++)
101 {
102 Bubble *other = &playfield->bubbles[j];
103 if (!other->flagIsActive) continue;
104 float otherR = other->radius;
105 float sumR2 = (r + otherR) * (r + otherR);
106 float d2 = Vector2DistanceSqr(bubble->position, other->position);
107 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType;
108 if (d2 < sumR2 * 1.05f)
109 {
110 if (canMerge)
111 {
112 bubble->sameLevelContact = 1;
113 other->sameLevelContact = 1;
114 }
115 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f
116 && other->bubbleMergeCooldown <= 0.0f)
117 {
118 // merge bubbles
119 bubble->bubbleLevel++;
120 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
121 bubble->bubbleMergeCooldown = 1.0f;
122 other->flagIsActive = 0;
123 }
124 }
125
126 if (d2 < sumR2)
127 {
128 float overlap = r + otherR - sqrtf(d2);
129 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
130 // resolve overlap by moving the bubbles apart
131 const float errorCorrection = 0.25f;
132 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
133 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
134
135 // bounce off each other
136 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
137 float dot = Vector2DotProduct(relativeVelocity, normal);
138 if (dot > 0.0f)
139 {
140 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000));
141 float impulse = -dot * 0.85f;
142 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
143 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
144 }
145 }
146 }
147
148 if (!bubble->sameLevelContact)
149 {
150 bubble->bubbleMergeCooldown = 1.0f;
151 }
152 else
153 {
154 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
155 }
156
157 float buoyancy = -20.0f;
158 if (bubble->position.y < playfield->waterLevel)
159 {
160 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
161 }
162
163 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius ||
164 bubble->position.y > playfield->maxHeight - bubble->radius;
165 bubble->lifeTime += gameTime.fixedDeltaTime;
166 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f;
167
168 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, buoyancy});
169 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
170 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
171 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
172 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
173 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
174 {
175 bubble->velocity.x *= -0.9f;
176 }
177
178 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
179 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
180
181 // debug velocity
182 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000));
183 }
184
185 int outsideCount = 0;
186 for (int i = 0; i < MAX_BUBBLES; i++)
187 {
188 Bubble *bubble = &playfield->bubbles[i];
189 if (bubble->isOutsideZone)
190 {
191 outsideCount++;
192 }
193 }
194
195 if (outsideCount == 0)
196 {
197 playfield->strikes = 0;
198 playfield->gameoverCooldown = 0.0f;
199 }
200 if (playfield->strikes >= MAX_STRIKES)
201 {
202 playfield->gameoverCooldown += gameTime.fixedDeltaTime;
203 if (playfield->gameoverCooldown > GAMEOVER_COOLDOWN)
204 {
205 nextScene = GAME_SCENE_GAMEOVER;
206 }
207 }
208 }
209
210 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
211 {
212 if (playfield->strikes >= MAX_STRIKES) return;
213
214 for (int i = 0; i < MAX_BUBBLES; i++)
215 {
216 Bubble *bubble = &playfield->bubbles[i];
217 if (!bubble->flagIsActive)
218 {
219 bubble->flagIsActive = 1;
220 bubble->position = position;
221 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
222 bubble->bubbleType = playfield->nextBubbleType;
223 bubble->bubbleLevel = playfield->nextBubbleLevel;
224 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
225 bubble->lifeTime = 0.0f;
226 playfield->spawnedBubbleCount++;
227
228 for (int j = 0; j < MAX_BUBBLES; j += 1)
229 {
230 Bubble *other = &playfield->bubbles[j];
231 if (!other->flagIsActive) continue;
232 if (other->isOutsideZone)
233 {
234 playfield->strikes++;
235 break;
236 }
237 }
238
239 playfield->nextBubbleType = GetRandomValue(0, gameDifficulty);
240 playfield->nextBubbleLevel = GetRandomValue(0, 3);
241 break;
242 }
243 }
244
245 }
246
247 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield)
248 {
249 PlayfieldScores scores = {0};
250 for (int i = 0; i < MAX_BUBBLES; i++)
251 {
252 Bubble *bubble = &playfield->bubbles[i];
253 if (bubble->flagIsActive)
254 {
255 scores.bubbleCount++;
256 uint32_t bubbleScore = 1 << bubble->bubbleLevel;
257 scores.score += bubbleScore;
258
259 if (bubble->isOutsideZone)
260 {
261 scores.outsideBubbleCount++;
262 }
263 }
264 }
265 scores.score += playfield->spawnedBubbleCount;
266 return scores;
267 }
268
269 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
270 {
271 Vector2 spawnPosition = position;
272 spawnPosition.y = BubbleLevelRadius(5);
273 return spawnPosition;
274 }
275
276 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
277 {
278 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
279 cursorPosition.x += playfield->fieldSize.x / 2;
280 cursorPosition.y += playfield->fieldSize.y / 2;
281
282 Vector2 pos = {cursorPosition.x, cursorPosition.y};
283 return PlayfieldPositionToSpawnPosition(playfield, pos);
284 }
285
286 void DrawBubble(Vector3 position, int level, Color color)
287 {
288 float bubbleExtraRadius = 5.0f;
289 float r = BubbleLevelRadius(level) + bubbleExtraRadius;
290 DrawModel(bubbleModel, position, r, color);
291 if (level < 1) return;
292 position.z -= r;
293 float tinyR = level < 6 ? 2 : 4;
294 int count = level < 6 ? level : level - 5;
295 for (int i = 0; i < count; i++)
296 {
297 float ang = (i * 25.0f + 30.0f) * DEG2RAD;
298 float offsetR = i % 2 == 0 ? 0.4f : 0.7f;
299 Vector3 offset = {cosf(ang) * offsetR * r, sinf(ang) * offsetR * r, 0};
300 DrawModel(bubbleModel, Vector3Add(position, offset), tinyR, WHITE);
301 }
302 }
303
304 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera)
305 {
306 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff));
307 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
308 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588));
309
310 // cursor bubble
311 if (currentScene == GAME_SCENE_PLAY)
312 {
313 Vector2 mousePos = GetMousePosition();
314 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
315 Vector3 drawPos = (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0};
316 if (playfield->strikes < MAX_STRIKES && drawPos.x >= -playfield->fieldSize.x * 0.5f && drawPos.x <= playfield->fieldSize.x * 0.5f)
317 {
318 DrawBubble(drawPos, playfield->nextBubbleLevel, bubbleTypeColors[playfield->nextBubbleType]);
319 }
320 }
321
322 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588));
323 rlPushMatrix();
324 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
325 // draw bubbles into playfield space
326 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f;
327 for (int i = 0; i < MAX_BUBBLES; i++)
328 {
329 Bubble *bubble = &playfield->bubbles[i];
330 if (!bubble->flagIsActive) continue;
331 Vector3 position = {bubble->position.x, bubble->position.y, 0};
332 Color bubbleColor = bubbleTypeColors[bubble->bubbleType];
333 int isOutsideZone = bubble->isOutsideZone;
334
335 if (isOutsideZone)
336 {
337 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink);
338 }
339 // lazy: encode id into rgb values
340 bubbleColor.r = bubbleColor.r - i * 8 % 32;
341 bubbleColor.g = bubbleColor.g - (i * 8 / 32) % 32;
342 bubbleColor.b = bubbleColor.b - (i * 8 / 512) % 32;
343 bubbleColor.a = 255;
344 DrawBubble(position, bubble->bubbleLevel, bubbleColor);
345 }
346 rlPopMatrix();
347 }
348
349 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera)
350 {
351 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400);
352 int divides = 10;
353 float divWidth = playfield->fieldSize.x / divides;
354 for (int i = 0; i < divides; i+=2)
355 {
356 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f;
357 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
358 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
359 }
360 }
361
362 void PlayfieldFullDraw(Playfield *playfield, Camera3D camera)
363 {
364 ClearBackground(COLOR_FROM_HEX(0x4488cc));
365
366 BeginTextureMode(bubbleFieldTexture);
367 rlSetClipPlanes(-128.0f, 128.0f);
368 BeginMode3D(camera);
369
370 ClearBackground(BLANK);
371 PlayfieldDrawBubbles(playfield, camera);
372 EndMode3D();
373 EndTextureMode();
374
375 float outlineSize = 1.0f;
376 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
377 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height };
378
379 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT);
380 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4);
381 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2);
382
383 rlDisableDepthMask();
384 BeginShaderMode(bubbleOutlineShader);
385 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width,
386 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()},
387 (Vector2){0, 0}, 0.0f, WHITE);
388 EndShaderMode();
389 rlEnableDepthMask();
390
391 BeginMode3D(camera);
392 PlayfieldDrawRange(playfield, camera);
393 EndMode3D();
394
395 const char *difficultyText = "Tutorial";
396 switch (gameDifficulty)
397 {
398 case GAME_DIFFICULTY_EASY: difficultyText = "Easy"; break;
399 case GAME_DIFFICULTY_NORMAL: difficultyText = "Normal"; break;
400 case GAME_DIFFICULTY_HARD: difficultyText = "Hard"; break;
401 default:
402 break;
403 }
404 const char *modeText = TextFormat("Mode: %s", difficultyText);
405 int screenWidth = GetScreenWidth();
406 int x = screenWidth - 215;
407 DrawText(modeText, x, 10, 20, WHITE);
408 DifficultyScores table = storage.scores[gameDifficulty];
409 for (int i = 0; i < 8; i++)
410 {
411 HighscoreEntry entry = table.highscores[i];
412 if (entry.score == 0) break;
413 char buffer[64];
414 sprintf(buffer, "%d:", i + 1);
415 int y = 40 + i * 30;
416 DrawText(buffer, x + 18 - MeasureText(buffer, 20), y, 20, WHITE);
417 sprintf(buffer, "%d", entry.score);
418 DrawText(buffer, x + 55 - MeasureText(buffer, 20) / 2, y, 20, WHITE);
419 sprintf(buffer, "%s", entry.date);
420 DrawText(buffer, screenWidth - 15 - MeasureText(buffer, 20), y, 20, WHITE);
421 }
422 }
423
424 void UpdateSceneGameOver()
425 {
426 if (IsKeyPressed(KEY_ENTER))
427 {
428 nextScene = GAME_SCENE_MENU;
429 }
430 // Draw
431 //----------------------------------------------------------------------------------
432
433 ClearBackground(COLOR_FROM_HEX(0x4488cc));
434
435 PlayfieldFullDraw(&playfield, camera);
436
437 DrawText("Game Over", 20, 20, 35, WHITE);
438
439 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
440 DrawText(TextFormat("Final Score: %d", scores.score), 20, 90, 20, WHITE);
441
442 if (Button("Restart", (Vector2){20, 130}, (Vector2){100, 50}))
443 {
444 nextScene = GAME_SCENE_MENU;
445 }
446 }
447
448 void UpdateScenePlay()
449 {
450 if (IsKeyPressed(KEY_ESCAPE))
451 {
452 nextScene = GAME_SCENE_MENU;
453 }
454
455 if (IsClickActioned() && playfield.strikes < MAX_STRIKES)
456 {
457 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
458 GetMousePosition());
459 if (pos.y >= 0.0f && pos.y <= playfield.fieldSize.y
460 && pos.x >= 0.0f && pos.x <= playfield.fieldSize.x)
461 {
462 PlayfieldTryAddBubble(&playfield, pos);
463 }
464 }
465
466 while (gameTime.fixedTime < gameTime.time)
467 {
468 gameTime.fixedTime += gameTime.fixedDeltaTime;
469 PlayfieldFixedUpdate(&playfield);
470 }
471
472 // Draw
473 //----------------------------------------------------------------------------------
474
475 PlayfieldFullDraw(&playfield, camera);
476
477 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
478 DrawText(TextFormat("Score: %d", scores.score), 10, 10, 20, WHITE);
479 DrawText(TextFormat("Bubbles: %d", scores.bubbleCount), 10, 40, 20, WHITE);
480 DrawText(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, 70, 20, WHITE);
481 DrawText(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, 100, 20, WHITE);
482 DrawText(TextFormat("Strikes: %d", playfield.strikes), 10, 130, 20, WHITE);
483 DrawText(TextFormat("Gameover in: %.1f", GAMEOVER_COOLDOWN - playfield.gameoverCooldown), 10, 160, 20, WHITE);
484 }
485
486 void UpdateSceneMenu()
487 {
488 // Draw
489 //----------------------------------------------------------------------------------
490
491 ClearBackground(COLOR_FROM_HEX(0x4488cc));
492
493 DrawText("Bubble Pop", GetScreenWidth() / 2 - MeasureText("Bubble Pop", 40) / 2, 10, 40, WHITE);
494
495 int y = 100;
496 if (Button("Tutorial", (Vector2){GetScreenWidth() / 2 - 100, y}, (Vector2){200, 50}))
497 {
498 gameDifficulty = GAME_DIFFICULTY_TUTORIAL;
499 nextScene = GAME_SCENE_PLAY;
500 }
501
502 if (Button("Play easy", (Vector2){GetScreenWidth() / 2 - 100, y += 60}, (Vector2){200, 50}))
503 {
504 gameDifficulty = GAME_DIFFICULTY_EASY;
505 nextScene = GAME_SCENE_PLAY;
506 }
507
508 if (Button("Play normal", (Vector2){GetScreenWidth() / 2 - 100, y+=60}, (Vector2){200, 50}))
509 {
510 gameDifficulty = GAME_DIFFICULTY_NORMAL;
511 nextScene = GAME_SCENE_PLAY;
512 }
513
514 if (Button("Play hard", (Vector2){GetScreenWidth() / 2 - 100, y+=60}, (Vector2){200, 50}))
515 {
516 gameDifficulty = GAME_DIFFICULTY_HARD;
517 nextScene = GAME_SCENE_PLAY;
518 }
519
520 if (Button("Settings", (Vector2){GetScreenWidth() / 2 - 100, y+=60}, (Vector2){200, 50}))
521 {
522 nextScene = GAME_SCENE_SETTINGS;
523 }
524 }
525 #if defined(PLATFORM_WEB)
526 #include <emscripten.h>
527
528 // Function to store data in Local Storage
529 void StoreData(const char *key, const char *value) {
530 EM_ASM_({
531 localStorage.setItem(UTF8ToString($0), UTF8ToString($1));
532 }, key, value);
533 }
534
535 // Function to retrieve data from Local Storage
536 const char* RetrieveData(const char *key) {
537 return (const char*)EM_ASM_INT({
538 var value = localStorage.getItem(UTF8ToString($0));
539 if (value === null) {
540 return 0;
541 }
542 var lengthBytes = lengthBytesUTF8(value) + 1;
543 var stringOnWasmHeap = _malloc(lengthBytes);
544 stringToUTF8(value, stringOnWasmHeap, lengthBytes);
545 return stringOnWasmHeap;
546 }, key);
547 }
548 #else
549 void StoreData(const char *key, const char *value) {}
550 const char* RetrieveData(const char *key) { return 0; }
551 #endif
552
553 uint32_t RetrieveUInt32(const char *key)
554 {
555 const char *value = RetrieveData(key);
556 if (value)
557 {
558 uint32_t result = atoi(value);
559 free((void*)value);
560 return result;
561 }
562 return 0;
563 }
564
565 void StoreUInt32(const char *key, uint32_t value)
566 {
567 char buffer[16];
568 sprintf(buffer, "%d", value);
569 StoreData(key, buffer);
570 }
571
572 void RetrieveFixedChar(const char *key, char *buffer, int size)
573 {
574 const char *value = RetrieveData(key);
575 if (value)
576 {
577 strncpy(buffer, value, size);
578 free((void*)value);
579 }
580 else
581 {
582 buffer[0] = '\0';
583 }
584 }
585
586 void StoreFixedChar(const char *key, const char *value)
587 {
588 StoreData(key, value);
589 }
590
591
592 void LoadStorage()
593 {
594 // ignore version as this is first version. Upgrades need to be backwards compatible
595 for (int i = 0; i < 4; i++)
596 {
597 for (int j = 0; j < 8; j++)
598 {
599 char key[64];
600 sprintf(key, "storage.highscore_%d_%d", i, j);
601 storage.scores[i].highscores[j].score = RetrieveUInt32(key);
602 sprintf(key, "storage.name_%d_%d", i, j);
603 RetrieveFixedChar(key, storage.scores[i].highscores[j].name, 16);
604 sprintf(key, "storage.date_%d_%d", i, j);
605 RetrieveFixedChar(key, storage.scores[i].highscores[j].date, 16);
606 }
607 }
608 storage.tutorialStep = RetrieveUInt32("storage.tutorialStep");
609 storage.tutorialStepCount = RetrieveUInt32("storage.tutorialStepCount");
610 storage.tutorialCompleted = RetrieveUInt32("storage.tutorialCompleted");
611 storage.startups = RetrieveUInt32("storage.startups");
612 StoreUInt32("storage.startups", storage.startups + 1);
613 }
614
615 void SaveStorage()
616 {
617 StoreUInt32("storage.version", 1);
618 for (int i = 0; i < 4; i++)
619 {
620 for (int j = 0; j < 8; j++)
621 {
622 char key[64];
623 sprintf(key, "storage.highscore_%d_%d", i, j);
624 StoreUInt32(key, storage.scores[i].highscores[j].score);
625 sprintf(key, "storage.name_%d_%d", i, j);
626 StoreFixedChar(key, storage.scores[i].highscores[j].name);
627 sprintf(key, "storage.date_%d_%d", i, j);
628 StoreFixedChar(key, storage.scores[i].highscores[j].date);
629 }
630 }
631 StoreUInt32("storage.tutorialStep", storage.tutorialStep);
632 StoreUInt32("storage.tutorialStepCount", storage.tutorialStepCount);
633 StoreUInt32("storage.tutorialCompleted", storage.tutorialCompleted);
634 }
635
636 void LogStorage()
637 {
638 TraceLog(LOG_INFO, "[storage] Storage log");
639 for (int i = 0; i < 4; i++)
640 {
641 for (int j = 0; j < 8; j++)
642 {
643 TraceLog(LOG_INFO, "[storage] Highscore %d %d: %d %s %s", i, j, storage.scores[i].highscores[j].score,
644 storage.scores[i].highscores[j].name, storage.scores[i].highscores[j].date);
645 }
646 }
647 TraceLog(LOG_INFO, "[storage] Tutorial: %d %d %d", storage.tutorialStep, storage.tutorialStepCount, storage.tutorialCompleted);
648 TraceLog(LOG_INFO, "[storage] Startups: %d", storage.startups);
649 }
650
651 #include <time.h>
652 void StoreHighScore(PlayfieldScores scores)
653 {
654 DifficultyScores *difficultyScores = &storage.scores[gameDifficulty];
655
656 for (int i = 0; i < 8; i++)
657 {
658 if (scores.score > difficultyScores->highscores[i].score)
659 {
660 for (int j = 7; j > i; j--)
661 {
662 difficultyScores->highscores[j] = difficultyScores->highscores[j - 1];
663 }
664 difficultyScores->highscores[i].score = scores.score;
665 time_t now = time(0);
666 struct tm *tm = localtime(&now);
667 strftime(difficultyScores->highscores[i].date, 16, "%Y-%m-%d", tm);
668 difficultyScores->highscores[i].name[0] = '\0';
669 SaveStorage();
670 break;
671 }
672 }
673 }
674
675 void TutorialBubble(int step, const char *text, int x, int y, int width, int height)
676 {
677 if (playfield.tutorialStep != step) return;
678 Rectangle rect = {x, y, width, height};
679 DrawRectangleRec(rect, (Color){200, 200, 200, 255});
680 DrawRectangleLinesEx(rect, 2, BLACK);
681 float textWidth = MeasureText(text, 20);
682 DrawText(text, x + width * 0.5f - textWidth * 0.5f, y + 10, 20, BLACK);
683 }
684
685 void TutorialSetClicksBlocked(int step, int blocked)
686 {
687 if (playfield.tutorialStep != step) return;
688 isClickActionBlocked = blocked;
689 }
690
691 void TutorialProceedOnClick(int step)
692 {
693 if (playfield.tutorialStep != step) return;
694 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
695 playfield.nextTutorialStep = step + 1;
696 }
697 }
698
699 void TutorialHighlightCircle(int step, int x, int y, int radius)
700 {
701 if (playfield.tutorialStep != step) return;
702 float pulse = sinf(gameTime.time * 10.0f) * 0.25f + 1.0f;
703 DrawCircleLines(x, y, radius * pulse, COLOR_FROM_HEX(0xff0000));
704 DrawCircleLines(x, y, radius * pulse + 0.5f, COLOR_FROM_HEX(0xff0000));
705 DrawCircleLines(x, y, radius * pulse + 1.0f, COLOR_FROM_HEX(0xff0000));
706 }
707
708 void TutorialSpawnBubble(int step, int x, int y, int level, int type, int nextLevel, int nextType)
709 {
710 if (playfield.tutorialStep != step || !playfield.tutorialStepStart) return;
711 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
712 (Vector2){x, y});
713 playfield.nextBubbleLevel = level;
714 playfield.nextBubbleType = type;
715 PlayfieldTryAddBubble(&playfield, pos);
716 playfield.nextBubbleLevel = nextLevel;
717 playfield.nextBubbleType = nextType;
718 }
719
720 void UpdateTutorialSystem_PlayTutorial()
721 {
722 int step = 0;
723
724 TutorialSetClicksBlocked(step, 1);
725 TutorialBubble(step, "Welcome to my fishtank...", 20, 20, 400, 75);
726 TutorialProceedOnClick(step);
727 step++;
728
729 TutorialSetClicksBlocked(step, 1);
730 TutorialBubble(step, "It is very boring here.\nSo I invented this game:", 20, 20, 400, 75);
731 TutorialProceedOnClick(step);
732 step++;
733
734 TutorialSetClicksBlocked(step, 1);
735 TutorialBubble(step, "Please tap on the marked area ...", 20, 20, 400, 75);
736 TutorialProceedOnClick(step);
737 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
738 step++;
739
740 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 1, 0);
741 TutorialBubble(step, "Very good!\nNow do it again!", 20, 20, 400, 75);
742 TutorialProceedOnClick(step);
743 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
744 step++;
745
746 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 1, 0);
747 TutorialBubble(step,
748 "These two bubbles have\n"
749 "the same size and color.\n"
750 "When they touch, they will merge.", 20, 20, 400, 75);
751 TutorialProceedOnClick(step);
752 step++;
753 // finished
754 TutorialSetClicksBlocked(step, 0);
755 }
756
757 void UpdateTutorialSystem()
758 {
759 if (currentScene == GAME_SCENE_PLAY && gameDifficulty == GAME_DIFFICULTY_TUTORIAL)
760 {
761 UpdateTutorialSystem_PlayTutorial();
762 }
763
764 if (playfield.nextTutorialStep != playfield.tutorialStep)
765 {
766 playfield.tutorialStepStart = 1;
767 }
768 else
769 {
770 playfield.tutorialStepStart = 0;
771 }
772 playfield.tutorialStep = playfield.nextTutorialStep;
773 }
774
775 int main(void)
776 {
777 // Initialization
778 //--------------------------------------------------------------------------------------
779 const int screenWidth = 800;
780 const int screenHeight = 450;
781
782 InitWindow(screenWidth, screenHeight, "GGJ25 - Bubble Pop");
783
784 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
785 //--------------------------------------------------------------------------------------
786
787 TraceLog(LOG_INFO, "loading shaders");
788 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs");
789 // Get shader locations
790 outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize");
791 outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor");
792 textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize");
793
794 LoadStorage();
795 LogStorage();
796
797 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
798 // Main game loop
799 while (!WindowShouldClose()) // Detect window close button or ESC key
800 {
801 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight())
802 {
803 UnloadRenderTexture(bubbleFieldTexture);
804 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
805 }
806
807 float dt = GetFrameTime();
808 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
809 if (dt > 0.2f) dt = 0.2f;
810 gameTime.time += dt;
811 gameTime.deltaTime = dt;
812
813 BeginDrawing();
814 switch (currentScene)
815 {
816 default: UpdateSceneMenu(); break;
817 case GAME_SCENE_PLAY: UpdateScenePlay(); break;
818 case GAME_SCENE_GAMEOVER: UpdateSceneGameOver(); break;
819 }
820
821 UpdateTutorialSystem();
822
823 switch (nextScene)
824 {
825 case GAME_SCENE_NONE: break;
826 default: currentScene = nextScene; break;
827 case GAME_SCENE_GAMEOVER:
828 StoreHighScore(CalculatePlayfieldScores(&playfield));
829 currentScene = GAME_SCENE_GAMEOVER;
830 break;
831 case GAME_SCENE_PLAY:
832 playfield = (Playfield){
833 .fieldSize = {230, 300},
834 .waterLevel = 200.0f,
835 .minHeight = 90.0f,
836 .maxHeight = 280.0f,
837 };
838 currentScene = GAME_SCENE_PLAY; break;
839 }
840
841 if (nextScene != GAME_SCENE_NONE)
842 {
843 playfield.tutorialStep = 0;
844 playfield.nextTutorialStep = 0;
845 }
846
847 nextScene = GAME_SCENE_NONE;
848 EndDrawing();
849 }
850
851 // De-Initialization
852 //--------------------------------------------------------------------------------------
853 CloseWindow(); // Close window and OpenGL context
854 //--------------------------------------------------------------------------------------
855
856 return 0;
857 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8 #include <math.h>
9
10 typedef enum GameDifficulty
11 {
12 GAME_DIFFICULTY_TUTORIAL,
13 GAME_DIFFICULTY_EASY,
14 GAME_DIFFICULTY_NORMAL,
15 GAME_DIFFICULTY_HARD,
16 } GameDifficulty;
17
18 typedef struct HighscoreEntry
19 {
20 uint32_t score;
21 char name[16];
22 char date[16];
23 } HighscoreEntry;
24
25 typedef struct DifficultyScores
26 {
27 HighscoreEntry highscores[8];
28 } DifficultyScores;
29
30
31 typedef struct Storage
32 {
33 uint32_t version;
34 uint32_t startups;
35 DifficultyScores scores[4];
36 uint32_t tutorialStep;
37 uint32_t tutorialStepCount;
38 uint32_t tutorialCompleted;
39 } Storage;
40
41 typedef enum GameScene
42 {
43 GAME_SCENE_NONE,
44 GAME_SCENE_MENU,
45 GAME_SCENE_DIFFICULTY_SELECT,
46 GAME_SCENE_PLAY,
47 GAME_SCENE_GAMEOVER,
48 GAME_SCENE_HIGHSCORES,
49 GAME_SCENE_SETTINGS,
50 } GameScene;
51
52
53 #define MAX_BUBBLE_TYPES 3
54
55 typedef struct Bubble
56 {
57 uint8_t flagIsActive:1;
58 uint8_t sameLevelContact:1;
59 uint8_t isOutsideZone:1;
60 uint8_t bubbleType:3;
61 uint8_t bubbleLevel;
62
63 Vector2 position;
64 Vector2 velocity;
65 float bubbleMergeCooldown;
66 float radius;
67 float lifeTime;
68 } Bubble;
69
70 #define MAX_BUBBLES 64
71 #define MAX_STRIKES 3
72 #define GAMEOVER_COOLDOWN 2.0f
73
74 typedef struct PlayfieldScores
75 {
76 uint8_t bubbleCount;
77 uint8_t outsideBubbleCount;
78 uint32_t score;
79 } PlayfieldScores;
80
81 typedef struct Playfield
82 {
83 Bubble bubbles[MAX_BUBBLES];
84 Matrix transform;
85 Vector2 fieldSize;
86 float waterLevel;
87 float maxHeight;
88 float minHeight;
89 uint8_t nextBubbleLevel;
90 uint8_t nextBubbleType;
91 // every placed bubble when a bubble is outside the zone counts
92 // as a strike, 3 strikes and it's game over. Strikes are
93 // reset when all bubbles are inside the zone.
94 uint8_t strikes;
95 uint8_t tutorialStepStart;
96 float gameoverCooldown;
97 uint32_t spawnedBubbleCount;
98 uint32_t tutorialStep;
99 uint32_t nextTutorialStep;
100 } Playfield;
101
102 typedef struct GameTime
103 {
104 float time;
105 float deltaTime;
106 float fixedTime;
107 float fixedDeltaTime;
108 } GameTime;
109
110 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF}
111 #endif
It's 23:30 and I have to go home. I played the game way too much. I balanced the sizes, so the game can be played longer when the bubbles are placed carefully. I have an idea which character will lead the player to the tutorial: A little shrimp!
Tomorrow, I will have to finish the tutorial and add graphics, effects and sounds...