Unity компиляция для андроид

Unity: Сборка под Android или «размер имеет значение»

В некоторых случаях необходимо работать над снижением размера сборки для Андроид. Например, установка тяжеловесных APK для пользователей мобильного интернета может влететь в копеечку. Превышение размера APK в 50 Мб в Google Play выливается в дополнительные трудности при аплоаде.

Мы разрабатывали под Андроид на Unity 2D-игру, которая изобилует картинками (большинство с областями прозрачности) и разнообразными звуками, и столкнулись с проблемой размера APK. Забегая вперед скажу, что решив ее и снизив вес в 1,5 раза, мы получили в 1,5 раза больше скачиваний. Заставляет задуматься, не правда ли?

Пытаясь побороть проблему приложений-тяжеловесов, мы обратились к замечательной документации от Unity. Выяснилось, что там данная тема обсуждается лишь вскользь. Упоминается, какие именно аспекты влияют на размер сборки и как воздействовать на эти аспекты, чтобы сделать APK меньше, а также приведено немного конкретики по поводу настроек импорта изображений.

Другую полезную информацию по данному вопросу в интернете мы практически не нашли. Потому провели небольшое исследование, результатами которого спешим с вами поделиться.

Итак, согласно документации, на размер сборки влияет:

  1. Размер ассетов (изображения и звуки);
  2. Меши и анимационные клипы;
  3. Размер включаемых dll.

С мешами и импортированными анимационными клипами мы не работали, потому начнем с ассетов.

ИЗОБРАЖЕНИЯ

Уточним насчёт изображений. В нашей игре соблюдается принцип pixel-perfect. Все изображения без альфа-канала имеют плавный градиент. Остальные изображения с прозрачностью, а непрозрачные области тоже имеют плавный градиент.

Проанализируем, какой вклад в размер АРК вносят изображения в формате PNG и JPG.

Картинки в формате PNG

Для PNG этот вклад зависит от двух «переменных»:

  1. Размер картинки на файловой системе;
  2. Настройка импорта.

Мы сохранили 3 картинки в Photoshop с настройками: Compression->Smallest/Slow, Intrelaced: No. Картинка PNG размером 295 Кбайт заняла в сборке места 236 Кбайт. Картинка 612 Кбайт заняла 480 Кбайт. Картинка 21,2 Кбайт заняла 23 Кбайт (естественно, при одинаковых настройках импорта). Пропроциональность очевидна.

Далее мы сохранили первую картинку в Photoshop с настройками: Compression->None/Fast, Intrelaced: Yes. Получили файл размером 9000 Кбайт, однако в сборку он добавил те же 236 Кбайт.

Это подтверждает слова из документации, о том, что Unity перекодирует все ассеты во внутренний формат. Очевидно, он приблизительно соответствует формату PNG, сохранённому в фотошопе с настройками: Compression->Smallest/Slow, Intrelaced: No. И Unity перекодирует все PNG в этот формат, независимо от начального формата файла.

Картинки в формате JPG

Опыты однозначно показали, что изображения JPG вносят вклад в размер сборки пропорционально своему размеру. Пока не совсем понятно, как ведёт себя Unity в случае, если ассет находится в формате JPG. Мы сохранили ту же самую (первую) картику в JPG и получили очень маленький файл на файловой системе (75 Кбайт). Однако в сборке он занял непомерных 767 Кбайт (*).

Мы проводили эксперимент на большом количестве разных картинок, здесь я привожу лишь малую часть исследований. В других экспериментах нам наоборот удавалось снизить общий размер сборки конвертированием всех файлов из PNG в JPG.
Причин этого явления выявить пока не удалось.
Скорее всего, снова имеет место внутренний формат Unity, однако точные принципы взаимодействия колдовского механизма Unity с ассетами JPG выяснить пока не удалось.

Еще один способ работы с JPG будет описан ниже.

Выводы

  1. Судя по всему, Unity берет во внимание и формат, в котором сохранено изображение, и характеристики изображения и выполняет сжатие по каким-либо внутренним принципам;
  2. Один принцип совершенно однозначен: изображения в одинаковом формате вносят вклад в размер сборки, прямо пропорциональный (но НЕ РАВНЫЙ) своему размеру на файловой системе.

Настройки импорта влияют на размер сборки совершенно так, как это описано в доке (чем качественнее картинка, тем больший её вклад в размер сборки). Здесь скрытых особенностей не выявлено. (С другими форматами изображений эксперименты пока не проводились.)

ЗВУКИ

Для звуков значение имеют две настройки импорта: Audio Format и ForceToMono.

