Connect インターフェースを使用して EOS ゲーム サービスにアクセスする

Rajen Kishna、Epic Games テクニカル アカウント マネージャー |
2021年11月16日
この Epic Online Services 入門編シリーズ の前回までの記事を読んでいない場合は、先週の Friends インターフェース API に Epic アカウント サービスを使用するときのまとめがあります。ゲーム サービスを掘り下げていく準備ができましたが、Connect インターフェース をまず確認し実装する必要があります。

このインターフェースは、名前が示すとおり、サポート対象の認証済み ID を EOS 製品ユーザー ID (PUID) に関連付けるために使用します。この ID はすべてのゲーム サービスで製品固有のプレイヤー識別子として使用されます。重要な点は、ゲーム サービスと PUID は Epic アカウント サービスおよび Epic アカウントから切り離されていることです。

実行する場合は、必ず Epic Online Services 1.14.1 SDK を使用してください。Connect フローと Epic アカウント サービスが連動するしくみに変更が加えられているからです。この記事では、次の内容を扱います。
 

Auth インターフェースと Connect インターフェースの比較 (概要)

これらのインターフェース間の違いについては、Epic アカウント サービス (EAS) での認証 の記事で詳しく解説しました。その概要は、Auth インターフェース は Epic アカウントを認証して、プレゼンスやフレンド情報で Epic アカウント サービスを使用する、さらに Epic Games ストアで購入するために使用されます

Connect インターフェース は今後このシリーズで使用するもので、使用する製品の各プレイヤーに対する固有の製品ユーザー ID (PUID) を生成し、マルチプレイヤー、進行状況、ゲームの運用をカバー するゲーム サービスを使用します。これらはそれぞれ独立して使用でき、Connect インターフェースは サポート対象の ID プロバイダ とともに使用できます。Epic アカウントを使用する必要はありません。

Connect の認証フロー

このサンプル アプリでは、Epic アカウントを使用する Connect API インテグレーションを実装します。このコードは実装済みだからです。ただし、先に進む前に、サポート対象 ID プロバイダを使用するために、どのようなフローで実装されているのかを順に確認します。
 
  1. ゲームでプレイヤーに確認するのは、ID プロバイダ (A) を使用して Connect.Login() を通じて認証するかどうかです。
  2. 指定 ID プロバイダで対象プレイヤーに対する製品 ID (PUID) が見つかった場合、このコールバックの ResultCode が Result.Success になり、コールバックが PUID を LocalUserId メンバーに返します。
  3. PUID が見つからない場合、ResultCode が Result.InvalidUser になり、コールバックは ContinuanceToken を返します。この時点で、複数の ID プロバイダをサポートしている場合に、プレイヤーが他のサポート ID プロバイダを使用して認証するかどうかをプレイヤーに確認できます。 これを実行するのは、セカンド PUID が作成され、進行状況データが新しいものとしてそれに関連付けられないようにするためです。この場合は後でアカウントをマージする必要があります。他の方法として、この ID を引き続き使用するのかを確認します (それから新しい PUID を作成)。
  4. ユーザーがこの ID をそのまま使用することを希望する場合、Connect.CreateUser() を使用して、ContinuanceToken を CreateUserOptions を通じて渡します。このとき ResultCode が Result.Success であれば、PUID が LocalUserId メンバーに見つかります。
  5. ユーザーが異なる ID プロバイダ (B) での認証を希望する場合、この ContinuanceToken を保存し、再び Connect.Login() を ID プロバイダ B を使用して呼び出します。ログインに成功した後、両方の ID をリンクするかどうかをユーザーに確認できます。Connect.LinkAccount() を呼び出して、両方のアカウントが同じ PUID に関連付けられていることを確認します。
