少し前からUnityの開発を手助けするライブラリとして有名になっているのがUniRxです
そしてオンラインゲームの開発エンジンとして有名なPhotonというものが存在します
今回はこの2つの軽い紹介と、実際に用いて同期ロジックを組み上げてみたものを紹介していきます
UniRx
詳しく説明するとキリがないのでかなりざっくりと説明します
イベントや非同期処理を時間を軸としてフロー化し処理することで、ReactivePrograming(RP)を実現させている実装ライブラリのことをReactiveExtensions(Rx)と呼びます
元は.Netが提供していたライブラリでしたが、様々な分野で評価され、jsやSwift、kotlin等さまざまな言語でも実装がされています
これをUnityリファレンスに向けて作ったのがUniRxというわけです
Photon
Unityをはじめとして 様々なクロスプラットフォームで活躍するネットワーキングエンジンです
マルチプレイヤーでのマッチングシステムや同期などを簡単に実現することができるため、数多くのゲームで採用されています
Unityでマルチプレイヤーのゲームを作る際の定番ですね
実装
説明も軽くしたところで実装です
今回UniRxやPhotonのリファレンス自体の説明は省きますのでその点は自己補完よろしくお願いします
また、簡易的な仮実装なので実際はその開発環境や設計に沿った実装になります
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using Photon;
using Events;
public class PhotonRPCManager : Photon.MonoBehaviour {
[SerializeField]
private PhotonView _photonView;
public Subject<TestInfo> OnRPCTest = new Subject<TestInfo>();
public void TestRPC(int damage, int time, int id, TestType type)
{
_photonView.RPC("testRPC", PhotonTargets.All, damage, time, id, type);
}
[PunRPC]
private void testRPC(int damage, int time, int id, TestType type)
{
OnRPCTest.OnNext(new TestInfo(damage, time, id, type));
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Events
{
public enum TestType { enemy, self }
public struct TestInfo
{
public readonly int Damage;
public readonly int Time;
public readonly int Id;
public readonly TestType Type;
public TestInfo(int damage, int time, int id, TestType type)
{
Damage = damage;
Time = time;
Id = id;
Type = type;
}
}
}
まずはRPC管理側の実装です
Subjectでイベントを管理、RPCでイベントを発行していきます
今回は適当な構造体を作って発行しています
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using Events;
public class TestModel : MonoBehaviour {
[SerializeField]
private PhotonRPCManager _manager;
[SerializeField]
private PhotonView _photonView;
private int _damage = 100;
private int _time = 10;
private int _id = 0;
private void Start()
{
_manager.OnRPCTest
.Where(info => info.Type == TestType.self)
.Subscribe(info => Debug.Log(string.Format("{0}:{1}:{2}", info.Damage, info.Time, info.Id)))
.AddTo(this);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
Debug.Log(string.Format("EnemyEvent:{0}",_id));
_manager.TestRPC(_damage, _time, _id, TestType.enemy);
UpdateInfo();
}
if (Input.GetKeyDown(KeyCode.B))
{
Debug.Log(string.Format("SelfEvent:{0}", _id));
_manager.TestRPC(_damage, _time, _id, TestType.self);
UpdateInfo();
}
}
private void UpdateInfo()
{
_damage += 100;
_time += 10;
_id++;
}
}
続いて同期するモデルです、同期するモデル自体にはPhotonViewをアタッチせずに、ストリームを購読することによって同期を行います
また、購読の際にフィルタリングをかけて一部のイベントだけをトリガーとして処理するようにします、今回は仮のenumによって判定を行います
ついでにイベントも簡易的に設定しました、今回は特定のキーを叩くだけですね
今回はかなり簡易的に作っているので考慮がありませんが、フィルタリングやイベント通知に際して通知者をしっかりと設定する必要があります
(同期して生成した場合に1回のイベントを多重に呼ばせないため)
一応結果です、Aキーを押した場合はイベントは起きていますがフィルタリングではじかれているため処理が呼ばれません、しかしBキーを押した場合は処理が呼ばれていることがわかります
このようにして同期オブジェクト側は、適宜同期イベントをストリームから購読し同期処理を行うといった仕組みになります
簡易的かつ最低限の実装ですが、形としてはこういった感じですね
利点や問題点
まず、利点です
一番大きいのは同期処理をストリームとして処理できることでしょう
マルチプレイにおける同期処理では様々なイベントが飛び交い、それらを正しく処理していく必要があります
その際、ストリーム処理であれば、送られてきたイベントから様々な加工を施し処理を行うことができます
遅延処理や、待機処理などを手軽に柔軟にできるのでこの点では大きく助かりますね
次に問題点です
まず本質から外れた問題点ですが、単純にRxを導入するのに様々なコストがかかること
パフォーマンス面もそうですし、導入コスト自体もあります
そして設計の本質的な問題点ですが、様々なイベント処理に対応するにあたり機構が肥大化していくことです
様々なパラメータが存在し、同期イベントが増えるたびにストリームとそれに呼応したメソッドを用意していかなければなりません
共通パラメータが存在するのであれば複数イベントのストリームをまとめてしまいフィルタリングして取得することもできますが、結局ストリームが大きくなりすぎて発行するたびに無駄な記述も増えていくでしょう
これは仮組なので大した肥大化はしないように見えますが、大規模な開発形態になりRPCが複雑に絡まってくるとこういった問題は浮き彫りになってきます
以上をふまえて、開発のケースに合わせて適宜使っていきたいですね
間違っているところの指摘や、これに対するコメントなどお待ちしております