LucifeP(ルシフェP)の日記

Unityでゲームつくっている人です。

Epic Online Services(EOS) + Mirror を使ったUnityゲーム開発【Part1】

Epic Online Services(以下、EOS)のC#SDKをUnityで使う方法を備忘録として残します。


開発環境

  • Windows 10
  • Unity 2021.3.4
  • C# EOSSDK 1.15.4 (途中でバージョンアップしたので1.14.2からの導入を含みます)
  • Mirror 70.0.0


はじめに

EOSによりサーバーを自分で用意しなくてもオンラインゲームをつくることができます。 EOSは様々なサービスを提供してくれており、そのうちの一つにP2P機能があります。

P2PそのままではUnityでゲーム開発は厳しいですが、 ありがたいことに、EOSを使ってMirrorのTransportを実装してくださった方がいますので使わせていただきます。 これを使うことにより、Mirrorを使ってオンラインゲームの開発が行えます。

今回利用させていただくEpicOnlineTransportのGitレポジトリ github.com

また、今回は利用しませんが、Playeverywhere社開発の公式サンプルもあります。 EOSの使い方で分からないところがあれば参考になります。 github.com


0. Epic Online Services利用準備

EpicGamesアカウントを作成

EOSを利用するためにはEpicGamesアカウントが必要ですので、下記リンクからアカウント作成します。
https://dev.epicgames.com/portal

アプリページを作成

下記リンクよりDeveloper Potalにログインします。
https://dev.epicgames.com/portal

1. ページ左側メニューの 製品を作成をクリックしてアプリページを作成。
製品名は任意の名前を入力します。


2. その後 左側メニューの 製品設定 からクライアントとクライアントポリシーを作成
新規クライアントを追加 をクリックします。
任意のクライアント名を記入して、クライアントポリシーを選択します。
クライアントポリシーは未作成なので、 +新規クライアントポリシーを追加 をクリックします。
任意のクライアントポリシーの名称を記入して、クライアントポリシーの種類にはPeer2Peerを選択して、そのまま保存します。

3. 認証情報の確認
SDKのダウンロードと認証情報 のページ下部に 製品, クライアント, アプリケーション, サンドボックス, デプロイメント のIDが見つかると思います。
これらのIDは後ほど使います。
クライアントとアプリケーションが存在していることを確認してください。


1. EpicOnlineTransport導入

Mirrorのインポート

Unityアセットストアより最新のMirrorのダウンロードしてインポートします。
執筆当時は70.0.0が最新でした。

EpicOnlineTransportのインポート

下記リンクからEpicOnlineTransportの最新のunitypackageをダウンロード後 起動してUnityにインポートします。 github.com

私は1.5.0をインストールしましたが、執筆当時最新の1.5.1をおすすめします。
(1.5.1の変更の取り込みについては後述します)


1.5.0をインストールした場合はエラーが出るので、EosTransport.csを修正します。

1.5.1をインストールした場合でもMirrorのバージョンが新しい場合はServer.csでエラーが出ているかもしれません。


unitypackageからダウンロードすると、 Assets/Mirror/Runtime/Transport/EpicOnlineTransport にEpicOnlineTransportの実装があります。

配置場所がMirrorフォルダ内部になっているため、支障はありませんが管理しづらいため分離します。 今回はAssetsフォルダ直下に_Assetsフォルダを作成してまとまることにしました。

以下のように配置を修正しました。

  • EOS : Assets/_Assets/EpicOnlineTransport
  • Mirror : Assets/_Assets/Mirror

配置を変えた影響で、このままだとエラーが出てしまうので少し修正します。 EOSSDKComponent.csのlibraryPathが文字列でパスを持っているので、変更した配置場所に合わせて書き換えます。

// 修正前
var libraryPath = "Assets/Mirror/Runtime/Transport/EpicOnlineTransport/EOSSDK/" + Config.LibraryName;  

// 修正後
var libraryPath = "Assets/_Assets/EpicOnlineTransport/EOSSDK/" + Config.LibraryName;  


また、EpicOnlineTransport/EOSSDK/iOS/EOSSDK.framework は100MBを超えており、GitLFSの対象になってしまうため、不要であれば削除をおすすめします。 ※EOSSDK.frameworkはiOSビルドに必要になります。


2. 最新のEOSSDK導入

先程インポートしたEpicOnlineTransportには既にEOSSDKが含まれていますが、最新バージョンに更新します。
EOSSDKは EpicOnlineTransport/EOSSDK に置かれています。
EOSSDK/CoreEOSSDK/Generatedソースコードで、残りは各プラットフォームでの実行に必要なライブラリです。

