【Unity】階層型タスクネットワークを実装してみた【ゲームAI】

# 概要
　Unityによって階層型タスクネットワークを実装し、以下のデモのようにプランニングと実行をできるようにします。
![anim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189786/9a9d88dc-9a17-5864-668c-58c8531cf247.gif)
　Unityのバージョンは2020.2.0f1です。
　プロジェクトはGitHubに上げておきます。
　https://github.com/saragai/SimpleHTN
（イラストの再配布を防ぐために画像がMissingになっています）

# 階層型タスクネットワークとは
　英語では**Hierarchical Task Network**と呼ばれます。以下**HTN**と呼びます。
　**HTN**はその名の通り、**タスク**を**階層的**に組み合わせて作られるネットワークのことです。
　**HTN**はゲームAIの**プランニング**に用いられます。

### プランニングとは
　プランニングとは、**何らかの目標に対して、どの行動をどの順序で行うべきかをあらかじめ考えること**です。

　例えば、現在家にいて、「**新宿駅へ行く**」という目標を立てたとします。
　この目標は、最寄り駅が池袋駅だとすると

- 「**家を出る** → **池袋駅まで歩く** → **電車に乗る**」

のような方法で実現できるとします。
　しかし、電車で行くとするとお金が必要ですが、お金が手元にないとしたらどうでしょうか。その場合は,


- 「**家を出る** → **銀行まで歩く** -> **お金を下ろす** -> **池袋駅まで歩く** → **電車に乗る**」

とする必要があります。また、足を骨折していた場合は

- 「**タクシーを呼ぶ** -> **家を出る** -> **池袋駅までタクシーで行く** → **電車に乗る**」

のような方法をとる必要があり、この場合はさらにお金が多く必要になります。

　ゲームAIとしてよく用いられる**FSM**や**BehaviorTree**などは、行動を決定する際に現在の状況しか見ることができません。
　しかし、上の例のようにどのようなルートを通るかで、必要なお金は変わります。その場合、「**お金が足りるか**」という条件を確かめるだけでも今から行うであろう行動を想定して手動で設定しなければならず、条件分岐が大量に必要になります。

　しかし、目標達成までに行うべき行動をあらかじめ列挙**(=プランニング)**できれば、「**お金が足りるか**」はおのずとわかるはずです。
　ここで、プランニングを行う際には、**その行動に必要な条件**と**行動の影響**がわかっている必要があります。例えば、電車に乗るのであれば、必要な条件は「運賃260円を持っている」であり、行動の影響は「所持金が260円減る・自分のいる場所が池袋から新宿にかわる」です。
　これらの条件を行動自体と合わせてまとめたものを、**タスク**と呼びます。

### タスクとは
　改めてまとめます。HTNにおいて、**タスク**は以下の情報を持ちます。

- **必要な条件**
- **実際にする行動**
- **行動した際の影響**

　**必要な条件**と**行動した際の影響**がまさにプランニングに必要な要素です。この情報のおかげで実際に行動しなくても仮想的に行動の結果がわかってシミュレーションが可能になります。
　この二つは、どちらも**WorldState**と呼ばれる世界の状態を参照します。
　プランニングの際は、現在の**WorldState**のコピーを作成し、仮想的にタスクを実行して**WorldState**のコピーを更新していくことでシミュレーションを行います。

### 階層的とは
　タスクには様々な粒度があります。
　先ほどの例のように**「新宿駅へ行く」**はタスクですが、これは**「家を出る」****「池袋駅まで行く」****「電車に乗る」**などのタスクに分解できます。さらに**「家を出る」**は**「靴を履く」****「ドアを開ける」****「鍵を閉める」**へと分解できるでしょう。
　逆に言うと、**「靴を履く」**というような細かい**具体的なタスク**をまとめることによって**「新宿駅へ行く」**という**抽象的なタスク**にして扱いやすくできるのです。

　また、**抽象的なタスク**には複数の表現方法があることも述べました。

- 「**タクシーを呼ぶ** -> **家を出る** -> **池袋駅までタクシーで行く** → **電車に乗る**」
- 「**家を出る** → **銀行まで歩く** -> **お金を下ろす** -> **池袋駅まで歩く** → **電車に乗る**」

