Qt飞机大战飞机类设计:QGraphicsScene、状态机与碰撞检测
1. 飞机类挂在 QGraphics 场景里是我试出来的最优解1.1 一开始我也用过 QWidget 硬拼飞机大战在 Qt 里算一个很经典的练手项目但大多数教程会把重点放在怎么把窗口搭出来怎么加背景图真到自己动手写飞机类的时候反而卡住的人特别多。我自己最开始也偷过懒想着往主窗口上堆 QWidget 控件就能把飞机做出来用 QLabel 放飞机图片监听键盘事件然后调用 label-move() 去挪位置。单机单弹的 demo 确实能跑但只要一加入子弹、敌机、爆炸特效这套方案很快就撑不住了。问题最明显的是碰撞检测。控件方案里我必须自己拿两张图片的 QRect 做 intersects 判断子弹一多这种手动判断的代码就成了一团乱麻。更难受的是层叠关系想让主角永远显示在最上面、子弹在中间、敌机在下层得用 raise_ 和 lower 之类的控件方法去维护写起来又绕又容易出错。还有性能问题每个 QWidget 本质上都是一个窗口对象几十上百个控件在场景里频繁创建销毁画面响应肉眼可见地变慢。所以试了两轮之后我彻底回到 QGraphics 框架上。QGraphicsView 这套体系看起来比 QWidget 复杂但它就是为场景里有大量可交互图形对象这种需求设计的。飞机大战里的主角、敌人、子弹、特效都是典型的图形对象用 QGraphics 体系几乎是标准答案。1.2 Scene、View、Item 三个角色的分工要写飞机类先要搞明白它到底往哪个位置上放。我习惯用拍戏打比方QGraphicsScene 是舞台负责管理所有上台的演员也负责演员之间的碰撞计算和区域查询QGraphicsView 是摄像机和显示器负责把舞台画面渲染到窗口上键盘鼠标事件、滚轮缩放也都在这层处理而飞机、子弹、敌机、爆炸特效全部都是 QGraphicsItem也就是一个个演员。Item 在场景里的前后遮挡关系由 zValue 控制。zValue 大的画在上面zValue 小的画在下面但碰撞检测时两者都会参与计算。我在飞机类里通常会给主角设一个比较高的 zValue比如 10子弹设为 5敌机设为 3背景就算没入场景也能单独用一个低层级图元。这个设置放在 QGraphics 体系里是一条赋值语句的事而 QWidget 控件的表现就差得远了。这套舞台、摄像机、演员的模型对游戏开发的好处是全局逻辑非常统一。飞机在场景里移动坐标天然就是场景坐标子弹飞行也是场景坐标碰撞检测时场景直接给出和这个演员重叠的其他演员列表。你不需要自己维护坐标状态、矩形容器、视觉层级这些杂事只需要关注每个 Item 自己的行为逻辑。1.3 加入场景item 的户口问题有个新手很容易忽略的细节new 出一个 item 之后它并不会自动出现在舞台上你必须主动调用 scene-addItem(playerPlane)。这个动作相当于给它上户口。上了户口之后场景才会在渲染时画出它碰撞检测时才会算它区域查询时才会找到它。我在第一篇里已经搭好了窗口所以这篇默认你手头有一个 GameView 和一个已经 setSceneRect 过的 QGraphicsScene。飞机类这一篇核心就是把主角这个 Item 写好并正确挂进场景。这里要特别提醒addItem 之后item 的 pos() 是相对于 scene 的。如果你在别的地方给 item 设置了 parentItem坐标又会变成相对于父 item。飞机大战里飞机直接挂在 scene 下统一用 scene 坐标能省下一堆坐标换算的麻烦。我见过有人把飞机设置成某个背景图的子 item结果移动飞机时坐标老是偏移排查了半天才发现是父坐标问题。2. 飞机类的骨架设计成员、状态机、对外接口2.1 QObject 与 QGraphicsPixmapItem 的多重继承先看一个我建议的类声明骨架这是一个贴图飞机类的基础结构#ifndef PLAYERPLANE_H #define PLAYERPLANE_H #include QObject #include QGraphicsPixmapItem class QTimer; class PlayerPlane : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: enum PlaneState { StateAlive 0, StateInvincible, StateDead }; explicit PlayerPlane(QGraphicsItem *parent nullptr); void setSpeed(int pixelPerSecond) { m_speed pixelPerSecond; } int speed() const { return m_speed; } void moveLeft(int delta); void moveRight(int delta); void moveUp(int delta); void moveDown(int delta); void tryFire(); signals: void hpChanged(int currentHp); void planeDied(); public slots: void takeDamage(int damage); private: void doMove(int dx, int dy); void clampWithinScene(); int m_maxHp 3; int m_hp 3; int m_speed 240; PlaneState m_state StateAlive; QTimer *m_invincibleTimer nullptr; int m_fireIntervalMs 150; qint64 m_lastFireMs 0; }; #endif为什么要两个都继承QGraphicsPixmapItem 提供在场景里显示一张图片的全部能力但它不是 QObject没有信号槽、没有定时器。飞机需要发信号告诉外界我掉血了我阵亡了需要定时器处理无敌帧这些能力只有 QObject 给得了。所以正确姿势是两者一起继承。这里有一个人人都可能踩的经典坑多重继承时 QObject 必须写在第一位。我第一次写反了编译器的报错信息极其晦涩查了半天才发现是基类顺序问题。这个顺序还会影响 Q_OBJECT 宏的元对象编译流程务必记住。继承 QObject 之后类里必须有 Q_OBJECT 宏。在部分集成开发环境里如果你写完 Q_OBJECT 却忘记重新跑元对象编译器还会出现一些莫名其妙的链接错误。遇到这种情况时重新执行一下 qmake 让项目重新生成一般就能解决。2.2 用状态枚举替代散乱bool飞机类里最常见的错误设计就是堆上一堆 bool 变量isDead、isInvincible、isShooting、isBombReady。这种写法在游戏复杂度上来之后就是灾难因为状态与状态之间会互相打架。比如死亡状态和无敌状态同时为 true扣血逻辑就会乱套。我上面用了一个三态枚举Alive、Invincible、Dead。StateAlive正常状态能吃伤害StateInvincible被击中后的无敌帧不吃伤害通常持续 12 秒StateDead血量为 0禁止移动和射击等待外部清理。无敌帧是这类弹幕游戏里不可省的机制。飞机中弹之后如果没有短暂免伤时间在密集弹幕里一帧内可能同时撞上三四颗子弹血条瞬间清零玩家完全没有反应机会。加无敌帧之后中弹瞬间只扣一次飞机短暂闪烁透明玩家利用无敌时间赶紧走位手感会立刻舒服很多。三态枚举还有一个好处你在写 takeDamage 时只需要一个条件判断 if (m_state ! StateAlive) return; 就能过滤掉所有不该受伤的情况不用去记一堆布尔变量的组合条件。2.3 接口保持语义化对外接口不要暴露一堆裸奔的 setPos、moveBy那样外部代码就能随便操作飞机位置游戏逻辑会散落得到处都是。飞机类应该提供语义化方法外部调用方只需要表达意图moveLeft(int delta) 表示左移多少moveRight(int delta) 表示右移多少tryFire() 表示尝试开火takeDamage(int damage) 表示受到多少伤害。外部代码不需要关心边界检测、状态判断、子弹创建的内部细节。主循环里读起来就是一行行能看懂的句子而不是一串操作皮毛的代码。如果你后续还想做不同机型差异化比如 1 号机血量高但射速慢、2 号机脆皮但攻击频率高可以先把这个 PlayerPlane 类视为基类再派生出具体机型子类也可以通过一个配置结构体在构造时注入参数。我建议新手先用配置结构体不要在入门阶段过早依赖继承树等需求真的多了再重构成本也不高。3. 移动手感的关键bool键位表和定时器轮询3.1 直接响应 keyPressEvent 的手感问题如果你只重写视图的 keyPressEvent很快会发现一个尴尬问题飞机只在按下那一刻动一下按住不放并不会持续移动。除非操作系统开启了按键重复而按键重复有初始延迟延迟期间游戏里没有任何反应手感像踩了棉花的牛顿第一定律。原因在于 keyPressEvent 本质上是离散事件每按一次只会触发一次。按住不放时的自动重复由操作系统键盘机制接管延迟和速率都不在你的控制范围内。玩家要的是按下立刻动、松手立刻停系统自定义的重复机制完全靠不住。还有一个隐藏问题键盘焦点。视图没拿到焦点时keyPressEvent 根本不会触发。我第一次跑通时遇到的现象是窗口能显示、飞机能画出来但键盘怎么按都没反应。问题就是没设置焦点策略。解决方案是在视图构造函数里调用 setFocusPolicy(Qt::StrongFocus)并主动调一下 setFocus()。还有一个更隐蔽的情况玩家点击了其他控件后焦点可能会从视图上离开。如果处理不好会出现飞机突然不听使唤的假 bug。我后面会提到焦点丢失时清空键位表来规避。3.2 键位表 帧定时器的经典方案我的做法是维护一个 bool 数组记录当前哪些键处于按下状态再用一个 QTimer 按固定间隔去查这张表、调用飞机的移动方法。这个方案在 2D 小游戏里非常经典代码直接、逻辑清晰。在 GameView 或 GameController 里维护一张键位表bool keyTable[6] { false, false, false, false, false, false }; // 下标约定0左1右2上3下4射击在 keyPressEvent 和 keyReleaseEvent 里更新这张表void GameView::keyPressEvent(QKeyEvent *event) { if (!event-isAutoRepeat()) { switch (event-key()) { case Qt::Key_Left: case Qt::Key_A: keyTable[0] true; break; case Qt::Key_Right: case Qt::Key_D: keyTable[1] true; break; case Qt::Key_Up: case Qt::Key_W: keyTable[2] true; break; case Qt::Key_Down: case Qt::Key_S: keyTable[3] true; break; case Qt::Key_Space: keyTable[4] true; break; default: break; } } QGraphicsView::keyPressEvent(event); } void GameView::keyReleaseEvent(QKeyEvent *event) { if (!event-isAutoRepeat()) { switch (event-key()) { case Qt::Key_Left: case Qt::Key_A: keyTable[0] false; break; case Qt::Key_Right: case Qt::Key_D: keyTable[1] false; break; case Qt::Key_Up: case Qt::Key_W: keyTable[2] false; break; case Qt::Key_Down: case Qt::Key_S: keyTable[3] false; break; case Qt::Key_Space: keyTable[4] false; break; default: break; } } QGraphicsView::keyReleaseEvent(event); }这里的 isAutoRepeat() 是必写的关键。系统在按键按住时会自动重复发送 keyPress 事件如果不过滤keyTable 里的 true 就会在没有真正松开按键的情况下被反复设置导致 release 事件和 press 事件无法配对状态永远清不掉。过滤自动重复之后press 和 release 就能严格配对。视野焦点丢失时还应该在 focusOutEvent 里把整张 keyTable 清零否则玩家切到别的窗口再切回来会发现飞机自己往一个方向飞那是按键状态残留在表里了。void GameView::focusOutEvent(QFocusEvent *event) { for (int i 0; i 6; i) keyTable[i] false; QGraphicsView::focusOutEvent(event); }3.3 定时器轮询与边界限制然后启动一个 QTimer每 16ms 触发一次相当于约 60FPS 的轮询频率void GameController::onTick() { qreal step m_plane-speed() * 0.016; if (keyTable[0]) m_plane-moveLeft(step); if (keyTable[1]) m_plane-moveRight(step); if (keyTable[2]) m_plane-moveUp(step); if (keyTable[3]) m_plane-moveDown(step); if (keyTable[4]) m_plane-tryFire(); }16ms 是理论间隔实际 QTimer 可能略有抖动但对飞机大战这种休闲小游戏来说完全够用。移动速度我建议按每秒移动多少像素来设计而不是每帧移动多少像素这样以后把定时器间隔改成 10ms或者换用 QElapsedTimer 做真实帧间隔同步飞机的绝对速度不会变不用重新调参数。边界限制是另一个容易翻车的地方。很多人写成 if (x() 0) setX(0)然后发现飞机还没完全到边缘就先卡死了甚至机身一半已经在屏幕外面。原因在于 setPos 设置的是 item 的原点坐标而不是可见区域的左上角。我通常会把飞机的锚点放在中心也就是 setTransformOriginPoint(boundingRect().center())。后续旋转、缩放都会围绕中心进行坐标 x()、y() 仍然代表 item 的原点。边界检测时不能只拿原点判断要把机身的半宽、半高算进去void PlayerPlane::clampWithinScene() { if (!scene()) return; QRectF limit scene()-sceneRect(); qreal halfW boundingRect().width() / 2.0; qreal halfH boundingRect().height() / 2.0; qreal xMin limit.left() halfW; qreal xMax limit.right() - halfW; qreal yMin limit.top() halfH; qreal yMax limit.bottom() - halfH; setPos(qBound(xMin, x(), xMax), qBound(yMin, y(), yMax)); }这样飞机中心最多贴近场景边缘向内一个机身半径的位置视觉上机身可以贴着边但不会被切走一半。如果你想做露半个头可以极限擦弹的效果保留这个半身边界反而更好如果想让飞机完全限制在场景内就把 halfW 改成整机宽度。另外斜向移动时有一个物理细节要处理。如果同时按下左和上两个方向都以满速合成位移斜向速度就会比直线快 1.414 倍玩家会感觉斜着飞发飘。我通常会在 GameController 的 tick 里判断是否有两个正交方向同时输入如果是就把 step 乘 0.7071。这是个小细节但对手感的影响不小。4. 射击逻辑让子弹类配合射速控制4.1 子弹的飞行逻辑子弹和飞机的职责不同它可以设计得非常薄。继承 QGraphicsPixmapItem 后用 moveBy 按方向移动即可我常用的子弹类骨架是这样class Bullet : public QGraphicsPixmapItem { public: Bullet(const QPointF dir, qreal speed, int damage) : m_dir(dir), m_speed(speed), m_damage(damage) {} int damage() const { return m_damage; } void tick(qreal dt) { moveBy(m_dir.x() * m_speed * dt, m_dir.y() * m_speed * dt); } private: QPointF m_dir; qreal m_speed; int m_damage; };子弹不需要关心按键、状态、信号只需要知道朝哪个方向、多快、碰到人打多少伤害。在 GameController 的 onTick 里遍历场景中所有子弹调用它的 tick并且把离开 sceneRect 的子弹移除并删除。这一步不做的话子弹一多飞出屏幕外的 item 全堆积在场景里碰撞检测的开销会缓慢上涨密集弹幕关卡里帧率可能从 60 掉到 30。有一个手感细节出界删除边距不要设成 0。子弹刚碰到屏幕边界就瞬间删除玩家在屏幕边缘有时会看到子弹凭空消失。我习惯让子弹飞出场景边界 50100 像素再回收视觉上更自然玩家也不会觉得边缘有隐形墙。4.2 射速节奏时间戳比普通计数器稳tryFire 方法如果直接 new 一颗子弹按住空格之后系统键盘自动重复加帧循环会导致子弹连成一条直线游戏平衡瞬间崩掉。弹幕游戏里射击间隔是必须有的参数。我推荐用发射时间戳控制void PlayerPlane::tryFire() { if (m_state StateDead) return; qint64 now QDateTime::currentMSecsSinceEpoch(); if (now - m_lastFireMs m_fireIntervalMs) return; Bullet *bullet new Bullet(QPointF(0, -1), 500, 1); bullet-setPos(x(), y() - boundingRect().height() / 2.0); scene()-addItem(bullet); m_lastFireMs now; }m_fireIntervalMs 默认 150ms 时每秒大约 67 发节奏感适中。控制方法你也可以用 QTimer但 QTimer 有一个缺点你没办法在按下的那一瞬间立刻打出第一发总是要等定时器先触发一次。时间戳方案则天然支持即时首发这对射击手感很重要。玩家按下按键子弹必须立刻出现在屏幕上等半拍就会感觉界面迟钝。创建子弹之后m_lastFireMs 要立刻更新。如果先创建子弹再更新遇到同一帧多次调用 tryFire 的情况就会连射两发。别问我是怎么知道的这种反直觉的 bug 很容易写出来。4.3 子弹类型与安全转换碰撞检测时场景里会有各种 item怎么能确定某个 item 是不是敌方子弹我给 Bullet 增加一个自定义类型标记enum { TypeBullet QGraphicsItem::UserType 1 }; int type() const override { return TypeBullet; }有了这个类型就可以用 qgraphicsitem_cast 做安全转换if (item-type() Bullet::TypeBullet) { Bullet *bullet qgraphicsitem_castBullet *(item); // 子弹的 damage 等信息就安全可用了 }qgraphicsitem_cast 内部会检查类型匹配比 static_cast 安全得多。如果 type 对不上static_cast 会把一个毫不相干的 item 强转成 Bullet 指针后续访问 damage() 就是未定义行为程序可能当场崩溃而且崩溃位置看起来和碰撞毫无关系很难排查。5. 碰撞与死亡流程飞机怎么通知外界5.1 collidesWithItem 与 collidingItems 的分工Qt 场景内置了两种碰撞查询方式itemA.collidesWithItem(itemB)判断两个 item 是否发生碰撞scene-collidingItems(itemA)返回和 itemA 碰撞的所有 item 列表。在 GameController 的 onTick 里我建议站在子弹的角度查碰撞。子弹数量多、生命周期短一个子弹只关心我有没有打到主角或敌人。遍历场景里所有子弹调用 scene-collidingItems(bullet)再对返回列表里的 item 逐个判断类型。这比站在主角角度查所有子弹直观也更容易扩展敌人的子弹打主角玩家的子弹打敌人走同一套遍历逻辑只是目标类型不同。性能方面collidingItems 是逐 item 计算场景里几百个 item 时完全没问题如果弹幕海量再考虑按子弹/敌人分组维护列表减少全量遍历。入门阶段不建议提前优化先把一套逻辑跑通更重要。5.2 用 shape() 精修碰撞区QGraphicsPixmapItem 默认的碰撞区是整张图片的矩形范围。如果飞机贴图本身是矩形机身当然没问题但多数飞机素材是带透明边缘的机翼之间还有镂空用矩形碰撞区会让玩家觉得明明没碰到就掉血了很受挫。更精确的做法是重写 shape()返回一个更贴合机身轮廓的 QPainterPathQPainterPath shape() const override { QPainterPath path; qreal w boundingRect().width(); qreal h boundingRect().height(); path.addEllipse(QPointF(0, 0), w * 0.45, h * 0.45); return path; }注意 shape() 是在 item 局部坐标下定义的所以椭圆的中心是 (0, 0)半径由 boundingRect 缩放。shape() 不会画任何可见内容它只负责告诉场景物理碰撞轮廓长什么样。很多人第一次看到这个函数会误会成用来画形状的其实绘制逻辑在 paint() 里shape() 只服务于碰撞检测。形状也不是越精确越好。shape 越复杂碰撞计算的顶点越多。单个飞机几十个顶点的开销当然无所谓但敌人如果是密集的小蝌蚪群动辄上百个 item用简单椭圆近似反而更合理。对入门项目用矩形完全没问题等手感调优时再引入 shape() 也不迟。5.3 takeDamage 里的状态判断飞机被击中后是一套完整的状态流程我强烈建议把受击、无敌、死亡封装在飞机类内部不要暴露外部快来删我的自由操作void PlayerPlane::takeDamage(int damage) { if (m_state ! StateAlive) return; m_hp - damage; if (m_hp 0) { m_state StateDead; emit planeDied(); } else { m_state StateInvincible; m_invincibleTimer-start(2000); } emit hpChanged(m_hp); }先判断状态不是 Alive 就直接忽略。扣血后再判断是否死亡死亡就发 planeDied 信号否则进入无敌状态并启动计时器2 秒后超时回到 Alive。这个流程简单但能把无敌帧和死亡绑得很稳固不会出现已经死了还被打无敌状态还能掉血的怪事。无敌帧期间最好让飞机视觉上闪烁或半透明让玩家明确感知我现在无敌可以保命走位。简单做法是在无敌计时器生效期间每 100ms 切换一下 setOpacity(0.3) 和 setOpacity(1.0)计时结束恢复不透明。透明度只影响视觉不影响碰撞因为碰撞用的是 shape()和 opacity 无关这一点可以放心。5.4 谁来负责 delete这是初学者最容易踩的雷飞机被击中后直接在 takeDamage 里把自己 delete 掉或者在槽函数里 delete 了发送信号的 sender。两种写法都会让程序在信号槽返回后访问一个已经不存在的对象崩溃来得毫无预兆。我的原则是飞机类内部永远不要 delete 自己。飞机死亡后它仍然是一个有效的 item只是状态切到 StateDead停止移动和射击等待外部 GameController 收到 planeDied 信号之后再统一收尾。收尾工作包括播放爆炸动画、扣命、重置飞机位置并恢复满血、或者清空场景进入游戏结束界面。具体怎么收尾是 GameController 的事飞机类只负责报告我死了这个事实。子弹也是同理。飞机负责创建子弹并 addItem 到场景但子弹后续的移动、碰撞、销毁都不由飞机管。每次 onTick 里GameController 统一遍历场景中的子弹该移动的移动该出界的回收该碰撞的删除。这种谁创建谁回收的职责分离能让生命周期变得极其清晰不会出现某个对象被两个模块同时管理的混乱局面。如果场景里 item 很多也可以借助 QGraphicsScene 的 clear() 一键清理但 clear() 会删掉全部 item。如果个别 item 是 QObject 子类且正在发信号析构顺序可能会引发二次释放问题。所以我更推荐手工调度虽然多写几行但每个对象的生死都在掌控之中。6. 踩坑记录图片、碰撞盒、内存管理三大雷区6.1 图片显示异常的两个经典来源编译运行之后飞机消失最常见的两个原因我各踩过一次。第一图片带了不透明的白底。QGraphicsPixmapItem 虽然按像素图的 alpha 通道绘制但前提是图片本身有透明背景。如果美术素材是白底 PNG 或 JPG可以把白底先在画图工具里抠掉再放进资源文件也可以用 setMask 做启发式抠图QPixmap pm(:/images/plane.png); pm.setMask(pm.createHeuristicMask());这个方法对锯齿边缘不太友好只适合临时应急正式素材还是用透明底 PNG 最稳。第二资源路径问题。Qt 里用相对路径加载图片时路径是相对于程序启动时的当前工作目录的而不是工程源码目录。开发时从 IDE 启动可能没问题但换台电脑、换个启动方式图片就丢了。最稳的方案是把图片放进 .qrc 资源系统通过:/images/plane.png这种路径访问。资源会被编译进程序里不依赖外部文件也不会受工作目录影响。6.2 没有 addItem 就调用碰撞检测我为图省事在某一个版本里先写了碰撞检测逻辑再创建飞机结果怎么调都没有碰撞。问题就是我前面反复强调的item 必须在场景里才有坐标、才有碰撞关系。一个 item 如果没 addItem 进 scene它在场景里就是一个幽灵即使屏幕上看起来有图形但场景的碰撞系统对它是完全不可见的。这种 bug 最恶劣的地方在于画面表现完全正常所有逻辑都在跑就是碰撞等于零。排查时要先在 addItem 之后打印一下 scene-items().count()确认自己放进场景的 item 数量对不对。如果数量对但碰撞还是不对再检查 shape 和坐标原点。6.3 生命周期混乱delete 之后还在访问子弹从场景中 removeItem 之后内存并不会自动释放如果后续还有地方引用这个指针就成了经典的悬空指针。我见过一种写法子弹出界时直接 delete但 onTick 的循环里还保留着一份子弹指针列表循环到已经 delete 过的指针时程序直接崩溃。比较稳的做法是不要边遍历边删除而是每帧先收集需要销毁的 item 到一个临时 QList循环结束之后统一清理。还要注意同一个指针不能重复 deleteremoveItem delete 的组合操作只在当前帧执行一次。更复杂的场景是玩家子弹、敌方子弹、飞机三者互相引用。我的建议是统一在 GameController 这一层管理所有子弹飞机只管创建回收全部交给 controller。这样每个子弹指针只有一个所有者生命周期清晰不会出现两个模块同时 delete 同一块内存的情况。6.4 关于帧率与定时器的手感再补两句QTimer 的理想触发间隔是 16ms但实际可能会有抖动。飞机大战这类休闲小游戏不要求精确帧同步但移动速度最好按每秒多少像素设计而不是每帧多少像素。当你把定时器间隔改到 10ms 或将来换用 QElapsedTimer 同步真实帧间隔时飞机的绝对速度不会变不需要重新调参。前后端逻辑也建议分离。飞机类的 tryFire、takeDamage 里不要混入画帧、动画特效等表现层代码。爆炸动画可以做成一个独立的特效 class收到 planeDied 信号后由 GameController 把它投放到场景里。这样飞机类永远是干净的逻辑单元看起来也更好维护。写到这里我最深的体会是飞机类的核心价值不在于能显示图片、能移动而在于它把所有和主角相关的状态与行为收拢在一个对象里。外部想影响它只能走语义化接口它想告诉外界发生了什么就发信号。这套结构一旦稳定下来后面加双发、三发、护盾、道具、不同机体都只是在类里加成员、加分支的事不会牵连整个项目。你写飞机大战时如果也把主角类当成一个有状态、会说话的对象来设计后面几篇的敌机类、子弹类、关卡逻辑都会顺很多。

