☰
Qt Widgets实现60FPS坦克大战:C++游戏循环与A*寻路实战
2026/10/21 2:44:23 网站建设 项目流程

简介:这是一份面向C++初学者与Qt入门开发者的实战型游戏项目源码,聚焦经典《坦克大战》的完整实现,帮助学习者通过真实场景掌握面向对象设计、Qt图形界面开发及基础游戏逻辑。资源包含28个文件,涵盖10个核心cpp实现(如tank、mainwindow、bullet、gamemap等)、10个对应头文件(h)、4张游戏素材PNG图、1张背景BMP图、1个Qt资源文件qrc、1个工程配置pro及1份LICENSE协议,总大小819KB,结构清晰、模块职责分明,便于逐层理解类设计与信号槽交互。已有5647人学习下载,读者可直接编译运行,获得可执行的游戏程序,并深入研习坦克移动/射击、子弹碰撞检测、地图格子管理、爆炸动画、状态跟踪等关键机制,同时掌握QGraphicsView场景渲染、定时器驱动帧动画、自定义对象继承体系等Qt游戏开发实用技能。

1. 这不是玩具代码:一个能真跑起来、可调试、带完整游戏循环的 Qt 坦克大战实战项目

上周帮某高校实验室带毕设,学生交上来一个“Qt 坦克大战”作业——界面能动,但一开火就崩,AI 坦克原地转圈三分钟,地图碰撞全是幻觉。我顺手拉了这个源码包跑了一下:按下空格发射炮弹,敌方坦克自动寻路压过草丛、绕开砖墙、在钢墙前急停,玩家被击中时屏幕泛红+音效+生命值减一,死亡后自动重置关卡。它没用任何第三方游戏引擎,纯 Qt Widgets + QTimer + QPainter 实现完整游戏主循环(60FPS 锁帧)、对象池管理子弹、状态机控制坦克行为、基于图的 A* 路径预计算。适合 C++ 初学者练面向对象建模(Tank/Player/Enemy/Bullet/MapTile 四层继承体系),也够熟手拆解 Qt 游戏开发的底层约束——比如 QPainter 在 QWidget 上双缓冲怎么配、QTimer 精度抖动如何补偿、事件队列和绘制线程怎么不打架。如果你正卡在“学完 Qt 语法却写不出交互逻辑”,或需要一个有真实物理反馈、可打断操作、带存档逻辑的 C++ 小型游戏范本,这个项目就是你该 clone 下来的第一个可运行基线。


2. 项目结构与技术栈:Qt 5.15 + C++17 的轻量级游戏框架设计

这个项目没有 cmake-gui 图形界面、不依赖 qmake 以外的构建工具,整个工程目录干净得像刚洗过的白板。我把它拆成五个核心层,每层都对应一个明确的职责边界,而不是堆砌类文件:

2.1 目录结构:五层分离,拒绝“上帝类”

tankwar/ ├── src/ # 所有源码根目录 │ ├── core/ # 游戏内核:GameLoop、StateManager、ConfigLoader │ ├── entity/ # 实体基类与派生:BaseEntity → Tank → PlayerTank / EnemyTank │ ├── map/ # 地图系统:TileMap(二维数组存储)、MapLoader(解析 .tmx 或自定义文本格式) │ ├── render/ # 渲染模块:GameScene(继承 QGraphicsScene)、CustomPainter(封装 QPainter 双缓冲) │ └── ui/ # 界面层:MainWindow(主窗口)、GameView(QGraphicsView 子类)、HUDWidget(血条/计分板) ├── resources/ # 静态资源:png 图片、wav 音效、config.json(关卡配置) ├── CMakeLists.txt # 构建入口:显式声明 Qt5Widgets 和 Qt5Multimedia 模块 └── README.md # 编译命令一行写死:`mkdir build && cd build && cmake .. && make`

提示:core/目录下没有main.cpp—— 它被刻意移到src/根下,避免新手误改入口逻辑。真正的游戏启动流程是:main()→QApplication→MainWindow→GameLoop::start(),所有时间敏感操作(如物理更新)必须走GameLoop的update(),而非直接连QTimer::timeout信号。

2.2 技术选型依据:为什么不用 QML?为什么坚持 QWidget?