EAS の記事 で概要を説明したとおり、ユーザーの認証プロセスを簡素化するためのオプションがさらに 1 つあります。これを次に示します。デバイス ID を使用する。プレイヤーがどの ID プロバイダにもログインすることなく、ゲームをプレイするオプションを用意する場合は、代わりになる擬似アカウントを作成できます。

重要な点は、デバイス ID が ID にリンクされないことです。そこでゲームが進行したときに、アカウントの情報が失われないように、サポート ID プロバイダを使用して、ログイン (そしてリンク) するようにユーザーに確認することを推奨します。PC では Windows にログオンしているユーザーにリンクされますが、iOS と Android では、デバイス ID がアプリのアンインストールとともに削除されます。デバイス ID 機能はコンソール プラットフォームでサポートされません。簡単に利用できるローカル認証ユーザーが常にあるからです。

デバイス ID のフローの詳細については、このドキュメント を参照してください。ここでは扱いません。

Connect ログインを実装する

ここまでフローおよびそれに統合できる別の方法について解説したので、認証済みローカル Epic アカウント ユーザーを使用して、Connect インターフェースの Login メソッドを呼び出すときの基本フローを確認します。
 
  1. 「MainWindow.xaml」を開き、上部のグリッド コンテンツ (TabControl の上) を次に示す内容で置き換え、PUID を表示し、Connect ログイン ボタンを追加します。

<StackPanel Grid.Column="1">
    <StackPanel Orientation="Horizontal">
        <Button Width="100" Height="23" Margin="2" Command="{Binding AuthLogin}" Content="Auth Login" />
        <Button x:Name="AuthLogoutButton" Width="100" Height="23" Margin="2" Command="{Binding AuthLogout}" Content="Auth Logout" />
    </StackPanel>
    <Button x:Name="ConnectLoginButton" HorizontalAlignment="Right" Width="100" Height="23" Margin="2" Command="{Binding ConnectLogin}" Content="Connect Login" />
</StackPanel>

<StackPanel Grid.Column="0">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="AccountId:"Margin="2" />
        <TextBox Text="{Binding AccountId}" IsReadOnly="True" Margin="2" />
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="DisplayName:"Margin="2" />
        <TextBox Text="{Binding DisplayName}" IsReadOnly="True" Margin="2" />
    </StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="ProductUserId:"Margin="2" />
        <TextBox Text="{Binding ProductUserId}" IsReadOnly="True" Margin="2" />
    </StackPanel>
</StackPanel>

 
  1. PUID メンバーを MainViewModel.cs に追加し、有効期限とステータス変更通知に対して、各メンバーが ID を保持するようにします。

private string _productUserId;
public string ProductUserId
{
    get { return _productUserId; }
    set { SetProperty(ref _productUserId, value); }
}

private ulong _connectAuthExpirationNotificationId;
public ulong ConnectAuthExpirationNotificationId
{
    get { return _connectAuthExpirationNotificationId; }
    set { SetProperty(ref _connectAuthExpirationNotificationId, value); }
}

private ulong _connectLoginStatusChangedNotificationId;
public ulong ConnectLoginStatusChangedNotificationId
{
    get { return _connectLoginStatusChangedNotificationId; }
    set { SetProperty(ref _connectLoginStatusChangedNotificationId, value); }

}
 
  1. ConnectLoginCommand.cs クラスを「Commands」フォルダに追加:

public class ConnectLoginCommand :CommandBase
{
    public override bool CanExecute(object parameter)
    {
        return !string.IsNullOrWhiteSpace(ViewModelLocator.Main.AccountId) && string
.IsNullOrWhiteSpace(ViewModelLocator.Main.ProductUserId);
    }

    public override void Execute(object parameter)
    {
        ConnectService.ConnectLogin();
    }
}

 
  1. ConnectService.cs クラスを、使用するログイン ロジックの「Services」フォルダに追加:

