UnityのEOS通信ライブラリSynicSugarの使い方

 SynicSugarはEpic Online Servicesを使ったUnity用の通信ライブラリーです。EOSにDeviceIDで認証し、マッチメイク後、p2pで繋ぎ、NAT越えできなければepicのリレーで同期します。Android、iOS、Windows、コンシューマー等で動きます。UDPなのでブラウザでは動きません。また、Apple Storeの初回レビューはTCP環境で行われるため、リジェクト後にレビュワーにudpを試すようにお願いする必要があります。

https://github.com/skeyll/SynicSugar


特長
・EOSに登録するだけで使えて、無料。限度は常識的な範囲ならないそう。
・device ID認証（epicや googleアカウントを使った認証はなし）
・最大64人通信
・マッチメイク（100個までの条件や特定の数字文字など）
・ホストマイグレーション
・再接続
通信でできること
・RPC（第一引数を送信し、他の画面の同idインスタンスで同関数起動）
・Target RPC（第一引数は対象のuserid、第二引数を送信。自分と対象の画面で関数起動）
・SyncVar（同期間隔個別指定可能。言語で用意されてる型、MemoryPackでシリアライズできる型を同期可能）＋NetworkCommonsに関してはホストの変数のみを同期指定可能

[1.無駄な話](#無駄な話)
[2.更新予定](#追加の可能性)
[3.使い方](#使い方)

### 無駄な話
　Sync+Sonic SugarでSynicSugarです。Unityエンジン向けのEOS（Epic online services）の糖衣構文であり、誰でも簡単にオンラインゲーム開発をコンセプトにしています。通信に必要な全てのコードを事前生成して、Sonicの名に恥じぬように高速化を目指しました。ホストマイグレーションと再接続に対応し、リレーやマッチングにEOSを使った無料でオンライン実装できる通信ライブラリです。
　 一応サンプルプロジェクトでは動いていますが、今すぐ無料の対戦ゲームを作りたい方以外には現時点ではあまりおすすめできません。自分のゲームに組み込んでいく作業はこれからなため、ほとんどが想像で作られています。私が開発しているゲームは1v1の対戦ゲームであり、今後少人数の対戦ゲームに関する機能は充実していくはずです。Playfabでデータ回りは管理し、通信はSynicSugarで実装します。似たようなゲーム、構成なら、確実に自分のゲームはこれで通信周りを動かすため、動かないことはないです。また、既存のライブラリはどちらかというとMMOやパーティーゲーム向きで、SynicSugarは最初から少人数の対戦ゲームを想定して作っています。対戦ゲームはより簡単に作れるかもしれません。夏にはゲームが完成し、オーブンベータもリリースも終えているはずで、そのころにはちゃんとしたライブラリになっていてほしいです。

　今でもUnityにはメジャーなオンラインゲーム開発の方法がいくつかあります。まずPhotonが圧倒的な人気を誇っています。SynicSugarの実装や利用方法はあれにわりと近いです。Photonのリレーサーバーを使って通信を行います。SynicSugarではEOSのリレーを使います。また、Mirrorの名前もよく聞きます。MirrorはMMOを想定して作られており、多人数接続の実績もあります。MagicOnionなんかも最近頻繁にアップデートされています。これらはサーバーを自前で借りたりして接続を実現します。
　いくつか選択肢がある中今回なぜ新しく作ったのかというと、EOSを対戦ゲームで使うためです。EOSは一般公開されてから二年たちますがUnityでの使用は国内外ともにほぼなく、無料でオンラインゲームを作れるにもかかわらずEPIC傘下に入ったゲームでしか使われていません。ローンチすぐの頃のイメージのまま誰も触れていないせいですが、頻繁に更新され、フォーラムを見るとUEではちらほらインディーゲームでも採用されているようです。MirrorにもEOSをリレーとして使う方法はあるのですが、ホストマイグレーションはなく、ホストが抜けたら部屋も解散されます。トランスポート部分をEOSに差し替えているだけで、基盤はMirrorの専用サーバー方式です。そのためカジュアルゲームや対戦ゲームでEOSを使う方法は現状ほぼありません。でもEOSがあるから、Photonではなく無料でオンラインゲームを作りたい、そういう経緯です。
　自分がUnityの2021.3を使っているため、ライブラリも最新の環境で作っています。基本的にはソースジェネレーターで属性に応じたすべてのコードを自動生成し、生成したコードを使わないといけない部分に関してはILPostProcesserでILコードを自動で書き換えています。継承などの条件はなく、同期するpubicなpartialなクラスにNetworkPlayerかNetwokCommons属性をつける、その中の同期したいものにもそれぞれSyncVar、Rpc、TargetRpc属性をつけて同期します。

　NetworkPlayerはその他通信ライブラリのNetworkBehaviourです。インスタンスごとにIDを持ち、ネットワーク越しでは基本的にそれぞれのIDのユーザーしか値を直接変更できません。キャラクターのprefabにNetworkPlayer属性のコンポーネントをつけて、ライブラリの生成系APIでインスタンスを作ったり、自由なタイミングでインスタンスを作り自分でidを引っ張ってきてIDを設定して使います。

　一方、NetworkCommonsは全員で共有しているものにつけるイメージです。時間やゲーム進行、敵のオブジェクトなどにこれをつけて全員で値を減らしていくみたいな使い方を想定しています。一応ホストかどうかの情報は持たせていて、ここのSyncVarはホストの値を他に同期する使い方もできます。とくに制約はないので、NetworkCommosだけで全てを実装可能ではあります。ただし誰が呼んだかすら識別しておらず、誰かが呼ぶと他の画面全てで同一処理が呼ばれます。
　
　NetworkPlayerはeosが発行するユーザーIDが必須です。一括生成メソッドを用意しているので、それにprefabを渡すと通信するすべてのユーザーのid付きで生成します。ユーザーidリストから直接IDを拾って設定することも可能です。最初にユーザー生成処理用のnetwork系クラスさえ生成すれば、そこからuserid付きのRPCで生成関数を呼んだりもできます。たとえば対戦シーンに入った時にNetworkCommonsを持ったクラスのインスタンスを一つだけ作り、そこからホストがRPCでid指定してゲームオブジェクトを作っていくこともできます。しかし、各画面で対応するインスタンスがなければ受信はできないので、ホスト経由でキャラクター生成後、ステータスなどの初期値の同期を生成されるNetworkPlayerを通じて行う場合、他のクライアントでNetworkPlayerがまだ生成されていなかった場合はちゃんと同期できません。そのため使いそうなものは通信開始時にローカルで生成することをおすすめします。

　アクションゲームを開発したことがないので使うかは分かりませんが、一応それぞれのインスタンスにpublicなUserIDを保持させていたり、GetInstance(UserId, 取得したいクラスのインスタンス）で対象ユーザーのインスタンスを取得できたりもします。攻撃した対象のIDを拾うみたいなのはいい方法が思いつきませんでした。より適切な方法や必要なものがあれば教えてください。
　オンラインゲームでは同じ条件なら他の画面でも同じ状態だろうと思っているのでシーンに関する条件等もありません
シーンが変わってインスタンスを破棄したり生成したりする場合はどの画面でもそうなるようにしてください。シーンが変わってもライブラリ側で自動破棄などはせず、他の画面では別シーンの同地点を歩行するような処理になります。
　緩めなので他のライブラリでは不可能だったビジュアルスクリプティング系のライブラリとも絡めて使えるはずです。ECSに関してはもう少し理解が深まれば対応できるかもしれません。

　無料で緩めですが、フルメッシュ通信で、10人と通信する場合は１つ同期するのに10回送信、10人から受信する場合は10回受信が必要です。fpsを超えて送受信はできないのは他の方法でも同様ですが、全てが対等に通信し合うところは違います。中央のサーバーやサーバー代わりのPCユーザーだけがたくさん通信して、周りのモバイルや回線環境の悪いクライアントは送受信1回なんてことはないです。そのため64人でマルチサバイバルモバイルゲームなんかは難しいです。モバイルなら30人までぐらいならいけると思います。そのかわりすべてのユーザーが直接結ばれていて間に何も入らないので専用サーバーより早いです。ホストマイグレーションや再接続も簡単です。そのため対戦ゲームには向いています。
　同期する数に条件はありませんがすべて合わせて256種類しか同期できません（うち3つはライブラリの処理をそのうち追加予定）。EOS p2pのパケット送受信には(byte)チャンネルという戻り値があり、それを全てのRPCとSyncVarに割り振っているためです。同期する何か一つ当たり１チャンネルです。多分チャンネルによって処理速度は変わりませんが、若い番号がより早い等あれば、手動でチャンネルを振り分けられるようにするかもしれません。

　実装に関しては認証やロビー部分は基本的にEOSプラグインのラッパー、通信部分は独自の実装です。高速化のための内部的な仕様変更はしますので、より適切な方法があればアドバイスください。


### 追加の可能性
高い
復帰時の必要データ一括送信、ゲームへの途中参加、送信データが大きい時のデータ圧縮、送受信頻度の自動調節、簡易チート判定機能（明らかにチーターっぽいのは複数マッチ様子見して追放するような）、eosのサーバーからデータをダウンロード（通信には関係ない）、内部の高速化など
可能性はあり
VC機能、専用サーバー、LAN内マッチメイク、eos pluginの独自化、より正確な同期
自分ではほぼしない
アンチチートやユーザーデータのようなアカウント認証が必要なEOSの機能

　基本的に自分のゲームをもとに必要な機能追加、修正をしていきます。要望があればユーザー認証等もやりますが、EOSにはサーバーでなにかを処理する機能はないため、単体ではアイテム増殖やスコア変更なども防げません。ですので、データ部分は他のサーバーやサービスを使い、ゲームの通信部分だけで使われることを想定しています。


### 使い方
1.Unityへの対応は2021.3以降
SynicSugarには2021.3以降のUnityが必要です。コード生成のためのソースジェネレーターがそれ以降でしか対応していないためです。SynicSugarはめんどくさい作業を自動でやるだけなので、手動で生成されるものと同一のコードを書けば同様の処理でEOSを使うこともできます。

2.ライブラリのインポート
ライブラリのImportにはOpenUPMを使います。
Unityプロジェクト上で Edit/ProjectSetting/PackageManagerと進みます。そしてScopedRegistriesに
Name: OpenUPM
URL: https://package.openupm.com
Scope(s):
	• net.skeyll.synicsugar
	• com.cysharp.unitask
	• com.cysharp.memorypack
	• com.playeveryware.eos
を登録します。SyncSugar以外は依存ライブラリです。それぞれ非同期処理、送信する値のシリアライズ処理、EOSのプラグインです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/0884f210-ec1a-fec0-b96c-76cf823b7408.png)


 ここで登録したライブラリはWindow/PackageManagerのMyRegistriesからインポートできるようになります。SynicSugarの最新版をインポートすれば依存関係も自動的にインポートされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/aded0aaf-ad64-5f73-5e9e-b11bd6ba513f.png)


 あとはMono.CecilというILを書き換えるためのライブラリも必要になります。C#から中間言語に変換され、その段階でライブラリが生成したコードを使うように自動修正します。
同一ページ内の＋を押し、Add package from git URLにcom.unity.nuget.mono-cecilと入れてください。

 依存関係のあるMemoryPackはSystem.Runtime.CompilerServices.Unsafeというdllも必要としているので、
https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/6.0.0
のDownload packageを押しzipファイルをダウンロード後、解凍して、lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dllをプロジェクト内にインポートしてください。Assets下のどこにおいても大丈夫です。

 これで必要なもののインポートは終わりです。

3.eosの設定
EOSへ登録します。この部分はEOS系の既存の記事と同じなので、詳しくはそちらを参照してください。
https://dev.epicgames.com/ja/news/introduction-to-epic-online-services-eos

アカウント登録後、ゲームの製品ページを作成し、製品設定のクライアントからクライアントポリシーを作ります。EOSには二種類あり、EPICが製品やプライバシーポリシーやサイトなどを確認して、ブランド登録をしたもの、そうでないものがあります。ブランド登録するとアンチチートサービスや認証が必要そうな機能が使えます。また認証画面の警告が消えます。通信やマッチメイク自体はEOSに登録するだけで使え、もちろんリリースもできます。SynicSugarはDeviceID認証というユーザーの同意を得ない、ブランド登録なしで使える機能しか使っていないため、登録後すぐに通信可能です。
クライアントポリシーにはPeer2Peerを使います。最低限必要なものだけを指定する場合はこれらです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/96744e8e-03b4-5954-e56e-38204c72d543.png)


4.EOS pluginの設定
　先ほど作成したEOSのトークン情報をEOSPluginに登録していきます。UnityEditorのToolsからEpicOnlineServicesConfigEditorをクリックし、ProductNameからDeploymentIDまで、DefaultClientCredentialsの三つを登録しGenerateを押し、Save All Changesを押します。ProductNameはゲーム名をバージョンは何でも大丈夫です。それ以外は”SDKのダウンロードと認証情報”のそれっぽいものを設定していきます。EncriptKeyは自由入力です。適当に入れてGenerateしてください。
　Mainだけ登録すればとりあえずは使えます。なおこれらを公開してしまうと自分のEOS経由で通信が行われてしまうので注意です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/00b27f1b-fd08-30f8-12c6-7744cd4db98b.png)

