게임에 업적 추가하기

에픽게임즈 테크니컬 어카운트 매니저 Rajen Kishna |
2022년 1월 25일
지난 연재글에서는 통계순위표에 대해 알아봤습니다. 이번에는 업적 인터페이스를 사용하여 게임에 업적을 추가하는 방법을 알아보겠습니다. 업적은 통계 진행 상황 기반으로 자동 잠금해제하거나 API를 사용하여 수동으로 잠금해제하도록 설정할 수 있습니다. 이번 연재글에서 살펴볼 내용은 다음과 같습니다.
 

에픽 온라인 서비스 업적 및 에픽 업적

에픽 온라인 서비스 업적 구현을 살펴보기 전에 에픽 온라인 서비스(EOS) 업적과 2021년 10월 공개된 에픽 업적(이 글을 쓰는 시점에서 얼리 액세스 단계) 사이의 관계와 차이점에 대해 잠시 설명하겠습니다.
에픽 온라인 서비스(EOS) 업적
  • EOS 업적은 에픽 온라인 서비스의 많은 기능과 마찬가지로 플랫폼과 무관하게 게임 내에 구현 가능한 업적입니다. 
  • EOS 업적을 사용하면 진행 상황을 추적하고 자체 인게임 UI를 통해 업적을 잠금해제할 수 있습니다. 
  • EOS 업적은 편의상 오버레이로 렌더링되지만 에픽게임즈 스토어의 게임과 반드시 연동되지는 않으므로 에픽게임즈 스토어 플레이어 프로필이나 게임 페이지에 표시되지 않습니다.
  • EOS 업적에는 XP가 어태치되지 않습니다. 또한 플랫폼과 무관하다는 특징으로 인해 EOS 업적으로는 게임에 따라 어떤 스코어 메커니즘이든 구현할 수 있습니다.
에픽 업적
  • 에픽 업적은 코딩 없이 환경설정만으로 EOS 업적을 에픽게임즈 스토어를 비롯한 에픽 생태계로 확장하는 기능입니다. 
  • 잠금해제되는(또는 이전에 잠금해제된) 모든 EOS 업적에 따라 사용자의 해당 에픽 업적도 잠금해제됩니다. 
  • 에픽 업적에서는 각 업적에 XP 값을 추가합니다. XP 값의 범위는 5~200XP입니다. 게임에서는 모든 업적을 통틀어 총 1,000XP를 할당해야 합니다(이 연재글 작성 시점 기준).
  • 에픽 업적은 할당된 XP 값에 따라 세 가지 등급으로 분류됩니다. 5~45XP면 브론즈 등급, 50~95XP면 실버 등급, 100~200XP면 골드 등급입니다.
    • 다른 모든 업적을 완료하면 추가로 플래티넘 업적(250XP)이 자동 달성되므로 게임마다 총 1,250XP를 획득할 수 있습니다.
  • 에픽 업적 진행 상황은 플레이어의 에픽게임즈 스토어 라이브러리, 플레이어 프로필, 에픽게임즈 스토어 게임 페이지(웹 페이지와 에픽게임즈 런처 모두)에 자동으로 표시됩니다.
  • 에픽 업적을 구성한 게임에서는 이전에 표시되던 EOS 업적 대신 XP를 포함한 에픽 업적이 오버레이에 표시됩니다.
  • 마지막으로 발표 블로그에서 일부 공개했듯이 에픽 업적은 향후 아바타와 같은 다른 소셜 기능을 포함하도록 확장될 것입니다.

에픽 업적은 현재 얼리 액세스 단계이며 게임을 에픽게임즈 스토어에 퍼블리싱하도록 설정해야 하므로 이번 글에서는 EOS 업적에만 집중하겠습니다.

클라이언트 정책 변경하기