public static class ConnectService
{
    public static void ConnectLogin()
    {
        ViewModelLocator.Main.StatusBarText = "Getting auth interface...";

        var authInterface = App.Settings.PlatformInterface.GetAuthInterface();
        if (authInterface == null)
        {
            Debug.WriteLine("Failed to get auth interface");
            ViewModelLocator.Main.StatusBarText = string.Empty;
            return;
        }
        var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions()
        {
            AccountId = EpicAccountId.FromString(ViewModelLocator.Main.AccountId)
        };

        var result = authInterface.CopyIdToken(copyIdTokenOptions, out var userAuthToken);

        if (result == Result.Success)
        {
            var connectInterface = App.Settings.PlatformInterface.GetConnectInterface();
            if (connectInterface == null)
            {
                Debug.WriteLine("Failed to get connect interface");
                return;
            }

            var loginOptions = new LoginOptions()
            {
                Credentials = new Credentials()
                {
                    Type = ExternalCredentialType.EpicIdToken,
                    Token = userAuthToken.JsonWebToken
                }
            };

            ViewModelLocator.Main.StatusBarText = "Requesting user login...";

            // Ensure platform tick is called on an interval, or the following call will never callback.
            connectInterface.Login(loginOptions, null, (LoginCallbackInfo loginCallbackInfo)=>
            {
                Debug.WriteLine($"Connect login {loginCallbackInfo.ResultCode}");

                if (loginCallbackInfo.ResultCode == Result.Success)
                {
                    ViewModelLocator.Main.StatusBarText = "Connect login successful.";

                    ViewModelLocator.Main.ConnectAuthExpirationNotificationId = connectInterface.AddNotifyAuthExpiration(new AddNotifyAuthExpirationOptions(), null, AuthExpirationCallback);
                    ViewModelLocator.Main.ConnectLoginStatusChangedNotificationId = connectInterface.AddNotifyLoginStatusChanged(new AddNotifyLoginStatusChangedOptions(), null, LoginStatusChangedCallback);

                    ViewModelLocator.Main.ProductUserId = loginCallbackInfo.LocalUserId.ToString();
                }
                else if (loginCallbackInfo.ResultCode == Result.InvalidUser)
                {
                    ViewModelLocator.Main.StatusBarText = "Connect login failed:" + loginCallbackInfo.ResultCode;

                    var loginWithDifferentCredentials = MessageBox.Show("User not found.Log in with different credentials?", "Invalid User", MessageBoxButton.YesNo, MessageBoxImage.Question);
                    if (loginWithDifferentCredentials == MessageBoxResult.No)
                    {
                        var createUserOptions = new CreateUserOptions()
                        {
                            ContinuanceToken = loginCallbackInfo.ContinuanceToken
                        };

                        connectInterface.CreateUser(createUserOptions, null, (CreateUserCallbackInfo createUserCallbackInfo) =>
                        {
                            if (createUserCallbackInfo.ResultCode == Result.Success)
                            {
                                ViewModelLocator.Main.StatusBarText = "User successfully created.";

                                ViewModelLocator.Main.ConnectAuthExpirationNotificationId = connectInterface
.AddNotifyAuthExpiration(new AddNotifyAuthExpirationOptions(), null, AuthExpirationCallback);
                                ViewModelLocator.Main.ConnectLoginStatusChangedNotificationId = connectInterface
.AddNotifyLoginStatusChanged(new AddNotifyLoginStatusChangedOptions(), null, LoginStatusChangedCallback);

                                ViewModelLocator.Main.ProductUserId = createUserCallbackInfo.LocalUserId.ToString();
                            }
                            else if (Common.IsOperationComplete(loginCallbackInfo.ResultCode))
                            {
                                Debug.WriteLine("User creation failed:" + createUserCallbackInfo.ResultCode);
                            }

                            ViewModelLocator.Main.StatusBarText = string.Empty;
                            ViewModelLocator.RaiseConnectCanExecuteChanged();
                        });
                    }
                    else
                    {
                        // Prompt for login with different credentials
                    }
                }
                else if (Common.IsOperationComplete(loginCallbackInfo.ResultCode))
                {
                    Debug.WriteLine("Connect login failed:" + loginCallbackInfo.ResultCode);
                }

                ViewModelLocator.Main.StatusBarText = string.Empty;
                ViewModelLocator.RaiseConnectCanExecuteChanged();
            });
        }
        else if (Common.IsOperationComplete(result))
        {
            Debug.WriteLine("CopyIdToken failed:" + result);
            ViewModelLocator.Main.StatusBarText = string.Empty;
        }
    }

