From 823d9872971fa689870361022de41b4b94c60f89 Mon Sep 17 00:00:00 2001 From: fengjiansu Date: Wed, 3 Jun 2026 13:56:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=A4=BA=E4=BE=8B=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE&&=E4=BF=AE=E6=94=B908=5Fbreakout=20=20=20=20=20-?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=A7=81=E5=B3=B0=E6=8F=92=E9=92=88=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=20=20=20=20=20-08=5Fbreakout=20=E5=B0=8F=E7=90=83?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=96=B9=E5=90=91=E8=AE=BE=E4=B8=BA=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=20=20=20=20=20-o8=5Fbreakout=20=E5=B0=86=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=80=BC=E8=AE=BE=E4=B8=BA=E5=B8=B8=E9=87=8F,?= =?UTF-8?q?=E4=BE=BF=E4=BA=8E=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- SDL2PORT.md | 4 +- docs/GameLib.SDL.md | 2 +- examples/08_breakout.cpp | 48 ++++-- examples/16_coreball.cpp | 307 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 347 insertions(+), 17 deletions(-) create mode 100644 examples/16_coreball.cpp diff --git a/README.md b/README.md index 621ab50..bb1c94f 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Dev C++ 用户:新建项目 > 添加 `main.cpp` > 编译运行,完事。 ## 示例程序 -`examples/` 目录包含 15 个由浅入深的示例,每个兼容 Win32 和 SDL 两条产品线: +`examples/` 目录包含 16 个由浅入深的示例,每个兼容 Win32 和 SDL 两条产品线: ### 入门基础 @@ -142,6 +142,7 @@ Dev C++ 用户:新建项目 > 添加 `main.cpp` > 编译运行,完事。 | `13_clip_rect.cpp` | 裁剪矩形 | SetClip/ClearClip | | `14_space_shooter.cpp` | 太空射击 | 综合实战 | | `15_ui_controls.cpp` | UI 控件演示 | Button、Checkbox、RadioBox | +| `16_coreball.cpp` | 见缝插针 | 圆周运动、角度碰撞、状态机 | 编译任意示例: diff --git a/SDL2PORT.md b/SDL2PORT.md index 98cfc7f..e8dfb9f 100644 --- a/SDL2PORT.md +++ b/SDL2PORT.md @@ -186,5 +186,5 @@ Windows 下如果使用动态库版本,需要让运行时能找到这些 DLL ### 5.6 迁移现状 -- 当前 SDL 回归入口是 `examples/01_hello.cpp` ~ `examples/15_ui_controls.cpp`,这 15 个统一示例通过预处理器自动选择 Win32 或 SDL 后端,同时作为两条产品线的回归验证。 -- 覆盖路径包括:基础窗口、图元、精灵、动画、字体、音频、Tilemap、裁剪矩形、缩放、旋转与 UI 控件。 \ No newline at end of file +- 当前 SDL 回归入口是 `examples/01_hello.cpp` ~ `examples/16_coreball.cpp`,这 16 个统一示例通过预处理器自动选择 Win32 或 SDL 后端,同时作为两条产品线的回归验证。 +- 覆盖路径包括:基础窗口、图元、精灵、动画、字体、音频、Tilemap、裁剪矩形、缩放、旋转、UI 控件与角度碰撞小游戏。 diff --git a/docs/GameLib.SDL.md b/docs/GameLib.SDL.md index 7f6f1f6..a8f4850 100644 --- a/docs/GameLib.SDL.md +++ b/docs/GameLib.SDL.md @@ -1156,7 +1156,7 @@ static bool _srandDone; - `docs/GameLib.SDL.md` 已同步到当前实现状态,仓库根目录也已新增 `SDL2PORT.md` 作为简版移植说明。 - `AGENTS.md` 已补充 `GameLib.SDL.h` / `docs/GameLib.SDL.md` 的索引与用途。 - README 仍只保留一句 SDL 产品线提示,主叙事继续突出 Win32 零依赖主线;具体 SDL 编译命令与限制统一放到 `SDL2PORT.md`。 -- `examples/01_hello.cpp` ~ `examples/15_ui_controls.cpp` 已统一为 Win32 / SDL 双后端示例,通过预处理器自动选择后端,覆盖基础窗口、图元、精灵、动画、字体、音频、Tilemap、裁剪矩形、缩放、旋转与 UI 控件等路径,同时作为 SDL 版的回归入口。 +- `examples/01_hello.cpp` ~ `examples/16_coreball.cpp` 已统一为 Win32 / SDL 双后端示例,通过预处理器自动选择后端,覆盖基础窗口、图元、精灵、动画、字体、音频、Tilemap、裁剪矩形、缩放、旋转、UI 控件与角度碰撞小游戏等路径,同时作为 SDL 版的回归入口。 - 本阶段收口结论:`GameLib.SDL.h` 已具备独立产品线的最小可维护状态,后续工作可以从“补齐代表性回归入口”切换到“按具体差异或新需求增量演进”。 ### 14.5 当前已知差异与可继续完善项 diff --git a/examples/08_breakout.cpp b/examples/08_breakout.cpp index 2a2af21..4ebbcfb 100644 --- a/examples/08_breakout.cpp +++ b/examples/08_breakout.cpp @@ -23,6 +23,19 @@ #define BRICK_OFFSET_X 12 #define BRICK_OFFSET_Y 50 +static const int PADDLE_W = 80; +static const int PADDLE_H = 12; +static const int PADDLE_START_X = 280; +static const int PADDLE_Y = 450; +static const int PADDLE_SPEED = 6; + +static const float BALL_START_VY = -4.0f; +static const float BALL_START_MIN_ABS_VX = 1.5f; +static const float BALL_START_MAX_ABS_VX = 4.0f; +static const int BALL_R = 5; +static const float BALL_PADDLE_MAX_VX = 8.0f; +static const int LIVES_START = 1; + static const char *ChooseExistingPath(const char *pathA, const char *pathB) { FILE *file = fopen(pathA, "rb"); @@ -32,8 +45,17 @@ static const char *ChooseExistingPath(const char *pathA, const char *pathB) return pathA; } -int main() -{ +static void ResetBallVelocity(float *vx, float *vy) +{ + int minSpeedX10 = (int)(BALL_START_MIN_ABS_VX * 10.0f); + int maxSpeedX10 = (int)(BALL_START_MAX_ABS_VX * 10.0f); + float speed = (float)GameLib::Random(minSpeedX10, maxSpeedX10) / 10.0f; + if (GameLib::Random(0, 1) == 0) speed = -speed; + *vx = speed; + *vy = BALL_START_VY; +} +int main() +{ GameLib game; game.Open(640, 480, "08 - Breakout", true); @@ -55,14 +77,14 @@ int main() bool bricks[BRICK_ROWS][BRICK_COLS]; uint32_t brickColors[] = {COLOR_RED, COLOR_ORANGE, COLOR_YELLOW, COLOR_GREEN, COLOR_CYAN, COLOR_PURPLE}; - int padW = 80, padH = 12; - int padX = 280, padY = 450; + int padW = PADDLE_W, padH = PADDLE_H; + int padX = PADDLE_START_X, padY = PADDLE_Y; float ballX = 320, ballY = 430; - float ballVX = 3.0f, ballVY = -4.0f; - int ballR = 5; + float ballVX = 0.0f, ballVY = BALL_START_VY; + int ballR = BALL_R; - int score = 0, lives = 3, totalBricks = 0; + int score = 0, lives = LIVES_START, totalBricks = 0; bool started = false, gameOver = false, gameWin = false; for (int r = 0; r < BRICK_ROWS; r++) @@ -75,8 +97,8 @@ int main() if (game.IsKeyPressed(KEY_ESCAPE)) break; if (!gameOver && !gameWin) { - if (game.IsKeyDown(KEY_LEFT)) padX -= 6; - if (game.IsKeyDown(KEY_RIGHT)) padX += 6; + if (game.IsKeyDown(KEY_LEFT)) padX -= PADDLE_SPEED; + if (game.IsKeyDown(KEY_RIGHT)) padX += PADDLE_SPEED; if (padX < 0) padX = 0; if (padX + padW > game.GetWidth()) padX = game.GetWidth() - padW; @@ -85,7 +107,7 @@ int main() ballY = (float)(padY - ballR - 1); if (game.IsKeyPressed(KEY_SPACE)) { started = true; - ballVX = 3.0f; ballVY = -4.0f; + ResetBallVelocity(&ballVX, &ballVY); sfxToPlay = launchSfx; } } else { @@ -107,7 +129,7 @@ int main() if (ballY + ballR > game.GetHeight()) { lives--; if (lives <= 0) { gameOver = true; sfxToPlay = gameOverSfx; } - else { started = false; ballVX = 3.0f; ballVY = -4.0f; sfxToPlay = loseLifeSfx; } + else { started = false; ballVX = 0.0f; ballVY = BALL_START_VY; sfxToPlay = loseLifeSfx; } } if (ballVY > 0 && @@ -116,7 +138,7 @@ int main() ballVY = -ballVY; ballY = (float)(padY - ballR); float hitPos = (ballX - padX) / padW; - ballVX = (hitPos - 0.5f) * 8.0f; + ballVX = (hitPos - 0.5f) * BALL_PADDLE_MAX_VX; if (!sfxToPlay) sfxToPlay = bounceSfx; } @@ -156,7 +178,7 @@ int main() for (int c = 0; c < BRICK_COLS; c++) bricks[r][c] = true; totalBricks = BRICK_ROWS * BRICK_COLS; - score = 0; lives = 3; padX = 280; + score = 0; lives = LIVES_START; padX = PADDLE_START_X; started = false; gameOver = false; gameWin = false; sfxToPlay = restartSfx; } diff --git a/examples/16_coreball.cpp b/examples/16_coreball.cpp new file mode 100644 index 0000000..e597177 --- /dev/null +++ b/examples/16_coreball.cpp @@ -0,0 +1,307 @@ +// 16_coreball.cpp - Coreball / Pin the Core +// +// Shoot all waiting balls onto the rotating core. If the new ball touches +// an existing ball, the level fails. +// Learn: circular motion, angle math, state machine, circle collision +// +// Compile (Win32): g++ -o 16_coreball.exe 16_coreball.cpp -mwindows +// Compile (SDL): g++ -std=c++11 -O2 -o 16_coreball 16_coreball.cpp -lSDL2 + +#if defined(_WIN32) && !defined(USE_SDL) +#include "../GameLib.h" +#else +#include "../GameLib.SDL.h" +#endif + +#include +#include + +#define MAX_PINS 64 +#define LEVEL_COUNT 10 + +static const double PI_VALUE = 3.14159265358979323846; + +static const int WIN_W = 640; +static const int WIN_H = 480; +static const int CORE_X = 320; +static const int CORE_Y = 170; +static const int CORE_R = 44; +static const int BALL_R = 10; +static const int ORBIT_R = 130; +static const int LAUNCH_X = 320; +static const int LAUNCH_Y = 410; +static const double LAUNCH_SPEED = 520.0; + +enum GameState { + STATE_READY, + STATE_LAUNCHING, + STATE_LEVEL_CLEAR, + STATE_GAME_OVER +}; + +struct LevelDef { + int shots; + int blockers; + double speedDeg; + double offsetDeg; +}; + +struct Pin { + double localAngle; + int playerShot; +}; + +static LevelDef levels[LEVEL_COUNT] = { + { 9, 3, 92.0, 36.0 }, + { 10, 3, -102.0, 0.0 }, + { 10, 4, 112.0, 24.0 }, + { 11, 4, -124.0, 45.0 }, + { 11, 5, 136.0, 18.0 }, + { 12, 5, -148.0, 54.0 }, + { 12, 6, 160.0, 12.0 }, + { 13, 7, -172.0, 30.0 }, + { 14, 8, 184.0, 20.0 }, + { 15, 9, -196.0, 10.0 } +}; + +static double DegToRad(double deg) +{ + return deg * PI_VALUE / 180.0; +} + +static const char *ChooseSavePath() +{ + FILE *file = fopen("../GameLib.h", "rb"); + if (file != NULL) { + fclose(file); + return "../coreball.save"; + } + return "coreball.save"; +} + +static double NormalizeDeg(double deg) +{ + while (deg < 0.0) deg += 360.0; + while (deg >= 360.0) deg -= 360.0; + return deg; +} + +static int PinX(double angleDeg) +{ + return CORE_X + (int)(cos(DegToRad(angleDeg)) * ORBIT_R); +} + +static int PinY(double angleDeg) +{ + return CORE_Y + (int)(sin(DegToRad(angleDeg)) * ORBIT_R); +} + +static void ResetLevel(int level, Pin pins[], int *pinCount, int *shotsLeft, + double *rotation, double *launchY, GameState *state) +{ + int i; + LevelDef def = levels[level]; + *pinCount = 0; + *shotsLeft = def.shots; + *rotation = 0.0; + *launchY = (double)LAUNCH_Y; + *state = STATE_READY; + + for (i = 0; i < def.blockers && i < MAX_PINS; i++) { + pins[*pinCount].localAngle = NormalizeDeg(def.offsetDeg + i * (360.0 / def.blockers)); + pins[*pinCount].playerShot = 0; + (*pinCount)++; + } +} + +static void DrawPin(GameLib &game, double worldAngle, int playerShot) +{ + int x = PinX(worldAngle); + int y = PinY(worldAngle); + int lineX = CORE_X + (int)(cos(DegToRad(worldAngle)) * (CORE_R - 2)); + int lineY = CORE_Y + (int)(sin(DegToRad(worldAngle)) * (CORE_R - 2)); + uint32_t ballColor = playerShot ? COLOR_WHITE : COLOR_RGB(92, 112, 150); + + game.DrawLine(lineX, lineY, x, y, COLOR_RGB(118, 132, 160)); + game.FillCircle(x, y, BALL_R, ballColor); + game.DrawCircle(x, y, BALL_R, COLOR_RGB(25, 31, 44)); +} + +static void DrawWaitingBalls(GameLib &game, int shotsLeft) +{ + int i; + int visible = shotsLeft; + if (visible > 8) visible = 8; + + for (i = 0; i < visible; i++) { + int y = LAUNCH_Y + 28 + i * 18; + uint32_t c = (i == 0) ? COLOR_WHITE : COLOR_RGB(150, 160, 178); + if (y < WIN_H - 8) { + game.FillCircle(LAUNCH_X, y, 6, c); + } + } + + if (shotsLeft > visible) { + game.DrawPrintf(LAUNCH_X + 14, WIN_H - 28, COLOR_LIGHT_GRAY, "+%d", shotsLeft - visible); + } +} + +static int WouldHitExistingBall(Pin pins[], int pinCount, double rotation) +{ + int i; + int newX = PinX(90.0); + int newY = PinY(90.0); + + for (i = 0; i < pinCount; i++) { + double worldAngle = NormalizeDeg(pins[i].localAngle + rotation); + int x = PinX(worldAngle); + int y = PinY(worldAngle); + if (GameLib::CircleOverlap(newX, newY, BALL_R + 1, x, y, BALL_R + 1)) { + return 1; + } + } + return 0; +} + +int main() +{ + GameLib game; + game.Open(WIN_W, WIN_H, "16 - Coreball", true); + game.AspectLock(true, COLOR_BLACK); + game.ShowMouse(false); + + Pin pins[MAX_PINS]; + int pinCount = 0; + int shotsLeft = 0; + int level = 0; + const char *savePath = ChooseSavePath(); + int bestLevel = GameLib::LoadInt(savePath, "best_level", 1); + double rotation = 0.0; + double launchY = (double)LAUNCH_Y; + GameState state = STATE_READY; + + ResetLevel(level, pins, &pinCount, &shotsLeft, &rotation, &launchY, &state); + + while (!game.IsClosed()) { + double dt = game.GetDeltaTime(); + int fire = 0; + int i; + + if (game.IsKeyPressed(KEY_ESCAPE)) break; + + rotation = NormalizeDeg(rotation + levels[level].speedDeg * dt); + + if (state == STATE_READY) { + if (game.IsKeyPressed(KEY_SPACE) || + game.IsMousePressed(MOUSE_LEFT) || + game.IsMousePressed(MOUSE_RIGHT)) { + fire = 1; + } + if (fire && shotsLeft > 0) { + state = STATE_LAUNCHING; + launchY = (double)LAUNCH_Y; + game.PlayBeep(520, 40, 1, 200); + } + } else if (state == STATE_LAUNCHING) { + launchY -= LAUNCH_SPEED * dt; + + if (launchY <= PinY(90.0)) { + if (WouldHitExistingBall(pins, pinCount, rotation)) { + state = STATE_GAME_OVER; + launchY = (double)PinY(90.0); + game.PlayBeep(160, 250, 1, 250); + } else { + if (pinCount < MAX_PINS) { + pins[pinCount].localAngle = NormalizeDeg(90.0 - rotation); + pins[pinCount].playerShot = 1; + pinCount++; + } + shotsLeft--; + launchY = (double)LAUNCH_Y; + game.PlayBeep(740, 50, 1, 180); + + if (shotsLeft <= 0) { + state = STATE_LEVEL_CLEAR; + if (level + 1 > bestLevel) { + bestLevel = level + 1; + GameLib::SaveInt(savePath, "best_level", bestLevel); + } + game.PlayBeep(980, 180, 1, 220); + } else { + state = STATE_READY; + } + } + } + } else if (state == STATE_LEVEL_CLEAR) { + if (game.IsKeyPressed(KEY_SPACE) || + game.IsMousePressed(MOUSE_LEFT) || + game.IsMousePressed(MOUSE_RIGHT)) { + if (level + 1 < LEVEL_COUNT) level++; + ResetLevel(level, pins, &pinCount, &shotsLeft, &rotation, &launchY, &state); + } + } else if (state == STATE_GAME_OVER) { + if (game.IsKeyPressed(KEY_R) || + game.IsKeyPressed(KEY_SPACE) || + game.IsMousePressed(MOUSE_LEFT) || + game.IsMousePressed(MOUSE_RIGHT)) { + ResetLevel(level, pins, &pinCount, &shotsLeft, &rotation, &launchY, &state); + game.PlayBeep(420, 70, 1, 180); + } + } + + game.Clear(COLOR_RGB(16, 19, 28)); + + for (i = 0; i < WIN_H; i += 40) { + uint32_t stripe = ((i / 40) & 1) ? COLOR_RGB(18, 23, 34) : COLOR_RGB(14, 17, 26); + game.FillRect(0, i, WIN_W, 40, stripe); + } + + game.DrawPrintf(18, 16, COLOR_LIGHT_GRAY, "Level %d / %d", level + 1, LEVEL_COUNT); + game.DrawPrintf(18, 32, COLOR_GRAY, "Best: %d", bestLevel); + game.DrawPrintf(WIN_W - 112, 16, COLOR_LIGHT_GRAY, "Pins: %d", shotsLeft); + + for (i = 0; i < pinCount; i++) { + double worldAngle = NormalizeDeg(pins[i].localAngle + rotation); + DrawPin(game, worldAngle, pins[i].playerShot); + } + + game.FillCircle(CORE_X, CORE_Y, CORE_R + 7, COLOR_RGB(36, 43, 62)); + game.FillCircle(CORE_X, CORE_Y, CORE_R, COLOR_RGB(232, 236, 242)); + game.DrawCircle(CORE_X, CORE_Y, CORE_R, COLOR_RGB(25, 31, 44)); + game.DrawPrintfScale(CORE_X - 12, CORE_Y - 13, COLOR_RGB(25, 31, 44), 14, 14, "%d", shotsLeft); + + if (state == STATE_LAUNCHING || state == STATE_GAME_OVER) { + game.FillCircle(LAUNCH_X, (int)launchY, BALL_R, COLOR_WHITE); + game.DrawCircle(LAUNCH_X, (int)launchY, BALL_R, COLOR_RGB(25, 31, 44)); + } else if (state == STATE_READY) { + game.FillCircle(LAUNCH_X, LAUNCH_Y, BALL_R, COLOR_WHITE); + game.DrawCircle(LAUNCH_X, LAUNCH_Y, BALL_R, COLOR_RGB(25, 31, 44)); + } + + DrawWaitingBalls(game, shotsLeft); + + game.DrawText(150, WIN_H - 34, "SPACE / LEFT / RIGHT CLICK: shoot ESC: quit", COLOR_GRAY); + + if (state == STATE_LEVEL_CLEAR) { + game.FillRect(190, 214, 260, 86, COLOR_RGB(32, 40, 58)); + game.DrawRect(190, 214, 260, 86, COLOR_RGB(130, 150, 190)); + if (level + 1 >= LEVEL_COUNT) { + game.DrawTextScale(226, 228, "ALL CLEAR", COLOR_GOLD, 12, 12); + game.DrawText(228, 268, "SPACE / CLICK to replay", COLOR_LIGHT_GRAY); + } else { + game.DrawTextScale(220, 228, "LEVEL CLEAR", COLOR_GREEN, 12, 12); + game.DrawText(228, 268, "SPACE / CLICK for next", COLOR_LIGHT_GRAY); + } + } else if (state == STATE_GAME_OVER) { + game.FillRect(198, 214, 244, 86, COLOR_RGB(54, 32, 38)); + game.DrawRect(198, 214, 244, 86, COLOR_RGB(190, 110, 120)); + game.DrawTextScale(228, 228, "GAME OVER", COLOR_RED, 12, 12); + game.DrawText(230, 268, "R / SPACE to retry", COLOR_LIGHT_GRAY); + } + + game.Update(); + game.WaitFrame(60); + } + + return 0; +}