　この二つの方法の内容に差異はありますが、どちらの分解方法を選んだとしても最終的に目的は達成できるため、同一のタスクとして扱われるべきです。

　このような前提から上で述べた**タスク**をまとめた**複合タスク**を考えることができます。
　**複合タスク**は、**「目的を達成するための一連のタスク」**と**「一連のタスクを選ぶための条件」**のセットを**複数**持つものとします。
　この『**「目的を達成するための一連のタスク」**と**「一連のタスクを選ぶための条件」**のセット』は**Method**と呼ばれます。

　この**複合タスク**も、以下のように確かに**タスク**の要件を満たすため、**タスク**と同様に扱うことが可能になっています。

- **必要な条件**：Methodを選ぶための条件と、選ばれたMethodが持つ一連のタスクに必要な条件
- **実際にする行動**：選ばれたMethodの持つ一連のタスクの実際にする行動
- **行動した際の影響**：選ばれたMethodの持つ一連のタスクを実行したときの影響

　この**複合タスク**を導入することによって、**タスク**を**階層的**に扱うことができるようになります。

# 用語
　ここまでで**HTN**に必要な一通りの要素を解説しました。実装に移る前に用語を整理しておきます。

- **Primitive Task**: **最も基本的なタスク**のこと
  - **Condition**: **必要な条件**のこと
  - **Operator**: **実際にする行動**のこと
  - **Effect**: **行動したときの影響**のこと


- **Compound Task**: **複合タスク**のこと
  - **Method**: Conditionと一連のタスクのセット
     - **Sub Task**: 一連のタスク
- **World State**: 世界の状態。タスクの条件や影響で参照される
- **Planner**: タスクを一つ設定し、プランニングして実行する行動の順列を割り出すひと
  - **Root Task**: プランニングしたいタスク

- **Plan Runner**: プランを実行するひと


# 実装
### 一般的な実装
　HTNを解説している記事はたくさんありますが、どれも[Game AI Pro 12] (http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf)を参考にしているようです。
　以下に疑似コードを引用します。

``` C
WorkingWS = CurrentWorldState
TasksToProcess.Push(RootTask)
while TasksToProcess.NotEmpty
{
    CurrentTask = TasksToProcess.Pop()
    if CurrentTask.Type == CompoundTask
    {
        SatisfiedMethod = CurrentTask.FindSatisfiedMethod(WorkingWS) 
        if SatisfiedMethod != null
        {
            RecordDecompositionOfTask(CurrentTask, FinalPlan, DecompHistory) 
            TasksToProcess.InsertTop(SatisfiedMethod.SubTasks) 
        }
        else
        {
            RestoreToLastDecomposedTask() 
        }
    }
    else//Primitive Task
    {
        if PrimitiveConditionMet(CurrentTask)
        {
            WorkingWS.ApplyEffects(CurrentTask.Effects)
            FinalPlan.PushBack(CurrentTask)
        }
        else
        {
            RestoreToLastDecomposedTask()
        }
    }
}
```
　これについて軽く解説をします。
　はじめに処理スタックに`RootTask`を積み、以下の処理を処理スタックが空になるまで繰り返します。

- 処理スタックに乗っているタスクを取り出して、`CompoundTask`か`PrimitiveTask`かで場合分ける
  - `CompoundTask`であれば、実行できる`Method`があるか確認して、あれば`SubTasks`を再び処理スタックに積む
  - `PrimitiveTask`であれば、実行できるか確認して、実行できるならプランにそのタスクを加える
- どちらの場合でも、失敗したら最後に`CompoundTask`を展開したときまで状態を差し戻す


**……なんかちょっと気になりませんか？**
　気になるポイントは二点あります

- **CompoundTask**か**PrimitiveTask**かで場合分けしているのが気持ち悪い
- 最後に展開したときまで状態を差し戻すというのが分かりづらく、今何をしているのか混乱する


