5个实战案例解析:如何用分层强化学习(HRL)解决复杂游戏AI问题

在游戏AI开发领域,我们常常会遇到一些令人头疼的“硬骨头”:比如《蒙特祖玛的复仇》里那稀疏得可怜的奖励信号,或者《星际争霸》中动辄需要上千步才能完成的宏观战略决策。传统的强化学习(RL)智能体在这些场景下,往往像个无头苍蝇,探索效率低下,学习过程漫长且不稳定。这背后的核心难题,在于长期信用分配稀疏奖励。智能体很难理解一个在数百步之后才获得的胜利,与自己当前某个具体的“移动”或“攻击”动作之间,究竟有何关联。

分层强化学习(Hierarchical Reinforcement Learning, HRL)正是为破解这类难题而生的一种优雅范式。它不再试图让一个“大脑”去处理从微观操作到宏观战略的所有决策,而是引入了一种分而治之的思维。想象一下,一家公司的CEO不会去亲自过问每个员工的打卡细节,他只需要制定季度目标,并授权给部门经理;部门经理再将目标拆解为周计划,交给团队组长;组长最终安排每位成员的每日任务。HRL的智能体也是如此,它通过构建一个决策层次结构,让高层策略专注于长期目标规划,而底层策略则负责执行具体的、短期的子任务。

这种架构带来了几个显而易见的好处:首先,它通过引入时间抽象,极大地压缩了决策空间,高层策略可以在更粗的时间粒度上做出选择,比如“接下来五分钟是发展经济还是发动进攻”,而不是“此刻是让这个农民去采矿还是种田”。其次,它天然地促进了探索的结构化,底层策略被约束在完成特定子任务的范围内进行探索,避免了在无效动作上的随机游走。最后,它极大地提升了知识的复用与迁移能力,学会“采集资源”这个子任务的智能体,可以将其应用到多种需要资源的游戏场景中。

对于游戏开发者而言,HRL不再是遥不可及的学术概念,而是能够切实提升AI表现、创造更智能、更具挑战性对手的实用工具箱。本文将避开繁复的理论推导,直接切入五个具体的游戏开发实战案例,展示如何将Option框架、HIRO等主流HRL算法,在Unity或Unreal引擎中落地,并分享关键的代码片段与调参心得。

1. 案例一:征服《蒙特祖玛的复仇》—— 破解极致稀疏奖励

《蒙特祖玛的复仇》是Atari 2600上一款经典平台跳跃游戏,也因其极端的稀疏奖励(仅当拿到钥匙打开最终大门时才获得正奖励)而成为RL领域的著名“基准测试”。传统DQN在这里几乎寸步难行,因为智能体在获得第一次正奖励前,需要完成一系列精确的操作(跳跃、躲避敌人、拾取物品),任何一步失误都可能导致前功尽弃,而过程中没有任何中间奖励作为指引。

1.1 策略架构:h-DQN的双层设计

应对此类问题,一个经典的HRL解决方案是h-DQN。其核心思想是建立一个两层结构:

  • 上层控制器(Manager):负责设定抽象的、长期的内在目标(Intrinsic Goal)。在《蒙特祖玛的复仇》中,这个目标可以是“到达屏幕右侧某个区域”或“获取某把钥匙”。上层以较低的频率(例如每N帧)运行,观察全局游戏状态(如玩家位置、钥匙状态、敌人位置等),并输出一个目标向量 g
  • 下层控制器(Worker):负责执行具体的原子动作(上、下、左、右、开火等),但其策略的输入不仅是当前状态 s,还包括上层下达的目标 g。下层的奖励函数是目标达成奖励,即当智能体状态满足目标 g 时(例如位置接近目标点),获得一个小的正奖励,否则为0或小的负奖励。

这样,原本遥不可及的“通关”终极奖励,被分解为一系列可逐步达成的子目标。下层策略通过完成这些子目标获得密集的“教学信号”,从而快速学习基础技能;上层策略则学习如何规划一系列有效的子目标序列,以最终导向通关。

1.2 Unity中的代码实现要点

在Unity中,我们可以利用ML-Agents框架来实现h-DQN的思想。关键在于设计两个独立的Agent脚本,并让它们通过自定义的通信机制协作。

