Help us understand the problem. What is going on with this article?

PUN2UniRx対応試作覚書

More than 1 year has passed since last update.

Everything is a spaghetti...

Rxに適応できないプログラマは北極で人喰いペンギンと戦わされます。
なのでわたしたちは日夜こうしてリアクティブスパゲティを茹でているわけですが、PhotonはコールバックがいっぱいでつらいのでみんなRx化したくなるわけです。

PhotonRx

PUN1はありますがPUN2はありません。
「誰か作っといて!」と吠えていたのですが誰も作ってくれないので自作を試みます。

いけるっしょの精神でUniRxとPhotonRxとPUN2入れてみる

赤い

がんばって自分で実装する

IConnectionCallbacks

とりあえずこのinterfaceに実装されているものだけ作ってみます。

ConnectionCallbacksTriggers

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

拡張メソッドクラス。

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のエラーコールバックはこんな感じで来る。

OnJoinRoomFailed
public override void OnJoinRoomFailed(short returnCode, string message)
{
    base.OnJoinRoomFailed(returnCode, message);
}

なのでこういうExceptionを定義する。

PunException
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わからん。

おしまい。


  1. あとHashTableとかいうC#標準クラスと同じ名前のクラス定義するの、ほんと! マジで! やめて! 2でもそのまま! なんで!!! 

nekomimi-daimao
プログラミングで大事なのは「気合」と「勘」考えるより先に「殴れ」
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした