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:
- Air bubbles spawn at the bottom of a water tank
- The player decides the spawn location
- The bubble rises up
- When two bubbles of the same size collide, they merge
- Special: I want soft body physics for the bubbles. That is going to be my personal challenge.
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.