上层Manager Agent脚本核心逻辑:

public class ManagerAgent : Agent
{
    public WorkerAgent worker; // 关联的下层Worker
    private int stepCounter = 0;
    private int managerDecisionInterval = 50; // 每50步决策一次

    public override void OnActionReceived(ActionBuffers actions)
    {
        // 假设动作空间是连续的,输出一个二维目标坐标
        float goalX = actions.ContinuousActions[0];
        float goalY = actions.ContinuousActions[1];
        Vector2 intrinsicGoal = new Vector2(goalX, goalY);

        // 将目标下达给Worker
        worker.SetCurrentGoal(intrinsicGoal);

        // Manager的奖励仅来自环境最终奖励(稀疏)
        // 这部分奖励会在Episode结束时由环境给出
    }

    public override void CollectObservations(VectorSensor sensor)
    {
        // 收集全局状态信息
        sensor.AddObservation(transform.position); // 自身位置
        sensor.AddObservation(worker.HasKey); // 是否持有钥匙
        sensor.AddObservation(FindNearestDoorPosition()); // 最近的门的位置
        // ... 其他全局特征
    }

    public override void Heuristic(in ActionBuffers actionsOut)
    {
        // 手动控制模式,用于演示或数据收集
        var continuousActions = actionsOut.ContinuousActions;
        continuousActions[0] = Input.GetAxis("Horizontal"); // 模拟目标X
        continuousActions[1] = Input.GetAxis("Vertical"); // 模拟目标Y
    }
}

下层Worker Agent脚本核心逻辑:

public class WorkerAgent : Agent
{
    private Vector2 currentGoal;
    private Vector2 previousPosition;

    public void SetCurrentGoal(Vector2 goal)
    {
        currentGoal = goal;
    }

    public override void OnActionReceived(ActionBuffers actions)
    {
        // 离散动作:0-上,1-下,2-左,3-右,4-跳跃,5-开火
        int moveAction = actions.DiscreteActions[0];

        // 执行动作,影响环境...
        ExecuteMovement(moveAction);

        // 计算内在奖励:基于与目标距离的减少
        float distanceToGoal = Vector2.Distance(transform.position, currentGoal);
        float previousDistance = Vector2.Distance(previousPosition, currentGoal);
        float intrinsicReward = (previousDistance - distanceToGoal) * 0.1f; // 缩放因子

        AddReward(intrinsicReward);

        // 如果非常接近目标,给予一个完成奖励
        if (distanceToGoal < 0.5f)
        {
            AddReward(1.0f);
            // 可以通知Manager子目标已完成,触发Manager新决策
        }

        previousPosition = transform.position;
    }

    public override void CollectObservations(VectorSensor sensor)
    {
        sensor.AddObservation(transform.position); // 自身状态
        sensor.AddObservation(currentGoal); // Manager下达的目标状态
        // ... 其他局部特征,如脚下地面类型、前方是否有敌人等
    }
}

提示:内在奖励函数的设计是下层Worker学习的关键。简单的距离负奖励可能不够,可以结合目标方向、速度等多种因素设计更稠密、更引导性的奖励。

1.3 调参经验与避坑指南

  1. 决策频率平衡managerDecisionInterval 是关键参数。太短(如10步),上层频繁改变目标,下层无所适从;太长(如200步),上层规划可能已过时。通常从50-100步开始调试。
  2. 目标空间表示:直接将游戏世界的绝对坐标作为目标 g 可能不是最优的,因为地图可能很大且状态多变。可以尝试使用相对坐标(相对于智能体的偏移)、或者抽象特征(如“靠近钥匙”、“远离敌人”的one-hot编码)。
  3. 非平稳性问题:在下层策略学习过程中,其对同一目标 g 的响应能力在变化。这会导致上层策略基于过时的下层模型做出决策。h-DQN对此处理较弱,这也是我们下一个案例要解决的痛点。

2. 案例二:驾驭《星际争霸II》的宏观运营 —— HIRO算法应对非平稳挑战

