type
status
date
slug
summary
tags
category
icon
password
fullWidth
fullWidth
强烈推荐!若想了解游戏AI,可观看Games104王希老师的视频: 【17.游戏引擎Gameplay玩法系统:高级AI (Part 1) | GAMES104-现代游戏引擎:从入门到实践】
分层任务网络简单应用-AI猫咪模拟器
Github传送门:Shirakoko/HTNSimulateDemo: 分层任务网络的简单应用
itch.io传送门(WebGL平台):HTNSimulateDemo by Yuki
游戏AI简介
游戏AI(Game AI)用于模拟游戏中的人物角色、NPC(非玩家角色)、敌人等的智能行为,以及游戏的自动化管理和决策系统。
反应型AI和慎思型AI
反应型AI(Reactive AI)先接受刺激输入,然后执行对应行为,如有限状态机和行为树,更适合需要快速响应和简单决策的场景;慎思型AI(Deliberative AI)将环境和背景条件纳入决策考量,可以胜任复杂任务,如目标导向行动规划和分层任务网络,更适合需要复杂规划和长期策略的场景。
常见游戏AI简介
在介绍分层任务网络(HTN)之前,首先了解其他几种常见的游戏AI技术。
- 有限状态机(Finite State Machine, FSM):是一种经典的AI模型,由一组状态(State)和状态之间的转换(Transition)组成。AI根据当前状态和输入条件决定下一步的行为。


- 行为树(Behavior Tree):是一种树状结构的AI模型,由节点组成,节点分为控制节点(如选择节点、序列节点)和执行节点(如动作节点、条件节点)。AI通过从根节点开始遍历树,决定执行哪些行为。

- 目标导向行动规划(Goal-Oriented Action Planning, GOAP):是一种基于目标的AI规划方法,AI通过评估当前状态和目标状态,选择一系列动作来达成目标。它通常使用搜索算法(如A*)来找到最优的动作序列。

- 分层任务网络(Hierarchical Task Network):是一种通过分解复杂任务为子任务来规划行动的AI系统,适用于需要层次化任务管理的场景,与目标导向行动规划相比,HTN更注重任务层次结构,而GOAP则基于目标直接寻找最优行动序列。

HTN的构成
任务
分层任务网络(HTN)的任务构成包括原子任务(PrimitiveTask)、复合任务(CompoundTask)和方法(Method)。
- 原子任务(PrimitiveTask):
- 基础执行单元,不可再分解(如移动、攻击)
- 直接作用于世界状态
- 复合任务(CompoundTask):
- 由多个方法组成的逻辑容器
- 执行时根据条件动态选择其中一个方法(类似行为树选择节点)
- 方法(Method):
- 由多个原子任务或复合任务组合而成。
- 包含前提条件(condition)和一组子任务(Subtasks);前提条件决定方法能否执行,子任务是具体的执行步骤
- 按顺序执行子任务(类似行为树顺序节点),任一子任务失败则方法终止

