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