---
title: VitalRouterで行うイベント伝達
tags: 
author: [中野洋輔](https://convert.docswell.com/user/naninunenoy)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/4EZK1YWGE3.jpg?width=480
description: スイカゲームライクな実装を行う上でVitalRouterを使ってみた際の知見を共有します  【年末だよ】 Unity お・と・なのLT大会 2024【ポロリもあるかもよ】での発表  https://github.com/naninunenoy/SuikaLike
published: December 28, 24
canonical: https://convert.docswell.com/s/naninunenoy/5R2849-2024-12-28-151902
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/4EZK1YWGE3.jpg)

VitalRouter
で行うイベント伝達
Unity お・と・なのLT大会 2024
中野洋輔


# Page. 2

![Page Image](https://bcdn.docswell.com/page/Y76QLDGQ7V.jpg)

発表者
名前：なかの
所属：HIKKY
担当：アバターメイカー
Unity歴：7-8年程
https://avatarmaker.vket.com/
興味：Unity/C#における設計・コーディング手法


# Page. 3

![Page Image](https://bcdn.docswell.com/page/G75R13V2E4.jpg)

今年?のゲーム (2)
https://store-jp.nintendo.com/item/software/D70010000043363
https://x.com/RioPAPA_GAMEs/status/1712617309913915707


# Page. 4

![Page Image](https://bcdn.docswell.com/page/9J2D1Z31JR.jpg)

LTの概要
・スイカゲームの実装方針
・VitalRouterについて
・VitalRouterを用いた実装
・その利点
・and ..
https://github.com/naninunenoy/SuikaLike


# Page. 5

![Page Image](https://bcdn.docswell.com/page/DEY2ZR14EM.jpg)

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) { ... }
}


# Page. 6

![Page Image](https://bcdn.docswell.com/page/VJN83D2678.jpg)

スイカゲームのイベント経路
SuikaManager
衝突の評価 (スコア計算/次のスイカの選出など)
生成
Suika
Suika
Suika
衝突発生
衝突のイベント通知


# Page. 7

![Page Image](https://bcdn.docswell.com/page/YE9K9G1RE3.jpg)

生成時に必要そうな処理
・SuikaにIDを振る
・Suikaの衝突イベントを購読
- Observable/UnityEvent
- 衝突の評価の詳細
・Suika消滅時に購読破棄
・(意外とあるな...)
public class SuikaManager : MonoBehaviour
{
List&lt;Suika&gt; _suikaList = new();
public void SpawnNewSuika()
{
var suika = Spawn&lt;Suika&gt;(&quot;何らかのID&quot;);
suika.OnCollisionEnterAsObservable()
.Subscribe(x =&gt;
{
// 衝突判定
// 得点の計算
// スイカを破棄
// 次のスイカを生み出す
})
.AddTo(suika);
_suikaList.Add(suika);
}
}


# Page. 8

![Page Image](https://bcdn.docswell.com/page/GE8R9V4GJD.jpg)

VitalRouterを使ったイベント経路
SuikaManager
衝突の評価
Commandの送信は
ICommandPubliser.PublishAsync()
に対して行う
生成
ICommandPublisher
Suika
Suika
衝突発生
CollisionCommand
コマンド発行・通知
public class CollisionCommand : ICommand
{
public GameObject Me { get; set; }
public GameObject Other { get; set; }
}


# Page. 9

![Page Image](https://bcdn.docswell.com/page/LELKW56XJR.jpg)

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でなくなったことにより、
購読の破棄の考慮がなくなった


# Page. 10

![Page Image](https://bcdn.docswell.com/page/4JM29353JW.jpg)

VitalRouterの利点
・[Routes]属性だけの簡便さ
・Subscribe/DisposeやAdd/RemoveListenerなし
・メソッド名決定からの解放
・自分は↓みたいに付けてます
public void __DontCallMe(HogeCommand command)
・メソッドが public になるのでユニットテストしやすい
・アニメーションや効果音などの副作用に便利そう


# Page. 11

![Page Image](https://bcdn.docswell.com/page/PJRMGQ3RE9.jpg)

VitalRouterの注意点
・簡便さは ICommandPublisher への依存とのトレードオフ
・DIへの理解が必須ではないが必要そう
・Interceptorなど強力そうな機能(よくわかってない)
・Unity上に構成するオレオレ基盤の実装に使い、
わかってる人だけVitalRouterを使った実装するのが理想か
YourGame
YourSystem
VitalRouter
Unity


# Page. 12

![Page Image](https://bcdn.docswell.com/page/PEXVX54Y7X.jpg)

受信側のテストコード
[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


# Page. 13

![Page Image](https://bcdn.docswell.com/page/3EKQW6Z4ED.jpg)

送信側のテストコード
internal class CommandPublisherMock : ICommandPublisher
{
public ICommand LastPublishedCommand { get; private set; }
public ValueTask PublishAsync&lt;T&gt;(T command, CancellationToken cancellation = new()) where T : ICommand
{
LastPublishedCommand = command;
return default;
}
public ICommandPublisher WithFilter(ICommandInterceptor interceptor)
{
// ???
}
}
・送信側もメソッド2つだけなのでMockは作りやすい(はず)


# Page. 14

![Page Image](https://bcdn.docswell.com/page/L73P16DDE5.jpg)

One More Thing


# Page. 15

![Page Image](https://bcdn.docswell.com/page/87DLXVP2EG.jpg)

MRuby scripting
・Rubyで記述されたスクリプトからコマンドを送ることができる機能
https://hadashikick.land/tech/vitalrouter-mruby


# Page. 16

![Page Image](https://bcdn.docswell.com/page/VJP2PVNLJ8.jpg)

つまり…どういうことだってばよ
・任意の場所にスイカを生み出したい場合
- Command定義+ Preset に登録(左)
- Comanndを受け取る口を用意(右)
[MRubyObject]
public partial struct SuikaSpawnCommand : ICommand
{
public int SuikaType;
public Vector2 Position;
}
[MRubyCommand(&quot;spawn&quot;, typeof(SuikaSpawnCommand))]
partial class MyCommandPreset : MRubyCommandPreset { }
[Routes]
public partial class SuikaManager
{
public void __DontCallMe(SuikaSpawnCommand command)
{
Spawn&lt;Suika&gt;((SuikaType)command.SuikaType,
command.Position);
}
}


# Page. 17

![Page Image](https://bcdn.docswell.com/page/2EVD2ZK6EQ.jpg)

試してみた
・下のように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&lt;Router&gt;();
context.CommandPreset = new MyCommandPreset();
var scenario = Resources.Load&lt;TextAsset&gt;(&quot;scenario&quot;);
using var script = context
.CompileScript(scenario.text);
script.RunAsync(cancellation);
return UniTask.CompletedTask;
}
}


# Page. 18

![Page Image](https://bcdn.docswell.com/page/57GYR4Q27L.jpg)

可能性の塊
・ゲームシーケンスの分離
・自動操作
・自動UI操作とスクショ自動化とか？
YourGame
YourSystem
VitalRouter
Unity


# Page. 19

![Page Image](https://bcdn.docswell.com/page/4EQMVGK9JP.jpg)

Thank you


