# 纸牌上的坦克大战 基于 C++17 + SDL2 的 2D 坦克对战游戏,支持三种难度、实时战斗与胜负判定。 ## 功能特性 - **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 背景音乐循环播放 |