    private static void AuthExpirationCallback(AuthExpirationCallbackInfo data)
    {
        // Handle 10-minute warning prior to token expiration by calling Connect.Login()
    }
    private static void LoginStatusChangedCallback(LoginStatusChangedCallbackInfo data)
    {
        switch (data.CurrentStatus)
        {
            case LoginStatus.NotLoggedIn:
                if (data.PreviousStatus == LoginStatus.LoggedIn)
                {
                    // Handle token expiration
                }
                break;
            case LoginStatus.UsingLocalProfile:
                break;
            case LoginStatus.LoggedIn:
                break;
        }
    }

    public static void RemoveNotifications()
    {
        App.Settings.PlatformInterface.GetConnectInterface()
.RemoveNotifyAuthExpiration(ViewModelLocator.Main
.ConnectAuthExpirationNotificationId);
        App.Settings.PlatformInterface.GetConnectInterface()
.RemoveNotifyLoginStatusChanged(ViewModelLocator.Main
.ConnectLoginStatusChangedNotificationId);
    }
}


かなりの量のコードがここにあるので分割して説明します。
 
  • まず、Auth インターフェースの参照を取得して、Auth.CopyIdToken を呼び出し、ログインした Epic アカウントの ID トークンを取得します。これは実装している ID プロバイダの種類により変わります。ただし、Epic アカウント サービスをすでに使用しているので、Auth インターフェースを通じて簡単にトークンを取得できます。
    • Auth インターフェースは EOS ゲーム サービスでは使用する必要がないことに注意してください。Connect インターフェースを通じて認証するためにサポート ID プロバイダを使用できるからです。
  • 続いて Connect.Login を ExternalCredentialType EpicIdToken を指定して呼び出し、取得したばかりの ID トークンを渡します。これは SDK 1.14.1 の新しい機能です。
  • このコールバックで、ResultCode が Result.Success であるかどうかをチェックします。この場合、PUID を LocalUserId メンバーから取得するだけで、ViewModel にコピーできます。
  • 一方 ResultCode が Result.InvalidUser である場合、別の認証情報でログインするかどうかをユーザーに確認します。異なる場合、そのまま Connect.CreateUser を呼び出して、新しい PUID を作成し、最初の Login コールバックで受け取った ContinuanceToken を渡します。
    • ユーザーが別の ID プロバイダでのログインを希望する場合は、何も実行することがないことに注意してください。ただしここでは 前に説明した 動作をここに実装しています。
  • CreateUser が正常に返ったとき、コールバック データから LocalUserId を取り出し、ViewModel にコピーするだけです。
  • 両方の場合 (最初のログインが有効または CreateUser が成功)、Connect.AddNotifyAuthExpiration および Connect.AddNotifyLoginStatusChanged を使用し、認証の有効期限が切れるタイミング (ID プロバイダで認証されたので、期限切れの約 10 分前に通知されることに注意) または他にログイン ステータスが変わったタイミングを通知します。
    • ここにコードが実装されていませんが、2 つのメソッドはそれぞれの場合に応じて処理できる場所にあります。
    • 重要なのは、それぞれの Connect 認証の更新で、Connect.Login API で使用するため、コードで ID プロバイダから新しいフレッシュな認証トークンも生成することです。他の場合、Connect.Login 呼び出しは、プラットフォーム認証トークンの期限が切れているために失敗します。
  • 最後に、新しい RaiseConnectCanExecuteChanged メソッドを ViewModel で呼び出します。これは将来の記事で解説しますが、ユーザーが Connect インターフェースで認証された後にのみ、ゲーム サービス機能を呼び出せるようにするためのものです。
 
  1. 「AuthLogoutCommand.cs」を開き、次の内容を Execute() メソッドに追加し、ログアウト時に、セットアップした通知のリッスンを必ず停止します。