世界状态
世界状态(World State) 是描述当前环境的一组属性的集合;用于评估任务的可行性并选择合适的方法,例如:
- 哪里有树?
- 哪里有电锯?
- 电锯的油量是多少?
在HTN的规划阶段,智能体会复制一份世界状态的副本,用于任务的选择和分解。规划阶段不会改变真实的世界状态,只有在执行原子任务时,世界状态才会被真正改变。
为什么规划阶段要复制一份世界状态?规划完成后,只有被选中的原子任务会执行并修改真实的世界状态,如果直接操作真实的世界状态,可能会导致状态被错误修改。
HTN的运行逻辑
规划阶段(Planning Phase)
在规划阶段,HTN根据当前的世界状态,选择要执行的任务,并将其分解为一系列原子任务。具体步骤如下:
- 复制世界状态:
- 智能体复制一份当前的世界状态,用于任务的选择和分解。这个副本不会影响实际的世界状态。
- 用栈遍历任务:
- HTN从最顶层的根任务(Root)开始遍历,它是一个复合任务。
- 依次取出栈顶的任务,根据其类型做不同处理:
- 处理复合任务:
- 如果当前任务是复合任务,HTN会从该复合任务的所有方法中选择一个合适的方法。
- HTN会递归地处理该方法中的子任务(subtasks)。
- 处理方法:
- 如果当前任务是方法,HTN会依次遍历该方法中的所有子任务(subtasks)。
- 对于每个子任务,HTN会递归地处理,直到所有子任务都被分解为原子任务。
- 处理原子任务:
- 如果当前任务是原子任务,HTN会将其加入任务栈中,等待执行。
- 原子任务是HTN中最底层的任务,不可再分解。
- 得到最终结果
- 最终HTN会生成一个由原子任务组成的任务栈,这些原子任务将按照顺序执行。
执行阶段(Execution Phase)
在执行阶段,HTN会依次执行规划阶段分解出的原子任务。具体步骤如下:
- 执行原子任务:
- 智能体按照规划阶段的顺序执行原子任务。
- 每个原子任务的执行会对世界状态产生影响,如减少弹药数量、减少树木数量等。
- 任务完成或重新规划:
- 在执行每个原子任务之前,检查该任务的执行条件是否满足。如果条件不满足,中止当前的任务链,并重新进入规划阶段。
- 如果所有原子任务都成功执行完毕,认为当前的任务列表已完成。
循环过程
HTN的运行逻辑是一个循环过程,循环具体流程如下:
- 规划阶段:根据世界状态把根任务(Root)分解为原子任务。
- 执行阶段:依次执行原子任务,并更新世界状态。
- 重新规划:如果任务链被中断或完成,HTN会重新进入规划阶段,选择新的任务。
代码实现
世界状态(World State)
世界状态通常由多个对象属性组成;例如,角色的血量、位置、装备等。这些状态可能会频繁变化,并且需要被多个系统(如AI、UI、物理引擎等)读取和修改。
在实现世界状态时,主要面临两个问题:
- 状态数据的多样性
世界状态的数据类型多种多样(如整数、布尔值、对象等),如何统一存储这些不同类型的数据?
- 状态数据的实时更新
状态数据会不断变化,如何确保存储的数据能够同步更新?例如,角色的血量变为0时,存储的数据也应实时反映这一变化。
解决方案
问题 1:统一存储多种类型的数据
可以使用
<string, object>
字典来存储状态数据。在C#中object
是所有类型的基类,因此可以容纳任何数据类型。问题 2:实时同步状态数据
如果直接使用字典存储状态值,会出现以下问题:
- 修改实际对象的状态(如角色血量)不会更新字典中的值。
- 修改字典中的值也不会影响实际对象的状态。
为了解决这个问题,引入
getter
和setter
机制:getter
:一个Func<object>
委托,用于动态获取状态的值。
setter
:一个Action<object>
委托,用于动态修改状态的值。
通过这两个委托,将状态的读取和写入操作与实际对象的属性绑定,从而实现状态的实时同步。
如果直接使用一个简单的字典(
<string, object>
)来存储这些状态,会面临以下问题:- 状态无法同步更新:字典中存储的是某个时刻的状态值,而不是动态的引用。例如,角色的血量变化时,字典中的值不会自动更新。
- 无法触发副作用:直接修改字典中的值不会触发与状态相关的逻辑(如血量变化时触发死亡事件)。
以下是世界状态类的代码实现:
任务接口(Task)
复合任务、方法和原子任务它们有共通之处1.需要一个方法判断是否满足任务执行条件;2.添加子任务(原子任务不需要);把这些共通之处以接口的形式提炼出来:
原子任务(Primitive Task)
原子任务是行为树中的最小执行单元,通常表示一个简单的动作,例如「开火」或「奔跑」;其他原子任务可以通过继承原子任务抽象类来实现具体的任务逻辑。
核心特点:
- 不可分解:原子任务是最小任务单元,不能再分解为子任务。
- 双模式处理:
- 规划时:使用世界状态的副本进行条件检查和效果模拟。
- 运行时:直接操作真实的世界状态。
- 条件与效果分离:
- 条件检查(
MetCondition
)用于判断任务是否可以执行。 - 效果应用(
Effect
)用于在任务执行后修改世界状态。
方法(Method)
方法是行为树中的一个重要组成部分,用于定义任务的执行逻辑。它由多个子任务组成,这些子任务可以是复合任务或原子任务。方法的执行需要满足两个条件:
- 方法自身的前提条件:通过构造函数传入的条件函数。
- 所有子任务的条件:每个子任务的条件都必须满足。
核心特点:
- 子任务组合:方法可以包含多个子任务,形成一个任务序列。
- 条件检查:
- 方法自身的前提条件必须满足。
- 所有子任务的条件也必须满足。
- 状态隔离:
- 使用临时世界状态副本(
tpWorld
)来追踪子任务的效果。 - 只有所有子任务条件都满足时,才会将临时状态应用到真实世界状态。
复合任务(Compound Task)
复合任务是行为树中的一种任务类型,用于组合多个方法;复合任务只能添加方法作为子任务,不能直接添加原子任务或其他复合任务。
核心特点:
- 子任务限制:只能包含方法作为子任务。
- 条件检查:
- 复合任务的条件满足只需其中一个方法满足条件即可。
- 一旦找到满足条件的方法,会将其记录为
ValidMethod
,供后续执行使用。 - 可支持不同选择策略(比如顺序选择和随机选择);可以根据需求使用适合的方式。
规划器(HTN Planner)
规划器是分层任务网络(HTN)的核心组件,负责将复合任务逐步分解为可执行的原子任务。其核心思想是通过递归分解任务,最终生成一个由原子任务组成的执行计划。
核心特点:
- 根任务:每个HTN必须有一个复合任务作为根任务,类似于行为树的根节点,规划过程由此开始。
- 任务分解:
- 使用栈(
taskOfProcess
)缓存待分解的任务。 - 通过深度优先的方式分解复合任务,直到所有任务都被分解为原子任务。
- 状态管理:
- 使用世界状态的副本进行条件检查,避免影响真实世界状态。
- 最终生成的原子任务按执行顺序存储在栈(
FinalTasks
)中。
执行器(HTN PlanRunner)
执行器是分层任务网络(HTN)的执行组件,负责按顺序执行规划器生成的原子任务,并管理任务的状态切换和效果应用。
核心特点:
- 任务执行:
- 按顺序从规划器的
FinalTasks
中取出原子任务并执行。 - 通过
Operator
方法获取任务的当前状态(Running
、Success
或Failure
)。
- 状态管理:
- 根据任务状态决定是否应用任务效果或重新规划。
- 支持任务失败时的自动重新规划。
- 效果应用:
- 在任务成功完成后,调用
Effect
方法应用任务效果。
构造器(HTN PlanBuilder)
构造器是分层任务网络的构建组件,负责创建和管理任务的层次结构,并将任务组织成一个可执行的计划。它通过栈结构来管理任务的嵌套关系,并提供了创建复合任务、方法任务以及原子任务的接口。
核心特点:
- 任务构建:
- 通过辅助栈构建任务的层次关系,栈顶元素表示正在构建的复合任务或方法。
- 通过复合任务和方法中重写的
AddNextTask
处理任务的嵌套关系,确保子任务被正确添加到父任务中。
- 任务层次管理:
- 如果当前任务栈不为空,新任务会被添加为栈顶任务的子任务;反之,新任务会被视为根任务,并初始化计划器和执行器。
- 使用
Back()
方法可以返回到上一级任务层次,继续构建其他任务。
- 任务类型区分:
- 复合任务和方法可以包含子任务,会被压入辅助栈;原子任务(
PrimitiveTask
)不会包含子任务,因此不需要压入栈中。
- 计划生成与执行:
- 通过
End()
结束任务构建,清空任务栈并返回计划器,用户可以进一步使用该计划器来生成可执行的计划。 - 使用
RunPlan()
可以执行生成的 HTN 计划,执行器会根据任务的层次结构和条件来逐步执行任务。
应用实践
通过将猫的行为拆分为原子任务,再将这些原子任务组合成方法和复合任务,可以构建一个复杂、逼真的猫猫行为系统,用于模拟猫猫的日常活动。
状态和任务设计
1. 世界状态
世界状态是猫猫当前的状态,可以用以下变量表示:
_energy
:精力值,整型,范围 0-10
_full
:饱腹值,整型,范围 0-10
_mood
:心情值,整型,范围 0-10
_masterBeside
:主人是否在旁边,布尔值,true/false
2. 原子任务
原子任务是猫猫可以执行的最小任务单元,每个任务都有条件和效果:
任务名称 | 条件 | 效果 |
吃饭 | _full <= 8 | _full += 2 , _mood += 1 |
喝水 | 无条件 | _full += 1 , _mood += 1 |
睡觉 | _energy <= 2 | _energy += 4 , _mood -= 1 , _masterBeside = false |
拉屎 | _full >= 6 | _full -= 2 , _mood -= 2 _masterBeside = false |
拆家 | _energy >= 4 && mood <= 4 && _masterBeside == false | _masterBeside = true _energy -= 1 |
跑酷 | _energy >= 5 && _mood <= 7 | _energy -= 2 , _full -= 3 , _mood += 2 |
追蟑螂 | _energy >= 5 | _energy -= 3 , _full -= 2 , _mood += 1 |
吃蟑螂 | _full <= 7 | _full += 1 , _mood -= 3 |
叫唤 | _mood >= 7 && _full >= 5 | _mood -= 1 , _full -= 1 _masterBeside = true |
蹭主人 | _masterBeside == true | _mood -= 2 |
发呆 | 无条件 | _mood += 1 |
舔毛 | _mood <= 5 | _mood += 1 , _energy -= 1 |
3. 方法任务
方法任务由【原子任务】和【复合任务】组成:
任务名称 | 方法条件 | 子任务组成 | 描述 |
进食 | ㅤ | 原子【吃饭】 + 原子 【喝水】 | 猫猫通过吃饭和喝水来恢复饱腹值和心情 |
捕猎 | ㅤ | 原子【追蟑螂】 + 原子【吃蟑螂】 | 猫猫通过追蟑螂和吃蟑螂来获取食物 |
排泄 | ㅤ | 原子【拉屎】 | 猫猫通过拉屎来减少饱腹值 |
跑酷 | ㅤ | 原子【跑酷】 | 猫猫通过跑酷来消耗精力并提升心情 |
运动 | ㅤ | 复合【玩耍】+ 原子【拆家】 | 猫猫通过拆家来吸引主人注意 |
叫唤 | ㅤ | 原子【叫唤】 | 猫猫可能只是随便叫叫 |
撒娇 | _masterBeside == true | 原子【蹭主人】 + 原子【叫唤】 | 猫猫通过蹭主人和叫唤来撒娇 |
休息 | ㅤ | 原子【睡觉】 + 原子【发呆】 | 猫猫通过睡觉和发呆来恢复精力 |
清洁 | ㅤ | 原子【舔毛】 | 猫猫通过舔毛来提升心情 |
维持生命 | ㅤ | 复合【维持生命】 | 猫猫需要维持声明 |
4. 复合任务
复合任务是最高层次的任务,只能由【方法任务】组成:
任务名称 | 子任务组成 | 描述 |
生活(终极任务) | 【维持生命】 + 【运动】 + 【叫唤】+【撒娇】 + 【休息】 + 【清洁】 | 猫猫的日常生活,包含所有可能的行为 |
玩耍 | 【跑酷】 + 【捕猎】 | 猫猫通过跑酷和追蟑螂来消耗精力并提升心情 |
维持生命 | 【进食】 + 【拉屎】 | 猫猫通过进食和拉屎来维持生命体征 |
HTN网络结构构建
1.HTN网络构建
在CatHTN类的
Start()
中构建HTN网络,可直接写成代码(缩进是为了更方便查看HTN的网络结构),也可通过读取配置文件构建:
2.HTN计划执行
在每一帧中调用
htnBuilder.RunPlan()
,执行当前生成的 HTN 计划。除了在
Update()
中循环执行计划,还可利用多线程实现,如ThreadPool
或Task
。任务执行表现
1.定时器和任务前摇
为了控制任务的执行时间,在
PrimitiveTask
基类的Operator()
中使用定时器;为了让猫猫的表现更加真实生动(定点进食、定点如厕、定点睡觉等),增加让猫猫执行任务前让其移动到指定位置的“前摇”,在此期间函数返回Running
状态。- 如果
_startTime < 0
,返回Running
状态,表示任务尚未开始;如果_isMoving == false
,则开始移动,并设置相关状态和时间。
- 如果任务已经完成(当前时间减去开始时间大于等于持续时间),则执行任务结束操作,返回
Success
状态,并重置计时器。
- 如果任务正在执行时间范围内,则返回
Running
状态。
PrimitiveTask → Operator
:根据_startTime
和_isMoving
判断任务状态。CatHTN.cs → MoveToNextPosition
:使用Dotween.OnComplete
实现动画结束回调。2.任务开始和结束表现
移动到指定位置的过程是原子任务通用的,可以写在
PrimitiveTask
基类中;而不同原子任务又有不同的表现,比如游戏物体材质、UI、特效、音效表现;因此提供TaskStartOperation()
和TaskEndOperation()
供子类覆写。PrimitiveTask.cs
中提供虚方法:如原子任务
P_ChaseCock
,需要在让蟑螂游戏物体开始前显示、结束后隐藏;需覆写基类方法实现。优化空间
1.配置文件驱动的 HTN 网络生成
目前的世界状态、任务条件和HTN网络结构都是写死在代码里的;后续可通过JSON或YAML配置文件动态生成HTN网络(包括世界状态定义、原子任务类、HTN网络构建的代码),支持用户定义世界状态、任务和条件,自动生成原子任务具体类和网络结构,提升灵活性和可维护性。
例如,通过以下json对象:
生成
P_Bat.cs
原子任务类脚本:部分功能已实现:
2.动态世界状态管理
引入动态世界状态机制(昼夜交替、饱腹度随时间流逝等),支持游戏进程自动更新状态,且允许用户通过配置文件或编辑器动态添加、修改、删除变量,增强HTN网络的适应性。
3.方法优先级与权重
目前方法的选择仅有顺序选择和随机选择两种模式,可为方法添加优先级和权重参数,使HTN网络行为更符合预期。
- Author:Yuki
- URL:http://shirakoko.xyz/article/htn
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts