PlayMakerとBehaviourTreeを使ってステートマシンにイベントハンドリングをできるようにしてみた


はじめに

この記事はSynamon Advent Calendar 2021の20日目の記事になります。

数年ぶりのQiita投稿になります。
Unityでゲーム向けアプリの開発について、これまではコードですべて実装することを考えていましたが、ゲーム毎にロジックが変わる部品をビジュアルスクリプティングに落とし込むことを考えており、今回アドベントカレンダーの記事を書くことをきっかけとしてまとめてみました。
今回は有限ステートマシン(FSM)の概念について触れつつ、Unityでモックまたはプロトタイプのアプリケーションを作る上で簡単にビジュアルスクリプティングからステートマシンを扱うことができるPlayMakerとBehaviour Designerを使ってステートマシンについて触れてみました。

ゲームアプリケーションのステートマシンについて

一般的にゲームで呼称されるステートマシンは(特にUnityではAnimatorで)有限ステートマシンが一般的に使われていますが、実際には順番にステートが遷移するものもあれば、ステートがループするものもあります。

タイトル画面~ゲームが始まるまでのステートマシンの例

厳密に以下の図はステートマシンであるかはちょっと言い切れませんが、GUI画面を一つのステートとしてとらえ、ステートマシンとして図に書いたものです。
シナリオはゲームアプリケーション開始時~ゲーム開始とし、矢印上の文字はアクションで、このアクションをトリガーとして次のステートに移行します。

画面遷移などにステートマシンを使う場合など、実装されているギミックがステートマシンの内側で動いている場合、個々の実装はステートマシンの状態を知らなくても動作することができます。
一方で、ステートマシンの外側でギミックを動かしている場合は、ステートマシンとのやり取りを意識する必要があります。

次にその例を紹介したいと思います。

ゲームキャラクターのスキル実行に関するステートマシンの例

以下は何らかのゲームでのスキル実行に関するふるまいを表したステートマシンの図になります。このスキルは以下の性質を持っています。

  • プレイヤーからの特定のキー(ボタン)入力によって実行される
  • 一度スキルを実行するとリチャージタイム(クールダウン)が発生する
  • リチャージタイム中はスキルを実行することができない

図として表したものです。

基本的にはこれだけでもスキル実行として駆動します。ただし、キー入力は特定のステートへの遷移のフラグとして利用されているので、他のステートでこのキー入力は無視されるようになります。
そこで、ステートから外部にイベントを送ることで、キー入力に対する状態を送信します。これにより、事前に入力したキーが有効であるかを判断することができるようになります。

PlayMakerとBehaviour Designerを使ってステートマシン再現

PlayMakerとBehaviour Designer(Behaviour Tree)を使って、上記のステートマシンを再現し、イベントのハンドリングを行ってみました。

PlayMakerの設定

上記のステートマシンをPlayMakerに落とし込んでみました。Behaviour DesignerにはPlayMakerの統合パッケージを取り込んでいます。TransitionがStartFSMとUseSkillになっている箇所はBehaviourTreeから呼び出されます。

各ステートのイベント通知は以下のように設定しています。PlayMakerはステートマシンでのアクションはentryのギミックのみ利用できます。
SendEventはBehaviour DesignerのBehaviour Treeにイベントを送信しています。

Behaviour Treeの設定

Behabiour Treeはいくつかのパートに分かれており、キー入力をPlayMaker側に渡す処理、スキル開始イベント、スキル完了イベント、スキル再利用可能イベント、そしてスキル再利用可能前にキー入力があった場合にキー入力が無効であることを通知するイベントを送るギミックを追加しています。

Unityスクリプト(BTEventHandler.cs)の設定

おまけとして、本来PlayMakerではアドオンとしてEvent Proxyなどを使うことでUnityのスクリプトでスクリプトを受け取れるようになりますが、Behabiour Treeを使うことでアドオンを使わずにUnity側にイベントを送信することができます。

BTEventHandler.cs

using BehaviorDesigner.Runtime;
using UnityEngine;

[RequireComponent(typeof(BehaviorTree))]
public class BTEventHandler : MonoBehaviour
{
    [SerializeField]
    private BehaviorTree? behaviorTree;

    public void OnUseSkill_0()
    {
        behaviorTree?.SendEvent("UseSkill");
    }

    public void OnPendingSkill_0()
    {
        Debug.Log("Skill is pending.");
    }

    private void OnStartSkill_0()
    {
        Debug.Log("Skill is started.");
    }

    private void OnFinishSkill_0()
    {
        Debug.Log("Skill is finished.");
    }

    private void OnEnableSkill_0()
    {
        Debug.Log("Skill is enabled.");
    }

    #region MonoBehaviour implements

    private void Reset()
    {
        behaviorTree = GetComponent<BehaviorTree>();
    }

    public void Start()
    {
        behaviorTree?.RegisterEvent("EnableSkill", OnEnableSkill_0);
        behaviorTree?.RegisterEvent("PendingSkill", OnPendingSkill_0);
        behaviorTree?.RegisterEvent("StartSkill", OnStartSkill_0);
        behaviorTree?.RegisterEvent("FinishSkill", OnFinishSkill_0);
    }

    #endregion
}

OnUseSkill_0()のメソッドはInputSystemのPlayerInput経由でInputActionのバインドを受け取れるようにしています。

上記のものをGameObjectにアタッチしたものになります。

実行結果

実行結果はUnity側で受け取ったイベントのログを出しています。
[ スキル利用可能 > スキル使用開始 > スキル使用完了 > スキル利用可能 ] というサイクルで回っています。

また、以下はスキル再利用可能前にキー入力をした場合のログになります。

このようにUnityスクリプト~BehaviourTree~PlayMaker間でステートマシンのイベントのやり取りができるようになりました。

さいごに

PlayMakerだけでゲームロジックは作れると思いますが、様々なサードパーティ製ライブラリを組み合わせて使う場合、どの単位で切り分けができるかを知る良いきっかけになりました。
BehaviourTree(Behaviour Designer)側のパフォーマンスは悪いと思うので、今後は使いこなして、もっと効率の良い使い方ができればと思います。