Интеграция Unreal в экосистему Apple Mac
Меня зовут Кайл Эрф, я занимаю должность технического директора в Moving Pieces Interactive, небольшой студии из Бруклина, Нью-Йорк. Мы создали игру Dodo Peak, являющуюся современным видением всеми любимых аркадных платформеров 80-х.
Ранее в этом году мы удостоились чести выпустить Dodo Peak в качестве одного из стартовых тайтлов в Apple Arcade subscription service. Один из коммерческих аргументов Apple Arcade — игры работают на всех устройствах, включая Mac. Чтобы выпустить Dodo Peak в Apple Arcade, нам пришлось интегрироваться с экосистемой Mac App Store и другими сервисами Apple, такими как iCloud и Game Center.
Почему следует взглянуть в сторону выпуска приложений для Mac:
- В Mac App Store ваша игра может достичь пользователей, которые не пользуются игро-ориентированными цифровыми магазинами.
- Используя iCloud или Game Center, чтобы работать, например, с лидербордами и облачным сохранением, вы можете положиться на бесплатные, доверенные услуги напрямую, не привлекая сторонние сервисы.
- Apple Arcade требует Game Center и iCloud, как часть своего обещания облачных сохранений, доступных на всех устройствах.
Unreal поддерживает Game Center и iCloud для реализации на iOS. Плохая новость в том, что такая же поддержка не встроена для сборок, ориентированных на macOS. До недавнего времени игры на Unreal, нацеленные на Mac App Store, были достаточно редким явлением.
К счастью, существует возможность преодолеть эти проблемы вручную. За последние несколько месяцев я, сам того не ожидая, стал экспертом в области интеграции созданных на Unreal игр в сервисы Apple для Mac. Гайд, главной целью которого является донесение моего опыта до читателей, раскроет, как интегрировать вашу Mac игру с сервисами Apple и как правильно упаковать вашу игру для Mac App Store.
Настройка Mac приложения на портале разработчика Apple
Первый шаг к тому, чтобы ваша игра работала с сервисами Apple и в Mac App Store — настройка вашего приложения в бэкэнде Apple. Нужно будет:
- Присоединиться к Apple Developer Program (ежегодная плата — $99)
- Создать свое приложение через секцию identifiers на странице Certificates, Identifiers & Profiles.
- Задайте Bundle Identifier для вашего приложения «com.mycompany.mygame».
- Включите возможности iCloud, Game Center и любые другие, которые вам интересны.
- Примечание: если у вас уже есть рабочий идентификатор приложения для IOS версии вашей игры, вы можете автоматически сгенерировать идентификатор Mac для нее, выбрав функцию “Mac” в настройках идентификатора приложения IOS.
- Создать сертификаты для вашего аккаунта в секции Certificates на странице Certificates, Identifiers & Profiles.
- В зависимости от вашей локации, создайте Certificate Signing Request.
- Используйте его для создания и скачивания следующих сертификатов:
- Mac Development
- Mac App Distribution
- Mac Installer Distribution
- Добавьте все ваши компьютеры Mac в секции Devices на странице Certificates, Identifiers & Profiles.
- Найти Mac UDIDs можно с помощью функции System Report.
- (Опционально) Создайте хранилище iCloud в секции Identifiers на странице Certificates, Identifiers & Profiles.
- Отредактируйте свой идентификатор и назначьте хранилище кнопку рядом с iCloud capability.
- Создайте и скачайте соответствующие профили обеспечения.
- Создайте профиль обеспечения Mac Development.
- Удостоверьтесь, что выбраны созданный вами ранее идентификатор, сертификаты и устройства. - Создайте профиль обеспечения Mac App Store.
- Создайте профиль обеспечения Mac Development.
- Создайте новое приложение macOS в App Store Connect и выберите ваш новый Bundle Identifier.
- Откройте ваш проект в Unreal. В настройках проекта пройдите в Platforms > iOS и настройте ваш Bundle Identifier, как новый Bundle ID.
Это все весьма пригодится, когда придет время упаковывать или тестировать вашу игру.
Внедрение Apple API в вашу игру для Mac
Так как у движка нет инструментов, с помощью которых ваша игра могла бы взаимодействовать с iCloud или Game Center, придется внедрять их самостоятельно/вручную. При беглом просмотре документации Apple становится ясно, что единственный способ раскусить орех их операционной системы программного интерфейса (API) и получить самую сочную сердцевину — использование Objective-C или Swift.
Звучит немного страшно, учитывая, что Unreal использует C++. Неужели придется возиться с компиляцией и связыванием статических библиотек? Никак нет!
Вы можете обойти это таким же способом, каким пользуется код Unreal; с помощью мощной, малоизвестной возможности икскода (X code), называемой Objective-C++.
Что такое Objective-C++
Objective-C++ — это возможность встраивать код от Objective-C в ваш С++ или наоборот. С++ и Objective-C надстройки построены на С, следовательно, поддерживают весь синтаксис обычного С и дополнительные возможности. Есть случаи, когда эти языки отличаются друг от друга, но почти всегда их можно использовать вместе в икскоде (Xcode).
Для желающих более глубокого гайда по Objective-C++, я бы посоветовал эту статью. В Objective-C есть пара вещей, которых стоит избегать, и несколько опасных моментов в Objective-C, связанных с ссылками на управление памятью, на основе счета. Их стоит избегать, но в основном все работает просто.
Поддержка Game center на MAC
У Apple есть собственные гайды, как писать код для Game Center, но если вкратце:
- Аутентифицируйте вашего игрока
- Используйте этого игрока для вызова достижений, списка лидеров и т.д.
Перед тем как вы сможете скомпилировать любой приведенный ниже код, вам понадобится обработать необходимые библиотеки в вашей сборке с помощью встроенной в Unreal системы. В файле Build.cs вашей игры добавьте следующую логику к вашему конструктору:
if (Target.Platform == UnrealTargetPlatform.Mac) { PublicFrameworks.AddRange(new string[]{"GameKit"}); }
Вот пример, как вы можете аутентифицировать вашего игрока с помощью Objective-C++. Заметьте, я передаю Objective-C вызов к authenticateHandler, но код внутри функции вызова может содержать С++.
#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 }
Пример кода, позволяющего разблокировать достижения:
#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 }
А вот код, вызывающий список лидеров API из Game Center:
#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 }
ПОМНИТЕ! Вызов функций Game Center вашим кодом внутри Unreal Editor может привести к странному поведению редактора, так что я бы предложил вставить код, связанный с Game Center, в макрос #if WITH_EDITOR или выключить эти возможности в распакованной версии игры каким-либо другим способом.
Использование iCloud для сохранений в облаке
Этот гайд не объяснит все хитросплетения iCloud. У Apple есть документация, на которую вам стоит глянуть перед тем, как использовать облачное сохранение iCloud в вашем тайтле.
Для большего понимания вы можете представить iCloud, как большое хранилище ключей, к которому игрок может обращаться или получать информацию. Есть несколько способов смоделировать сохранение данных в iCloud:
- Воспринимайте каждое поле сохраняемого объекта как отдельное поле в iCloud (к примеру, поле уровня вашего игрока будет целочисленным значением, названным “Level”, имя его персонажа будет String, названный “Name” и так далее).
- Сериализируйте ваш сохраненный объект в двоичные данные и загрузите весь сейв как один бинарный glob в одно поле iCloud.
Для Dodo Peak моя команда выбрала второй вариант, потому что это позволяло нам использовать ту же логику при сериализации, которую использует Unreal при сохранении.
Перед тем как использовать любой код, мы должны указать системе использовать 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) { // 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]; } }
А вот пример кода, который загружает данные SaveGame из iCloud и считывает бинарные данные в новый объект SaveGame:
#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; } }
Эти примеры помогут вам начать. У всех разные потребности, и чтобы полностью внедрить облачные сохранения в вашу игру, необходимо задуматься о следующих вопросах:
- Как часто ваша игра сохраняется? Как часто вы загружаетесь? Нужно ли каждый раз обращаться к iCloud при каждом сохранении и загрузке или иногда?
- Нужно ли поддерживать множество устройств, записывающих и считывающих один и тот же iCloud?
- Что делать с прогрессом в оффлайне?
- Как ваша игра справится со случаями, когда локальный сейв игрока будет конфликтовать с данными из iCloud?
Учтите, что когда вы попытаетесь запустить код Mac iCloud в редакторе или после переноса вашего приложения, ваша игра выдаст исключение, ругающееся на отсутствие прав. Как в случае с Game Center, я бы посоветовал исключить любой код iCloud из редактора.
Подписание и упаковка вашей игры
Время все собирать. Чтобы преодолеть последнее препятствие для запуска вашей Unreal игры с Apple API и загрузки ее в Mac App Store, потребуется настроить подпись, права Plist кода вашей игры.
Можно написать отдельный блог только по этой теме, но если быстро и вкратце:
- Подпись кода — способ Apple гарантировать, что исполняемый код на самом деле ваш, и что код не был подделан. Больше информации об этом — по ссылке.
- Права — это пары ключ-значение, встраиваемые в код вашего приложения при подписи, которые обозначают определенные возможности OS, которыми ваше приложение может воспользоваться, например iCloud. Здесь вы можете прочитать больше.
- Information Property List или “Info.plist” включает необходимые настройки данных из вашего приложения, также как App Store включает такие вещи, как расположение иконки вашего приложения или поддерживаемые языки. Здесь вы можете прочитать больше.
Если вы знакомы с тем, как Unreal справляется с развертыванием (packaging), с обеспечением профилей и сертификатов для iOS, вам это будет очень знакомо, но придется вручную повторять шаги, которые обычно Unreal и Xcode делают за вас.
Подписание и упаковка для локальной разработки
Перед загрузкой игры в App Store надо удостовериться, что она вообще работает. Так как же вам сделать билд, который запустится локально? Как уже упоминалось ранее, ваша игра крашнется, если вызвать функции iCloud без подписи вашего приложения правами iCloud.
Давайте пофиксим недостающие права. Процесс прямолинейный, но кропотливый. Чтобы внедрить новые права в Mac приложение, необходимо предоставить приложению обеспечивающий профиль, после чего подписать его нужными правами. Вот как это делается.
Запакуйте вашу игру в Unreal. После чего в проводнике найдите директорию, содержащую ваше только что экспортированное приложение. Создайте копию вашего Development профиля подготовки (provisioning profile), переименуйте его в “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 — полный app bundle identifier (важно: удостоверьтесь, что bundle identifier начинается с ID вашей команды, которая является string букв и чисел, например “3Y1CL48M1K”).
- Измените com.apple.developer.icloud-container-identifiers, чтобы он содержал ваш iCloud container 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 идет по следующим этапам. Пройдите через эти шаги один раз вручную, чтобы понять их смысл, после чего напишите собственный скрипт, который будет делать это автоматически.
Первое, экспортируйте вашу игру как Shipping package с включенным Distribution.
Затем измените plist, чтобы получить дополнительную информацию, которую можно отправить на страницу в магазине. Например, можно перечислить поддерживаемые контроллеры с помощью GCSupportedGameControllers и используйте CFBundleLocalizations, чтобы получить поддерживаемые языки (App Store не может автоматически определить поддерживаемые языки) так же, чтобы вручную изменять номера версий и bundle IDs. Необходимо установить LSApplicationCategoryType под тип вашего приложения, чтобы Application Loader его принял.
Скопируйте Distribution provisioning profile (профиль подготовки) , чтобы потом переименовать его в “embedded.provisionprofile”. Скопируйте его в “YourGame.app/Contents/embedded.provisionprofile”.
Теперь, вместо подписи вашего билда для разработки, нам надо будет немного почистить наш экспортированный .app, чтобы Mac App Store его одобрил.
Mac App Store не поддерживает 32-битный исполняемый код (версии macOS 10.15 и выше тоже не поддерживают). Unreal автоматически собирает некоторые динамические аудио библиотеки в вашей игре, которые содержат 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
Далее, Unreal добавляет один субкомпонент к вашей экспортированной игре, у которого есть свой Bundle ID, из-за чего Application Loader сходит с ума. Этот компонент — “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 — полный app bundle identifier (важно: удостоверьтесь, что bundle identifier начинается с ID вашей команды, которая является string букв и чисел, например “3Y1CL48M1K”).
- Измените com.apple.developer.icloud-container-identifiers, чтобы он содержал ваш iCloud container ID.
Сохраните файл как “entitlements.plist”.
Время подписи! Для дистрибуции вам надо будет подписать весь двоичный код игры, все динамические библиотеки и само приложение .app. Мы это сделали с помощью:
codesign -f -v -s "3rd Party Mac Developer Application:" --entitlements entitlements.plist MyGame.app/Contents/MacOS/MyGame
Эта команда подписывает все динамические библиотеки (.dylibs) в файле, используя find:
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
Теперь откройте приложение Application Loader. Кликните на кнопку “choose” и выберите ваш новый .pkg файл. Application Loader просканирует ваш package на ошибки, затем загрузите его на портал App Store Connect.
Распространенные ошибки
Если вы встречаете проблемы по время сборки — вот действующий тред на форуме Unreal.
Application Loader может не принять ваше приложение и выдать ошибку "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.” Эта ошибка — настоящий кошмар. Она говорит “что-то пошло не так, но мы не можем сказать, в чем ошибка". Моя команда натыкалась на эту ошибку несколько дней. В нашем случае оказалось, что мы добавляли отладочные символы (debug symbols) в билд, из-за чего ломалась работа приложения. Удостоверьтесь, что отключили “Include Debug Files” в настройках проекта.
В итоге, самая частая причина отказа Application Loader заключается в иконке приложения. Есть разнообразнейшие инструменты и техники для создания .icns файла, в нашем случае мы решили использовать App Wrapper 3, чтобы создать иконку из PNG файла.