type
status
date
slug
summary
tags
category
icon
password
fullWidth
fullWidth
🐈
强烈推荐!若想了解游戏AI,可观看Games104王希老师的视频: 【17.游戏引擎Gameplay玩法系统:高级AI (Part 1) | GAMES104-现代游戏引擎:从入门到实践】

分层任务网络简单应用-AI猫咪模拟器


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

HTN的构成

任务

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

世界状态

世界状态(World State) 是描述当前环境的一组属性的集合;用于评估任务的可行性并选择合适的方法,例如:
  • 哪里有树?
  • 哪里有电锯?
  • 电锯的油量是多少?
在HTN的规划阶段,智能体会复制一份世界状态的副本,用于任务的选择和分解。规划阶段不会改变真实的世界状态,只有在执行原子任务时,世界状态才会被真正改变
🐈
为什么规划阶段要复制一份世界状态?规划完成后,只有被选中的原子任务会执行并修改真实的世界状态,如果直接操作真实的世界状态,可能会导致状态被错误修改。

HTN的运行逻辑

规划阶段(Planning Phase)

在规划阶段,HTN根据当前的世界状态,选择要执行的任务,并将其分解为一系列原子任务。具体步骤如下:
  1. 复制世界状态
      • 智能体复制一份当前的世界状态,用于任务的选择和分解。这个副本不会影响实际的世界状态。
  1. 用栈遍历任务
      • HTN从最顶层的根任务(Root)开始遍历,它是一个复合任务。
      • 依次取出栈顶的任务,根据其类型做不同处理:
    1. 处理复合任务
        • 如果当前任务是复合任务,HTN会从该复合任务的所有方法中选择一个合适的方法
        • HTN会递归地处理该方法中的子任务(subtasks)。
    2. 处理方法
        • 如果当前任务是方法,HTN会依次遍历该方法中的所有子任务(subtasks)。
        • 对于每个子任务,HTN会递归地处理,直到所有子任务都被分解为原子任务。
    3. 处理原子任务
        • 如果当前任务是原子任务,HTN会将其加入任务栈中,等待执行。
        • 原子任务是HTN中最底层的任务,不可再分解。
  1. 得到最终结果
      • 最终HTN会生成一个由原子任务组成的任务栈,这些原子任务将按照顺序执行。

执行阶段(Execution Phase)

在执行阶段,HTN会依次执行规划阶段分解出的原子任务。具体步骤如下:
  1. 执行原子任务
      • 智能体按照规划阶段的顺序执行原子任务。
      • 每个原子任务的执行会对世界状态产生影响,如减少弹药数量、减少树木数量等。
  1. 任务完成或重新规划
      • 在执行每个原子任务之前,检查该任务的执行条件是否满足。如果条件不满足,中止当前的任务链,并重新进入规划阶段
      • 如果所有原子任务都成功执行完毕,认为当前的任务列表已完成。

循环过程

HTN的运行逻辑是一个循环过程,循环具体流程如下:
  1. 规划阶段:根据世界状态把根任务(Root)分解为原子任务。
  1. 执行阶段:依次执行原子任务,并更新世界状态。
  1. 重新规划:如果任务链被中断或完成,HTN会重新进入规划阶段,选择新的任务。

代码实现

世界状态(World State)

世界状态通常由多个对象属性组成;例如,角色的血量、位置、装备等。这些状态可能会频繁变化,并且需要被多个系统(如AI、UI、物理引擎等)读取和修改。
在实现世界状态时,主要面临两个问题:
  1. 状态数据的多样性
    1. 世界状态的数据类型多种多样(如整数、布尔值、对象等),如何统一存储这些不同类型的数据?
  1. 状态数据的实时更新
    1. 状态数据会不断变化,如何确保存储的数据能够同步更新?例如,角色的血量变为0时,存储的数据也应实时反映这一变化。

解决方案
问题 1:统一存储多种类型的数据
可以使用 <string, object> 字典来存储状态数据。在C#中object 是所有类型的基类,因此可以容纳任何数据类型。
问题 2:实时同步状态数据
如果直接使用字典存储状态值,会出现以下问题:
  • 修改实际对象的状态(如角色血量)不会更新字典中的值。
  • 修改字典中的值也不会影响实际对象的状态。
为了解决这个问题,引入gettersetter机制:
  • getter:一个Func<object>委托,用于动态获取状态的值。
  • setter:一个Action<object>委托,用于动态修改状态的值。
通过这两个委托,将状态的读取和写入操作与实际对象的属性绑定,从而实现状态的实时同步
🐈‍⬛
如果直接使用一个简单的字典(<string, object>)来存储这些状态,会面临以下问题:
  • 状态无法同步更新:字典中存储的是某个时刻的状态值,而不是动态的引用。例如,角色的血量变化时,字典中的值不会自动更新。
  • 无法触发副作用:直接修改字典中的值不会触发与状态相关的逻辑(如血量变化时触发死亡事件)。

以下是世界状态类的代码实现:

任务接口(Task)

复合任务、方法和原子任务它们有共通之处1.需要一个方法判断是否满足任务执行条件;2.添加子任务(原子任务不需要);把这些共通之处以接口的形式提炼出来:

原子任务(Primitive Task)

