【Unity】VisualScriptingにスキル攻撃のダメージ計算を任せてみる

VisualScripting(旧 BOLT)を利用してダメージ計算処理をノード設計できるようにし、誰でもさわれるようにする実験をしてみました

github.com

結論。

問題なく分離でき「ダメージ計算」「敵AI処理」など単一処理をVisual化するのはとても素晴らしいことだと認識できました。


前置き

ダメージ計算処理はよくある(?)計算クラスとして分離して書こうと思っていましたが

    /// <summary>
    /// スキルに関する計算関連
    /// </summary>
    public static class CharaSkillCalculator
    {
        /// <summary>
        /// unitがopponentに与えるダメージを計算して返す
        /// </summary>
        public static long Calculate(ICharaController unit, ICharaController opponent)
        {
            // 今は単純に引くか
            return Math.Max(unit.Status.TotalAttack - opponent.Status.TotalDefence, 0);
        }
    }

まだ大したコードを書いていなかったこと、 そして計算部分をVisualScriptingに任せることで利点が出るのではと思いVisualScriptingで計算処理を組み立てることにしました


考えうる利点

  1. 計算式のコード変更がいらなくなるのでコンパイルが走らない
    1. 「倍率値いじるためにスクリプト変更→コンパイルイライラタイム」が無い
  2. VisualScriptingのUnitをつなぎを変えるだけで処理の変更ができる。Visual化による処理の可視化
  3. ログユニット付けてログ出したり、デバッグ処理を付けたりも簡単にできる
  4. プログラマー以外が触れる

プログラマーとしては1だけでもやる価値あった


実装

ダメージ計算処理は別で切り離したいので SkillCalculater というGameObjectに ScriptMachine を付ける。

そしてスクリプト上から計算処理を呼び出すために SkillCalculater MonoBehaviour クラスを作成。

f:id:toshizabeth:20211114192219p:plain

計算処理呼び出しSkillCalculaterクラスはZenjectでDI管理。

/// <summary>
/// スキル計算について持つ
/// </summary>
public interface ISkillCalculater
{
    /// <summary>
        /// スキルダメージを計算する
        /// </summary>
    UniTask<int> ExecuteAsync(ICharaController unit, ICharaController target, SkillDamageEntity entity);
}

/// <summary>
/// ダメージスキル計算のイベント
/// </summary>
public interface IDamageCalculateEvent
{
    /// <summary>
        /// 与えるダメージ量
        /// </summary>
    void SetCalcDamageValue(int damage);
}


/// <summary>
/// スキル計算について持つ
/// </summary>
public class SkillCalculater : MonoBehaviour, ISkillCalculater, IDamageCalculateEvent
{
    private bool _isFinish;
    private int _value;


    async UniTask<int> ISkillCalculater.ExecuteAsync(ICharaController unit, ICharaController target, SkillDamageEntity entity)
    {
        // 呼び出し
        CustomEvent.Trigger(gameObject, "Execute", this, entity, unit, target);

        // 終了するまで待機する
        if (!_isFinish)
        {
            await UniTask.WaitUntil(() => _isFinish);
        }
        
        return _value;
    }

    void IDamageCalculateEvent.SetCalcDamageValue(int value)
    {
        _value = value;

        _isFinish = true;
    }
}

DI管理

   public class CharaInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
....

            Container
                .Bind<Skill.ISkillCalculater>()
                .FromComponentInHierarchy()
                .AsSingle();
        }
    }


メインは ExecuteAsync メソッドでここでVisualScripintのCustomTriggerを叩いています。

まだまだVisualScriptingについては疎いため、以下のようにTriggerの後は計算処理が帰ってくるまでフラグ待機をしています(他のやり方が思い浮かばない)

WaitUntil は強制1f待機するため即時終了する場合を考慮してif入れてます (ここらへんは雑に対応)

// VisualScriptingの"Execute"呼び出し
CustomEvent.Trigger(gameObject, "Execute", this, entity, unit, target);

// 終了するまで待機する
if (!_isFinish)
{
    await UniTask.WaitUntil(() => _isFinish);
}


計算処理に必要なのは

  • スキル情報
  • 攻撃者
  • ターゲット

上記をVisualScripting側に伝えてTrigger。

※ VisualScriptでUnitとして使うクラスはVisualScriptに登録すること


f:id:toshizabeth:20211114193537p:plain

CustomEvent Unit で Execute という名前で受けて。4つの引数をそれぞれ SetVariableしてどこでもさわれるようにする


ここまで来たら後は if 文で 「固定ダメージ」かそうじゃないかで分岐したりしてダメージ結果をスクリプト側に返すと。

f:id:toshizabeth:20211114193844p:plain

↑ 矢印先まで来たらこの処理は終わり。(今はif文分岐先どちらも同じ処理)


これだけで計算処理をVisualScript側に移動することができました


後は使用するクラスにまずは Inject して

       [Inject] private Chara.Skill.ISkillCalculater _skillCalculater;

実際に処理を書く部分で ExecuteAsync メソッドを await

       {
            foreach (var target in _targets)
            {
                // HPをへらす
                var damage = await _skillCalculater.ExecuteAsync(_chara, target, _entity);

                await target.Damage(damage);

                // 死亡しているか
                if (target.Status.IsDead.Value)
                {
                    _deadChara.Add(target);
                }

                await UniTask.Delay(500);
            }

こんな感じにする。

以上で楽に処理をVisual化できました。


作業量もほぼかからず、これだけで様々な利点を享受できるのであればVisualScriptingにロジック処理を書くのも全然ありだと感じました。


終わり

VisualScriptingにロジックを書く上でUniTask、Zenjectのパワフルさを利用することで更に便利に実装できる。


逆に大変なのはテストが書きにくいことかなと思いました

(どうしてもPlayModeテストになる?)