업적을 사용하려면 우선 다음과 같이 클라이언트 정책에 액션을 추가해야 합니다.
 
  1. https://dev.epicgames.com/portal/에서 개발자 포털에 로그인합니다.
  2. 왼쪽 메뉴에서 해당 제품의 ‘제품 세팅’으로 이동한 후 제품 세팅 화면의 ‘클라이언트’ 탭을 클릭합니다.
  3. 사용 중인 클라이언트 정책 옆의 점 세 개 버튼을 클릭하고 ‘Update policy’를 클릭합니다.
  4. 아래의 ‘기능’으로 스크롤하여 ‘Achievements’ 옆의 토글 버튼을 클릭합니다.
  5. 다음 액션 옆의 박스를 체크합니다. 
    • findAchievementDefinitions: 업적 정의 쿼리에 사용됩니다.
    • findAchievementsForLocalUser: 플레이어 업적 진행 상황 쿼리에 사용됩니다.
    • unlockAchievementForLocalUser: 로그인한 사용자의 업적 잠금해제에 사용됩니다.
      • 프로덕션 환경에서 이 기능을 사용할 때는 주의해야 합니다. 악성 사용자가 리버스 엔지니어링을 통해 업적을 실제로 획득하지 않고 잠금해제할 수 있기 때문입니다. 보다 안정적인 접근법은 통계 또는 자체 백엔드 서버를 통해 업적을 잠금해제하는 것입니다.
  6. ‘저장 & 종료’를 클릭하여 확인합니다.
Developer Portal Client Policy Achievements
업적 클라이언트 정책에서 허용하는 기능 및 액션

개발자 포털에서 업적 생성하기

업적은 개발자 포털에서 생성되며 통계에 기반하거나 수동으로 잠금해제되도록 설정할 수 있습니다. 또한 업적을 잠금해제하기 전에 플레이어에게 어떤 업적이 있는지 알리고 싶은지 여부에 따라 업적 표시 여부도 설정할 수 있습니다. 포털에서 몇 가지 업적을 설정해 보겠습니다.
 
  1. 왼쪽 메뉴에서 해당 제품의 ‘게임 서비스’ > ‘업적’으로 이동합니다.
  2. 여기서 제품 내 각 디플로이에 대한 업적을 볼 수 있습니다. 현재는 ‘Live 샌드박스에 Release’만 있을 것입니다.
    • 오른쪽 상단에 있는 ‘대량 임포트/익스포트’ 버튼을 사용하면 업적 백업 및 생성 프로세스를 간소화할 수 있습니다. 하지만 이번 글에서는 이 프로세스를 다루지 않습니다.
  3. 새 업적을 추가하려면 오른쪽 상단의 ‘새로 생성’을 클릭합니다.
  4. 플라이아웃 메뉴에 업적을 자동으로 잠금해제할 통계를 정의하는 첫 번째 단계가 표시됩니다. 드롭다운 메뉴에서 ‘SumStat‘ 통계를 선택합니다. 통계 오른쪽에 있는 박스에 한계치 값도 입력해야 합니다. 50을 입력하세요.
  5. 업적을 잠금해제할 기준이 되는 통계는 3개까지 정의할 수 있지만 여기서는 하나만 정의하겠습니다. ‘다음’을 클릭하여 두 번째 단계로 넘어갑니다.
  6. 여기서는 업적 세팅을 대부분 정의하겠습니다. 업적 ID로 ‘50_TOTAL_CLICKS’를 입력합니다.
  7. 테스트할 로케일을 유지하거나 선택합니다.
  8. 이 업적의 ‘비저빌리티’ 세팅은 ‘표시’로 두겠습니다.
  9. 업적의 잠금해제 아이콘과 잠금 아이콘을 업로드합니다. 아이콘은 1.02MB 이하, 1024x1024px 이하의 JPG, PNG, BMP 또는 움직이지 않는 GIF 이미지 파일이면 됩니다.
    • 모든 업적의 아이콘 파일명은 서로 달라야 합니다. 동일한 이름의 파일을 새로 업로드하면 기존 파일을 덮어씁니다.
  10. 잠금 및 잠금해제의 표시명과 설명을 입력합니다.
    • 잠금 표시명: 클릭, 클릭, 클릭...
    • 잠금 설명: 클릭을 몇 번이나 해야 할까요?
    • 잠금해제된 표시명: 클릭 챔피언
    • 잠금해제된 설명: 총 50번 클릭했습니다.
    • 선택 사항으로 플레이버 텍스트를 입력할 수도 있습니다. 게임에 사용할 수 있고 임의로 현지화되는 스트링인데, 여기서는 다루지 않겠습니다.
  11. 이제 ‘생성’을 클릭하여 업적 생성을 완료하면 됩니다. 플라이아웃 메뉴가 닫힌 뒤 목록에 업적이 표시되는 것을 볼 수 있습니다.
 
