Global Game Jam 2025, Day 1

The topic has been announced two hours ago and it is "Bubble".

After plenty of rejected ideas, I think I want to play safe and do something proven. Johan Peitz's Cosmic Collapse features a mechanic where spheres (planets) fall down and merge with other spheres of the same type.

I think I will try to make something similar but with a few modifications:

Let's get started with that! Note: It is 20:30.

  1 #include "raylib.h"
  2 
  3 int main(void)
  4 {
  5     // Initialization
  6     //--------------------------------------------------------------------------------------
  7     const int screenWidth = 800;
  8     const int screenHeight = 450;
  9 
 10     InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window");
 11 
 12     SetTargetFPS(60);               // Set our game to run at 60 frames-per-second
 13     //--------------------------------------------------------------------------------------
 14 
 15     // Main game loop
 16     while (!WindowShouldClose())    // Detect window close button or ESC key
 17     {
 18         // Update
 19         //----------------------------------------------------------------------------------
 20         // TODO: Update your variables here
 21         //----------------------------------------------------------------------------------
 22 
 23         // Draw
 24         //----------------------------------------------------------------------------------
 25         BeginDrawing();
 26 
 27         ClearBackground(RAYWHITE);
 28 
 29         DrawText("Hello, World!", 190, 200, 20, LIGHTGRAY);
 30 
 31         EndDrawing();
 32         //----------------------------------------------------------------------------------
 33     }
 34 
 35     // De-Initialization
 36     //--------------------------------------------------------------------------------------
 37     CloseWindow();        // Close window and OpenGL context
 38     //--------------------------------------------------------------------------------------
 39 
 40     return 0;
 41 }

Obligatorily hello world to get started. Next: Formatting the code and making bubble spawn on cursor or touch and let them fly up on release.

