CHUAN2 DEV ENGINE
996 正版授权研发中心 · 360 授权合作教学中心 · 抖音传奇直播合作授权 · 快手推广运营商授权
OFFICIAL LICENSED ACADEMY 查验官方授权证书 →
// 威海旷世互娱教学基地 · 技术文章
进阶实战游戏功能996引擎

挂机为什么先打架后捡钱:AutoFindActorBehavior决策链解剖

2026-10-03 06:57 作者:996 技术组 996引擎Lua教程传奇脚本进阶实战游戏功能996引擎

挂机系统是私服二开里被点单最多的功能,也是被抄得最走样的功能。十个自己写挂机的团队,八个写出来是"见怪就打、见物就捡"的一条流水线,玩家挂一晚上,包满了、药空了、人还站在怪堆里发呆。996 引擎的挂机在 auto_behavior/AutoFindActorBehavior.lua,一百来行,却藏着一套很讲究的决策次序,今天把它讲透。

一、挂机的灵魂是"不干什么"

先看总入口,简短到离谱:

lua
function AutoFindActorBehavior:Update(player, actCompleted)
    if self:CheckAutoPick() then           -- 优先级1:正在拾取,拾取到底
        return true
    end
    if self:CheckAutoFindDropItem() then   -- 优先级2:判断要不要去捡掉落
        return true
    end
    if self:CheckAutoFindTarget(actCompleted) then  -- 优先级3:找怪开打
        return true
    end
    return false
end

三个 Check 从上往下问,谁点头谁接管,跟行为树那套优先级裁决一个模子——挂机本来就是玩家行为树的延伸。重点不在它干了什么,在那些拐弯抹角的否决条件。我把每个 Check 里劝退逻辑摘出来给你看,这才是精华。

在拆否决链之前,先把状态表说清楚。挂机系统全部状态住在 AutoProxy 里,常用的就四个查询:IsAFKState 挂机总开关、IsAutoFightState 自动战斗、IsCanAutoPick 自动拾取许可、GetPickItemID 手头的拾取单。注意前两个是"或"的关系撑起整个系统——只开自动战斗,捡货流程整体静默;只开挂机不开拾取,打完架站在钱堆上发呆。设置面板上那三个勾选框,背后就是这三个查询加一个单据字段,UI 和逻辑一一对应,没有隐藏状态,这是排查挂机问题时最幸运的一点:面板上看到什么,引擎里就是什么。

lua
-- 查找掉落物前的连环否决
function AutoFindActorBehavior:CheckAutoFindDropItem()
    local autoProxy = global.Facade:retrieveProxy(global.ProxyTable.AutoProxy)
    -- 挂机、自动战斗两种状态都没开?不捡
    if not autoProxy:IsAFKState() and not autoProxy:IsAutoFightState() then
        return false
    end
    -- 自动拾取开关没开?不捡
    if not autoProxy:IsCanAutoPick() then
        return false
    end
    -- 内挂范围内怪还够多?不捡,打架要紧
    if proxyUtils:CheckIsEnoughMonster() then
        return false
    end
    -- 小精灵一个个拾取模式下玩家主动放弃了?不捡
    if proxyUtils:CheckIsPlayerAbandonPickup() then
        return false
    end
    return self:IsCanPick()
end

五道门,过全了才允许弯腰捡东西。最值得品味的是 CheckIsEnoughMonster 这道:周围怪还打不完的时候,掉落物再香也不捡。 挂机的老玩家都懂这个手感——你要是在怪堆里见钱就捡,捡一次挨两刀,血瓶消耗比捡的钱贵,一晚上下来是负收益。引擎把"打架优先于捡钱"写成了硬规则,这不是代码技巧,是游戏经济的设计判断。

二、拾取里的细节比看起来多

优先级 1 的 CheckAutoPick 专门处理"捡到一半"的状态,看它的判断链:

