【Unity】実質3分でオンラインゲームを作る方法、SynicSugar最小構成

# オンラインゲームを作る
SynicSugarというUnity用のネットワークライブラリを書いています。
EpicOnlineServicesをバックエンドとして使っているため、CCU制限や料金などなしでオンラインゲームを作れます。制限としてはEpicがギャンブル系のゲームは禁止していることぐらいです。

https://github.com/skeyll/SynicSugar


https://dev.epicgames.com/ja

今回は最小構成でハローワールドを送るまでの処理を書きます。


## 環境構築
3分と言いましたが、ここは若干時間がかかります。プロジェクトとEPICのアカウントを作って、ライブラリのインポートなどを行います。5分〜ぐらいかかるかもしれません。

### 1 プロジェクトを作る
EOS Pluginがまだ6に正式対応していないので**21か22でプロジェクト**を作ります。
6の場合は若干手間がかかるので、今回はどちらかでお願いします。

### 2 パッケージのインポート
#### OpenUPM
ダウンロードはOpenUPM経由で行います。**ProjectSetting/PackageManager**にOpenUPM経由でダウンロードするものを指定します。
Name: OpenUPM
URL: https://package.openupm.com
Scope(s):
	• net.skeyll.synicsugar
	• com.cysharp.unitask
	• com.playeveryware.eos
	• com.cysharp.memorypack
    
![import.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/218f2163-a4b6-fb72-7ea5-a0766987f820.png)

#### その他
OpenUPMにないMonoCecilとMemoryPackで使うDllをインポートも必要です。
これは以下をフォルダごとダウンロードして、そのままプロジェクトに突っ込むか、正式な手順を踏んでダウンロードします。

https://github.com/skeyll/SynicSugar/tree/main/SynicSugar/Assets/Plugins


以下は正規の場所からダウンロードする方法です。
##### MonoCecil
まずはMonoCecilをインポートします。**PackageManagerのプラス**を押して、**Add package git URL**に**com.unity.nuget.mono-cecil**と入れてください。
![MonoCecil.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/dcb30913-cb90-64df-2250-6f285af9deee.png)

##### System.Runtime.CompilerServices.Unsafe
MemoryPackが追加のDLLが必要なのでダウンロードします。

https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/6.0.0 


からダウンロードして、解凍し、**lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll**のdllをAssets下にドラッグ＆ドロップしてください。

これで必要なものは全てインポートできました。

### EOSのキーなどを設定
EOSを利用するのにプロダクトキーなどが必要です。

#### アカウント作成
まず、エピックアカウントを作って、EpicDeveloperPortalにアクセスしてください。

https://dev.epicgames.com/portal/ja/?lang=ja

画面左側のメニューより製品を作成より、ゲーム名を入れて新しい製品を作ります。後から名前を変更できるので適当でも大丈夫です。

![portal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/64ce4357-09b5-eb9a-b21d-867348624274.png)

#### ポリシーの設定
EOSでは求める権限のことをクライアントポリシーといい、そのポリシーを設定したものをクライアントと呼んでいます。クライアントごとにKeyが設定されており、それを使い分けることで開発環境や実行環境によって求める権限を変更できます。
先ほど作ったゲームをクリックし、下の方にある**製品設定**、**クライアント**と移動します。

![policy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/e62e6d38-6fd4-0e8f-6292-84910635130f.png)

まずは、**新規クライアントポリシーを追加**を押し、名称は適当に、ポリシーには**Peer2Peer**を選んでください。
その後、**新規クライアントを追加**より、クライアント名は適当に、**クライアントポリシーには先ほど作ったもの**を選択して、作成します。その他の部分は自分のサーバーを認証サーバーに使う際、必要なものなのでそのままで大丈夫です。

:::note info
このクライアントごとに割り振られたIDを使い分けることで、たとえばモバイルやPCなど配布プラットフォームごとで付与する権限を変更できます。なお、クライアントが違っても通信は行え、ポータルで表示される統計上以外の区別はありません。
一人でもログインしていたらクライアントを変更・削除することができませんが、バージョンごとにこのクライアントを分けておき、EOS自体のサーバメンテ時に誰もダウンロードしていない状態でこのクライアントを削除、変更することで過去バージョンの停止機能を擬似的に実装できます。
また、SynicSugarの全ての機能がEpicに登録するだけで使えますが、イージーアンチチートやEPIC Storeなどを利用する場合は登録後、ゲームの認証を受ける必要があります。
:::

#### キーの入力
EOS上で必要なものは以上です。**Unityプロジェクトでキーを設定**していきます。
**Tools/EOS plugin/EOS Configuration*に、EOSの**製品設定SDKのダウンロードと認証情報**というところにあるIDを設定していってください。必要なものは以下のとおりです。