　個人的な理解としては**CompoundTask**は**PrimitiveTask**の拡張なので、同じインターフェースによって統一的に扱えるのではないかと思いました。また、スタックに新たなタスクがどんどん積まれていく中で、「最後に**CompoundTask**を展開した時まで戻す」がどういう挙動になるのかちょっと想像しにくい気がしました。

　もちろんそのまま組んで悪いことは全然ないのですが、そのままの実装については他の記事でもたくさん実装例があるのでちょっと変えてみようと思います。
　気になるポイントへの対処としてはそれぞれ、

- **Task**をインターフェースで扱って、**CompoundTask**と**PrimitiveTask**の処理の違いはそれぞれのTask側に実装する
- スタックではなく再帰によって階層を実現する

です。

### インターフェース & 再帰による実装
　**CompoundTask**と**PrimitiveTask**はともに`ITask`インターフェースを実装します。

```C#
public interface ITask
{
    bool TryPlanTask(WorldState state, ref Plan plan);
}
```
　唯一のメソッド`TryPlanTask()`は、`WorldState`を受け取ってタスクを実行できるか確認し、実行できるなら`Plan`に追加していきます。このメソッドの中で`WorldState`も更新していくことにします。

　また、本筋ではないですが、Unityを使った開発ではTaskの設定のために`ScriptableOject`を使うのではないかと思うので、`ScriptableOject`を継承した抽象クラスも間に挟みます。

```C#
public abstract class Task : ScriptableObject, ITask
{
    public abstract bool TryPlanTask(WorldState state, ref Plan plan);
}
```

それぞれ、以下のように実装します。

#### Primitive Taskの実装
　これは何のひねりもなく、ただ**Condition**を満たしていたら**WorldState**を**Effect**で更新し、プランに**Operator**を加えているだけです。

``` C#
[CreateAssetMenu(fileName ="primitive_task.asset", menuName = "Primitive Task", order = 0)]
public class PrimitiveTask : Task
{
    [SerializeField] List<Condition> m_Preconditions;
    [SerializeField] Operator.Operator m_Operator;
    [SerializeField] List<Effect> m_Effects;

    public override bool TryPlanTask(WorldState state, ref Plan plan)
    {
        foreach (var condition in m_Preconditions)
        {
            if (!condition.Match(state))
                return false;
        }

        foreach(var effect in m_Effects)
            effect.ApplyTo(state);

        plan.Enqueue(m_Operator);

        return true;
    }
}

```
#### Compound Taskの実装
　ここで再帰が出てきます。
　階層は**CompoundTask**が**Method**を介してサブタスクの`TryPlanTask()`を呼んで再帰することで実現しています。
　再帰で実装すると、「現在の状況を保存する場所」と「その状況に戻す場所」が近く、二つの対応が一目でわかるようになります。

```C#
[CreateAssetMenu(fileName ="compound_task.asset", menuName = "Compound Task", order = 1)]
public class CompoundTask : Task
{
    [SerializeField] List<Method> m_Methods;

    public override bool TryPlanTask(WorldState state, ref Plan plan)
    {
        foreach (var method in m_Methods)
        {
            // 実行するタスクを見つける
            if (!method.Match(state))
            {
                continue;
            }

            // 現在のplanを保存
            var currentPlan = new Plan(plan);

            // サブタスクを行う
            if(!method.TryPlanSubTasks(state, ref plan))
            {
                // 失敗したときplanを元に戻す
                plan = currentPlan;
                continue;
            }

            // サブタスクが全て完了したので成功
            return true;
        }

        // 条件を満たすメソッドがなかったので失敗
        return false;
    }
}
```

#### Methodの実装
```C#
[System.Serializable]
public class Method
{
    [SerializeField] List<Condition> m_Conditions;
    [SerializeField] List<Task> m_SubTasks;

    public bool Match(WorldState state)
    {
        foreach (var condition in m_Conditions)
        {
            if (!condition.Match(state))
                return false;
        }

        return true;
    }

    public bool TryPlanSubTasks(WorldState state, ref Plan plan)
    {
        foreach(var task in m_SubTasks)
        {
            if(!task.TryPlanTask(state, ref plan))
            {
                return false;
            }
        }

        return true;
    }
}
```

