Everything is a spaghetti...
Rxに適応できないプログラマは北極で人喰いペンギンと戦わされます。
なのでわたしたちは日夜こうしてリアクティブスパゲティを茹でているわけですが、PhotonはコールバックがいっぱいでつらいのでみんなRx化したくなるわけです。
PUN1はありますがPUN2はありません。
「誰か作っといて!」と吠えていたのですが誰も作ってくれないので自作を試みます。
いけるっしょの精神でUniRxとPhotonRxとPUN2入れてみる
赤い
がんばって自分で実装する
とりあえずこのinterfaceに実装されているものだけ作ってみます。
ConnectionCallbacksTriggers
using System;
using System.Collections.Generic;
using Photon.Pun;
using Photon.Realtime;
using UniRx;
using UniRx.Triggers;
public class ConnectionCallbacksTriggers : ObservableTriggerBase, IConnectionCallbacks
{
#region Photon
private Subject<Unit> onConnected;
public void OnConnected()
{
onConnected?.OnNext(Unit.Default);
}
public IObservable<Unit> OnConnectedAsObservable()
{
return onConnected ?? (onConnected = new Subject<Unit>());
}
private Subject<Unit> onConnectedToMaster;
public void OnConnectedToMaster()
{
onConnectedToMaster?.OnNext(Unit.Default);
}
public IObservable<Unit> OnConnectedToMasterAsObservable()
{
return onConnectedToMaster ?? (onConnectedToMaster = new Subject<Unit>());
}
private Subject<DisconnectCause> onDisconnected;
public void OnDisconnected(DisconnectCause cause)
{
onDisconnected?.OnNext(cause);
}
public IObservable<DisconnectCause> OnDisconnectedAsObservable()
{
return onDisconnected ?? (onDisconnected = new Subject<DisconnectCause>());
}
private Subject<RegionHandler> onRegionListReceived;
public void OnRegionListReceived(RegionHandler regionHandler)
{
onRegionListReceived?.OnNext(regionHandler);
}
public IObservable<RegionHandler> OnRegionListReceivedAsObservable()
{
return onRegionListReceived ?? (onRegionListReceived = new Subject<RegionHandler>());
}
private Subject<Dictionary<string, object>> onCustomAuthenticationResponse;
public void OnCustomAuthenticationResponse(Dictionary<string, object> data)
{
onCustomAuthenticationResponse?.OnNext(data);
}
public IObservable<Dictionary<string, object>> OnCustomAuthenticationResponseAsObservable()
{
return onCustomAuthenticationResponse ?? (onCustomAuthenticationResponse = new Subject<Dictionary<string, object>>());
}
private Subject<string> onCustomAuthenticationFailed;
public void OnCustomAuthenticationFailed(string debugMessage)
{
onCustomAuthenticationFailed?.OnNext(debugMessage);
}
public IObservable<string> OnCustomAuthenticationFailedAsObservable()
{
return onCustomAuthenticationFailed ?? (onCustomAuthenticationFailed = new Subject<string>());
}
#endregion
// Subjectまとめとこうとするとなんかエラー出る.
// SubjectってもしかしてListとかArrayとかに入れられないやつ?
// CompositeDisposableみたいなのを自作する必要がある?
// 他のObservableTriggerBase継承クラスはSubjectひとつひとつにOnCompletedやDisposeしている…….
private Subject<object>[] _subjectable;
#region lifecycle
private void OnEnable()
{
PhotonNetwork.AddCallbackTarget(this);
}
private void OnDisable()
{
PhotonNetwork.RemoveCallbackTarget(this);
foreach (var subject in _subjectable)
{
subject.Dispose();
}
}
#endregion
#region UniRx
protected override void RaiseOnCompletedOnDestroy()
{
foreach (var subject in _subjectable)
{
subject.OnCompleted();
}
}
#endregion
}
ConnectionCallbacksTriggersExtension
拡張メソッドクラス。
using System;
using System.Collections.Generic;
using Photon.Realtime;
using UniRx;
using UnityEngine;
public static class ConnectionCallbacksTriggersExtension
{
public static IObservable<Unit> OnConnectedAsObservable(this Component component)
{
return component?.gameObject == null
? Observable.Empty<Unit>()
: GetOrAddComponent<ConnectionCallbacksTriggers>(component.gameObject).OnConnectedAsObservable();
}
public static IObservable<Unit> OnConnectedToMasterAsObservable(this Component component)
{
return component?.gameObject == null
? Observable.Empty<Unit>()
: GetOrAddComponent<ConnectionCallbacksTriggers>(component.gameObject).OnConnectedToMasterAsObservable();
}
public static IObservable<DisconnectCause> OnDisconnectedAsObservable(this Component component)
{
return component?.gameObject == null
? Observable.Empty<DisconnectCause>()
: GetOrAddComponent<ConnectionCallbacksTriggers>(component.gameObject).OnDisconnectedAsObservable();
}
public static IObservable<RegionHandler> OnRegionListReceivedAsObservable(this Component component)
{
return component?.gameObject == null
? Observable.Empty<RegionHandler>()
: GetOrAddComponent<ConnectionCallbacksTriggers>(component.gameObject).OnRegionListReceivedAsObservable();
}
public static IObservable<Dictionary<string, object>> OnCustomAuthenticationResponseAsObservable(this Component component)
{
return component?.gameObject == null
? Observable.Empty<Dictionary<string, object>>()
: GetOrAddComponent<ConnectionCallbacksTriggers>(component.gameObject).OnCustomAuthenticationResponseAsObservable();
}
public static IObservable<string> OnCustomAuthenticationFailedAsObservable(this Component component)
{
return component?.gameObject == null
? Observable.Empty<string>()
: GetOrAddComponent<ConnectionCallbacksTriggers>(component.gameObject).OnCustomAuthenticationFailedAsObservable();
}
private static T GetOrAddComponent<T>(GameObject gameObject)
where T : Component
{
var component = gameObject.GetComponent<T>();
if (component == null)
{
component = gameObject.AddComponent<T>();
}
return component;
}
}
実動作
テストとかぜんぜんしてないけど以下のコードでログとか出てるから動いてるっぽい。
全体の実装方針はこれでよいと思われる。
this.OnConnectedAsObservable().Subscribe(v => Debug.Log("connect!!!!"));
this.OnConnectedToMasterAsObservable().Subscribe(x =>
{
Debug.Log("master room!!!!");
var roomOptions = new RoomOptions();
PhotonNetwork.JoinOrCreateRoom("room", roomOptions, TypedLobby.Default);
});
PhotonNetwork.ConnectUsingSettings();
今後の課題一覧
Subscribeのタイミングを自由にする
ReplaySubject<>にしてAwakeでnewしておき、後から好きなObservableをSubscribeしても値が受け取れるようにした方がいい?
-
Singletonにする必要がある =ObservableTriggerBaseを継承できなくなる(なんで継承してるのかよくわかってない) - DestroyされるObjectにAddComponentされると困る(自前で
new GameObject()すればよいか) - Hot、Coldは大丈夫なのか(いまいちこいつらを把握しきれていない)
複雑になってよくない気配がする。そこまでのコストを払って対応するべきか。
OnError活用したい
PUN2のエラーコールバックはこんな感じで来る。
public override void OnJoinRoomFailed(short returnCode, string message)
{
base.OnJoinRoomFailed(returnCode, message);
}
なのでこういうExceptionを定義する。
public class PunException : Exception
{
public short ErrorCode { get; private set; }
public string ErrorMessage { get; private set; }
public PunException(short errorCode, string errorMessage)
{
this.ErrorCode = errorCode;
this.ErrorMessage = errorMessage;
}
public PunException()
: base()
{
}
public PunException(string message)
: base(message)
{
}
public PunException(string message, Exception innerException)
: base(message, innerException)
{
}
protected PunException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
でこんな感じで使えたらいいな。
Subject<Unit> JoinRoom = new Subject<Unit>();
void JoinRoomSubscriber()
{
JoinRoom.Subscribe(
unit => { Debug.Log("joined!"); },
exception => { Debug.Log("join failed..."); }
);
}
public override void OnJoinedRoom()
{
base.OnJoinedRoom();
JoinRoom.OnNext(Unit.Default);
}
public override void OnJoinRoomFailed(short returnCode, string message)
{
base.OnJoinRoomFailed(returnCode, message);
JoinRoom.OnError(new PunException(returnCode, message));
}
- 失敗→リトライ→成功 した場合どうしよう。
- 正常コールバックに対してどのコールバックがエラーとして来るのかわからない……。
**「エラー時にどのコールバックが来るのかわからない」**ってPUNの設計ですっごい嫌いなところ1だったんですけど、2でもそのままでかなしい。ドキュメントにはちらっと書いてはありますが。
結局総当たりでログ打ってどのコールバックが来るか検証するしかないのか……。
これはライブラリではなくセルフサービスでやってね! なやつ。
Subscribeするだけで接続される
// OnConnectedToMaster時にOnCompletedされる想定
Subject<Unit> ConnectMaster = new Subject<Unit>();
Subject<Unit> JoinRoom = new Subject<Unit>();
public IObservable<Unit> ConnectAndJoinRoomAsObserVable(string roomName)
{
return ConnectMaster.DoOnSubscribe(() => PhotonNetwork.ConnectUsingSettings())
.DoOnCompleted(() =>
{
var roomOptions = new RoomOptions();
PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, TypedLobby.Default);
}).Concat(JoinRoom).AsObservable()
.Share();
}
書いておいてなんだけど複数回SubscribeするとDo~が毎回走って死ぬしObservableがここまでやるのはなんか違う気がするのでなし。
クラス分割
各コールバックに応じてTriggerクラスを分割するべきか否か。
ひとまとめにすることで、Componentの数が少なくなってPhotonNetwork.AddCallbackTarget(this);に登録されるコールバックの数が少ないほうがよい気はする。メンテナンスはつらい気持ちになるがライブラリなので度外視。
でもUniRxではひとつのコールバックにつきひとつのTriggerクラスをくっつけている。なにか理由がある? それくらい分割するように踏襲したほうがよい?
まとめ
ひさしぶりに自分で9.99からライブラリつくろうかな! と思いましたが道が遠い。
設計マンとしてはやっぱりライブラリ作ると頭使うので楽しいですけれど。
がんばってUniRxのコード読みます。UniTaskとCoroutineのあたりはよくわかってない。というかTaskわからん。
おしまい。
-
あと
HashTableとかいうC#標準クラスと同じ名前のクラス定義するの、ほんと! マジで! やめて! 2でもそのまま! なんで!!! ↩