Developer Portal Achievements Click Champion
생성된 ‘클릭 챔피언’ 업적

다음으로 수동으로 잠금해제할 수 있는 숨겨진 업적을 만들어 보겠습니다.
 
  1. ‘새로 생성’을 클릭하여 새 업적 추가를 시작합니다.
  2. 플라이아웃 메뉴의 첫 번째 단계에서 통계를 지정하지 않고 ‘다음’을 클릭합니다.
  3. 업적의 세부 사항은 이렇게 지정합니다.
    • 업적 ID: TOP_SECRET
    • 비저빌리티: 숨김
    • 잠금해제된 표시명: 찾으셨군요.
    • 잠금해제된 설명: 숨겨진 업적을 잠금해제했습니다.
    • 숨겨진 업적에는 잠금 표시명 및 설명이 없다는 점에 유의하세요.

업적 정의 쿼리하기

이제 업적 정의를 쿼리하는 코드를 구현해 봅시다.
 
  1. Views 폴더에 AchievementView라는 새로운 사용자 컨트롤을 생성합니다.
  • 업적 UI에는 두 개의 ListView가 있습니다. 하나는 모든 정의를 표시하고, 다른 하나는 플레이어 진행 상황을 표시합니다. 우선 UI의 일부만 차지하는 모든 정의의 ListView부터 시작해 봅시다.
&Nbsp;
  1. AchievementsView.xaml.cs를 열어서 ViewModel을 어태치하고 자리 표시자 SelectionChanged 이벤트 핸들러를 포함합니다.
  1. AchievementsViewModel.cs 클래스를 ViewModels 폴더에 추가합니다.
  1. AchievementsViewModel에 대한 레퍼런스를 ViewModelLocator.cs에 추가합니다.
  1. AchievementsService.cs 클래스를 Services 폴더에 추가하여 정의 쿼리 로직을 포함합니다.
  • Achievements.QueryDefinitions를 호출하여 로컬 캐시를 모든 업적 정의로 채웁니다.
  • 이후 반환된 모든 업적에 대해 반복작업을 진행한 뒤, Achievements.CopyAchievementDefinitionV2ByIndex를 통해 업적을 캐시에서 ObservableCollection으로 복사합니다.
    • SDK에서 폐기된 이전 API(예: CopyAchievementDefinitionByIndex) 대신 V2 API를 사용하고 있다는 점에 유의하세요.
 
  1. AchievementsQueryDefinitionsCommand.cs 클래스를 Commands 폴더에 추가합니다.
  1. AchievementsViewModel.cs를 열어 다음 명령을 선언하고 인스턴스화합니다.
  1. 연결 인터페이스를 통해 로그인한 뒤에만 업적 정의를 쿼리할 수 있도록 다음 명령줄을 ViewModelLocator.cs의 RaiseConnectCanExecuteChanged() 메서드에 추가합니다.
  1. 마지막으로 AchievementsView를 MainWindow.xaml의 TabControl에 추가합니다.
이제 앱을 실행하고 인증 및 연결을 통해 인증한 다음, ‘Achievements‘ 탭에서 ‘Query definitions‘ 버튼으로 업적 정의를 쿼리할 수 있습니다.
 