![EOS Config.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/b5c4c7db-dc92-d430-253a-2b80ae3e6009.png)

EncryptionKeyはPlayerStorageを使う際にEpicがファイルの中身を見れないように暗号化するためのKeyです。今回は使わないので不要ですが、Generateを押すとPluginによってランダムな鍵が作られます。

:::note info
クライアントやデプロイメントが選択できるのは基本的に統計上の区分がメインです。詳細なデータを取得したい場合はサンドボックス/デプロイメントより作って新規サンドボックスIDを作ってみてもいいかもしれません。
:::


#### ビルド設定（Android向け）
もし、**Android向けにビルドする場合は追加で設定が必要**になります。ビルド時にEOS SDKを別途ビルドするための設定です。
**Unity21+22.33**までかそれ以降かでUnity自体の外部パッケージビルド方法が変わっており、**22.34より新しい場合はさらに設定が必要**になります。

まず、**ProjectSettingのOtherSetting**を開き、ScriptBackEndをMonoからIL2CPPにし、もう少し下の方にあるAllow unsafe Codeにチェックを入れます。ここまでは共通です。

![Project Setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/b7001f67-44a0-ab6c-80a6-92863b0d1ba8.png)


以下は**Unity22.34以降**の場合の作業です。EOSSDKをビルドしてパッケージに含めるために必要になります。
まず、Arm7のチェックを外して**Arm64**にチェックを入れてください。
これらはプラグイン自体はArm7環境で開発されているため、Arm7からチェックを外さなければデフォルトでArm7向けにビルドされてしまいます。（ポケGOも最近Arm64のみに絞ったので、外すことによる影響はあまりないはずです。）

![スクリーンショット 2025-01-18 12.36.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/5e430c72-6639-73b4-f3ad-d8e4d9eaeb5d.png)

そして、**Publishing Settings**より**Custom Gradle Properties Template**にチェックを入れます。
ビルド時するためのSDKを指定しています。そして対象ファイルを開いて以下のように変更します。

```gradleTemplate.properties
unityStreamingAssets=**STREAMING_ASSETS**
org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M
org.gradle.parallel=true
**ADDITIONAL_PROPERTIES**
android.enableJetifier=true
android.useAndroidX=true
android.suppressUnsupportedCompileSdk=35
```

初期設定は以上になります。
もし、この作業をする前にビルドをしてしまった場合、プロジェクトを再起動してビルドをして下さい。Unityがビルド効率化のために外部パッケージのビルド周りのキャッシュデータを再利用するので、しばらくの間変更が反映されません。


## RPCを送る
ここからはプロジェクトでの作業です。

### ログインや通信に使うシングルトンをシーンに追加する
Scene上のヒエラルキーで右クリックし、SynicSugarからSynicSugarManagerとNetworkManagerを作成して下さい。
![スクリーンショット 2025-01-13 12.17.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924887/c1e3f0fe-822f-f553-57ba-508742b854fc.png)

SynicSugarManagerはログインするような基本的なページに、NetworkManagerは通信を行うシーンに作成します。両方シングルトンなので、一つ作ればシーンに跨って同じものが使用されます。実行時は重複していれば勝手に削除されるので全てのシーンに作成しても大丈夫です。

### 基本編: ハローワールド
Minimum1というスクリプトを追加し、以下のコードをコピペしてください。

