February 26, 2020
Integrating Unreal into Apple's Mac ecosystem
Earlier this year, we had the privilege of releasing Dodo Peak as one of the launch titles on Apple’s new Arcade subscription service. One of the main selling points for Apple Arcade is that games work across all devices, including Macs. To release Dodo Peak on Apple Arcade, we had to integrate with Apple’s Mac App Store ecosystem along with Apple services like iCloud and Game Center.
There are good reasons why anyone targeting a Mac release should consider doing this:
- On the Mac App Store, your game can organically reach players who aren’t plugged into more games-focused digital stores.
- Using iCloud or Game Center to handle things like leaderboards and cloud saves means you can rely on free, trusted, first-party services rather than integrating third-party infrastructure into your app.
- Apple Arcade requires Game Center and iCloud as part of its promise of cloud saves accessible across devices.
You’ll be happy to learn Unreal supports Game Center and iCloud for iOS targets out of the box. The bad news is that the same support isn’t built-in for builds targeting macOS. Until recently, targeting your Unreal game to the Mac App Store was a much less common use case.
Fortunately, anyone can manually work around these feature gaps. Over the last few months, I accidentally became an expert on integrating Unreal games with Apple services on Mac, and I’d like to get all that knowledge out there so you don’t have to piece it together yourself. This guide will cover both how to integrate your Mac game with Apple services and how to package your game for the Mac App Store.
Setting up your Mac app in the Apple developer portal
The first step toward getting your game working with Apple services and up on the Mac App Store is configuring your app in Apple’s backend. There are plenty of guides that show you how to do this, but for the sake of completion, here is a quick checklist of what you’ll have to do:- Join the Apple Developer Program (this costs a yearly $99 fee).
- Create your app via the Identifiers section of the Certificates, Identifiers & Profiles page.
- Give your app a useful bundle ID like “
com.mycompany.mygame
”. - Enable iCloud and Game Center capabilities, plus any others you want.
- Note: if you already have a working app identifier for an iOS version of your game, you can auto-generate a Mac identifier for it by selecting the “Mac” capability from the iOS app identifier’s settings.
- Give your app a useful bundle ID like “
- Create certificates for your account in the Certificates section of the Certificates, Identifiers & Profiles page.
- Locally, create a Certificate Signing Request.
- Use it to create and download certificates for:
- Mac Development
- Mac App Distribution
- Mac Installer Distribution
- Add any test or development Macs you own in the Devices section of the Certificates, Identifiers & Profiles page.
- You can find Mac UDIDs via the System Report feature.
- (Optional) Create an iCloud container via the Identifiers section of the Certificates, Identifiers & Profiles page.
- Edit your app identifier and assign the container using the button next to the iCloud capability.
- Create and download relevant provisioning profiles.
- Create a Mac Development provisioning profile.
- Be sure to select your newly-created app identifier, certificates, and devices.
- Create a Mac App Store provisioning profile.
- Create a Mac Development provisioning profile.
- Create a new macOS app in App Store Connect and select your new bundle identifier.
- Open your project in Unreal. In your project settings, go to Platforms > iOS, and set your Bundle Identifier to be your new Bundle ID.
That’s the boring part. This will all come in handy later when it comes time to package or test your game. The next section will go over how to actually write code around features like Game Center and iCloud.
Integrating your Mac game with Apple APIs
Since the engine doesn’t have the hooks you need for your games to talk to iCloud or Game Center on macOS, you’ll have to implement them yourself. A quick scan of Apple’s documentation shows us that the only way to crack the shell of their operating system APIs and get at the nutrient-rich center is by using Objective-C or Swift.That’s a little scary to hear, since Unreal uses C++. So how do you do it? Are you in for a nasty time compiling and linking static libraries? Nope!
You can get around it the same way the Unreal Engine source does: with a powerful, little-publicized feature of Xcode called Objective-C++.
What is Objective-C++?
Objective-C++ is what it sounds like: the ability to inline Objective-C code inside of your C++ (or vice versa). Both C++ and Objective-C are supersets of C, meaning they support all of the syntax of vanilla C plus additional features. While there are cases where the two languages are at odds, for the most part, using both of them together in Xcode just works. For folks looking for a more in-depth guide to Objective-C++, I’d recommend this Medium article. There are a few dangers to avoid around Objective-C’s reference counting-based memory management, but for the most part, things just work. For now, let’s get to what you came here for: code samples.
Supporting Game Center on Mac
Apple has their own guides on how to code for Game Center, but the short version goes something like:- Authenticate your player
- Use that authenticated player to make calls to achievements, leaderboards, and so on
Before you can compile any of the following code, you’ll need to surface the requisite libraries to your package via Unreal’s build system. In your game’s Build.cs file, add the following logic to your constructor:
if (Target.Platform == UnrealTargetPlatform.Mac) {
PublicFrameworks.AddRange(new string[]{"GameKit"});
}
Here’s a sample of how one might authenticate their player using Objective-C++. Note I am passing an Objective-C callback to the authenticateHandler, but the code inside that callback function can contain C++.
#include <GameKit/GameKit.h>
void UMyBlueprintFunctionLibrary::GameCenterSignIn() {
#if WITH_EDITOR
// do not run in editor
#elif PLATFORM_MAC
// asynchronously log into GameCenter. In your code I recommend wiring this up
// with delegates or UBlueprintAsyncActionBase so that you can handle cases
// where logging in takes a while.
dispatch_async(dispatch_get_main_queue(), ^{
[[GKLocalPlayer localPlayer]
setAuthenticateHandler:^(NSViewController *_Nonnull viewController, NSError *error) {
if ([[GKLocalPlayer localPlayer] isAuthenticated])
{
// Success
return;
}
if (error)
{
// Failure
}
else if (viewController)
{
// display login
GKDialogController *presenter = [GKDialogController sharedDialogController];
presenter.parentWindow = [NSApp keyWindow];
[presenter presentViewController:(NSViewController * _Nonnull) viewController];
}
}];
});
#endif
}
Here’s some example code that unlocks achievements.
#include <GameKit/GameKit.h>
void UMyBlueprintFunctionLibrary::WriteAchievement(FString ID, float Percent)
{
#if PLATFORM_MAC
if (![[GKLocalPlayer localPlayer] isAuthenticated])
return;
// convert FString to 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
}
And here’s code calling out to Game Center’s leaderboard API.
#include <GameKit/GameKit.h>
void UMyBlueprintFunctionLibrary::WriteScoreToLeaderboard(FString LeaderboardID, int Integer) {
#if PLATFORM_MAC
if (![[GKLocalPlayer localPlayer] isAuthenticated])
return;
// convert FString to 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
}
As a final warning: calling Game Center functions from your code within the Unreal Editor can lead to some weird behavior, so I suggest you wrap your Game Center code in
#if WITH_EDITOR
macros or disable these features in unpackaged versions of your game in some other way.Using iCloud for cloud saves
This guide will not explain all the intricacies of iCloud. Apple has some primers you should take a look at before diving into using iCloud to add cloud save support to your title.For your purposes, you can think of iCloud as a big key-value store that your players can write to and read from. There are several ways to model save data for iCloud:
- Treat each field of your save object as its own field in iCloud (for example, your player’s level field would be an Integer called “Level,” their character’s name would be a String called “Name,” and so on).
- Serialize your save object to binary data and upload the entire save as one binary glob to a single iCloud field.
For Dodo Peak, my team decided to go with option #2, since it allowed us to use the same serialization logic Unreal uses to write saves out to disk.
Once again, before we can run any code, we’ll have to tell the build system to use the CloudKit library:
if (Target.Platform == UnrealTargetPlatform.Mac) {
PublicWeakFrameworks.Add("CloudKit");
}
Here’s some EXAMPLE code that serializes a SaveGame object and uploads it to 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)
{
// initialization failed
return;
}
else
{
CKDatabase *DB = [defaultContainer privateCloudDatabase];
CKRecordID *recordId = [[[CKRecordID alloc] initWithRecordName:@"save_game_id"] autorelease];
// RecordType "SaveGame" is configured in Apple's online iCloud dashboard
CKRecord *record = [[CKRecord alloc] initWithRecordType:@"SaveGame" recordID:recordId];
// Convert Unreal data array to NSData bytes
NSData *data = [NSData dataWithBytes:ObjectBytes.GetData() length:ObjectBytes.Num()];
record[@"SaveData"] = data;
// use CKModifyRecordsOperation to allow updating existing records
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];
}
}
And here’s EXAMPLE code that loads SaveGame data from iCloud and reads the binary data into a new SaveGame object:
#include <CloudKit/CloudKit.h>
#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;
// hold onto results in this block to keep data from being GC'd from under us
__block CKRecord *holdResults;
// NOTE: this should be synchronized using delegates, semaphores, or some other means
[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"]];
}
}];
// DO NOT SYNCHRONIZE WITH A SLEEP -- this is for example purposes only
usleep(3000000); // wait for async load
// read data into a save object
TArray<uint8> 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<UCustomSaveGame>( UGameplayStatics::CreateSaveGameObject(UCustomSaveGame::StaticClass()));
LoadedSave->Serialize(Ar);
return LoadedSave;
}
}
The above code is just to get you started. Everyone’s needs are different and to fully implement cloud saves for your game, you’ll have to consider things like:
- How often is your game saving? How often are you loading? Should you talk to the cloud for each save and load or just some?
- Do you need to support multiple devices writing and reading to the same iCloud record?
- How do you handle offline progress?
- How will your game handle cases where your player’s local save might conflict with the data in the cloud?
Note that if you try and run any Mac iCloud code from the editor or after exporting your app, your game will throw an exception complaining about a missing entitlement. Like with Game Center, I advise you to exclude any iCloud code from running in your editor.
The next section outlines how to actually run that iCloud code in your development builds.
Signing and packaging your game
Time to wrap all this up, literally. The final hill to climb on your path toward running your Unreal game with Apple APIs and uploading it to the Mac App Store requires configuring your game’s code signature, entitlements, and Plist.One could write a blog post just on these, but in quick summary:
- Codesigning is Apple’s way of ensuring executable code is actually from who it says it is, and that the code has not been tampered with. Find out more here.
- Entitlements are key-value pairs embedded into your app during code signing that denote certain secure OS features your app can take advantage of, like iCloud. Find out more here.
- An Information Property List or “Info.plist” includes essential configuration data for your application as well as the App Store, including things like the location of your App Icon or supported languages. Find out more here.
If you’re familiar with how Unreal handles packaging with provisioning profiles and certificates for iOS, this will be a lot like that, but you’ll have to do the steps Unreal and Xcode would normally do for you on your own.
Packaging and signing for local development
Before you push your game to the App Store, you’ll probably want to make sure it, you know, works. So how do you make a build that can run locally? As mentioned earlier, your game will crash if you try and call an iCloud function without signing your application with iCloud entitlements.So let’s fix those missing entitlements. The process is straightforward but tedious. To introduce new entitlements into a Mac application, you’ll need to supply the app with a provisioning profile and then sign it with necessary entitlements. Here’s how.
From Unreal, package your game. Then, in a terminal, navigate to the directory containing your newly-exported app. Create a copy of your Development provisioning profile, then rename it to “
embedded.provisionprofile
”. Copy that provisioning profile to “YourGame.app/Contents/embedded.provisionprofile
”.To bring it all together with a new code signature, you’ll first have to create an XML file with the entitlements you’d like to add to the application. That file might look something like:
<?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>
Make sure you change the above XML so that:
- Your
com.apple.application-identifier
is your full app bundle identifier (important: make sure the bundle identifier starts with your team ID, which is a string of letters and numbers like “3Y1CL48M1K”). - Change the
com.apple.developer.icloud-container-identifiers
array to contain your iCloud container ID.
Save this text file as “
entitlements.plist
”.(As an aside, you can view any application’s entitlements with
codesign -dv --entitlements - AppName.app
, which may come in handy for debugging.)To properly sign the app, you’ll have to first sign all of the executable code then sign the app itself. You can do this all with one command using:
codesign --deep -f -v -s "3rd Party Mac Developer:" --entitlements entitlements.plist MyGame.app
And that’s it! Double-click your app in Finder and you should be able to play your game and make iCloud and Game Center requests!
Packaging and signing for distribution
Sending your App to the Mac App Store follows the same steps but is slightly more involved. Go through these steps manually one time to understand them, and then write your own script to do it automatically.First, export your game as a Shipping package with Distribution enabled.
Next, edit your plist to get any additional information you might want to send to your store page. For example, you can list supported controllers with GCSupportedGameControllers and use CFBundleLocalizations to set supported languages (the App Store cannot automatically detect Unreal’s localization support) as well as manually edit version numbers and bundle IDs. You must set LSApplicationCategoryType for your app to be accepted by the Application Loader.
Copy your Distribution provisioning profile to then rename it to “
embedded.provisionprofile
”. Copy the provisioning profile to “YourGame.app/Contents/embedded.provisionprofile
”.Now, unlike signing your development builds, we’ll have to do a little cleanup of our exported .app in order to get it approved by the Mac App Store.
Firstly, the Mac App Store does not support 32-bit executable code (neither do versions of macOS 10.15 and up). Unreal automatically bundles some dynamic audio libraries with your game that contain 32-bit code, so we’ll have to remove that code. Thankfully, this has been a fairly common problem over the course of computer science history and so there are tools to do just that. You can take advantage of the lipo command and run:
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
Next, Unreal adds a single sub-component to your exported game that already has its own Bundle ID, which drives the Application Loader crazy. This component is the “
RadioEffectUnit.component
.” As far as I can tell, it’s an audio effect, though I’m not sure why it is treated this way by the build system. The good news is that if you aren’t using the RadioEffectUnit, you can just remove it from your app:
rm -rf MyGame.app/Contents/Resources/RadioEffectUnit.component
rm -rf MyGame.app/Contents/UE4/Engine/Build
With this cleanup out of the way, we can resume signing the build.
To prepare your code signature, you’ll first have to create an XML file with the entitlements you’d like to add to the application. That file might look something like:
<?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>
Make sure you change the above XML so that:
- Your
com.apple.application-identifier
is your full app bundle identifier (important: make sure the bundle identifier starts with your team ID, which is a string of letters and numbers like “3Y1CL48M1K”). - Change the
com.apple.developer.icloud-container-identifiers
to contain your iCloud container ID.
Save this text file as “
entitlements.plist
”.It’s signing time! For distribution, you’ll have to sign the game binary, all dynamic libraries, and finally the .app itself. We did this with:
codesign -f -v -s "3rd Party Mac Developer Application:" --entitlements entitlements.plist MyGame.app/Contents/MacOS/MyGame
This command signs all dynamic libraries (.dylibs) in the file using
find
:
find MyGame.app/Contents/ | grep .dylib | xargs codesign -f -v -s "3rd Party Mac Developer Application:" --entitlements entitlements.plist
Then sign the whole app:
codesign -f -v -s "3rd Party Mac Developer Application:" --entitlements entitlements.plist MyGame.app/
With everything signed, we can now package the app. To generate an uploadable .pkg, run:
productbuild --component MyGame.app/ /Applications --sign "3rd Party Mac Developer Installer:" MyGame.pkg
Now open the Application Loader app on your system. Click the “choose” button and select your new .pkg file. The Application Loader will scan your package for any errors then upload it to the App Store Connect portal.
Common packaging problems
Finally, I have a few quick issues to point out that might come up during your journey.First, if you encounter any problems with packaging, the ongoing thread for these issues in the Unreal forums is: https://forums.unrealengine.com/community/community-content-tools-and-tutorials/68346-how-to-create-the-proper-pkg-file-for-deployment-to-the-macstore/page2
Next, the Application Loader might reject your app with the reason "ERROR ITMS-90135: The executable could not be re-signed for submission to the App Store. The app may have been built or signed with non-compliant or pre-release tools.” This error message is truly a nightmare, since it basically means “something is wrong but we are not able to tell you what.” My team hit this issue for several days, and in our case, it turned out we were including debug symbols in our build, which was breaking the app processing. Make sure you uncheck “Include Debug Files” in your project settings.
Finally, the most common Application Loader rejection reason we’ve seen has to do with our application icon. There are a variety of tools and techniques for generating a .icns file that is up to spec, but in our case, our workflow ended up using App Wrapper 3 to generate an icon set from a PNG file.