你的 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 就丢掉的那条反馈链路。

围绕一条线:交互产生反馈 → 反馈纠正预测 → 纠正后的预测再次和现实碰撞 → 循环收敛。


Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