これで通信の準備はできました。

5.ログイン
　ここからは実際の処理に入ります。このあとコードを書く際に、usingでnamespaceがなかったりした場合はEdit/Preferences/ExternalToolsのRegenerateProjectFileを押してください。パッケージを正確に読み込めてないのが原因です。
　EOSはログイン後にTickを定期的に送り、ユーザーが有効かを確認、一時間ごとに新たなTickを発行して認証を継続する必要があります。そうしたものをEOSManagerで管理します。プロジェクトの下部PackagesからSynicSugarを探し、Runtime/Prefabs下からログインするシーンにEOSmanagerをドラックアンドドロップしてください。これはEOSPlugin側の処理で、おそらくシングルトンで、シーンをまたいで処理が引き継がれます。

次にコード側です。
　ログインにはデバイス認証とdev authの二種類あります。
　DeviceID認証は内部的に勝手に行われ、特にユーザーに同意などを求めない方法です。アプリをアンインストールするとデバイスidのデータは消え、そのユーザーとして再接続できなくなります。アプリをアンインストールしたり明示的に削除しない限りはローカルに保存され、同一のユーザーとして認識されます。SynicSugarにおける基本的なログイン方法です。
　DevAuthはEOSの組織に登録したメンバーのアカウントでのみ認証できる開発用のログイン方法です。EOSSDK内のDevAuthToolというのを起動し、認証を済ませると簡単にデバッグ処理ができるようになります。この処理は開発用なのでサンプルプロジェクト内の名前空間下にあります。使う場合はPackageManagerからサンプルをダウンロードする必要があります。
　注意点としてデバイスログインはエディタ、ビルドしたものでも同じpcなら同一idなので、pcだけでテストする時は片方を開発者認証にする必要があります。端末一つでテストする場合は、エディタとビルドしたものでそれぞれDeviceIDとDevAuthの二つ使います。３つでテストする場合は、開発者用のアカウントをもう一つメンバーに登録して、DevAuthを増やす、といった感じです。

