Global Game Jam 2025, Day 3

It's 10:00 and I have already created a few images for the game. The avatar for the tutorial is a little Shrimp:

Time to see Shrimpy in action:

  • 💾
  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 Texture2D texShrimpy; 16 Texture2D texUiAtlas; 17 Texture2D texGround; 18 Font fontMainMedium; 19 Font fontMainLarge; 20 21 NPatchInfo nPatchButton = { 22 .source = {0,0,240,145}, 23 .left = 10, 24 .top = 10, 25 .right = 10, 26 .bottom = 10, 27 .layout = NPATCH_NINE_PATCH 28 }; 29 Rectangle rectUiAtlasCircle = { 30 .x = 250, 31 .y = 0, 32 .width = 148, 33 .height = 148, 34 }; 35 Rectangle rectUiAtlasShrimpy = { 36 .x = 420, 37 .y = 390, 38 .width = 512-420, 39 .height = 512-390, 40 }; 41
42 int outlineSizeLoc = 0; 43 int outlineColorLoc = 0; 44 int textureSizeLoc = 0; 45 46 Camera3D camera = { 47 .position = { 0.0f, 0.0f, -10.0f }, 48 .target = { 0.0f, 0.0f, 0.0f }, 49 .up = { 0.0f, 1.0f, 0.0f }, 50 .projection = CAMERA_ORTHOGRAPHIC, 51 .fovy = 320.0f, 52 }; 53 54 55 Playfield playfield = { 56 .fieldSize = {250, 300}, 57 .waterLevel = 200.0f, 58 .minHeight = 90.0f, 59 .maxHeight = 280.0f, 60 }; 61 62 GameTime gameTime = { 63 .fixedDeltaTime = 1.0f / 60.0f, 64 }; 65 66 Color bubbleTypeColors[] = { 67 COLOR_FROM_HEX(0xaaccff),
68 COLOR_FROM_HEX(0x2277ff),
69 COLOR_FROM_HEX(0x33ffaa),
70 COLOR_FROM_HEX(0x9933aa),
71 COLOR_FROM_HEX(0xaa33ff), 72 }; 73 74 Model bubbleModel; 75 Shader bubbleOutlineShader; 76 RenderTexture2D bubbleFieldTexture; 77
78 int isClickActionBlocked = 0; 79 80 void SaveStorage(); 81 82 void DrawText2(const char *text, int x, int y, Color color) 83 { 84 DrawTextEx(fontMainMedium, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color); 85 } 86 87 void DrawText3(const char *text, int x, int y, Color color) 88 { 89 DrawTextEx(fontMainLarge, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color); 90 } 91 92 float MeasureText2(const char *text) 93 { 94 return MeasureTextEx(fontMainMedium, text, fontMainMedium.baseSize, 1).x; 95 }
96 97 float BubbleLevelRadius(int level) 98 { 99 return powf((level + 1) * 120, .5f); 100 } 101 102 int IsClickActioned() 103 { 104 int result = 0; 105 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !isClickActionBlocked) 106 { 107 result = 1; 108 } 109 return result; 110 } 111 112 int Button(const char *text, Vector2 position, Vector2 size) 113 {
114 Rectangle rect = {position.x, position.y, size.x, size.y}; 115 int isHovered = !isClickActionBlocked && CheckCollisionPointRec(GetMousePosition(), rect); 116 int result = isHovered && IsClickActioned(); 117 118 Color color = isHovered ? COLOR_FROM_HEX(0xcceeff) : COLOR_FROM_HEX(0xaaccff); 119 if (!isClickActionBlocked && isHovered && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
120 {
121 color = COLOR_FROM_HEX(0xddf8ff);
122 }
123 124 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, color); 125 126 int fontSize = fontMainMedium.baseSize; 127 Vector2 tsize = MeasureTextEx(fontMainMedium, text, fontSize, 1); 128 DrawTextEx(fontMainMedium, text, (Vector2){position.x + size.x * 0.5f - tsize.x * 0.5f, position.y + size.y * 0.5f - tsize.y * 0.5f}, fontSize, 1, BLACK);
129 return result; 130 } 131 132 void PlayfieldFixedUpdate(Playfield *playfield) 133 { 134 for (int i = 0; i < MAX_BUBBLES; i++) 135 { 136 Bubble *bubble = &playfield->bubbles[i]; 137 bubble->sameLevelContact = 0; 138 } 139 140 for (int i = 0; i < MAX_BUBBLES; i++) 141 { 142 Bubble *bubble = &playfield->bubbles[i]; 143 if (!bubble->flagIsActive) continue; 144 float r = bubble->radius; 145 146 for (int j = i + 1; j < MAX_BUBBLES; j++) 147 { 148 Bubble *other = &playfield->bubbles[j]; 149 if (!other->flagIsActive) continue; 150 float otherR = other->radius; 151 float sumR2 = (r + otherR) * (r + otherR); 152 float d2 = Vector2DistanceSqr(bubble->position, other->position); 153 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType; 154 if (d2 < sumR2 * 1.05f) 155 { 156 if (canMerge) 157 { 158 bubble->sameLevelContact = 1; 159 other->sameLevelContact = 1; 160 } 161 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f 162 && other->bubbleMergeCooldown <= 0.0f) 163 { 164 // merge bubbles 165 bubble->bubbleLevel++; 166 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel); 167 bubble->bubbleMergeCooldown = 1.0f; 168 other->flagIsActive = 0; 169 } 170 } 171 172 if (d2 < sumR2) 173 { 174 float overlap = r + otherR - sqrtf(d2); 175 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position)); 176 // resolve overlap by moving the bubbles apart 177 const float errorCorrection = 0.25f; 178 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection)); 179 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection)); 180 181 // bounce off each other 182 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity); 183 float dot = Vector2DotProduct(relativeVelocity, normal); 184 if (dot > 0.0f) 185 { 186 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000)); 187 float impulse = -dot * 0.85f; 188 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse)); 189 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse)); 190 } 191 } 192 } 193 194 if (!bubble->sameLevelContact) 195 { 196 bubble->bubbleMergeCooldown = 1.0f; 197 } 198 else 199 { 200 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime; 201 } 202 203 float buoyancy = -20.0f; 204 if (bubble->position.y < playfield->waterLevel) 205 { 206 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f; 207 } 208 209 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius || 210 bubble->position.y > playfield->maxHeight - bubble->radius; 211 bubble->lifeTime += gameTime.fixedDeltaTime; 212 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f; 213
214 float horizontalV = 0.0f; 215 if (playfield->enableTutorialMerge) { 216 horizontalV = fminf(fmaxf(-10.0f, (playfield->fieldSize.x / 2 - bubble->position.x) * 0.1f), 10.0f); 217 } 218 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){horizontalV, buoyancy});
219 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f); 220 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime; 221 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime; 222 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) || 223 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f)) 224 { 225 bubble->velocity.x *= -0.9f; 226 } 227 228 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x; 229 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y; 230 231 // debug velocity 232 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000)); 233 } 234 235 int outsideCount = 0; 236 for (int i = 0; i < MAX_BUBBLES; i++) 237 { 238 Bubble *bubble = &playfield->bubbles[i]; 239 if (bubble->isOutsideZone) 240 { 241 outsideCount++; 242 } 243 } 244 245 if (outsideCount == 0) 246 { 247 playfield->strikes = 0; 248 playfield->gameoverCooldown = 0.0f; 249 } 250 if (playfield->strikes >= MAX_STRIKES) 251 { 252 playfield->gameoverCooldown += gameTime.fixedDeltaTime; 253 if (playfield->gameoverCooldown > GAMEOVER_COOLDOWN) 254 { 255 nextScene = GAME_SCENE_GAMEOVER; 256 } 257 } 258 } 259 260 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position) 261 { 262 if (playfield->strikes >= MAX_STRIKES) return; 263 264 for (int i = 0; i < MAX_BUBBLES; i++) 265 { 266 Bubble *bubble = &playfield->bubbles[i]; 267 if (!bubble->flagIsActive) 268 { 269 bubble->flagIsActive = 1; 270 bubble->position = position; 271 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)}; 272 bubble->bubbleType = playfield->nextBubbleType; 273 bubble->bubbleLevel = playfield->nextBubbleLevel; 274 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel); 275 bubble->lifeTime = 0.0f; 276 playfield->spawnedBubbleCount++; 277 278 for (int j = 0; j < MAX_BUBBLES; j += 1) 279 { 280 Bubble *other = &playfield->bubbles[j]; 281 if (!other->flagIsActive) continue; 282 if (other->isOutsideZone) 283 { 284 playfield->strikes++; 285 break; 286 } 287 } 288
289 playfield->nextBubbleType = GetRandomValue(0, gameDifficulty > 0 ? gameDifficulty : 1);
290 playfield->nextBubbleLevel = GetRandomValue(0, 3); 291 break; 292 } 293 } 294 295 } 296 297 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield) 298 { 299 PlayfieldScores scores = {0}; 300 for (int i = 0; i < MAX_BUBBLES; i++) 301 { 302 Bubble *bubble = &playfield->bubbles[i]; 303 if (bubble->flagIsActive) 304 { 305 scores.bubbleCount++; 306 uint32_t bubbleScore = 1 << bubble->bubbleLevel; 307 scores.score += bubbleScore; 308 309 if (bubble->isOutsideZone) 310 { 311 scores.outsideBubbleCount++; 312 } 313 } 314 } 315 scores.score += playfield->spawnedBubbleCount; 316 return scores; 317 } 318 319 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position) 320 { 321 Vector2 spawnPosition = position; 322 spawnPosition.y = BubbleLevelRadius(5); 323 return spawnPosition; 324 } 325 326 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition) 327 { 328 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position; 329 cursorPosition.x += playfield->fieldSize.x / 2; 330 cursorPosition.y += playfield->fieldSize.y / 2; 331 332 Vector2 pos = {cursorPosition.x, cursorPosition.y}; 333 return PlayfieldPositionToSpawnPosition(playfield, pos); 334 } 335 336 void DrawBubble(Vector3 position, int level, Color color) 337 { 338 float bubbleExtraRadius = 5.0f; 339 float r = BubbleLevelRadius(level) + bubbleExtraRadius; 340 DrawModel(bubbleModel, position, r, color); 341 if (level < 1) return; 342 position.z -= r; 343 float tinyR = level < 6 ? 2 : 4; 344 int count = level < 6 ? level : level - 5; 345 for (int i = 0; i < count; i++) 346 { 347 float ang = (i * 25.0f + 30.0f) * DEG2RAD; 348 float offsetR = i % 2 == 0 ? 0.4f : 0.7f; 349 Vector3 offset = {cosf(ang) * offsetR * r, sinf(ang) * offsetR * r, 0}; 350 DrawModel(bubbleModel, Vector3Add(position, offset), tinyR, WHITE); 351 } 352 } 353 354 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera) 355 { 356 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff)); 357 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x, 358 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588)); 359 360 // cursor bubble 361 if (currentScene == GAME_SCENE_PLAY) 362 { 363 Vector2 mousePos = GetMousePosition(); 364 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos); 365 Vector3 drawPos = (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0}; 366 if (playfield->strikes < MAX_STRIKES && drawPos.x >= -playfield->fieldSize.x * 0.5f && drawPos.x <= playfield->fieldSize.x * 0.5f) 367 { 368 DrawBubble(drawPos, playfield->nextBubbleLevel, bubbleTypeColors[playfield->nextBubbleType]); 369 } 370 } 371 372 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588)); 373 rlPushMatrix(); 374 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0); 375 // draw bubbles into playfield space 376 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f; 377 for (int i = 0; i < MAX_BUBBLES; i++) 378 { 379 Bubble *bubble = &playfield->bubbles[i]; 380 if (!bubble->flagIsActive) continue; 381 Vector3 position = {bubble->position.x, bubble->position.y, 0}; 382 Color bubbleColor = bubbleTypeColors[bubble->bubbleType]; 383 int isOutsideZone = bubble->isOutsideZone; 384 385 if (isOutsideZone) 386 { 387 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink); 388 } 389 // lazy: encode id into rgb values
390 bubbleColor.r = bubbleColor.r - i * 2 % 4; 391 bubbleColor.g = bubbleColor.g - (i * 2 / 4) % 4; 392 bubbleColor.b = bubbleColor.b - (i * 2 / 16);
393 bubbleColor.a = 255; 394 DrawBubble(position, bubble->bubbleLevel, bubbleColor); 395 } 396 rlPopMatrix(); 397 } 398 399 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera) 400 { 401 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400); 402 int divides = 10; 403 float divWidth = playfield->fieldSize.x / divides; 404 for (int i = 0; i < divides; i+=2) 405 { 406 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f; 407 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor); 408 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor); 409 } 410 } 411 412 void PlayfieldFullDraw(Playfield *playfield, Camera3D camera) 413 { 414 ClearBackground(COLOR_FROM_HEX(0x4488cc)); 415 416 BeginTextureMode(bubbleFieldTexture); 417 rlSetClipPlanes(-128.0f, 128.0f); 418 BeginMode3D(camera); 419 420 ClearBackground(BLANK); 421 PlayfieldDrawBubbles(playfield, camera); 422 EndMode3D(); 423 EndTextureMode(); 424 425 float outlineSize = 1.0f; 426 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; 427 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height }; 428 429 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT); 430 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4); 431 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2); 432 433 rlDisableDepthMask(); 434 BeginShaderMode(bubbleOutlineShader); 435 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width, 436 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()}, 437 (Vector2){0, 0}, 0.0f, WHITE); 438 EndShaderMode(); 439 rlEnableDepthMask(); 440 441 BeginMode3D(camera); 442 PlayfieldDrawRange(playfield, camera); 443 EndMode3D(); 444 445 const char *difficultyText = "Tutorial"; 446 switch (gameDifficulty) 447 { 448 case GAME_DIFFICULTY_EASY: difficultyText = "Easy"; break; 449 case GAME_DIFFICULTY_NORMAL: difficultyText = "Normal"; break; 450 case GAME_DIFFICULTY_HARD: difficultyText = "Hard"; break; 451 default: 452 break; 453 } 454 const char *modeText = TextFormat("Mode: %s", difficultyText); 455 int screenWidth = GetScreenWidth();
456 int x = screenWidth - 220; 457 DrawText2("Highscores", x, 25, WHITE); 458 DrawText2(modeText, x, 55, WHITE);
459 DifficultyScores table = storage.scores[gameDifficulty]; 460 for (int i = 0; i < 8; i++) 461 { 462 HighscoreEntry entry = table.highscores[i]; 463 if (entry.score == 0) break; 464 char buffer[64]; 465 sprintf(buffer, "%d:", i + 1);
466 int y = 110 + i * 30; 467 DrawText2(buffer, x + 18 - MeasureText2(buffer), y, WHITE);
468 sprintf(buffer, "%d", entry.score);
469 DrawText2(buffer, x + 50 - MeasureText2(buffer) / 2, y, WHITE);
470 sprintf(buffer, "%s", entry.date);
471 DrawText2(buffer, screenWidth - 10 - MeasureText2(buffer), y, WHITE); 472 } 473 DrawTexture(texShrimpy, GetScreenWidth() - 200, GetScreenHeight() - 260, WHITE);
474 } 475 476 void UpdateSceneGameOver() 477 { 478 if (IsKeyPressed(KEY_ENTER)) 479 { 480 nextScene = GAME_SCENE_MENU; 481 } 482 // Draw 483 //---------------------------------------------------------------------------------- 484 485 ClearBackground(COLOR_FROM_HEX(0x4488cc)); 486 487 PlayfieldFullDraw(&playfield, camera); 488
489 DrawText3("Game Over", 20, 20, WHITE);
490 491 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
492 DrawText2(TextFormat("Final Score: %d", scores.score), 20, 90, WHITE);
493
494 if (Button("< Menu", (Vector2){20, GetScreenHeight() - 110}, (Vector2){180, 90}))
495 { 496 nextScene = GAME_SCENE_MENU; 497 }
498 } 499
500 501 void UpdateScenePlay() 502 { 503 if (IsKeyPressed(KEY_ESCAPE)) 504 { 505 nextScene = GAME_SCENE_MENU; 506 } 507 508 if (IsClickActioned() && playfield.strikes < MAX_STRIKES) 509 { 510 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera, 511 GetMousePosition()); 512 if (pos.y >= 0.0f && pos.y <= playfield.fieldSize.y 513 && pos.x >= 0.0f && pos.x <= playfield.fieldSize.x) 514 { 515 PlayfieldTryAddBubble(&playfield, pos); 516 } 517 } 518 519 while (gameTime.fixedTime < gameTime.time) 520 { 521 gameTime.fixedTime += gameTime.fixedDeltaTime; 522 PlayfieldFixedUpdate(&playfield); 523 } 524 525 // Draw 526 //---------------------------------------------------------------------------------- 527 528 PlayfieldFullDraw(&playfield, camera); 529 530 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
531 int y = 50; 532 DrawText2("Shrimpy Bubble", 10, 20, WHITE); 533 DrawText2("Merge", 150, 42, WHITE); 534 535 DrawText2(TextFormat("Score: %d", scores.score), 10, y+=30, WHITE); 536 // DrawText2(TextFormat("Bubbles: %d", scores.bubbleCount), 10, y+=30, WHITE); 537 // DrawText2(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, y+=30, WHITE); 538 // DrawText2(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, y+=30, WHITE); 539 DrawText2(TextFormat("Strikes: %d", playfield.strikes), 10, y+=30, 540 playfield.strikes > 0 ? ColorLerp(WHITE, COLOR_FROM_HEX(0xff4433), sinf(gameTime.time * 10.0f)) : WHITE); 541 if (playfield.strikes >= MAX_STRIKES) 542 { 543 DrawText2(TextFormat("Gameover in: %.1fs", GAMEOVER_COOLDOWN - playfield.gameoverCooldown), 10, y+=30, RED); 544 } 545 546 if (Button("< Menu", (Vector2){10, GetScreenHeight() - 100}, (Vector2){150, 80})) 547 { 548 nextScene = GAME_SCENE_MENU; 549 }
550 } 551
552 void UpdateSceneSettings()
553 {
554 ClearBackground(COLOR_FROM_HEX(0x4488cc)); 555 int hCenter = 200; 556 557 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge Settings", 558 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE); 559 560 DrawTexture(texShrimpy, GetScreenWidth() - 400, GetScreenHeight() - 260, WHITE); 561 562 int buttonH = 60; 563 int y = 100; 564 if (Button("Clear data", (Vector2){hCenter - 100, y}, (Vector2){200, buttonH})) 565 { 566 nextScene = GAME_SCENE_MENU; 567 memset(&storage, 0, sizeof(storage)); 568 SaveStorage(); 569 } 570 571 if (Button("< Menu", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH})) 572 { 573 nextScene = GAME_SCENE_MENU; 574 } 575 } 576 577 void UpdateSceneMenu() 578 {
579 ClearBackground(COLOR_FROM_HEX(0x4488cc)); 580
581 int hCenter = 200; 582 583 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge", 584 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE); 585 586 DrawTexture(texShrimpy, GetScreenWidth() - 400, GetScreenHeight() - 260, WHITE); 587 588 int buttonH = 60;
589 int y = 100;
590 if (Button("Tutorial", (Vector2){hCenter - 100, y}, (Vector2){200, buttonH}))
591 { 592 gameDifficulty = GAME_DIFFICULTY_TUTORIAL; 593 nextScene = GAME_SCENE_PLAY; 594 } 595
596 if (Button("Play easy", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH}))
597 { 598 gameDifficulty = GAME_DIFFICULTY_EASY; 599 nextScene = GAME_SCENE_PLAY; 600 } 601
602 if (Button("Play normal", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
603 { 604 gameDifficulty = GAME_DIFFICULTY_NORMAL; 605 nextScene = GAME_SCENE_PLAY; 606 } 607
608 if (Button("Play hard", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
609 { 610 gameDifficulty = GAME_DIFFICULTY_HARD; 611 nextScene = GAME_SCENE_PLAY; 612 } 613
614 if (Button("Settings", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
615 { 616 nextScene = GAME_SCENE_SETTINGS; 617 } 618 } 619 #if defined(PLATFORM_WEB) 620 #include <emscripten.h> 621 622 // Function to store data in Local Storage 623 void StoreData(const char *key, const char *value) { 624 EM_ASM_({ 625 localStorage.setItem(UTF8ToString($0), UTF8ToString($1)); 626 }, key, value); 627 } 628 629 // Function to retrieve data from Local Storage 630 const char* RetrieveData(const char *key) { 631 return (const char*)EM_ASM_INT({ 632 var value = localStorage.getItem(UTF8ToString($0)); 633 if (value === null) { 634 return 0; 635 } 636 var lengthBytes = lengthBytesUTF8(value) + 1; 637 var stringOnWasmHeap = _malloc(lengthBytes); 638 stringToUTF8(value, stringOnWasmHeap, lengthBytes); 639 return stringOnWasmHeap; 640 }, key); 641 } 642 #else 643 void StoreData(const char *key, const char *value) {} 644 const char* RetrieveData(const char *key) { return 0; } 645 #endif 646 647 uint32_t RetrieveUInt32(const char *key) 648 { 649 const char *value = RetrieveData(key); 650 if (value) 651 { 652 uint32_t result = atoi(value); 653 free((void*)value); 654 return result; 655 } 656 return 0; 657 } 658 659 void StoreUInt32(const char *key, uint32_t value) 660 { 661 char buffer[16]; 662 sprintf(buffer, "%d", value); 663 StoreData(key, buffer); 664 } 665 666 void RetrieveFixedChar(const char *key, char *buffer, int size) 667 { 668 const char *value = RetrieveData(key); 669 if (value) 670 { 671 strncpy(buffer, value, size); 672 free((void*)value); 673 } 674 else 675 { 676 buffer[0] = '\0'; 677 } 678 } 679 680 void StoreFixedChar(const char *key, const char *value) 681 { 682 StoreData(key, value); 683 } 684 685 686 void LoadStorage() 687 { 688 // ignore version as this is first version. Upgrades need to be backwards compatible 689 for (int i = 0; i < 4; i++) 690 { 691 for (int j = 0; j < 8; j++) 692 { 693 char key[64]; 694 sprintf(key, "storage.highscore_%d_%d", i, j); 695 storage.scores[i].highscores[j].score = RetrieveUInt32(key); 696 sprintf(key, "storage.name_%d_%d", i, j); 697 RetrieveFixedChar(key, storage.scores[i].highscores[j].name, 16); 698 sprintf(key, "storage.date_%d_%d", i, j); 699 RetrieveFixedChar(key, storage.scores[i].highscores[j].date, 16); 700 } 701 } 702 storage.tutorialStep = RetrieveUInt32("storage.tutorialStep"); 703 storage.tutorialStepCount = RetrieveUInt32("storage.tutorialStepCount"); 704 storage.tutorialCompleted = RetrieveUInt32("storage.tutorialCompleted"); 705 storage.startups = RetrieveUInt32("storage.startups"); 706 StoreUInt32("storage.startups", storage.startups + 1); 707 } 708 709 void SaveStorage() 710 { 711 StoreUInt32("storage.version", 1); 712 for (int i = 0; i < 4; i++) 713 { 714 for (int j = 0; j < 8; j++) 715 { 716 char key[64]; 717 sprintf(key, "storage.highscore_%d_%d", i, j); 718 StoreUInt32(key, storage.scores[i].highscores[j].score); 719 sprintf(key, "storage.name_%d_%d", i, j); 720 StoreFixedChar(key, storage.scores[i].highscores[j].name); 721 sprintf(key, "storage.date_%d_%d", i, j); 722 StoreFixedChar(key, storage.scores[i].highscores[j].date); 723 } 724 } 725 StoreUInt32("storage.tutorialStep", storage.tutorialStep); 726 StoreUInt32("storage.tutorialStepCount", storage.tutorialStepCount); 727 StoreUInt32("storage.tutorialCompleted", storage.tutorialCompleted); 728 } 729 730 void LogStorage() 731 { 732 TraceLog(LOG_INFO, "[storage] Storage log"); 733 for (int i = 0; i < 4; i++) 734 { 735 for (int j = 0; j < 8; j++) 736 { 737 TraceLog(LOG_INFO, "[storage] Highscore %d %d: %d %s %s", i, j, storage.scores[i].highscores[j].score, 738 storage.scores[i].highscores[j].name, storage.scores[i].highscores[j].date); 739 } 740 } 741 TraceLog(LOG_INFO, "[storage] Tutorial: %d %d %d", storage.tutorialStep, storage.tutorialStepCount, storage.tutorialCompleted); 742 TraceLog(LOG_INFO, "[storage] Startups: %d", storage.startups); 743 } 744 745 #include <time.h> 746 void StoreHighScore(PlayfieldScores scores) 747 { 748 DifficultyScores *difficultyScores = &storage.scores[gameDifficulty]; 749 750 for (int i = 0; i < 8; i++) 751 { 752 if (scores.score > difficultyScores->highscores[i].score) 753 { 754 for (int j = 7; j > i; j--) 755 { 756 difficultyScores->highscores[j] = difficultyScores->highscores[j - 1]; 757 } 758 difficultyScores->highscores[i].score = scores.score; 759 time_t now = time(0); 760 struct tm *tm = localtime(&now); 761 strftime(difficultyScores->highscores[i].date, 16, "%Y-%m-%d", tm); 762 difficultyScores->highscores[i].name[0] = '\0'; 763 SaveStorage(); 764 break; 765 } 766 } 767 } 768 769 void TutorialBubble(int step, const char *text, int x, int y, int width, int height) 770 { 771 if (playfield.tutorialStep != step) return; 772 Rectangle rect = {x, y, width, height};
773 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, COLOR_FROM_HEX(0xcceeff)); 774 775 Font font = fontMainMedium; 776 int lineHeight = font.baseSize - 4; 777 int lineCount = 1; 778 for (int i = 0; i < strlen(text); i++) 779 { 780 if (text[i] == '\n') 781 { 782 lineCount++; 783 } 784 } 785 int yTop = y; 786 float textHeight = lineCount * lineHeight; 787 const char *line = text; 788 y = y + height * 0.5f - textHeight * 0.5f; 789 while (*line) { 790 char lineBuffer[256]; 791 const char *end = line; 792 while (*end && *end != '\n') { 793 lineBuffer[end - line] = *end; 794 end++; 795 } 796 lineBuffer[end - line] = '\0'; 797 798 Vector2 tsize = MeasureTextEx(font, lineBuffer, font.baseSize, 1); 799 DrawTextEx(font, lineBuffer, (Vector2){ x + width * 0.5f - tsize.x * 0.5f, y }, font.baseSize, 1, BLACK); 800 801 line = end; 802 while (*line == '\n') line++; 803 y += lineHeight; 804 } 805 806 DrawTextureRec(texUiAtlas, rectUiAtlasCircle, (Vector2){x + width - 30, yTop}, COLOR_FROM_HEX(0xccddff)); 807 DrawTextureRec(texUiAtlas, rectUiAtlasShrimpy, (Vector2){x + width, yTop + 10}, WHITE);
808 } 809 810 void TutorialSetClicksBlocked(int step, int blocked) 811 { 812 if (playfield.tutorialStep != step) return; 813 isClickActionBlocked = blocked; 814 } 815 816 void TutorialProceedOnClick(int step) 817 { 818 if (playfield.tutorialStep != step) return; 819 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { 820 playfield.nextTutorialStep = step + 1; 821 } 822 } 823 824 void TutorialHighlightCircle(int step, int x, int y, int radius) 825 { 826 if (playfield.tutorialStep != step) return; 827 float pulse = sinf(gameTime.time * 10.0f) * 0.25f + 1.0f; 828 DrawCircleLines(x, y, radius * pulse, COLOR_FROM_HEX(0xff0000)); 829 DrawCircleLines(x, y, radius * pulse + 0.5f, COLOR_FROM_HEX(0xff0000)); 830 DrawCircleLines(x, y, radius * pulse + 1.0f, COLOR_FROM_HEX(0xff0000)); 831 } 832 833 void TutorialSpawnBubble(int step, int x, int y, int level, int type, int nextLevel, int nextType) 834 { 835 if (playfield.tutorialStep != step || !playfield.tutorialStepStart) return; 836 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera, 837 (Vector2){x, y}); 838 playfield.nextBubbleLevel = level; 839 playfield.nextBubbleType = type; 840 PlayfieldTryAddBubble(&playfield, pos); 841 playfield.nextBubbleLevel = nextLevel; 842 playfield.nextBubbleType = nextType; 843 } 844 845 void UpdateTutorialSystem_PlayTutorial() 846 { 847 int step = 0;
848 int boxHeight = 105;
849 TutorialSetClicksBlocked(step, 1);
850 TutorialBubble(step, "Welcome to my fishtank...", 10, 10, 600, boxHeight);
851 TutorialProceedOnClick(step); 852 step++; 853 854 TutorialSetClicksBlocked(step, 1);
855 TutorialBubble(step, "It is very boring here.\nSo I invented this little game:", 10, 10, 600, boxHeight);
856 TutorialProceedOnClick(step); 857 step++; 858 859 TutorialSetClicksBlocked(step, 1);
860 TutorialBubble(step, "Please tap on the marked area ...", 10, 10, 600, boxHeight);
861 TutorialProceedOnClick(step); 862 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 863 step++; 864 865 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 1, 0);
866 TutorialBubble(step, "Very good! Now do it again!", 10, 10, 600, boxHeight);
867 TutorialProceedOnClick(step);
868 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 869 TutorialSetClicksBlocked(step, 1);
870 step++; 871
872 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 2, 1);
873 TutorialBubble(step,
874 "These two bubbles have the same size and\n" 875 "color. When they touch, they will merge.", 10, 10, 600, boxHeight); 876 TutorialProceedOnClick(step); 877 TutorialSetClicksBlocked(step, 1); 878 step++; 879 880 TutorialBubble(step, "But currently, they don't :(\nLet me help you a little.", 10, 10, 600, boxHeight); 881 TutorialProceedOnClick(step); 882 TutorialSetClicksBlocked(step, 1);
883 step++;
884 TutorialBubble(step, "...", 10, 10, 600, boxHeight); 885 if (step == playfield.tutorialStep) { 886 playfield.enableTutorialMerge = 1; 887 if (playfield.bubbles[0].bubbleLevel == 2) 888 { 889 playfield.nextTutorialStep = step + 1; 890 } 891 } 892 step++; 893 TutorialBubble(step, "See? The two bubbles merged and now\nit is bigger - look and the two dots it got.", 10, 10, 600, boxHeight); 894 TutorialProceedOnClick(step); 895 TutorialSetClicksBlocked(step, 1); 896 step++; 897 TutorialBubble(step, "Oh, the next bubble has a different color.\nTap again!", 10, 10, 600, boxHeight); 898 TutorialProceedOnClick(step); 899 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 900 TutorialSetClicksBlocked(step, 1); 901 902 step++; 903 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1); 904 TutorialBubble(step, "It won't merge with the bright bubble.\nI don't know why.", 10, 10, 600, boxHeight); 905 TutorialProceedOnClick(step); 906 907 step++; 908 TutorialBubble(step, "But look! We have another bubble of \nthe same color! Release it!", 10, 10, 600, boxHeight); 909 TutorialProceedOnClick(step); 910 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 911 step++; 912 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1); 913 TutorialBubble(step, "Wait for it...", 10, 10, 600, boxHeight); 914 if (step == playfield.tutorialStep) { 915 playfield.enableTutorialMerge = 1; 916 if (playfield.bubbles[1].bubbleLevel == 3) 917 { 918 playfield.nextTutorialStep = step + 1; 919 } 920 } 921 922 step++; 923 TutorialBubble(step, "Just watch out: When the bubbles go too high\nor low, they all pop :(", 10, 10, 600, boxHeight); 924 TutorialProceedOnClick(step); 925 926 step++; 927 TutorialBubble(step, "I love these bubbles!\nGo and try to see how many you can get!", 10, 10, 600, boxHeight); 928 TutorialProceedOnClick(step); 929 if (step == playfield.tutorialStep) { 930 playfield.enableTutorialMerge = 0; 931 } 932 933 step++; 934 TutorialSetClicksBlocked(step, 0); 935 936 if (playfield.strikes > 0 && playfield.tutorialStep == step) 937 { 938 playfield.nextTutorialStep = step + 1; 939 } 940 step++; 941 942 TutorialBubble(step, "Oh no! The bubbles are about to pop!\nCareful, you have 2 more tries!", 10, 10, 600, boxHeight); 943 TutorialProceedOnClick(step); 944 TutorialSetClicksBlocked(step, 1); 945 step++;
946 TutorialSetClicksBlocked(step, 0); 947 } 948 949 void UpdateTutorialSystem() 950 { 951 if (currentScene == GAME_SCENE_PLAY && gameDifficulty == GAME_DIFFICULTY_TUTORIAL) 952 { 953 UpdateTutorialSystem_PlayTutorial(); 954 } 955 956 if (playfield.nextTutorialStep != playfield.tutorialStep) 957 { 958 playfield.tutorialStepStart = 1; 959 } 960 else 961 { 962 playfield.tutorialStepStart = 0; 963 } 964 playfield.tutorialStep = playfield.nextTutorialStep; 965 } 966 967 int main(void) 968 { 969 // Initialization 970 //-------------------------------------------------------------------------------------- 971 const int screenWidth = 800; 972 const int screenHeight = 450; 973
974 InitWindow(screenWidth, screenHeight, "Shrimpy Bubble Merge");
975 976 SetTargetFPS(60); // Set our game to run at 60 frames-per-second 977 //-------------------------------------------------------------------------------------- 978 979 TraceLog(LOG_INFO, "loading shaders"); 980 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs"); 981 // Get shader locations 982 outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize"); 983 outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor"); 984 textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize"); 985 986 LoadStorage(); 987 LogStorage();
988 989 texShrimpy = LoadTexture("data/shrimpy.png"); 990 texUiAtlas = LoadTexture("data/ui-atlas.png"); 991 texGround = LoadTexture("data/ground.png"); 992 fontMainMedium = LoadFontEx("data/IndieFlower-Regular.ttf", 40, 0, 0); 993 fontMainLarge = LoadFontEx("data/IndieFlower-Regular.ttf", 70, 0, 0);
994 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24)); 995 // Main game loop 996 while (!WindowShouldClose()) // Detect window close button or ESC key 997 { 998 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight()) 999 { 1000 UnloadRenderTexture(bubbleFieldTexture); 1001 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight()); 1002 } 1003 1004 float dt = GetFrameTime(); 1005 // clamp dt to prevent large time steps, e.g. when browser tab is inactive 1006 if (dt > 0.2f) dt = 0.2f; 1007 gameTime.time += dt; 1008 gameTime.deltaTime = dt; 1009
1010 BeginDrawing(); 1011 1012 int y = GetScreenHeight() - texGround.height; 1013 for (int x=0; x<screenWidth; x+=texGround.width) 1014 { 1015 DrawTexture(texGround, x, y, WHITE); 1016 } 1017 1018 // draw wavy water line at top 1019 Vector2 waterLine[80]; 1020 float waterXStep = GetScreenWidth() / 38; 1021 for (int i = 0; i < 40; i++) 1022 { 1023 waterLine[i * 2] = (Vector2){i * waterXStep, 0}; 1024 waterLine[i * 2 + 1] = (Vector2){i * waterXStep, 15 1025 + sinf(gameTime.time * 2.9f + waterLine[i * 2 + 1].x * 0.008f) * 2.0f 1026 + sinf(gameTime.time * 4.3f + waterLine[i * 2 + 1].x * 0.015f) * 1.0f 1027 + sinf(gameTime.time * 6.9f + waterLine[i * 2 + 1].x * 0.032f) * 0.5f 1028 }; 1029 } 1030 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0x88ccff)); 1031 1032 for (int i = 0; i < 40; i++) 1033 { 1034 float x = i * waterXStep; 1035 waterLine[i * 2 + 1] = (Vector2){x, 16 1036 + sinf(gameTime.time * 2.9f + x * 0.008f) * 2.0f 1037 + sinf(gameTime.time * 4.3f + x * 0.015f) * 1.0f 1038 + sinf(gameTime.time * 6.9f + x * 0.032f) * 0.5f 1039 }; 1040 waterLine[i * 2] = (Vector2){x, waterLine[i * 2 + 1].y - 2.0f}; 1041 } 1042 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0xeef8ff)); 1043 1044 1045
1046 switch (currentScene) 1047 {
1048 default: UpdateSceneMenu(); break; 1049 case GAME_SCENE_SETTINGS: UpdateSceneSettings(); break;
1050 case GAME_SCENE_PLAY: UpdateScenePlay(); break; 1051 case GAME_SCENE_GAMEOVER: UpdateSceneGameOver(); break; 1052 } 1053 1054 UpdateTutorialSystem(); 1055 1056 switch (nextScene) 1057 { 1058 case GAME_SCENE_NONE: break; 1059 default: currentScene = nextScene; break; 1060 case GAME_SCENE_GAMEOVER: 1061 StoreHighScore(CalculatePlayfieldScores(&playfield)); 1062 currentScene = GAME_SCENE_GAMEOVER; 1063 break; 1064 case GAME_SCENE_PLAY: 1065 playfield = (Playfield){ 1066 .fieldSize = {230, 300}, 1067 .waterLevel = 200.0f, 1068 .minHeight = 90.0f, 1069 .maxHeight = 280.0f,
1070 }; 1071 if (gameDifficulty == GAME_DIFFICULTY_TUTORIAL) 1072 { 1073 playfield.waterLevel = 180.0f; 1074 }
1075 currentScene = GAME_SCENE_PLAY; break; 1076 } 1077 1078 if (nextScene != GAME_SCENE_NONE)
1079 { 1080 isClickActionBlocked = 0;
1081 playfield.tutorialStep = 0; 1082 playfield.nextTutorialStep = 0; 1083 } 1084 1085 nextScene = GAME_SCENE_NONE; 1086 EndDrawing(); 1087 } 1088 1089 // De-Initialization 1090 //-------------------------------------------------------------------------------------- 1091 CloseWindow(); // Close window and OpenGL context 1092 //-------------------------------------------------------------------------------------- 1093 1094 return 0; 1095 }
  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 uint8_t enableTutorialMerge;