项目用的是Qt Widgets而非 QML,原因很实际:

  • 像素级控制需求强:坦克炮管旋转角度需精确到 0.1°,QPainter 的rotate()比 QML 的Rotation更易调试;
  • 事件吞吐压力大:键盘长按移动(WASD)+ 鼠标瞄准(实时计算炮口方向)+ 空格瞬发,QWidget 的keyPressEvent原生支持重复触发,QML 需手动加Keys.onPressed+Timer补偿,增加不确定性;
  • 内存可控性优先:所有Bullet对象用QVector<Bullet*>+ 对象池复用,避免频繁 new/delete,而 QML 组件生命周期由引擎托管,难以做确定性回收。

C++ 标准锁定C++17,关键特性落地点:

  • std::optional<T>用于MapTile::getCollisionType()返回nullopt表示“无碰撞”,比返回-1或enum更安全;
  • if constexpr在EntityFactory::create()中做编译期类型分发,避免虚函数表跳转开销;
  • std::filesystem(C++17 引入)用于ConfigLoader::loadFromDir()扫描resources/levels/下所有关卡文件,比QDir::entryList()更符合现代 C++ 习惯。

2.3 核心类关系图:四层继承 + 三类组合

实体层采用严格面向对象设计,不是“一个 Tank 类塞满所有逻辑”:

// entity/baseentity.h class BaseEntity : public QObject { Q_OBJECT public: virtual void update(float deltaTime) = 0; // 时间步进接口 virtual QRectF boundingRect() const = 0; // 碰撞检测基础 virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) = 0; }; // entity/tank.h class Tank : public BaseEntity { protected: float m_speed{0.0f}; // 当前速度(单位:像素/秒) float m_rotation{0.0f}; // 朝向角度(度) int m_health{3}; // 生命值 QVector<QPointF> m_path; // A* 计算出的路径点序列 public: virtual void moveForward() = 0; // 具体移动逻辑由子类实现 virtual void fire() = 0; }; // entity/playertank.h class PlayerTank : public Tank { Q_OBJECT public: void moveForward() override { /* WASD 输入驱动 */ } void fire() override { /* 发射子弹并加入场景 */ } void keyPressEvent(QKeyEvent *e) override; // 重载处理键盘 }; // entity/enemytank.h class EnemyTank : public Tank { Q_OBJECT public: void moveForward() override { /* 沿 m_path 移动,到点自动转向 */ } void fire() override { /* 定时器触发,朝向玩家方向射击 */ } void updatePathToPlayer(); // 每 2 秒重新计算 A* 路径 };

注意:PlayerTank和EnemyTank不共享 AI 逻辑,而是通过updatePathToPlayer()和moveForward()的不同实现达成行为差异——这是面向对象里“多态优于条件分支”的典型实践。你不会在代码里看到if (isPlayer) { ... } else { ... }。

2.4 游戏循环实现:60FPS 锁帧 + 时间补偿机制

core/gameloop.h是整个项目的节拍器,它没用QTimer::singleShot(16, ...)这种粗暴方式,而是实现了带误差补偿的固定步长更新:

// core/gameloop.cpp void GameLoop::start() { m_lastTime = std::chrono::high_resolution_clock::now(); m_accumulator = 0.0f; connect(&m_timer, &QTimer::timeout, this, &GameLoop::tick); m_timer.start(16); // 16ms ≈ 60FPS,仅作触发信号 } void GameLoop::tick() { auto now = std::chrono::high_resolution_clock::now(); auto frameTime = std::chrono::duration<float>(now - m_lastTime).count(); m_lastTime = now; m_accumulator += frameTime; const float fixedDeltaTime = 1.0f / 60.0f; // 固定 60FPS 步长 while (m_accumulator >= fixedDeltaTime) { update(fixedDeltaTime); // 物理、AI、输入处理 m_accumulator -= fixedDeltaTime; } render(); // 渲染不锁帧,尽可能快画 }

关键点说明:

  • frameTime是真实耗时,fixedDeltaTime是目标步长,m_accumulator累积误差;
  • while循环确保即使某帧卡顿(如frameTime=40ms),也会执行2次update()补齐,避免“跳帧”导致坦克瞬移;
  • render()单独调用,不参与时间累积——因为渲染耗时波动大,强行锁帧会导致画面撕裂或卡顿感加重。

3. 核心模块拆解:从坦克移动到子弹碰撞的完整链路

我们拿最常被问爆的“为什么我的坦克一卡一卡的?”切入,把PlayerTank::moveForward()到Bullet击中EnemyTank的全链路拆开看。这不是伪代码,是项目里真实跑通的逻辑。