ログインは
```csharp
using Cysharp.Threading.Tasks;
using System.Threading;
using SynicSugar.Auth;
public class YourLoginClass : MonoBehaviour {
    async UniTask LoginWithDeviceID(){
        CancellationTokenSource cancellationToken = new CancellationTokenSource();
	bool  isSuccess = await EOSAuthentication.LoginWithDeviceID(cancellationToken);

        if(isSuccess){
            //Success
            return;
        }
        //Failure
    }
}
```

で行います。認証が終わるまでそこでの処理はいったん停止させ、認証が終わったらそれ以下の処理が呼ばれます。SynicSugarは基本すべて非同期処理で書いていますが、以下のように書くこともできます。
```csharp
// using Cysharp.Threading.Tasks;
using System.Threading;
using SynicSugar.Auth;
public class YourLoginClass : MonoBehaviour {
    public void LoginWithDeviceID(){
        CancellationTokenSource cancellationToken = new CancellationTokenSource();
        
        EOSAuthentication.Instance.LoginWithDeviceID(cancellationToken);
        // or　明示的に.Forget()をつけるとちょっとだけパフォーマンスがよくなる
        // EOSAuthentication.Instance.LoginWithDeviceID(cancellationToken).Forget();
        
        //以降の処理はログインを待たずに行われる
        // ...
    }
 }
```
Unityのボタン等のEventにはvoidの関数しか登録できないのでその場合はasyncを呼ぶだけのvoidから非同期の関数をForget()で呼んだりして対処しています。認証自体はよっぽどのことがない限りすぐ終わるので、最初のゲーム画面のStart等で`EOSAuthentication.LoginWithDeviceID(cancellationToken).Forget();`を呼んでおけば、ユーザーが画面なりボタンなりを押して次の画面に行く頃にはおそらく認証も完了しています。きっちりやるのであればSuccessの後で、次のページへの遷移処理できるようなフラグを立てたりします。


