Разработка ПО в современном мире идет семимильными шагами. От релиза до релиза количество добавленых фич может составить несколько десятков (а порой счет идет и на сотни), а API измениться до неузнаваемости. И, вроде бы, в этом нет ничего плохо, но из-за количественных изменений довольно часто страдает качество. Баг-трекер забивается нерешенными задачами, частенько можно наблюдать регрессию по уже имеющемуся функционалу, а часть багов не правится по непонятным причинам, а порой и вовсе без таковых. Отправляемые баг-репорты отклоняются или ожидают минимального проекта для воспроизведения ошибки, а последний, зачастую, невозможно создать. А если все-таки удалось создать, то в ответ нередко получаешь отписку «У меня все работает».
И вишенка на торте — голосование за исправление багов. И если «ваша» проблема наберет большое количество лайков, то тогда, возможно, разработчики обратят внимание и поправят ошибку, хотя не факт. Бывают случаи, когда даже большое количество лайков не помогает. Увы (. И если баги (и то в основном критические, и то не всегда) худо-бедно правятся, то разного рода мелкие улучшения и полезности, которые могли бы сэкономить время, остаются за бортом, что не может не печалить. Само собой, вышеперечисленное относится не ко всему ПО вообще, а, скорее, это что-то типа средней температуры по палате.
Как вы уже могли догадаться, сегодня речь пойдет о нескольких мелких проблемах в Unity, вероятность исправления/улучшения которых стремится к нулю. Перечисленные ниже проблемы — это небольшая капля из того моря проблем, с которыми приходится сталкиваться ежедневно при работе с Unity. Думаю, если вы плотно общаетесь с Unity, то вам должно быть понятно, о чем идет речь. Вообще, стабильность Unity улучшается, и сейчас хотя бы можно стало работать (иногда недолго). Раньше это был просто кромешный ад из зависаний и крешей (сейчас же можно отработать день-другой, а можно встрять всего на полдня). И если на нашем относительно маленьком проекте столько проблем, трудно представить, сколько их на крупных проектах.
Invalid AABB inAABB
Перед нами стояла задача добавить возможность использования разных вариантов ассетов в зависимости от разрешения экрана целевого устройства, чтобы загрузка, да и сама игра, у пользователей со слабыми устройствами происходила быстрее. В самом разгаре разработки при очередном запуске приложения в редакторе выскочила ошибка:
1 2 |
Invalid AABB inAABB UnityEngine.Canvas:SendWillRenderCanvases() |
Аналогичная ошибка падала при попытке отрисовать UI в текстуру во время анимации открытия окна.
Хорошо, что эта ошибка падала сразу при старте и с завидным постоянством повторялась. А ведь аналогичная проблема могла возникнуть в другом месте, и тогда эту ошибку бы пропустили, а исправление заняло бы еще больше времени. Столкнувшись впервые с подобной ошибкой, я обратился к всезнающему гуглу, для которого эта ситуация уже была известна. В основном были ответы с ошибками в расчетах, приводящих к невалидным значениям, наподобие NaN; в некоторых случаях помогало отключение сортировки дочерних канвасов; в других случаях проблемным местом была система частиц. Ну, и другие подобные проблемы. Особняком стоял баг-репорт с отметкой Won’t Fix, что не могло не «радовать» (.
Перепробовав все из найденного, мы поняли, что у нас проблема в чем-то другом. Пытаясь найти игровой объект с компонентами с невалидными размерами или скейлом, мы пришли к мысли, что, возможно, проблема в некорректных размерах одного из спрайтов в спрайт-листе. Само собой, найти вручную спрайт нелегко. Благо, редатор Unity легко расширяем. Была добавлена временная команда, и злополучный спрайт с нулевым размером был найден. Для генерации спрайт-листов мы используем небезызвестный TexturePacker, который умеет автоматически генерировать спрайт-листы под разные разрешения. Один из спрайтов имел ширину в 1 пиксель; при его масштабировании в сторону уменьшения получился спрайт с шириной в 0 пикселей. Таким образом, TexturePacker сгенерировал невалидный спрайт-лист и не только не выдал ошибку, но даже не показал предупреждения. Аналогичным образом поступил и плагин TexturePacker-а для Unity — ни ошибок, ни предупреждений, все отлично. Unity не отстает, при импорте получившегося спрайт-листа все прошло гладко. И только в run-time при попытке отрисовать невалидный спрайт Unity чертыхнулась, да и то, вместо того, чтобы выдать вразумительную ошибку, выдала нечто, что выкидывается, как оказалось, в куче других случаев. Само собой, чтобы подобной ошибки не произошло в будущем, был написан тест, наподобие этого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
[Test] public void SizeShouldBeValid() { const int minSize = 1; var errors = new StringBuilder(); var textures = AssetDatabase.FindAssets("t:texture2d"); foreach (var textureGuid in textures) { var texturePath = AssetDatabase.GUIDToAssetPath(textureGuid); var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType<Sprite>(); foreach (var sprite in sprites) { var spriteRect = sprite.rect; if ((int) spriteRect.width < minSize || (int) spriteRect.height < minSize) { errors.AppendLine($"Invalid sprite size: {spriteWidth}x{spriteHeight} at " + $"{texturePath} {sprite.name}"); } } } Assert.That(errors.Length, Is.Zero, errors.ToString); } |
Missing References
Думаю, сложно найти разработчика на Unity, который бы никогда не сталкивался с проблемой испорченных ссылок в префабах, игровых сценах или в ScriptableObject-ах. Эта проблема стара, как и сама Unity. Ссылки на объекты могут теряться совершенно по разным причинам: случайное удаление (или изменение) .meta файлов; мерджи (как с конфликтами, так и без) сцен, префабов и т.п.; проблемы и конфликты с Cache Server-ом; затыки при переключении сильно несовместимых веток и т.д. и т.п. Не так важно, из-за чего возникает эта проблема, как то, что нет каких-то стандартных средств для борьбы с ошибками такого рода. Поэтому каждый разработчик борется с этой проблемой самостоятельно. И, если обратиться к поисковику, легко можно найти заветные строчки кода, позволяющие обнаружить missing references в сценах, префабах и т.п. Коварство этой проблемы состоит в том, что даже при билде приложения Unity не заикнется о проблеме, вследствие чего очень легко выкатить в релиз приложение, которое будет глючить. И, если посмотреть на код, то, чаще всего, это какие-то команды редактора, которые необходимо запускать вручную (видимо, перед билдом?), что странно и чревато ошибками из-за человеческого фактора. Гораздо логичнее написать тест, который будет запускаться (вместе с остальными тестами) перед сборкой приложения и фейлить билд в случае обнаружения проблемы. Для написания теста достаточно простого метода, определяющего наличие некорректных ссылок. Что-то наподобие такого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
private static void FindMissingReferences(GameObject go, StringBuilder stringBuilder) { var components = _componentBuffer; components.Clear(); go.GetComponentsInChildren(true, components); foreach (var c in components) { if (c == null) { stringBuilder.AppendLine("Missing script found on: " + GetFullPath(go)); } else { using (var so = new SerializedObject(c)) { var sp = so.GetIterator(); while (sp.NextVisible(true)) { if (sp.propertyType != SerializedPropertyType.ObjectReference) continue; // objectReferenceValue будет null и если просто не задана ссылка, // а это валидная ситуация, поэтому такой проверки не достаточно. // Раньше была "sp.objectReferenceInstanceIDValue != 0", но теперь // Unity переделали геттер и он возвращает значение по objectReferenceValue // поэтому будем проверять строковое значение objectReferenceStringValue, // оно вроде пока отличается (к сожалению оно internal) if (sp.objectReferenceValue == null && ContainsMissingObjectReferenceStringValue(sp)) { stringBuilder.AppendLine("Missing reference found in: " + GetFullPath(c) + ", Property : " + sp.name); } } } } } } private static bool ContainsMissingObjectReferenceStringValue(SerializedProperty sp) { var objectReferenceStringValue = ReflectUtil.GetPropertyValue<SerializedProperty, string>(sp, "objectReferenceStringValue"); return objectReferenceStringValue != null && objectReferenceStringValue.Contains("Missing"); } |
Ну и пример теста префабов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[Test] public void PrefabsShouldBeValid() { var prefabs = AssetDatabase.FindAssets("t:prefab"); var errors = new StringBuilder(); foreach (var prefabGuid in prefabs) { var prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid); var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath); CheckPrefab(prefab, errors); FindMissingReferences(prefab, errors); } Assert.That(errors.Length, Is.Zero, errors.ToString); } |
Шейдеры
В счастливые времена Flash-а, чтобы показать простую анимацию открытия/закрытия окна через альфу и скейл, требовалась всего пара строчек. И, хотя тогда тоже существовала проблема с наложением нескольких прозрачных спрайтов при наложении альфы, решалась она одной строчкой: window.blendMode = BlendMode.LAYER;. В Unity, к сожалению, такого простого решения нет, а проблема есть. И для решения этой проблемы можно попробовать отрисовать окно в текстуру и анимировать не само окно, а эту текстуру. Само собой, у такого решения есть куча плюсов и минусов. И кому-то оно, возможно, не подойдет вовсе. Но даже в нем есть свои проблемы. Если у вас есть полностью непрозрачное изображение (например, фон окна) и поверх него прозрачное изображение (иконка или панелька), то при отрисовке такого окна на пересечении полностью непрозрачного и прозрачного получится (готовы?) полупрозрачное изображение. Т.е. сквозь ваше непрозрачное окно можно увидеть то, что находится под ним. Происходит это из-за того, что стандартный UI шейдер производит отрисовку с наложением
1 |
Blend SrcAlpha OneMinusSrcAlpha |
. Частично эту проблему можно решить, если использовать свой шейдер с наложением, сохраняющим альфа-канал при смешивании:
1 |
Blend SrcAlpha OneMinusSrcAlpha, OneMinusDstAlpha One |
Благо, канвас позволяет изменить материал через Canvas.GetDefaultCanvasMaterial(). И, в принципе, это работает в большинстве случаев. Но в некоторых случаях различия все равно видны, например, если весь фон окна полупрозрачный, а поверх него распологаются прозрачные иконки.
Анимация на основе отрисовки окна была реализована и работала в редакторе, но при тестировании на устройстве мы увидели фиолетовые прямоугольники вместо окон. Опытные Unity-разработчики, думаю, уже поняли, в чем была проблема. Но у нас основной вариант, который нужно было проверить, вылетел из головы. В логе при запуске на устройстве выдавалась куча разных ошибок в шейдерах, но, как оказалось позже, никакого отношения к отрисовке окон они не имели. Более ничего полезного в логе не было. Помучившись какое-то время с этой проблемой, было решено временно отложить выкладку этой фичи (утро вечера мудренее, ага), тем более что на горизонте маячило очередное обновление Unity. Мы понадеялись, что, возможно, проблема решится после обновления. Накатили обновление, а проблема никуда не делась. И только через некоторое время, решая обсолютно никак не связанную с ней (но связанную с шейдерами) задачу, один из разработчиков понял, что мы просто забыли добавить новый шейдер в билд. После добавления шейдера на устройстве все заработало, как и в редакторе. Само собой, этой проблемы в принципе не было бы, если бы Unity выводила в лог ошибку при обращении к шейдеру, которого нет, что сэкономило бы уйму времени. Но увы, пока что она такого делать не умеет.
В качестве заключения хотелось бы пожелать, чтобы Unity и другие компании уделяли время не только на добавление новых возможностей, но и на исправление багов и добавление простых фич, позволяющих экономить время здесь и сейчас. А всем разработчикам — беспроблемного и стабильного софта.