App Achievements QueryDefinitions
쿼리된 업적 정의

플레이어 업적 진행 상황 쿼리하기

이제 모든 업적 정의를 얻는 방법을 알았으니, 이번에는 업적 정의에 대한 플레이어의 진행 상황을 얻겠습니다.
 
  1. AchievementsView.xaml을 열고 두 번째 ListView 및 StackPanel을 이전 </ListView> 태그 바로 밑에 추가합니다.
  • 업적에 어태치된 통계(있는 경우)를 표시하기 위해 약간 강제적인 방법을 사용하겠습니다. 여기서는 배열의 첫 번째 통계를 표시하고 있습니다(없더라도 표시). 프로덕션 환경에서는 StatInfo 프로퍼티에서 반환된 PlayerStatInfo 배열을 사용 전에 검증해야 합니다.
 
  1. AchievementsViewModel.cs를 열고 다음 컬렉션을 SelectedAchievement 밑에 선언하여 플레이어 업적 진행 상황을 포함합니다.
  1. AchievementsService.cs를 열고 다음 메서드를 추가하여 플레이어 업적 진행 상황의 쿼리 로직을 포함합니다.
 
  1. AchievementsQueryPlayerAchievementsCommand.cs 클래스를 Commands 폴더에 추가합니다.
  1. AchievementsViewModel.cs를 열어 다음 명령을 선언하고 인스턴스화합니다.
  1. 연결 인터페이스를 통해 로그인한 뒤에만 플레이어 업적 진행 상황을 쿼리할 수 있도록 ViewModelLocator.cs를 열고 다음 명령줄을 RaiseConnectCanExecuteChanged() 메서드에 추가합니다.
이제 앱을 재실행한 다음 ‘Query progress‘ 버튼으로 플레이어 업적 진행 상황을 얻을 수 있습니다.
 
App Achievements QueryPlayerAchievementsInitial
쿼리된 플레이어 업적 진행 상황

여기에는 다음과 같은 몇 가지 유의 사항이 있습니다.
 
  • QueryPlayerAchievements 호출에서 반환된 DisplayName 프로퍼티는 null입니다. Description, IconURL, FlavorText 프로퍼티(UI에 표시되지 않음)와 마찬가지입니다. 이러한 프로퍼티는 QueryDefinitions 호출로 얻어 SDK 캐시에 저장되므로 우선 ‘Query definitions‘ 버튼을 클릭하여 캐시에 프로퍼티를 채워야 합니다.
  • StatInfo 프로퍼티는 플레이어가 통계 값을 수집하지 않는 한 null입니다. 위 스크린샷에서는 로그인한 플레이어의 통계를 리셋했기 때문에 아무 값도 표시되지 않습니다.
  • 플레이어 진행 상황은 0~1 사이의 double 값으로 보고됩니다. 이 값은 0~100% 사이의 값을 나타냅니다. 스크린샷의 로그인한 플레이어는 통계를 수집하지 않았으므로 진행 상황이 0입니다.

샘플 앱의 ‘Stats‘ 탭으로 이동하여 몇 번의 클릭을 수집한 뒤 ‘Achievements‘ 탭으로 돌아가 ‘Query definitions‘와 ‘Query progress‘를 순서대로 실행하여 다음과 같이 플레이어 진행 상황을 표시할 수 있습니다.
 
App Achievements QueryPlayerAchievementsFinal
쿼리된 플레이어 업적 진행 상황

업적 잠금해제하기

업적 중 하나는 수동으로, 다른 하나는 통계를 사용하여 자동으로 잠금해제되도록 설정했습니다. 수동으로 업적을 잠금해제하는 코드를 살펴보고, 업적이 자동으로 잠금해제될 때 알림을 받는 방법을 알아봅시다.