6.マッチング
　これ以降の処理はEOSp2pManagerというオブジェクトが必要になります。プロジェクトの下部PackagesからSynicSugarを探し、Runtime/Prefabs下から通信するシーンにEOSp2pManagerをドラックアンドドロップしてください。このオブジェクトはシングルトンで、Don’tDestroyなオブジェクトです。
　通信中はずっと残しておき、通信を終える処理を呼ぶときに一緒に削除されます。MatchMakeとp2p通信の処理がセットになっているのはロビーの再接続や退出の通知をゲーム中にも使うためです。いくつかエディター上で設定もできます。
:::note
上から検索結果数、Lobby作成時の待機時間、再接続処理です。MatchStateはマッチング待機中・・・。マッチング中。。。検索中。。。みたいなのを表示するときに使います。コード側から設定すれば多言語対応できます。
P2pManagerはDelaySendToAllはRpcで送る時のそれぞれのユーザーに送る時のDelay、SyncVarの同期頻度、受信頻度、あとはパケットのタイプです。DelaySendToAllは、現在それぞれのユーザーにLoopで送っているのですが、送信バッファーがいっぱいにならないようにDelayを設けています。ゲームレベルではあふれたりないようであればLobby人数に応じて並列でDelayなしで一気に送るようにするかもしれません。次は同期頻度です。SyncVarは値が変わるたびに送るわけではなく、一番最初に値が変わった後このインターバル間は値が変わっても送信はされません。これはあとのSyncVarを設定するときにも個別指定できます。次は受信頻度です。Apexは各受信ごとに50msのDelayがあるそうです。専用サーバー系のゲームはサーバーから一回で受信してくるのでそれぐらいの頻度ですが、SynicSugarはフルメッシュで10人から受け取るなら10回ループが必要なので倍の頻度をデフォルトにしています。これも自分のゲーム次第で変えていきますが、ゲーム自体のFPSは超えれないのでこれぐらいの頻度でいいのでは、と思っています。次はEpicの通信の設定です。順番通りに送る、ちゃんと届けるために数回送る等の設定ができます。EOSはUDPを暗号化したDTLSで通信しています。TCPよりもパケットが小さく、無駄なやり取りもないため高速ですが、きちんと届くかはわかりません。おそらくだいたいは届きますが、適宜設定してください。
:::


