Unity Behavior DesignerからADX2の再生制御を行う

# Behavior DesignerとADX2の連携機構を作る

本投稿では、Unity開発環境においてBehavior Tree開発用アセットのBehavior DesignerとサウンドミドルウェアADX2の連携方法を紹介します。

CRI ADX2を初めて利用する場合は、先に次の投稿をご覧ください。

Unity: Unityのサウンド機能をADX2で強化する
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377

## Behavior Designerとは

Behavior Treeという挙動の記述モデルをUnityで作成するためのアセットです。アセットストアで定価80ドルで販売されています。
挙動(Behavior)を木構造で記述するもので、ゲームではキャラクターのAI制御に使われています。

![behavior-designer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/5e2554b8-59a4-c659-5f6f-9b6262d8a9fc.png)

https://opsive.com/support/documentation/behavior-designer/overview/

様々な条件と組み合わせて、AI制御キャラクターがゲーム内の状況に応じてどのような動きをするかGUIで設計してくことができます。

## ADX2と組み合わせるメリット

Behavior DesignerではUnity標準のサウンドを再生するタスクが用意されています。AudioClipを再生・停止したり、音量などのパラメータを変更することもできます。しかし、キャラクターの挙動や状態に合わせて音を再生しようとすると、複雑なタスク設計が必要になってきます。

たとえば「剣を振る音」を再生したいとして、画面内にキャラクターが１０体居たとしましょう。たまたまAIの判定により１０体全員が同時に剣を振った時、同じ音が重なって鳴らないように制御することはかなり難しそうです。

そこで、ADX2の機能が生きてきます。ADX2はサウンドデータを直接再生するのではなく「キュー」と呼ばれる単位で音を扱います。キューには複数のサウンドデータと、さまざまな再生制御情報を入れることができます。上記の例でいえば、キューに対して「ゲーム全体での再生上限数」を設定できるので、ゲームから大量の再生リクエストが来ても、実際に鳴る音は一定数以下に制限できます。

また、キャラクターの足音を鳴らしたい時、左右の足音を交互に鳴らす、といったような異なるサウンドデータを一定のルールで鳴らす仕組みをデータ側に埋め込むことができます。ボリュームやピッチ変更、フィルタの適用などのエフェクトに関しても、ADX2の「AISAC」という操作パラメータの経由で複数のエフェクトをまとめて制御できます。

AISAC機能の例については、次の投稿をご参照ください。

ゲームのUIでボタン音を操作やカーソル位置に応じて変える
https://qiita.com/Takaaki_Ichijo/items/d4804fcc641f046d68a7

## 連携方法

Behavior Designerは、開発者が独自のタスクを開発できるように、拡張用の基底クラスが用意されています。「アクション」タスクを作りたい場合は、BehaviorDesigner.Runtime.Tasks名前空間のActionクラスの派生クラスを作るだけで、Behavior Designerのエディタ内で使えるタスクを作成できます。

WRITING A NEW ACTION TASK
https://opsive.com/support/documentation/behavior-designer/writing-a-new-action-task/

# ADX2制御用アクションの開発

CRI ADX2には様々なサウンド制御機構がありますが、Behaviour Designerは主にゲーム内のキャラクターAI制御に使われますので、キャラクターが音を発するときに使いそうな機能を呼び出すことができれば良さそうです。
今回は「キューの再生」「キューの停止」「AISAC値の設定」の３つのアクションを実装します。

アイコン画像にはAssets/Gizmos/CriWareに入っているものを流用します。

![PlayStopCueBehaviourDesigner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/fe2849de-6382-05d9-b452-2e70f21ab85a.png)

Behavior Desingerのウィンドウでは、右クリックメニューに「ADX2」の項目が追加され、これら３つのアクションを配置できます。

<img width="567" alt="BehaviorDesingerADX2Menu.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/3cd25ff1-9773-0bc6-fb45-0cf6613dfdfc.png">


## キューの再生
なにがともあれ、サウンドの再生を行わなくてはなりません。

ADX2でのサウンド再生には、「再生するサウンドデータが入ったファイル（キューシート）」のロードと、サウンドの再生単位である「キュー」の名前指定が必要です。

Behaviour Designerのアクションとしてキューシートのロード機構も作れると思うのですが、データの読み込みと破棄はAI制御の内部でやらないほうが良い気がしますので、今回はCRIAtom.csのインスペクターで読み込み設定がされているものとします。
コードは次の通りです。

```PlayAtomSource.cs
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
using UnityEngine;

[TaskCategory("ADX2")]
[TaskIcon("Assets/Gizmos/CriWare/VoiceOn.png")]
public class PlayAtomSource : Action
{
    public SharedGameObject atomSourceGameObject;
    public SharedString cueSheetName, cueName;

    private CriAtomSource atomSource;
    private GameObject prevGameObject;

    public override void OnStart()
    {
        var currentGameObject = GetDefaultGameObject(atomSourceGameObject.Value);
        if (currentGameObject != prevGameObject)
        {
            atomSource = currentGameObject.GetComponent<CriAtomSource>();
            prevGameObject = currentGameObject;
        }
    }

    public override TaskStatus OnUpdate()
    {
        if (atomSource == null)
        {
            Debug.LogWarning("Atom Source is null");
            return TaskStatus.Failure;
        }

        if (string.IsNullOrEmpty(cueName.Value))
        {
            Debug.LogWarning("Cue Name is empty");
            return TaskStatus.Failure;
        }

        if (string.IsNullOrEmpty(cueSheetName.Value))
        {
            Debug.LogWarning("Cue Sheet Name is empty");
            return TaskStatus.Failure;
        }
        
        if (CriAtom.GetCueSheet(cueSheetName.Value) == null)
        {
            Debug.LogWarning("Cue Sheet is not loaded");
            return TaskStatus.Failure;
        }

        atomSource.cueSheet = cueSheetName.Value;
        atomSource.Play(cueName.Value);

        return TaskStatus.Success;
    }
}

```