lua
function AutoFindActorBehavior:IsCanPick()
    local inputProxy = global.Facade:retrieveProxy(global.ProxyTable.PlayerInputProxy)
    local autoProxy = global.Facade:retrieveProxy(global.ProxyTable.AutoProxy)
    if not inputProxy:IsMoving() then          -- 只有站定时才认拾取单
        if autoProxy:GetPickItemID() then
            autoProxy:CheckPickTimeout()       -- 捡了太久?判超时
            return true
        end
    end
    if nil == autoProxy:GetPickItemID() then
        global.Facade:sendNotification(global.NoticeTable.AutoFindDropItem)  -- 单子空了,广播找新货
    end
    if -1 == global.PathFindController:CheckNextMovePosAble() then
        autoProxy:SetPickItemID(nil)           -- 下一格走不了?这单作废
        return false
    end
    return true
end

三个细节,个个都是实战喂出来的。第一,IsMoving 的守卫:玩家自己正在跑图的时候,挂机不许插队去捡东西——你正赶去下副本,挂机突然拐弯捡了根木棍,搁谁身上都想砸键盘。第二,CheckPickTimeout:掉落物可能在远处被别人捡走、可能被地图清理,捡单挂着的每一秒都可能失效,必须带超时对账。第三,CheckNextMovePosAble 返回 -1 意思是"下一格是过不去的地形",这时候果断把拾取单作废,别让角色在墙对面无限抽搐。

还有那句 sendNotification(AutoFindDropItem)——拾取单空了,引擎不发起新的查找,而是广播一条通知让找物逻辑自己认领。又是通知机制那套"事实广播、各方认领"的思路,挂机系统自己不认识掉落物,它只管消费 PickItemID 这个单据。分层干净到这个程度,改起来才有缝可钻:你想加"只捡装备不捡药水",改的是掉落物筛选那层,挂机流程一个字不用碰。

mermaid
flowchart TD
A[挂机 Update 每帧裁决] --> B{CheckAutoPick 手里有拾取单?}
B -->|是且站定| C[走向掉落物, 捡到底]
B -->|否| D{AFK或AutoFight开着?}
D -->|否| E[挂机不工作, 返回false]
D -->|是| F{自动拾取开着? 怪不够多? 没被放弃?}
F -->|全过| G[认领掉落物, 发拾取单]
F -->|任一不过| H{actCompleted==IDLE? 没目标?}
H -->|是| I[按目标类型找怪/找NPC/找采集物]
H -->|否| J[原地待机]

三、找怪的分流:目标类型决定脾气

优先级 3 的 CheckAutoFindTarget 里有个按类型分流的判断:

lua
local targetType = autoProxy:GetTargetType()
if global.MMO.ACTOR_NPC == targetType then
    global.Facade:sendNotification(global.NoticeTable.AutoFindNPC)
    if inputProxy:GetTargetID() then
        needTerminate = true
    end
elseif global.MMO.ACTOR_MONSTER == targetType or global.MMO.ACTOR_PLAYER == targetType then
    GUIFunction:OnAutoFindHumanoidFunc()      -- 怪/玩家走人形目标查找
elseif global.MMO.ACTOR_COLLECTION == targetType then
    global.Facade:sendNotification(global.NoticeTable.AutoFindCollection)
end

挂机的"找目标"不是一个动作,是三套:找 NPC(自动交任务、自动买药都靠它)、找人形目标(怪和敌对玩家,走战斗查找)、找采集物(挖矿挂机)。每套的查找范围、选中条件、失败重试策略都不一样,引擎干脆全用通知外包出去。注意 NPC 和采集那条 needTerminate = true 的门道:找到就终止本轮挂机决策,把控制权交还给上层——因为跟 NPC 对话是交互流程,不该在挂机循环里转圈。战斗目标则不同,杀完继续找,循环常驻。同一个函数里两种生命周期,读代码时注意这个分野。

开头第一道门 actCompleted ~= global.MMO.ACTION_IDLE then return false 也别漏:上一个动作没干完,找怪的申请直接驳回。这是跟行为树那套确认机制的接口——挂机想干活,先等角色闲下来,插队会打断玩家的手动操作。