　マッチングとゲームシーンはサンプルに二種類用意しています。マッチング後ゲームシーンに移動する、マッチングとゲームシーンを同じにする等自由にできます。ただし、毎マッチごとにこのManagerを新しく作ったほうがいろいろと楽なのでMenuや待機シーン→Managerを持ったそうしたロビーシーンにいくような構成がいいです。部屋を解散しないのであれば、何回でも同じ相手と通信できます。

コード側の処理です。
　ではマッチングする前に条件を作成していきます。SampleではMatchMakeConditionsというスクリプトで条件を作っています。マッチング関係の処理は
`using SynicSugar.MatchMake;`にあります。まずlobbyを作り、条件となるattributeをaddしていきます。

```csharp
public static Lobby GenerateLobby(string mode = "", string region = "",
                                            string mapName = "", uint MaxPlayers = 2,
                                            bool bPresenceEnabled = false)
```

　Mode、Region、MapNameはEpicの構成にならっています。EPIC本来のC++Sdkには
BucketIdという特別な条件があり、Mode:Region:Mapなどの重要そうな情報BucketIDとして追加
し、最優先の検索条項として使います。C＃SDKではBucketIdは使えないためbucketという一つのattributeとして追加していますが、一応型にならい条件作成時に指定するようにしています。
マッチング自体は近い地域で行われるようなので、不要なものは使わなくても大丈夫です。以下はモードと地域、レベルをGUIボタンで指定しておき、それを使って条件を作成する例です。
```csharp
using SynicSugar.MatchMake;
using UnityEngine.UI;
using UnityEngine;

public class MatchMakeConditions : MonoBehaviour {
	public enum GameMode{ Rank, Casual, Friend }
	public enum Region{ ASIA, EU, NA }
	GameMode mode;
	Region region;
	int level;

	public Lobby GetLobbyCondition(){
	     //Create conditions
	     Lobby lobbyCondition = EOSLobbyExtenstions.GenerateLobby(mode.ToString(), region.ToString());
	            
	     lobbyCondition.MaxLobbyMembers = 2; //2-64
	     LobbyAttribute attribute = new LobbyAttribute();
	     attribute.Key = "Level"; //String
	     attribute.SetValue(level); //int string bool double
	     attribute.comparisonOption = Epic.OnlineServices.ComparisonOp.Equal;
	     lobbyCondition.Attributes.Add(attribute);
		
		//他の条件を追加するときは同様にNewしてAddしていく
	     //attribute = new LobbyAttribute();
	     //attribute.Key = "Level";
	     //attribute.SetValue(level);　
	     //attribute.comparisonOption = Epic.OnlineServices.ComparisonOp.Equal;
	     //lobbyCondition.Attributes.Add(attribute);
	
	     return lobbyCondition;
     }
}
```
　条件にはstring int bool doubleが使え、　
https://dev.epicgames.com/docs/game-services/lobbies#searching-by-attributes
にあるような比較等で結構自由に設定できます。それぞれ値が同じとか含んでるとか小さいとかの条件があります。100個までしか条件追加できません。ルームマッチやフレンドマッチは適当な共通の数字等を入力してその一致でマッチングさせます。