Файлы в формате WAV занимают больше места в сборке, чем файлы в формате MP3 (вклад в размер сборки приблизительно равен размеру файла). Стерео WAV файл займёт меньше места, если выставить ForceToMono = true. Точно такого же эффекта можно достичь, если конвертировать файл в «моно» ещё до импорта (тогда ForceToMono не будет доступна в инспекторе).

Однако если для файла WAV установить настройку импорта AudioFormat = Compressed, то в сборке он займёт места столько же, сколько и соответствующий MP3 файл высшего качества (Audacity: variable bitrate, 220-260 kbps). То есть Unity самостоятельно кодирует звук в формат MP3.

Читайте также:  Диспетчер для андроида apk

Для файлов в одинаковом формате действует принцип прямо пропорционального вклада в размер сборки.

Остальные настройки импорта на размер сборки никак не влияют. Они влияют на количество места, занимаемое звуком в RAM.

А ТЕПЕРЬ О КОНКРЕТНЫХ ШАГАХ К НАШЕЙ ЦЕЛИ

    По возможности снижать размер и качество исходных файлов графики.
    (1-a) Обрезка областей прозрачности.
    В нашей игре использовалось много картинок с внушительными областями прозрачности вокруг изображения. Области прозрачности лишь на считанные проценты увеличивают размер файла и не очень влияют на размер сборки (хотя обрезка 102х немаленьких картинок сэкономила нам 2 Мбайт). Но области прозрачности увеличивают использование RAM (поскольку там картинки представляются в BMP).

Таким образом, совет: для снижения нагрузки на ОЗУ
* избегать больших областей прозрачности в изображениях;
* не формировать атласы с большими участками прозрачности, особенно в NGUI.