97 float gameoverCooldown; 98 uint32_t spawnedBubbleCount; 99 uint32_t tutorialStep; 100 uint32_t nextTutorialStep; 101 } Playfield; 102 103 typedef struct GameTime 104 { 105 float time; 106 float deltaTime; 107 float fixedTime; 108 float fixedDeltaTime; 109 } GameTime; 110 111 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF} 112 #endif

Much nicer now. But time is running out. It is 14:50 and I need to focus on the important things... Graphic is all rudiemtary in. Sounds are missing and the game over screen should pop the bubbles. After that, there is also visualization improvements for the bubble interactions, like when they touch and can merge.

  • 💾
  1 #include "bpop_main.h"
  2 #include <string.h>
  3 #include <stdlib.h>
  4 #include <stdio.h>
  5 
6 int playSounds = 1;
7 GameScene currentScene = GAME_SCENE_MENU; 8 GameScene nextScene = GAME_SCENE_NONE;
9 PlayfieldScores finalScores; 10 Sound sfxPop;
11 GameDifficulty gameDifficulty = GAME_DIFFICULTY_NORMAL; 12 13 Storage storage = { 14 .version = 1, 15 }; 16
17 Texture2D texShrimpy; 18 Texture2D texLeftCorner;
19 Texture2D texUiAtlas; 20 Texture2D texGround; 21 Font fontMainMedium; 22 Font fontMainLarge; 23 24 NPatchInfo nPatchButton = { 25 .source = {0,0,240,145}, 26 .left = 10, 27 .top = 10, 28 .right = 10, 29 .bottom = 10, 30 .layout = NPATCH_NINE_PATCH 31 }; 32 Rectangle rectUiAtlasCircle = { 33 .x = 250, 34 .y = 0, 35 .width = 148, 36 .height = 148, 37 }; 38 Rectangle rectUiAtlasShrimpy = { 39 .x = 420, 40 .y = 390, 41 .width = 512-420, 42 .height = 512-390, 43 }; 44 45 int outlineSizeLoc = 0; 46 int outlineColorLoc = 0; 47 int textureSizeLoc = 0; 48 49 Camera3D camera = { 50 .position = { 0.0f, 0.0f, -10.0f }, 51 .target = { 0.0f, 0.0f, 0.0f }, 52 .up = { 0.0f, 1.0f, 0.0f }, 53 .projection = CAMERA_ORTHOGRAPHIC, 54 .fovy = 320.0f, 55 }; 56 57 58 Playfield playfield = { 59 .fieldSize = {250, 300}, 60 .waterLevel = 200.0f, 61 .minHeight = 90.0f, 62 .maxHeight = 280.0f, 63 }; 64 65 GameTime gameTime = { 66 .fixedDeltaTime = 1.0f / 60.0f, 67 }; 68 69 Color bubbleTypeColors[] = { 70 COLOR_FROM_HEX(0xaaccff), 71 COLOR_FROM_HEX(0x2277ff), 72 COLOR_FROM_HEX(0x33ffaa), 73 COLOR_FROM_HEX(0x9933aa), 74 COLOR_FROM_HEX(0xaa33ff), 75 }; 76 77 Model bubbleModel; 78 Shader bubbleOutlineShader; 79 RenderTexture2D bubbleFieldTexture; 80 81 int isClickActionBlocked = 0;
82 83 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield);
84 void SaveStorage(); 85 86 void DrawText2(const char *text, int x, int y, Color color) 87 { 88 DrawTextEx(fontMainMedium, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color); 89 } 90 91 void DrawText3(const char *text, int x, int y, Color color) 92 { 93 DrawTextEx(fontMainLarge, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color); 94 } 95 96 float MeasureText2(const char *text) 97 { 98 return MeasureTextEx(fontMainMedium, text, fontMainMedium.baseSize, 1).x; 99 } 100 101 float BubbleLevelRadius(int level) 102 { 103 return powf((level + 1) * 120, .5f); 104 } 105 106 int IsClickActioned() 107 { 108 int result = 0; 109 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !isClickActionBlocked) 110 { 111 result = 1; 112 } 113 return result; 114 } 115 116 int Button(const char *text, Vector2 position, Vector2 size) 117 { 118 Rectangle rect = {position.x, position.y, size.x, size.y}; 119 int isHovered = !isClickActionBlocked && CheckCollisionPointRec(GetMousePosition(), rect); 120 int result = isHovered && IsClickActioned(); 121 122 Color color = isHovered ? COLOR_FROM_HEX(0xcceeff) : COLOR_FROM_HEX(0xaaccff); 123 if (!isClickActionBlocked && isHovered && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) 124 { 125 color = COLOR_FROM_HEX(0xddf8ff); 126 } 127 128 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, color); 129 130 int fontSize = fontMainMedium.baseSize; 131 Vector2 tsize = MeasureTextEx(fontMainMedium, text, fontSize, 1); 132 DrawTextEx(fontMainMedium, text, (Vector2){position.x + size.x * 0.5f - tsize.x * 0.5f, position.y + size.y * 0.5f - tsize.y * 0.5f}, fontSize, 1, BLACK); 133 return result; 134 } 135
136 void PlayfieldFixedUpdate(Playfield *playfield, int popBubbles)
137 { 138 for (int i = 0; i < MAX_BUBBLES; i++) 139 { 140 Bubble *bubble = &playfield->bubbles[i]; 141 bubble->sameLevelContact = 0; 142 } 143 144 for (int i = 0; i < MAX_BUBBLES; i++) 145 { 146 Bubble *bubble = &playfield->bubbles[i]; 147 if (!bubble->flagIsActive) continue; 148 float r = bubble->radius; 149 150 for (int j = i + 1; j < MAX_BUBBLES; j++) 151 { 152 Bubble *other = &playfield->bubbles[j]; 153 if (!other->flagIsActive) continue; 154 float otherR = other->radius; 155 float sumR2 = (r + otherR) * (r + otherR); 156 float d2 = Vector2DistanceSqr(bubble->position, other->position); 157 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType; 158 if (d2 < sumR2 * 1.05f) 159 { 160 if (canMerge) 161 { 162 bubble->sameLevelContact = 1; 163 other->sameLevelContact = 1; 164 } 165 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f 166 && other->bubbleMergeCooldown <= 0.0f) 167 { 168 // merge bubbles
169 bubble->bubbleLevel++; 170 bubble->position = Vector2Lerp(bubble->position, other->position, 0.5f);
171 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
172 bubble->bubbleMergeCooldown = 1.0f; 173 174 playfield->poppingBubbles[i] = *bubble; 175 playfield->poppingBubbles[i].lifeTime = 0.0f; 176 playfield->comboPopPoints += playfield->comboPop; 177 playfield->comboPop++; 178 179 if (playSounds) 180 PlaySound(sfxPop); 181
182 other->flagIsActive = 0; 183 } 184 } 185 186 if (d2 < sumR2) 187 { 188 float overlap = r + otherR - sqrtf(d2); 189 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position)); 190 // resolve overlap by moving the bubbles apart 191 const float errorCorrection = 0.25f; 192 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection)); 193 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection)); 194 195 // bounce off each other 196 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity); 197 float dot = Vector2DotProduct(relativeVelocity, normal); 198 if (dot > 0.0f) 199 { 200 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000)); 201 float impulse = -dot * 0.85f; 202 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse)); 203 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse)); 204 } 205 } 206 } 207 208 if (!bubble->sameLevelContact) 209 { 210 bubble->bubbleMergeCooldown = 1.0f; 211 } 212 else 213 { 214 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime; 215 } 216 217 float buoyancy = -20.0f; 218 if (bubble->position.y < playfield->waterLevel) 219 { 220 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f; 221 } 222 223 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius || 224 bubble->position.y > playfield->maxHeight - bubble->radius; 225 bubble->lifeTime += gameTime.fixedDeltaTime; 226 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f; 227 228 float horizontalV = 0.0f; 229 if (playfield->enableTutorialMerge) { 230 horizontalV = fminf(fmaxf(-10.0f, (playfield->fieldSize.x / 2 - bubble->position.x) * 0.1f), 10.0f); 231 } 232 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){horizontalV, buoyancy}); 233 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f); 234 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime; 235 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime; 236 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) || 237 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f)) 238 { 239 bubble->velocity.x *= -0.9f; 240 } 241
242 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x; 243 244 if (popBubbles && GetRandomValue(0, 1000) < 50) 245 { 246 playfield->poppingBubbles[i] = *bubble; 247 playfield->poppingBubbles[i].lifeTime = 0.0f; 248 bubble->flagIsActive = 0; 249 }
250 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y; 251 252 // debug velocity 253 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000)); 254 } 255 256 int outsideCount = 0; 257 for (int i = 0; i < MAX_BUBBLES; i++) 258 { 259 Bubble *bubble = &playfield->bubbles[i]; 260 if (bubble->isOutsideZone) 261 { 262 outsideCount++; 263 } 264 } 265 266 if (outsideCount == 0) 267 { 268 playfield->strikes = 0; 269 playfield->gameoverCooldown = 0.0f; 270 }
271 if (playfield->strikes >= MAX_STRIKES && !popBubbles)
272 { 273 playfield->gameoverCooldown += gameTime.fixedDeltaTime; 274 if (playfield->gameoverCooldown > GAMEOVER_COOLDOWN)
275 { 276 finalScores = CalculatePlayfieldScores(playfield);
277 nextScene = GAME_SCENE_GAMEOVER; 278 } 279 } 280 } 281 282 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position) 283 { 284 if (playfield->strikes >= MAX_STRIKES) return; 285 286 for (int i = 0; i < MAX_BUBBLES; i++) 287 { 288 Bubble *bubble = &playfield->bubbles[i]; 289 if (!bubble->flagIsActive) 290 { 291 bubble->flagIsActive = 1; 292 bubble->position = position; 293 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)}; 294 bubble->bubbleType = playfield->nextBubbleType; 295 bubble->bubbleLevel = playfield->nextBubbleLevel; 296 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel); 297 bubble->lifeTime = 0.0f; 298 playfield->spawnedBubbleCount++; 299 300 for (int j = 0; j < MAX_BUBBLES; j += 1) 301 { 302 Bubble *other = &playfield->bubbles[j]; 303 if (!other->flagIsActive) continue; 304 if (other->isOutsideZone) 305 { 306 playfield->strikes++; 307 break; 308 } 309 }
310 playfield->comboPop = 0;
311 playfield->nextBubbleType = GetRandomValue(0, gameDifficulty > 0 ? gameDifficulty : 1); 312 playfield->nextBubbleLevel = GetRandomValue(0, 3); 313 break; 314 } 315 } 316 317 } 318 319 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield) 320 { 321 PlayfieldScores scores = {0}; 322 for (int i = 0; i < MAX_BUBBLES; i++) 323 { 324 Bubble *bubble = &playfield->bubbles[i]; 325 if (bubble->flagIsActive) 326 { 327 scores.bubbleCount++; 328 uint32_t bubbleScore = 1 << bubble->bubbleLevel; 329 scores.score += bubbleScore; 330 331 if (bubble->isOutsideZone) 332 { 333 scores.outsideBubbleCount++; 334 } 335 } 336 }
337 scores.score += playfield->spawnedBubbleCount; 338 scores.score += playfield->comboPopPoints;
339 return scores; 340 } 341 342 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position) 343 { 344 Vector2 spawnPosition = position; 345 spawnPosition.y = BubbleLevelRadius(5); 346 return spawnPosition; 347 } 348 349 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition) 350 { 351 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position; 352 cursorPosition.x += playfield->fieldSize.x / 2; 353 cursorPosition.y += playfield->fieldSize.y / 2; 354 355 Vector2 pos = {cursorPosition.x, cursorPosition.y};
356 return PlayfieldPositionToSpawnPosition(playfield, pos); 357 } 358 359 void DrawBubblePop(Vector3 position, float t, int level, Color color) 360 { 361 // int oldSeed = GetRandomValue(0, 0xfffffff); 362 // SetRandomSeed((int)(position.x * 1000) ^ (int)(position.y * 1000) + level * 1000); 363 color.a = 255 * (1.0f - t); 364 DrawModel(bubbleModel, position, BubbleLevelRadius(level) * (1 + t), color);
365 } 366 367 void DrawBubble(Vector3 position, int level, Color color) 368 { 369 float bubbleExtraRadius = 5.0f; 370 float r = BubbleLevelRadius(level) + bubbleExtraRadius; 371 DrawModel(bubbleModel, position, r, color); 372 if (level < 1) return; 373 position.z -= r; 374 float tinyR = level < 6 ? 2 : 4; 375 int count = level < 6 ? level : level - 5; 376 for (int i = 0; i < count; i++) 377 { 378 float ang = (i * 25.0f + 30.0f) * DEG2RAD; 379 float offsetR = i % 2 == 0 ? 0.4f : 0.7f; 380 Vector3 offset = {cosf(ang) * offsetR * r, sinf(ang) * offsetR * r, 0}; 381 DrawModel(bubbleModel, Vector3Add(position, offset), tinyR, WHITE); 382 } 383 } 384 385 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera) 386 { 387 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff)); 388 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x, 389 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588)); 390 391 // cursor bubble 392 if (currentScene == GAME_SCENE_PLAY) 393 { 394 Vector2 mousePos = GetMousePosition(); 395 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos); 396 Vector3 drawPos = (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0}; 397 if (playfield->strikes < MAX_STRIKES && drawPos.x >= -playfield->fieldSize.x * 0.5f && drawPos.x <= playfield->fieldSize.x * 0.5f) 398 { 399 DrawBubble(drawPos, playfield->nextBubbleLevel, bubbleTypeColors[playfield->nextBubbleType]); 400 } 401 } 402 403 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588)); 404 rlPushMatrix(); 405 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0); 406 // draw bubbles into playfield space 407 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f; 408 for (int i = 0; i < MAX_BUBBLES; i++) 409 { 410 Bubble *bubble = &playfield->bubbles[i]; 411 if (!bubble->flagIsActive) continue; 412 Vector3 position = {bubble->position.x, bubble->position.y, 0}; 413 Color bubbleColor = bubbleTypeColors[bubble->bubbleType]; 414 int isOutsideZone = bubble->isOutsideZone; 415 416 if (isOutsideZone) 417 { 418 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink); 419 } 420 // lazy: encode id into rgb values 421 bubbleColor.r = bubbleColor.r - i * 2 % 4; 422 bubbleColor.g = bubbleColor.g - (i * 2 / 4) % 4; 423 bubbleColor.b = bubbleColor.b - (i * 2 / 16); 424 bubbleColor.a = 255;
425 DrawBubble(position, bubble->bubbleLevel, bubbleColor); 426 } 427 428 for (int i = 0; i < MAX_BUBBLES; i++) 429 { 430 Bubble *bubble = &playfield->poppingBubbles[i]; 431 if (!bubble->flagIsActive) continue; 432 bubble->lifeTime += gameTime.deltaTime; 433 float t = bubble->lifeTime / 0.125f; 434 if (t >= 1.0f) 435 { 436 bubble->flagIsActive = 0; 437 continue; 438 } 439 Vector3 position = {bubble->position.x, bubble->position.y, 0}; 440 DrawBubblePop(position, t, bubble->bubbleLevel, bubbleTypeColors[bubble->bubbleType]);
441 } 442 rlPopMatrix(); 443 } 444 445 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera) 446 { 447 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400); 448 int divides = 10; 449 float divWidth = playfield->fieldSize.x / divides; 450 for (int i = 0; i < divides; i+=2) 451 { 452 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f; 453 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor); 454 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor); 455 } 456 } 457 458 void PlayfieldFullDraw(Playfield *playfield, Camera3D camera) 459 { 460 ClearBackground(COLOR_FROM_HEX(0x4488cc)); 461 462 BeginTextureMode(bubbleFieldTexture); 463 rlSetClipPlanes(-128.0f, 128.0f); 464 BeginMode3D(camera); 465 466 ClearBackground(BLANK); 467 PlayfieldDrawBubbles(playfield, camera); 468 EndMode3D(); 469 EndTextureMode(); 470 471 float outlineSize = 1.0f; 472 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; 473 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height }; 474 475 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT); 476 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4); 477 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2); 478 479 rlDisableDepthMask(); 480 BeginShaderMode(bubbleOutlineShader); 481 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width, 482 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()}, 483 (Vector2){0, 0}, 0.0f, WHITE); 484 EndShaderMode(); 485 rlEnableDepthMask(); 486 487 BeginMode3D(camera); 488 PlayfieldDrawRange(playfield, camera); 489 EndMode3D(); 490 491 const char *difficultyText = "Tutorial"; 492 switch (gameDifficulty) 493 { 494 case GAME_DIFFICULTY_EASY: difficultyText = "Easy"; break; 495 case GAME_DIFFICULTY_NORMAL: difficultyText = "Normal"; break; 496 case GAME_DIFFICULTY_HARD: difficultyText = "Hard"; break; 497 default: 498 break; 499 } 500 const char *modeText = TextFormat("Mode: %s", difficultyText); 501 int screenWidth = GetScreenWidth(); 502 int x = screenWidth - 220; 503 DrawText2("Highscores", x, 25, WHITE); 504 DrawText2(modeText, x, 55, WHITE); 505 DifficultyScores table = storage.scores[gameDifficulty]; 506 for (int i = 0; i < 8; i++) 507 { 508 HighscoreEntry entry = table.highscores[i]; 509 if (entry.score == 0) break; 510 char buffer[64]; 511 sprintf(buffer, "%d:", i + 1); 512 int y = 110 + i * 30; 513 DrawText2(buffer, x + 18 - MeasureText2(buffer), y, WHITE); 514 sprintf(buffer, "%d", entry.score); 515 DrawText2(buffer, x + 50 - MeasureText2(buffer) / 2, y, WHITE); 516 sprintf(buffer, "%s", entry.date); 517 DrawText2(buffer, screenWidth - 10 - MeasureText2(buffer), y, WHITE); 518 }
519 520 int sx = 0, sy = 0; 521 if (playfield->comboPop > 2) 522 { 523 sx = 1; 524 } 525 if (playfield->strikes > 0) 526 { 527 sx = 0; 528 sy = 1; 529 } 530 if (currentScene == GAME_SCENE_GAMEOVER) 531 { 532 sx = 1; 533 sy = 1; 534 } 535 DrawTextureRec(texShrimpy, 536 (Rectangle){sx * 256, sy * 256, 256, 256}, 537 (Vector2){GetScreenWidth() - 200, GetScreenHeight() - 260}, WHITE);
538 } 539 540 void UpdateSceneGameOver() 541 { 542 if (IsKeyPressed(KEY_ENTER)) 543 { 544 nextScene = GAME_SCENE_MENU; 545 } 546 // Draw 547 //---------------------------------------------------------------------------------- 548 549 ClearBackground(COLOR_FROM_HEX(0x4488cc));
550 551 PlayfieldFixedUpdate(&playfield, 1);
552 PlayfieldFullDraw(&playfield, camera); 553 554 DrawText3("Game Over", 20, 20, WHITE);
555 556 DrawText2(TextFormat("Final Score: %d", finalScores.score), 20, 90, WHITE);
557 558 if (Button("< Menu", (Vector2){20, GetScreenHeight() - 110}, (Vector2){180, 90})) 559 { 560 nextScene = GAME_SCENE_MENU; 561 } 562 } 563 564 565 void UpdateScenePlay() 566 { 567 if (IsKeyPressed(KEY_ESCAPE)) 568 { 569 nextScene = GAME_SCENE_MENU; 570 } 571 572 if (IsClickActioned() && playfield.strikes < MAX_STRIKES) 573 { 574 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera, 575 GetMousePosition()); 576 if (pos.y >= 0.0f && pos.y <= playfield.fieldSize.y 577 && pos.x >= 0.0f && pos.x <= playfield.fieldSize.x) 578 { 579 PlayfieldTryAddBubble(&playfield, pos); 580 } 581 } 582 583 while (gameTime.fixedTime < gameTime.time) 584 {
585 gameTime.fixedTime += gameTime.fixedDeltaTime;
586 PlayfieldFixedUpdate(&playfield, 0); 587 } 588 589 // Draw 590 //---------------------------------------------------------------------------------- 591 592 PlayfieldFullDraw(&playfield, camera); 593 594 PlayfieldScores scores = CalculatePlayfieldScores(&playfield); 595 int y = 50; 596 DrawText2("Shrimpy Bubble", 10, 20, WHITE); 597 DrawText2("Merge", 150, 42, WHITE);
598 599 DrawText2(TextFormat("Score: %d", scores.score), 10, y+=30, WHITE);
600 DrawText2(TextFormat("Combos: %d", playfield.comboPop), 10, y+=30, WHITE); 601 // DrawText2(TextFormat("Bubbles: %d", scores.bubbleCount), 10, y+=30, WHITE); 602 // DrawText2(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, y+=30, WHITE);
603 // DrawText2(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, y+=30, WHITE);
604 DrawText2(TextFormat("Strikes: %d", playfield.strikes), 10, y+=40, 605 playfield.strikes > 0 ? ColorLerp(WHITE, COLOR_FROM_HEX(0xff4433), sinf(gameTime.time * 10.0f)) : WHITE); 606 if (playfield.strikes >= MAX_STRIKES) 607 { 608 DrawText2(TextFormat("Gameover in: %.1fs", GAMEOVER_COOLDOWN - playfield.gameoverCooldown), 10, y+=30, RED); 609 } 610 611 if (Button("< Menu", (Vector2){10, GetScreenHeight() - 100}, (Vector2){150, 80})) 612 { 613 nextScene = GAME_SCENE_MENU; 614 } 615 } 616 617 void UpdateSceneSettings() 618 { 619 ClearBackground(COLOR_FROM_HEX(0x4488cc)); 620 int hCenter = 200; 621 622 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge Settings", 623 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE);
624 625 DrawTextureRec(texShrimpy, 626 (Rectangle){0, 256, 256, 256},
627 (Vector2){GetScreenWidth() - 400, GetScreenHeight() - 260}, WHITE); 628
629 int buttonH = 60; 630 int y = 50; 631 632 if (Button(playSounds ? "Sound: On" : "Sound: Off", 633 (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH})) 634 { 635 playSounds = !playSounds; 636 SaveStorage(); 637 } 638
639 if (Button("Clear data", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH})) 640 { 641 nextScene = GAME_SCENE_MENU; 642 memset(&storage, 0, sizeof(storage)); 643 SaveStorage(); 644 } 645 646 if (Button("< Menu", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH})) 647 { 648 nextScene = GAME_SCENE_MENU; 649 } 650 } 651 652 void UpdateSceneMenu() 653 { 654 ClearBackground(COLOR_FROM_HEX(0x4488cc)); 655 656 int hCenter = 200; 657 658 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge", 659 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE);
660 661 DrawTextureRec(texShrimpy, 662 (Rectangle){0, 0, 256, 256},
663 (Vector2){GetScreenWidth() - 400, GetScreenHeight() - 260}, WHITE); 664 665 int buttonH = 60; 666 int y = 100; 667 if (Button("Tutorial", (Vector2){hCenter - 100, y}, (Vector2){200, buttonH})) 668 { 669 gameDifficulty = GAME_DIFFICULTY_TUTORIAL; 670 nextScene = GAME_SCENE_PLAY; 671 } 672 673 if (Button("Play easy", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH})) 674 { 675 gameDifficulty = GAME_DIFFICULTY_EASY; 676 nextScene = GAME_SCENE_PLAY; 677 } 678 679 if (Button("Play normal", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH})) 680 { 681 gameDifficulty = GAME_DIFFICULTY_NORMAL; 682 nextScene = GAME_SCENE_PLAY; 683 } 684 685 if (Button("Play hard", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH})) 686 { 687 gameDifficulty = GAME_DIFFICULTY_HARD; 688 nextScene = GAME_SCENE_PLAY; 689 } 690 691 if (Button("Settings", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH})) 692 { 693 nextScene = GAME_SCENE_SETTINGS; 694 } 695 } 696 #if defined(PLATFORM_WEB) 697 #include <emscripten.h> 698 699 // Function to store data in Local Storage 700 void StoreData(const char *key, const char *value) { 701 EM_ASM_({ 702 localStorage.setItem(UTF8ToString($0), UTF8ToString($1)); 703 }, key, value); 704 } 705 706 // Function to retrieve data from Local Storage 707 const char* RetrieveData(const char *key) { 708 return (const char*)EM_ASM_INT({ 709 var value = localStorage.getItem(UTF8ToString($0)); 710 if (value === null) { 711 return 0; 712 } 713 var lengthBytes = lengthBytesUTF8(value) + 1; 714 var stringOnWasmHeap = _malloc(lengthBytes); 715 stringToUTF8(value, stringOnWasmHeap, lengthBytes); 716 return stringOnWasmHeap; 717 }, key); 718 } 719 #else 720 void StoreData(const char *key, const char *value) {} 721 const char* RetrieveData(const char *key) { return 0; } 722 #endif 723 724 uint32_t RetrieveUInt32(const char *key) 725 { 726 const char *value = RetrieveData(key); 727 if (value) 728 { 729 uint32_t result = atoi(value); 730 free((void*)value); 731 return result; 732 } 733 return 0; 734 } 735 736 void StoreUInt32(const char *key, uint32_t value) 737 { 738 char buffer[16]; 739 sprintf(buffer, "%d", value); 740 StoreData(key, buffer); 741 } 742 743 void RetrieveFixedChar(const char *key, char *buffer, int size) 744 { 745 const char *value = RetrieveData(key); 746 if (value) 747 { 748 strncpy(buffer, value, size); 749 free((void*)value); 750 } 751 else 752 { 753 buffer[0] = '\0'; 754 } 755 } 756 757 void StoreFixedChar(const char *key, const char *value) 758 { 759 StoreData(key, value); 760 } 761 762
763 void LoadStorage() 764 {
765 playSounds = RetrieveUInt32("storage.playSounds"); 766 // ignore version as this is first version. Upgrades need to be backwards compatible 767 for (int i = 0; i < 4; i++) 768 { 769 for (int j = 0; j < 8; j++) 770 { 771 char key[64]; 772 sprintf(key, "storage.highscore_%d_%d", i, j); 773 storage.scores[i].highscores[j].score = RetrieveUInt32(key); 774 sprintf(key, "storage.name_%d_%d", i, j); 775 RetrieveFixedChar(key, storage.scores[i].highscores[j].name, 16); 776 sprintf(key, "storage.date_%d_%d", i, j); 777 RetrieveFixedChar(key, storage.scores[i].highscores[j].date, 16);
778 } 779 }
780 781 storage.tutorialStep = RetrieveUInt32("storage.tutorialStep"); 782 storage.tutorialStepCount = RetrieveUInt32("storage.tutorialStepCount"); 783 storage.tutorialCompleted = RetrieveUInt32("storage.tutorialCompleted"); 784 storage.startups = RetrieveUInt32("storage.startups"); 785 StoreUInt32("storage.startups", storage.startups + 1); 786 } 787
788 void SaveStorage() 789 {
790 StoreUInt32("storage.playSounds", playSounds); 791 StoreUInt32("storage.version", 1); 792 for (int i = 0; i < 4; i++) 793 { 794 for (int j = 0; j < 8; j++) 795 { 796 char key[64]; 797 sprintf(key, "storage.highscore_%d_%d", i, j); 798 StoreUInt32(key, storage.scores[i].highscores[j].score); 799 sprintf(key, "storage.name_%d_%d", i, j); 800 StoreFixedChar(key, storage.scores[i].highscores[j].name); 801 sprintf(key, "storage.date_%d_%d", i, j); 802 StoreFixedChar(key, storage.scores[i].highscores[j].date); 803 } 804 } 805 StoreUInt32("storage.tutorialStep", storage.tutorialStep); 806 StoreUInt32("storage.tutorialStepCount", storage.tutorialStepCount); 807 StoreUInt32("storage.tutorialCompleted", storage.tutorialCompleted); 808 } 809 810 void LogStorage() 811 { 812 TraceLog(LOG_INFO, "[storage] Storage log"); 813 for (int i = 0; i < 4; i++) 814 { 815 for (int j = 0; j < 8; j++) 816 { 817 TraceLog(LOG_INFO, "[storage] Highscore %d %d: %d %s %s", i, j, storage.scores[i].highscores[j].score, 818 storage.scores[i].highscores[j].name, storage.scores[i].highscores[j].date); 819 } 820 } 821 TraceLog(LOG_INFO, "[storage] Tutorial: %d %d %d", storage.tutorialStep, storage.tutorialStepCount, storage.tutorialCompleted); 822 TraceLog(LOG_INFO, "[storage] Startups: %d", storage.startups); 823 } 824 825 #include <time.h> 826 void StoreHighScore(PlayfieldScores scores) 827 { 828 DifficultyScores *difficultyScores = &storage.scores[gameDifficulty]; 829 830 for (int i = 0; i < 8; i++) 831 { 832 if (scores.score > difficultyScores->highscores[i].score) 833 { 834 for (int j = 7; j > i; j--) 835 { 836 difficultyScores->highscores[j] = difficultyScores->highscores[j - 1]; 837 } 838 difficultyScores->highscores[i].score = scores.score; 839 time_t now = time(0); 840 struct tm *tm = localtime(&now); 841 strftime(difficultyScores->highscores[i].date, 16, "%Y-%m-%d", tm); 842 difficultyScores->highscores[i].name[0] = '\0'; 843 SaveStorage(); 844 break; 845 } 846 } 847 } 848 849 void TutorialBubble(int step, const char *text, int x, int y, int width, int height) 850 { 851 if (playfield.tutorialStep != step) return; 852 Rectangle rect = {x, y, width, height}; 853 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, COLOR_FROM_HEX(0xcceeff)); 854 855 Font font = fontMainMedium; 856 int lineHeight = font.baseSize - 4; 857 int lineCount = 1; 858 for (int i = 0; i < strlen(text); i++) 859 { 860 if (text[i] == '\n') 861 { 862 lineCount++; 863 } 864 } 865 int yTop = y; 866 float textHeight = lineCount * lineHeight; 867 const char *line = text; 868 y = y + height * 0.5f - textHeight * 0.5f; 869 while (*line) { 870 char lineBuffer[256]; 871 const char *end = line; 872 while (*end && *end != '\n') { 873 lineBuffer[end - line] = *end; 874 end++; 875 } 876 lineBuffer[end - line] = '\0'; 877 878 Vector2 tsize = MeasureTextEx(font, lineBuffer, font.baseSize, 1); 879 DrawTextEx(font, lineBuffer, (Vector2){ x + width * 0.5f - tsize.x * 0.5f, y }, font.baseSize, 1, BLACK); 880 881 line = end; 882 while (*line == '\n') line++; 883 y += lineHeight; 884 } 885 886 DrawTextureRec(texUiAtlas, rectUiAtlasCircle, (Vector2){x + width - 30, yTop}, COLOR_FROM_HEX(0xccddff)); 887 DrawTextureRec(texUiAtlas, rectUiAtlasShrimpy, (Vector2){x + width, yTop + 10}, WHITE); 888 } 889 890 void TutorialSetClicksBlocked(int step, int blocked) 891 { 892 if (playfield.tutorialStep != step) return; 893 isClickActionBlocked = blocked; 894 } 895 896 void TutorialProceedOnClick(int step) 897 { 898 if (playfield.tutorialStep != step) return; 899 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { 900 playfield.nextTutorialStep = step + 1; 901 } 902 } 903 904 void TutorialHighlightCircle(int step, int x, int y, int radius) 905 { 906 if (playfield.tutorialStep != step) return; 907 float pulse = sinf(gameTime.time * 10.0f) * 0.25f + 1.0f; 908 DrawCircleLines(x, y, radius * pulse, COLOR_FROM_HEX(0xff0000)); 909 DrawCircleLines(x, y, radius * pulse + 0.5f, COLOR_FROM_HEX(0xff0000)); 910 DrawCircleLines(x, y, radius * pulse + 1.0f, COLOR_FROM_HEX(0xff0000)); 911 } 912 913 void TutorialSpawnBubble(int step, int x, int y, int level, int type, int nextLevel, int nextType) 914 { 915 if (playfield.tutorialStep != step || !playfield.tutorialStepStart) return; 916 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera, 917 (Vector2){x, y}); 918 playfield.nextBubbleLevel = level; 919 playfield.nextBubbleType = type; 920 PlayfieldTryAddBubble(&playfield, pos); 921 playfield.nextBubbleLevel = nextLevel; 922 playfield.nextBubbleType = nextType; 923 } 924 925 void UpdateTutorialSystem_PlayTutorial() 926 { 927 int step = 0; 928 int boxHeight = 105; 929 TutorialSetClicksBlocked(step, 1); 930 TutorialBubble(step, "Welcome to my fishtank...", 10, 10, 600, boxHeight); 931 TutorialProceedOnClick(step); 932 step++; 933 934 TutorialSetClicksBlocked(step, 1); 935 TutorialBubble(step, "It is very boring here.\nSo I invented this little game:", 10, 10, 600, boxHeight); 936 TutorialProceedOnClick(step); 937 step++; 938 939 TutorialSetClicksBlocked(step, 1); 940 TutorialBubble(step, "Please tap on the marked area ...", 10, 10, 600, boxHeight); 941 TutorialProceedOnClick(step); 942 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 943 step++; 944 945 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 1, 0); 946 TutorialBubble(step, "Very good! Now do it again!", 10, 10, 600, boxHeight); 947 TutorialProceedOnClick(step); 948 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 949 TutorialSetClicksBlocked(step, 1); 950 step++; 951 952 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 2, 1); 953 TutorialBubble(step, 954 "These two bubbles have the same size and\n" 955 "color. When they touch, they will merge.", 10, 10, 600, boxHeight); 956 TutorialProceedOnClick(step); 957 TutorialSetClicksBlocked(step, 1); 958 step++; 959 960 TutorialBubble(step, "But currently, they don't :(\nLet me help you a little.", 10, 10, 600, boxHeight); 961 TutorialProceedOnClick(step); 962 TutorialSetClicksBlocked(step, 1); 963 step++; 964 TutorialBubble(step, "...", 10, 10, 600, boxHeight); 965 if (step == playfield.tutorialStep) { 966 playfield.enableTutorialMerge = 1; 967 if (playfield.bubbles[0].bubbleLevel == 2) 968 { 969 playfield.nextTutorialStep = step + 1; 970 } 971 } 972 step++; 973 TutorialBubble(step, "See? The two bubbles merged and now\nit is bigger - look and the two dots it got.", 10, 10, 600, boxHeight); 974 TutorialProceedOnClick(step); 975 TutorialSetClicksBlocked(step, 1); 976 step++; 977 TutorialBubble(step, "Oh, the next bubble has a different color.\nTap again!", 10, 10, 600, boxHeight); 978 TutorialProceedOnClick(step); 979 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 980 TutorialSetClicksBlocked(step, 1); 981 982 step++; 983 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1); 984 TutorialBubble(step, "It won't merge with the bright bubble.\nI don't know why.", 10, 10, 600, boxHeight); 985 TutorialProceedOnClick(step); 986 987 step++; 988 TutorialBubble(step, "But look! We have another bubble of \nthe same color! Release it!", 10, 10, 600, boxHeight); 989 TutorialProceedOnClick(step); 990 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20); 991 step++; 992 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1); 993 TutorialBubble(step, "Wait for it...", 10, 10, 600, boxHeight); 994 if (step == playfield.tutorialStep) { 995 playfield.enableTutorialMerge = 1; 996 if (playfield.bubbles[1].bubbleLevel == 3) 997 { 998 playfield.nextTutorialStep = step + 1; 999 } 1000 } 1001 1002 step++; 1003 TutorialBubble(step, "Just watch out: When the bubbles go too high\nor low, they all pop :(", 10, 10, 600, boxHeight); 1004 TutorialProceedOnClick(step); 1005 1006 step++; 1007 TutorialBubble(step, "I love these bubbles!\nGo and try to see how many you can get!", 10, 10, 600, boxHeight); 1008 TutorialProceedOnClick(step); 1009 if (step == playfield.tutorialStep) { 1010 playfield.enableTutorialMerge = 0; 1011 } 1012 1013 step++; 1014 TutorialSetClicksBlocked(step, 0); 1015 1016 if (playfield.strikes > 0 && playfield.tutorialStep == step) 1017 { 1018 playfield.nextTutorialStep = step + 1; 1019 } 1020 step++; 1021 1022 TutorialBubble(step, "Oh no! The bubbles are about to pop!\nCareful, you have 2 more tries!", 10, 10, 600, boxHeight); 1023 TutorialProceedOnClick(step); 1024 TutorialSetClicksBlocked(step, 1); 1025 step++; 1026 TutorialSetClicksBlocked(step, 0); 1027 } 1028 1029 void UpdateTutorialSystem() 1030 { 1031 if (currentScene == GAME_SCENE_PLAY && gameDifficulty == GAME_DIFFICULTY_TUTORIAL) 1032 { 1033 UpdateTutorialSystem_PlayTutorial(); 1034 } 1035 1036 if (playfield.nextTutorialStep != playfield.tutorialStep) 1037 { 1038 playfield.tutorialStepStart = 1; 1039 } 1040 else 1041 { 1042 playfield.tutorialStepStart = 0; 1043 }
1044 playfield.tutorialStep = playfield.nextTutorialStep; 1045 } 1046 1047 // out of time 1048 // void DrawWaterPlant(float x, float y, float height, Color color) 1049 // { 1050 // Vector2 lineStrip[90]; 1051 // float yStep = height / 30; 1052 // for (int i = 0; i < 45; i++) 1053 // { 1054 // float t = i / 45.0f; 1055 // float w = powf(t, 0.6f) * height / 90.0f; 1056 // float offset = sinf(gameTime.time * 2.0f + i * 0.1f) * 2.0f; 1057 // lineStrip[i * 2] = (Vector2){x + w + offset, y - i * yStep}; 1058 // lineStrip[i * 2 + 1] = (Vector2){x - w + offset, y - i * yStep}; 1059 // } 1060 // DrawTriangleStrip(lineStrip, 90, color);
1061 // } 1062 1063 int main(void) 1064 { 1065 // Initialization 1066 //-------------------------------------------------------------------------------------- 1067 const int screenWidth = 800; 1068 const int screenHeight = 450;
1069 1070 InitWindow(screenWidth, screenHeight, "Shrimpy Bubble Merge");
1071 InitAudioDevice(); 1072 1073 SetTargetFPS(60); // Set our game to run at 60 frames-per-second 1074 //-------------------------------------------------------------------------------------- 1075 1076 TraceLog(LOG_INFO, "loading shaders"); 1077 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs"); 1078 // Get shader locations 1079 outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize"); 1080 outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor"); 1081 textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize"); 1082 1083 LoadStorage(); 1084 LogStorage();
1085 1086 texShrimpy = LoadTexture("data/shrimpy.png");
1087 texLeftCorner = LoadTexture("data/left-corner.png"); 1088 texUiAtlas = LoadTexture("data/ui-atlas.png"); 1089 texGround = LoadTexture("data/ground.png");
1090 fontMainMedium = LoadFontEx("data/IndieFlower-Regular.ttf", 40, 0, 0); 1091 fontMainLarge = LoadFontEx("data/IndieFlower-Regular.ttf", 70, 0, 0); 1092 sfxPop = LoadSound("data/pop1.wav"); 1093 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24)); 1094 1095 Vector2 floatingBubbles[32]; 1096 Vector2 floatingBubblesVelocities[32]; 1097 for (int i = 0; i < 32; i++) 1098 { 1099 floatingBubbles[i] = (Vector2){GetRandomValue(0, screenWidth), GetRandomValue(0, screenHeight)}; 1100 floatingBubblesVelocities[i] = (Vector2){GetRandomValue(-100, 100) * 0.1f, GetRandomValue(-100, 100) * 0.1f};
1101 } 1102 // Main game loop 1103 while (!WindowShouldClose()) // Detect window close button or ESC key 1104 { 1105 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight()) 1106 { 1107 UnloadRenderTexture(bubbleFieldTexture); 1108 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight()); 1109 } 1110 1111 float dt = GetFrameTime(); 1112 // clamp dt to prevent large time steps, e.g. when browser tab is inactive 1113 if (dt > 0.2f) dt = 0.2f; 1114 gameTime.time += dt; 1115 gameTime.deltaTime = dt;
1116 1117 BeginDrawing(); 1118 1119 DrawTexture(texLeftCorner, 0, GetScreenHeight() - texLeftCorner.height - 16, WHITE); 1120 1121 float decay = expf(-dt * 2.0f); 1122 for (int i=0; i < 32; i++) 1123 { 1124 DrawCircle(floatingBubbles[i].x, floatingBubbles[i].y, i %4 + 2, ColorLerp(WHITE, COLOR_FROM_HEX(0x4488cc), sinf(gameTime.time + i * 0.1f) * 0.5f + 0.5f)); 1125 floatingBubbles[i] = Vector2Add(floatingBubbles[i], Vector2Scale(floatingBubblesVelocities[i], dt)); 1126 floatingBubblesVelocities[i] = Vector2Add(floatingBubblesVelocities[i], (Vector2){GetRandomValue(-100,100) * 0.05f, -10.0f * dt}); 1127 floatingBubblesVelocities[i] = Vector2Scale(floatingBubblesVelocities[i], decay); 1128 if (floatingBubbles[i].x < 0 || floatingBubbles[i].x > GetScreenWidth() 1129 || floatingBubbles[i].y < 0 || floatingBubbles[i].y > GetScreenHeight()) 1130 { 1131 floatingBubbles[i].x = GetRandomValue(0, GetScreenWidth()); 1132 floatingBubbles[i].y = GetRandomValue(0, GetScreenHeight()); 1133 floatingBubblesVelocities[i].x = GetRandomValue(-100, 100) * 0.1f; 1134 floatingBubblesVelocities[i].y = GetRandomValue(-100, 100) * 0.1f; 1135 }
1136 } 1137 1138 int y = GetScreenHeight() - texGround.height; 1139 for (int x=0; x<screenWidth; x+=texGround.width) 1140 { 1141 DrawTexture(texGround, x, y, WHITE); 1142 } 1143 1144 // draw wavy water line at top 1145 Vector2 waterLine[80]; 1146 float waterXStep = GetScreenWidth() / 38; 1147 for (int i = 0; i < 40; i++) 1148 { 1149 waterLine[i * 2] = (Vector2){i * waterXStep, 0}; 1150 waterLine[i * 2 + 1] = (Vector2){i * waterXStep, 15 1151 + sinf(gameTime.time * 2.9f + waterLine[i * 2 + 1].x * 0.008f) * 2.0f 1152 + sinf(gameTime.time * 4.3f + waterLine[i * 2 + 1].x * 0.015f) * 1.0f 1153 + sinf(gameTime.time * 6.9f + waterLine[i * 2 + 1].x * 0.032f) * 0.5f 1154 }; 1155 } 1156 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0x88ccff)); 1157 1158 for (int i = 0; i < 40; i++) 1159 { 1160 float x = i * waterXStep; 1161 waterLine[i * 2 + 1] = (Vector2){x, 16 1162 + sinf(gameTime.time * 2.9f + x * 0.008f) * 2.0f 1163 + sinf(gameTime.time * 4.3f + x * 0.015f) * 1.0f 1164 + sinf(gameTime.time * 6.9f + x * 0.032f) * 0.5f 1165 }; 1166 waterLine[i * 2] = (Vector2){x, waterLine[i * 2 + 1].y - 2.0f};
1167 } 1168 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0xeef8ff)); 1169
1170 // DrawWaterPlant(50, GetScreenHeight() - 20, 130, COLOR_FROM_HEX(0x449955)); 1171 1172 1173 1174 switch (currentScene) 1175 { 1176 default: UpdateSceneMenu(); break; 1177 case GAME_SCENE_SETTINGS: UpdateSceneSettings(); break; 1178 case GAME_SCENE_PLAY: UpdateScenePlay(); break; 1179 case GAME_SCENE_GAMEOVER: UpdateSceneGameOver(); break; 1180 } 1181 1182 UpdateTutorialSystem(); 1183 1184 switch (nextScene) 1185 { 1186 case GAME_SCENE_NONE: break; 1187 default: currentScene = nextScene; break; 1188 case GAME_SCENE_GAMEOVER: 1189 StoreHighScore(CalculatePlayfieldScores(&playfield)); 1190 currentScene = GAME_SCENE_GAMEOVER; 1191 break; 1192 case GAME_SCENE_PLAY: 1193 playfield = (Playfield){ 1194 .fieldSize = {230, 300}, 1195 .waterLevel = 200.0f, 1196 .minHeight = 90.0f, 1197 .maxHeight = 280.0f, 1198 }; 1199 if (gameDifficulty == GAME_DIFFICULTY_TUTORIAL) 1200 { 1201 playfield.waterLevel = 180.0f; 1202 } 1203 currentScene = GAME_SCENE_PLAY; break; 1204 } 1205
1206 if (nextScene != GAME_SCENE_NONE) 1207 {
1208 gameTime.fixedTime = gameTime.time; 1209 isClickActionBlocked = 0; 1210 playfield.tutorialStep = 0; 1211 playfield.nextTutorialStep = 0; 1212 } 1213 1214 nextScene = GAME_SCENE_NONE; 1215 EndDrawing(); 1216 } 1217 1218 // De-Initialization 1219 //-------------------------------------------------------------------------------------- 1220 CloseWindow(); // Close window and OpenGL context 1221 //-------------------------------------------------------------------------------------- 1222 1223 return 0; 1224 }
  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 Bubble poppingBubbles[MAX_BUBBLES];
85 Matrix transform; 86 Vector2 fieldSize; 87 float waterLevel; 88 float maxHeight;
89 float minHeight; 90 uint8_t comboPop; 91 uint8_t comboPopPoints;
92 uint8_t nextBubbleLevel; 93 uint8_t nextBubbleType; 94 // every placed bubble when a bubble is outside the zone counts 95 // as a strike, 3 strikes and it's game over. Strikes are 96 // reset when all bubbles are inside the zone. 97 uint8_t strikes; 98 uint8_t tutorialStepStart; 99 uint8_t enableTutorialMerge; 100 float gameoverCooldown; 101 uint32_t spawnedBubbleCount; 102 uint32_t tutorialStep; 103 uint32_t nextTutorialStep; 104 } Playfield; 105 106 typedef struct GameTime 107 { 108 float time; 109 float deltaTime; 110 float fixedTime; 111 float fixedDeltaTime; 112 } GameTime; 113 114 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF} 115 #endif

Todos:

After the game jam: Retrospective

As always, the final hours were quite hectic. I got a couple of things polished. The game was received very well and in the final votes, I got a 1st place in the category "Theme" and 2nd place in the category gameplay. What I am most proud of is however was how many people kept playing the game for quite a while - and moreover, that there were no bugs, besides issues on mobile devices and a problem with mapping mouse/touch inputs to the screen when the game runs in fullscreen mode. This is however a raylib issue. Maybe I should investigate this and see if I can fix it.