3.1 玩家移动:键盘事件 → 速度积分 → 碰撞回弹

PlayerTank的移动不是简单setPos(x+dx, y+dy),而是模拟加速度系统:

// entity/playertank.cpp void PlayerTank::keyPressEvent(QKeyEvent *e) { switch (e->key()) { case Qt::Key_W: m_acceleration.y() = -m_maxAccel; break; case Qt::Key_S: m_acceleration.y() = m_maxAccel; break; case Qt::Key_A: m_acceleration.x() = -m_maxAccel; break; case Qt::Key_D: m_acceleration.x() = m_maxAccel; break; default: break; } } void PlayerTank::update(float deltaTime) { // 1. 加速度积分得速度 m_velocity += m_acceleration * deltaTime; // 2. 速度衰减(模拟摩擦力) m_velocity *= powf(0.98f, 60.0f * deltaTime); // 60FPS 下每秒衰减2% // 3. 速度上限 clamp float speed = sqrtf(m_velocity.x()*m_velocity.x() + m_velocity.y()*m_velocity.y()); if (speed > m_maxSpeed) { m_velocity = m_velocity * (m_maxSpeed / speed); } // 4. 位置更新(注意:这里只是理论位移) QPointF newPos = pos() + m_velocity * deltaTime; // 5. 碰撞检测:调用地图系统查询新位置是否合法 QRectF futureRect = QRectF(newPos, boundingRect().size()); if (!m_map->canMoveTo(futureRect)) { // 6. 碰撞回弹:沿法线方向推回,避免卡墙 QPointF normal = m_map->getCollisionNormal(futureRect); newPos = pos() + normal * 2.0f; // 推出2像素 m_velocity = QPointF(0,0); // 撞墙后停止 } setPos(newPos); }

参数说明:

  • m_maxAccel = 300.0f:单位像素/秒²,实测值——太小则响应迟钝,太大则“飘”;
  • powf(0.98f, ...)是指数衰减,比线性m_velocity *= 0.98更平滑;
  • m_map->canMoveTo()内部遍历futureRect覆盖的所有MapTile,只要任一格子是WALL_STEEL或WALL_BRICK就返回false;
  • getCollisionNormal()返回归一化法向量(如撞左墙返回(-1,0)),用于精准回弹,不是简单setX(oldX)。

3.2 子弹生成与飞行:对象池复用 + 独立更新线程

子弹不每次new Bullet(),而是从池子里取:

// entity/bullet.h class Bullet : public BaseEntity { public: static Bullet* acquire(); // 从池中取,若空则 new void release(); // 归还池中,不清除数据仅标记可用 void update(float deltaTime) override { // 1. 位置更新:匀速直线运动 setPos(pos() + m_velocity * deltaTime); // 2. 边界检测:飞出屏幕即回收 if (!scene()->sceneRect().contains(pos())) { release(); return; } // 3. 碰撞检测:只检测与 Tank 和 MapTile 的碰撞 QList<QGraphicsItem*> colliding = collidingItems(); for (auto *item : colliding) { if (auto *tank = dynamic_cast<Tank*>(item)) { tank->takeDamage(m_damage); // 造成伤害 release(); // 子弹销毁 return; } if (auto *tile = dynamic_cast<MapTile*>(item)) { if (tile->isSolid()) { release(); return; } } } } private: static QVector<Bullet*> s_pool; static QMutex s_poolMutex; float m_damage{50}; QPointF m_velocity; // 发射时设定,如:QPointF(cos(rot), sin(rot)) * 800.0f };

注意:Bullet::update()里collidingItems()返回的是QGraphicsItem*,必须用dynamic_cast安全转型,不能直接qgraphicsitem_cast——因为MapTile是QGraphicsItem子类,但未声明QGraphicsItem的Q_OBJECT,qgraphicsitem_cast会失败。

3.3 敌方 AI:A* 路径规划 + 状态机驱动

EnemyTank的智能不是“随机走”,而是每 2 秒调用一次updatePathToPlayer():