　このようにすることで**Planner**のコードはとてもシンプルになります。

#### Plannerの実装
```C#
public class HTNPlanner : MonoBehaviour
{
    [SerializeField]
    WorldStateHolder m_WorldStateHolder;

    Plan m_Plan;

    public void Awake()
    {
        m_Plan = new Plan();
    }

    public Plan DoPlan(ITask rootTask)
    {
        m_Plan.Clear();

        // WorldStateのコピーを取って自由に書き換えられるようにする
        var tmpState = m_WorldStateHolder.WorldState.CreateCopy();
        rootTask.TryPlanTask(tmpState, ref m_Plan);

        return m_Plan;
    }
}
```

　ここで`WorldState`ではなく`WorldStateHolder`を介しているのは、`WorldState`を`MonoBehaviour`にしたくないからです。`WorldState`を`MonoBehavior`にしてしまうとコピーを気軽にInstantiateできなくなってしまいます。

#### Planの実装
**Operator**を表すインターフェース**IOperator**を要素とする**Queue**の簡素なラッパークラスになっています。

```C#
public class Plan
{
    Queue<IOperator> m_Operators;

    public IEnumerable<IOperator> Operators => m_Operators;

    public Plan()
    {
        m_Operators = new Queue<IOperator>();
    }

    public Plan(Plan other)
    {
        m_Operators = new Queue<IOperator>(other.m_Operators);
    }

    public void Clear()
    {
        m_Operators.Clear();
    }

    public void Enqueue(IOperator @operator)
    {
        m_Operators.Enqueue(@operator);
    }
}
```

# デモの実装

ここからはデモ用の実装をしながら、ここまで解説なしで出していたクラスについても説明します。
そのため、少し具体的な実装になってしまっています。本来はまだもっと抽象化すべきだと思うのですが、デモを兼ねているということで少し手を抜きました。

### デモの状況
状態の種類とタスクについてはこのスライドを参考にしています
https://www.slideshare.net/dena_genom/gdm37aileft-alive

**WorldState**の種類は**enum**で指定、値は**bool**のみとします。
ここでは状態は

- 空腹か
- 金を持っているか
- 食べ物を持っているか

のみとします。

**Primitive Task**は

- 働く（金を得る）
- 食べ物を買う（金を使って食べ物を得る）
- 食べ物を食べる（食べ物を使って空腹を満たす）

**Compound Task**は

- 食事をする

として、どのような状況でも食べ物を食べるまで行きつくように**Method**を設定します。

**Operator**で指定される実際の行動は、移動のみとします。

#### WorldStateの実装
```C#
[System.Serializable]
public class WorldState
{
    /// 状態の種類
    public enum TYPE
    {
        IsHungry,
        HaveMoney,
        HaveMeal,

        MaxCount,
    }
    public static int NumStates => (int)TYPE.MaxCount;

    [SerializeField]
    private bool[] m_States;

    /// コンストラクタ
    public WorldState()
    {
        m_States = new bool[(int)TYPE.MaxCount];
    }

    /// 種類と値を指定して状態を設定
    public void Set(TYPE type, bool value)
    {
        m_States[(int)type] = value;
    }

    /// 種類を指定して状態の値を取得
    public bool Get(TYPE type)
    {
        return m_States[(int)type];
    }

    /// 自身を複製する（プランニングの際に必要）
    public WorldState CreateCopy()
    {
        var copy = new WorldState();
        for (int i = 0; i < NumStates; i++)
        {
            copy.m_States[i] = m_States[i];
        }
        return copy;
    }
}
```

**Condition**は、**WorldState**の種類を指定して、その真偽を問い合わせます。
**Effect**は、**WorldState**の種類を指定して、その真偽を設定します。


#### Conditionの実装
```C#
[System.Serializable]
public class Condition
{
    [SerializeField] WorldState.TYPE m_Type;
    [SerializeField] bool m_Value;

    /// 状態を指定して条件を満たすか判断する
    public bool Match(WorldState state)
    {
        return state.Get(m_Type) == m_Value;
    }
}
```