相关新闻

多语言AI Agent幽默理解与生成系统实战解析

多语言AI Agent幽默理解与生成系统实战解析

带过一个面向多语言用户的AI Agent项目,最让我头疼的不是多轮对话,也不是工具调用,而是幽默。用户抛来一句“你这回答比我家Wi-Fi还快,就是信号不太好”,Agent接不住这个梗,冷冰冰回了一句“感谢您的反馈&a…

2026/10/17 16:13:26 阅读更多 →
上下文工程实战:如何让大模型更听话的Prompt优化指南

上下文工程实战:如何让大模型更听话的Prompt优化指南

1. 为什么 Prompt 写了一大堆,模型还是听不懂人话 1.1 一次让人抓狂的“长指令”实验 先讲一个我亲眼见过的场景,估计很多人也遇到过。当时某个内部工具项目组要做“客户资料清洗”,把不同渠道的零散对话整理成标准表格。同事A为了保险&…

2026/10/17 16:13:26 阅读更多 →
打字不如说话,说话不如截图:AI代码助手多模态输入实战

打字不如说话,说话不如截图:AI代码助手多模态输入实战

我最近有个很直接的感受:跟AI代码助手打交道,输入方式的效率差距,比模型本身的差距还大。上周同事在处理一个跨端样式问题,对着屏幕嘀咕了老半天,最后拍了一张手机上出错的截图发给我。我本来想把这堆报错一字一句敲给…