앞서 언급했듯이 게임 코드에서 업적을 수동으로 잠금해제하면 악성 사용자가 획득하지 않은 업적을 잠금해제할 수 있는 공격 벡터가 노출될 가능성이 있습니다.
 
  1. AchievementService.cs를 열고 다음 메서드를 추가하여 업적의 수동 잠금해제 로직을 포함합니다.
  • 아주 간단한 프로세스입니다. Achievements.UnlockAchievements를 호출하고 AchievementIDs 배열을 전달하면 업적이 잠금해제됩니다.
 
  1. AchievementsUnlockAchievementCommand.cs 클래스를 Commands 폴더에 추가합니다.
  1. AchievementsViewModel.cs를 열어 다음 명령을 선언하고 인스턴스화합니다.
  1. 마지막으로 업적을 선택한 뒤에만 잠금해제 버튼을 클릭할 수 있도록 AchievementsView.xaml.cs를 열고 다음 명령줄을 AchievementsListView_SelectionChanged 메서드에 추가합니다.
앱을 실행하고 인증한 뒤 ‘Achievements‘ 탭으로 이동하여 ‘Query definitions‘를 클릭합니다. 그런 다음 ‘TOP_SECRET‘ 업적을 클릭하고 ‘Unlock‘ 버튼을 클릭합니다. UI는 변하지 않지만 Visual Studio의 ’디버그 출력’에 ‘UnlockAchievements Success’라는 메시지가 표시될 것입니다. 이 코드는 표시되는 업적과 숨긴 업적에 모두 적용된다는 점에 유의하세요.

이제 ‘Query progress‘를 클릭하면 업적이 목록에 표시될 것입니다. 숨겨진 업적은 앞서 살펴보았듯 잠금해제되기 전까지는 QueryPlayerAchievements 호출로 반환되지 않습니다.
 
App Achievements UnlockAchievement
수동 잠금해제된 숨겨진 업적

마지막으로 업적 잠금해제 알림을 구독하여 업적이 통계 진행 상황으로 잠금해제될 때 자체 코드를 실행해 봅시다.
 
  1. AchievementsService.cs를 열고 다음 메서드를 추가하여 업적 알림을 구독 또는 구독 해지합니다.
  • Achievements.AddNotifyAchievementsUnlockedV2를 호출하여 알림을 구성하고, 업적이 관련 통계 수집으로 잠금해제될 때 호출되는 콜백 메서드를 전달합니다. 이 메서드는 업적을 수동으로 잠금해제할 때도 호출됩니다.
  • AchievementsUnlockedCallback은 이 경우 ‘디버그 출력’에 들어갈 내용 한 줄만을 작성하지만, 여기서 플레이어에게 업적 잠금해제를 알리는 모든 UI를 처리합니다.
  • AddNotifyAchievementsUnlockedV2는 업적 잠금해제 알림을 중단하는 데 사용할 수 있는 notificationId를 반환합니다. 이 샘플에서는 구현하지 않지만, RemoveNotification 메서드가 그 역할을 합니다.
 
  1. QueryDefinitions 호출 후 간단히 알림을 받기 시작하려면 다음 QueryDefinitions 메서드 호출을 AchievementsService.cs의 ViewModelLocator.Main.StatusBarText = string.Empty 바로 위에 추가합니다.
앱을 재실행하고 인증한 다음 ‘Achievements‘ 탭으로 이동하여 ‘Query definitions‘ 버튼을 클릭합니다. 총 50번 클릭하는 업적의 진행 상황을 확인하려면 ‘Query progress‘ 버튼을 클릭합니다. 아직 잠금해제되지 않았다면 진행 상황을 확인하고 ‘Stats‘ 탭으로 이동하여 잠금해제가 될 때까지 클릭 횟수를 더 수집하세요.

Visual Studio의 ‘디버그 출력’에 업적이 잠금해제되었다는 메시지가 표시될 것입니다. 업적을 이미 잠금해제했다면 다음 섹션의 단계를 따라 업적을 리셋하고, 필요하다면 플레이어 통계도 리셋하세요.

