refactor: 项目全面重构 — 从单文件过程式重写为模块化 OOP

**问题背景**
原项目全部逻辑集中在 main.cpp(1468 行),大量全局变量、4 方向逻辑
复制粘贴 4 遍、场景通过递归函数调用跳转(栈溢出隐患),难以维护扩展。

**重构内容**

文件架构(新增 src/ 目录):
  - src/Game.h/.cpp       — 状态机驱动主循环,Scene 枚举消除递归跳转
  - src/AssetManager.h/.cpp — 纹理/字体/BGM 加载与缓存管理
  - src/Renderer.h/.cpp   — SDL 渲染封装(清屏/纹理/文字/提交)
  - src/Map.h/.cpp        — 地图数据 + 统一碰撞检测
  - src/Tank.h/.cpp       — 坦克类,dx()/dy() 方向向量消除 4x 复制粘贴
  - src/Bullet.h/.cpp     — 子弹类,peekAt()/step() 封装移动逻辑
  - src/UI.h              — 按钮区域常量 + 命中检测

核心改进:
  - 全局变量清零 → 全部归入 Game 类实例管理
  - 场景递归跳转 → Scene 枚举状态机,主循环驱动
  - 4 方向复制粘贴 → 方向向量 {dx,dy} 表统一处理
  - 修复 EnemiesMove 中循环变量 i 遮蔽 bug
  - clock() → SDL_GetTicks(),跨平台时间精度
  - 清理死代码 map1[50]、旧注释、未使用常量
  - data_config.h 精简为纯常量

文件变更:
  - 新增 7 个源文件(src/下)
  - 重写 main.cpp(1468 → 6 行入口)
  - 重写 data_config.h、CMakeLists.txt
  - 删除 tank.h、bullet.h、map.h、map.cpp(被新架构替代)
  - 更新 .gitignore(build_Debug/、.hermes/ 等)
