올해 초, 우리는 도도 피크를 Apple의 새로운 Arcade 구독 서비스 런치 타이틀로 출시하는 영광을 누렸습니다. Apple Arcade는 Mac을 비롯한 모든 플랫폼에서 게임이 실행된다는 점이 핵심 셀링 포인트입니다. 우리는 Apple Arcade에 도도 피크를 출시하기 위해, 게임을 Apple의 Mac App Store 환경을 비롯해 iCloud, Game Center 등 Apple Service와 연동해야 했습니다.
Mac에서 게임을 출시하려 한다면 다음과 같은 사항을 고려해야 합니다.
- Mac App Store에서 게임을 출시하면, 게임 위주의 디지털 스토어를 사용하지 않는 플레이어에게도 게임이 자연스럽게 노출됩니다.
- iCloud 또는 Game Center를 사용하여 리더보드와 클라우드 저장을 처리하면, 앱에 서드파티 프레임워크를 도입하지 않아도 믿을 수 있는 퍼스트파티 서비스를 무료로 이용할 수 있습니다.
- Apple Arcade는 다양한 기기에 걸쳐 클라우드 저장 기능을 제공하기 위해 Game Center와 iCloud가 필요합니다.
언리얼 엔진은 iOS 앱을 위한 Game Center 및 iCloud를 지원합니다. 반면에, macOS용 빌드에 대한 동일한 수준의 지원은 제공되지 않습니다. 최근까지는 언리얼 게임을 Mac App Store에 출시하는 경우가 드물었죠.
다행히도, 누구나 이런 기능의 차이를 해결할 수 있습니다. 지난 몇 개월 동안, 저는 본의 아니게 언리얼 게임과 Mac의 Apple 서비스를 연동하는 작업의 전문가가 되었습니다. 여러분이 시행착오를 겪지 않도록 모든 노하우를 알려드리겠습니다. 이 가이드는 Mac 게임을 Apple 서비스와 연동하는 방법과 게임을 Mac App Store용으로 패키징하는 방법을 다룹니다.
Apple Developer 포털에서 Mac 앱 설정
게임을 Apple 서비스와 연동하여 Mac App Store에 등록하기 위한 첫 번째 단계는 Apple 백엔드에서 앱을 구성하는 작업입니다. 작업 방법을 보여주는 가이드는 많지만, 작업 완성도를 고려해서 다음과 같은 체크리스트를 준비했습니다.- Apple Developer Program에 가입합니다(1년에 $99 USD).
- 인증서, 식별자 및 프로필 페이지의 식별자 항목을 통해 앱을 생성합니다.
- 앱에 '
com.mycompany.mygame
'과 같은 유용한 번들 ID를 부여합니다. - iCloud와 Game Center 기능을 활성화하고, 따로 원하는 기능이 있으면 활성화합니다.
- 참고: 이미 게임의 iOS 버전용 앱 식별자가 있는 경우, iOS 앱 식별자 설정에서 'Mac' 기능을 선택하여 Mac 식별자를 자동 생성할 수 있습니다.
- 앱에 '
- 인증서, 식별자 및 프로파일 페이지의 인증서 항목을 통해 계정의 인증서를 생성합니다.
- 로컬에서 인증서 서명 요청을 생성합니다.
- 이를 통해 다음 용도의 인증서를 생성, 다운로드합니다.
- Mac 개발
- Mac 앱 배포
- Mac 인스톨러 배포
- 인증서, 식별자 및 프로파일 페이지의 기기 항목을 통해 테스트 또는 개발용 Mac을 추가합니다.
- 시스템 보고 기능을 통해 Mac UDID를 확인할 수 있습니다.
- (선택사항) 인증서, 식별자 및 프로필 페이지의 식별자 항목을 통해 iCloud 저장소를 생성합니다.
- 앱 식별자를 편집하고, iCloud 기능 옆에 있는 버튼으로 저장소를 배정합니다.
- 관련 프로비저닝 프로파일을 생성 및 다운로드합니다.
- Mac 개발 프로비저닝 프로필을 생성합니다.
- 반드시 새로 생성한 앱 식별자, 인증서, 기기를 선택합니다.
- Mac App Store 프로비저닝 프로필을 생성합니다.
- Mac 개발 프로비저닝 프로필을 생성합니다.
- App Store Connect에서 새로운 macOS 앱을 생성하고, 새로운 번들 식별자를 선택합니다.
- 언리얼에서 프로젝트를 엽니다. 프로젝트 설정에서 플랫폼 > iOS로 이동한 다음, 번들 식별자를 새로운 번들 ID로 설정합니다.
재미없는 부분은 여기까지입니다. 위 과정은 패키징하거나 게임을 테스트할 때 빛을 발합니다. 다음은 Game Center와 iCloud 관련 코드를 작성하는 법을 다룰 것입니다.
Mac 게임과 Apple API 연동
언리얼 엔진에는 게임이 macOS에서 iCloud나 Game Center와 소통할 수 있는 창구가 엔진에는 없으므로, 직접 구현해야 합니다. Apple 문서를 훑어보면, Apple OS API의 핵심 요소에 접근하려면 Objective-C 또는 Swift를 사용하는 것이 유일한 방법이라는 것을 알 수 있습니다.언리얼은 C++를 사용하므로, 듣기만 해도 벅차죠. 그러면 어떻게 해야 할까요? 스태틱 라이브러리를 컴파일 및 연결하는 데 시간을 낭비하실 건가요? 그럴 필요 없습니다!
Xcode의 잘 알려지지 않은 기능인 Objective-C++를 이용해 언리얼 엔진 소스와 동일한 방법으로 우회할 수 있습니다.
Objective-C++란?
Objective-C++는 이름 그대로 C++ 내에서 Objective-C 코드를 사용(또는 Objective-C에서 C++ 코드를 사용)할 수 있는 기능입니다. C++와 Objective-C 모두 C언어의 확장형이므로, C언어의 모든 구문과 추가 기능을 지원합니다. 두 언어가 충돌하는 경우도 있지만, Xcode에서 두 언어를 함께 사용해도 문제가 없는 경우가 대부분입니다.Objective-C 세부 가이드를 원하신다면, 미디엄(Medium)의 블로그를 추천합니다. Objective-C의 레퍼런스 카운팅 기반 메모리 관리를 우회하는 과정에는 몇 가지 위험 요소가 존재하지만, 문제가 없는 경우가 대부분입니다. 이제 여러분이 원하던 코드 예시를 살펴보겠습니다.
Mac에서 Game Center 지원
Apple은 Game Center 코딩에 관한 자체 가이드라인을 보유하고 있습니다. 요약하자면 다음과 같습니다.- 플레이어 인증
- 인증된 플레이어를 사용하여 업적, 리더보드 등을 호출합니다.
아래 코드를 컴파일하기 전에, 언리얼 빌드 시스템을 통해 게임 패키지에 필요 라이브러리를 추가해야 합니다. 게임의 Build.cs 파일에 있는 생성자에 다음과 같은 로직을 추가합니다.
if (Target.Platform == UnrealTargetPlatform.Mac) {
PublicFrameworks.AddRange(new string[]{"GameKit"});
}
오브젝티브-C++로 플레이어를 인증하는 방법의 예시입니다. 저는 오브젝티브-C 콜백을 authenticateHandler로 넘겼지만, 콜백 함수의 내부 코드가 C++를 포함할 수 있다는 점을 참고하시기 바랍니다.
#include <GameKit/GameKit.h>
void UMyBlueprintFunctionLibrary::GameCenterSignIn() {
#if WITH_EDITOR
// 에디터에서 실행하지 않음
#elif PLATFORM_MAC
// GameCenter에 비동기로 로그인. 코드에 다음 사항을 추가하는 것을 추천합니다
// 로그인이 오래 걸리는 경우를 제어할 수 있도록
// 델리게이트 또는 UBlueprintAsyncActionBase 포함.
dispatch_async(dispatch_get_main_queue(), ^{
[[GKLocalPlayer localPlayer]
setAuthenticateHandler:^(NSViewController *_Nonnull viewController, NSError *error) {
if ([[GKLocalPlayer localPlayer] isAuthenticated])
{
// 성공
return;
}
if (error)
{
// 실패
}
else if (viewController)
{
// 디스플레이 로그인
GKDialogController *presenter = [GKDialogController sharedDialogController];
presenter.parentWindow = [NSApp keyWindow];
[presenter presentViewController:(NSViewController * _Nonnull) viewController];
}
}];
});
#endif
}
업적을 잠금 해제하는 예시 코드입니다.
#include <GameKit/GameKit.h>
void UMyBlueprintFunctionLibrary::WriteAchievement(FString ID, float Percent)
{
#if PLATFORM_MAC
if (![[GKLocalPlayer localPlayer] isAuthenticated])
return;
// FString을 NSString으로 전환
NSString *nsID = [NSString stringWithUTF8String:TCHAR_TO_ANSI(*ID)];
GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:nsID];
achievement.percentComplete = Percent;
achievement.showsCompletionBanner = YES;
[GKAchievement reportAchievements:@[ achievement ]withCompletionHandler:^(NSError *error) {
if (error != nil)
{
NSLog(@"%@", [error localizedDescription]);
}
}];
#endif
}
Game Center 리더보드 API에 신호를 보내는 코드입니다.
#include <GameKit/GameKit.h>
void UMyBlueprintFunctionLibrary::WriteScoreToLeaderboard(FString LeaderboardID, int Integer) {
#if PLATFORM_MAC
if (![[GKLocalPlayer localPlayer] isAuthenticated])
return;
// FString을 NSString으로 전환
NSString *nsID = [NSString stringWithUTF8String:TCHAR_TO_ANSI(*LeaderboardID)];
GKScore *score = [[GKScore alloc] initWithLeaderboardIdentifier:nsID];
score.value = Integer;
[GKScore reportScores:@[score] withCompletionHandler:^(NSError *error) {
if (error != nil)
{
NSLog(@"%@", [error localizedDescription]);
}
}];
#endif
}
마지막으로 주의사항을 말씀드리자면, 언리얼 에디터 내의 코드로 Game Center 함수를 호출하면 이상 현상이 발생할 수 있습니다. 따라서 #if WITH_EDITOR 매크로로 Game Center 코드를 사용하거나, 게임의 언패키지 버전에서는 다른 방법으로 이 기능을 비활성화하는 것을 권장합니다.
클라우드 저장에 iCloud 사용
본 가이드는 iCloud의 복잡함을 전부 설명하지는 않습니다. iCloud로 게임에 클라우드 저장 기능을 추가하려면 우선 Apple 지침을 읽어보세요.iCloud는 플레이어가 정보를 읽거나 쓸 수 있는 커다란 키-밸류 스토어로 생각하시면 됩니다. iCloud용 저장 데이터를 모델링하는 방법은 여러 가지입니다.
- iCloud에서는 저장 오브젝트의 각 항목을 고유 항목으로 다루어야 합니다. 예를 들어 플레이어의 레벨 항목은 ‘Level’이라는 Integer, 캐릭터 이름은 ‘Name’이라는 String이 되어야 합니다.
- 저장 오브젝트를 바이너리 데이터로 직렬화하고, 저장 정보 전체를 단일 iCloud 항목에 하나의 바이너리 글로브로 업로드합니다.
도도 피크의 경우, 두 번째 방법을 선택했습니다. 언리얼이 디스크에 저장할 때 사용하는 것과 동일한 직렬 로직을 쓸 수 있기 때문이었습니다.
다시 말씀드리지만, 코드를 실행하기 전에, 빌드 시스템이 CloudKit 라이브러리를 사용하도록 다음과 같은 준비가 필요합니다.
if (Target.Platform == UnrealTargetPlatform.Mac) {
PublicWeakFrameworks.Add("CloudKit");
}
SaveGame 오브젝트를 직렬화하고 iCloud에 업로드하는 예시 코드는 다음과 같습니다.
#include <CloudKit/CloudKit.h>
#include "GameFramework/SaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
void UMyBlueprintFunctionLibrary::WriteSaveToCloud(USaveGame* MySave) {
TArray<uint8> ObjectBytes;
FMemoryWriter MemoryWriter(ObjectBytes, true);
FObjectAndNameAsStringProxyArchive Ar(MemoryWriter, false);
MySave->Serialize(Ar);
CKContainer *defaultContainer =
[CKContainer containerWithIdentifier:@"iCloud.unrealtutorial.mygame"];
if (defaultContainer == nil)
{
// 초기화 실패
return;
}
else
{
CKDatabase *DB = [defaultContainer privateCloudDatabase];
CKRecordID *recordId = [[[CKRecordID alloc] initWithRecordName:@"save_game_id"] autorelease];
// Apple 온라인 대시보드에서 RecordType "SaveGame" 설정 완료
CKRecord *record = [[CKRecord alloc] initWithRecordType:@"SaveGame" recordID:recordId];
// 언리얼 데이터 배열을 NSData 바이트로 전환
NSData *data = [NSData dataWithBytes:ObjectBytes.GetData() length:ObjectBytes.Num()];
record[@"SaveData"] = data;
// CKModifyRecordsOperation을 사용하여 기존 기록 업데이트
CKModifyRecordsOperation *modifyRecords =
[[CKModifyRecordsOperation alloc] initWithRecordsToSave:@[ record ]
recordIDsToDelete:nil];
modifyRecords.savePolicy = CKRecordSaveAllKeys;
modifyRecords.qualityOfService = NSQualityOfServiceUserInitiated;
modifyRecords.perRecordCompletionBlock = ^(CKRecord *results, NSError *error) {
if (error != nil)
{
NSLog(@"icloud save error: %@", error);
}
else
{
NSLog(@"icloud save success: %@", results);
}
};
[DB addOperation:modifyRecords];
}
}
SaveGame 데이터를 iCloud로부터 로드하여 새로운 SaveGame 오브젝트로 읽어들이는 예시 코드는 다음과 같습니다.
#include
#include "GameFramework/SaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
UCustomSaveGame* UMyBlueprintFunctionLibrary::LoadFromCloud() {
CKContainer *defaultContainer =
[CKContainer containerWithIdentifier:@"iCloud.unrealtutorial.mygame"];
if (defaultContainer == nil)
{
return nullptr;
}
else
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"TRUEPREDICATE"];
CKDatabase *publicDatabase = [defaultContainer privateCloudDatabase];
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"save_game_id"];
__block NSData *data = nil;
// 이 블록의 결과값을 계속 유지하여 데이터가 GC되지 않도록 합니다.
__block CKRecord *holdResults;
// 참고: 델리게이트, 세마포어 또는 다른 바으로 동기화되어야 합니다
[publicDatabase
fetchRecordWithID:recordID
completionHandler:^(CKRecord *results, NSError *error) {
holdResults = results;
if (error != nil)
{
NSLog(@"icloud load error: %@", error);
}
else
{
NSLog(@"icloud load success: %@", results);
data = [[NSData alloc] initWithData:(NSData *) results[@"SaveData"]];
}
}];
// Sleep으로 동기화하지 마시오 -- 예시에만 해당합니다
usleep(3000000); // 비동기 로드 대기
// 저장된 오브젝트로 데이터를 읽어들입니다
TArray ObjectBytes;
ObjectBytes.AddUninitialized(data.length);
FMemory::Memcpy(ObjectBytes.GetData(), data.bytes, data.length * sizeof(uint8));
FMemoryReader MemoryReader(ObjectBytes, true);
FObjectAndNameAsStringProxyArchive Ar(MemoryReader, true);
UCustomSaveGame *LoadedSave = Cast( UGameplayStatics::CreateSaveGameObject(UCustomSaveGame::StaticClass()));
LoadedSave->Serialize(Ar);
return LoadedSave;
}
}
상기 코드는 작업 시작에 도움을 주려는 것일 뿐입니다. 각자 필요한 사항이 다르므로, 클라우드 저장을 게임에 완전히 적용하려면 다음과 같은 사항을 고려해야 합니다.
- 게임이 얼마나 자주 저장됩니까? 얼마나 자주 불러옵니까? 저장하고 불러올 때마다 클라우드와 소통해야 합니까, 아니면 일부만 그렇습니까?
- 동일한 iCloud 기록을 읽고 쓰기 위해서 여러 기기를 지원해야 합니까?
- 오프라인 진행률은 어떻게 처리합니까?
- 플레이어의 로컬 저장 데이터와 클라우드 데이터가 충돌하면 게임에서 어떻게 처리합니까?
Mac iCloud 코드를 에디터에서 실행하려 하거나 앱을 익스포트한 다음 실행하려 하면, 게임은 자격이 없다며 예외 오류를 띄울 것입니다. Game Center 작업과 마찬가지로, 에디터에서 iCloud 코드를 실행하지 않는 것이 좋습니다.
다음 항목은 개발 빌드에서 iCloud 코드를 실행하는 방법을 다룹니다.
코드 서명 및 패키징
이제 이 작업을 끝낼 때가 됐습니다. 언리얼 게임을 Apple API로 실행하고, Mac App Store에 업로드하는 작업의 마지막 관문입니다. 게임의 코드 서명, 자격, 프로퍼티 리스트가 필요합니다.블로그 게시글로 써도 될 분량이지만, 요약하자면 다음과 같습니다.
- 코드 서명은 Apple이 실행 가능한 코드의 실제작자를 확인하고, 코드가 조작되지 않았음을 보장하기 위해 사용하는 수단입니다. 자세한 내용은 여기서 확인하세요.
- 자격은 앱이 활용할 수 있는 iCloud 등 안전한 OS 기능 이용을 위해 코드 서명 시 앱에 키값 쌍을 넣는 것입니다. 자세한 내용은 여기서 확인하세요.
- 정보 프로퍼티 리스트(Information Property List) 또는 ‘Info.plist’는 App Icon 위치 또는 지원 언어 등 애플리케이션과 App Store의 핵심 설정 데이터를 포함합니다. 자세한 내용은 여기서 확인하세요.
언리얼이 iOS 프로비저닝 프로파일, 인증서와 함께 패키징을 운용하는 방식에 익숙하신가요? 이 작업도 거의 비슷하지만, 언리얼과 Xcode가 대신 수행했던 단계를 스스로 해결해야 합니다.
로컬 개발 패키징 및 서명
게임을 App Store에 출시하기 전에, 문제가 없는지 확인해야 합니다. 그래서 로컬로 실행할 수 있는 빌드는 어떻게 만들 수 있을까요? 앞서 말씀드렸지만, iCloud 자격 없이 애플리케이션 서명을 하면 iCloud 함수 호출 시 게임이 비정상 종료됩니다.이제 자격 누락 문제를 고쳐봅시다. 이 과정은 직관적이지만 지루합니다. Mac 애플리케이션에 새로운 자격을 도입하려면, 앱에 프로비저닝 프로필을 부여한 다음, 필요 자격을 확보하여 서명해야 합니다. 방법은 다음과 같습니다.
언리얼에서 게임을 패키징합니다. 단말기에서 새로 익스포트한 앱이 있는 디렉터리로 이동합니다. 개발 프로비저닝 프로파일 사본을 생성한 다음, 이름을 '
embedded.provisionprofile'
로 바꿉니다. 해당 프로비저닝 프로파일을 'YourGame.app/Contents/embedded.provisionprofile'
로 복사합니다.새로운 코드 서명으로 전부 묶으려면, 애플리케이션에 추가하고 싶은 자격을 보유한 XML 파일부터 생성해야 합니다. 파일 내용은 다음과 같습니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>TEAMID.com.mycompany.mygame</string>
<key>com.apple.developer.icloud-container-environment</key>
<string>Development</string>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.mycompany.mygame</string>
</array>
<key>com.apple.developer.game-center</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
위 XML을 수정하여 다음이 가능하도록 합니다.
com.apple.application-identifier
는 전체 앱 번들의 식별자입니다. (중요: 번들 식별자는 team ID로 시작해야 합니다. Team ID는 ‘3Y1CL48M1K’와 같이 문자와 숫자로 이루어진 String입니다)com.apple.developer.icloud-container-identifiers
배열에 iCloud 저장소 ID를 추가합니다.
이 텍스트 파일을 ‘
entitlements.plist
’로 저장합니다.(참고로,
codesign -dv --entitlements - AppName.app
으로 원하는 애플리케이션의 자격을 확인할 수 있습니다. 디버깅 시 유용합니다.)앱을 정상적으로 서명하려면, 실행 가능한 모든 코드부터 서명한 다음, 앱 자체를 서명해야 합니다. 이 과정은 다음과 같은 단일 커맨드로 수행할 수 있습니다.
codesign --deep -f -v -s "3rd Party Mac Developer:" --entitlements entitlements.plist MyGame.app
됐습니다! Finder에서 앱을 두 번 클릭하면, 게임 플레이 및 iCloud, Game Center 요청을 할 수 있습니다!
배포를 위한 패키징 및 서명
앱을 Mac App Store에 보내는 방법은 대동소이하지만, 할 일이 약간 더 있습니다. 이해를 돕기 위해 이 과정을 한 번만 직접 수행한 다음, 스크립트를 작성하여 자동화하세요.우선, Distribution을 활성화한 Shipping 패키지로 게임을 익스포트합니다.
프로퍼티 리스트를 편집하여 스토어 페이지에 보낼 추가 정보를 넣습니다. 예를 들어 GCSupportedGameControllers로 지원 컨트롤러를 나열하고, CFBundleLocalizations로 지원 언어를 설정할 수 있습니다. (App Store에서는 언리얼 현지화 지원을 자동 감지할 수 없습니다) 수동으로 버전 및 번들 ID를 편집할 수도 있습니다. 앱의 LSApplicationCategoryType을 설정해야 애플리케이션 로더의 수락을 받을 수 있습니다.
Distribution 프로비저닝 프로필을 복사한 다음, 이름을 '
embedded.provisionprofile'
로 바꿉니다. 해당 프로비저닝 프로필을 'YourGame.app/Contents/embedded.provisionprofile'
로 복사합니다.개발 빌드의 서명 과정과 달리, 익스포트된 .app이 Mac App Store의 승인을 받을 수 있도록 정리 작업을 할 것입니다.
우선, Mac App Store는 32비트 실행 가능 코드를 지원하지 않습니다(macOS 10.15 이상 버전도 마찬가지입니다). 언리얼은 32비트 코드가 있는 게임을 다이내믹 오디오 라이브러리와 자동으로 번들화하므로, 관련 코드를 제거해야 합니다. 다행히 이 문제는 컴퓨터공학 역사상 가장 흔한 사례이므로, 문제를 해결할 툴이 존재합니다. lipo 커맨드를 활용하여 다음과 같이 실행합니다.
lipo MyGame.app/Contents/UE4/Engine/Binaries/ThirdParty/Ogg/Mac/libogg.dylib -remove i386 -output MyGame.app/Contents/UE4/Engine/Binaries/ThirdParty/Ogg/Mac/libogg.dylib
lipo MyGame.app/Contents/UE4/Engine/Binaries/ThirdParty/Vorbis/Mac/libvorbis.dylib -remove i386 -output MyGame.app/Contents/UE4/Engine/Binaries/ThirdParty/Vorbis/Mac/libvorbis.dylib
lipo MyGame.app/Contents/UE4/Engine/Binaries/ThirdParty/OpenVR/OpenVRv1_0_16/osx32/libopenvr_api.dylib -remove i386 -output MyGame.app/Contents/UE4/Engine/Binaries/ThirdParty/OpenVR/OpenVRv1_0_16/osx32/libopenvr_api.dylib
언리얼이 자체 번들 ID를 가진 익스포트된 게임에 싱글 서브-컴포넌트를 추가하면, 애플리케이션 로더는 광기에 휩싸입니다. 이 컴포넌트는 '
RadioEffectUnit.component'
입니다. 오디오 이펙트로 보이지만, 빌드 시스템이 왜 이렇게 다루는지는 잘 모르겠습니다. 다행히도, RadioEffectUnit을 사용하지 않으면 그냥 앱에서 지우면 됩니다.
rm -rf MyGame.app/Contents/Resources/RadioEffectUnit.component
rm -rf MyGame.app/Contents/UE4/Engine/Build
정리가 끝났으니 빌드 서명 작업을 재개합니다.
코드 서명을 진행하기에 앞서서, 애플리케이션에 추가하고 싶은 자격을 보유한 XML 파일부터 생성해야 합니다. 파일 내용은 다음과 같습니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>TEAMID.com.mycompany.mygame</string>
<key>com.apple.developer.icloud-container-environment</key>
<string>Production</string>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.mycompany.mygame</string>
</array>
<key>com.apple.developer.game-center</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
위 XML을 변경하여 다음이 가능하도록 합니다.
com.apple.application-identifier
는 전체 앱 번들의 식별자입니다. (중요: 번들 식별자는 team ID로 시작해야 합니다. Team ID는 '3Y1CL48M1K'와 같이 문자와 숫자로 이루어진 String입니다)com.apple.developer.icloud-container-identifiers
에 iCloud 저장소 ID를 추가합니다.
이 텍스트 파일을 '
entitlements.plist
'로 저장합니다.서명할 시간입니다! 배포의 경우 게임 바이너리, 모든 다이내믹 라이브러리 그리고 .app 자체를 서명해야 합니다. 우리는 다음을 통해 해당 작업을 수행했습니다.
codesign -f -v -s "3rd Party Mac Developer Application:" --entitlements entitlements.plist MyGame.app/Contents/MacOS/MyGame
이 커맨드는
find
를 이용해 파일 내 모든 다이내믹 라이브러리(.dylibs)를 서명합니다.
find MyGame.app/Contents/ | grep .dylib | xargs codesign -f -v -s "3rd Party Mac Developer Application:" --entitlements entitlements.plist
이제 앱을 통째로 서명합니다.
codesign -f -v -s "3rd Party Mac Developer Application:" --entitlements entitlements.plist MyGame.app/
전부 서명했으니, 이제 앱을 패키징할 수 있습니다. 업로드 가능한 .pkg를 생성하려면 다음을 실행합니다.
productbuild --component MyGame.app/ /Applications --sign "3rd Party Mac Developer Installer:" MyGame.pkg
이제 시스템에서 애플리케이션 로더를 엽니다. ‘선택’ 버튼을 클릭하여 새로운 .pkg 파일을 선택합니다. 애플리케이션 로더가 패키지의 오류를 스캔하고, App Store Connect 포털에 패키지를 업로드합니다.
일반적인 패키징 문제
마지막으로, 위 과정 수행 중에 몇 가지 문제가 발생할 수 있습니다.우선, 패키징 문제가 발생할 수 있습니다. 언리얼 포럼의 관련 글은 https://forums.unrealengine.com/community/community-content-tools-and-tutorials/68346-how-to-create-the-proper-pkg-file-for-deployment-to-the-macstore/page2에서 확인할 수 있습니다.
애플리케이션 로더가 'ERROR ITMS-90135'를 이유로 앱을 거절하는 경우도 있습니다. 실행 가능한 항목을 App Store에 제출하기 위해 재서명할 수 없다는 뜻입니다. 해당 앱은 지원하지 않거나 정식 출시되지 않은 툴을 이용해 빌드 또는 서명되었을 수 있습니다. 이 오류 메시지는 악몽 그 자체입니다. ‘문제가 있기는 한데 정확히 무슨 문제인지는 모르겠습니다’라는 뜻이기 때문입니다. 우리 팀은 이 문제 때문에 며칠을 허비했는데, 알고 보니 빌드에 디버그 심볼을 추가한 것 때문에 앱 처리가 되지 않았던 것이었습니다. 프로젝트 설정에서 ‘디버그 파일 포함’을 반드시 체크 해제하시기 바랍니다.
마지막으로, 애플리케이션 로더 거절 이유 중 가장 흔한 것은 애플리케이션 아이콘 문제입니다. 조건에 맞는 .icns 파일을 만들 수 있는 툴과 기술은 매우 많습니다. 우리는 App Wrapper 3으로 PNG 파일에서 아이콘 세트를 생성하여 작업을 마칠 수 있었습니다.