플레이어 업적 조회 및 수정하기

언제든지 플레이어의 업적을 잠금해제하거나, 리셋하거나, 잠금 설정하는 등 수동으로 조정해야 하는 경우, 개발자 포털에서 이 작업을 수행할 수 있습니다.
 
  1. https://dev.epicgames.com/portal/에서 개발자 포털에 로그인합니다.
  2. 왼쪽 메뉴에서 해당 제품의 ‘게임 서비스’ > ‘업적’으로 이동합니다.
  3. 오른쪽 상단에서 ‘플레이어 조회’ 버튼을 클릭합니다.
  4. 플라이아웃 메뉴에서 플레이어의 제품 사용자 ID(PUID) 또는 계정별 ID(예: 에픽 계정 ID)를 입력하고 ‘검색’을 클릭합니다.
  5. 플레이어의 업적 목록이 현재 진행 상황 및 잠금해제 상태와 함께 표시됩니다. 이 목록에서 점 세 개 버튼을 클릭하여 이 업적을 잠금해제하거나 리셋할 수 있습니다.

‘모두 리셋’ 또는 ‘모두 잠금해제’ 버튼을 사용하여 모든 업적을 빠르게 토글할 수도 있지만, 이를 프로덕션 환경에서 사용할 때는 주의해야 합니다. 이 기능은 개발 도중에 가장 유용합니다. 또 다른 좋은 방법은 개발 중 업적 테스트용으로 별도의 디플로이를 생성하는 것입니다. 플레이어 데이터는 모두 특정 디플로이에 지정되기 때문입니다.

Developer Portal Achievements Player Lookup
개발자 포털에서 업적 잠금해제 또는 리셋하기

사용 제한 및 요구 사항

지금까지 살펴본 다른 서비스와 마찬가지로 업적은 모든 사용자에게 안정성과 가용성을 보장하기 위해 사용 제한을 두고 있습니다. 이 글이 작성된 시점의 제한 사항은 다음과 같습니다(최신 정보는 문서를 참조하세요).
 
  • 디플로이당 업적 수 최대 1,000개
  • 업적이 자동으로 잠금해제되는 통계 최대 3개

또한 업적은 다음 요구 사항을 준수해야 합니다.
 
  • 업적 ID 문자 수 최대 256자
  • 업적 ID에 포함할 수 없는 문자: , { ^ } % ` ] > [ ~ < # | & $ @ = ; ? \ ( ) * /
  • 아이콘 파일명에 포함할 수 없는 문자:  , { ^ } % ` ] > [ ~ < # | & $ @ = ; : + ? ! \ ( ) * /
  • 업적당 현지화되는 텍스트 베리언트 최대 22개
  • 업적 아이콘 파일 크기 최대 1.02MB
  • 업적 아이콘 해상도 최대 1024×1024 픽셀
  • PNG, JPG, BMP, 움직이지 않는 GIF 형식만 업적 아이콘으로 사용 가능

마지막으로 사용자당 또는 디플로이당 클라이언트 API 호출 또는 서버 호출에 대한 속도 제한이 있습니다. 이에 대한 정보는 문서에서 보실 수 있습니다.

코드 다운로드

아래에서 이 연재글에 쓰인 코드를 확인할 수 있습니다. 다운로드한 코드를 GitHub 저장소의 사용 지침에 따라 설정하세요.
 
이 연재글 시리즈의 전체 목록은 시리즈 참조를 확인하세요. 피드백이나 질문은 커뮤니티 포럼을 방문해 주세요.

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

    에픽은 통합되고 열려 있는 게임 커뮤니티를 지향합니다.
    에픽은 이러한 온라인 서비스를 모두에게 무료로 제공함으로써, 더 많은 개발자들이 자체 플레이어 커뮤니티에 서비스를 지원할 수 있도록 하는 것을 목표로 합니다.