연결 인터페이스로 EOS 게임 서비스에 액세스하기

에픽게임즈 테크니컬 어카운트 매니저 Rajen Kishna |
2021년 10월 28일
에픽 온라인 서비스 시작하기의 이전 연재글을 놓친 분들을 위해 설명하자면, 지난 주에는 친구 인터페이스 API에 대해 알아보며 에픽 계정 서비스에 관한 내용을 마무리했습니다. 이제 게임 서비스를 알아볼 준비가 되었으니 먼저 연결 인터페이스를 살펴보고 구현해 보겠습니다.

이름에서 알 수 있듯이 이 인터페이스를 통해 지원되는 인증 ID를 EOS 제품 사용자 ID(PUID)와 연결할 수 있으며, 이 ID는 모든 게임 서비스에서 제품별 플레이어 식별자로 사용됩니다. 여기에서 중요한 점은 게임 서비스와 PUID는 에픽 계정 서비스 및 에픽 계정과는 독립적이라는 것입니다.

잘 따라오고 계시다면, 에픽 온라인 서비스 1.14.1 SDK를 사용하고 있는지 확인하세요. 연결 플로와 에픽 계정 서비스를 함께 사용하는 방식에 변경된 내용이 있습니다. 이 글에서는 다음 내용을 다룹니다.

인증 인터페이스와 연결 인터페이스의 차이 요약

에픽 계정 서비스(EAS)를 통한 인증 문서에서 두 인터페이스 간의 차이점을 자세히 다루었는데, 요약하자면 다음과 같습니다. 인증 인터페이스는 에픽 계정을 인증하여 에픽 계정 서비스로 친구 정보 및 현재상태를 확인하고 에픽게임즈 스토어에서 구매하는 데 사용됩니다.

연결 인터페이스는 이 연재글에서 앞으로 에픽 제품의 각 플레이어를 위한 고유 PUID를 생성하고 멀티플레이어, 진행 상황, 게임 운영을 지원하는 게임 서비스를 이용하는 데 사용할 것입니다. 이러한 서비스는 독립적으로 이용할 수 있고, 연결 인터페이스는 지원되는 모든 ID 제공자와 함께 사용할 수 있으며 에픽 계정을 사용할 필요가 없습니다.

연결 인증 플로

샘플 앱에서는 이미 코드를 구현해 놓은 에픽 계정을 사용하여 연결 API 통합을 구현할 것입니다. 하지만 이에 앞서 지원되는 ID 제공자를 사용하여 이를 어떻게 구현하는지 플로를 살펴보도록 하겠습니다.
  1. 게임에서 플레이어에게 Connect.Login()을 통해 ID 제공자(A)를 사용하여 인증하라는 메시지가 표시됩니다.
  2. 특정 ID 제공자를 사용하는 이 플레이어의 PUID가 발견되면 콜백의 ResultCode는 Result.Success가 되고 콜백은 LocalUserId 멤버의 PUID를 반환합니다.
  3. PUID가 없다면 ResultCode는 Result.InvalidUser가 되고 콜백은 ContinuanceToken을 반환합니다. 다양한 ID 제공자를 지원하는 경우 이 시점에서 플레이어에게 다른 ID 제공자를 사용하여 인증할 것인지 물어볼 수 있습니다. 이는 별도의 진행 상황 데이터가 어태치되는 두 번째 PUID가 생성되고 향후 계정이 병합되지 않는 상황을 방지하기 위해 수행됩니다. 또는 이 ID를 계속 사용하도록 제안할 수도 있습니다(이후에 새 PUID를 생성합니다).
  4. 사용자가 이 ID를 사용하여 계속하려는 경우 Connect.CreateUser()를 사용하고 CreateUserOptions를 통해 ContinuanceToken을 전달합니다. Result.Success의 ResultCode가 반환되면 LocalUserId 멤버 내에서 PUID를 찾을 수 있습니다.
  5. 사용자가 다른 ID 제공자(B)를 사용하여 인증하려는 경우, ContinuanceToken을 저장하고 ID 제공자 B를 사용하여 Connect.Login()을 다시 호출합니다. 로그인에 성공하면 사용자에게 두 ID를 연동할 것인지 묻는 메시지를 표시할 수 있으며, Connect.LinkAccount()를 호출하여 두 계정이 동일한 PUID에 연결되어 있는지 확인합니다.
EAS 연재글에서 간략하게 언급했듯이 사용자의 인증 프로세스를 더욱 간소화하기 위해 제공할 수 있는 추가 옵션으로 디바이스 ID를 사용하는 방법이 있습니다. 플레이어에게 ID 제공자에 로그인하지 않고도 게임을 플레이할 수 있는 옵션을 제공하려는 경우 플레이어를 대신하여 가계정을 만들 수 있습니다.

여기서 유의해야 할 사항은, 디바이스 ID가 ID에 연결되어 있지 않기 때문에, 일정 진행 상황에 도달하면 지원되는 ID 제공자를 사용하여 로그인(및 연결)하라는 메시지를 사용자에게 표시하는 게 좋습니다. PC에서는 Windows에 로그인한 사용자에게 연결되지만 iOS와 Android에서는 앱 삭제와 함께 디바이스 ID가 삭제됩니다. 콘솔 플랫폼에는 항상 즉시 사용 가능한 로컬 인증 사용자가 있기 때문에 디바이스 ID 기능은 지원되지 않습니다.

이 문서에서 디바이스 ID 플로에 대한 추가 정보를 확인할 수 있으므로, 이 연재글에서는 자세한 내용을 다루지 않겠습니다.

연결 로그인 구현

이제 플로와 이를 통합할 수 있는 다양한 방법을 이해했으니 인증된 로컬 에픽 계정 사용자를 활용하여 연결 인터페이스의 로그인 메서드를 호출하는 기본 플로를 살펴보겠습니다.
  1. MainWindow.xaml을 열고 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. PUID 멤버와 만료 및 상태 변경 알림을 위한 ID를 보유할 멤버를 MainViewModel.cs에 추가합니다.

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을 호출하여 로그인한 에픽 계정의 ID 토큰을 얻습니다. 이 부분은 구현하는 ID 제공자에 따라 달라지지만, 지금은 에픽 계정 서비스를 사용하고 있으므로 인증 인터페이스를 통해 쉽게 토큰을 얻을 수 있습니다.
    • EOS 게임 서비스를 사용하기 위해 반드시 인증 인터페이스를 사용할 필요는 없습니다. 지원되는 ID 제공자라면 무엇이든 사용하여 연결 인터페이스를 통해 인증할 수 있습니다.
  • 그런 다음 ExternalCredentialType EpicIdToken으로 Connect.Login을 호출하고 방금 얻은 ID 토큰을 전달합니다. 이건 SDK 1.14.1의 새로운 기능입니다.
  • 콜백에서 ResultCode가 Result.Success인지 확인하고, 맞다면 LocalUserId 멤버에서 PUID를 가져와 ViewModel에 복사하기만 하면 됩니다.
  • ResultCode가 Result.InvalidUser라면 사용자에게 다른 크리덴셜로 로그인할 것인지 묻는 메시지를 표시합니다. 사용자가 거절한다면 Connect.CreateUser를 호출하여 새 PUID를 생성하고 최초 로그인 콜백에서 얻은 ContinuanceToken을 전달합니다.
    • 사용자가 다른 ID 제공자로 로그인하는 경우 여기서 수행해야 할 작업은 없지만, 위에서 설명한 행동을 구현해야 합니다.
  • CreateUser가 성공적으로 반환되면 콜백 데이터에서 LocalUserId를 가져와 ViewModel에 복사합니다.
  • 두 경우(유효한 초기 로그인 또는 CreateUser 성공) 모두 Connect.AddNotifyAuthExpirationConnect.AddNotifyLoginStatusChanged를 사용하여 인증이 만료되려고 할 때(사용자가 ID 제공자를 사용하여 인증했으므로 약 10분 전에 만료 알림이 표시됨) 또는 로그인 상태가 변경될 때 알림을 받습니다.
    • 여기에는 코드가 구현되어 있지 않지만 이러한 경우에 적절하게 처리할 수 있는 메서드가 두 가지 있습니다.
    • 각 연결 인증이 새로고침되면 코드가 ID 제공자로부터 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를 비우도록 합니다. if (logoutCallbackInfo.ResultCode == Result.Success) 블럭 내 AuthLogout() 메서드에 다음 줄을 추가하세요.

ViewModelLocator.Main.ProductUserId = string.Empty;
 
  1. 마지막으로 ViewModelLocator.cs에서 RaiseAuthCanExecuteChanged에 다음 행을 추가하여 인증 로그인이 성공한 후에만 연결 로그인 버튼이 활성화되도록 합니다. 다시 언급하지만 이 내용은 샘플 플로에만 해당하며, 실제 게임에서는 에픽 계정 서비스와 인증 인터페이스를 사용하지 않아도 연결 인터페이스 및 EOS 게임 서비스를 사용할 수 있습니다.

Main.ConnectLogin.RaiseCanExecuteChanged();

이제 샘플을 실행하여 인증 로그인을 성공적으로 완료한 후 로그인 연결(Connect login)을 클릭하면 InvalidUser 응답을 받고 메시지 창이 표시됩니다.
App Connect Login Failed InvalidUser
InvalidUser 결과가 반환되고 로그인 연결 실패

'아니요(No)'를 클릭하면 코드가 실행되어 CreateUser를 호출하고 유효한 PUID가 UI에 표시됩니다.
App Connect Login Success
로그인 연결 성공

코드 받기

아래에서 이 연재글의 코드를 받으세요(이 연재글의 5단계와 10단계에 따라 SDK를 솔루션에 추가하고 SDK 크리덴셜을 ApplicationSettings.cs에 추가합니다).
이제 인증을 성공적으로 마쳤으니 다음 글에서는 클라우드에서 게임별 데이터를 얻을 수 있게 해 주는 타이틀 스토리지 인터페이스 구현 방법에 대해 알아보겠습니다.

전체 연재글은 시리즈 참조에서 확인할 수 있습니다. 질문이나 피드백은 커뮤니티 포럼에 올려주세요.

    여러분의 성공이 곧 에픽의 성공입니다

    에픽은 개방되고 통합된 게이밍 커뮤니티를 지향합니다. 에픽 온라인 서비스를 누구에게나 무료로 제공함으로써 더 많은 개발자들이 플레이어 커뮤니티에 더 나은 서비스를 제공할 수 있도록 지원하고자 합니다.