#### Effectの実装
```C#
[System.Serializable]
public class Effect
{
    [SerializeField] WorldState.TYPE m_Type;
    [SerializeField] bool m_Value;

    /// 状態を指定して、新たな状態を設定する
    public void ApplyTo(WorldState state)
    {
        state.Set(m_Type, m_Value);
    }
}
```

#### Operatorの実装

　**Operator**は、用途を考えると多種多様なものが必要になりそうなものなので、少し抽象化します。

　**Operator**を**Plan Runner**が実行するとき、個々の**Operator**が何をするかを考えたくはありません。しかし、**Operator**には実行対象を渡さないといけないため、実行時は**IOperatable**というインターフェースを引数で渡すことにします。

　また、**Operator**は**Planner**においてはプランニングのキューに入るだけですが、**Plan Runner**が実行するときに実際に実行するときはある程度時間がかかるものが多いと思います。そのため、コルーチンで実装します。

```C#

// 操作対象
public interface IOperatable
{
    Transform Transform { get; }
}

// 操作
public interface IOperator
{
    string OperatorName { get; }
    IEnumerator Execute(IOperatable operatable);
}

public abstract class Operator : ScriptableObject, IOperator
{
    [SerializeField] string m_OperatorName;
    public string OperatorName => m_OperatorName;

    /// 操作を実行
    public abstract IEnumerator Execute(IOperatable operatable);
}

```

##### Move To Operator (Operatorの具象クラス）
　行きたい場所を指定してそこまで直進する**Operator**とします。
　場所はとりあえず**PlaceManager**というシングルトンを作ってそこから取っていますが、細かいところなので適当です。

```C#
[CreateAssetMenu(fileName ="move_to_operator.asset", menuName = "Operator/Move To", order = 0)]
public class MoveToOperator: Operator
{
    [SerializeField] PlaceManager.PLACE m_Place;
    [SerializeField] float m_Velocity = 1f;

    public override IEnumerator Execute(IOperatable operatable)
    {
        var targetPos = PlaceManager.GetPosition(m_Place);

        while (true)
        {
            var transform = operatable.Transform;
            var dir = targetPos - (Vector2)transform.position;

            if(dir.magnitude < 0.1f)
            {
                yield break;
            }

            dir.Normalize();

            // 動かす
            transform.position += (Vector3)( m_Velocity * dir * Time.deltaTime);
            yield return null;
        }
    }
}
```

#### Plan Runnerの実装
**Planner**に**Root Task**を渡して、受け取った**IOperator**のキューに入っている**Execute**を順番に呼んでいきます。


```C#
public class HTNPlanRunner : MonoBehaviour
{
    [SerializeField] HTNPlanner m_Planner;
    [SerializeField] Operatable m_Operatable;

    [SerializeField] Task m_Task;

    [SerializeField] float m_ReplanningInterval = 1f;

    List<IEnumerator> m_RunningCoroutines;

    // Start is called before the first frame update
    void Start()
    {
        m_RunningCoroutines = new List<IEnumerator>();
        StartCoroutine(SetTaskRoutine(m_Task));
    }

    IEnumerator SetTaskRoutine(ITask task)
    {
        while (true)
        {
            // 一定時間ごとにリプランニング
            SetTask(task);
            yield return new WaitForSeconds(m_ReplanningInterval);
        }
    }

    public void SetTask(ITask task)
    {
        StopCoroutine();

        var plan = m_Planner.DoPlan(task);
        StartPlan(plan);
    }

    /// プランの実行を開始
    void StartPlan(Plan plan)
    {
        var coroutine = Execute(plan.Operators);
        m_RunningCoroutines.Add(coroutine);

        StartCoroutine(coroutine);
    }

    /// 進行中のコルーチンを止める
    void StopCoroutine()
    {
        foreach (var coroutine in m_RunningCoroutines)
        {
            StopCoroutine(coroutine);
        }
        m_RunningCoroutines.Clear();
    }

    IEnumerator Execute(IEnumerable<IOperator> operators)
    {
        foreach (var @operator in operators)
        {
            var action = Execute(@operator);
            m_RunningCoroutines.Add(action);
            yield return StartCoroutine(action);
        }
    }

    IEnumerator Execute(IOperator @operator)
    {
        var action = @operator.Execute(m_Operatable);
        m_RunningCoroutines.Add(action);
        yield return StartCoroutine(action);
    }
}
```

