news 2026/9/15 21:56:20

Qt/C++贪吃蛇游戏开发:从游戏循环到碰撞检测的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt/C++贪吃蛇游戏开发:从游戏循环到碰撞检测的工程实践

简介:基于Qt与C++的贪吃蛇游戏毕业设计完整方案,适合计算机相关专业学生完成课程设计或毕业设计时参考。压缩包内含可编译运行的源码、毕业设计论文、任务书以及答辩PPT,从需求分析、总体设计到编码实现均有对应文档支撑;论文中对技术选型、Snake窗口界面布局、碰撞检测和食物生成策略做了详细说明,任务书则明确了设计目标、预期成果和时间安排,便于读者对照规划自己的开发进度。通过阅读源码,可以掌握Qt的事件驱动模型、信号槽机制以及C++面向对象封装方法,游戏中的按键控制、得分刷新和状态管理都有清晰的代码示例。资源共16个文件,涵盖cpp、h源码,pro、ui、qrc工程配置,doc论文与pptx演示文稿,以及png/jpg界面截图,整体6.82MB,轻量易用。目前已有505人学习下载,对于需要快速上手Qt游戏开发、梳理毕设文档写作框架的读者,是一份颇具参考价值的实例资源。

1. 开学前的那个晚上,你对着 .zip 文件在想什么

“基于Qt_C++的贪吃蛇游戏设计与实现源码论文.zip”,这个名字对两类人最扎眼:一类是正在赶课程设计或毕业设计的本科生,另一类是刚把 Qt 环境配好、想拿小项目练手的在职开发者。前者需要的是“能复现、能讲清楚、能应付答辩”,后者需要的是“结构不绕、代码能跑、改起来不费劲”。这篇博文就把这两件事一起做掉:不贴整段源码,而是把贪吃蛇在 Qt/C++ 里最合理的组织方式拆开讲——类怎么划、信号槽怎么连、定时器怎么驱动游戏循环、碰撞判定放在哪一层。你会看到一套直接能用的工程思路,而不是一个只会在你机器上跑一次的 Demo。

先给结论:Qt 版贪吃蛇的技术核心不在“蛇怎么画”,而在游戏循环的驱动机制。用QTimer驱动刷新、用事件循环处理按键、用状态机管理运行/暂停/结束,这三件事想清楚,游戏本体写起来反而很快。本文将按“选型→实现→实战→验证”的顺序推进,第 2 章先解决“用 Qt 的哪套绘图方案”这个分叉口,第 3、4 章分别拆开核心类设计和游戏主循环,最后一章给你几个答辩或自测时能说出口的加分点。

2. Qt 贪吃蛇的选型分叉:QWidget 直接绘图 vs QGraphicsView

2.1 两类方案的适用边界

用 Qt 实现贪吃蛇,绕不开的第一个选择题是:在QWidget::paintEvent里直接画,还是上QGraphicsView + QGraphicsScene的图形视图框架。常见做法是前者,因为本项目的规模根本不需要图元管理和碰撞检测接口——QGraphicsItem的优势在大量可交互对象(拖拽、旋转、层级)时才显现。贪吃蛇的“蛇身”无非是一串整型索引点阵,自己管反而更直接,没有信号槽层面的额外负担,也不用担心场景迭代更新带来的性能损耗。

我一般会建议用QWidget方案,理由有三点:

  • 整张游戏地图是一个固定矩形,重绘成本极低,一帧最多 20 × 20 个方块;
  • 键盘事件用keyPressEvent重写即可,QGraphicsView里还得装事件过滤器,徒增复杂度;
  • 答辩时聊“绘图原理”比聊“框架 API”更有含金量,面试官也更想听到你解释双缓冲和局部刷新。

2.2 工程骨架:一个 MainWindow 加三个自定义类

接下来按“最小可运行工程”的标准搭目录。打开 Qt Creator,新建 Qt Widgets Application(注意不要勾生成 .ui 文件,手动代码更可控),然后在项目里加四个文件:mainwindow.h/cppgamewidget.h/cppsnake.h/cppfood.h/cpp。分工是 MainWindow 管菜单栏和状态栏,GameWidget 管绘制和事件,Snake 管蛇的移动、转向和自撞检测,Food 管食物生成坐标。

项目文件game.pro里需要确认两件事:第一是greaterThan(QT_MAJOR_VERSION, 4): QT += widgets这一行存在,第二是CONFIG += c++11让 lambda 表达式能用在信号槽里。下面给出一个完整的game.pro参考:

QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = qt_snake TEMPLATE = app CONFIG += c++11 SOURCES += main.cpp \ mainwindow.cpp \ gamewidget.cpp \ snake.cpp \ food.cpp HEADERS += mainwindow.h \ gamewidget.h \ snake.h \ food.h

这里TARGET指定生成的可执行文件名,SOURCESHEADERS必须和实际文件一一对应,漏一个都会在编译时报“undefined reference”。为什么用CONFIG += c++11?因为后续连接信号槽时会频繁用到connect(sender, &SenderType::signal, receiver, [this]{ ... })这种 lambda 写法,C++11 是它的最低要求。如果遇到error: microsoft visual c++ 14.0 or greater is required这类报错,检查的是你本机的 MSVC 编译器版本,与game.pro无关——Qt 5.15 及以上通常要求 VS2017 之后的工具链。

2.3 最小的绘图基座:paintEvent 与双缓冲

GameWidget是核心绘图类,它的paintEvent承担全部画面输出。贪吃蛇的地图可以抽象成二维网格,宏定义如下:

// gamewidget.h 中的常量定义 #define ROWS 20 // 行数(Y 方向格子数) #define COLS 20 // 列数(X 方向格子数) #define BLOCK_SIZE 24 // 每个格子的像素边长 #define OFFSET_X 10 // 地图左边距 #define OFFSET_Y 10 // 地图上边距

paintEvent里最实用的技巧是:先用QPainter填充整块背景,再绘制网格线,最后按蛇身坐标逐格填色。代码如下:

void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 绘制背景 painter.fillRect(rect(), QColor(40, 44, 52)); // 绘制网格线 painter.setPen(QColor(60, 64, 72)); for (int col = 0; col <= COLS; ++col) { int x = OFFSET_X + col * BLOCK_SIZE; painter.drawLine(x, OFFSET_Y, x, OFFSET_Y + ROWS * BLOCK_SIZE); } for (int row = 0; row <= ROWS; ++row) { int y = OFFSET_Y + row * BLOCK_SIZE; painter.drawLine(OFFSET_X, y, OFFSET_X + COLS * BLOCK_SIZE, y); } // 绘制食物(这里由 GameWidget 的 food 成员变量提供坐标) painter.setBrush(QColor(231, 76, 60)); painter.setPen(Qt::NoPen); painter.drawEllipse(food.pos().x() * BLOCK_SIZE + OFFSET_X, food.pos().y() * BLOCK_SIZE + OFFSET_Y, BLOCK_SIZE - 2, BLOCK_SIZE - 2); // 绘制蛇身 auto body = snake.body(); for (int i = 0; i < body.size(); ++i) { int col = body[i].x(); int row = body[i].y(); painter.setBrush(i == 0 ? QColor(52, 152, 219) : QColor(46, 134, 193)); painter.drawRect(col * BLOCK_SIZE + OFFSET_X, row * BLOCK_SIZE + OFFSET_Y, BLOCK_SIZE - 2, BLOCK_SIZE - 2); } }

这段代码有四处值得注意:第一,fitInView和缩放都没做,因为窗口尺寸是固定的COLS * BLOCK_SIZE + 2*OFFSET_X,省去自适应逻辑;第二,“蛇头用亮蓝色、蛇身用深蓝色”是通过三目运算符区分索引 0 和其他索引实现的,不需要额外数组;第三,绘制食物用的是drawEllipse而不是drawRect,目的只是视觉上区分圆形的食物和方形的蛇身;第四,BLOCK_SIZE - 2这个偏移让相邻方块之间有 2 像素间隙,视觉上更有“格子感”。

3.核心设计:Snake 类、Food 类和碰撞判定

3.1 Snake 类:数据结构决定实现成本

贪吃蛇的蛇身是一个线性序列,每个节点头部插入、尾部删除。这个操作特征对应两个候选结构:QQueue<QPoint>QList<QPoint>。前者出队是 O(1),但中间访问需要遍历;后者支持随机访问,头部插入最坏是 O(n)。当蛇长只有几十个点时要 “乱谈复杂度” 吗?要谈,但结论是:真正常用QList,不是因为它最快,而是因为绘制和自撞检测都需要按下标遍历。蛇身定义如下:

class Snake { public: enum class Direction { UP, DOWN, LEFT, RIGHT }; Snake(); // 初始化三节身体,向右前进 void setDirection(Direction d); // 设置下次移动方向 Direction direction() const; bool move(QPoint foodPos); // 按方向移动,返回是否吃到食物 bool isSelfCollision() const; // 判断头是否撞到自己 QList<QPoint> body() const { return m_body; } private: QList<QPoint> m_body; Direction m_dir; };

move()的返回值设计比较关键:它返回“这次移动是否吃到了食物”,因为吃到了蛇就长一节,没吃到就删尾。外部拿到这个布尔值之后决定要不要加分、要不要让Food重新生成:

bool Snake::move(QPoint foodPos) { QPoint head = m_body.first(); switch (m_dir) { case Direction::UP: head.ry() -= 1; break; case Direction::DOWN: head.ry() += 1; break; case Direction::LEFT: head.rx() -= 1; break; case Direction::RIGHT: head.rx() += 1; break; } m_body.prepend(head); // 头先插进去 if (head == foodPos) { return true; // 吃到食物:尾部不删除,相当于蛇长+1 } m_body.removeLast(); // 没吃到:去掉尾巴,保持长度 return false; }

head.ry() -= 1里用ry()返回引用再自减,是为了让代码更紧凑;等价写法是head.setY(head.y() - 1)。注意prependremoveLast的顺序:必须先插头再决定删不删尾,否则吃到食物时蛇身长度会少 1。Direction用枚举类而不是普通枚举,是为了避免UPDOWN这类名字在全局命名空间里冲突——很多 Qt 头文件里也有Up之类的定义。

3.2 Food 类:随机生成要看“可用格子”

Food类的唯一责任是产生一个新坐标,并且这个坐标不能落在蛇身上。最简单也最容易出错的写法是“循环随机直到不在蛇身上”,这在蛇身小于半个棋盘时效率很高,但接近满盘时可能死循环。更稳妥的处理是枚举所有空闲格子,再随机挑一个。代码如下:

QPoint Food::generate(const QList<QPoint>& snakeBody, int colCount, int rowCount) { QVector<QPoint> freeCells; freeCells.reserve(colCount * rowCount - snakeBody.size()); for (int x = 0; x < colCount; ++x) { for (int y = 0; y < rowCount; ++y) { QPoint pos(x, y); if (!snakeBody.contains(pos)) { freeCells.append(pos); } } } if (freeCells.isEmpty()) { return QPoint(-1, -1); // 满盘,返回无效坐标触发结束 } int index = QRandomGenerator::global()->bounded(freeCells.size()); return freeCells.at(index); }

这段代码的关键是先遍历再随机,而不是随机碰运气。QRandomGenerator::global()->bounded(n)返回[0, n)区间内的整数,是 Qt 5.10 之后的推荐写法,替代已经被废弃的qrand()。当返回值是(-1, -1)时,外部应该判定为“玩家获胜”,而不是继续调用move(),否则蛇头会跑到左上角。这里的reserve只是预分配内存,算是一个小优化,但能避免频繁扩容带来的拷贝开销。

3.3 两种碰撞:撞墙与自撞,判定放在哪一层

贪吃蛇游戏结束条件有二:蛇头越界,或者蛇头撞到自己的身体。越界判定极其直白——head.x() < 0 || head.x() >= COLS || head.y() < 0 || head.y() >= ROWS。自撞判定如果写成“遍历蛇身、逐个与头比较”,要注意一个隐藏缺陷:prepend之后蛇头在索引 0,那么body[0] == body[1]吗?不可能,因为新头的位置要么在前一个头的相邻格,要么在吃到食物时前一个头的位置被顶到索引 1。所以在move()执行完后再检查body().indexOf(body().first(), 1) != -1是安全的:

bool Snake::isSelfCollision() const { return m_body.indexOf(m_body.first(), 1) != -1; }

第二个参数1表示从索引 1 开始搜索,避免indexOf直接匹配到自身。把这个检查放在move()内部还是放GameWidget里?建议放Snake内部,由GameWidget在每次移动后调用:

// GameWidget 的 updateGame 方法中被调用的逻辑 bool eaten = snake.move(food.pos()); if (snake.isSelfCollision() || snake.isOutOfBoundary(COLS, ROWS)) { gameOver(); return; } if (eaten) { score += 10; food.setPos(Food::generate(snake.body(), COLS, ROWS)); if (food.pos() == QPoint(-1, -1)) { win(); return; } } update();

这里isOutOfBoundary是一个新增的辅助函数,内部对m_body.first()做越界值检查。你可能会疑惑:为什么不把撞墙也封装进Snake?因为越界的范围是地图属性而不是蛇的属性,把行列数传进去会让Snake依赖具体地图尺寸。作为课程设计,代码分层干净是一个稳定的加分项。

4. 游戏主循环:QTimer 驱动、键盘事件与状态机

4.1 定时器驱动的游戏循环

Qt 没有提供专门的GameLoop类,游戏的动态效果来自QTimer。实例化一个QTimer并把它和“更新逻辑”的槽函数连接起来,这是 Qt 版的“无限循环模式”——每次定时器超时,就执行一次移动+碰撞检测+重绘。关键点在于:不要重写一个while(true)循环,因为那会阻塞 Qt 事件循环,导致窗口无响应、界面白屏、键盘按键丢失。把控制权交给事件分发机制,界面才能在游戏运行时正常响应点击和按键。

初始化代码放在GameWidget构造函数里:

timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &GameWidget::onTimeout); startGame(); // 内部会调用 timer->start(150);

onTimeout里依次做三件事:推进蛇的移动、处理碰撞结果、触发重绘。这个槽函数是整个游戏的“心脏”。为什么用&QTimer::timeout而不是SIGNAL(timeout())宏?前者编译期检查类型,回调闭包可以是 lambda,且不容易把槽函数名拼错——老式宏写法在重构函数名后经常留下编译通不过或运行期崩溃。

4.2 键盘方向控制与反向拦截

重写keyPressEvent是捕获方向键最简单的路径。注意三个平台差异:Windows 上方向键键值是Qt::Key_Up这一组;macOS 的 Option 组合键干扰不了这组枚举值;但如果做跨平台,event->isAutoRepeat()需要单独处理——按下不放会连续触发keyPressEvent,导致蛇连续转向。

void GameWidget::keyPressEvent(QKeyEvent *event) { if (event->isAutoRepeat()) { QWidget::keyPressEvent(event); return; } switch (event->key()) { case Qt::Key_Up: if (snake.direction() != Snake::Direction::DOWN) snake.setDirection(Snake::Direction::UP); break; case Qt::Key_Down: if (snake.direction() != Snake::Direction::UP) snake.setDirection(Snake::Direction::DOWN); break; case Qt::Key_Left: if (snake.direction() != Snake::Direction::RIGHT) snake.setDirection(Snake::Direction::LEFT); break; case Qt::Key_Right: if (snake.direction() != Snake::Direction::LEFT) snake.setDirection(Snake::Direction::RIGHT); break; case Qt::Key_Space: togglePause(); break; default: QWidget::keyPressEvent(event); } }

反向拦截是贪吃蛇最容易翻车的地方:当蛇正向右移动时,玩家按下左箭头,若没有“当前方向为右时禁止改为左”的检查,蛇会在同一帧内头部向右而方向却被改成左,下一帧move()就会把头反向移动——穿体而过或者直接自撞。上面代码用snake.direction()做比较,注意“若当前方向是 DOWN 时才不允许 UP”,反过来也一样。空格键togglePause是额外加的交互,符合 P 键或空格键暂停的玩家认知惯性。

用户提到“qt 槽函数 返回值”这个搜索词在这里有一处强关联:槽函数不能返回值给信号发起的调用方,所以onTimeout不必返回游戏状态,所有状态都通过成员变量传递,打印到statusBar()或窗口标题。如果试图让onTimeout返回布尔值来判断“是否继续”,这在 Qt 的信号槽机制里行不通,会引导出错误的设计。

4.3 状态机:Running / Paused / GameOver / Win

不引入第一个状态变量前,代码会越改越乱——比如暂停时定时器还在跑,停止时又不知道按哪个键重新开始。我一般会用枚举做状态机:

enum class GameState { READY, // 等待开始 RUNNING, // 游戏进行中 PAUSED, // 暂停 GAMEOVER, // 撞墙或自撞 WIN // 满盘胜利 };

游戏主循环的推进函数变成:

void GameWidget::onTimeout() { if (state != GameState::RUNNING) return; bool eaten = snake.move(food.pos()); if (snake.isSelfCollision() || snake.isOutOfBoundary(COLS, ROWS)) { setState(GameState::GAMEOVER); return; } if (eaten) { score += 10; food.setPos(Food::generate(snake.body(), COLS, ROWS)); if (food.pos() == QPoint(-1, -1)) { setState(GameState::WIN); return; } } update(); }

暂停时定时器不一定需要停止。如果你用timer->stop()来实现暂停,会多出“恢复后重新计时的延时”和“状态恢复顺序”的细节;更简单的方式是像上面代码那样让onTimeout第一行检查state != GameState::RUNNING直接返回,定时器照跑,反正所有操作都被状态门卫挡在门外面。这种写法还有一个好处:调试时可以直接在onTimeout里打断点,而不必关心定时器是否还在激活。

setState内部负责 UI 反馈:

void GameWidget::setState(GameState s) { state = s; switch (s) { case GameState::READY: statusBar()->showMessage(tr("按空格键开始游戏")); break; case GameState::RUNNING: statusBar()->showMessage(tr("得分: %1").arg(score)); break; case GameState::PAUSED: statusBar()->showMessage(tr("游戏暂停,按空格键继续")); break; case GameState::GAMEOVER: statusBar()->showMessage(tr("游戏结束,按 R 重新开始")); break; case GameState::WIN: statusBar()->showMessage(tr("恭喜!你已填满整张地图")); break; } update(); }

tr()包裹字符串是 Qt 国际化的习惯。标题里的“源码论文.zip”如果有配套论文,里面一定会提到 Qt 的QTranslator机制,这里用tr()是正确姿势,它会给字符串留出翻译入口。顺带一提,热词里的“qt国际化”就是这么回事:tr()只是第一步,后面还有.ts文件和lrelease发布步骤。

5. 从“能玩”到“能过答辩”:三个可讲的实现细节

踩完上面的坑,游戏已经能玩。但“能玩”和“能讲出东西”之间有一段距离。三个细节建议你实际做在代码里,答辩时面试官问哪一句你都有话说。

5.1 蛇的移动速度动态加速:一个公式的事

需求是:吃到的食物越多,蛇跑得越快。简单实现是 “每吃一份食物,就把定时器间隔减 10ms”,但间隔减到 50ms 以下后性能会撞墙,人眼也反应不过来。常见做法是用一个反比公式控制延迟:interval = max(40, 150 - eatenCount * 4)。每次move()返回 true 时更新timer->setInterval(interval)。这个设计把“难度曲线”具体成数字,比一句“支持加速”更经得起追问。

同样的思路可以放到“分数设计”上:加 10 分可能太单调,按蛇的长度加权,比如score += snakeBodySize() * 2,既能体现“越长越有价值”,又能在状态栏快速反馈玩家操作。如果你想做得更细,可以仿照经典贪吃蛇,把“吃到食物”和“每活过 N 次移动”都计入得分,二者频率相差不多,但带来的正反馈维度不同。

5.2 平滑移动:定时器间隔与 QVariantAnimation 的取舍

很多 Qt 贪吃蛇教程止步于“格子跳变”:每次定时器到点,蛇硬生生地跳到下一格,画面看起来比较生硬。实际上有个轻量方案:绘制时不做“整格对齐”,而是根据两次定时器之间的时间比例绘制一个偏移动画。设lastMoveTime为上一次移动的时刻,now为当前重绘时间,则偏移量t = min(1.0, (now - lastMoveTime) / intervalMs)。绘制蛇头时,用QPointF坐标在两个相邻格子之间做线性插值:

qreal t = ...; // 0.0 到 1.0,表示本帧位于两个格子之间的进度 QPointF drawPos( (prevHead.x() + (currHead.x() - prevHead.x()) * t) * BLOCK_SIZE + OFFSET_X, (prevHead.y() + (currHead.y() - prevHead.y()) * t) * BLOCK_SIZE + OFFSET_Y );

注意:绘制用的坐标系统从QPoint(整型)提升为QPointF(浮点),所以paintEvent里蛇身绘制部分的参数类型要一起改。改动范围不超过 20 行,但视觉质感提升一个档次。这个方案比引入QVariantAnimation更适合本项目,因为QVariantAnimation是对象绑定的动画,和游戏循环耦合在一起会增加状态管理的复杂度。

5.3 构造回放:杀死不可复现的 Bug

最后建议在GameWidget里维护一个大小为 600 的QVector<QPoint>历史队列,每一帧记录蛇头坐标,按 Ctrl+T 触发“回放模式”,它会把历史坐标每隔 30ms 重放一遍。这个功能对调试极有价值:自撞的边界情况往往需要连按几个方向键才能复现,没有回放就得手速不够重开一局。

实现也不难:在onTimeout里把蛇头坐标追加进m_history,回放时清空蛇目前的状态,用一个只读定时器去驱动历史坐标序列,播放结束后恢复真实状态。答辩时就可以说:“我做了操作记录与回放,方便复现问题”,这在“设计与实现”型的论文里是一个很扎实的工作量证明。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 21:55:30

拒绝丑模板!汕头建站模板搭建的3个最佳实践避坑指南

拒绝丑模板!汕头建站模板搭建的3个最佳实践避坑指南 别再盯着那些一眼假、加载慢、还改不动的免费模板发愁了。很多老板觉得建站就是下个模板换个Logo,结果上线后客户直摇头,说像十年前网页,更别提转化了。这就是典型的“模板网站太丑不够用”。…

作者头像 李华
网站建设 2026/9/15 21:50:52

Linux跑Windows应用中文乱码?WinBoat字体修复指南

Linux跑Windows应用中文乱码&#xff1f;WinBoat字体修复指南 【免费下载链接】winboat Run Windows apps on &#x1f427; Linux with ✨ seamless integration 项目地址: https://gitcode.com/GitHub_Trending/wi/winboat 在 WinBoat 里打开记事本输入中文&#xff0…

作者头像 李华
网站建设 2026/9/15 21:49:45

LIN总线UDS OTA升级实战:调度表与原子性设计

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

作者头像 李华
网站建设 2026/9/15 21:49:16

跨境电商哪个平台好?这份建站避坑指南帮你省10万

跨境电商哪个平台好?这份建站避坑指南帮你省10万 很多老板一上来就问:“跨境电商哪个平台好?”但往往还没选对平台,网站就崩了。最让人头大的,莫过于备案流程一头雾水,域名解析配置错误,或者服务器带宽选小了导致访问卡顿。这不仅是技术问题,更是生意问题。今天不谈虚的,直接给出一套经过实战检验的 避坑指南…

作者头像 李华
网站建设 2026/9/15 21:48:44

Element表格固定列踩坑指南:错位、阴影与CSS sticky替代方案

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

作者头像 李华
网站建设 2026/9/15 21:46:17

Unity ECS入门:用Entitas插件重构游戏逻辑的实战指南

做了这么多年Unity&#xff0c;最让我头疼的从来不是shader写不出来&#xff0c;而是项目跑到后期&#xff0c;一堆MonoBehaviour互相乱引用&#xff0c;改一个功能牵一发动全身。尤其是需要同时维护几百上千个AI单位、子弹、可破坏物的时候&#xff0c;传统的GameObject Comp…

作者头像 李华