// entity/enemytank.cpp void EnemyTank::updatePathToPlayer() { auto playerPos = m_player->pos(); QPoint start = m_map->worldToGrid(pos()); // 当前坐标转格子索引 QPoint end = m_map->worldToGrid(playerPos); // 使用预计算的 A*(map 已提前生成邻接表) m_path = m_map->findPath(start, end, [](const QPoint &p) { return !m_map->isSolidAt(p); }); // 路径点转世界坐标,供 moveForward() 使用 m_worldPath.clear(); for (const QPoint &grid : m_path) { m_worldPath.append(m_map->gridToWorld(grid)); } } void EnemyTank::moveForward() { if (m_worldPath.isEmpty()) return; QPointF target = m_worldPath.first(); QPointF toTarget = target - pos(); float dist = sqrtf(toTarget.x()*toTarget.x() + toTarget.y()*toTarget.y()); if (dist < 5.0f) { // 到达路径点,删掉 m_worldPath.removeFirst(); if (m_worldPath.isEmpty()) return; target = m_worldPath.first(); toTarget = target - pos(); } // 朝向目标点旋转炮塔(平滑插值,非瞬转) float targetAngle = atan2f(toTarget.y(), toTarget.x()) * 180.0f / M_PI; m_rotation = m_rotation + (targetAngle - m_rotation) * 0.1f; // 向目标点移动(匀速) setPos(pos() + toTarget.normalized() * m_speed * 0.016f); // 16ms 步长 }

关键设计点:

  • findPath()返回的是QVector<QPoint>(格子坐标),避免浮点误差累积;
  • toTarget.normalized()是单位向量,乘以m_speed * 0.016f得到本次移动偏移量,保证速度恒定;
  • 旋转用0.1f插值系数,让炮管转动有“惯性感”,不是机器人式瞬转。

3.4 碰撞检测优化:空间分区 + 早期退出

Map::canMoveTo()如果每次都遍历全部 13x13 格子(共 169 个),性能会崩。项目用了四叉树空间分区:

// map/tilemap.h class TileMap { private: struct QuadNode { QRectF bounds; QVector<int> tileIndices; // 本节点内所有 solid tile 的索引 std::unique_ptr<QuadNode> children[4]; }; std::unique_ptr<QuadNode> m_quadRoot; public: bool canMoveTo(const QRectF &rect) const { return !hasSolidInRect(rect, m_quadRoot.get()); } private: bool hasSolidInRect(const QRectF &rect, const QuadNode *node) const { if (!node || !rect.intersects(node->bounds)) return false; // 1. 先查本节点的 tileIndices(快速命中) for (int idx : node->tileIndices) { if (m_tiles[idx].isSolid() && m_tiles[idx].rect().intersects(rect)) { return true; } } // 2. 再递归子节点(如果存在) if (node->children[0]) { for (int i = 0; i < 4; ++i) { if (hasSolidInRect(rect, node->children[i].get())) { return true; } } } return false; } };

实测效果:13x13 地图下,canMoveTo()平均耗时从 0.08ms 降到 0.012ms,对 60FPS 循环至关重要。


4. 编译与运行:从零开始跑通的完整步骤(含 Windows/macOS/Linux 三端适配)

别信 README 里那句“cmake .. && make”——那是给已经配好环境的人看的。我按新手第一次接触的真实路径重写,每一步都验证过。

4.1 环境准备:Qt 安装与路径确认

Windows(推荐 Qt 5.15.2 MSVC2019 64-bit)

  • 下载 Qt Online Installer → 选择Qt 5.15.2→MSVC 2019 64-bit→Qt Charts(可选,用于后期加统计图);
  • 安装后检查环境变量:打开 CMD,输入qmake -v,应输出Using Qt version 5.15.2;
  • 关键:确保C:\Qt\5.15.2\msvc2019_64\bin在PATH中,否则cmake找不到moc。

macOS(Qt 5.15.2 clang 64-bit)

  • brew install qt@5(Homebrew 安装);
  • 若用 Qt 官方安装包,需手动添加:export PATH="/Users/xxx/Qt/5.15.2/clang_64/bin:$PATH"到~/.zshrc;
  • 验证:qmake -query QT_INSTALL_PREFIX应返回 Qt 安装路径。

Linux(Ubuntu 22.04,Qt 5.15.2 GCC 64-bit)

  • sudo apt install qt5-default libqt5multimedia5-dev;
  • 验证:qmake -v输出版本,pkg-config --modversion Qt5Widgets应返回5.15.2。

提示:项目CMakeLists.txt显式指定find_package(Qt5 5.15.2 REQUIRED COMPONENTS Widgets Multimedia),版本低于 5.15 会直接报错,不降级兼容。