1 #include <raylib.h> 2 #include <raymath.h> 3 #include <stdint.h> 4 5 Color ColorFromHex(int hexValue) 6 { 7 Color color = {0}; 8 color.r = (hexValue >> 16) & 0xFF; 9 color.g = (hexValue >> 8) & 0xFF; 10 color.b = hexValue & 0xFF; 11 color.a = 0xFF; 12 return color; 13 } 14 15 typedef struct Bubble 16 { 17 uint8_t flagIsActive:1; 18 Vector2 position; 19 Vector2 velocity; 20 float radius; 21 } Bubble; 22 23 #define MAX_BUBBLES 64 24 25 typedef struct Playfield 26 { 27 Bubble bubbles[MAX_BUBBLES]; 28 Vector2 fieldSize; 29 } Playfield; 30 31 typedef struct GameTime 32 { 33 float time; 34 float deltaTime; 35 float fixedTime; 36 float fixedDeltaTime; 37 } GameTime; 38 39 GameTime gameTime = { 40 .fixedDeltaTime = 1.0f / 60.0f, 41 }; 42 43 void PlayfieldFixedUpdate(Playfield *playfield) 44 { 45 for (int i = 0; i < MAX_BUBBLES; i++) 46 { 47 Bubble *bubble = &playfield->bubbles[i]; 48 if (!bubble->flagIsActive) continue; 49 float r = bubble->radius; 50 for (int j = i + 1; j < MAX_BUBBLES; j++) 51 { 52 Bubble *other = &playfield->bubbles[j]; 53 if (!other->flagIsActive) continue; 54 float r2 = other->radius; 55 float d2 = Vector2DistanceSqr(bubble->position, other->position); 56 if (d2 < (r + r2) * (r + r2)) 57 { 58 float overlap = r + r2 - sqrtf(d2); 59 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position)); 60 // resolve overlap by moving the bubbles apart 61 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * 0.5f)); 62 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * 0.5f)); 63 } 64 } 65 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime; 66 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime; 67 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) || 68 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f)) 69 { 70 bubble->velocity.x *= -1; 71 } 72 if ((bubble->position.y < r && bubble->velocity.y < 0.0f) || 73 (bubble->position.y > playfield->fieldSize.y - r && bubble->velocity.y > 0.0f)) 74 { 75 bubble->velocity.y *= -1; 76 } 77 } 78 } 79 80 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position) 81 { 82 for (int i = 0; i < MAX_BUBBLES; i++) 83 { 84 Bubble *bubble = &playfield->bubbles[i]; 85 if (!bubble->flagIsActive) 86 { 87 bubble->flagIsActive = 1; 88 bubble->position = position; 89 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)}; 90 bubble->radius = GetRandomValue(10, 30); 91 break; 92 } 93 } 94 } 95 96 void PlayfieldDraw(Playfield *playfield) 97 { 98 DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, ColorFromHex(0x225588)); 99 for (int i = 0; i < MAX_BUBBLES; i++) 100 { 101 Bubble *bubble = &playfield->bubbles[i]; 102 if (!bubble->flagIsActive) continue; 103 DrawCircleV(bubble->position, bubble->radius, ColorFromHex(0xaaccff)); 104 } 105 }
106 107 int main(void) 108 { 109 // Initialization 110 //-------------------------------------------------------------------------------------- 111 const int screenWidth = 800; 112 const int screenHeight = 450; 113 114 InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window"); 115 116 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
117 //-------------------------------------------------------------------------------------- 118 119 Playfield playfield = { 120 .fieldSize = {300, 300} 121 };
122 123 // Main game loop 124 while (!WindowShouldClose()) // Detect window close button or ESC key 125 {
126 float dt = GetFrameTime(); 127 // clamp dt to prevent large time steps, e.g. when browser tab is inactive 128 if (dt > 0.2f) dt = 0.2f; 129 gameTime.time += dt; 130 gameTime.deltaTime = dt; 131 132 while (gameTime.fixedTime < gameTime.time) 133 { 134 gameTime.fixedTime += gameTime.fixedDeltaTime; 135 PlayfieldFixedUpdate(&playfield); 136 } 137 138 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) 139 { 140 PlayfieldTryAddBubble(&playfield, GetMousePosition()); 141 }
142 // Draw 143 //---------------------------------------------------------------------------------- 144 BeginDrawing(); 145
146 ClearBackground(ColorFromHex(0x4488cc));
147
148 PlayfieldDraw(&playfield); 149
150 151 EndDrawing(); 152 //---------------------------------------------------------------------------------- 153 } 154 155 // De-Initialization 156 //-------------------------------------------------------------------------------------- 157 CloseWindow(); // Close window and OpenGL context 158 //-------------------------------------------------------------------------------------- 159 160 return 0; 161 }

Spawning and moving bubbles kinda works. Simple collisions work too, but no velocity changes from the collision. Wanted to do that, but thinking about it, I think if I want soft bubbles, I should not treat the bubbles as circles, maybe?

Also, I should use the negative gravity and dampen the velocity of the bubbles.

  1 #include <raylib.h>
  2 #include <raymath.h>
  3 #include <stdint.h>
  4 
  5 Color ColorFromHex(int hexValue)
  6 {
  7     Color color = {0};
  8     color.r = (hexValue >> 16) & 0xFF;
  9     color.g = (hexValue >> 8) & 0xFF;
 10     color.b = hexValue & 0xFF;
 11     color.a = 0xFF;
 12     return color;
 13 }
 14 
 15 typedef struct Bubble
 16 {
 17     uint8_t flagIsActive:1;
 18     Vector2 position;
 19     Vector2 velocity;
 20     float radius;
 21 } Bubble;
 22 
 23 #define MAX_BUBBLES 64
 24 
 25 typedef struct Playfield
 26 {
 27     Bubble bubbles[MAX_BUBBLES];
 28     Vector2 fieldSize;
 29 } Playfield;
 30 
 31 typedef struct GameTime
 32 {
 33     float time;
 34     float deltaTime;
 35     float fixedTime;
 36     float fixedDeltaTime;
 37 } GameTime;
 38 
 39 GameTime gameTime = {
 40     .fixedDeltaTime = 1.0f / 60.0f,
 41 };
 42 
 43 void PlayfieldFixedUpdate(Playfield *playfield)
 44 {
 45     for (int i = 0; i < MAX_BUBBLES; i++)
 46     {
 47         Bubble *bubble = &playfield->bubbles[i];
 48         if (!bubble->flagIsActive) continue;
 49         float r = bubble->radius;
 50         for (int j = i + 1; j < MAX_BUBBLES; j++)
 51         {
 52             Bubble *other = &playfield->bubbles[j];
 53             if (!other->flagIsActive) continue;
 54             float r2 = other->radius;
 55             float d2 = Vector2DistanceSqr(bubble->position, other->position);
 56             if (d2 < (r + r2) * (r + r2))
 57             {
 58                 float overlap = r + r2 - sqrtf(d2);
 59                 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
 60                 // resolve overlap by moving the bubbles apart
61 const float errorCorrection = 0.25f; 62 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection)); 63 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection)); 64 65 // bounce off each other 66 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity); 67 float dot = Vector2DotProduct(relativeVelocity, normal); 68 if (dot > 0.0f) 69 { 70 // DrawLineV(bubble->position, other->position, ColorFromHex(0xff0000)); 71 float impulse = -dot * 0.85f; 72 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse)); 73 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse)); 74 }
75 }
76 } 77 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, -20.0f}); 78 bubble->velocity = Vector2Scale(bubble->velocity, 0.98f);
79 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime; 80 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime; 81 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) || 82 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f)) 83 {
84 bubble->velocity.x *= -0.9f;
85 } 86 if ((bubble->position.y < r && bubble->velocity.y < 0.0f) || 87 (bubble->position.y > playfield->fieldSize.y - r && bubble->velocity.y > 0.0f)) 88 {
89 bubble->velocity.y *= -0.9f; 90 } 91 92 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x; 93 bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y; 94 95 // debug velocity 96 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), ColorFromHex(0xff0000));
97 } 98 } 99 100 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position) 101 { 102 for (int i = 0; i < MAX_BUBBLES; i++) 103 { 104 Bubble *bubble = &playfield->bubbles[i]; 105 if (!bubble->flagIsActive) 106 { 107 bubble->flagIsActive = 1; 108 bubble->position = position; 109 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)}; 110 bubble->radius = GetRandomValue(10, 30); 111 break; 112 } 113 } 114 } 115 116 void PlayfieldDraw(Playfield *playfield) 117 { 118 DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, ColorFromHex(0x225588)); 119 for (int i = 0; i < MAX_BUBBLES; i++) 120 { 121 Bubble *bubble = &playfield->bubbles[i]; 122 if (!bubble->flagIsActive) continue;
123 DrawCircleLinesV(bubble->position, bubble->radius, ColorFromHex(0xaaccff));
124 } 125 } 126 127 int main(void) 128 { 129 // Initialization 130 //-------------------------------------------------------------------------------------- 131 const int screenWidth = 800; 132 const int screenHeight = 450; 133 134 InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window"); 135 136 SetTargetFPS(60); // Set our game to run at 60 frames-per-second 137 //-------------------------------------------------------------------------------------- 138 139 Playfield playfield = {
140 .fieldSize = {300, 300} 141 }; 142 143 playfield.bubbles[0] = (Bubble){ 144 .flagIsActive = 1, 145 .position = {50, 100}, 146 .velocity = {20, 0}, 147 .radius = 55 148 }; 149 150 playfield.bubbles[1] = (Bubble){ 151 .flagIsActive = 1, 152 .position = {200, 95}, 153 .velocity = {-20, 0}, 154 .radius = 40
155 }; 156 157 // Main game loop 158 while (!WindowShouldClose()) // Detect window close button or ESC key 159 {
160 float dt = GetFrameTime(); 161 // clamp dt to prevent large time steps, e.g. when browser tab is inactive 162 if (dt > 0.2f) dt = 0.2f; 163 gameTime.time += dt; 164 gameTime.deltaTime = dt; 165
166 167 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) 168 { 169 PlayfieldTryAddBubble(&playfield, GetMousePosition()); 170 } 171 // Draw 172 //----------------------------------------------------------------------------------
173 BeginDrawing(); 174 175 ClearBackground(ColorFromHex(0x4488cc)); 176 177 PlayfieldDraw(&playfield); 178 179 // fixed step update in post draw to allow debug drawing
180 while (gameTime.fixedTime < gameTime.time) 181 { 182 gameTime.fixedTime += gameTime.fixedDeltaTime; 183 PlayfieldFixedUpdate(&playfield); 184 } 185 186 EndDrawing(); 187 //---------------------------------------------------------------------------------- 188 } 189 190 // De-Initialization 191 //-------------------------------------------------------------------------------------- 192 CloseWindow(); // Close window and OpenGL context 193 //-------------------------------------------------------------------------------------- 194 195 return 0; 196 }