# デモの設定
　上で**Primitive Task**は

- 働く（金を得る）
- 食べ物を買う（金を使って食べ物を得る）
- 食べ物を食べる（食べ物を使って空腹を満たす）

の3つと言いました。それぞれ以下のように設定します。
・`働く`
![htn4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189786/1cf658d3-982c-0074-ae40-f21161807e7f.png)

・`食べ物を買う`
![htn1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189786/20ba77cc-9d6e-105e-c9e5-069504a9ff9a.png)

・`食べ物を食べる`
![htn3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189786/01d8c5ca-0115-a060-1f1b-89f645949850.png)

　全て、実際の行動は指定の場所に行くこととしています。

　余談ですが、Unity2020.2からリストがデフォルトでReorderableListになっています。便利です。
　参考：https://zenn.dev/ohbashunsuke/articles/1134c1615efc2b636769

**Compound Task**の`食事をする`は以下のように設定します。
![htn2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189786/5089628f-f7b4-d13b-7d06-06f90e3b8951.png)
これは3つの**Method**からできており、それぞれ

- 食べ物を持っているなら、食べ物を食べる
- 金を持っているなら、食べ物を買ってから再度食事タスクをする
- （条件なしで）働いて再度食事タスクをする

となっており、これが上から順に評価されて実行されます。

# デモの結果
　初めに貼ったgifの挙動になります。
![anim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189786/5a7d0d2b-3de8-6df5-0420-e6bf6cfcddb7.gif)


# 課題
　実は今、無限再帰を防ぐための処理を全くしていません。
　例えば、CompoundTaskにおいて、条件は常に真でSubTaskとして自分自身を指定すると無限ループが起きます。
　無限再帰を防ぐために、「同じ条件で同じタスクを与えられたときはスキップする」というような処理が必要です。

　そのほかにも、デモでは本当に単純なことしかやっていないので実際にゲーム内で使用するともっと課題が出てくると思います。そのような課題は[こちらのスライド](https://www.slideshare.net/dena_genom/gdm37aileft-alive)などが詳しいです。

# おわりに
　HTNの解説は以前から見聞きしていましたし、Qiitaにもいくつか記事があります。

- [階層化型タスクネットワークを勉強して、ゲームAIのコードを書いてみた](https://qiita.com/tyemiya/items/5c56678fe4b8afb2ee4f)
- [【UE4】HTNを実装してみた](https://qiita.com/Dv7Pavilion/items/3998531b66d50475bd5b)  

　ですので完全に後追いなのですが、単純に自分で実装してみたかったことと、自分なりの解釈を書いておくことで誰かの理解の助けになればよいなと思ったので記事を書くことにしました。
　HTNのような綺麗な概念の実装は、ログを出して確認するだけだとすぐでも、実際使うことを考えると、どういうクラス構造にするか、データは誰が持つか、という複雑さがあるので、デモが動くところまで出来て良かったです。

　ここはもっとこうした方がよい、のような意見があればコメントで教えていただけるとありがたいです。
　ご拝読ありがとうございました。

# 参考文献

- [はじめての階層型タスクネットワーク](https://labs.gree.jp/blog/2017/02/16317/)
- [階層化型タスクネットワークを勉強して、ゲームAIのコードを書いてみた](https://qiita.com/tyemiya/items/5c56678fe4b8afb2ee4f)
- [【UE4】HTNを実装してみた](https://qiita.com/Dv7Pavilion/items/3998531b66d50475bd5b)  
- [プレイヤーに反応するだけのAIはもう古い！ゲームAIへのプランニング技術の導入](https://cedil.cesa.or.jp/cedil_sessions/view/1476)
- [【GDM37】ゲームAIにおける意思決定と地形表現～『LEFT ALIVE』を事例に紹介～](https://www.slideshare.net/dena_genom/gdm37aileft-alive)
- [Exploring HTN Planners through Example (GameAiPro, Chapter12)](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf)