通过连接接口访问EOS游戏服务

Epic Games技术客户经理Rajen Kishna |
2021年11月16日
请不要错过Epic在线服务入门系列的前几篇文章,我们上周讲到了好友接口API。现在我们已经准备好深入了解游戏服务,所以我们首先要探索并实现连接接口

顾名思义,该接口用于将任何受支持并经过验证的身份与EOS产品用户ID(PUID)连接起来,而PUID将在所有游戏服务中充当特定于产品的玩家标识符。这里需要注意的是,游戏服务和PUID并不依赖于Epic账户服务和Epic账户。

如果你跟着这个系列的文章一路学过来,请确保使用的是Epic在线服务1.14.1版SDK,因为它包含了当与Epic账户服务结合时,连接流程工作方式中的一些变化。在本文中,我们将介绍:
 

身份验证接口与连接接口的简要总结

我们在通过Epic账户服务(EAS)验证玩家身份一文中详细介绍了这两个接口之间的差异,来回顾一下:身份验证接口用于验证Epic账户,以使用Epic账户服务获取在线状态和好友信息,或在Epic Games商城中购买商品。

连接接口是我们将在本系列中用到的接口,用于为我们产品的每个玩家生成一个独有的产品用户ID(PUID),以使用包含多人管理、进度追踪和游戏运营等功能的游戏服务。它们可以相互独立地使用,且连接接口可以与任何受支持的身份提供商一起使用,不要求使用Epic账户。

连接身份验证流程

在我们的示例应用程序中,我们将使用Epic账户实现连接API集成,因为我们已经实现了该代码。但在继续之前,我们先了解一下如何使用任何受支持的身份提供商实现这一点:
 
  1. 游戏通过Connect.Login()提示玩家使用身份提供商(A)进行身份验证。
  2. 如果使用指定的身份提供商找到了该玩家的产品用户ID(PUID),则回调的ResultCode将为Result.Success,且回调将返回LocalUserId成员中的PUID。
  3. 如果未找到PUID,则ResultCode将为Result.InvalidUser,回调将返回一个ContinuanceToken。此时,你可以询问玩家是否想要使用其他受支持的身份提供商(如果你支持多个身份提供商)进行身份验证。这样做是为了防止创建第二个连接了单独进度数据的PUID,避免将来合并账户。或者,让玩家继续使用这个身份(然后我们将为其创建一个新的PUID)。
  4. 如果用户想继续使用这个身份,我们会使用Connect.CreateUser(),通过CreateUserOptions传入ContinuanceToken。当返回的ResultCode为Result.Success时,可在LocalUserId成员中找到PUID。
  5. 如果用户想使用其他身份提供商(B)进行身份验证,我们将储存ContinuanceToken,使用身份提供商B再次调用Connect.Login()。登录成功后,我们可以提示用户是否想要关联这两个身份,并调用Connect.LinkAccount()以确保这两个账户与同一PUID相关联。
正如EAS文章中简要提到的,我们还可以提供一个额外选项来进一步简化用户验证过程:使用设备ID。如果我们希望允许玩家无需登录任何身份提供商即可玩游戏,我们可以为他们创建一个伪账户。

需要注意的是,设备ID没有连接到身份,所以我们建议在用户取得一些进展时,提示他们使用受支持的身份提供商登录(并进行关联),确保他们的账户不会丢失。在PC上,设备ID与登录Windows的用户连接,但在iOS和Android上,设备ID会随着应用程序的卸载而被删除。设备ID特性在主机平台上不受支持,因为主机上始终会有一个经过身份验证的本地用户。

你可以在此文档中找到关于设备ID流程的更多信息,但在本文中我们不讨论它。

实现连接登录

我们已经了解了这个流程和它的各种集成方式,现在来看看如何使用经过身份验证的本地Epic账户调用连接接口的Login方法:
 
  1. 打开MainWindow.xaml,并将顶部Grid的内容(在TabControl上方)替换为以下内容,以显示PUID并添加一个“连接登录”按钮:

<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. 向MainViewModel.cs添加PUID成员,以及保存过期ID的成员和状态更改通知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. 向Commands文件夹添加一个ConnectLoginCommand.cs类:

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. 向Services文件夹添加一个ConnectService.cs类,实现登录逻辑:

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.CopyIdToken为已登录的Epic账户检索ID令牌。这取决于你实现的身份提供商,由于我们已经使用了Epic账户服务,通过身份验证接口获得令牌非常容易。
    • 请注意,你无需使用身份验证接口即可使用EOS游戏服务,因为你可以使用任何受支持的身份提供商通过连接接口验证身份。
  • 然后使用ExternalCredentialType EpicIdToken调用Connect.Login,并传入我们刚刚检索到的ID令牌。这是SDK 1.14.1中的新功能。
  • 在回调中,我们检查ResultCode是否等于Result.Success,若是,我们可以简单地从LocalUserId成员获取PUID,并将其复制到我们的ViewModel中。
  • 相反,如果ResultCode为Result.InvalidUser,我们会提示用户是否想用其他凭证登录。如果他们不想,则继续调用Connect.CreateUser创建一个新的PUID,传入我们在初始Login回调中获得的ContinuanceToken。
    • 注意,如果用户想通过其他身份提供商登录,我们在这里没有实现任何操作,但你应该实现上面描述的行为。
  • 若CreateUser成功返回,我们只需从回调数据中获取LocalUserId并将其复制到我们的ViewModel中。
  • 在这两种情况下(初始登录有效或CreateUser成功),我们使用Connect.AddNotifyAuthExpirationConnect.AddNotifyLoginStatusChanged在身份验证即将到期(切记它们通过身份提供商进行身份验证,因此将提前大约10分钟获得到期通知)或登录状态发生更改时获得通知。
    • 注意,我没有在这里实现代码,但你可以使用这两个方法相应地处理这些情况。
    • 对于每次连接身份验证刷新,你的代码还需要生成一个来自身份提供商的新身份验证令牌,并与Connect.Login API一起使用,这一点非常重要。否则,由于平台身份验证令牌到期,Connect.Login调用将失败。
  • 最后,我们在ViewModel上调用一个新的RaiseConnectCanExecuteChanged方法,我们将在以后的文章中用到它,确保只有在用户通过连接接口成功验证身份后,我们才能调用游戏服务功能。
 
  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中,添加成员变量来保存我们的通知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. 打开 AuthService.cs ,在登出部分的逻辑中清空ProductUserId。在AuthLogout()方法中添加以下行,具体位于if (logoutCallbackInfo.ResultCode == Result.Success) 这段代码中,添加内容如下:

ViewModelLocator.Main.ProductUserId = string.Empty;
 
  1. 最后,在ViewModelLocator.cs中,将以下代码行添加到RaiseAuthCanExecuteChanged中,确保“连接登录”按钮仅在身份验证登录成功后可用。这同样特定于我们的示例流程,但在你的游戏中,你也可以使用连接接口和EOS游戏服务,而不使用Epic账户服务和认证接口。

Main.ConnectLogin.RaiseCanExecuteChanged();

现在,如果我们运行示例并在成功完成身份验证登录后单击“连接登录”,我们会在取得InvalidUser响应时看到提示:
 
App Connect Login Failed InvalidUser
连接登录失败,结果为InvalidUser

点击“No”后,代码继续调用CreateUser,最终,用户界面上显示了有效的PUID:
 
App Connect Login Success
连接登录成功

获取代码

在下方获取本文的代码(按照这篇文章中的步骤5和步骤10,将SDK添加到解决方案中,然后将SDK凭证添加到ApplicationSettings.cs中)。
现在我们已经成功地进行了身份验证,在下一篇博文中我们将继续实现作品存储接口,它允许我们从云中检索特定于游戏的数据。

你可以在系列目录中找到包含本系列所有文章的完整列表,如果你对本文有任何疑问或反馈,请前往社区论坛告知我们。

    你的成功就是我们的成功

    Epic秉承开放、整合的游戏社区理念。通过免费向所有人提供在线服务,我们致力于帮助更多开发者服务好他们自己的玩家社区。