Ok, after figuring out why the math was wrong (several wrong signs, of course), The result looks fairly decent for now. Also got me an idea how to handle bubble softness. I think I can fake it by making the physics tolerance for collision resolving high and faking softness in the rendering step. But will try that tomorrow.

Current time is 22:30. One more hour I guess.

Next should be some game rules. The bubbles float up too easily, so small bubbles will always float up and merge with the first bubble they encounter. So having the basic rules in place to test this would be nice.

  1 #include <raylib.h>
  2 #include <raymath.h>
  3 #include <stdint.h>
  4 
  5 Color ColorFromHex(int hexValue)
  6 {
  7     Color color = {0};
  8     color.r = (hexValue >> 16) & 0xFF;
  9     color.g = (hexValue >> 8) & 0xFF;
 10     color.b = hexValue & 0xFF;
 11     color.a = 0xFF;
 12     return color;
 13 }
 14 
15 float BubbleLevelRadius(int level) 16 { 17 return powf(level * 30, .75f); 18 } 19
20 typedef struct Bubble 21 {
22 uint8_t flagIsActive:1; 23 uint8_t sameLevelContact:1; 24 uint8_t bubbleLevel:6;
25 Vector2 position;
26 Vector2 velocity; 27 float bubbleMergeCooldown;
28 float radius; 29 } Bubble; 30 31 #define MAX_BUBBLES 64 32 33 typedef struct Playfield 34 { 35 Bubble bubbles[MAX_BUBBLES]; 36 Vector2 fieldSize; 37 } Playfield; 38 39 typedef struct GameTime 40 { 41 float time; 42 float deltaTime; 43 float fixedTime; 44 float fixedDeltaTime; 45 } GameTime; 46 47 GameTime gameTime = { 48 .fixedDeltaTime = 1.0f / 60.0f, 49 }; 50 51 void PlayfieldFixedUpdate(Playfield *playfield)
52 { 53 for (int i = 0; i < MAX_BUBBLES; i++) 54 { 55 Bubble *bubble = &playfield->bubbles[i]; 56 bubble->sameLevelContact = 0; 57 } 58
59 for (int i = 0; i < MAX_BUBBLES; i++) 60 { 61 Bubble *bubble = &playfield->bubbles[i]; 62 if (!bubble->flagIsActive) continue;
63 float r = bubble->radius; 64
65 for (int j = i + 1; j < MAX_BUBBLES; j++) 66 { 67 Bubble *other = &playfield->bubbles[j]; 68 if (!other->flagIsActive) continue;
69 float otherR = other->radius; 70 float sumR2 = (r + otherR) * (r + otherR);
71 float d2 = Vector2DistanceSqr(bubble->position, other->position);
72 int isSameLevel = bubble->bubbleLevel == other->bubbleLevel; 73 if (d2 < sumR2 * 1.05f)
74 {
75 if (isSameLevel) 76 { 77 bubble->sameLevelContact = 1; 78 other->sameLevelContact = 1; 79 } 80 if (isSameLevel && bubble->bubbleMergeCooldown <= 0.0f 81 && other->bubbleMergeCooldown <= 0.0f) 82 { 83 // merge bubbles 84 bubble->bubbleLevel++; 85 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel); 86 bubble->bubbleMergeCooldown = 1.0f; 87 other->flagIsActive = 0; 88 } 89 } 90 91 if (d2 < sumR2) 92 { 93 float overlap = r + otherR - sqrtf(d2);
94 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position)); 95 // resolve overlap by moving the bubbles apart 96 const float errorCorrection = 0.25f; 97 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection)); 98 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection)); 99 100 // bounce off each other 101 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity); 102 float dot = Vector2DotProduct(relativeVelocity, normal); 103 if (dot > 0.0f) 104 { 105 // DrawLineV(bubble->position, other->position, ColorFromHex(0xff0000)); 106 float impulse = -dot * 0.85f; 107 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
108 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse)); 109 } 110 } 111 } 112 113 if (!bubble->sameLevelContact) 114 { 115 bubble->bubbleMergeCooldown = 1.0f; 116 } 117 else 118 { 119 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
120 } 121 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){0, -20.0f}); 122 bubble->velocity = Vector2Scale(bubble->velocity, 0.98f); 123 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime; 124 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime; 125 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) || 126 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f)) 127 { 128 bubble->velocity.x *= -0.9f; 129 } 130 if ((bubble->position.y < r && bubble->velocity.y < 0.0f) || 131 (bubble->position.y > playfield->fieldSize.y - r && bubble->velocity.y > 0.0f)) 132 { 133 bubble->velocity.y *= -0.9f; 134 } 135 136 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x; 137 bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y; 138 139 // debug velocity 140 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), ColorFromHex(0xff0000)); 141 } 142 } 143 144 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position) 145 { 146 for (int i = 0; i < MAX_BUBBLES; i++) 147 { 148 Bubble *bubble = &playfield->bubbles[i]; 149 if (!bubble->flagIsActive) 150 { 151 bubble->flagIsActive = 1; 152 bubble->position = position; 153 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
154 bubble->bubbleLevel = GetRandomValue(1, 3); 155 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
156 break; 157 } 158 } 159 } 160 161 void PlayfieldDraw(Playfield *playfield) 162 { 163 DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, ColorFromHex(0x225588)); 164 for (int i = 0; i < MAX_BUBBLES; i++) 165 { 166 Bubble *bubble = &playfield->bubbles[i]; 167 if (!bubble->flagIsActive) continue;
168 DrawCircleLinesV(bubble->position, bubble->radius, ColorFromHex(0xaaccff)); 169 const char* bubbleLevel = TextFormat("%d:%.1f", bubble->bubbleLevel, bubble->bubbleMergeCooldown); 170 float width = MeasureText(bubbleLevel, 20); 171 DrawText(bubbleLevel, bubble->position.x - width / 2, bubble->position.y - 10, 20, ColorFromHex(0xaaccff));
172 } 173 }
174 175 int main(void) 176 { 177 // Initialization 178 //-------------------------------------------------------------------------------------- 179 const int screenWidth = 800; 180 const int screenHeight = 450; 181 182 InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window"); 183 184 SetTargetFPS(60); // Set our game to run at 60 frames-per-second 185 //-------------------------------------------------------------------------------------- 186 187 Playfield playfield = { 188 .fieldSize = {300, 300}
189 }; 190 // Main game loop 191 while (!WindowShouldClose()) // Detect window close button or ESC key 192 { 193 float dt = GetFrameTime(); 194 // clamp dt to prevent large time steps, e.g. when browser tab is inactive 195 if (dt > 0.2f) dt = 0.2f; 196 gameTime.time += dt; 197 gameTime.deltaTime = dt; 198 199 200 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) 201 { 202 PlayfieldTryAddBubble(&playfield, GetMousePosition()); 203 } 204 // Draw 205 //---------------------------------------------------------------------------------- 206 BeginDrawing(); 207 208 ClearBackground(ColorFromHex(0x4488cc)); 209 210 PlayfieldDraw(&playfield); 211 212 // fixed step update in post draw to allow debug drawing 213 while (gameTime.fixedTime < gameTime.time) 214 { 215 gameTime.fixedTime += gameTime.fixedDeltaTime; 216 PlayfieldFixedUpdate(&playfield); 217 } 218 219 EndDrawing(); 220 //---------------------------------------------------------------------------------- 221 } 222 223 // De-Initialization 224 //-------------------------------------------------------------------------------------- 225 CloseWindow(); // Close window and OpenGL context 226 //-------------------------------------------------------------------------------------- 227 228 return 0; 229 }

23:30. I think I will call it a day. The testing is actually quite fun.

🍪