最新バージョンに更新する場合は、CoreとGeneratredを全置き換えとこれらのライブラリを置き換える必要があります。
Windowsの場合はEOSSDK-Win64-ShippingとEOSSDK-Win32-Shippingだけ最低限置き換えれば大丈夫です。

最新のSDKは下記Developer Potalからダウンロードできます。 https://dev.epicgames.com/portal

「0. Epic Online Services利用準備 / 3. 認証情報確認」で開いたページの上部に SDKをダウンロード というボタンが見つかると思います。
SDKの種類としてC#SDKを選択し、任意のバージョンをダウンロードします。
Part1のこの記事では1.14.2で進めています。

※バージョン1.15に破壊的変更があったため、EpicOnlineTransportで使う場合には大規模な書き換え作業が必要になります。
書き換え作業については軽く説明するつもりですが、多少古くてもすぐ動かしたい方はバージョン1.14.2をおすすめします。

C#SDKをダウンロードしたら、前述のとおりCoreフォルダとGeneratedフォルダを丸々置き換えます。
ビルド対象のライブラリも忘れずに置き換えます。
注意点として、ライブラリのPlatform settingsがデフォルトだと以下画像のように両方にチェックされているため、修正します。

修正前の設定(EOSSDK-Win64-Shipping)

修正後の設定(EOSSDK-Win64-Shipping)

EOSSDK-Win32-Shipping はx86にチェックして、x64のチェックを外し、同様にしてください。 Windows用のライブラリに限りませんが、他プラットフォームについては割愛します。


3. Unity側 設定

EosApiKeyを作成

Project内右クリックから、Create/EOS/API Key をクリックして、認証情報を格納するScriptableObjectを作成します。

作成したEosApiKeyを開いて、中の項目を適切に入力します。

- Epic Product Name
「0. Epic Online Services利用準備」で作成したアプリ名を入力します。

- Epic Product Version
任意のバージョンを入力します。

- Epic Product Id
「0. Epic Online Services利用準備 / 3. 認証情報確認」で確認した製品 のI製品Dを入力します。

- Epic Sandbox Id
「0. Epic Online Services利用準備 / 3. 認証情報確認」で確認したサンドボックスサンドボックスIDを入力します。

- Epic Development Id
「0. Epic Online Services利用準備 / 3. 認証情報確認」で確認したデプロイメント のデプロイメントIDを入力します。

- Epic Client Id
「0. Epic Online Services利用準備 / 3. 認証情報確認」で確認したクライアント のクライアントIDを入力します。

- Epic Client Secret
「0. Epic Online Services利用準備 / 3. 認証情報確認」で確認したクライアント のクライアントシークレットを入力します。

EOSSDKComponenetをシーンに作成

初期ロードシーンに空オブジェクトを作成し、EOSSDKComponentをアタッチします。
インスペクター上でApiKeys欄にEosApiKeyをセットします。

後のために、EOSSDKComponentのDelayed Initializationにチェックを入れておきます。


ここまでで最低限のセットアップは完了です。


4. ログイン

ログイン方法の種類

EOSへのログイン方法はいくらかあります。

  • EpicGamesアカウント等を使うアカウント認証
  • バイスIDを使う認証
  • DevAuthToolを使う認証 (デバッグ専用)

DevAuthToolを使う認証は、デバッグ専用の認証方法です。
ツールはダウンロードしたSDK内の
SDK/Tools/EOS_DevAuthTool-win32-x64- XXX.zip を解凍して起動できます。
予めツール内に複数のアカウントをログインさせ、CredentialNameと紐付けておくことで、ログイン処理を簡略化できます(後述します)。

バイスIDを使う認証は、最も簡単で扱いやすい認証方法です。
初めてデバイスIDの認証行う際に、PC内にデバイスID情報が1つ作成されます。
以降は意図的に削除しない限り、このデバイスID情報を用いてログインを行います。
削除は専用のAPIを呼び出すことや、Windowsの資格情報マネージャーのWindows資格情報にeos-sdk:XXXXXXXXXのような形で保存されているため削除することができます。

iOSAndroidの場合は端末内にデバイスID情報が作成され、これはアプリのアンインストールと共に削除されます。
保存場所は未調査です。
ご存知の方いましたらコメントで教えていただけますと幸いです。

今回は扱いやすさを重視して、基本的にデバイスIDを使う認証で進めていきます。

ログイン処理
EOSSDKComponentシングルトン

ログインを行う処理を書いていきたいのですが、EOS実装のコアとなるEOSSDKComponentクラスを扱いやすくするためにシングルトン処理を書き替えます。
書き換えは必須ではありませんが、以降のスクリプトは置き換え後になりますのでご了承ください。