ConnectService.RemoveNotifications();
 
  1. 「MainViewModel.cs」を再び開き、ConnectLoginCommand を次のとおり宣言し、インスタンス化します。

public ConnectLoginCommand ConnectLogin { get; set; }
public MainViewModel()
{
    AuthLogin = new AuthLoginCommand();
    AuthLogout = new AuthLogoutCommand();
    ConnectLogin = new ConnectLoginCommand();
}

 
  1. MainViewModel.cs でさらに Notification ID を保持するためのメンバーを追加します:

private ulong _connectAuthExpirationNotificationId;
public ulong ConnectAuthExpirationNotificationId
{
    get { return _connectAuthExpirationNotificationId; }
    set { SetProperty(ref _connectAuthExpirationNotificationId, value); }
}

private ulong _connectLoginStatusChangedNotificationId;
public ulong ConnectLoginStatusChangedNotificationId
{
    get { return _connectLoginStatusChangedNotificationId; }
    set { SetProperty(ref _connectLoginStatusChangedNotificationId, value); }
}

 
  1. 「ViewModelLocator.cs」を開き、RaiseConnectCanExecuteChanged メソッドを追加:

public static void RaiseConnectCanExecuteChanged()
{
    Main.ConnectLogin.RaiseCanExecuteChanged();
}

 
  1. ログアウトする際に ProductUserId をクリアするために AuthService.cs を開きます。AuthLogout() メソッドの if (logoutCallbackInfo.ResultCode == Result.Success) ブロック内部に以下を追加します:

ViewModelLocator.Main.ProductUserId = string.Empty;
 
  1. 最後に、ViewModelLocator.cs で、次の行を RaiseAuthCanExecuteChanged に追加し、Auth ログインが成功した後にだけ、Connect ログイン ボタンが有効になるようにします。繰り返しますが、これはこのサンプル フロー固有のもので、自分のゲームでは、Connect インターフェースと EOS ゲーム サービスだけを使用できます。Epic アカウント サービスと Auth インターフェースを使用する必要はありません。

Main.ConnectLogin.RaiseCanExecuteChanged();

ここでサンプルを実行し、Auth ログインが成功した後に Connect ログインをクリックすると、プロンプトが表示され、InvalidUser の応答が返ります。
 
App Connect Login Failed InvalidUser
InvalidUser の結果で Connect ログインが失敗

「No」をクリックした後、コードは CreateUser の呼び出しに進み、有効な PUID が UI に表示されます。
 
App Connect Login Success
Connect ログインが成功

コードを入手する

この記事のコードを次から入手してください (この記事 のステップ 5~10 を実行して、SDK をソリューションに追加し、自分の SDK 資格情報を ApplicationSettings.cs に追加)。
正しく認証されたので、次の週は、Title Storage インターフェース の実装に進みます。これによりゲーム固有のデータをクラウドから取得できます。

このシリーズの完全な記事のリストは、シリーズ リファレンス で見つけることができます。フィードバックや質問がある場合は、コミュニティ フォーラムにアクセスして、お知らせください。

    デベロッパーみなさんの成功が、Epic の成功です

    Epic では、オープンで調和のとれたゲーム コミュニティを実現することは可能であると信じています。オンライン サービスをすべての人に無料で提供することにより、より多くのデベロッパーがそれぞれのプレイヤー コミュニティにサービスを提供できるようにすることが目標です。