　ではマッチングしていきます。MatchMakeManager.Instance.StartMatchMake(条件, CancelToken);に先ほどの条件を渡します。再接続にLobbyIDを使うのですが、その保存場所にPlayerPrefs以外を使う場合は第三引数で保存処理を渡します。マッチングが成功すればtrue、EOSに問題があったりタイムアウトの時間が過ぎた場合はfalseを返します。
マッチングはデフォルトでは検索し、ロビーがあれば参加、なければ自分で作成するようにしています。独自に検索だけ、作成だけなどもMatchMakeManager内の処理を使ってできます。
```csharp
using Cysharp.Threading.Tasks;
using SynicSugar.MatchMake;
using System.Threading;
using UnityEngine;

public class MatchMake : MonoBehaviour{
	async UniTask StartMatching(){
	     //Prep
	     CancellationTokenSource matchCancellToken = new CancellationTokenSource();
	     //Try MatchMaking
	     bool isSuccess = await MatchMakeManager.Instance.StartMatchMake(matchConditions.GetLobbyCondition(), matchCancellToken);

		if(!isSuccess){
			return;
		}
	
	}
}
```

　EOSではマッチングミスを減らすために送信してからサーバー内でデータを反映するまでに2秒のラグがあります。テスト時は少し待ってからもう片方でマッチングしてください。検索時に条件に一致し、まだ入れるロビーをmanager側の設定の分だけ拾ってきて、上から順番に参加を試みます。入れたらロビー内でロビーが締め切られるのを待ちます。
　部屋を作るユーザーはホストとしてユーザーの参加を待ちます。SynicSugarではホストもゲストもやることはたいして変わらず、唯一SyncVarをホストの値で同期したり、復帰したプレイヤーにホストがデータを送ったりする予定です。
　部屋がいっぱいになるとロビーは自動で閉じられて、ロビーidでしか検索できないようになります。p2pmangaerの useridsに通信相手のidなどが入り、ソケットネームというロビーごとのランダムなキーが設定されます。eosではこのソケットネームとユーザーidを使ってp2p通信を行います。そしてこの段階でこのソケットネームで送られてきたパケットの受信を受け付けるようになります。
　ロビーで名前などの初期値を交換もできますが、手間やサーバーの負荷も考えてロビーではマッチング以外は行っていません。p2pの初回のNAT越え等の時間が長く、そうした初回接続もかねてp2p側で初期値を同期しておいた方がゲーム自体がスムーズに進行できるためということもあります。
　ここまでの一連の処理が終るとtrueが返ってきて、マッチングが完了します。

7.p2p通信
　受信は既に開始しており、もし他のユーザーがデータを送信していた場合、受信したデータは受信バッファーに積まれています。そのためデータを受け取るためのそれぞれのユーザーのインスタンスを作り、パケットをそれらのインスタンスに振り分けるレシーバーをループで回します。
　インスタンスの作り方は結構自由でprefabを渡してspawn処理を呼ぶ、もしくは任意のタイミングで先ほどのuseridsからidを指定して設定します。ただ現在はidのlistが存在しているだけなので、少人数でなければ手動指定は手間かもしれません。useridの設定時にセッタープロパティ内でconnecthubというシングルトンにインスタンスを登録する処理が書かれており、connecthubに登録されます。受信したものはすべてこのConnectHub.Instanceのシングルトンを経由し、パケットに応じたそれぞれのインスタンスに振り分けられていきます。
```csharp
using SynicSugar.P2P;
using UnityEngine;

public class PlayerObject : MonoBehaviour{
	[SerializeField] GameObject playerPrefab;
     public PlayerData localPlayer;
     void Start(){
	     //もし手動で設定する場合はループ等で設定
		//OwnerUserIDを直接指定して設定。これはそのうちInitのプロパティーに変更予定
	     foreach(var id in p2pManager.Instance.userIds.RemoteUserIds){
	  	   PlayerData playerData = new TankPlayerData(){ OwnerUserID = id };
	     }
		localPlayer = new TankPlayerData();
		//設定するための関数も使える
	     localPlayer.SetOwnerID(p2pManager.Instance.userIds.LocalUserId);
	     //実際はplayerPrefabにNetworkPlayerをいっぱいつけておけば全部にIDを設定した状態で生成
	     SynicObject.AllSpawn(playerPrefab);
	     //一応全部生成してから名前を送信。これはSyncVarに設定している
	     localPlayer.PlayerName = ConfigData.PlayerName;
	     //受信準備ができたためパケットレシーバーをスタートする
	     ConnectHub.Instance.StartPacketReceiver();
	}
}
```

