WebGL на Unity3d — двенадцать проблем при сборке проекта

от автора

Не так давно в Unity вышла из беты возможность создания проектов для WebGL. Делюсь своим опытом сборки под эту платформу большого игрового проекта.

Disclaimer: Статья только для тех, кто сам собирается сделать что-то подобное — она очень техническая и использует специфическую для Unity терминологию.

1. Загрузка типа через reflection

Первая проблема: вот такой простой код будет работать неправильно:

    var type = Type.GetType("TypeName"); 

Он работает, но тип возвращается неправильный и «пустой». Проблемы начинаются при работе с этим типом — почти все его методы возвращают пустые значения. Чтобы всё стало правильно, надо писать так:

    var assembly = Assembly.Load("Assembly-CSharp"); //или другое имя assembly      var type = assembly.GetType("TypeName"); 

Вот тут заявлено, что это — ожидаемое поведение.

2. Динамическая загрузка ресурсов

Следующая проблема возникает при использовании методов Resources.Load или Resources.LoadAll. Эти методы сказочно долго работают. Для однопоточного браузера это легко становится критичным. Динамическую загрузку ресурсов этими и схожими методами лучше не использовать вовсе. Где возможно, надо менять её на статическую (заранее проставлять ссылки на нужные префабы). Разница во времени загрузки на моем проекте доходила до десяти секунд.

3. Синхронизация файловой системы

Если вы пользуетесь файловой системой, то нужно знать, что она реализована как wrapper над базой данной, встроенной в браузер. Но самое главное — синхронизация браузера с этой базой происходит только по прямой яваскриптовой команде:

FS.syncfs(false,function (err) {      //alert('syncing fs'); }); } 

Если её не вызывать, то после выключения браузера данные могут и не сохраниться. Вызывать её можно так:

1) Добавляем это в WebGL template:

<script type = "text/javascript" language = "javascript" >  function SyncFiles()  {  FS.syncfs(false,function (err) {      //alert('syncing fs');  });  }  </script> 

2) Вызываем это из c#:

private void Sync()     { #if UNITY_WEBGL             Application.ExternalCall("SyncFiles"); #endif      } 

4. Инициализация WebGL и компиляция шейдеров

При старте приложения существенное время может занимать инициализация WebGL. Это очень важно, потому что данное время учитывается браузером в общее время непрерывной работы скрипта, которое ограничено (в Firefox, например, оно 10 секунд по умолчанию).

Основное время при инициализации WebGL занимает компиляция шейдеров. Причем компилируются только те шейдеры, которые есть на сцене или в префабах, на которые есть со сцены ссылки. Если вы (как я) просто брали в свою игру много разных ассетов из разных источников, то шейдеров у вас будет неприлично много и их компиляция может занять более 10 секунд.

Что нужно делать?

1) Минимизировать число различных шейдеров, используемых в проекте. Часто проект использует почти одинаковые шейдеры, которые приехали в него из разных покупных ассетов.

2) Если этого недостаточно, придется переносить часть ассетов в динамически загружаемые ресурсы или бандлы . Да, это дольше, чем статическая загрузка. Но динамическую загрузку можно сдедать отложенной и загружать свои ассеты частями так, чтобы загрузка каждой из частей точно занимала не более 10с. В итоге суммарное время загрузки вырастет. Но, по крайней мере, браузер перестанет назойливо предлагать игроку остановить зависший скрипт.

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

5. Кэш Firefox

Если вы собираетесь отлаживать ваш проект, используя локальный сервер и Firefox, то столкнётесь с тем, что браузер неверно кэширует часть WebGL проекта.

Сценарий такой:

Я делаю и запускаю версию 1. Она работает. Затем делаю версию 2. Запускаю — падает с непонятной ошибкой.

Лечится это ручной чиской кэшей FF по адресу:

1) C:\Users\{ИМЯЮЗЕРА}\Application Data\Mozilla\Firefox\Profiles\{ИМЯПРОФИЛЯ}\storage\temporary\

Тут можно удалять вообще всё.

2) C:\Users\{ИМЯЮЗЕРА}\Application Data\Mozilla\Firefox\Profiles\{ИМЯПРОФИЛЯ}\storage\temporary\

Тут надо удалить только ваш сайт. Например

«http+++127.0.0.1+7888»

Замечание: Это происходит только при работе с HTTP сервером. Если запускать проект из файлов ошибки не происходит, поскольку кэши для каждого пути к файлам — разные. А вот для сервера один кэш, независимо от пути.

6. Script does not respond for a long time. Stop it?

Firefox спросит у вас об этом почти наверняка. Если проект большой, то может спросить и несколько раз.

Частично об этой проблеме говорилось выше — в пункте, посвященном инициализации WebGL. Но важно понимать как происходит загрузка WebGL проекта и какое время учитывает браузер, когда предлагает отстрелить зависший скрипт.

Этапы запуска:

1) Идет загрузка данных браузером с сервера. Тут мы ничего ускорить не можем, но это время браузер и не ограничивает.

2) Идет декомпрессия данных.

Например:

Decompressed Release/w69.memgz in 100ms. You can remove this delay if you configure your web server to host files using gzip compression. UnityLoader.js:1:775
Decompressed Release/w69.jsgz in 391ms. You can remove this delay if you configure your web server to host files using gzip compression. UnityLoader.js:1:775
Decompressed Release/w69.datagz in 2764ms. You can remove this delay if you configure your web server to host files using gzip compression.

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

3) Идет компиляция asm.js.

Successfully compiled asm.js code (total compilation time 9088ms; stored in cache)1 56937f89-a8fd-4b65-94aa-453e33be78d8

Может занимать 5-10 сек, но тоже не ограничивается браузером, так как код игры еще не стартовал.

4) А вот дальше стартует код игры

И, с точки зрения браузера, код Unity (такой, как инициализация WebGL) не отличается от кода самой игры. Причем выполняются они одним куском, подряд. Поэтому тут за 10 секунд должна пройти и инициализация WebGL и пользовательский код. Учитывая, что время инициализации WebGL обычно не может быть сокращено сильнее, чем до 4-5 секунд на хорошем компьютере, тут лучше не рисковать и снизить время инициализации пользовательского кода до минимума. В идеале, он вообще ничего не должен делать. А как же тогда инициализировать игру? Её можно отложить. Например, так:

        void Awake()         {             DontDestroyOnLoad(gameObject);              StartCoroutine(Init());         }          IEnumerator  Init()         {            yield return new WaitForSeconds(0.1f);             //Инициализация тут         } 

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

И последнее: ограничение на 10секунд непрерывного исполнения скрипта применяется не только во время инициализации.

7. Использование бандлов

При использовании asset bundles важно помнить, что они должны быть загружены с того же сервера, что и сама игра. Иначе работать не будет из-за нарушения single origin policy.

Второй момент — чтобы избежать задержки на декомпрессию бандла после загрузки лучше при создании бандла использовать не gz (по умолчанию), а lz4:

//Используйте ChunkBasedCompression (это и есть lz4)   BuildPipeline.BuildAssetBundles (outputPath, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget); 

8. Ограничение на 512 mb

На моей машине ни один браузер не был в состоянии выделить игре более 512mb. Хотя памяти на машине много. Полагаю, что не нужно выделять играм, собираемым под WebGL, больше памяти, чем 512mb. И саму игру нужно делать так, чтобы ей этого хватало. В идеале вообще оставить 256mb которые стоят по умолчанию.

9. Strip engine code

Strip engine code — это галочка в настройках сборки, которая заставляет Unity выбрасывать из сборки неиспользуемые системные скрипты.
Это позволяет значительно сокращать итоговый объем сборки. Проблема здесь в том, что если какой-то код используется только ассетами, попавшими в бандл, он тоже будет выброшен. А итоговая сборка работать не будет. Причем exception будет абсолютно непонятный.

Выводы:

1) Если получили непонятный exception, попробуйте собрать убрав эту галочку;
2) Можно прямо использовать скрипты, нужные ассетам из бандлов в коде или использовать специальую возможность Unity — файл link.xml.

10. Developer builds — no fast builds!

Когда собираешь developer build есть возможность выбора между быстрой сборкой и быстрым исполнением. На самом деле быстрая сборка занимает почти такое же (громадное) количество времени и при этом, на большом проекте часто совсем не работает из-за нехватки памяти. Лучше её не использовать.

11. Crunched textures

Для проектов под WebGL всегда используйте crunched texture. Иначе объем игры будет неприлично большим для web приложения.
Чтобы не выставлять тип каждой текстуре вручную, можно использовать вот такую технику (нужно перегрузить метод OnPostprocessTexture).

А двенадцатой пробемы нет, просто «двенадцать проблем» звучит лучше, чем одиннадцать.

ссылка на оригинал статьи https://habrahabr.ru/post/281428/