見出し画像

PUN2からPhotonFusionに移植したときにハマったところ3選

※この記事は2023年11月13日に弊社運営の技術情報サイト「ギャップロ」に掲載した記事です。


PUN2で作ったアプリを、PhotonFusionで移植する機会があり、その際に実際にハマったところ3選をまとめてみました!マルチプレイのアプリ初学者がどのようなところで躓きやすいのか、事例と共に参考にしてもらえればと思います!

アプリの概要

バージョン

Unityバージョン: 2022.2.2f1
PhotonFusionバージョン:PhotonFusion 1.1.8

基本性能

移植したアプリはXRアプリです。基本的な機能は以下のような内容です。

  • 同じセッションに入っているユーザーの位置と回転、コントローラーのレイを共有する。

  • シーン内の共通のオブジェクトを複数のユーザーが移動、回転させる

  • マーカーを基準にした座標を利用して実空間の同じ位置にオブジェクトが見えるようにする。

基本的には、同じ空間にいるユーザーでマルチプレイを行うことを想定していますが、他の場所でも利用可能なアプリになっています。

利用したFusionのモードと、利用方針

利用したのはFusionのSharedモードです。とにかくPUN2でできているものをうまく変換することを重視して利用したため、Fusionの機能をフル活用してやるぜ!という意気込みの実装ではありません。最小の機能を利用してどのようにPUN2の機能を実現するかという視点で利用しました。

ハマったところ

では、具体的にハマったところです。ハマりどころの抽象度がまちまちですが、ご容赦いただけたらと思います。

1. カスタムプロパティの置換権限を意識せず実装してしまった

マルチプレイ実装でハマるところとしてお馴染み(?)の権限周りの注意点になります。
PUN2には、セッションを行っているルームに対してカスタムプロパティというルームで共有されるプロパティを設定できました。カスタムプロパティは、Stringをキーとしたハッシュテーブルで管理され、どのユーザーも変更できます。Fusionではカスタムプロパティが廃止されたので、何かしらで置き換える必要があります。この代わりとして、利用が想定されているのがネットワークプロパティです。

ネットワークプロパティ

ネットワークプロパティは、[Networked]とつけられたプロパティで、その変更があった際にすべてのクライアントに対して変更が反映されます。公式サイトでは、ボールが発射された瞬間にキューブを白色にし、段々と青色にしていくサンプルでネットワークプロパティが紹介されています。

このサンプルコードを見ると下のようなコードでルームのカスタムプロパティが実装できるように思えます。

public class Room : NetworkBehaviour
{
    // String型ネットワークプロパティ、OnChangeRoomNameをコールバックとして設定
    [Networked(OnChanged = nameof(OnChangeRoomName))] 
    public String RoomName { get; set; }
    
    // ルームの名前をuGUIで表示する想定
    [SerializeField] private TextMeshProUGUI _roomName;
    
    //  ネットワークプロパティが変更されたかどうかのフラグ
    private static bool isChangeRoomName;
    
    // ネットワークプロパティが変更されたら、コールバックを受ける。
    public static void OnChangeRoomName(Changed<Room> changed)
    {
        isChangeRoomName = true;
    }

    // Fusion固有のティックで呼ばれる。Fusion内のUpdateのようなもの
    public override void FixedUpdateNetwork()
    {
        // 変更されていたらTextをアップデート
        if (isChangeRoomName)
        {
            _roomName.text = RoomName;
            isChangeRoomName = false;
        }
    }

    // ネットワークプロパティの変更
    public void ChangeRoomName()
    {
        RoomName = "HogeHoge";
    }
}

ChangeRoomName()を呼んでネットワークプロパティを変更しようとしています。しかし、このコードではカスタムプロパティの時のように、全てのユーザーがネットワークプロパティを変更することはできません。公式サイトには以下のように書かれています。

クライアントが StateAuthority を持たないオブジェクトの Networked Property を変更した場合、その変更はネットワーク上で同期されるのではなく、ローカルな予測として適用され、将来StateAuthority からの変更によって上書きされます



photonFusion公式サイト

つまり、ローカルで変更した後にStateAuthorityを持つユーザー(StateAuthor)の値に上書きされるため実質的にはStateAuthor以外は変更できないことになります。この仕様は、オブジェクトを共有するためアタッチするNetworkObjectの仕様になります。

では、カスタムプロパティと同じ機能を作ろうとするとどのようにするかというと、RPCを利用してStateAuthorに変更を依頼します。(ホストモードで主に利用するネットワークインプットという新しい方法がありますが、今回の実装ではRPCを利用しました。)

public class Room : NetworkBehaviour
{
    // ネットワークプロパティ
    [Networked(OnChanged = nameof(OnChangeRoomName))] 
    public String RoomName { get; set; }
  
  // ルームの名前をuGUIで表示する想定
    [SerializeField] private TextMeshProUGUI _roomName;
    
