【Unity】VisualScriptingにスキル攻撃のダメージ計算を任せてみる
VisualScripting(旧 BOLT)を利用してダメージ計算処理をノード設計できるようにし、誰でもさわれるようにする実験をしてみました
結論。
問題なく分離でき「ダメージ計算」「敵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で計算処理を組み立てることにしました
考えうる利点
- 計算式のコード変更がいらなくなるのでコンパイルが走らない
- VisualScriptingのUnitをつなぎを変えるだけで処理の変更ができる。Visual化による処理の可視化
- ログユニット付けてログ出したり、デバッグ処理を付けたりも簡単にできる
- プログラマー以外が触れる
プログラマーとしては1だけでもやる価値あった
実装
ダメージ計算処理は別で切り離したいので SkillCalculater
というGameObjectに ScriptMachine を付ける。
そしてスクリプト上から計算処理を呼び出すために SkillCalculater
MonoBehaviour クラスを作成。
計算処理呼び出し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に登録すること
CustomEvent Unit で Execute
という名前で受けて。4つの引数をそれぞれ SetVariableしてどこでもさわれるようにする
ここまで来たら後は if 文で 「固定ダメージ」かそうじゃないかで分岐したりしてダメージ結果をスクリプト側に返すと。
↑ 矢印先まで来たらこの処理は終わり。(今は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テストになる?)