原子任务是行为树中的最小执行单元,通常表示一个简单的动作,例如「开火」或「奔跑」;其他原子任务可以通过继承原子任务抽象类来实现具体的任务逻辑。
核心特点:
  1. 不可分解:原子任务是最小任务单元,不能再分解为子任务。
  1. 双模式处理
      • 规划时:使用世界状态的副本进行条件检查和效果模拟。
      • 运行时:直接操作真实的世界状态。
  1. 条件与效果分离
      • 条件检查(MetCondition)用于判断任务是否可以执行。
      • 效果应用(Effect)用于在任务执行后修改世界状态。

方法(Method)

方法是行为树中的一个重要组成部分,用于定义任务的执行逻辑。它由多个子任务组成,这些子任务可以是复合任务原子任务。方法的执行需要满足两个条件:
  1. 方法自身的前提条件:通过构造函数传入的条件函数。
  1. 所有子任务的条件:每个子任务的条件都必须满足。
核心特点:
  1. 子任务组合:方法可以包含多个子任务,形成一个任务序列。
  1. 条件检查
      • 方法自身的前提条件必须满足。
      • 所有子任务的条件也必须满足。
  1. 状态隔离
      • 使用临时世界状态副本(tpWorld)来追踪子任务的效果。
      • 只有所有子任务条件都满足时,才会将临时状态应用到真实世界状态。

复合任务(Compound Task)

复合任务是行为树中的一种任务类型,用于组合多个方法;复合任务只能添加方法作为子任务,不能直接添加原子任务或其他复合任务。
核心特点:
  1. 子任务限制:只能包含方法作为子任务。
  1. 条件检查
      • 复合任务的条件满足只需其中一个方法满足条件即可。
      • 一旦找到满足条件的方法,会将其记录为ValidMethod,供后续执行使用。
      • 可支持不同选择策略(比如顺序选择和随机选择);可以根据需求使用适合的方式。

规划器(HTN Planner)

规划器是分层任务网络(HTN)的核心组件,负责将复合任务逐步分解为可执行的原子任务。其核心思想是通过递归分解任务,最终生成一个由原子任务组成的执行计划。
核心特点:
  1. 根任务:每个HTN必须有一个复合任务作为根任务,类似于行为树的根节点,规划过程由此开始。
  1. 任务分解
      • 使用栈(taskOfProcess)缓存待分解的任务。
      • 通过深度优先的方式分解复合任务,直到所有任务都被分解为原子任务。
  1. 状态管理
      • 使用世界状态的副本进行条件检查,避免影响真实世界状态。
      • 最终生成的原子任务按执行顺序存储在栈(FinalTasks)中。

执行器(HTN PlanRunner)

执行器是分层任务网络(HTN)的执行组件,负责按顺序执行规划器生成的原子任务,并管理任务的状态切换和效果应用。
核心特点:
  1. 任务执行
      • 按顺序从规划器的FinalTasks中取出原子任务并执行。
      • 通过Operator方法获取任务的当前状态(RunningSuccessFailure)。
  1. 状态管理
      • 根据任务状态决定是否应用任务效果或重新规划。
      • 支持任务失败时的自动重新规划。
  1. 效果应用
      • 在任务成功完成后,调用Effect方法应用任务效果。

构造器(HTN PlanBuilder)

构造器是分层任务网络的构建组件,负责创建和管理任务的层次结构,并将任务组织成一个可执行的计划。它通过栈结构来管理任务的嵌套关系,并提供了创建复合任务、方法任务以及原子任务的接口。
核心特点:
  1. 任务构建
      • 通过辅助栈构建任务的层次关系,栈顶元素表示正在构建的复合任务方法
      • 通过复合任务和方法中重写的AddNextTask处理任务的嵌套关系,确保子任务被正确添加到父任务中。
  1. 任务层次管理
      • 如果当前任务栈不为空,新任务会被添加为栈顶任务的子任务;反之,新任务会被视为根任务,并初始化计划器和执行器。
      • 使用Back()方法可以返回到上一级任务层次,继续构建其他任务。
  1. 任务类型区分
      • 复合任务和方法可以包含子任务,会被压入辅助栈;原子任务(PrimitiveTask)不会包含子任务,因此不需要压入栈中。
  1. 计划生成与执行
      • 通过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的网络结构),也可通过读取配置文件构建:
构建的HTN网络结构
构建的HTN网络结构
2.HTN计划执行
在每一帧中调用htnBuilder.RunPlan(),执行当前生成的 HTN 计划。
除了在Update()中循环执行计划,还可利用多线程实现,如ThreadPoolTask

任务执行表现

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网络行为更符合预期。
 
CrossChess-井字棋小游戏联网模块游戏AI行为决策-目标导向行为规划(GOAP)通用框架
Loading...
Latest posts
游戏算法-Floyd搜索算法知识梳理和通用框架
2025-4-2
游戏算法-A*搜索算法知识梳理和通用框架
2025-4-2
游戏AI行为决策-目标导向行为规划(GOAP)通用框架
2025-3-23
自制Python任务调度模块-MySchedule
2025-3-20
学习笔记:23种设计模式
2025-3-19
学习笔记:计算机网络(自顶向下方法)课程笔记
2025-3-15