4.2 构建流程:四步命令,拒绝 GUI 工具

# 步骤1:进入项目根目录(确保 CMakeLists.txt 在当前目录) cd /path/to/tankwar/ # 步骤2:创建独立构建目录(严禁在源码目录下 cmake) mkdir build && cd build # 步骤3:配置(关键:指定 Qt 路径,尤其 Windows 多版本共存时) # Windows(MSVC): cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_PREFIX_PATH="C:/Qt/5.15.2/msvc2019_64" .. # macOS(Clang): cmake -DCMAKE_PREFIX_PATH="/Users/xxx/Qt/5.15.2/clang_64" .. # Linux(GCC): cmake -DCMAKE_PREFIX_PATH="/usr/lib/x86_64-linux-gnu/cmake/Qt5" .. # 步骤4:编译(Windows 用 msbuild,其他用 make) # Windows: msbuild tankwar.sln /p:Configuration=Release # macOS/Linux: make -j$(nproc)

编译成功后,可执行文件位置:

  • Windows:build/Release/tankwar.exe
  • macOS:build/tankwar.app/Contents/MacOS/tankwar
  • Linux:build/tankwar

4.3 首次运行必做三件事

  1. 检查资源路径:程序启动时会读resources/config.json,如果报错Failed to load config,说明resources/文件夹没放在可执行文件同级目录。正确结构:

    build/ ├── tankwar.exe # 或 tankwar └── resources/ # 必须存在! ├── config.json ├── images/ └── sounds/
  2. 验证音频模块:resources/sounds/fire.wav若播放无声,大概率是 Qt Multimedia 插件缺失。Windows 下检查build/Release/platforms/qwindows.dll是否存在;macOS 检查tankwar.app/Contents/PlugIns/audio/下是否有libqtaudio_coreaudio.dylib。

  3. 禁用高 DPI 缩放(Windows 高分屏用户):在main.cpp的QApplication a(argc, argv);后加:

    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);

    否则坦克贴图会模糊。这行已写在源码里,但新手常忽略。

4.4 常见问题排查:新手翻车现场实录

现象:窗口一闪而退,控制台无报错

原因:resources/文件夹缺失或路径错误,ConfigLoader构造时抛异常未捕获,main()中return 1退出。
解决:在main.cpp的QApplication创建后加qInstallMessageHandler(customMessageHandler);,自定义 handler 打印所有qCritical日志;或直接用调试器断点ConfigLoader::load()。

现象:坦克能动,但子弹不显示

原因:Bullet对象创建后未addToScene(),或GameScene未设置为MainWindow的 scene。
解决:检查PlayerTank::fire()中是否调用scene()->addItem(bullet);再确认GameView::setScene()是否传入了正确的GameScene*实例。

现象:敌方坦克卡在墙角不动,updatePathToPlayer()日志显示路径为空

原因:MapLoader解析地图时,将WALL_STEEL误标为WALL_BRICK,导致 A* 认为钢墙可通行,实际canMoveTo()拒绝移动,形成死锁。
解决:打开resources/maps/level1.txt,确认第 5 行第 3 列字符是S(steel)而非B(brick);用qDebug() << "Path size:" << path.size();打印路径长度验证。

现象:macOS 下编译报错ld: library not found for -lQt5Multimedia

原因:Homebrew 安装的 Qt5 默认不包含 Multimedia 模块。
解决:brew uninstall qt@5 && brew install qt@5 --with-multimedia(注意:Homebrew 4.0+ 已弃用--with-*,此时改用brew install qt@5 && brew install qt@5-multimedia)。

现象:Linux 下运行闪退,dmesg显示segfault at 0 ip 0000000000000000

原因:QPainter在QWidget上双缓冲未启用,paintEvent()中直接QPainter(this)导致空指针。
解决:检查render/custompainter.h,确认CustomPainter::paint()内部使用QPainter painter(&m_offscreenPixmap);而非QPainter painter(widget);;m_offscreenPixmap必须在resizeEvent()中随窗口大小重建。


5. 二次开发指南:加功能、改逻辑、换皮肤的实操技巧

现在你已经能跑通、能调试、知道每个类在干什么。接下来,我带你亲手改三个真实需求:加一个暂停键、把敌方坦克换成会扔手雷的 Boss、换一套像素风 UI。不讲理论,只给可粘贴的代码和验证方法。

5.1 加暂停功能:三处修改,零风险

暂停不是“停掉 QTimer”,而是冻结游戏状态。项目已预留GameStateManager,只需三处改动:

① 在core/statemanager.h中新增状态枚举

// core/statemanager.h enum class GameState { Running, Paused, // ← 新增 GameOver, LevelComplete };

② 在core/gameloop.cpp的tick()中加判断

// core/gameloop.cpp void GameLoop::tick() { if (m_stateManager->currentState() == GameState::Paused) { return; // 直接跳过 update 和 render } // ... 原有 tick 逻辑 }

③ 在ui/mainwindow.cpp中绑定空格键

// ui/mainwindow.cpp void MainWindow::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Space) { auto state = m_stateManager->currentState(); if (state == GameState::Running) { m_stateManager->setState(GameState::Paused); statusBar()->showMessage("PAUSED"); } else if (state == GameState::Paused) { m_stateManager->setState(GameState::Running); statusBar()->clearMessage(); } } else { QMainWindow::keyPressEvent(e); // 交给父类处理其他键 } }

验证方法:运行游戏,按空格,坦克和子弹立刻静止,再按空格恢复。statusBar()显示提示即成功。注意:不要改QTimer::stop(),否则 resume 时时间戳错乱。

5.2 改 Boss 敌人:继承 EnemyTank,重写 fire()

想让第 3 关 Boss 敌人扔手雷(爆炸范围伤害),不改原有EnemyTank,而是新建类:

// entity/bosstank.h #include "enemytank.h" class BossTank : public EnemyTank { Q_OBJECT public: BossTank(QGraphicsItem *parent = nullptr); void fire() override; void update(float deltaTime) override; private slots: void onHandGrenadeTimeout(); private: QTimer m_grenadeTimer; int m_grenadeCooldown{3000}; // 3秒扔一次 }; // entity/bosstank.cpp BossTank::BossTank(QGraphicsItem *parent) : EnemyTank(parent) { connect(&m_grenadeTimer, &QTimer::timeout, this, &BossTank::onHandGrenadeTimeout); m_grenadeTimer.start(m_grenadeCooldown); } void BossTank::onHandGrenadeTimeout() { // 1. 创建手雷(继承自 Bullet,但爆炸逻辑不同) Grenade *grenade = Grenade::acquire(); grenade->setPos(pos()); grenade->setVelocity(QPointF(0, -500)); // 向上抛 scene()->addItem(grenade); // 2. 重置冷却 m_grenadeTimer.setInterval(qrand() % 2000 + 2000); // 2~4秒随机 } void BossTank::fire() { // 原有子弹逻辑保留,现在是“子弹+手雷”双模式 EnemyTank::fire(); }

关键点:Grenade类需重写update(),落地后延时 800ms 爆炸,调用m_map->explodeAt(pos(), 60)(爆炸半径 60 像素,摧毁范围内所有砖墙)。这个explodeAt()已在map/tilemap.cpp中实现,你只需调用。

5.3 换 UI 皮肤:替换图片 + 修改尺寸常量

项目所有贴图路径硬编码在resources/images/,换皮肤只需两步:

① 替换图片文件

  • player_tank.png→ 你的新玩家坦克图(尺寸必须 48x48)
  • enemy_tank.png→ 新敌人图(48x48)
  • bullet.png→ 新子弹图(16x16)
  • steel_wall.png/brick_wall.png→ 新墙体图(32x32)

② 修改entity/tank.h中的尺寸常量

// entity/tank.h static constexpr QSize TANK_SIZE{48, 48}; // ← 改为你新图的宽高 static constexpr QSize BULLET_SIZE{16, 16}; // ← 改为你新子弹图的宽高

验证方法:改完图片和常量,重新编译运行。如果坦克变大但移动速度没变,说明TANK_SIZE没改对;如果子弹飞出去就消失,检查BULLET_SIZE是否与图片实际尺寸一致。血泪经验:Qt 的QPainter::drawPixmap()会按原始尺寸绘制,不缩放,所以图片尺寸必须和代码常量完全匹配。

从那以后我每次接手 Qt 游戏项目,第一件事就是 grepQPainter和QTimer,确认双缓冲和锁帧是否落地;第二件事是跑一遍valgrind --tool=memcheck ./tankwar(Linux/macOS)或Application Verifier(Windows),看有没有Bullet对象池泄漏。这些不是玄学,是 Qt 游戏开发的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询