💨

Fusionの[Networked]属性はどう動いている?IL変換と自動同期の仕組みを解剖する

に公開

はじめに

Photon Fusionは、高性能・高柔軟性なネットワーク同期システムを提供する次世代ネットワークライブラリです。
その中でも特に重要な役割を担っているのが、[Networked] 属性です。

この属性を使うことで、プレイヤーの位置やアニメーション状態、ゲーム内オブジェクトの変化などを自動的に同期でき、煩雑な手動のデータ送受信処理を省くことができます。

本シリーズでは、Fusionの内部同期処理を段階的に掘り下げて解説していきますが、今回は、[Networked] 属性の裏側で何が起きているのか、どのようにして自動同期が実現されているのかを詳しく見ていきます。

Fusionにおける[Networked]属性の同期処理概要

[Networked] 属性は?

Fusionでは、[Networked] 属性が付与されたプロパティはネットワーク状態の同期対象として扱われますが、ユーザーが手動で同期コードを書く必要はありません。
この仕組みは、Unityが提供する ILPostProcessor API を活用しており、C# で書かれたコードをビルド後にIL(中間言語)レベルで自動変換しています。

IL

ILは、.NET における中間言語です。
Unity では、C# で書いたスクリプトは必ずこの IL を経由してからネイティブコードに変換されます。

具体的には、以下のような処理が行われます:

  • NetworkedWeaved という属性を追加して、該当プロパティがネットワーク状態バッファに割り当てられることを明示

  • getter/setter が NetworkBehaviour の内部バッファ(StateBuffer)へのメモリアクセスに差し替えられる

  • 実行時に Spawned() されていない場合のアクセス制限も挿入される

変換前のC#コード:

	[Networked] private Vector2 _position { get; set; }

IL変換後(ILSpyで逆コンパイルしたコード):

    [Networked]  
    [NetworkedWeaved(0, 2)]  
    private unsafe Vector2 _position  
    {  
        get  
        {  
            if (base.Ptr == null)  
            {  
                throw new InvalidOperationException("Error when accessing PhotonCapybara._position. Networked properties can only be accessed when Spawned() has been called.");  
            }  
            return (Vector2)base.Ptr[0];  
        }  
        set  
        {  
            if (base.Ptr == null)  
            {  
                throw new InvalidOperationException("Error when accessing PhotonCapybara._position. Networked properties can only be accessed when Spawned() has been called.");  
            }  
            Unsafe.Write(base.Ptr + 0, value);  
        }  
    }

補足解説:

  • base.PtrNetworkBehaviour に割り当てられたネットワーク状態メモリの先頭ポインタです。

  • base.Ptr[0] はオフセット0の位置からデータを読み書きします(ここでは _position のデータが格納されている)

  • Unsafe.Write() を使うことで、型変換を伴わない高速なバイナリ書き込みが可能になります

  • NetworkedWeaved(0, 2)0 はオフセットインデックス、2 は使用ワード数(32bit単位)を示します

Networked値はどのように同期される?

IL変換された [Networked] プロパティは、NetworkBehaviourのStateBufferに書き込まれるだけでは同期は完了しません
実際には、以下のようなステップで「同期パケット」に含まれて送信されます:

  1. Simulation.Update() 内部で StateBuffer の変化を検出

    • ScanStructForChanges() 関数で、現在の StateBuffer と前回スナップショットを比較します。

    • 差分があれば、それが「更新対象」としてマークされます。

  2. PreparePacket() で同期データをバッファに詰める

    • 差分があるフィールドのみが、送信パケット用バッファに書き出されます。

    • Fusion はこのとき、可能な限り bit圧縮 を行って帯域を節約します。

  3. 送信→受信→復元処理(Deserialize)

    • リモート側では、受信したパケットを元に StateBuffer が上書きされ、プロパティの get 結果が最新値になります。

このように、[Networked] 属性は IL変換 → メモリ書き込み → 差分抽出 → パケット送信 → 同期反映 の一連の流れで、自動的に同期処理が行われているのです。

見えない同期の仕組みを理解することの意味

[Networked] 属性は、一見ただのマークアップのように見えますが、実際にはILレベルで動作を拡張し、パフォーマンスと汎用性を両立した設計になっていることがわかりました。

Fusionを「使う」だけでなく「理解して使う」ためには、こうした仕組みを知ることが非常に重要です。
今後の記事では、さらに深く掘り下げていきます。

本記事はQiitaで公開した内容を、Zenn向けに加筆したものです。

Discussion