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\default\

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

«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).

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

UPDATE от 1го июня 2017г:

Двенадцатая проблема все-таки есть. Связана она с передачей строковых параметров между Unity и WebGL плагинами. Передача строки из Unity в webgl работает нормально. А вот наоборот — не всегда.
Unity предлагает использовать такой вот snippet:

var buffer = _malloc(val.length + 1);
writeStringToMemory(val, buffer);

Здесь — val — строка в javascript.
Это работает до тех пор, пока длина строки совпадает с количеством байтов в ней. Как только в строке встречаются неоднобайтовые (например, русские) символы, этот код приводит к записи за пределы выделенной памяти, что выражается в случайных крэшах вашего приложения в произвольных местах.

Так что либо используйте encode, чтобы ваши строки использовали лишь символы из latin1, либо перепишите этот snippet так, чтобы он использовал количество байт, а не символов в строке.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 36

    +1
    12. Сам WebGL. Не работают кастомные потоки (Thread) ни в каком виде, есть решение только через браузерные костыли webworker-ов и тп.
      +1
      Потому что в браузере яваскрипт однопоточный. Тут ничего не сделать.
      В Unity можно использовать CoRoutine-ы. Хотя и с осторожностью — они используют тот же единственный поток, замедляя основной процесс.
        +1
        Мне это не нужно объяснять, я просто указал, что можно дописать в статью. :)
          0
          Я описал то, что счёл неочевидным. То что в браузере один поток — общеизвестно.
      +1
      13. Не работают System.Net.Sockets
        0
        Конечно. Но есть WebSocket
          0
          В нашем случае используется библиотека для работы с ejabberd, не так легко найти аналог на WebSocket'ах.
            0
            Вот тут Unity раздает пример клиента для Websocket.
            А сервер можно взять на любой вкус. Я для прототипа взял Jetty. В production пойдет сервер на golang.
            Хотя, это не аналог ejabberd, конечно.
          0
          Общий принцип включения в статью — неочевидные проблемы, связанные именно с использованием Unity для сборки под WebGL.
          Sockets не работает из-за отсутствия поддержки sockets в браузерах. Это — не проблема Unity.
            +3
            Это как раз проблемы юнити — write once, run everywhere, почти как у явы. Если платформа не поддерживает какие-то базовые классы — код не должен компилироваться или компилироваться прозрачно для пользователя во что-то иное. Ведь они транслируют основной msil код сборок + clr в js, почему не транслировать автоматом код про сокеты + делать обертки вокруг них в стиле вебсокетов? Получается, что код проекта превращается в кашу и работа перекладывается на юзвера в плане обеспечения совместимости по платформам. В чем тогда смысл использования юнити, если то же самое можно запилить на чистом webgl и оно будет весить не 100мб, а, например, 10?
              0
              Я не ставил целью пропаганду юнити. Статья написана для тех, кто столкёнтся с конкретной задачей. Цель статьи — сэкономить людям время. Предполагается (и я прозрачно намекнул об этом в Disclaimer), что этим людям не надо объяснять что такое ассет, рассказывать почему браузер не может запускать треды или пояснять, что браузеры не поддерживают сокеты.
              Ваша претензия не к статье, а к технологии. А значит она не по адресу.
                0
                > почему не транслировать автоматом код про сокеты + делать обертки вокруг них в стиле вебсокетов?
                Каким образом вы себе представляете *прозрачное* оворачивание низкоуровневого протокола внутрь высокоуровневого? Вебсокеты — это надстройка над обычными сокетами, в обратную сторону это не работает.
                Заверните-ка прозрачно TCP в HTTP, угу.
                  0
                  Я ведь не зря написал — «или компилироваться прозрачно для пользователя во что-то иное.». У них есть штатная сеть, абстрагированная от сокетов и предоставляющая транспорт данных пользователя — почему она тоже не рабочая? И если штатные сокеты не пашут, то почему бы не предоставить новый слой абстракции в пространстве UnityEngine.xxx для симуляции работы через сокеты? Ведь вебсокет — это тот же tcp stream, отличающийся только способом установки коннекта. Да, отпадут все udp решения, но хоть что-то да будет работать реально кроссплатформенно.
                  Заверните-ка прозрачно TCP в HTTP, угу.

                  WebSocket не заворачивается в http — это обычный потоковый tcp, который маскируется первыми пакетами под htpp заголовки, чтобы пробиться через браузер. Дальше идет обычный бинарный поток, через который можно слать как текст (заворачиваемый в utf8 и потом в бинарный вид), так и любые бинарные данные.
                    0
                    > У них есть штатная сеть, абстрагированная от сокетов и предоставляющая транспорт данных пользователя — почему она тоже не рабочая?

                    Она рабочая — http://docs.unity3d.com/Manual/webgl-networking.html

                    >WebSocket не заворачивается в http — это обычный потоковый tcp

                    Браузеры поддерживают только асинхронную отправку в WebSocket. Как быть с синхронной?
                      0
                      Она рабочая

                      Это новая сеть, которую они пилят с 5-ки и к которой выкатывают 100500 фиксов к шоустоперам каждый патч. WWW работал, да, но это запрос-ответ с вырезанием keep-alive и принудительным закрытием коннекта, что не всегда подходит.
                      Как быть с синхронной?

                      Не совсем понял, с чего сеть вообще должна быть синхронной? Отсюда вытекает и первая проблема — отсутствие потоков. Например, я использовал вебсокеты в своем потоке и все шло прекрасно. Потом меня утомило поддерживать rfc и я переписал сеть на чистый tcp stream со своими костылями и в том же потоке — для остального кода ничего не поменялось, даже в апи, потому что оно все было изначально асинхронное (с возвращением результата через колбеки путем прокидывания данных-событий через очереди в основной поток). Юнитеки двигаются в том же направлении с новой сетью, молодцы. Но на данный момент нет ничего продакшн уровня стабильности + пользователю приходится городить огород вокруг внешних вызовов в js, что доставляет с той же отладкой и тп.
                        0
                        >Это новая сеть, которую они пилят с 5-ки и к которой выкатывают 100500 фиксов к шоустоперам каждый патч.

                        Так часто бывает с ноыми продуктами. Скорее всего, процесс скоро сойдется.

                        >Не совсем понял, с чего сеть вообще должна быть синхронной?

                        Сеть не должна быть синхронной. Я к тому, что нельзя просто так обернуть Socket в WebSocket хотя бы потому, что первый поддерживает синхронные сообщения, а второй в браузерах не поддерживает.
                          0
                          Рендер / работа со звуком тоже не синхронна по своей сути (не подготовка данных, а сам процессинг), однако это не помешало запилить качественные врапперы вокруг подсистем браузера без изменения пользовательского кода. У меня претензии исключительно к тому, что юнитеки все системы обернули вполне качественно (даже физику), а вот к сети отнеслись абсолютно халатно — переложили работу на потребителя. В остальном все нормально.

                          Так часто бывает с ноыми продуктами. Скорее всего, процесс скоро сойдется.

                          И возникнет старая проблема — невозможность создания кастомных серверов без хостинга в юнити. Да, они планируют запилить некую ServerLibrary, но это пока просто строка в роадмапе.
                            0
                            >И возникнет старая проблема — невозможность создания кастомных серверов без хостинга в юнити.

                            Тут согласен с вами. Поэтому сам просто взял здесь простой клиент к WebSocket и прицепил его к Jetty (читай — к любому серверу с поддержкой WebSocket). Работает прекрасно.
                          0
                          У нас кстати тоже кастомные штуки для сети: вместо убогого www своя реализация на raw socket, которая, к сожалению, в WebGL не работает =\
              0
              Спасибо, полезная статья. Сам пока столкнулся только с ограничением по памяти. Мне вроде не критично, влезаю в 512Mb, но, пожалуй, стоит пережать текстурки в разрешение пониже, может и в 256Mb влезу.
              Ресурсы, загружаемые через Resources.Load, я кеширую, соответственно, относительно долго они грузятся только первый раз.
                0
                1) C:\Users\{ИМЯЮЗЕРА}\Application Data\Mozilla\Firefox\Profiles\{ИМЯПРОФИЛЯ}\storage\temporary\

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

                А чем эти пути отличаются. Или это просто ошибка копипасты?
                  0
                  Спасибо. Поправил.
                  0
                  >> C:\Users\{ИМЯЮЗЕРА}\Application Data\Mozilla\Firefox\Profiles\{ИМЯПРОФИЛЯ}\storage\temporary\

                  два раза указан
                    0
                    Спасибо, поправил
                    0
                    Забавно, запустил создание сборки с настройкой Optimisation level: Fast и как раз наткнулся на вашу статью. Жаль, что так поздно, сэкономил бы время.
                      0
                      Вы ведь не один раз будете собирать. Надеюсь, еще сэкономите. Сам я потратил почти две недели на добычу этих знаний.
                      0
                      > 4. Инициализация WebGL и компиляция шейдеров
                      А стандартный подход с использованием Shader Variant Collections не работает на WebGL?
                        –1
                        Работает. Пожалуй, об этом тоже стоило написать.
                        0
                        Да проблем хоть отбавляй! Одно могу сказать со времен 5.0бета все стало горааааздо лучше. Билды собираются более стабильно (раньше валились раз через раз), уменьшилось потребление памяти, увеличилась производительнось, починили шейдеры и тени.

                        А проблемы вот на вскидку: Пока не работает антиалиасинг. Проблемы с работой IndexedDB в сафари при использовании LoadaFromCacheOrDownload (лечится кастомным js лоадером). Проблемы с переходом в полноэкранный режим по нажатию кнопки в гуи — в разных браузерах не всегда отрабатывает маусап (тут пока вылечил жутким костылем с проверкой фулскрина на апдейт). Проблемы с переходом в фулскрин в опере (часть экрана обрезается). Для звуков нужно принудительно отключать эффект Доплера. В ИЕ билд валится из за отсутсвия поддержки аудио апи (раньше валился только при обращении к звуковой системе). И такого еще полно.
                          –1
                          Технология сырая пока. Но есть надежда, что будет становиться лучше.
                          0
                          Загрузка типов с помощью reflection — это вообще очень-очень плохая идея, и дело тут не в OpenGL. Если ваш тип используется только в виде строки, то нет никакой гарантии что code stripping не пометит этот код как неиспользуемый и не удалит этот код. Именно поэтому в 5й Unity убрали AddComponent(string). Поправьте, если не прав.
                            0
                            Эмитировать MSIL с помощью рефлекшна — плохая идея. Пытаться создавать новые домены и грузить в них сборки — плохая идея. Получение мета-информации о типе (поля и тп) — вполне нормально (правда не по строковому имени, а по уже существующему и используемому типу), часто используется, например, при мапинге json данных на типы. Те reflection сильно ограничен со времен AOT (а скоро и на всех платформах с приходом il2cpp), об этом нужно помнить.
                              0
                              В целом, вы правы. Если для чего-то сильно нужна такая техника, с ней надо быть аккуратным.
                              0
                              Какие-то костыли и подпорки. Это называется «вышла из беты»? Интересно, куда.
                                0
                                Релиз 1.0.0 редко бывает беспроблемным. Но разница с тем, что было в бете, огромна.
                                Это даёт основания рассчитывать на улучшение ситуации в следующих релизах.
                                0
                                а воз и ныне там
                                https://habrahabr.ru/post/310086/

                                Only users with full accounts can post comments. Log in, please.