AI状态:复制决策结果,不要让客户端重跑决策
自动关联目录:AI状态:复制决策结果,不要让客户端重跑决策
核心判断:DS 负责感知、选敌和状态迁移;客户端只消费带版本的决策结果并恢复表现。同步 Idle/Chasing/Attacking 等状态,不等于把行为树、脚本计时器和迁移条件也复制到客户端执行。
一、业务冲突:AI 必须权威,又要让客户端及时演出
PVE 敌人的仇恨、攻击目标、伤害和死亡必须由 DS 决定,否则不同客户端会得到不同战局。但客户端若只等动画命令,又很难处理中途加入、相关性重建、丢包和状态覆盖。需要复制的不是“播放一次追击动画”,而是足够重建当前表现的状态事实。
二、实际实现:状态骨架、脚本逻辑和两条同步路径并存
工程中的一组 C++ 状态类型只声明状态 ID,公共基类在 OnEnter/OnUpdate/OnExit 中转调脚本或蓝图事件。底层状态基类又把非 DS 的进入、更新和退出回调留给特效、动画等表现逻辑。这种分层让状态枚举稳定、玩法迭代灵活。
同步层同时使用可靠 Multicast 传递即时状态变化,并用 Push Model 保存每个状态机的最新状态,供晚加入或断线恢复;负载包含状态机 ID、状态 ID、服务器时间以及少量通用字段。这个“事件加快照”的方向正确,但通用组件还保留 Autonomous Proxy 向 DS 上报状态的入口,服务端收到后可强制进入状态。对于服务器拥有的 AI,这条路径不应存在。
sequenceDiagram
participant B as DS Brain
participant S as DS State Machine
participant R as Replicated Snapshot
participant C as Client Presentation
B->>S: RequestTransition(reason, target)
S->>S: Validate + enter + revision++
S->>R: StateId, Revision, EnterTime, ExecutionId
R-->>C: OnRep / relevancy rebuild
C->>C: Seek animation from server time
Note over C: 不运行感知、选敌和迁移条件三、值得保留:事件负责及时,快照负责恢复
只发 Multicast 无法服务晚加入,只发属性又可能让一次性表现显得迟钝。即时事件与最新快照并存,且客户端状态回调和 DS 逻辑回调分开,是值得保留的设计。状态组件关闭自身 Tick、由统一生命周期驱动,也比每个组件各自注册 Tick 更容易做预算。
四、结构性缺陷:状态 ID 有了,迁移所有权仍不清楚
脚本状态每帧更新,行为树也可能判断迁移,Ability 还拥有自己的完成与取消时机,容易形成三个时钟。通用字符串、浮点和整数负载缺少状态级 Schema;服务器毫秒时间若存入 float,运行时间增长后精度也会下降。最危险的是复用通用“客户端请求状态”链路:若 NPC 入口没有强制 HasAuthority,新状态可能绕过感知、冷却或攻击条件。
五、目标设计:DS 状态提交必须是单一事务
Brain 只能提交“意图”,状态机统一验证迁移图、目标代际、打断优先级和当前执行实例;成功后生成单调 Revision。客户端收到旧 Revision 直接丢弃,只根据 EnterServerTime 恢复动画进度。攻击伤害等权威结果由 Ability/战斗服务产生,状态只引用其 ExecutionId。
StateReceipt CommitAIState(AIHandle ai, TransitionIntent intent)
{
if (!HasAuthority() || !ai.IncarnationMatches(intent.incarnation)) return Reject();
if (!Graph.CanEnter(ai.state, intent.next, intent.reason)) return Reject();
if (!Executions.IsCurrent(intent.executionId)) return Reject();
CancelOwnedWork(ai.stateRevision);
ai.state = intent.next;
ai.stateRevision++;
ai.enterServerTime = ServerClock::Now();
PublishSnapshot(ai.PresentationState());
return Accept(ai.stateRevision);
}六、决策规则:复制最小可恢复状态
| 内容 | 是否复制 | 原因 |
|---|---|---|
| 行为树节点、感知列表 | 否 | DS 决策内部状态,量大且会造成双重决策 |
| 状态 ID、Revision、进入时间 | 是 | 支持覆盖、Seek、晚加入和重连 |
| 当前目标稳定 ID 与代际 | 按相关性 | 客户端表现可能需要朝向、锁定和 UI |
| 攻击 ExecutionId | 是 | 把状态表现与权威 Ability 实例关联 |
| 脚本临时表、TimerHandle | 否 | 不能跨端恢复,也不应成为协议 |
| 伤害、Buff、死亡结果 | 是 | 由权威战斗系统复制,而非客户端状态推导 |
这套实践的价值在于已经区分 DS 与客户端回调,并同时考虑即时通知和恢复;下一步必须收紧迁移入口,让“谁能切状态、谁取消旧任务、谁产生战斗结果”各自只有一个答案。