// 変更前
protected static EOSSDKComponent instance;
protected static EOSSDKComponent Instance
{
    get
    {
        if (instance== null)
        {
            return new GameObject("EOSSDKComponent").AddComponent<EOSSDKComponent>();
        }
        else
        {
            return instance;
        }
    }
}

// 変更後
protected static bool Exist => _Inst != null;
protected static EOSSDKComponent _Inst;
public static EOSSDKComponent Inst
{
    get
    {
        if (_Inst != null) return _Inst;

        _Inst = FindObjectOfType<EOSSDKComponent>(true);
        return _Inst;
    }

    set { _Inst = value; }
}


DontDestroyOnLoad

EOSSDKComponentがシーン遷移で破棄されないようにDontDestroyOnLoad指定を行います。 Awakeのあとシングルトンの変数にインスタンスに代入した後で設定します。

private void Awake()
{
    // Prevent multiple instances
    if (_Inst != null)
    {
        Destroy(gameObject);
        return;
    }
    _Inst = this;
    DontDestroyOnLoad(gameObject);


EosLogin.cs

実際のログイン処理を扱うEosLogin.csを作成します。
次の項目のUI画面と合わせてみて頂くとわかりやすいかもしれません。

機能としては以下のとおりです。

  • 認証方法の選択
  • ログインボタンクリック時にEOS初期化処理呼び出し
  • EOS初期化終了後のコールバック設定

DevAuthToolを使う場合は、事前ログインしたアカウントに紐付ける名前を決めることができますが、このとき決めた名前をCredentialNameとして渡します。
recreateDeviceIdはデバイスID情報の再生成用のフラグです。
削除処理は後ほどEOSSDKComponentに追記します。

作成したら、空のオブジェクトを作成してアタッチしておきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using System;
using UnityEngine.UI;
using Epic.OnlineServices.Auth;
using EpicTransport;
using Epic.OnlineServices;
using UnityEngine.SceneManagement;

namespace Sakineko.Sample
{
    public class EosLogin : MonoBehaviour
    {
        [SerializeField] private TMP_Dropdown eosLoginType;     // ログイン方法を選択
        [SerializeField] private Button loginButton;            // ログインボタン
        [SerializeField] private TMP_InputField credentialName; // CredentialName(※これはDevAuthToolによるログインで使う)
        [SerializeField] private Toggle recreateDeviceId;       // デバイスIDを作り直すかどうか(※これはデバイスIDによるログインで使う)

        [Space]
        [Mirror.Scene]
        public string nextScene; // ログイン後に遷移するシーン

        private void Awake()
        {
            // Dropdown
            var options = new List<TMP_Dropdown.OptionData>();
            foreach (var str in Enum.GetNames(typeof(EosLoginType)))
            {
                options.Add(new TMP_Dropdown.OptionData(str));
            };
            eosLoginType.AddOptions(options);

            // Callback
            loginButton.onClick.AddListener(OnLoginButtonClicked);

            var eos = EOSSDKComponent.Inst;
            eos.onLoginSucceed += OnLoginSucceed;
            eos.onLoginFailed += OnLoginFailed;
        }

        private void OnLoginButtonClicked()
        {
            var eos = EOSSDKComponent.Inst;
            var loginType = (EosLoginType)Enum.ToObject(typeof(EosLoginType), this.eosLoginType.value);

            switch (loginType)
            {
                // アカウントを使う認証
                case EosLoginType.Account:
                    eos.authInterfaceLogin = true;
                    eos.authInterfaceCredentialType = LoginCredentialType.AccountPortal;
                    eos.connectInterfaceCredentialType = ExternalCredentialType.Epic;
                    break;
                // DevAuthToolを使う認証
                case EosLoginType.DevAuthTool:
                    eos.authInterfaceLogin = true;
                    eos.authInterfaceCredentialType = LoginCredentialType.Developer;
                    eos.connectInterfaceCredentialType = ExternalCredentialType.Epic;
                    eos.devAuthToolCredentialName = credentialName.text;
                    break;
                // デバイスIDを使う認証
                case EosLoginType.DeviceID:
                    eos.authInterfaceLogin = false;
                    eos.authInterfaceCredentialType = LoginCredentialType.Developer;
                    eos.connectInterfaceCredentialType = ExternalCredentialType.DeviceidAccessToken;
                    eos.recreateDeviceId = recreateDeviceId.isOn;
                    break;
            }

            Login();
        }

        private void OnLoginSucceed()
        {
            Debug.Log($"OnLoginSucceed");

            SceneManager.LoadScene(nextScene);
        }

        private void OnLoginFailed()
        {
            Debug.LogError($"OnLoginFailed");
        }

        private void Login()
        {
            // EOSの初期化処理
            EOSSDKComponent.Initialize();
        }

        public enum EosLoginType
        {
            DeviceID,
            DevAuthTool,
            Account,
        }
    }
}
UI配置

ログイン方法の選択用としてDropDown,
DevAuthToolのCredentialName入力用にInputField,
ログインボタンとしてButton,
バイスIDの削除用としてToggleを配置します。

nextSceneにはログイン後のシーンをセットします。
ここでは0002_Sample_Home としました。

EOSSDKComponent.csにコールバック追加

EOSSDKComponent.csにログイン成功時と失敗時のコールバックを追加します。

  • 成功時のコールバック onLoginSucceed
  • 失敗時のコールバック onLoginFailed


EOSSDKComponentクラスに変数追加

public Action onLoginSucceed;
public Action onLoginFailed;


OnAuthInterfaceLoginメソッドに追加

private void OnAuthInterfaceLogin(Epic.OnlineServices.Auth.LoginCallbackInfo loginCallbackInfo)
{
    if (loginCallbackInfo.ResultCode == Result.Success)
    {
        Debug.Log("Auth Interface Login succeeded");

        string accountIdString;
        Result result = loginCallbackInfo.LocalUserId.ToString(out accountIdString);
        if (Result.Success == result)
        {
            Debug.Log("EOS User ID:" + accountIdString);

            localUserAccountIdString = accountIdString;
            localUserAccountId = loginCallbackInfo.LocalUserId;
        }

        ConnectInterfaceLogin();

        // Change to next scene
        onLoginSucceed?.Invoke();
    }
    else if (Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode))
    {
        Debug.Log("Login returned " + loginCallbackInfo.ResultCode);
                
    // Faile to login 
        onLoginFailed?.Invoke();
    }
}