IsCanPick 里还有一个隐藏知识点值得单独讲:拾取单据 PickItemID 是挂在 AutoProxy 上的,不是挂在角色身上。为什么这个区别重要?因为"我想捡什么"是挂机系统的意图,不是角色的属性——角色死了、挂机关了、切图了,这张单据都该跟着挂机系统一起清算,挂在 Proxy 上就是跟着系统走,挂在角色上就会出"重生了还在找上一辈子的掉落物"这种灵异 bug。状态跟谁走,取决于它语义上属于谁,这条原则比任何具体代码都值钱。

下面这个演示把决策链做成了看得见的东西:三个开关对应 IsAFK、IsAutoFight、IsCanAutoPick,点"刷3只怪"加压力。盯着黄色日志看裁决过程——怪多时打死也不捡钱,怪杀得只剩零头才弯腰;关掉自动战斗只留挂机,角色捡完东西就站着;全关之后彻底躺平:

demo
fx-auto-hunt-1003b

四、实战案例:一个"捡了不该捡的"事故

某服做了"装备回收"玩法,掉落物里混了系统回收物,玩家的挂机把它们当宝贝全捡了,背包瞬间塞满,然后就没有然后了——背包满,药捡不进来,怪打不动只能白挨打,挂机一晚上死了十一次,世界频道骂声一片。

修的时候我们对着源码开了个会,结论是别在 AutoFindActorBehavior 里加过滤——它是流程裁判,不该认识"装备"和"垃圾"。正确的刀口在掉落物筛选层(发 AutoFindDropItem 通知前的那道查询),给查询加"回收物黑名单"。改完顺手做了个开关"挂机只捡蓝色以上",卖 点直接写进设置面板,当月这服的挂机活跃时长涨了四成。玩家对挂机的核心诉求从来不是"全自动",是"按我的规矩自动",规则开关越多,信任越足。

工时账给大家参考:黑名单过滤半天,设置面板加开关两小时,回归测试半天。一天半的活,让一个烂口碑功能翻身。二开的功能设计,先想玩家的"不爽清单",比堆新点子值钱。

紧接着的第二个需求更说明问题:老板要"挂机时药品低于 20 个自动回城补给"。按穿墙式写法,得在挂机循环里塞补给判断、塞回城流程、塞买药脚本,AutoFindActorBehavior 直接肿成一锅粥。按引擎的路数,补给就是又一个"认领事实"的模块:订阅挂机状态通知,自己数药,自己发回城通知,跟找怪拾取互不知晓。实现加测试两天,后来"低于 20 个自动买药""低于 10 个自动用绑定元宝买"全是这个模块里的开关行,主流程零改动。同一套架构,第一种改法是往积木上砸钉子,第二种是再叠一块积木,这就是框架二开的全部秘密。

五、自己写挂机要避的三个坑

坑一:把挂机写成 if-else 瀑布没有优先级。 捡东西和打架写成平级分支,谁先命中谁执行,玩家就会看到"放着三只怪不打好几趟去捡铜板"的迷惑行为。裁决必须有次序,次序背后是收益判断——引擎那三行 return true 就是教科书。

坑一点五:决策写进了渲染帧。 有人把挂机 Check 挂在每帧渲染回调里跑,同屏人多时一帧几十次代理查询,帧率当场教做人。挂机裁决有天然的节奏:角色空闲时每 0.2 秒问一次足够了,战斗中根本不用问。引擎这套挂在行为树的输入分发里,天然就是"有输入才裁决"的节奏。频率感是自动化系统的隐形性能账,写之前先问自己:这判断一秒跑几次,最坏情况呢?

坑二:状态判定不认状态机。 挂机在角色挥刀的前摇里插进移动指令,动作被拦腰打断,攻击判定丢失,表现是"自动战斗比手点伤害低一半"。所有插队动作都得以 actCompleted == ACTION_IDLE 为前提,这条守门不清,挂机永远写不利索。

坑三:超时和作废机制缺失。 拾取单、找怪目标都得带"保质期":目标死了要清引用,掉落物消失要作废单据,走不过去的地形要及时止损。引擎里 SetPickItemID(nil) 这种一行作废代码,背后都是"角色卡在墙对面抽搐一晚上"的真实事故。挂机系统跑一晚上不看着,任何没对账的引用都是定时炸弹。

