1.3K Views
December 28, 24
スライド概要
スイカゲームライクな実装を行う上でVitalRouterを使ってみた際の知見を共有します
【年末だよ】 Unity お・と・なのLT大会 2024【ポロリもあるかもよ】での発表
https://github.com/naninunenoy/SuikaLike
Unityエンジニア
VitalRouter で行うイベント伝達 Unity お・と・なのLT大会 2024 中野洋輔
発表者 名前:なかの 所属:HIKKY 担当:アバターメイカー Unity歴:7-8年程 https://avatarmaker.vket.com/ 興味:Unity/C#における設計・コーディング手法
今年?のゲーム (2) https://store-jp.nintendo.com/item/software/D70010000043363 https://x.com/RioPAPA_GAMEs/status/1712617309913915707
LTの概要 ・スイカゲームの実装方針 ・VitalRouterについて ・VitalRouterを用いた実装 ・その利点 ・and .. https://github.com/naninunenoy/SuikaLike
VitalRouter
・Unity向けメッセージングライブラリ
source-generatorを基盤とすることで簡便さと性能を両立
https://github.com/hadashiA/VitalRouter
vs. UniRx.MessageBroker/Zenject.EventBus/MessagePipe/ZeroMessenger
public readonly record struct FooCommand(int X, string Y) : ICommand;
[Routes]
定義したデータ型をコマンドとして定義し、
public partial class FooPresentor
ルート属性をつけたクラスやメソッドで
{
[Route] void On(FooCommand cmd) { ... }
コマンドを処理できる(非同期も可能らしい)
[Route] async UniTask On(FooCommand cmd, CancellationToken cancellation) { ... }
}
スイカゲームのイベント経路 SuikaManager 衝突の評価 (スコア計算/次のスイカの選出など) 生成 Suika Suika Suika 衝突発生 衝突のイベント通知
生成時に必要そうな処理
・SuikaにIDを振る
・Suikaの衝突イベントを購読
- Observable/UnityEvent
- 衝突の評価の詳細
・Suika消滅時に購読破棄
・(意外とあるな...)
public class SuikaManager : MonoBehaviour
{
List<Suika> _suikaList = new();
public void SpawnNewSuika()
{
var suika = Spawn<Suika>("何らかのID");
suika.OnCollisionEnterAsObservable()
.Subscribe(x =>
{
// 衝突判定
// 得点の計算
// スイカを破棄
// 次のスイカを生み出す
})
.AddTo(suika);
_suikaList.Add(suika);
}
}
VitalRouterを使ったイベント経路 SuikaManager 衝突の評価 Commandの送信は ICommandPubliser.PublishAsync() に対して行う 生成 ICommandPublisher Suika Suika 衝突発生 CollisionCommand コマンド発行・通知 public class CollisionCommand : ICommand { public GameObject Me { get; set; } public GameObject Other { get; set; } }
VitalRouterを使ったイベント購読の実装例 送信側 受信側 public class Suika : MonoBehaviour ※[inject]はVContainerの機能 { [Inject] ICommandPublisher _publisher; void OnCollisionEnter(Collision other) { // 衝突を判定してコマンドを送信 var command = new CollisionCommand { MyId = gameObject, OtherId = other.gameObject }; _publisher.PublishAsync(command); } } [Routes] ※partial クラスである必要あり public partial class SuikaManager { public void On(CollisionCommand command) { // 衝突判定 // 得点の計算 // スイカを破棄 // 次のスイカを生み出す } } ポイント 衝突イベントの購読元が Suikaでなくなったことにより、 購読の破棄の考慮がなくなった
VitalRouterの利点 ・[Routes]属性だけの簡便さ ・Subscribe/DisposeやAdd/RemoveListenerなし ・メソッド名決定からの解放 ・自分は↓みたいに付けてます public void __DontCallMe(HogeCommand command) ・メソッドが public になるのでユニットテストしやすい ・アニメーションや効果音などの副作用に便利そう
VitalRouterの注意点 ・簡便さは ICommandPublisher への依存とのトレードオフ ・DIへの理解が必須ではないが必要そう ・Interceptorなど強力そうな機能(よくわかってない) ・Unity上に構成するオレオレ基盤の実装に使い、 わかってる人だけVitalRouterを使った実装するのが理想か YourGame YourSystem VitalRouter Unity
受信側のテストコード [Routes] public partial class SuikaScorePresenter { [Inject] ISuikaScoreWriter _writer; public void On(SuikaScoreCommand command) { _writer.SetScore(command.Score); } //得点の更新をViewに反映 } ・C#のクラスなのでユニットテスト上でnewして Onに対する結果を確認すればOK ・SubjectをnewしてIObservableとして注入する必要なし! ※VContainerを使ったテストは↓ https://yotiky.hatenablog.com/entry/unity_testframework4-vcontainer
送信側のテストコード
internal class CommandPublisherMock : ICommandPublisher
{
public ICommand LastPublishedCommand { get; private set; }
public ValueTask PublishAsync<T>(T command, CancellationToken cancellation = new()) where T : ICommand
{
LastPublishedCommand = command;
return default;
}
public ICommandPublisher WithFilter(ICommandInterceptor interceptor)
{
// ???
}
}
・送信側もメソッド2つだけなのでMockは作りやすい(はず)
One More Thing
MRuby scripting ・Rubyで記述されたスクリプトからコマンドを送ることができる機能 https://hadashikick.land/tech/vitalrouter-mruby
つまり…どういうことだってばよ
・任意の場所にスイカを生み出したい場合
- Command定義+ Preset に登録(左)
- Comanndを受け取る口を用意(右)
[MRubyObject]
public partial struct SuikaSpawnCommand : ICommand
{
public int SuikaType;
public Vector2 Position;
}
[MRubyCommand("spawn", typeof(SuikaSpawnCommand))]
partial class MyCommandPreset : MRubyCommandPreset { }
[Routes]
public partial class SuikaManager
{
public void __DontCallMe(SuikaSpawnCommand command)
{
Spawn<Suika>((SuikaType)command.SuikaType,
command.Position);
}
}
試してみた
・下のようにrubyのテキストを読み取って実行できます
// scenario.rb
data_list = [
{ suikatype: 0, position: [-2.34, 1.80] },
{ suikatype: 4, position: [0.00, 1.80] }
]
data_list.each do |data|
cmd :spawn, suikaType: data[:suikatype], position: data[:position]
wait 0.6
end
public partial class GameEntryPoint : IAsyncStartable
{
[Inject] readonly IObjectResolver _resolver;
public UniTask StartAsync(CancellationToken cancellation)
{
var context = MRubyContext.Create();
context.Router = _resolver.Resolve<Router>();
context.CommandPreset = new MyCommandPreset();
var scenario = Resources.Load<TextAsset>("scenario");
using var script = context
.CompileScript(scenario.text);
script.RunAsync(cancellation);
return UniTask.CompletedTask;
}
}
可能性の塊 ・ゲームシーケンスの分離 ・自動操作 ・自動UI操作とスクショ自動化とか? YourGame YourSystem VitalRouter Unity
Thank you