このアクションのインスペクターでは、

- Atom Sourceがアタッチされたゲームオブジェクトの参照
- キューシート名
- キュー名

の３つのパラメーターを指定します。

![PlayAtomInspector.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/7e54ed1b-4e33-5d75-1e36-237a23415427.png)

「Atom Sourceがアタッチされたゲームオブジェクトの参照」には、SharedGameObjectクラスでatomSourceGameObjectフィールドを用意しています。これは、Behavior Designerが持つクラス参照の共有機構を使うためです。SharedGameObjectクラス経由でAtom Sourceを取得するので、ローカル変数としてAtom Sourceの参照を持っておき、複数のアクションで使いまわすことができます。

Behavior Designerエディタウィンドウの「Variables」で、アクションをまたぐゲームオブジェクトの参照を保持できます。
AtomSourceがアタッチされたゲームオブジェクトの参照を「AtomSourceGameObject」として以下のように定義し、シーン上のゲームオブジェクトの参照を保持します。

![BD_Variables.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/502817d2-6ce2-fc6d-70a9-2b8045136ce4.png)

タスクのインスペクターでフィールド左の「・」ボタンをクリックして、「AtomSourceGameObject」を指定します。

![BD_Action.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/dff930b8-f609-dbad-9dc1-2b0f97f1193d.png)

これにより、複数のアクションに毎回AtomSourceの参照をつけずに済みます。


## キューの停止
キューの停止はシンプルで、Atom Sourceに対してatomSource.Stop()を呼ぶだけです。

```StopAtomSource.cs

using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
using UnityEngine;

[TaskCategory("ADX2")]
[TaskIcon("Assets/Gizmos/CriWare/VoiceOff.png")]
public class StopAtomSource : Action
{
    public SharedGameObject atomSourceGameObject;

    private CriAtomSource atomSource;
    private GameObject prevGameObject;

    public override void OnStart()
    {
        var currentGameObject = GetDefaultGameObject(atomSourceGameObject.Value);
        if (currentGameObject != prevGameObject)
        {
            atomSource = currentGameObject.GetComponent<CriAtomSource>();
            prevGameObject = currentGameObject;
        }
    }

    public override TaskStatus OnUpdate()
    {
        if (atomSource == null)
        {
            Debug.LogWarning("Atom Source is null");
            return TaskStatus.Failure;
        }
        
        atomSource.Stop();

        return TaskStatus.Success;
    }
}

```

## AISACの設定

AISACを使って、AIの挙動にあわせて音が変わる仕組みを作ることができます。
こちらも、Atom Sourceの参照を取得して、タスクのインスペクターに指定された値を与えるだけのものです。

```SetAisac.cs
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
using UnityEngine;

[TaskCategory("ADX2")]
public class SetAisac : Action
{
    public SharedGameObject atomSourceGameObject;
    public SharedString aisacControlName;
    public SharedFloat aisacValue;
    
    private CriAtomSource atomSource;
    private GameObject prevGameObject;
    
    public override void OnStart()
    {
        var currentGameObject = GetDefaultGameObject(atomSourceGameObject.Value);
        if (currentGameObject != prevGameObject)
        {
            atomSource = currentGameObject.GetComponent<CriAtomSource>();
            prevGameObject = currentGameObject;
        }
    }

    public override TaskStatus OnUpdate()
    {
        if (atomSource == null)
        {
            Debug.LogWarning("Atom Source is null");
            return TaskStatus.Failure;
        }

        if (string.IsNullOrEmpty(aisacControlName.Value))
        {
            Debug.LogWarning("AisacControlName is Empty");
            return TaskStatus.Failure;
        }
        
        if (aisacValue == null)
        {
            Debug.LogWarning("AisacValue is Empty");
            return TaskStatus.Failure;
        }

        atomSource.SetAisacControl(aisacControlName.Value,aisacValue.Value);

        return TaskStatus.Success;
    }
}

```

AISACコントロール名も複数のBehavior Desingerタスクで共有して使いまわすことが多いと予想できるので、Variablesタブで定義しておくと便利です。

![距離アイザックのコントロール名を宣言.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/647fcae7-c643-c7b7-0446-53add01cf22d.png)

# まとめ
Behavior Designerへタスク追加は、以前チャレンジした[Doozy UIの拡張](https://qiita.com/Takaaki_Ichijo/items/dcce0fe27a4fc9808a1c)と比較して非常に簡単に追加できます。

ADX2で言えばブロック再生のブロック遷移や、セレクタ機能の利用なども、通常のメソッド呼び出しと変わらない感覚で実装できます。本投稿を参考にしてチャレンジしてみてください。