　NetworkCommonsも同様にConnectHubに登録する必要があります。こちらはid登録のように最初に全員が共通して呼びそうな処理が思いつかず、どこかに登録処理を挟み込むことができなかったので、現在は手動登録になっています。
```csharp
using UnityEngine;
using SynicSugar.P2P;
[NetworkCommons]
public partial class CommonsObject : MonoBehaviour {
	//どこかの早そうなタイミングでRegisterする
	void Awake() {
	     ConnectHub.Instance.RegisterInstance(this);
	}
}
```

Public partial classならMonoを継承していてもしていなくても、他の何かでも大丈夫です。一通り用意して、データの受信ができそうならConnectHub.Instance.StartPacketReceiver();を呼びます。

　サンプルはアクション風3dと2dターン制の二つ用意しています。なるべくライブラリ内の処理をいろんなパターンで使おうと無駄なことを結構しています。キャラクターがいるようなゲームならprefabに必要なnetworkplayerをつけてspawnが最もシンプルです。
　以下はそれぞれの属性の説明です。
　NetworkPlayerという他のライブラリのNetworkBehaviorのようなもの、NetworkCommonsという全員で共有するものの2種類あります。これらはpublic partial classに属性をどちらかの属性をつけることで同期できるようになります。publicな理由はconnecthubから登録されたインスタンスを振り分ける形で同期をしているからで、partialな理由はソースジェネレータでコードを追加するためです。これらは属性をつけるとそれぞれowneruserid、owneruser、そして同期するものに応じた関数やプロパティが追加されます。追加部分はライブラリがILを書き換えたり挿入する形で使いますが、個別に呼ぶことも可能です。commonsはishost、そして同期するための処理が加えられます。

　Rpcは第一パラメータだけを送信します。もし複数送信したい場合はmemory packで一括送信できます。引数なしでも大丈夫です。第二引数以降は送信しませんが、デフォルトの値を指定すれば設定自体は可能です。呼ばれたら即座に送信します。連続で読んだ場合は連続で送信します。
　TargetRpcは第一引数がuseridで第二引数が送信するパラメーターです。IDが必要な時はray等でOwnerUserIDを取得します。メッセージング等何かいいid取得方法があれば教えてください。なおこの処理は自分の画面とターゲットの画面でしか行われないので、個人メッセージやパーティー間のなにかしらに使えるかもしれません。

　SyncVarは変数を勝手に同期します。これは同期間隔というのが設けてあり、値が変わったら送信され、その後インターバルの間はどれだけ値が変わろうが送信しません。インターバル分経過した時にもう一度値を見て変化していたら送信、していなければ次の変更まではなにもしません。どちらかというとほとんどをローカルで計算し、さほど重要じゃない感じのものや微調整のためにこれを使うイメージです。
　実装はProperyのSetter内で同期のための処理を呼んでいるだけです。そのため、intやstringなどは値を変更すれば同期できます。vector3なんかやmemorypackでシリアライズできるクラスは値を変更する時にnew()でプロパティを通すようにすれば同期されるはずです。
 同期間隔はms指定ですが、小さい数字を指定すれば毎フレーム送信も可能です。2人程度ならかなり自由に同期可能ですが、unityの送受信のバッファーを超えた時に自動調整したりせず、パケットを破棄するようになってます。普通に送ってれば多分大丈夫です。

　通信は暗号化されており、再送処理、接続が切れた場合の再接続処理、リレーへの変更などはEOSSDKが行なっています。まだα版ですが、最低限の機能だけは用意しました。Githubのドキュメントはそのうち用意します。環境構築はややめんどくさいですが、実装はUnityで最も簡単だと思います。そして無料で制限もないです。Epicの目指す万人に開かれたオンラインゲーム開発がゴールです。今後私のゲームに組み込む過程でバグ修正や最適化を行なっていきますが、今ならEOSのドキュメントも一通り頭に入っており、忘れない限りは柔軟に対応できます。ぜひ使ってご意見、ご提案をください。