2026/10/17 16:13:26 阅读更多 →

最新新闻

Unity虚拟装配中的机械运动仿真:从可视化到工程验证

Unity虚拟装配中的机械运动仿真:从可视化到工程验证

1. 这不是“转个齿轮”那么简单:虚拟装配仿真里藏着的机械逻辑陷阱很多人第一次在Unity里拖进一个齿轮模型,加个Rigidbody和HingeJoint,再写几行代码让电机轴转起来,看到齿轮咬合转动,就以为“机械运动仿真”这事搞定了…

2026/10/17 17:11:17 阅读更多 →
从工具调用到具身执行:AI-Agent走出屏幕的关键跃迁

从工具调用到具身执行:AI-Agent走出屏幕的关键跃迁

1. 为什么AI-Agent必须走出屏幕:工具调用的天花板效应过去半年我一直在折腾AI-Agent,从最早的Function Calling到MCP协议,再到把Agent接进机械臂做抓取实验。说说最直观的感受:工具调用玩到极致之后,你会明显撞上一堵墙…

2026/10/17 17:11:17 阅读更多 →
MailCheap统计功能详解:打开/点击追踪与收件箱分布报告,数据不再虚高

MailCheap统计功能详解:打开/点击追踪与收件箱分布报告,数据不再虚高