OnConnectInterfaceLoginメソッドに追加

private void OnConnectInterfaceLogin(Epic.OnlineServices.Connect.LoginCallbackInfo loginCallbackInfo)
{
    if (loginCallbackInfo.ResultCode == Result.Success)
    {
        Debug.Log("Connect Interface Login succeeded");

        string productIdString;
        Result result = loginCallbackInfo.LocalUserId.ToString(out productIdString);
        if (Result.Success == result)
        {
            Debug.Log("EOS User Product ID:" + productIdString);

            localUserProductIdString = productIdString;
            localUserProductId = loginCallbackInfo.LocalUserId;
        }

        initialized = true;
        isConnecting = false;

        var authExpirationOptions = new Epic.OnlineServices.Connect.AddNotifyAuthExpirationOptions();
        authExpirationHandle = EOS.GetConnectInterface().AddNotifyAuthExpiration(authExpirationOptions, null, OnAuthExpiration);
                
    // Change to next scene
        onLoginSucceed?.Invoke();
    }
    else if (Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode))
    {
        Debug.Log("Login returned " + loginCallbackInfo.ResultCode + "\nRetrying...");
        EOS.GetConnectInterface().CreateUser(new Epic.OnlineServices.Connect.CreateUserOptions() { ContinuanceToken = loginCallbackInfo.ContinuanceToken }, null, (Epic.OnlineServices.Connect.CreateUserCallbackInfo cb) =>
        {
            if (cb.ResultCode != Result.Success) { Debug.Log(cb.ResultCode); return; }
            localUserProductId = cb.LocalUserId;
            ConnectInterfaceLogin();
        });
    }
}
バイスID削除処理

バイスID削除処理をEOSSDKComponentクラスに追加します。


EOSSDKComponentクラスに変数追加

[NonSerialized] public bool recreateDeviceId;


InitializeImplementationメソッドに削除処理を追加
以下の記述をEOS = PlatformInterface.Create(ref options);より後に追加します。

if (recreateDeviceId)
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
    var deleteDeviceIdOptions = new Epic.OnlineServices.Connect.DeleteDeviceIdOptions();
    EOS.GetConnectInterface().DeleteDeviceId(ref deleteDeviceIdOptions, null, OnDeleteDeviceId);
#else
    Debug.LogWarning($"Delete devideID is valid only development build or UnityEditor.");
#endif
}


InitializeImplementationメソッドのデバイスID作成処理に条件を追加

// 変更前
if (connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken)

// 変更後
if (recreateDeviceId && connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken)


5. 実際にログインしてみる

Unityエディタを実行して、ドロップダウンからDeviceIDを選択して、Loginボタンをクリックします。
※初回ログイン時はReCreateDeviceIDのチェックをONにします。(そうしないとデバイスID情報が作成されないように作りました)

無事に事前に設定しておいたnextSceneに遷移できれば成功です。



次回はMirror側の実装に移ります。


Part2

lucifep-angel.hatenablog.com