最后补一个压箱底的调试手段:把三个 Check 的每次裁决打成一行的流水日志,格式定成"时间 裁决层 结论 原因",比如 21:47:03 DropItem 否决 怪物充足(5>=2)。挂机问题全是"跑一晚上才出现"的类型,你不可能盯一晚上屏,让日志替你值夜班。第二天早上把日志倒出来一拉,捡了什么、漏了什么、为什么发呆,全在纸上。我们定位挂机争议(玩家说挂机偷了他东西)靠的全是这份日志,时间线一拉,谁也赖不了账。给自动化系统配对账日志,不是可选项,是保命符。

六、常见疑问

问:挂机和自动战斗两个开关有什么区别?
挂机(AFK)是全家桶:找怪、战斗、拾取、后续还会挂补给逻辑;自动战斗只负责打,不弯腰捡东西。所以引擎的判断是"或"关系——只开自动战斗时打怪不捡钱,想捡把自动拾取也勾上。玩家分不清这俩,设置面板里把文案写清楚,工单能少一半。

问:小精灵(宠物拾取)模式下挂机会怎样?
看 CheckIsPlayerAbandonPickup 那道门:小精灵设成"逐件拾取"时,玩家已经声明了"东西我来捡",挂机主动放弃弯腰,两者不打架。做宠物拾取二开时记得把这个约定透传给玩家,不然两边抢一件东西的观感非常滑稽。

问:挂机状态怎么和手动操作共存?
手动输入永远优先。挂机的每个动作都以"角色空闲"和"无手动目标"为前提,玩家一点鼠标,输入代理里有了手动路径,挂机的 Check 全部让路。挂机是管家不是司机,方向盘在你手里它才动手。

问:怪不够多怎么判定?阈值能调吗?
判定在 proxyUtils:CheckIsEnoughMonster,按主玩家周围一定切比雪夫距离内的存活怪数量算,阈值随挂机设置走。二开想改"捡货更勤快",把阈值调低或者直接在拾取许可上做时段开关(比如副本里只打不捡、野外全捡),别动主流程。

👉 完整课程入口:996 全套课程体系(千余节课录) | 想跟浮生梦老师系统学的,看 LUA 高并发商业架构路径。

作者履历与出处

本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理。团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与商业版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。

← 返回文章地图返回研学路径

幂尔框架 · 实战干货 · 接口调用

LATEST ARTICLES

全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →

进阶实战游戏功能

一把钥匙开全引擎的配置门:MetaValue元变量系统

全服喇叭喊话"恭喜 &<PLAYER_NAME & 获得 &<ITEM_NAME/2001 &",这条公告里的两个占位符是怎…

2026-10-03 11:55 996 技术组
进阶实战游戏功能

ui.按钮名就能拿到控件:LuaExtend元表的八十行魔法

读引擎 UI 代码时你一定会撞见这样的写法: self._quickUI.btnClose 、 SL:GetValue("x…

2026-10-03 11:30 996 技术组
进阶实战游戏功能

断线是谁先发现的:心跳三兄弟与切后台补发

玩家手机锁屏再解锁,游戏还在原地;地铁过隧道断网半分钟,回连后接着玩——这两件"理所当然"背后是 logic/gameWor…

2026-10-03 11:24 996 技术组
进阶实战游戏功能

全引擎的下一拍在这里敲:主循环Update解剖

每个引擎都有一个"心脏":每帧跳动一次、按固定顺序叫醒所有系统的主循环。996 引擎的心脏在 logic/gameWorld…

2026-10-03 11:17 996 技术组
进阶实战游戏功能

元宝数字为什么全屏同步:CostItemCell消耗格台账

玩家买一瓶药,背包角标的元宝、商店界面的元宝、充值面板的元宝三处数字同时跳——这个瞬间几乎没有玩家会注意到,但做客户端的人都…

2026-10-03 11:10 996 技术组
进阶实战游戏功能

每个东西都有专属座位:sceneGraph功能节点树

新接手引擎渲染层的人,打开场景会看到一锅粥:地图、角色、特效、血条、UI 全糊在一起。996 引擎的答案是把场景拆成一张"座…

2026-10-03 11:02 996 技术组