【免费下载链接】mailcheap Newsletters where you pay for the emails you send, not for dormant subscribers, at a fraction of the price of beehiiv and the like. Open source: self-host it, or use mailcheap.ai. 项目地址: https://gitcode.com/gh_mirror…

2026/10/17 17:11:17 阅读更多 →
实时识别自定义词库:语音转文字工具的配置与调优指南

实时识别自定义词库:语音转文字工具的配置与调优指南

我做会议记录和视频字幕已经有快五年了,中间换过不少语音转文字工具。这里面最让人头疼的就是那些专业术语,无论什么工具,碰上一堆人名、产品代号、技术名词,识别结果基本就是灾难现场。所以当我看到“实时识别可自定义词库”这个…

2026/10/17 17:11:17 阅读更多 →
Unity中的Task异步编程:线程模型、主线程调度与协程选型

Unity中的Task异步编程:线程模型、主线程调度与协程选型

1. 先搞懂Unity的线程模型1.1 主线程到底是什么很多人刚开始接触Unity多线程的时候,第一反应是:我直接开个线程不就行了?但很快就踩坑了——动不动就报错,或者画面直接卡死,甚至编辑器崩溃。要理解Task在Unity中该怎么…

2026/10/17 17:11:17 阅读更多 →
Claude Code模板体系实战:CLAUDE.md与Slash Commands提效指南

Claude Code模板体系实战:CLAUDE.md与Slash Commands提效指南

说真的,第一次接触 Claude Code 的时候我还是比较谨慎的,命令行式 AI 编程工具听起来很酷,但总觉得差点什么。直到我花了两周时间把所有重复性的工程操作沉淀成了一套模板体系,这工具才真正从"玩具"变成了每天离不开的生…

2026/10/17 17:10:14 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →