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:

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

  • 💾
  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:

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:

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

🍪