I added the game to the projects section, you can find it here.

The global game jam link is here.

Of all the game jams I have participated in, I think this was the most finished game and coming closest to the theme's topic.

Using raylib worked pretty well, just like the blog system I have set up. Being able to pass a link to other participants is great. I think it also helped me to focus on the game play and not on technical or graphical aspects early on. In the final hours, I would have loved to have some authoring tools for integrating effects, but I think I still managed to get a lot in and the limitations forced me to be creative.

What I found interesting: I used copilot and in most cases its contributions were rather minor. But in some cases, the suggestions were quite useful nonetheless.

When I didn't get audio to work, I asked for help and got the hint that I need to call the InitAudioDevice function, which I forgot about.

Bugwise there was not really much, not even during development: The initial physics implementation for the bubbles kept me busy for a few hours, mostly because signs were wrong. Another trouble giver was the shader for the bubble outlines. It didn't work correctly on mobile and the reason was the precision mode of the shader. After setting it to highp, it worked.

The structure of the game is following a common pattern that works pretty well:

In total, the entire source and header file is only 1.3k lines of code. That is not really much.

Another thing I've learned: I can use my Android tablet as an external monitor and even use its touch and pen input. I drew the artworks this way and it was pretty nice since the pen for that tablet is more precise than the stylus I have for my notebook.

What was also nice: I had all game states and scoring system in the game by yesterday afternoon. That gave me a lot of time to polish and add test the game.

Surprising: I started out with a plain copy of Cosmic Collapse and the idea to just retheming it. But over time, I changed some behavior of the physics, so by now, I think it differs quite a bit from my inspiration game. The waterline where the buyancy changes makes the game feel quite different in my opinion. Usually I am very reluctant to copy mechanics from other games during a game jam, but maybe it is actually useful to have a good starting point. I think I would try this again in the future.

As for what to do with the result: I think the game is fairly complete. There are several things I can think of that would be nice to have:

I think once these are in, I could upload it to browser gaming sites and see how it performs.

🍪