新闻详情

新闻详情

首页 / 资讯中心 / 详情

C++ Qt坦克大战实战:从类设计到碰撞检测的完整实现

发布时间:2026/9/14 2:55:31来源:尧图网络
C++ Qt坦克大战实战:从类设计到碰撞检测的完整实现
简介面向C初学者的坦克大战游戏源码工程基于Qt 5.14.1与C编写在Qt Creator 4.11.0中开发完整实现经典坦克对战玩法。资源为可编译运行的Qt工程共设置35个关卡每关包含20个敌方坦克玩家拥有3条生命击败全部敌人自动进入下一关通关35关即获胜若生命数耗尽或大本营被击中则游戏失败。游戏采用单人模式玩家通过W、S、A、D控制坦克移动按J键发射子弹敌方坦克由程序自动控制。压缩包整体约27.79MB内容完整可直接导入开发环境编译学习。已有1150人学习适合作为C课程设计、Qt入门练手或游戏编程参考项目通过阅读源码可系统掌握面向对象设计、信号与槽机制、定时器驱动动画、碰撞检测及关卡切换等核心编程技巧。1. 为什么把坦克大战做成C大作业而不是用现成引擎当年选大作业题目时大概十个里有七个交飞机大战剩下三个是俄罗斯方块。换坦克大战不是因为怀旧而是它正好卡在“纯C能写完、又不至于简单到答辩被问穿”的位置上。用Qt写意味着不碰Unity那套现成物理和渲染地图、碰撞、敌人AI、关卡推进全靠自己用C搭出来而这恰恰是C课程设计最想考察的东西类怎么划分、资源怎么管理、事件怎么流转。这个项目适合正在做C期末大作业、想把面向对象和Qt事件循环讲清楚的人。它不追求画面多炫但每个模块都能拿出来被问细节。2. 游戏对象建模从坦克到子弹的类层次设计2.1 为什么让所有游戏物件继承QObjectQt自带的对象树QObject parent-child能解决游戏里最难堪的内存释放问题。坦克死了、子弹出界只要调用deleteLater()事件循环结束就会自动销毁不需要手动维护一堆裸指针。另外QObject还带了信号槽和QTimer后面做敌方坦克生成、子弹碰撞通知都依靠它。下面是我在项目里用的类关系表类名父类职责关键成员GameObjectQObject所有游戏物体的公共基类QPoint pos; QSize size; bool alive;TankGameObject玩家和敌人共用Direction dir; int hp; int speed; QTimer* fireTimer;BulletGameObject子弹飞行与命中Direction dir; int speed; int damage;WallGameObject砖墙/钢墙/水/树WallType type; bool destructible;GameSceneQWidget主游戏窗口与事件入口QVectorGameObject* objects;这样分的好处是后面碰撞检测只需要写一个函数接收两个GameObject*然后按实际类型做分支。当然也可以把碰撞逻辑挂在基类里用虚函数但那样每个子类都要写一遍不如单独抽一个CollisionUtil。2.2 Tank类的定义与移动逻辑坦克是游戏里最核心的对象我把它拆成“状态”和“行为”两部分。状态就是位置、方向、生命值行为是移动、射击。注意不要把按键逻辑写进Tank按键是界面层的事Tank只提供move()和shoot()接口。代码如下enum Direction { Up, Down, Left, Right }; class Tank : public GameObject { Q_OBJECT public: Tank(int x, int y, Direction dir, int hp, QObject* parent nullptr) : GameObject(parent), m_pos(x, y), m_dir(dir), m_hp(hp) { m_speed 2; // 每帧移动2像素配合50ms定时器 m_fireCooldown 0; // 冷却帧数 } void move(Direction dir, const QRect bounds) { m_dir dir; QPoint next m_pos; switch (dir) { case Up: next.ry() - m_speed; break; case Down: next.ry() m_speed; break; case Left: next.rx() - m_speed; break; case Right: next.rx() m_speed; break; } // 边界约束留出坦克自身宽度 if (bounds.contains(QRect(next, m_size))) { m_pos next; } } void setDir(Direction dir) { m_dir dir; } QPoint pos() const { return m_pos; } bool alive() const { return m_hp 0; } private: QPoint m_pos; QSize m_size QSize(32, 32); Direction m_dir; int m_hp; int m_speed; int m_fireCooldown; };这段代码里的关键参数是m_speed2和bounds.contains。speed像素值要和QTimer的间隔配合如果定时器是50ms那么一秒移动20次一次2像素也就是40像素/秒大约0.8格/秒每格32像素。这个速度对坦克大战来说偏慢实际可以调到3或4但这时碰撞检测要把“穿透”问题考虑进去——详见第四章。注意bounds参数是游戏区域矩形移动前先判断目标位置是否合法可以避免坦克跑到地图外面去。2.3 地图用二维数组比Tiled地图更省事因为大作业不要求可视化编辑器我直接用static数组声明关卡const int ROWS 15, COLS 15; int map[ROWS][COLS] { {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,3,3,3,0,0,0,0,0,2,2,2,0,1}, // ... 简化真实关卡有35张表 {1,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,0,0,0,1,1,1,1,1,1}, };地图值含义0空地、1砖墙可打碎、2钢墙不可打碎、3水可阻挡、4树可穿透但遮挡、5大本营。绘制时只需要在QPainter::drawImage里根据map[i][j]选择对应贴图。这里有个很容易踩的坑地图索引和屏幕坐标是反的map[row][col]对应屏幕(xcol32, yrow32)写转换函数时三番两次把row和col写反导致坦克走斜线。我一般封装一个toScreen(row,col)函数统一处理。2.4 用QObject对象树管理游戏对象场景里既有砖墙、钢墙这类静态物体也有坦克、子弹这类动态物体。静态物体在关卡加载时创建后不会消失动态物体频繁创建销毁。我选择把所有物体都挂到GameScene这个QWidget上父对象是this这样窗口关闭时所有子对象自动析构。GameScene::GameScene(QWidget* parent) : QWidget(parent) { for (int r 0; r ROWS; r) { for (int c 0; c COLS; c) { if (map[r][c] 1) { auto* wall new Wall(c * 32, r * 32, Brick, this); m_objects.append(wall); } } } }这里new出来的Wall父指针传this一旦GameScene析构对象树会递归delete所有子节点。但注意如果某个子弹在碰撞后直接delete而不是deleteLater可能会在QTimer事件处理中触发“析构正在发送信号的对象”崩溃。所以动态物体建议统一用deleteLater()稍后我会在第四章给出具体碰撞处理代码。3. 事件驱动键盘控制、定时刷新与子弹飞行3.1 重写keyPressEvent是Qt小游戏最直接的做法控制坦克移动有几种方案QShortcut、事件过滤器、重写keyPressEvent。QShortcut适合按钮快捷键事件过滤器适合要拦截全局按键的场景而坦克大战只需要监控当前窗口直接重写键盘事件最清晰代码量也最少。void GameScene::keyPressEvent(QKeyEvent* event) { switch (event-key()) { case Qt::Key_W: m_playerTank-setDir(Up); m_playerTank-move(Up, this-rect()); break; case Qt::Key_S: m_playerTank-setDir(Down); m_playerTank-move(Down, this-rect()); break; case Qt::Key_A: m_playerTank-setDir(Left); m_playerTank-move(Left, this-rect()); break; case Qt::Key_D: m_playerTank-setDir(Right); m_playerTank-move(Right, this-rect()); break; case Qt::Key_J: if (m_playerTank-canShoot()) { shootBullet(m_playerTank); } break; default: QWidget::keyPressEvent(event); } }这里我故意没有用event-isAutoRepeat()因为按住W连续移动是正常需求坦克大战原版也是按住就持续走。但子弹不能按住J就连续射击否则几秒就能清屏所以要在Tank里加一个m_fireCooldown每发射一次子弹就置为10帧定时器每帧减1只有为0才允许发射。这个冷却参数直接影响游戏难度10帧配合50ms定时器是500ms一发比较合理。有个容易忽略的坑QWidget默认不接受键盘焦点必须在构造函数里setFocusPolicy(Qt::StrongFocus)否则keyPressEvent永远不会响应。我见过不少同学编译通过但按键盘没反应八成是漏了这句。3.2 用QTimer驱动游戏循环别用while(1)一开始有人会写while(true){ update(); }这在Qt里会让界面卡死因为QPainter只能在事件循环中工作。正确做法是启动一个QTimer让它每50ms触发一次update。// GameScene构造函数里 m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, GameScene::gameLoop); m_timer-start(50); // 20 FPS对像素风坦克大战足够 void GameScene::gameLoop() { // 1. 更新玩家和敌人的冷却计时 // 2. 移动所有敌方坦克 // 3. 移动所有子弹 // 4. 检查碰撞 // 5. 检查通关/死亡条件 // 6. 调用update()触发paintEvent重绘 update(); }定时器间隔50ms是常见的选择。间隔越小动画越平滑但碰撞穿透和CPU占用也越大。20FPS对于32×32像素的块状坦克肉眼已经能接受。如果要调成更流畅的60FPS就把start(16)同时把坦克speed从2改成1避免一帧跨过一整格导致碰撞漏检。3.3 子弹飞行与越界清理子弹没有自己的线程它在gameLoop里按方向线性移动。关键是要把子弹也当成GameObject存进m_objects统一遍历而不是单独维护一个list。void GameScene::updateBullets() { for (auto* bullet : m_bullets) { switch (bullet-dir()) { case Up: bullet-moveBy(0, -bullet-speed()); break; case Down: bullet-moveBy(0, bullet-speed()); break; case Left: bullet-moveBy(-bullet-speed(), 0); break; case Right: bullet-moveBy( bullet-speed(), 0); break; } // 超出屏幕就标记死亡 if (!this-rect().contains(bullet-pos())) { bullet-setAlive(false); } } // 统一移除死亡子弹 for (auto it m_bullets.begin(); it ! m_bullets.end(); ) { if (!(*it)-alive()) { (*it)-deleteLater(); it m_bullets.erase(it); } else { it; } } }注意上面代码里的分类讨论子弹飞行速度通常设为4或6比坦克快这样手感才爽。但速度越快越容易在相邻两帧之间直接跳过一堵墙——如果墙是32像素宽子弹速度大于32像素/帧就会穿透。所以speed上限建议不超过16。用erase加deleteLater可以防止QTimer回调中直接delete对象导致的悬垂指针。3.4 让敌方坦克随机转向的简单AI大作业不需要什么寻路原版坦克大战的敌人也就是“碰到墙或边界就随机换方向”。我实现的方式是每帧尝试移动如果移动失败被墙挡住或到达边界就随机换一个方向。void EnemyTank::updateAI(const QRect bounds) { int r rand() % 3; // 0保持1左转2右转 if (r 1) m_dir Direction((m_dir 3) % 4); if (r 2) m_dir Direction((m_dir 1) % 4); QPoint oldPos pos(); move(m_dir, bounds); if (pos() oldPos) { // 没移动成功强制转向 m_dir Direction(rand() % 4); } // 每10帧随机开一枪 if (rand() % 10 0 canShoot()) { emit shootRequested(m_dir); } }这里用rand()%3来控制转向频率rand()%10控制射击概率。这两个参数像麻将里的“手气”调大敌人会更凶调小就是散步。注意rand()要用srand(time(0))初始化否则每次运行敌人行为都一样答辩演示时容易被老师以为是写死。更稳的写法是用std::uniform_int_distribution但大作业里rand()足以。4. 碰撞检测与游戏规则状态机4.1 AABB碰撞检测为什么不用像素级坦克大战所有物体都是矩形用轴对齐包围盒AABB检测就够了。所谓AABB就是两个矩形在x轴和y轴的投影都重叠。Qt的QRect已经内置了intersects()但为了能说清楚原理我建议自己写一个答辩时老师必问。bool checkCollision(const QRect a, const QRect b) { if (a.right() b.left() || b.right() a.left()) return false; if (a.bottom() b.top() || b.bottom() a.top()) return false; return true; } // 在gameLoop里遍历所有动态物体 bool collided false; for (GameObject* obj : m_objects) { if (obj tank) continue; if (obj-alive() checkCollision(tank-rect(), obj-rect())) { // 如果是墙坦克退回上一帧位置 // 如果是敌人子弹坦克扣血 // 如果是大本营游戏失败 } }AABB的缺点是不能处理旋转物体但坦克大战里没有旋转只有四个方向矩形碰撞完全够用。碰撞后怎么处理取决于物体类型坦克撞墙应该阻止移动子弹撞墙应该销毁子弹并且如果是砖墙就消掉一块子弹撞坦克则减少生命。注意坦克和子弹都移动的情况下要先把两者都移动完再统一检测避免“我先移你后移”导致漏检。4.2 子弹与地图的交互砖墙扣除一个单位砖墙在地图数组里是1钢墙是2。子弹打中砖墙时把对应的map值改成0并删除那个Wall对象。这里有个细节如果用数组值判断那么一个格子只能整体消失要做成原版那样“一块砖一格砖地碎”需要把每块砖拆成4个小Wall。大作业里为了省事我直接整格消失答辩时可以补充说明这是简化版。void GameScene::handleBulletHitWall(Bullet* bullet, Wall* wall, int row, int col) { if (wall-type() Wall::Brick) { map[row][col] 0; // 地图数据更新 wall-setAlive(false); // 碰撞对象标记 bullet-setAlive(false); // 同时播放一个简单特效这里用setVisible(false) 下一次重绘消失 } else if (wall-type() Wall::Steel) { bullet-setAlive(false); // 钢墙不可破坏子弹消失 } }注意map[row][col]的下标顺序row是行col是列。因为墙是按row,col创建的销毁时也应该用row,col去索引而不是直接用像素坐标除以32。如果搞反了你会发现子弹打的是左边墙消失的却是右边墙这是二维数组行列映射的典型错误。4.3 关卡状态机Playing、LevelClear、GameOver游戏一共有35关每关20个敌人3条命。这些数据放在GameData里统一管理enum GameState { Playing, LevelClear, GameOver }; class GameData { public: int currentLevel 1; int enemiesLeft 20; // 当前关剩余敌人 int playerLives 3; bool baseAlive true; GameState state Playing; bool isLevelClear() const { return enemiesLeft 0; } bool isGameOver() const { return playerLives 0 || !baseAlive; } };每次敌人生成时enemiesLeft减1敌人被击毁时也减1——注意可能在生成时减还是在死亡时减原版是“本关敌人数”包括了还没出现的为了让玩家有压力我做成生成时减意味着你杀光场上的敌人后还可能有未被生成的敌人需要等他们冒出来。这个规则要在游戏说明里写清楚否则玩家会觉得“场上一只都没有却不通关”是bug。4.4 通关与失败的处理流程在gameLoop末尾统一检查状态void GameScene::checkGameState() { if (m_data.isLevelClear()) { if (m_data.currentLevel 35) { m_data.state GameOver; // 或者单独定义Victory QMessageBox::information(this, 胜利, 你通关了35关); close(); } else { m_data.currentLevel; m_data.enemiesLeft 20; loadLevel(m_data.currentLevel); } } else if (m_data.isGameOver()) { m_data.state GameOver; QMessageBox::information(this, 失败, 坦克基地被摧毁); close(); } }这个逻辑里有几个关键点。第一关卡完成后要重置敌人数量并重新loadLevel但不重置玩家生命。第二loadLevel里要清空现有子弹和敌人但不能清玩家坦克否则玩家会觉得血亏。第三35关作为胜利边界需要单独定义GameState::Victory否则和GameOver混在一起弹窗文案会变得很奇怪。我上面代码里偷懒了建议读者改成四个状态。4.5 敌人刷新队列与生成点调度35关不可能把20个敌人一次性放在地图上那会堵死。原版是每关分成多波在地图上固定生成点逐渐冒出。我用一个简单队列// 在loadLevel里填充生成队列 for (int i 0; i 20; i) { m_enemyQueue.enqueue(QPoint(0, 0)); // 三个生成点之一 } void GameScene::spawnEnemyIfNeeded() { // 场上敌人少于5且队列非空且距离上次生成超过2秒 if (m_enemiesOnField.size() 5 !m_enemyQueue.isEmpty() m_spawnTimer-remainingTime() 0) { auto* enemy createEnemy(m_enemyQueue.dequeue()); m_enemiesOnField.append(enemy); m_spawnTimer-start(2000); // 2秒生成间隔 } }这里的“场上少于5个”是为了避免同屏敌人太多导致性能下降。生成间隔2000ms可以调第一关可以短一点让玩家爽快后期关可以加长制造紧张感。注意生成点要避开玩家出生位置一共三个点左上、右上、左下。玩家诞生在右下基地附近四个点不能重叠否则玩家一出生就被围殴。5. 答辩加分项对象池、局部重绘与配置外置5.1 用对象池复用子弹避免频繁new/deletegameLoop里每50ms就可能有子弹创建销毁而new和delete在堆上很慢连续压力下帧率会波动。简单做法是预先创建30个子弹对象初始setAlive(false)需要发射时从池里取一个活状态为false的子弹并初始化位置打中或出界后再放回池子。class BulletPool { public: BulletPool(int size, QObject* parent) { for (int i 0; i size; i) { m_pool.append(new Bullet(parent)); m_pool.last()-setAlive(false); } } Bullet* acquire(const QPoint pos, Direction dir) { for (Bullet* b : m_pool) { if (!b-alive()) { b-setPos(pos); b-setDir(dir); b-setAlive(true); return b; } } return nullptr; // 池满了表现为射速突然下降 } void release(Bullet* b) { b-setAlive(false); } private: QVectorBullet* m_pool; };对象池的大小要根据关卡中的最大同屏子弹数来定玩家4个敌人每个敌人最多同时5发子弹最多25发。池子定成30就足够。如果acquire返回nullptr说明池满了可以放弃发射或强制回收最老的一颗。这个数据参数在答辩时讲出来会很加分说明你考虑过资源边界。5.2 用局部重绘替代整屏重绘QWidget默认paintEvent会把整个窗口重新绘制一遍如果地图上有很多砖墙和树会造成不必要的开销。可以在gameLoop里记录变化区域只更新包含移动物体和碰撞区域的那个矩形void GameScene::paintEvent(QPaintEvent* event) { Q_UNUSED(event); QPainter painter(this); // 绘制地图这里可以只绘制event-rect()范围内的格子 for (int r 0; r ROWS; r) { for (int c 0; c COLS; c) { QRect cell(c * 32, r * 32, 32, 32); if (event-rect().intersects(cell)) { drawCell(painter, r, c); } } } // 绘制物体 for (GameObject* obj : m_objects) { if (event-rect().intersects(obj-rect())) { obj-draw(painter); } } }但要注意QWidget的paintEvent接收到的event-rect()是“需要更新的区域”这个区域由update()和update(rect)来控制。如果你在gameLoop里调update()不带参数整个窗口都会被标脏局部重绘就失效了。正确写法是先计算所有移动物体的外接包围盒再调用update(dirtyRect)。这个优化对坦克大战这样的小场景收益不明显但体现了性能意识。5.3 把35关的关卡数据抽到JSON写35个二维数组放在代码里非常占篇幅而且改关卡要重新编译。可以把每个关卡的地图、敌人数量、生成点抽到一个JSON文件运行时用QJsonDocument读取。{ level: 1, rows: 15, cols: 15, enemies: 20, map: [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,1] ] }加载时用QFile读取再用QJsonDocument::fromJson解析。注意JSON里map是数组的数组读取时要先取到QJsonArray再遍历子数组。把关卡外置后玩家自己改个map值就能生成自定义关卡大作业演示时现场改文件再开游戏效果比单纯讲代码更唬人。5.4 答辩时这三个角落最容易让老师提问第一个是碰撞穿透你要能解释为什么子弹速度必须小于物体宽度。第二个是内存安全你会不会出现子弹销毁后其他物体还持有其指针的情况对象池和deleteLater怎么配合解决。第三个是关卡配置如果第30关和第1关一样是不是bug关卡难度有没有递增曲线。这三个问题我都被问过提前在代码里留好注释和上述设计基本可以稳稳过关。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Mastra @mastra/claude:将 Claude Agent SDK 的 Agent 循环接入 Mastra 的 generate/stream 体系 2026/9/14 3:40:34

Mastra @mastra/claude:将 Claude Agent SDK 的 Agent 循环接入 Mastra 的 generate/stream 体系

Mastra mastra/claude:将 Claude Agent SDK 的 Agent 循环接入 Mastra 的 generate/stream 体系 【免费下载链接】mastra Mastra is the modern TypeScript framework for AI-powered applications and agents. 项目地址: https://gitcode.com/GitHub_Trending/ma…

阅读更多 →
240张火焰烟雾图像,用YOLOv8训练自己的检测模型 2026/9/14 3:40:34

240张火焰烟雾图像,用YOLOv8训练自己的检测模型

简介:此数据集聚焦火焰、烟雾与正常三类场景的图像分类,共包含约240张已标注图片,已有110人学习下载,适合图像分类初学者、烟火检测项目开发者以及需要小型标注数据集验证模型流程的研究者使用。压缩包共243个文件,以2…

阅读更多 →
Python+OpenCV运动目标自动追踪系统:PID控制与云台实战 2026/9/14 3:40:34

Python+OpenCV运动目标自动追踪系统:PID控制与云台实战

简介:面向2023年电子设计竞赛E题备赛与智能控制开发人群,该源码包以B站程欢欢智能控制集为灵感,提供一套从三维机械设计到Python程序实现的完整参考方案,适合参赛学生、开发者快速理解电赛E题中的云台追踪、激光发射与视觉识别场景…

阅读更多 →
Cocos Creator微信小游戏斗地主开发实战:包体控制与性能优化 2026/9/14 3:40:34

Cocos Creator微信小游戏斗地主开发实战:包体控制与性能优化

简介:本资源是一个基于Cocos Creator开发的斗地主微信小游戏完整Demo,面向游戏开发初学者与微信小游戏实践者,旨在帮助开发者掌握Cocos Creator引擎在真实社交类小游戏项目中的工程化落地能力。资源包共470个文件,涵盖54个TypeScr…

阅读更多 →
Xpay-3.1开源支付网关部署与微信支付宝直连实战 2026/9/14 3:40:34

Xpay-3.1开源支付网关部署与微信支付宝直连实战

简介:Xpay-3.1版全开源无授权免签约支付源码,面向Java Web开发者、中小型项目技术负责人及支付系统学习者,提供可直接二次开发的轻量级支付解决方案,有效降低企业自建支付网关的技术门槛与授权成本。资源包共823个文件&#xff0c…

阅读更多 →
双臂机器人Matlab仿真与S型轨迹规划实践 2026/9/14 3:37:34

双臂机器人Matlab仿真与S型轨迹规划实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