    //  ネットワークプロパティの変更されたかどうかのフラグ
    private static bool isChangeRoomName;
    
    // ネットワークプロパティが変更されたら、コールバックを受ける。
    public static void OnChangeRoomName(Changed<Room> changed)
    {
        isChangeRoomName = true;
    }

    // Fusionの固有のティックで呼ばれる。Fusion内のUpdateみたいなもの。
    public override void FixedUpdateNetwork()
    {
        // 変更されていたらTextをアップデート
        if (isChangeRoomName)
        {
            _roomName.text = RoomName;
            isChangeRoomName = false;
        }
    }

    // ネットワークプロパティの変更
    public void ChangeRoomName()
    {
        SetRoomName_RPC("HogeHoge");
    }

    // PRCでStateAuthorに変更を依頼する。
   [Rpc(RpcSources.All, RpcTargets.StateAuthority)]
   public void SetRoomName_RPC(String roomName, RpcInfo info = default)
   {
        RoomName = roomName;
   }
}

このようにすることで、

StateAuthorに対してRoomNameの変更を依頼する

StateAuthorが、RoomNameの変更を行う。

OnChangeRoomName()で変更コールバックを受ける。

という形で、RoomNameの変更を行うことができます。ネットワークプロパティのコールバックはstaticである必要があるため、上のようにフラグを用意するかUniRxなどで変更イベントを作成すると便利かもしれません。

実は、PUN2の時にも同じようなフローを使う場面がありました。下記の記事に並行性問題の回避という章があり、一人のユーザーに値の変更をゆだねて値変更のバッティングを回避するという内容があります。この用途で同じような実装を行っています。

PUN2(Photon Unity Networking 2)で始めるオンラインゲーム開発入門


また、FusionのHostモードはネットワークオブジェクトのStateAuthorityをすべてホストユーザーが担います。なので、今回と同じようにホストに依頼して変更をかけるフローが基本になります。さらに、もともとホスト-クライアントの関係があるシステムで構成されているNetcode for GameobjectsのNetworkVariableも同じような実装を行います。

Fusionでは、このようにPUN2よりもオブジェクトの権限が重要になるので、ネットワークに乗せるものを作成する際は常に意識する必要があります。僕の場合は、ハマった時にNetcode for GameobjectsのNetworkVariableを思い出して修正することができました。

2. PUN2でデフォルトだった機能が、オプショナルになってハマる

具体的には、シーンオブジェクトとして生成したネットワークオブジェクトがアクティベートされないという状態になりました。

シーンオブジェクトとは、Runtimeで生成されたネットワークオブジェトではなくシーン上に配置されたネットワークオブジェクトのことで、シーン内に一つしかないモデルやマネージャー等を作りたいときに便利です。

PUN2では、シーンにネットワークオブジェクトを配置した状態で、セッションを開始した場合デフォルトでシーン内のネットワークオブジェクトをアクティベートしてくれていました。

しかし、Fusionでは、NetworkRunnerでセッションをスタートするとシーン内に配置されたネットワークオブジェクトはアクティベートされませんでした。(実際には、デフォルトの設定でそうなっていただけでオプションで可能です・・)

解決策をさがして色々調べているとシーンをロードする際にネットワークオブジェクトをアクティベートしているということが分かりました。Photon公式サイトのAsteroidsサンプルにおいてNetworkRunnerでセッションを開始した後、シーンの遷移を行ってシーンオブジェクトをアクティベートしていたので、その実装に倣うことで、うまくアクティベートすることができました。

PUN2と同じようにシーンオブジェクトは使えないのか→使えます。

最終的にUX的にシーンを分けたほうがよかったので、実装はしていないのですが、NetworkRunner.StartGame()というセッションを開始する時のメソッドの引数(StartGameArgs)にシーンを指定することができ、セッション開始時にそのシーンがロードされるようになっているようです。公式サイトでは、Host/Serverモードのチュートリアル内でStartGame()の引数としてシーンが指定されており、これによってロードが起こっているようです。(該当チュートリアル部分)

PUN2と同じような機能でもデフォルトの挙動が変わっていることはあり、便利な場合もある(便利だった例も後述します。)のですが、もしハマってしまった場合は一度確認したほうが良いかもしれません。

3. 新しい概念の理解と、自分が利用する機能なのかの選別

抽象的な課題ですがもっともハマりやすく、これを越えるととても実装が楽しくなる要素だと思います。僕は、オンラインマルチプレイのアプリ実装に関してはまだまだ初学者ですが、Fusionを利用する前にNetcode for GameObjectsとPUN2を続けて利用しており、一度実装したアプリの内容なら問題なく理解して実装できると思っていました。しかし、Fusionでは新しい概念と共に多くの機能が追加されており、同じ内容の実装であっても最初は理解することが難しかったです。

また、概念や機能を理解したとしてもそれが、自分の利用するものであるかどうかを判別する必要があります。FusionにはPUN2とは異なり、複数のモード(ネットワークトポロジー)が存在し、それぞれのモードごとにできることが異なり、実装自体も大幅に変わるからです。最初のチュートリアルがモードによって早々に分岐することからも実装が大きく変わることが分かると思います。

PhotonFusion公式サイトよりネットワークトポロジーでできること

特に、Sharedモードでのティックベースシミュレーションの役割を僕は軽視しており、途中でSharedモードであろうとFusionの根幹にあるシステムであると気づきました。後から考えると、この理解が進んだことでモードごとの違いを見分けられるようになっていった気がします。

ティックベースシミュレーション

ティックベースシミュレーションはFusionで導入された概念で、一般的なオンラインマルチプレイゲーム開発技術の一つです。公式サイトで詳しく説明されていますし、多くのFusionの記事内でティックベースシミュレーションについて触れられています。

ざっくり説明すると、Fusionでは各アプリの持つ時間軸と別の時間/フレーム(ティック)をFusion内で持っており共有しています。この共通したティック毎に、Fusion専用のFixedUpdateNetwork()で処理を行うことで各プレイヤーのネットワーク状況に関わらず共通の時間軸で処理ができるようになります。

上の説明を聞くと。PUN2でいうところのOnPhotonSerializeView() と同じ機能かな?と思うかと思います。僕もそう思いました。実際に比較するととても似ていて、実際に同じ役割を担う場面も多くあります。

しかし、Fusionにとってのティックベースシミュレーションはもっと根本的なシステムに関わっており、他の場所にも影響を及ぼしています。例えば、RPCでは処理をティックベースシミュレーション上で行うオプションが増えました。

[Rpc (RpcSources.All, RpcTargets.All, InvokeLocal = true, InvokeResim = true, TickAligned = false )]
void RpcStartBoost(){
    m_BoostAnim.StartBoostAnimation();
}

上のコードは、英語版の公式サイトのRPCに関するページに載っているサンプルです。メソッドの上のAttributeのオプションにTickAlignedという値を入れることでき、これがtrueになっているとティックシミュレーション上で処理を行ってくれるようです。デフォルト値は、 TickAligned = trueになっているので何も指定しなければティックシミュレーション上で処理してくれます。

※注意点:Sharedモードでは2022/4以降のSDKの仕様では TickAligned = falseと同じような挙動になるように修正されているようです。

Buil467 (May 04, 2022)Shared Mode: All RPCs are handled as if they were defined with TickAligned = false



PhotonFusion公式サイトSDK & リリースノート

Sharedモードで利用する機能を判別するには・・

今回実装対象じゃないので、割愛しますがPhotonがFusionで最も機能を充実させたいと思っているであろうHostモードに目を通すとSharedモードでできないことが理解できるようになると思います。o8queさんのPhoton Fusionのシミュレーションを理解しようという記事がおすすめで、これらのシミュレーションを行うためにFusionのティックベースシミュレーションが存在すると考えると、Sharedモードで利用できない機能が分かるようになってきます。

ティックベースシミュレーションは主にHost/Serverモードでメリットが大きいものです。ネット上の記事や公式サイトでもSharedモードでどのように生かされるのかが分かりづらく後回しにしがちですがFusionの根幹にあるシステムであることは間違いないです。

一旦、PUN2を移植する実装のみだけでは必要ない領域ではあるのですが、Hostモードの記事も読みながら並行して実装していけば、全体の実装スピードも上がっていくと思います。

さいごに

以上ハマったところ3選でした。実際にハマって時間を使ってしまったところなので、読んでいる方で共感できる部分があったら嬉しいです。

いろいろ話しましたが、今後PUN2とFusionどちらを使っていくのかというとFusionを使っていくだろうなと思っています。現状の使い勝手だけでも、ネットワークプロパティはカスタムプロパティよりも可読性が高く使いやすく、Netcode for Gameobjectsと同じような形で書けるので他人が見ても分かりやすいといえます。大きなアプリになればなるほどこの利便性は顕著になっていくでしょう。オンラインマルチプレイのコードは特殊なので、このようなハードルの下がり方はとても重要だなと思います。

一つ注意点を挙げるならば、公式サイトで日本語化されている資料が、英語版よりも古い内容であったり、一見Hostモードの内容の中にSharedモードもかかわる内容が書かれていたりすることですかね。日本語資料については、ZennのPhoton運営事務局TechBlogに質の高い内容がまとまっているのでそこから読んだほうが良いこともあるかと思います。

PUN2の機能追加は今後ないことが表明されています。Sharedモードならば、比較的簡単に移植できるのでこれを機会にみなさんも一度試してみたらいかがでしょうか。その際に、このハマりどころ記事が役に立ったら幸いです。

この記事が気に入ったらサポートをしてみませんか?