(Но это мы отвлеклись от основной темы.)

  • Изображениям, не имеющим прозрачности и отображаемым в самом высоком качестве, в настройках импорта необходимо задать Advanced->RGB24. RGBA32 для них не имеет смысла, а вот оставлять Automatic Truecolor не рекомендуется, потому что (как показал опыт) он может восприняться системой как RGBA32. А это является совершенно лишним при отсутствии областей прозрачности (добавляет веса сборке и увеличивает использование RAM).
  • Снизить качество изображений в настройках импорта по схеме: 32 bit -> 24 bit -> 16 bit, наблюдая за тем, чтобы уровень качества изображения оставался в пределах допустимого.
  • Ограничить maxTextureSize для изображений, для которых это возможно с сохранением приемлемого качества.
  • Тщательно вручную удалять неиспользуемые ассеты в папках Resources перед процессом сборки. Помните, что для ассетов, лежащих в папках Resources, Unity НЕ производит автоматическое их удаление, даже если они не назначены в Инспекторе.
  • Для изображений JPG: сменить расширение файла на bytes и он превратится в TextAsset. После чего воспользоваться функцией Texture2D.LoadImage() для загрузки картинки. Следует помнить, что эта функция нагружает процессор и может оказаться неподходящей в случае больших картинок, которые необходимо очень быстро загрузить. Однако этот способ зачастую помогает внушительно облегчить сборку.
  • Уменьшить размер звуковых файлов до минимально возможного размера (с учётом требований к качеству). По возможности использовать MP3, а не WAV. Но если нужны MP3 самого высокого качества, можно пользовать и WAV-файл с настройкой импорта AudioFormat = Compressed (Unity перекодирует самостоятельно). По возможности использовать Mono вместо Stereo. Для WAV-файлов — выставлять настройку импорта ForceToMono = true
  • После выполнения каждого пункта желательно проверять полученный эффект. Поскольку в поведении Unity всё же есть доля магии.

    Несколько слов о dll

    Документ приводит список обязательно включаемых в сборку dll и призывает минимизировать количество дополнительных dll (особенно тяжёлых). В частности, по возможности не использовать System.dll (добавляет к APK 2 Мбайт). Однако даже если мы будем избегать ссылок на методы из этой библиотеки, System.dll всё равно (по состоянию на Unity 4.5.5) включается в сборку, так как её подтягивает обязательная Mono.Security.dll. Потому, получается, что официальная документация Unity в этом месте не совсем релевантна.

    А вот использования других (необязательных) dll желательно избегать во имя снижения размера сборки.

    На сегодня у нас всё. Спасибо за внимание!

    (Просьба рассматривать данный материал как более-менее систематизированные результаты исследования, а не как универсальное руководство к действию. Буду очень рад комментариям, замечаниям, конструктивной критике, возражениям, предложениям, дополнениям. И, конечно, несказанно рад буду узнать, какие приёмы применяют коллеги из других компаний для решения обсуждаемого вопроса.)

    Источник

    Сборка плагинов для Android

    На этой странице описан нативный код плагинов для Android.

    Сборка плагина для Android

    Чтобы построить плагин для Android, вы должны сначала получить Android NDK и ознакомиться с инструкцией по созданию общей библиотеки.

    Если вы используете C++ (.cpp), при создании плагина вы должны убедиться, что функции объявлены с C-связями, чтобы избежать проблем с коверканьем имен.

    Использование вашего плагина из C

    После сборки, общая библиотека должна быть скопированы в папку Assets->Plugins->Android . Unity будет искать её по имени, когда вы определяете функцию вроде следующей в C#-скрипте:-

    Обратите внимание, что PluginName не должно включать ни префикс (‘Lib’), ни расширение (‘.so’). Вы должны обернуть все нативные методы кода дополнительным слоем C#-кода. Этот код должен проверить Application.platform и вызывать собственные методы только тогда, когда приложение работает на реальном устройстве; фиктивные значения могут быть возвращены из С#-кода при работе в редакторе. Вы также можете использовать определения платформы для контроля зависимости компиляции кода от платформы.

    Проекты библиотек для Android

    Вы можете закинуть проекты прекомпилированной Android библиотеки в папку Assets->Plugins->Android . Прекомпилированная — значит все .java файлы должны быть скомпилированы в jar файлы, расположенные либо в папке bin/, либо в папке libs/ проекта. AndroidManifest.xml из этих папок будет автоматически объединяться с основным файлом манифеста при сборке проекта.

    Смотрите Проекты библиотек для Android для большей информации.

    Читайте также:  Андроид как переместить картинку

    Развертывание

    Для кросс-платформенного развертывания, ваш проект должен включать плагины для каждой поддерживаемой платформы (т.е. libPlugin.so для Android, Plugin.bundle для Mac и Plugin.dll для Windows). Unity автоматически выбирает правильный плагин для целевой платформы и включает его с плеером.

    For specific Android platform (armv7, x86), the libraries (lib*.so) should be placed in the following:

    Использование плагинов Java

    Механизм Android-плагина также обеспечивает Java, который будет использоваться для взаимодействия с ОС Android.

    Сборка Java-плагина для Android

    Есть несколько способов создать плагин Java, но результатом во всех случаях является файл .jar, содержащий файлы .class для вашего плагина. Один из подходов — скачать JDK, затем скомпилировать файлы .java из командной строки с javac. Это создаст файлы .class, которые затем можно упаковать в .jar из командной строки с утилитой jar. Другой вариант заключается в использовании Eclipse IDE вместе с ADT.

    Примечание: Unity ожидает Java-плагины, которые будут построены с использованием JDK v1.6. Если вы используете v1.7, вы должны включить “-source 1,6 -target 1,6” в параметрах командной строки для компилятора.

    Использование ваших Java-плагинов из нативного кода

    После того как вы создали свой ​​Java-плагин (.jar), вы должны скопировать его в папку Assets->Plugins->Android в проекте Unity. Unity упакует ваши файлы .class вместе с остальной частью Java-кода, а затем получить доступ к коду, используя Java Native Interface (JNI). JNI используется как при вызове нативного кода из Java, так и при взаимодействии с Java (или JavaVM) из нативного кода.

    Чтобы найти Java код из нативной части, вы должны иметь доступ к Java VM. К счастью, доступ можно легко получить, добавив такую ​​функцию в свой C/C++ код:

    Это все, что необходимо, чтобы начать использовать Java из C/C++. Полное объяснение JNI выходит за рамки этого документа. Однако его использование, как правило, включает в себя поиск определения класса, поиск метода конструктора ( ) и создание нового экземпляра объекта, как показано в этом примере:-

    Использование Java-плагинов со вспомогательными классами

    AndroidJNIHelper и AndroidJNI могут быть использованы для избавления от некоторых проблем с первичным JNI.

    AndroidJavaObject и AndroidJavaClass автоматизируют множество задач и используют кэширование для ускорения вызовов к Java. Комбинация AndroidJavaObject и AndroidJavaClass надстраивается над AndroidJNI и AndroidJNIHelper , но также содержит много логики сама по себе (для управления автоматизацией). Эти классы также идут в ‘static’ версии для доступа к статическим членам Java классов.

    Вы можете выбрать в зависимости от того, что вам подходит, будь то первичный JNI с помощью методов класса AndroidJNI , или AndroidJNIHelper вместе с AndroidJNI , и в конце концов AndroidJavaObject/AndroidJavaClass для максимальной автоматизации и удобства.

    UnityEngine.AndroidJNI является оберткой для вызовов JNI доступных в C (как описано выше). Все методы этого класса являются статическими и соответствуют 1:1 Java Native Interface. UnityEngine.AndroidJNIHelper предоставляет вспомогательный модуль, используемый на следующем уровне, но предоставляется как открытые методы, потому что они могут быть полезны для некоторых частных случаев.

    Экземпляры UnityEngine.AndroidJavaObject и UnityEngine.AndroidJavaClass соответствуют один-в-один экземпляру java.lang.Object и java.lang.Class (или их подклассов) на стороне Java, соответственно. Они по существу обеспечивают 3 типа взаимодействия с Java:-

    • Вызов метода
    • Получить значения поля
    • Установить значение поля

    Call (Вызов) разделяется на две категории: Call (Вызов) ‘void’ метода, и Call (Вызов) метода, возвращающего не-void тип. Универсальный тип используется для представления типа возвращаемого методами, которые возвращают не-void тип. Get и Set всегда берут универсальный тип, представляющий тип поля.

    Пример 1

    Здесь мы создаем экземпляр java.lang.String, инициализируемстроку на свой выбор и извлекаем хеш значение для этой строки.

    Конструктор AndroidJavaObject принимает минимум один параметр, имя класса, экземпляр которого хотим создать. Любые параметры после имени класса предназначены для вызова конструктора объекта, в данном случае строка “some_string”. Последующий Call (Вызов) метода hashCode() возвращает ‘int’, который мы используем в качестве параметра универсального типа в Call (Вызове) метода.

    Примечание: Вы не можете создать экземпляр вложенного класса Java с помощью разделения точкой. Внутренние классы должны использовать разделитель $, и это должно работать в точечном и слэш формате. Так \[android.view.ViewGroup$LayoutParams или android/view/ViewGroup$LayoutParams могут быть использованы, где LayoutParams класс вложенный в класс ViewGroup\] .

    Пример 2

    Один из примеров плагина выше показывал, как получить директорию кэша для текущего приложения. Вот, как вы могли бы сделать то же самое на C# без плагинов:-

    В этом случае, мы начинаем с AndroidJavaClass вместо AndroidJavaObject , потому что мы хотим получить доступ к статическому члену com.unity3d.player.UnityPlayer , а не создавать новый объект (экземпляр создается автоматически в Android UnityPlayer ). Затем мы получаем доступ к статическому полю “currentActivity”, но на этот раз мы используем AndroidJavaObject как универсальный параметр. Потому что фактический тип поля (android.app.Activity) — это подкласс java.lang.Object), и любой не-примитивный тип обязательно должен быть доступен как AndroidJavaObject . Исключением из этого правила являются строки, которые могут быть доступны непосредственно, даже если они не представляют собой примитивный тип в Java.

    После чего это уже просто вопрос обхода Activity через getCacheDir() для получения объекта File, представляющего папку кэша с последующим вызовом getCanonicalPath() для получения строкового представления.

    Читайте также:  Частотомер звука для андроид

    Конечно, в настоящее время вам не нужно делать этого, чтобы получить каталог кэша, поскольку Unity предоставляет доступ к директории кэша и файлов приложения с Application.temporaryCachePath и Application.persistentDataPath.

    Пример 3

    Наконец, здесь есть уловка для передачи данных из Java в код скрипта с помощью UnitySendMessage .

    Класс Java com.unity3d.player.UnityPlayer теперь имеет статический метод UnitySendMessage , что эквивалентно функции UnitySendMessage в нативной части iOS. Он может быть использован в Java для передачи данных в код скрипта.

    Здесь, однако, мы вызываем непосредственно из кода скрипта, который по сути передает сообщение Java. Это посылает обратный вызов нативному/Unity коду, чтобы доставить сообщение объекту с именем “Main Camera”. Этот объект имеет скрипт, который содержит метод, называемый “JavaMessage”.

    Лучшая практика при использовании плагинов Java с Unity

    Так как этот раздел в основном ориентирован на людей, которые не имеют большого опыта работы с JNI, Java и Android, мы предполагаем, что подход с AndroidJavaObject/AndroidJavaClass был использован для взаимодействия с кодом Java из Unity.

    Первое, что нужно отметить, это то, что любая операция выполненная с AndroidJavaObject или AndroidJavaClass дорогая в отношении производительности (как первичный JNI подход). Крайне желательно, чтобы количество переходов между управляемым и нативным/Java кодом было минимальным, ради производительности, а также ясности кода.

    У вас может быть метод Java, чтобы сделать всю фактическую работу, а затем использовать AndroidJavaObject/AndroidJavaClass для взаимодействия с этим методом и получения результата. Однако стоит иметь в виду, что вспомогательные классы JNI попробуют кэшировать столько данных, сколько возможно, чтобы улучшить производительность.

    Сборщик мусора Mono должен освободить все созданные экземпляры AndroidJavaObject и AndroidJavaClass после использования, но желательно держать их в операторе using()<> , чтобы они удалялись как можно скорее. Без этого вы не можете точно знать, когда они будут уничтожены. Если вы установите значение AndroidJNIHelper.debug в true, вы увидите запись деятельности сборщика мусора в журнале отладки.

    Вы также можете непосредственно вызвать метод .Dispose() для гарантии, что нет сохранившихся Java-объектов. Фактически C# объект может жить немного дольше, но в конце концов будет удалён сборщиком мусора mono.

    Расширение Java-кода UnityPlayerActivity

    С Unity Android можно расширить стандартный класс UnityPlayerActivity (основной класс Java для Unity плеера на Android, аналогично AppController.mm на Unity iOS).

    Приложение может переопределить любые и все основные взаимодействия между ОС Android и Unity Android. Вы можете cделать это, создав новую Activity, которая является производной от UnityPlayerActivity(UnityPlayerActivity.java можно найти в /Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/src/com/unity3d/player на Mac и обычно в C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\src\com\unity3d\player на Windows).

    Чтобы сделать это, сначала найдите classes.jar , поставляемый с Unity Android. Он находится в папке установки (обычно C:\Program Files\Unity\Editor\Data (на Windows) или /Applications/Unity (на Mac)) в подпапке PlaybackEngines/AndroidPlayer/bin . Затем добавить PlaybackEngines/AndroidPlayer/bin в папку с классами, используемыми для компиляции новой Activity. Полученный(-ые) .class файл(ы) должны быть сжаты в .jar файл, а этот файл надо добавить в папку Assets->Plugins->Android . Поскольку манифест определяет запуск Activity, его также необходимо создать новый AndroidManifest.xml. Файл AndroidManifest.xml также должен быть помещен в папку Assets->Plugins->Android (размещение пользовательского манифеста полностью заменяет манифест Unity Android по умолчанию).

    Новая Activity может выглядеть, как в следующем примере OverrideExample.java:

    И соответствующий AndroidManifest.xml будет выглядеть так:

    UnityPlayerNativeActivity

    Кроме того, можно создать свой собственный подкласс UnityPlayerNativeActivity . Это будет иметь такой же эффект, как создать подкласс UnityPlayerActivity но с улучшенной задержкой ввода. Помните, однако, что NativeActivity был введен в Gingerbread и не работает со старыми устройствами. Поскольку события касания/движения обрабатываются в нативном коде, окна Java обычно не увидят эти события. Существует, однако, механизм переадресации в Unity, который позволяет, чтобы события распространялись на DalvikVM. Для доступа к этому механизму, необходимо изменить файл манифеста, выглядит следующим образом:-

    Обратите внимание на атрибут “.OverrideExampleNative” в элементе Activity и два дополнительных элемента мета-данных. Первый элемент мета-данных является инструкцией по использованию библиотеки libunity.so для Unity. Второй обеспечивает события, которые будут переданы пользовательскому подклассу UnityPlayerNativeActivity.

    Примеры

    Пример нативного плагина

    Простой пример использования кода нативного плагина можно найти здесь

    В этом примере демонстрируется как код С может быть вызван из приложения Unity Android. Пакет содержит сцену, которая отображает сумму двух значений, рассчитанную нативным плагином. Пожалуйста, обратите внимание, что вам нужен будет Android NDKдля компиляции плагина.

    Пример плагина Java

    Используемый в примере код Java можно найти здесь

    В этом примере демонстрируется как код Java может быть использован для взаимодействия с ОС Android, и как C++ создает мост между C# и Java. Сцена в пакете отображает кнопку, при нажатии на которую выдается каталог кэша приложения, как это определено в ОС Android. Пожалуйста, обратите внимание, что вам понадобится JDK и Android NDK для компиляции плагинов.

    Здесь находиться подобный пример, но на основе предварительно собранной библиотеки JNI, чтобы обернуть нативный код в C#.

    Источник

    Оцените статью