This commit is contained in:
2026-05-03 11:53:29 +08:00
parent 3294a66787
commit 3b6f6d39d2
21 changed files with 854 additions and 1644 deletions
+7
View File
@@ -443,3 +443,10 @@ CMakeScripts/
Makefile Makefile
*.make *.make
session-*.md
.hermes/
# CMake 构建产物
build_Debug/
build_Release/
build_*/
+17 -8
View File
@@ -7,26 +7,32 @@ project(tank_battles_on_the_scrap_paper
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── SDL2 依赖 ──
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(SDL2_image REQUIRED) find_package(SDL2_image REQUIRED)
find_package(SDL2_mixer REQUIRED) find_package(SDL2_mixer REQUIRED)
find_package(SDL2_ttf REQUIRED) find_package(SDL2_ttf REQUIRED)
# ── 源文件 ──
set(SOURCES set(SOURCES
tank_battles_on_the_scrap_paper/main.cpp tank_battles_on_the_scrap_paper/main.cpp
tank_battles_on_the_scrap_paper/map.cpp tank_battles_on_the_scrap_paper/src/AssetManager.cpp
tank_battles_on_the_scrap_paper/src/Renderer.cpp
tank_battles_on_the_scrap_paper/src/Map.cpp
tank_battles_on_the_scrap_paper/src/Bullet.cpp
tank_battles_on_the_scrap_paper/src/Tank.cpp
tank_battles_on_the_scrap_paper/src/Game.cpp
) )
set(HEADERS set(HEADERS
tank_battles_on_the_scrap_paper/bullet.h
tank_battles_on_the_scrap_paper/tank.h
tank_battles_on_the_scrap_paper/map.h
tank_battles_on_the_scrap_paper/data_config.h tank_battles_on_the_scrap_paper/data_config.h
tank_battles_on_the_scrap_paper/src/AssetManager.h
tank_battles_on_the_scrap_paper/src/Renderer.h
tank_battles_on_the_scrap_paper/src/Map.h
tank_battles_on_the_scrap_paper/src/Bullet.h
tank_battles_on_the_scrap_paper/src/Tank.h
tank_battles_on_the_scrap_paper/src/Game.h
tank_battles_on_the_scrap_paper/src/UI.h
) )
# ── 可执行文件 ──
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE
@@ -36,7 +42,10 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
SDL2_ttf::SDL2_ttf SDL2_ttf::SDL2_ttf
) )
# ── 复制资源文件到构建目录 ── target_include_directories(${PROJECT_NAME} PRIVATE
tank_battles_on_the_scrap_paper
)
file(COPY file(COPY
tank_battles_on_the_scrap_paper/images/ tank_battles_on_the_scrap_paper/images/
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images/
-24
View File
@@ -1,24 +0,0 @@
// 子弹定义
#ifndef BULLET_H
#define BULLET_H
/*
子弹:
子弹坐标:xy
攻击力damage:表示当前子弹造成的伤害
存活标志:is_alive
方向dir
UP 上
DOWN 下
LEFT 左
RIGHT 右
*/
struct bullet {
int x, y;
int damage; // 子弹攻击力
int dir;
bool is_live; // 是否存活
//tank shooter; //值为enemies[i]或player
};
#endif // !BULLET_H
+29 -44
View File
@@ -1,51 +1,36 @@
// ö // 项目配置常量
#ifndef DATA_CONFIG_H #ifndef DATA_CONFIG_H
#define DATA_CONFIG_H #define DATA_CONFIG_H
// tank bullet ͷļ
#include "tank.h"
#include "bullet.h"
// // 网格
const int CELL_SIZE = 30; // ÿԪ̹ˡǽյصȣĴСأ constexpr int CELL_SIZE = 30;
const int MAP_CELL_NUM = 20; // ͼСΪMAP_CELL_NUM^2 constexpr int MAP_CELL_NUM = 20;
const int ENEMIES_NUM = 10; // ͼĵ
const int BULLET_NUM = 20; // ͼӵ
const int SINGLE_BULLET = 3; // ÿ̹ͬʱڵӵ
const int White = 0; // Ҷͼ-
const int Black = 1; // Ҷͼ-
#define PIEXL1 30 //س // 偏移
#define PIEXL2 4 constexpr int X_OFFSET = 50;
#define X_OFFSET 50 //ϵһǽΪ׼ƫ constexpr int Y_OFFSET = 45;
#define Y_OFFSET 45 //ϵһǽΪ׼ƫ
/* // 窗口
ͼ constexpr int WINDOW_W = 700;
ʹͼϢ constexpr int WINDOW_H = 700;
map[x][y] ǰ̹Ϣ
BLANK յ // 实体数量
RED_TANK 췽̹ constexpr int ENEMIES_NUM = 10;
RED_DEAD_TANK ̹ constexpr int MAX_PLAYER_BULLETS = 3;
BLUE_TANK, ̹ constexpr int MAX_ENEMY_BULLETS = ENEMIES_NUM;
BLUE_DEAD_TANK ̹
WALL ǽ // 方向
BULLET ӵ enum Direction { UP, DOWN, LEFT, RIGHT };
*/
// 地图单元类型
enum CELL_Type { enum CELL_Type {
BLANK, // յ BLANK,
RED_TANK, // 췽̹ RED_TANK,
RED_DEAD_TANK, // ̹ RED_DEAD_TANK,
BLUE_TANK, // ̹ BLUE_TANK,
BLUE_DEAD_TANK, // ̹ BLUE_DEAD_TANK,
WALL, // ǽ WALL,
BULLET, // ӵ BULLET
}; };
// 全局变量声明(extern),定义在 main.cpp 中
extern int map[MAP_CELL_NUM][MAP_CELL_NUM];
extern int kill;
extern tank player;
extern tank enemies[ENEMIES_NUM];
extern bullet E_bullets[BULLET_NUM * ENEMIES_NUM];
extern bullet P_bullets[BULLET_NUM];
extern tank enemy;
#endif // !DATA_CONFIG_H #endif
File diff suppressed because it is too large Load Diff
-54
View File
@@ -1,54 +0,0 @@
// map.cpp
#include "map.h"
// 定义两个地图数组
int map1[50][50];
int map2[50][50];
void init_map1()
{
for (int i = 0; i < 50; i++)
{
for (int j = 0; j < 50; j++)
{
// 四周为墙
if (i == 0 || i == 49 || j == 0 || j == 49)
{
map1[i][j] = WALL;
}
// 构造中间的+形状
else if (i == 24 || j == 24)
{
map1[i][j] = WALL;
}
else
{
map1[i][j] = BLANK;
}
}
}
}
void init_map2()
{
for (int i = 0; i < 50; i++)
{
for (int j = 0; j < 50; j++)
{
// 四周为墙
if (i == 0 || i == 49 || j == 0 || j == 49)
{
map2[i][j] = WALL;
}
// 构造中间的X形状
else if (i == j || i + j == 49)
{
map2[i][j] = WALL;
}
else
{
map2[i][j] = BLANK;
}
}
}
}
-16
View File
@@ -1,16 +0,0 @@
// 预制地图定义
// map.h
#ifndef MAP_H
#define MAP_H
#include "data_config.h"
// 声明两个地图数组
extern int map1[50][50];
extern int map2[50][50];
// 声明地图初始化函数
void init_map1();
void init_map2();
#endif // !MAP_H
@@ -0,0 +1,52 @@
#include "AssetManager.h"
#include <cstdio>
AssetManager::AssetManager(SDL_Renderer* r) : renderer(r) {}
AssetManager::~AssetManager() {
for (auto& [_, tex] : textures)
SDL_DestroyTexture(tex);
if (font) TTF_CloseFont(font);
if (bgm) { Mix_HaltMusic(); Mix_FreeMusic(bgm); }
}
SDL_Texture* AssetManager::getTexture(const std::string& path) {
auto it = textures.find(path);
if (it != textures.end())
return it->second;
SDL_Surface* surf = IMG_Load(path.c_str());
if (!surf) {
fprintf(stderr, "IMG_Load error: %s - %s\n", path.c_str(), IMG_GetError());
return nullptr;
}
SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
SDL_FreeSurface(surf);
if (tex) {
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
} else {
fprintf(stderr, "CreateTexture error: %s - %s\n", path.c_str(), SDL_GetError());
return nullptr;
}
textures[path] = tex;
return tex;
}
bool AssetManager::loadFont(const char* path, int size) {
font = TTF_OpenFont(path, size);
if (font) {
printf("Loaded font: %s\n", path);
return true;
}
return false;
}
bool AssetManager::loadBGM(const char* path) {
bgm = Mix_LoadMUS(path);
if (bgm) {
Mix_PlayMusic(bgm, -1);
return true;
}
fprintf(stderr, "BGM load failed: %s\n", Mix_GetError());
return false;
}
@@ -0,0 +1,30 @@
#ifndef ASSET_MANAGER_H
#define ASSET_MANAGER_H
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_mixer.h>
#include <SDL2/SDL_ttf.h>
#include <string>
#include <unordered_map>
class AssetManager {
public:
AssetManager(SDL_Renderer* renderer);
~AssetManager();
SDL_Texture* getTexture(const std::string& path);
TTF_Font* getFont() const { return font; }
Mix_Music* getBGM() const { return bgm; }
bool loadFont(const char* path, int size);
bool loadBGM(const char* path);
private:
SDL_Renderer* renderer;
std::unordered_map<std::string, SDL_Texture*> textures;
TTF_Font* font = nullptr;
Mix_Music* bgm = nullptr;
};
#endif
@@ -0,0 +1,15 @@
#include "Bullet.h"
#include "Tank.h"
void Bullet::fire(int bx, int by, Direction d, int dmg) {
x = bx; y = by; dir = d; damage = dmg; isLive = true;
}
CELL_Type Bullet::peekAt(int map[MAP_CELL_NUM][MAP_CELL_NUM]) const {
return static_cast<CELL_Type>(map[x + dx(dir)][y + dy(dir)]);
}
void Bullet::step() {
x += dx(dir);
y += dy(dir);
}
@@ -0,0 +1,18 @@
#ifndef BULLET_H
#define BULLET_H
#include "../data_config.h"
class Bullet {
public:
int x = 0, y = 0;
int damage = 0;
Direction dir = UP;
bool isLive = false;
void fire(int bx, int by, Direction d, int dmg);
CELL_Type peekAt(int map[MAP_CELL_NUM][MAP_CELL_NUM]) const;
void step();
};
#endif
@@ -0,0 +1,451 @@
#include "Game.h"
#include "Tank.h"
#include "UI.h"
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <random>
#include <unistd.h>
Game::Game() {
std::srand(static_cast<unsigned>(std::time(nullptr)));
playerBullets.resize(MAX_PLAYER_BULLETS);
enemyBullets.resize(MAX_ENEMY_BULLETS);
enemies.resize(ENEMIES_NUM);
}
Game::~Game() {
delete render;
delete assets;
if (sdlRenderer) SDL_DestroyRenderer(sdlRenderer);
if (window) SDL_DestroyWindow(window);
Mix_CloseAudio();
TTF_Quit();
IMG_Quit();
SDL_Quit();
}
bool Game::initSDL() {
char* base = SDL_GetBasePath();
if (base) { chdir(base); SDL_free(base); }
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
fprintf(stderr, "SDL init failed: %s\n", SDL_GetError());
return false;
}
if (!(IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG) & (IMG_INIT_PNG | IMG_INIT_JPG))) {
fprintf(stderr, "SDL_image init failed: %s\n", IMG_GetError());
return false;
}
if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
fprintf(stderr, "SDL_mixer init failed: %s\n", Mix_GetError());
return false;
}
if (TTF_Init() < 0) {
fprintf(stderr, "SDL_ttf init failed: %s\n", TTF_GetError());
return false;
}
window = SDL_CreateWindow("纸牌坦克", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
WINDOW_W, WINDOW_H, SDL_WINDOW_SHOWN);
if (!window) { fprintf(stderr, "Window failed: %s\n", SDL_GetError()); return false; }
sdlRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!sdlRenderer) { fprintf(stderr, "Renderer failed: %s\n", SDL_GetError()); return false; }
assets = new AssetManager(sdlRenderer);
render = new Renderer(sdlRenderer, assets);
const char* fontPaths[] = {
"/usr/share/fonts/wqy-microhei-fonts/wqy-microhei.ttc",
"/usr/share/fonts/wqy-zenhei-fonts/wqy-zenhei.ttc",
"/usr/share/fonts/google-noto-sans-cjk-vf-fonts/NotoSansCJK-VF.ttc",
"/usr/share/fonts/google-noto-serif-cjk-vf-fonts/NotoSerifCJK-VF.ttc",
nullptr
};
bool fontOk = false;
for (int i = 0; fontPaths[i]; i++) {
if (assets->loadFont(fontPaths[i], 30)) { fontOk = true; break; }
}
if (!fontOk) fprintf(stderr, "Warning: no Chinese font found\n");
assets->loadBGM("music/bgm.wav");
lastPlayerMove = lastPlayerShoot = lastEnemyMove = lastEnemyShoot = 0;
lastBulletMove = lastEnemyBulletMove = 0;
return true;
}
int Game::run() {
if (!initSDL()) return -1;
scene = Scene::MENU;
SDL_Event e;
while (running) {
while (SDL_PollEvent(&e)) handleEvents(e);
update();
renderScene();
SDL_Delay(16);
}
return 0;
}
// ------------------- event -------------------
void Game::handleEvents(SDL_Event& e) {
if (e.type == SDL_QUIT) { running = false; return; }
if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) {
switch (scene) {
case Scene::LEVEL:
case Scene::MEMBER:
case Scene::GAME:
case Scene::WIN:
case Scene::LOSE:
scene = Scene::MENU;
map.reset();
break;
default: break;
}
return;
}
if (e.type != SDL_MOUSEBUTTONDOWN) return;
int mx = e.button.x, my = e.button.y;
switch (scene) {
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:
if (UI::hit(UI::BTN_EASY, mx, my)) {
enemyCfg.hp = 5; enemyCfg.attack = 1; scene = Scene::MENU;
}
if (UI::hit(UI::BTN_MEDIUM, mx, my)) {
enemyCfg.hp = 10; enemyCfg.attack = 2; scene = Scene::MENU;
}
if (UI::hit(UI::BTN_HARD, mx, my)) {
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;
break;
default: break;
}
}
// ------------------- update -------------------
void Game::update() {
if (scene != Scene::GAME) return;
updateGame();
}
void Game::initGame() {
map.reset();
killCount = 0;
player = Tank(5, 5);
player.x = MAP_CELL_NUM / 2;
player.y = MAP_CELL_NUM / 2;
player.dir = UP;
std::default_random_engine gen(std::random_device{}());
std::uniform_int_distribution<int> dist(1, MAP_CELL_NUM - 2);
for (auto& e : enemies) {
do {
e.x = dist(gen);
e.y = dist(gen);
} while (map.get(e.x, e.y) != BLANK);
e.hp = enemyCfg.hp;
e.attack = enemyCfg.attack;
e.dir = LEFT;
e.isTaken = false;
map.set(e.x, e.y, BLUE_TANK);
}
for (auto& b : playerBullets) b.isLive = false;
for (auto& b : enemyBullets) b.isLive = false;
}
bool Game::isWin() const {
for (auto& e : enemies)
if (e.hp > 0) return false;
return player.hp > 0;
}
void Game::updateGame() {
if (player.hp <= 0) { scene = Scene::LOSE; return; }
if (isWin()) { scene = Scene::WIN; return; }
Uint32 now = SDL_GetTicks();
// --- 玩家移动 ---
const Uint8* keys = SDL_GetKeyboardState(nullptr);
bool moved = false;
if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) { player.dir = UP; moved = true; }
else if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) { player.dir = DOWN; moved = true; }
else if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]) { player.dir = LEFT; moved = true; }
else if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]){ player.dir = RIGHT; moved = true; }
if (moved && now - lastPlayerMove >= 100) {
lastPlayerMove = now;
int tx = player.x + dx(player.dir);
int ty = player.y + dy(player.dir);
CELL_Type c = map.get(tx, ty);
if (c == BLANK) {
player.x = tx; player.y = ty;
} else if (c == BULLET) {
player.x = tx; player.y = ty;
player.hp--;
for (auto& b : enemyBullets) {
if (b.isLive && b.x == tx && b.y == ty) { b.isLive = false; break; }
}
} else if (c == BLUE_DEAD_TANK) {
player.x = tx; player.y = ty;
player.attack++;
if (player.hp < 5) player.hp++;
for (auto& e : enemies) {
if (e.hp <= 0 && !e.isTaken && e.x == tx && e.y == ty) {
e.isTaken = true; break;
}
}
}
}
// --- 玩家射击 ---
if (keys[SDL_SCANCODE_SPACE] && now - lastPlayerShoot >= 500) {
lastPlayerShoot = now;
for (auto& b : playerBullets) {
if (!b.isLive) {
b.fire(player.x, player.y, player.dir, player.attack);
break;
}
}
}
// --- 敌人移动 ---
if (now - lastEnemyMove >= 1000) {
lastEnemyMove = now;
for (auto& e : enemies) {
if (e.hp <= 0) continue;
Direction d = static_cast<Direction>(rand() % 4);
e.dir = d;
int tx = e.x + dx(d);
int ty = e.y + dy(d);
CELL_Type c = map.get(tx, ty);
if (c == BLANK) {
e.x = tx; e.y = ty;
} else if (c == BULLET) {
e.x = tx; e.y = ty;
e.hp--;
for (auto& pb : playerBullets) {
if (pb.isLive && pb.x == tx && pb.y == ty) { pb.isLive = false; break; }
}
}
}
}
// --- 敌人射击 ---
if (now - lastEnemyShoot >= 3000) {
lastEnemyShoot = now;
for (auto& e : enemies) {
if (e.hp <= 0) continue;
for (auto& b : enemyBullets) {
if (!b.isLive) {
b.fire(e.x, e.y, e.dir, enemyCfg.attack);
break;
}
}
}
}
// --- 玩家子弹移动 ---
if (now - lastBulletMove >= 100) {
lastBulletMove = now;
for (auto& b : playerBullets) {
if (!b.isLive) continue;
CELL_Type c = b.peekAt(map.grid);
if (c == BLANK) {
b.step();
} else if (c == WALL) {
b.isLive = false;
} else if (c == BLUE_TANK) {
b.isLive = false;
int tx = b.x + dx(b.dir), ty = b.y + dy(b.dir);
for (auto& e : enemies) {
if (e.hp > 0 && e.x == tx && e.y == ty) { e.hp -= b.damage; break; }
}
} else {
b.isLive = false;
}
}
}
// --- 敌人子弹移动 ---
if (now - lastEnemyBulletMove >= 500) {
lastEnemyBulletMove = now;
for (auto& eb : enemyBullets) {
if (!eb.isLive) continue;
CELL_Type c = eb.peekAt(map.grid);
if (c == BLANK) {
eb.step();
} else if (c == WALL) {
eb.isLive = false;
} else if (c == RED_TANK) {
eb.isLive = false;
player.hp -= eb.damage;
} else {
eb.step();
}
}
}
// --- 同步地图 ---
map.reset();
map.set(player.x, player.y, RED_TANK);
for (auto& e : enemies) {
if (e.hp > 0)
map.set(e.x, e.y, BLUE_TANK);
else if (!e.isTaken)
map.set(e.x, e.y, BLUE_DEAD_TANK);
}
}
// ------------------- render -------------------
void Game::renderScene() {
render->clear();
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;
default: break;
}
render->present();
}
static SDL_Color GRAY = {157, 157, 157, 255};
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);
}
void Game::renderLevel() {
render->drawImage("images/游戏主界面背景.jpg", 0, 0, WINDOW_W, WINDOW_H);
render->drawImage("images/简单难度.jpg", UI::BTN_EASY.x, UI::BTN_EASY.y, UI::BTN_EASY.w, UI::BTN_EASY.h);
render->drawImage("images/中等难度.jpg", UI::BTN_MEDIUM.x, UI::BTN_MEDIUM.y, UI::BTN_MEDIUM.w, UI::BTN_MEDIUM.h);
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);
SDL_Texture* hpTex = assets->getTexture("images/red_hp_bar.jpg");
for (int i = 0; i < player.hp; i++)
render->drawTexture(hpTex, UI::HP_BAR_X + i * CELL_SIZE, UI::HP_BAR_Y, CELL_SIZE, CELL_SIZE);
int dieNum = 0;
for (auto& e : enemies) if (e.hp <= 0) dieNum++;
render->drawText("杀敌:", 110, 660, GRAY);
render->drawInt(dieNum, 185, 660, GRAY);
render->drawText("/10", 210, 660, GRAY);
std::string atk = "当前攻击力" + std::to_string(player.attack);
render->drawText(atk, 325, 660, GRAY);
SDL_Texture* wallTex = assets->getTexture("images/wall.jpg");
for (int i = 0; i < MAP_CELL_NUM; i++)
for (int j = 0; j < MAP_CELL_NUM; j++)
if (map.grid[i][j] == WALL)
render->drawTexture(wallTex, j * CELL_SIZE + X_OFFSET, i * CELL_SIZE + Y_OFFSET, CELL_SIZE, CELL_SIZE);
// 子弹
auto drawBullet = [&](Bullet& b) {
if (!b.isLive) return;
int sx = b.x * CELL_SIZE + X_OFFSET;
int sy = b.y * CELL_SIZE + Y_OFFSET;
bool horiz = (b.dir == LEFT || b.dir == RIGHT);
int tx = horiz ? sx : sx + 10;
int ty = horiz ? sy + 10 : sy;
int bw = horiz ? CELL_SIZE / 2 : CELL_SIZE / 4;
int bh = horiz ? CELL_SIZE / 4 : CELL_SIZE / 2;
const char* img = nullptr;
switch (b.dir) {
case UP: img = "images/B_UP_b.png"; break;
case DOWN: img = "images/B_DOWN_b.png"; break;
case LEFT: img = "images/B_LEFT_b.png"; break;
case RIGHT: img = "images/B_RIGHT_b.png"; break;
}
SDL_Texture* tex = assets->getTexture(img);
render->drawTexture(tex, tx, ty, bw, bh);
};
for (auto& b : playerBullets) drawBullet(b);
for (auto& b : enemyBullets) drawBullet(b);
// 坦克
auto drawTank = [&](Tank& t, bool isPlayer) {
int px = t.x * CELL_SIZE + X_OFFSET;
int py = t.y * CELL_SIZE + Y_OFFSET;
const char* img = nullptr;
if (isPlayer) {
switch (t.dir) {
case UP: img = "images/red_up_b.png"; break;
case DOWN: img = "images/red_down_b.png"; break;
case LEFT: img = "images/red_left_b.png"; break;
case RIGHT: img = "images/red_right_b.png"; break;
}
} else {
switch (t.dir) {
case UP: img = "images/blue_up_b.png"; break;
case DOWN: img = "images/blue_down_b.png"; break;
case LEFT: img = "images/blue_left_b.png"; break;
case RIGHT: img = "images/blue_right_b.png"; break;
}
}
render->drawTexture(assets->getTexture(img), px, py, CELL_SIZE, CELL_SIZE);
};
drawTank(player, true);
for (auto& e : enemies) {
if (e.isTaken) continue;
if (e.hp > 0) drawTank(e, false);
else render->drawTexture(assets->getTexture("images/dead_tank_b.png"),
e.x * CELL_SIZE + X_OFFSET, e.y * CELL_SIZE + Y_OFFSET, CELL_SIZE, CELL_SIZE);
}
}
void Game::renderWin() {
render->drawImage("images/胜利.png", 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::renderLose() {
render->drawImage("images/失败.png", 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);
}
@@ -0,0 +1,62 @@
#ifndef GAME_H
#define GAME_H
#include <SDL2/SDL.h>
#include <vector>
#include "../data_config.h"
#include "Map.h"
#include "Tank.h"
#include "Bullet.h"
#include "AssetManager.h"
#include "Renderer.h"
enum class Scene { MENU, LEVEL, MEMBER, GAME, WIN, LOSE, QUIT };
class Game {
public:
Game();
~Game();
int run();
private:
SDL_Window* window = nullptr;
SDL_Renderer* sdlRenderer = nullptr;
AssetManager* assets = nullptr;
Renderer* render = nullptr;
Scene scene = Scene::MENU;
bool running = true;
Map map;
Tank player;
std::vector<Tank> enemies;
std::vector<Bullet> playerBullets;
std::vector<Bullet> enemyBullets;
Tank enemyCfg;
int killCount = 0;
// 计时
Uint32 lastPlayerMove = 0, lastPlayerShoot = 0;
Uint32 lastEnemyMove = 0, lastEnemyShoot = 0;
Uint32 lastBulletMove = 0, lastEnemyBulletMove = 0;
bool initSDL();
bool initFont();
void handleEvents(SDL_Event& e);
void update();
void renderScene();
void initGame();
void updateGame();
void renderMenu();
void renderLevel();
void renderMember();
void renderGameView();
void renderWin();
void renderLose();
bool isWin() const;
};
#endif
@@ -0,0 +1,34 @@
#include "Map.h"
Map::Map() {
reset();
}
void Map::reset() {
for (int i = 0; i < MAP_CELL_NUM; i++) {
for (int j = 0; j < MAP_CELL_NUM; j++) {
if (i == 0 || i == MAP_CELL_NUM - 1 || j == 0 || j == MAP_CELL_NUM - 1)
grid[i][j] = WALL;
else
grid[i][j] = BLANK;
}
}
}
CELL_Type Map::get(int x, int y) const {
return static_cast<CELL_Type>(grid[x][y]);
}
void Map::set(int x, int y, CELL_Type type) {
grid[x][y] = type;
}
CELL_Type Map::peek(int x, int y, Direction dir) const {
switch (dir) {
case UP: return static_cast<CELL_Type>(grid[x][y - 1]);
case DOWN: return static_cast<CELL_Type>(grid[x][y + 1]);
case LEFT: return static_cast<CELL_Type>(grid[x - 1][y]);
case RIGHT: return static_cast<CELL_Type>(grid[x + 1][y]);
}
return BLANK;
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef MAP_H
#define MAP_H
#include "../data_config.h"
class Map {
public:
int grid[MAP_CELL_NUM][MAP_CELL_NUM];
Map();
void reset();
CELL_Type get(int x, int y) const;
void set(int x, int y, CELL_Type type);
CELL_Type peek(int x, int y, Direction dir) const;
};
#endif
@@ -0,0 +1,40 @@
#include "Renderer.h"
Renderer::Renderer(SDL_Renderer* r, AssetManager* a) : renderer(r), assets(a) {}
void Renderer::clear() {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
}
void Renderer::present() {
SDL_RenderPresent(renderer);
}
void Renderer::drawTexture(SDL_Texture* tex, int x, int y, int w, int h) {
if (!tex) return;
SDL_Rect dst = {x, y, w, h};
SDL_RenderCopy(renderer, tex, nullptr, &dst);
}
void Renderer::drawImage(const std::string& path, int x, int y, int w, int h) {
drawTexture(assets->getTexture(path), x, y, w, h);
}
void Renderer::drawText(const std::string& text, int x, int y, SDL_Color color) {
TTF_Font* font = assets->getFont();
if (!font || text.empty()) return;
SDL_Surface* surf = TTF_RenderUTF8_Blended(font, text.c_str(), color);
if (!surf) return;
SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
if (tex) {
SDL_Rect dst = {x, y, surf->w, surf->h};
SDL_RenderCopy(renderer, tex, nullptr, &dst);
SDL_DestroyTexture(tex);
}
SDL_FreeSurface(surf);
}
void Renderer::drawInt(int value, int x, int y, SDL_Color color) {
drawText(std::to_string(value), x, y, color);
}
@@ -0,0 +1,25 @@
#ifndef RENDERER_H
#define RENDERER_H
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <string>
#include "AssetManager.h"
class Renderer {
public:
Renderer(SDL_Renderer* r, AssetManager* a);
void clear();
void present();
void drawTexture(SDL_Texture* tex, int x, int y, int w, int h);
void drawImage(const std::string& path, int x, int y, int w, int h);
void drawText(const std::string& text, int x, int y, SDL_Color color);
void drawInt(int value, int x, int y, SDL_Color color);
private:
SDL_Renderer* renderer;
AssetManager* assets;
};
#endif
@@ -0,0 +1,3 @@
#include "Tank.h"
Tank::Tank(int h, int a) : hp(h), attack(a) {}
@@ -0,0 +1,19 @@
#ifndef TANK_H
#define TANK_H
#include "../data_config.h"
class Tank {
public:
int x = 0, y = 0;
int hp = 5, attack = 1;
Direction dir = UP;
bool isTaken = false;
Tank(int h = 5, int a = 1);
};
inline int dx(Direction d) { return d == LEFT ? -1 : (d == RIGHT ? 1 : 0); }
inline int dy(Direction d) { return d == UP ? -1 : (d == DOWN ? 1 : 0); }
#endif
+32
View File
@@ -0,0 +1,32 @@
#ifndef UI_H
#define UI_H
#include <SDL2/SDL.h>
namespace UI {
inline bool hit(const SDL_Rect& r, int mx, int my) {
return mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h;
}
// 菜单按钮
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_EASY = {275, 150, 150, 100};
constexpr SDL_Rect BTN_MEDIUM = {275, 300, 150, 100};
constexpr SDL_Rect BTN_HARD = {275, 450, 150, 100};
// 返回按钮
constexpr SDL_Rect BTN_BACK = {30, 550, 150, 50};
// HP 条
constexpr int HP_BAR_X = 100;
constexpr int HP_BAR_Y = 10;
}
#endif
-33
View File
@@ -1,33 +0,0 @@
// 坦克定义
#ifndef TANK_H
#define TANK_H
/*
坦克:
坐标:xy
血量hp:表示当前坦克血量
攻击力attack:表示当前坦克攻击力
方向dir
UP 上
DOWN 下
LEFT 左
RIGHT 右
*/
enum Direction {
UP, // 上
DOWN, // 下
LEFT, // 左
RIGHT, // 右
};
struct tank {
int x, y;//坐标
int hp; // 血量
int attack; // 攻击力
int dir; // 方向
bool is_taken = false;//报销坦克是否已经被玩家捡取
tank(): attack(1), hp(5){}
};
#endif // !TANK_H