```Minimum1.cs
using UnityEngine;
using Cysharp.Threading.Tasks;
using SynicSugar;
using SynicSugar.Auth;
using SynicSugar.MatchMake;
using SynicSugar.P2P;

[NetworkCommons]
public partial class Minimum1 : MonoBehaviour
{
    async UniTaskVoid Start()
    {
        //ログイン
        //SynicSugarでは匿名でサーバーにログインします。
        //ベースとなる匿名IDにその他のアカウント情報を結びつけて使います。
        var result = await SynicSugarAuthentication.Login();
    
        if(result != Result.Success)
        {
            Debug.LogError($"ログイン失敗 {result}");
            return;
        }
        Debug.Log("ログイン成功");

        //マッチメイク
        //地域やモードなど重要な情報をbucketとして指定しロビーを作成します。ここの条件はやや高速で検索できます。
        //別途属性なども追加で指定できます。
        //参加人数やVCを使うかどうかもここで指定します。デフォルトでは二人で、VCなしです。
        string[] bucket = new string[1]{"Test"};
        Lobby lobby = MatchMakeManager.GenerateLobbyObject(bucket);
        //条件を付与したロビーを渡してマッチメイキングします。
        //APIにはホストゲスト関係なくマッチングする方法と、検索のみ、作成のみの方法があります。
        //今回は検索後、部屋がなければ作成する方法を使います。
        //なお、EOSのサーバーはAPIを呼び出した後サーバーに反映されるまで3秒ぐらいラグがあるので気をつけてください。
        result = await MatchMakeManager.Instance.SearchAndCreateLobby(lobby);

        if(result != Result.Success)
        {
            Debug.LogError($"マッチメイク失敗 {result}");
            return;
        }
        Debug.Log("マッチメイク成功");

        //P2P
        //この段階でp2p接続はされています。
        //しかし、なんでも自由に送受信できるわけではなく、NetworkPlayerもしくはNetworkCommonsを付与したクラスからRPCやSyncVar経由などでやりとりします。
        //今回はNetworkCommonsという誰でもデータを送れるシステム周りを扱うことを想定したNetworkObject経由で送ります。
        //[NetworkCommons]をつけ、クラス自体もpublic partial classにしてください。

        //インスタンスの登録
        //ConnectHubというシングルトンがサーバーのような役割をします。NetworkObjectはここに登録する必要があります。
        //NetworkCommonsはRegisterInstanceでここ経由でリモートで行われた処理を行うようになります。
        //NetworkPlayerはオブジェクトにUserIDを設定時に内部的に登録するようになっています。
        ConnectHub.Instance.RegisterInstance(this);

        //パケットを受信する
        //実際に受け取ったパケットの処理を行うオブジェクトを準備した後、パケットを受信します。
        //先にパケットを受信してしまうと、受け取ったパケットの処理を行うオブジェクトがないため、エラーが発生します。
        ConnectHub.Instance.StartPacketReceiver();

        //RPCを送信する
        //同期したい処理に[Rpc]をつけると、リモートでも実行されるようになります。
        //第一引数までは同期することが可能です。二つ目以降はデフォルト値を設定することで指定できますが、送信はされません。
        //MemoryPackで送れる型であれば送信可能で、LargePacketでない通常のパケットの最大サイズは1170ｂｙteです。
        //SyncVarに関してはホストの値だけを同期するかどうかを選べますが、基本的に誰が送信したかも分からないので今回はUserIdも一緒に送ります。
        SendHelloWorld(p2pInfo.Instance.LocalUserId);
    }

    [Rpc]
    public void SendHelloWorld(UserId id)
    {
        Debug.Log("Hello World from " + id);
    }
}
```
今回はNetworkCommonsという誰でも呼び出せる処理で同期しているので、誰が呼び出したかわかるように引数にUserIdを渡しています。
もし、実行中に何をしているか分かりにくい場合は**ProjectSettingのシンボル**に**SYNICSUGAR_LOG**と入れて下さい。API呼び出しごとに逐一LOGを出すので処理の流れがわかりやすくなるはずです。

最後に、上記のコードをシーン上のオブジェクトにアタッチしてください。カメラでも空のGameObjectを新規で作成してもアタッチしてもかまいません。
これでゲーム開始後、ログインを行い、マッチメイキングし、相手のクライアントへメッセージを送受信します。

:::note warn
認証にDeviceIdを使用しているので、片方を別端末にする必要があります。
:::


:::note info
SynicSugarにはネットワークオブジェクトが２種類あります。
NetworkCommonsというシステム周りの処理を書くような誰でも呼び出せるもの、
NetworkPlayerというUserIdを持ち、オブジェクトの所有者しか同期処理を呼び出せないものです。
:::


### 発展編: Commons経由でNetworkPlayerを作成してハローワールド
以下のコードは発展編です。NetworkCommons経由でNetworkPlayerを生成して、生成が完了したというメッセージを送ります。

マッチング後、ゲームに必要なオブジェクトは相手からの情報を待たなくてもわかるはずなので、各ローカルで勝手に作っても良いのですが、Photonがネットワーク越しに生成するように、NetworkCommons経由で丁寧に生成することも可能です。

```Minimum2.cs
using UnityEngine;
using Cysharp.Threading.Tasks;
using SynicSugar;
using SynicSugar.Auth;
using SynicSugar.MatchMake;
using SynicSugar.P2P;

[NetworkCommons]
public partial class Minimum2 : MonoBehaviour
{
    async UniTaskVoid Start()
    {
        //ログイン
        var result = await SynicSugarAuthentication.Login();
    
        if(result != Result.Success)
        {
            Debug.LogError($"ログイン失敗 {result}");
            return;
        }
        Debug.Log("ログイン成功");

        //マッチメイキング
        //条件作成
        string[] bucket = new string[1]{"Test"};
        Lobby lobby = MatchMakeManager.GenerateLobbyObject(bucket);
        //部屋がたったタイミングがわかりにくので、マッチング中に通知を出します。
        //GUIテキストを渡してそこに表示させる方法もあるのですが、
        //今回はロビー作成後にマッチングのキャンセルやホストのキックが可能になったタイミングでLogを出すことにします。
        MatchMakeManager.Instance.MatchMakingGUIEvents.EnableCancelKick += () => Debug.Log("待機中");
        //マッチメイク
        result = await MatchMakeManager.Instance.SearchAndCreateLobby(lobby);

        if(result != Result.Success)
        {
            Debug.LogError($"マッチメイク失敗 {result}");
            return;
        }
        Debug.Log("マッチメイク成功");

        //P2P
        //今回はNetworkCommonsで自分のNetworkPlayerを作り、そこ経由でHelloWorldを送ります。
        //こんな感じでNetworkCommons経由で作成しなくても、各ローカルで全員分のオブジェクトをSynicObject.AllSpawn(object)で作ることも可能です。

        //インスタンスの登録
        ConnectHub.Instance.RegisterInstance(this);

        //レシーバーの開始
        ConnectHub.Instance.StartPacketReceiver();

        //RPCを送信する
        InstantiatePlayerObject(p2pInfo.Instance.LocalUserId);
    }

    [Rpc]
    public void InstantiatePlayerObject(UserId id)
    {
        Debug.Log(id + "がPlayerを作成");
        new Player(id);
    }
}
[NetworkPlayer(true)] //trueにしておけば後からConnectHub.Instance.GetUserInstance(UserId)で参照を取れます。
public partial class Player
{
    public Player(UserId id){
        //NetworkPlayerはUserIdを持っており、そのIDを持ったローカルユーザーが呼び出さないとリモートでは実行されません。
        //ユーザーID設定時にConnectHubに登録されます。SynicObject経由で作成した場合は自動的にIDの設定や登録まで行われます。
        //今回はコンストラクタ内でIDの設定やRPCの送信まで行います。
        //UserIdの設定（インスタンスの登録）
        SetOwnerID(id);

        //RPC
        int RandomValue = Random.Range(0, 100);
        int RandomValue2 = Random.Range(0, 100);
        //if(isLocaｌ)などをつけていないので、この処理は3回呼び出されます。
        //つまり、Playerを作った時に合計２回呼ばれて、その後リモート経由でRPCとして２回呼び出されます。
        //RPC経由で呼び出されたものは第一引数以外受信していないことを確認して下さい。
        //もし、たくさんデータを送りたいならMemoryPackをつけてクラスにしてください。300KBまでは送れるので小さな画像ぐらいならやり取り可能です。
        HelloworldFromPlayer(RandomValue, RandomValue2, true);
    }
    
    [Rpc]
    public void HelloworldFromPlayer(int randomInt, int randomInt2 = 0, bool isLocal = false){
        Debug.Log($"Hello World from {OwnerUserID} / {randomInt} / {randomInt2} / ローカルで呼ばれたか？ {isLocal}");
    }
}
```

## 終わりに
#### SynicSugarの概要と特徴
SynicSugarは、モバイル、PC、コンシューマーに対応したオンラインゲーム向けのネットワークライブラリです。現在、ブラウザには対応していませんが、将来的にはWebRTCなどのトランスポートを実装し、ブラウザ対応を目指しています。また、ランタイム中にサーバーを変更できる機能も追加予定です。
今後のアップデート予定は
- Unity 6への対応
- マッチメイク機能の拡張（ロビーを締め切らずにゲームを開始したり、再開したりなど）
- 新たなトランスポートの実装
です。

現在、Unity向けに提供されているEOS関連のトランスポートは、多くがバグを抱えているか、最新のUnityバージョンに対応しておらず、使用が困難な状況です。そのため、UnityでEOSを活用するには以下の2つの選択肢があります。
- SynicSugarを利用する
- EOS SDKまたはそのラッパーライブラリを直接使用する


#### SynicSugarの強み
EOSを捨てるとして、開発が活発なNetcodeでトランスポートをPhotonにするというのもありかもしれません。CCU100分の無料枠と、将来はトランスポートを切り替えて自サーバーを専用サーバーにして運営できます。
しかし、将来的にも専用サーバー方式を利用しない場合はSynicSugarにも利があります。SynicSugarは、最初からサーバーレスP2P通信を前提に設計されており、以下のような基本機能を提供します。
- ホストマイグレーション
- 接続切断後の復帰機能と必要データの自動同期

昨今のオンラインゲームとライブラリはスケーラビリティやチート対策、同期の正確さなどAAA向けの機能を追求して開発されることが多いため、こうしたサーバーがないと必要にならない機能は追加されません。SynicSugarが今後100人同時通信に対応することはありませんが、個人開発者やインディーゲーム開発者が求める機能は提供できます。

EOSを使ってみたい方、無料でオンラインゲームを作りたい方はぜひSynicSugarを使ってみてください。感想、アドバイス、欲しい機能などお待ちしております。


ドキュメント

https://skeyll.github.io/SynicSugar/