《星际争霸II》的宏观运营是一个典型的长周期、高维度决策问题。智能体需要管理资源采集、建筑建造、科技研发、单位生产等多个并行序列,其决策的影响可能在十几分钟后的战斗中才显现。这里我们引入 HIRO 算法,它专门为解决分层强化学习中的非平稳性问题而设计。

2.1 HIRO的核心创新:目标重标记

HIRO同样采用封建架构,但其核心机制在于目标重标记。上层策略输出的目标 g_t 被定义为在 c 步后希望下层达到的状态变化量,即 s_{t+c} = s_t + g_t。当下层策略更新后,过去经验中上层采取的动作(目标 g)对于新的下层策略来说可能不再是最优的。HIRO通过一个目标转移函数 h,对旧经验中的目标进行修正,使其与新的下层策略匹配,然后再用修正后的经验来更新上层策略。

简单来说,HIRO在训练上层时,会“换位思考”:假设我现在用已经学会新本领的下层去执行当初那个任务,我当初定的那个小目标是不是该调整一下?通过这种方式,上下层策略在协同进化中能保持相对稳定。

2.2 在游戏宏观运营中的子目标设计

在《星际争霸II》中,上层Manager的观察空间可以高度抽象:

  • 资源比率(晶体矿/瓦斯)
  • 人口占用/上限
  • 关键建筑数量(基地、兵营、工厂、机场)
  • 科技等级
  • 敌方单位类型的估计数量(通过侦察)

上层Manager的动作空间(即子目标 g)可以设计为对未来一段时间内这些抽象状态的期望变化:

上层动作向量 g = [Δ矿物采集率, Δ瓦斯采集率, Δ人口上限, Δ兵营数量, Δ重工厂数量, ...]

例如,g = [0, 0, 10, 1, 0, ...] 表示未来c步内,目标是将人口上限提升10,并新建1个兵营。

下层Worker则接收当前的具体游戏状态(每个单位的详细信息、建筑队列等)和这个抽象目标 g,输出具体的微观指令:哪个农民去采矿、何时何地建造兵营、生产什么单位等。

2.3 关键代码片段与训练技巧

以下是HIRO中目标重标记环节的简化Python伪代码,适用于PySC2环境:

import numpy as np

def hiroy_style_relabel(experience_buffer, lower_level_policy, goal_transition_fn, c):
    """
    经验重标记
    experience_buffer: 存储 (s_t, g_t, a_t, r_t, s_{t+1}) 的经验池
    lower_level_policy: 当前的下层策略
    goal_transition_fn: 目标转移函数 h
    c: 子目标持续时间步长
    """
    relabeled_experiences = []
    for exp in experience_buffer:
        s_t, g_t_old, a_t, r_t, s_{t+1} = exp

        # 使用当前下层策略和旧目标,从s_t开始模拟(或查询模型)得到c步后的状态 s_{t+c}^old
        s_t_c_old = simulate_forward(s_t, g_t_old, lower_level_policy, steps=c)

        # 计算实际c步后达到的状态 s_{t+c}_real (从经验中获取)
        # 我们需要存储c步前的状态s_t和c步后的状态s_{t+c}
        # 这里假设经验池中存有(s_t, s_{t+c})对
        s_t_c_real = get_actual_future_state(exp)

        # 使用目标转移函数h,计算修正后的目标g_t_new
        # h试图找到一个新目标,使得用当前策略从s_t执行,能接近s_{t+c}_real
        g_t_new = goal_transition_fn(s_t, s_t_c_real, lower_level_policy)

        # 将修正后的经验(s_t, g_t_new, ...)加入新缓冲池
        relabeled_experiences.append((s_t, g_t_new, a_t, r_t, s_{t+1}))

    return relabeled_experiences

注意:goal_transition_fn h 本身通常也是一个需要学习的神经网络,其输入是 (s_t, s_{t+c}, lower_level_policy),输出是修正后的目标 g_t_new。训练 h 的目标是使得下层策略在 s_tg_t_new 为目标执行c步后,能达到的状态与 s_{t+c} 尽可能接近。

训练技巧

  • 分层训练节奏:可以先固定上层,集中训练下层一段时间,使其具备基本的目标达成能力;然后再开始联合训练,并启用目标重标记。
  • 探索策略:下层需要针对不同目标进行探索。可以在目标 g 上添加噪声,或者让下层策略本身具有探索性(如使用SAC、PPO等带熵正则的策略梯度方法)。
  • 奖励塑形:除了下层自身的目标达成奖励,可以给上层添加一些基于宏观状态的稀疏奖励,例如“成功建造第一个重工厂奖励+10”,“人口突破100奖励+20”,以加速初期学习。

3. 案例三:构建开放世界NPC的行为树 —— Option框架的模块化实践

在开放世界RPG中,NPC需要表现出丰富、合理且可中断的行为序列,例如“巡逻 -> 发现玩家 -> 追击 -> 攻击 -> 丢失目标 -> 返回巡逻”。Option框架为这种行为建模提供了完美的理论支撑。

3.1 Option框架与行为树的融合

一个Option ω 可以形式化地定义为三元组 <I_ω, π_ω, β_ω>

  • 初始条件 I_ω:什么状态下可以启动这个Option(例如,NPC处于“空闲”状态,且玩家在视野外)。
  • 策略 π_ω:在这个Option内部执行的具体策略(例如,巡逻路径点的循环移动)。
  • 终止条件 β_ω:什么条件下这个Option应该结束(例如,到达路径终点,或检测到异常)。

这天然对应了行为树中的节点。上层策略(行为树的根节点或选择节点)负责在多个可用的Option(行为树的行为节点或子树)中进行选择。这种结合带来了模块化可解释性的巨大优势。

3.2 Unreal Engine中基于Option的NPC AI实现

在Unreal Engine中,我们可以利用其强大的行为树系统来实现Option框架。每个BTTaskBTService可以封装一个Option的低层策略 π_ω,而Decorator可以用来实现初始条件 I_ω 和终止条件 β_ω 的检查。

