你的 NPC 活在开环里——论游戏 AI 和世界模型
你的 NPC 活在开环里——论游戏 AI 和世界模型之间差了一个反馈回路
一、一个没人注意到的盲区
先看一段几乎所有 UE4 项目里都有的代码:
void AMyAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (Stimulus.WasSuccessfullySensed())
{
Blackboard->SetValueAsObject("EnemyActor", Actor);
Blackboard->SetValueAsVector("EnemyLocation", Actor->GetActorLocation());
}
}
发现问题了吗?
感知输入 → 写入 Blackboard → 行为树根据新值做决策。这条链路少了一个所有人都觉得理所当然、但实际上极其关键的步骤:
没有人问 AI:“你上一帧以为敌人在哪?你猜对了吗?”
感知事件触发后,旧数据直接覆盖,预测和现实的差异消失了。AI 不知道自己对世界的理解是对是错——它只是不断收到新数据,然后照做。
这就是开环系统。输入进来,输出出去,中间没有反馈,没有纠错,没有"我错了"这条信号回路。
二、什么是真正在"交互"
真正的世界交互不是"收到感知事件然后响应"。真正的交互是:
我预测了一个状态 → 我根据这个预测行动 → 现实给我一个结果 → 我把结果和预测对比 → 差异改变我下一次的预测。
四个环节,缺一个都不叫交互。
用游戏场景说清楚。一个 NPC 扔手雷:
传统做法(开环):
EQS 打分选点 → 直接丢 → 炸没炸到?不管,下轮再打分。
有反馈回路的做法:
NPC 预测落点 → 按预测瞄准 → 实际爆炸 → 比较预测落点和实际爆炸中心
→ 偏差过大 → 记录"这个角度低估了距离"
→ 下一发调整初速度或角度 → 再预测 → 再验证
区别在于:前者的 NPC 每次扔手雷都是一次全新的、和过去毫无关系的行为。后者每次扔手雷都在积累经验——它对自己的物理理解在上一次反馈中被纠正过了。
注意,这里不需要机器学习,不需要神经网络,不需要离线训练。只需要在 Blackboard 旁边多写一个结构体:
USTRUCT()
struct FGrenadeThrowRecord
{
float LaunchAngle; // 我用什么角度扔的
FVector PredictedLanding; // 我以为会落在哪
FVector ActualLanding; // 实际落在哪
float ErrorMeters; // 偏了多少
};
每次扔完,往 TArray<FGrenadeThrowRecord> 里追一条。下次 EQS 打分时,把历史偏差作为修正因子喂进去。这就从"每次独立决策"变成了"每次决策都和上次现实反馈挂钩"。
三、反馈的来源不只有物理——感知本身就是纠错信号
物理落点只是反馈的一种。更常见、但更被忽略的反馈来源是感知和预期的不一致。
假设一个 NPC 在巡逻。它在行为树里有一个 Service 每 0.5 秒更新一次 PredictedEnemyLocation——根据敌人上次被看到的位置和移动方向做线性外推。
然后它走到墙角,探头一看——敌人不在那里。
这就是一个反馈事件。不是"没感知到"——预期存在但感知没确认,本身就是信息。
大多数项目在 OnPerceptionUpdated 里只处理"感知成功"的分支,把"感知未确认"当成无事发生。但真正有意义的是:
void AMyAIController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 每一帧检查:我的预测还在有效期内吗?
float PredictedAge = GetWorld()->GetTimeSeconds() - LastPredictionTime;
if (PredictedAge > PredictionTimeout)
{
return;
}
// 在预测位置做一次 Trace 检查
FVector PredictedLoc = Blackboard->GetValueAsVector("PredictedEnemyLocation");
FHitResult Hit;
bool bCanSee = GetWorld()->LineTraceSingleByChannel(Hit,
GetPawn()->GetActorLocation(), PredictedLoc, ECC_Visibility);
if (!bCanSee)
{
// 预测失效——现实否定了我的模型
Blackboard->SetValueAsBool("bPredictionFailed", true);
Blackboard->SetValueAsFloat("PredictionConfidence",
FMath::Max(0.0f, Confidence - DecayRate * DeltaTime));
// 这比"看到敌人"更需要触发行为变更
BehaviorTreeComponent->RestartTree();
}
}
这里面有两个关键设计:
置信度是会衰减的。 预测被验证的次越多,置信度越高;连续被现实否定,置信度持续降低。当置信度降到某个阈值,AI 不再"相信自己的模型",切换为搜索模式。
“预测失败"的优先级高于"感知成功”。 一个预期被打破的 AI 应该比一个正在按计划执行的 AI 更警觉。这不是编程技巧问题,这是世界模型的设计问题——你对待错误的认真程度,决定了模型能多快逼近真实。
四、反馈的粒度:从"对/错"到"错了多少"
上面说的还是二元的——预测对了还是错了。但真正的反馈纠错需要知道"差了多少,差在哪个维度"。
回到手雷的例子。只记录"没炸到"是不够的,需要知道"往哪个方向偏了多少"。同样,对于敌人位置预测,需要知道:
- X 方向偏了多远(距离误差)
- 方向角偏了多少(角度误差)
- 预测的时间差(敌人比预期的快还是慢)
这三组数不是给策划看的 debug 信息——它们是下一轮预测的输入参数。
void AMyAIController::UpdatePredictionModel(const FGrenadeThrowRecord& Record)
{
// 平均误差作为下一轮的偏置修正
RunningErrorSum += Record.ErrorMeters;
ThrowCount++;
float AvgError = RunningErrorSum / ThrowCount;
// 偏置修正:如果历史平均偏近,下次瞄准时自动加一个偏置
CurrentLaunchSpeedBias = BaseSpeed + AvgError * SpeedCorrectionFactor;
// 如果最近 3 次的误差在收窄,说明模型在收敛
if (RecentErrors.Num() >= 3)
{
bool bConverging = (RecentErrors[0] > RecentErrors[1]) &&
(RecentErrors[1] > RecentErrors[2]);
Blackboard->SetValueAsBool("bAimConverging", bConverging);
}
}
NPC 不需要知道空气阻力公式,不需要解微分方程。它只需要知道自己扔了几次,平均偏了多少,最近是越扔越准还是越来越离谱。这些全是能从真实交互反馈里算出来的东西。
这意味着:NPC 的"世界知识"不是策划写进 DataTable 的,是 NPC 自己用每次行动后的现实反馈在线算出来的。 它可以和策划配置完全不相关——它只信自己被现实纠正过的结果。
五、从一个 NPC 到一个团队:共享反馈 vs 私有反馈
前面说的都是单个 NPC 自己的反馈闭环。但游戏里通常有一群 NPC。这就有了一个架构选择:
选择 A:所有 NPC 共享同一个反馈模型。
NPC_1 扔歪了,所有 NPC 的瞄准都修正。优点是收敛快——一次交互,全队受益。缺点是丢失了个体差异。现实中每个士兵扔手雷的肌肉记忆不一样,共享模型把这种差异抹掉了。
选择 B:每个 NPC 维护私有反馈历史。
NPC_1 扔了 5 次,自己的偏置修好了。NPC_2 是新刷出来的,偏置还是初始值。优点是行为多样性,一个队伍里可以有"手雷专家"和"手雷菜鸟"的区别。缺点是收敛慢,菜鸟要用自己的血去换经验。
大部分项目缺省选了 A——因为 Blackboard 本来就是共享的,因为省事,因为"玩家看不出来"。
但我认为选 B 在特定游戏类型里有巨大价值。一个潜入游戏里,两个守卫对"刚才的声响"有不同解释——一个觉得是风声,另一个觉得是入侵者——这种认知差异本身就是 gameplay。
关键不是选 A 还是选 B,而是意识到有这个选择,然后根据你的游戏设计做取舍。大部分项目根本没想过"反馈数据是谁的"这个问题,因为压根没有反馈数据。
六、怎么落地:三个可以立刻改的东西
1. 给感知回调加上预测对比。
不要只在 OnPerceptionUpdated 里写 Blackboard->SetValueAs...。加一个对比:
新位置 vs 旧预测 → 算偏差 → 更新置信度 → 偏差超过阈值 → 标记"惊讶" → 触发重新评估
改的是 Behavior Tree 的 Service 层,不动 Task,不动整体架构。成本:一个 FVector 的减法和一次分支判断,每 Tick 不到 1 微秒。
2. 给关键行动加结果记录。
手雷扔完不是结束了——记录投射参数和实际落点。同理,近战攻击预判的敌人位置、掩体选择的预期安全性,都可以加一条记录。
这不需要一个新的子系统。在 AIController 里加一个 TArray<FActionRecord>,最多保留 20 条,旧的自动清。行为树的下一次同类决策时,读这些记录做修正。
3. 把置信度写进 Blackboard 的 Key 设计里。
EnemyLocation 不够——需要配一个 EnemyLocationConfidence。行为树里判断"要不要去追敌人",条件不应该是 HasEnemyLocation == true,而是 EnemyLocationConfidence > 0.6。
这三个改动零依赖、不动框架、不破坏现有逻辑。它们只是把原来被丢弃的信息(预测和现实的差距、行动的实际结果、信念的可信程度)捡回来了。
七、回到初衷:世界模型不是产品,是过程
世界模型不是你打开 UE4 时已经在那的 UWorld。它是你的 NPC 在不断和现实碰撞、被反馈纠正、调整内部预期之后,自己慢慢"长"出来的那个东西。
它不一定是策划写的。不一定是 DataTable 配的。不一定一开始就是对的。但它会越来越对——因为现实在不断地纠正它。
做游戏 AI 这些年,最容易被忽略的从来不是多复杂的算法。而是少写一个 if 就丢掉的那条反馈链路。
围绕一条线:交互产生反馈 → 反馈纠正预测 → 纠正后的预测再次和现实碰撞 → 循环收敛。
更多推荐
所有评论(0)