diff --git a/README.md b/README.md index beaaf4f..d381b78 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,230 @@ -## 版本日志: +# 纸牌上的坦克大战 -v16: 添加了全局背景音效 +基于 C++17 + SDL2 的 2D 坦克对战游戏,支持三种难度、实时战斗与胜负判定。 -v17: 修改了一点bug,在返回菜单后调用InitMap(),避免再次进入游戏界面时有脏数据导致显示出错 +## 功能特性 -v18: 从 Windows/EasyX 移植到 SDL2,支持 Linux;修复启动黑屏问题(自动设置工作目录为可执行文件所在目录) \ No newline at end of file +- **3 档难度**:简单(HP 5 / ATK 1)、中等(HP 10 / ATK 2)、困难(HP 15 / ATK 3) +- **实时坦克对战**:WASD 移动 + 空格开火,敌人 AI 随机移动与射击 +- **战场交互**:撞子弹扣血、踩残骸回血 + 提升攻击力、墙壁阻挡 +- **胜负判定**:消灭全部 10 辆敌方坦克获胜,HP 归零失败 +- **背景音乐**:循环 BGM 播放(静音自动降级) +- **跨平台**:Linux / Windows / macOS,CMake 一键构建 + +--- + +## 开发环境 + +| 依赖 | 版本要求 | +|------|----------| +| C++ | 17+ | +| CMake | 3.10+ | +| SDL2 | 2.x | +| SDL2_image | 2.x | +| SDL2_mixer | 2.x | +| SDL2_ttf | 2.x | +| 中文字体 | wqy-microhei 或 noto-cjk(用于界面文本渲染) | + +### Linux + +```bash +# Ubuntu / Debian +sudo apt install build-essential cmake \ + libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ + fonts-wqy-microhei + +# Fedora +sudo dnf install gcc-c++ cmake \ + SDL2-devel SDL2_image-devel SDL2_mixer-devel SDL2_ttf-devel \ + wqy-microhei-fonts + +# 构建 +cmake -B build +cmake --build build +./build/tank_battles_on_the_scrap_paper +``` + +### macOS + +```bash +brew install cmake sdl2 sdl2_image sdl2_mixer sdl2_ttf +cmake -B build +cmake --build build +./build/tank_battles_on_the_scrap_paper +``` + +### Windows + +```powershell +# 1. 安装 CMake(https://cmake.org/download) +# 2. 安装 vcpkg 并安装 SDL2 依赖: +vcpkg install sdl2 sdl2-image sdl2-mixer sdl2-ttf + +# 构建 +cmake -B build -DCMAKE_TOOLCHAIN_FILE=/scripts/buildsystems/vcpkg.cmake +cmake --build build +.\build\Release\tank_battles_on_the_scrap_paper.exe +``` + +--- + +## 操作指南 + +| 按键 | 功能 | +|------|------| +| W / ↑ | 向上移动 | +| S / ↓ | 向下移动 | +| A / ← | 向左移动 | +| D / → | 向右移动 | +| 空格 | 发射子弹 | +| ESC | 返回主菜单 | + +鼠标左键点击按钮进行菜单导航。 + +--- + +## 玩法说明 + +1. 主菜单选择**开始游戏**(使用当前难度设置)或**难度设置** +2. 操控红色坦克在 20×20 地图中移动、射击敌方蓝色坦克 +3. 踩到敌方坦克残骸(`is_taken`)可 +1 HP(上限 5)并 +1 攻击力 +4. 撞到敌方子弹扣 1 HP +5. 击败全部 10 辆敌方坦克即**胜利**;玩家 HP 归零则**失败** +6. 子弹射到墙壁消失;敌方子弹射到玩家扣 HP 并消失 + +--- + +## 项目结构 + +``` +tank_battles_on_the_scrap_paper/ +├── CMakeLists.txt # CMake 构建配置 +├── README.md +├── tank_battles_on_the_scrap_paper/ +│ ├── main.cpp # 入口:创建 Game 实例,调用 run() +│ ├── data_config.h # 全局常量(网格大小、数量上限、枚举) +│ ├── images/ # 58 张 PNG/JPG 素材 +│ ├── music/ # 背景音乐 bgm.wav +│ └── src/ +│ ├── Game.h / Game.cpp # 状态机主循环 + 游戏逻辑 +│ ├── AssetManager.h / .cpp # 纹理 / 字体 / BGM 加载与缓存 +│ ├── Renderer.h / .cpp # SDL 渲染封装 +│ ├── Map.h / .cpp # 地图数据 + 碰撞检测 +│ ├── Tank.h / .cpp # 坦克实体 +│ ├── Bullet.h / .cpp # 子弹实体 +│ └── UI.h # 界面按钮区域常量 +└── .gitignore +``` + +`images/` 与 `music/` 在 CMake 构建时自动拷贝到 `build/` 目录,游戏通过 `SDL_GetBasePath()` 自动定位可执行文件所在目录加载资源。 + +--- + +## 核心模块 + +### `Game` — 状态机主循环 + +| 职责 | 说明 | +|------|------| +| 场景管理 | `enum class Scene { MENU, LEVEL, GAME, WIN, LOSE, QUIT }` 驱动主循环,彻底消除旧版递归函数跳转导致的栈溢出隐患 | +| 事件分发 | `handleEvents()` 统一处理 SDL 事件(退出 / ESC / 鼠标点击),根据当前场景路由到不同按钮命中检测 | +| 更新循环 | `updateGame()` 每帧执行:玩家输入 → 玩家移动 / 射击 → 敌人 AI 移动 / 射击 → 子弹移动与碰撞 → 地图同步 | +| 渲染调度 | `renderScene()` 根据场景调用对应渲染函数 | + +**场景跳转流程**: + +``` +MENU ──开始游戏──→ GAME ──胜利──→ WIN ──返回──→ MENU + │ │ │ + ├──难度设置──→ LEVEL │ │ + │ │ └──失败──→ LOSE ──返回──┘ + │ └──返回──→ MENU + └──退出游戏──→ 退出 +``` + +### `Map` — 地图与碰撞 + +| 方法 | 职责 | +|------|------| +| `reset()` | 初始化 20×20 网格:四周为 `WALL`,内部为 `BLANK` | +| `get(x, y)` | 返回指定坐标的 `CELL_Type` 枚举值 | +| `set(x, y, type)` | 写入单元,每帧同步坦克位置 | +| `peek(x, y, dir)` | 查看某方向前方一格的内容,供坦克和子弹碰撞检测 | + +### `Tank` — 坦克实体 + +| 属性 | 说明 | +|------|------| +| `x, y` | 网格坐标 | +| `hp` | 生命值(玩家默认 5,敌人由难度决定) | +| `attack` | 攻击力(决定子弹伤害) | +| `dir` | 方向(`UP/DOWN/LEFT/RIGHT`) | +| `isTaken` | 仅敌方:残骸是否已被玩家拾取 | + +方向向量 `dx(dir)` / `dy(dir)` 统一处理四方向位移,消除旧版中 4 份复制粘贴的 switch 逻辑。 + +### `Bullet` — 子弹实体 + +| 方法 | 职责 | +|------|------| +| `fire(x, y, dir, dmg)` | 初始化子弹(位置、方向、伤害值) | +| `peekAt(map)` | 检测前方一格的地图单元类型 | +| `step()` | 向前移动一格 | +| `isLive` | 是否存活(碰墙/命中后置 `false`) | + +玩家子弹命中 `BLUE_TANK` 时遍历敌人数组扣血;敌方子弹命中 `RED_TANK` 时直接扣玩家 HP。子弹碰 `WALL` 立即消失。 + +### `AssetManager` — 资源管理 + +- **纹理缓存**:`std::unordered_map` 按路径缓存,避免重复加载 +- **字体加载**:`TTF_OpenFont()` 带多路径回退(wqy-microhei → wqy-zenhei → noto-cjk) +- **BGM 管理**:`Mix_LoadMUS()` + `Mix_PlayMusic(-1)` 循环播放,失败静默降级 +- **生命周期**:析构时自动释放全部纹理、字体、音频资源 + +### `Renderer` — 渲染封装 + +| 方法 | 职责 | +|------|------| +| `clear()` | 填充黑色背景 | +| `present()` | 提交帧缓冲区到屏幕 | +| `drawTexture()` | 渲染纹理到指定矩形区域 | +| `drawImage()` | 加载图片并渲染(通过 AssetManager 缓存) | +| `drawText()` | TTF 文字渲染(UTF-8 编码,`SDL_RenderCopy` 绘制) | +| `drawInt()` | 整数值渲染(`std::to_string` 后调用 `drawText`) | + +### `UI` — 界面常量 + +| 常量 | 用途 | +|------|------| +| `BTN_START` | 开始游戏按钮区域 {50, 250, 150, 50} | +| `BTN_LEVEL` | 难度设置按钮区域 {50, 330, 150, 50} | +| `BTN_QUIT` | 退出游戏按钮区域 {50, 410, 150, 50} | +| `BTN_EASY/MEDIUM/HARD` | 难度选择按钮区域 | +| `BTN_BACK` | 返回按钮区域 {30, 550, 150, 50} | +| `hit(rect, mx, my)` | 鼠标命中检测辅助函数 | + +--- + +## 计时机制 + +游戏使用 `SDL_GetTicks()` 替代跨平台不一致的 `clock()`,各逻辑模块以毫秒为间隔独立刷新: + +| 模块 | 间隔 | 说明 | +|------|------|------| +| 玩家移动 | 100 ms | 按住方向键连续移动 | +| 玩家射击 | 500 ms | 按住空格连续发射 | +| 敌人移动 | 1000 ms | 随机方向移动 | +| 敌人射击 | 3000 ms | 每个存活敌人发射一发 | +| 玩家子弹 | 100 ms | 逐格前进 | +| 敌方子弹 | 500 ms | 逐格前进 | + +--- + +## 版本日志 + +| 版本 | 变更 | +|------|------| +| v19 | 全面重构:1468 行单文件拆分为 8 个模块,状态机替代递归,全局变量清零,方向向量消除 4x 复制粘贴,修复 EnemiesMove 循环变量遮蔽 bug | +| v18 | 从 Windows/EasyX 迁移到 SDL2,支持跨平台;修复启动黑屏(`SDL_GetBasePath` 自动定位资源目录) | +| v17 | 修复返回菜单后 `InitMap()` 未调用导致地图脏数据 | +| v16 | 添加全局 BGM 背景音乐循环播放 | diff --git a/tank_battles_on_the_scrap_paper/src/Game.cpp b/tank_battles_on_the_scrap_paper/src/Game.cpp index 6f8a35c..5f6cbe2 100644 --- a/tank_battles_on_the_scrap_paper/src/Game.cpp +++ b/tank_battles_on_the_scrap_paper/src/Game.cpp @@ -98,8 +98,7 @@ void Game::handleEvents(SDL_Event& e) { if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) { switch (scene) { case Scene::LEVEL: - case Scene::MEMBER: - case Scene::GAME: + case Scene::GAME: case Scene::WIN: case Scene::LOSE: scene = Scene::MENU; @@ -117,7 +116,6 @@ void Game::handleEvents(SDL_Event& e) { case Scene::MENU: if (UI::hit(UI::BTN_START, mx, my)) { initGame(); scene = Scene::GAME; } if (UI::hit(UI::BTN_LEVEL, mx, my)) { scene = Scene::LEVEL; } - if (UI::hit(UI::BTN_MEMBER, mx, my)) { scene = Scene::MEMBER; } if (UI::hit(UI::BTN_QUIT, mx, my)) { running = false; } break; case Scene::LEVEL: @@ -131,9 +129,6 @@ void Game::handleEvents(SDL_Event& e) { enemyCfg.hp = 15; enemyCfg.attack = 3; scene = Scene::MENU; } break; - case Scene::MEMBER: - if (UI::hit(UI::BTN_BACK, mx, my)) scene = Scene::MENU; - break; case Scene::WIN: case Scene::LOSE: if (UI::hit(UI::BTN_BACK, mx, my)) scene = Scene::MENU; @@ -332,7 +327,6 @@ void Game::renderScene() { switch (scene) { case Scene::MENU: renderMenu(); break; case Scene::LEVEL: renderLevel(); break; - case Scene::MEMBER: renderMember(); break; case Scene::GAME: renderGameView(); break; case Scene::WIN: renderWin(); break; case Scene::LOSE: renderLose(); break; @@ -347,7 +341,6 @@ void Game::renderMenu() { render->drawImage("images/菜单界面.jpg", 0, 0, WINDOW_W, WINDOW_H); render->drawImage("images/开始游戏按键.jpg", UI::BTN_START.x, UI::BTN_START.y, UI::BTN_START.w, UI::BTN_START.h); render->drawImage("images/难度设置按键.jpg", UI::BTN_LEVEL.x, UI::BTN_LEVEL.y, UI::BTN_LEVEL.w, UI::BTN_LEVEL.h); - render->drawImage("images/相关人员按键.jpg", UI::BTN_MEMBER.x, UI::BTN_MEMBER.y, UI::BTN_MEMBER.w, UI::BTN_MEMBER.h); render->drawImage("images/退出游戏按键.jpg", UI::BTN_QUIT.x, UI::BTN_QUIT.y, UI::BTN_QUIT.w, UI::BTN_QUIT.h); } @@ -358,11 +351,6 @@ void Game::renderLevel() { render->drawImage("images/困难难度.jpg", UI::BTN_HARD.x, UI::BTN_HARD.y, UI::BTN_HARD.w, UI::BTN_HARD.h); } -void Game::renderMember() { - render->drawImage("images/开发人员界面.jpg", 0, 0, WINDOW_W, WINDOW_H); - render->drawImage("images/返回按钮.jpg", UI::BTN_BACK.x, UI::BTN_BACK.y, UI::BTN_BACK.w, UI::BTN_BACK.h); -} - void Game::renderGameView() { render->drawImage("images/游戏主界面背景.jpg", 0, 0, WINDOW_W, WINDOW_H); diff --git a/tank_battles_on_the_scrap_paper/src/Game.h b/tank_battles_on_the_scrap_paper/src/Game.h index cf3f3c3..99bd902 100644 --- a/tank_battles_on_the_scrap_paper/src/Game.h +++ b/tank_battles_on_the_scrap_paper/src/Game.h @@ -10,7 +10,7 @@ #include "AssetManager.h" #include "Renderer.h" -enum class Scene { MENU, LEVEL, MEMBER, GAME, WIN, LOSE, QUIT }; +enum class Scene { MENU, LEVEL, GAME, WIN, LOSE, QUIT }; class Game { public: @@ -51,7 +51,6 @@ private: void renderMenu(); void renderLevel(); - void renderMember(); void renderGameView(); void renderWin(); void renderLose(); diff --git a/tank_battles_on_the_scrap_paper/src/UI.h b/tank_battles_on_the_scrap_paper/src/UI.h index ce59453..190ae20 100644 --- a/tank_battles_on_the_scrap_paper/src/UI.h +++ b/tank_battles_on_the_scrap_paper/src/UI.h @@ -12,8 +12,7 @@ inline bool hit(const SDL_Rect& r, int mx, int my) { // 菜单按钮 constexpr SDL_Rect BTN_START = {50, 250, 150, 50}; constexpr SDL_Rect BTN_LEVEL = {50, 330, 150, 50}; -constexpr SDL_Rect BTN_MEMBER = {50, 410, 150, 50}; -constexpr SDL_Rect BTN_QUIT = {50, 490, 150, 50}; +constexpr SDL_Rect BTN_QUIT = {50, 410, 150, 50}; // 难度按钮 constexpr SDL_Rect BTN_EASY = {275, 150, 150, 100};