示例:一个“巡逻”Option的实现

  1. 创建自定义BTTask(BTTask_Patrol:这对应 π_ω

    // BTTask_Patrol.h 伪代码
    UCLASS()
    class MYAI_API UBTTask_Patrol : public UBTTaskNode
    {
        GENERATED_BODY()
    public:
        virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
        virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    
    private:
        FVector CurrentPatrolPoint;
        int32 CurrentPointIndex;
        UPROPERTY(EditAnywhere)
        TArray<FVector> PatrolPoints;
    };
    

    TickTask中,实现向CurrentPatrolPoint移动的逻辑。

  2. 创建自定义Decorator(BTDecorator_CanPatrol:这对应 I_ω,检查是否满足巡逻条件(如不在战斗状态、生命值充足)。

    // BTDecorator_CanPatrol.h 伪代码
    UCLASS()
    class MYAI_API UBTDecorator_CanPatrol : public UBTDecorator
    {
        GENERATED_BODY()
    public:
        virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
    };
    

    CalculateRawConditionValue中,查询AI感知组件和状态机,返回是否能巡逻。

  3. 在行为树中配置

    • 将一个Selector节点作为上层策略。
    • Selector下,为“巡逻”Option创建一个分支。该分支的入口挂载BTDecorator_CanPatrol
    • 该分支的子节点是BTTask_Patrol
    • BTTask_Patrol后,可以连接一个BTService来检查终止条件 β_ω(如“是否到达巡逻点”或“是否发现敌人”),并通过设置黑板键值来通知行为树退出当前Option。

上层策略的学习:我们可以用强化学习来训练行为树中Selector节点的选择逻辑。将游戏状态(NPC状态、玩家状态、环境信息)作为观察,将选择哪个Option作为动作,将游戏整体奖励(如任务完成度、玩家沉浸感评价)作为回报。UE4/5的GameplayAbilitySystemEnvironmentQuerySystem可以与ML框架结合,为RL提供丰富的状态和动作接口。

3.3 优势与扩展

这种模式的优势在于,基础Option(如移动、攻击、交互)可以手工精心设计,保证行为的底层合理性和性能;上层的选择策略则通过RL学习,使其能根据复杂动态的环境做出最优的行为序列规划。当需要增加新行为时,只需开发新的Option模块并加入行为树,上层RL策略会自动学习何时调用它。

4. 案例四:自动化技能发现 —— 让AI自创连招与战术

在前面的案例中,子任务(Option或子目标)都是我们手动设计的。但对于一些状态空间极其复杂、甚至我们开发者都难以穷举最优子行为的游戏(如复杂的格斗游戏、MOBA),我们希望能让AI自己发现有用的技能。这就是独立子任务发现统一策略学习的用武之地。

4.1 基于DIAYN的无监督技能发现

DIAYN 是一种优雅的无监督技能发现方法。其核心思想是:我们定义一个技能空间(比如一个10维的one-hot向量 z),并训练一个策略 π(a|s, z),使得:

  1. 给定不同的 z,策略能产生不同的行为轨迹。
  2. 给定一条行为轨迹,我们能较好地推断出产生它的 z

这通过最大化技能 z 与状态 s 的互信息来实现。简单实现中,我们会同时训练:

  • 技能策略网络 π_θ(a|s, z):输入状态 s 和技能ID z,输出动作 a
  • 判别器网络 D_φ(z|s):输入状态 s,预测是哪个技能 z 产生了这个状态。

训练在固定环境下进行,没有外部任务奖励。智能体唯一的目标是让自己的行为具备“辨识度”。

4.2 在格斗游戏中的应用:自创角色连招

想象一个简化的2D格斗游戏,角色有移动、跳跃、轻拳、重拳、防御等基本动作。我们应用DIAYN:

  1. 初始化:定义技能空间大小为 N(例如N=20),每个技能 z 是一个N维one-hot向量。
  2. 交互与收集:在每一局中,随机选择一个技能 z,然后使用策略 π(a|s, z) 控制角色与一个固定AI或自己对抗一段时间,收集状态轨迹。
  3. 训练更新
    • 更新判别器 D_φ:用收集到的 (s, z) 对训练 D_φ,使其能准确从状态 s 分类出技能 z。这鼓励不同技能产生不同的状态分布。
    • 更新策略 π_θ:策略的目标是最大化判别器对正确 z 的预测概率,同时保持动作的熵(鼓励探索)。其奖励函数为 log D_φ(z|s) - log p(z),其中 p(z) 是技能的先验分布(通常均匀分布)。

经过训练,我们可能会发现一些有趣的技能:

  • z_1: 倾向于不断后跳和防御,形成一个“龟缩流”打法。
  • z_2: 倾向于快速近身并打出“轻拳->轻拳->重拳”的固定三连击。
  • z_3: 倾向于保持中距离,使用跳跃重拳进行突袭。

这些技能本身就是在无监督下发现的“连招”或“战术风格”的抽象。

4.3 从技能到分层策略

发现技能后,我们可以构建一个两层HRL智能体:

  • 上层策略 π_high(z|s):根据当前对战状态(双方血量、距离、气势等),选择使用哪个技能 z。这个策略可以用RL在有奖励(如造成伤害、赢得回合)的环境下训练。
  • 下层固定技能集:直接使用之前无监督学习到的 π_θ(a|s, z)。对于选定的 z,下层策略会执行对应的风格化操作。
# 上层策略训练循环伪代码
for episode in range(total_episodes):
    state = env.reset()
    done = False
    while not done:
        # 上层选择技能
        skill_z = high_level_policy.select_skill(state)
        cumulative_reward = 0
        # 下层执行该技能一段时间(如100帧)
        for step in range(skill_duration):
            action = low_level_skill_policy(state, skill_z) # 固定技能策略
            next_state, reward, done, _ = env.step(action)
            cumulative_reward += reward
            state = next_state
            if done:
                break
        # 用累计奖励更新上层策略
        high_level_policy.update(state, skill_z, cumulative_reward)

这种方法将复杂的动作序列学习分解为“战术选择”和“战术执行”两个相对简单的子问题,并能复用预先发现的丰富技能库,大大加速了在复杂游戏中的学习过程。

5. 案例五:多智能体协作的微观操作 —— 封建分层与集中式训练

在像《DOTA2》、《王者荣耀》这样的MOBA游戏中,团队需要高度协作。我们可以将整个团队视为一个“封建王国”,将HRL的封建思想扩展到多智能体场景。

5.1 封建多智能体HRL架构

我们设计一个团队级Manager和多个英雄级Worker

  • 团队Manager:观察全局信息(所有英雄位置、状态、地图资源、敌方动向),输出团队级子目标。这些子目标不是具体动作,而是抽象的战术指令,例如:
    • g_team = [“推进下路”, “集结打Roshan”, “分散带线”, “准备团战”]
    • 或者更量化的向量:g_team = [下路进攻强度, 中路防守强度, 野区控制度]
  • 英雄Worker:每个英雄是一个独立的智能体。它接收团队子目标 g_team、全局观察的一部分以及自身的局部观察,输出具体的英雄操作(移动、施法、攻击等)。其奖励由两部分组成:
    1. 团队目标对齐奖励:根据其行为对实现团队子目标 g_team 的贡献度计算。
    2. 个人表现奖励:补刀、击杀、生存等。

5.2 集中式训练与分布式执行

这种架构非常适合集中式训练,分布式执行的模式。

  • 训练阶段:团队Manager和所有英雄Worker的策略在同一个中心化Critic的指导下进行训练。这个Critic拥有全局信息,可以评估团队整体动作的价值,并协调各个智能体的策略更新,促进协作。
  • 执行阶段:团队Manager根据当前状态输出 g_team,并广播给所有英雄。每个英雄Worker根据 g_team 和自身局部观察,独立做出决策,无需中央通信。

5.3 实现中的通信与奖励设计

通信设计:团队Manager的指令 g_team 需要被编码为一种所有英雄都能理解的格式。可以是一个离散的战术代码,也可以是一个连续的向量。在神经网络中,这通常通过一个共享的嵌入层来实现。

奖励工程:这是成功的关键。团队目标对齐奖励需要精心设计。例如,如果 g_team 是“推进下路”,那么:

  • 位于下路附近的英雄获得更高的基础奖励系数。
  • 对下路敌方防御塔造成伤害的英雄获得高额奖励。
  • 击杀下路敌方英雄获得额外奖励。
  • 远离下路的英雄获得的奖励会衰减。

同时,要设置一些团队违例惩罚,以防止英雄完全不顾团队指令只追求个人奖励。例如,当团队指令是“集结”时,单个英雄远离团队会受到惩罚。

# 团队Manager的奖励计算函数示例
def calculate_team_reward(global_state, team_goal, hero_actions):
    base_reward = 0
    if team_goal == "PUSH_BOT":
        # 计算下路推进度:塔血量减少、兵线位置等
        push_progress = calculate_bot_lane_push(global_state)
        base_reward += push_progress * 5.0

        # 为每个英雄计算对齐奖励
        for hero in heroes:
            hero_pos = get_hero_position(hero)
            if is_in_bot_lane(hero_pos):
                alignment = 1.0
            else:
                alignment = 0.2 # 在其他路贡献较小
            # 英雄的个人贡献(伤害、击杀)会乘以这个对齐系数
            hero.individual_reward *= alignment

    elif team_goal == "ROSHAN":
        # 计算Roshan区域控制度和伤害
        roshan_reward = calculate_roshan_contest(global_state)
        base_reward += roshan_reward
        # ... 类似地为英雄计算对齐系数

    # 添加团队违例惩罚
    if team_goal == "GATHER" and team_disperion > threshold:
        base_reward -= team_disperion * 0.1

    return base_reward

在实际项目《基于HRL的MOBA AI训练》中,采用这种封建多智能体架构后,AI团队在中期战术执行上的协同性显著提升。相比完全去中心化的多智能体RL,我们的AI更少出现“葫芦娃救爷爷”式的逐个送死,或者全员扎堆打野不理兵线的情况。团队Manager学会了在“分带”和“抱团”之间进行节奏切换,而英雄Worker则学会了在团队指令框架下最大化个人作用。

从这五个案例可以看出,分层强化学习并非一个单一的算法,而是一套应对复杂决策问题的方法论工具箱。无论是处理稀疏奖励、长周期规划、构建模块化AI、自动化技能发现还是协调多智能体,HRL都提供了清晰的解决路径。关键在于根据你的具体游戏类型和AI需求,选择合适的层次结构、子任务表示以及训练框架。

Logo

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

更多推荐