Gamedev Alex Smith
3 499

Кастомные редакторы для Unity3D, которые мы используем в игре

Unity — сказочный движок.

В закладки

Иногда эта сказка добрая — про пони и радугу. А иногда — про страшного бабайку. И очень важная часть данного движка, а именно расширения для редактора (custom inspectors) — как раз про бабайку. В статье я бы хотел поделиться опытом использования собственных расширений на примере нашей игры.

Некоторые из наиболее интересных наших расширений. Сколько на экран влезло

Начнём с определения. Вот, что говорится в документации про расширения: «Unity позволяет вам расширить редактор своими собственными инспекторами и окнами редактора (Editor Windows), и вы можете задать, как свойства должны отображаться и инспектора при помощи пользовательского Property Drawers».

То есть имеем три основных типа для расширения функциональности, наследуемые от базовых классов:

  • UnityEditor.Editor;
  • UnityEditor.EditorWindow;
  • UnityEditor.PropertyDrawer.

И сразу же первая загвоздка — все расширения делаются только с помощью кода. Никакого визуального программирования и блок-схем, WYSIWYG, перетаскивания кнопочек по формочкам. Только C #, только хардор. Изменил размер кнопки — будь добр пересобрать весь проект, чтобы увидеть изменения. Соответственно, написать новое расширение или поправить скаченное из Asset Store не программисту — практически непосильная задача.

Первый тип (Editor) используется для того, чтобы сказать Unity «как отобразить класс». Это может быть ScriptableObject или MonoBehaviour. Используется чаще всего, по крайне мере у нас. Второй (EditorWindow) — может быть использован для отображения почти любого содержимого в пределах нового окна, за которое он отвечает. Третий (PropertyDrawer) отвечает на вопрос «как отобразить параметр». Причём это может быть как один из стандартных типов параметров, так и свой собственный.

Пример кастомного окна редактора из документации

Для каждого из типов расширений можно найти по небольшому «Hello World» в документации. Но чтобы сделать действительно полезное, удобное и красивое расширение, придётся ещё постараться. Связано это, во-первых, с наличием огромного числа недокументированных и внутренних (Internal) функций движка, которые можно использовать.

Во-вторых, с большим количеством багов и общей неинтуитивностью подсистемы. В-третьих, почти все статьи про расширения на английском или вообще иероглифы. Если с английским лично я ещё могу справится, то китайский поддается только Google-переводчику, что слабо помогает общему усвоению информации. Именно поэтому сделать что-то сложнее двух дополнительных кнопок — сродни алхимии и чёрной магии. Но когда это нас останавливало?

В нашей, казалось бы, небольшой игре используется 22 собственных скрипта для расширений на приблизительно 2,5 тысяч строк кода в сумме. Приведу особо интересные примеры для каждого типа расширений.

PropertyDrawer

Здесь, как раз, ничего сильно интересного у нас нет. Просто для примера — отображение свойства неактивным, если объект на сцене. И возможность редактировать свойство, если объект является префабом.

Эта функция используется для свойств-идентификаторов объектов. Что-то типа ИД, необходимого для сериализации/десериализации структуры уровня. Защита от самого себя, чтобы не поменять идентификатор во время сборки уровня в редакторе. Если так сделать — уровень сохранится, но загружаться не будет.

Еще есть PropertyDrawer для класса «Координаты X и Y» — вместо двух строчек выводит в одну. Ещё один — для отображения коллекции string в виде выпадающего списка.

EditorWindow

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

Первый — получение картинки-превью префаба. То есть той картинки, которая отображается в самом редакторе Unity. Реализуется с помощью одного метода AssetPreview.GetAssetPreview(gameObject).

Второй — центрирование камеры в SceneView к выбранной ячейке.

Editor

Здесь начинается самое интересное. Во-первых, был реализован универсальный базовый кастомный эдитор, который выводит в начало своей верстки короткое информационное сообщение. Это может быть просто текст подсказки, предупреждение или сообщение об ошибке. В зависимости от типа сообщения, блок с текстом отображается индивидуально. Визуально выглядит так.

Дальше от него наследуются все остальные кастомные эдиторы. Самые «толстые» у нас — это ContentPackEditor, LevelEditor, LocalizationEditor, GameFieldEditor. Каждый от 200 до 300 строк кода. Опять же, приводить код каждого смысла не имеет. Расскажу только про интересные моменты.

Для ContentPackEditor нужны были функции визуальной сортировки элементов массива. И они в Unity есть, это ReorderableList в UnityEditorInternal. Вот статья по основам использования.

От себя я добавил сворачивание/разворачивание списка при клике на заголовок.

В LocalizationEditor надо было сделать что-то вроде таблицы, чтобы чётные и нечётные сроки выводились разным цветом. Решается это заведением двух разных стилей.

Вторым проблемным моментом было сделать поиск в уже добавленных ключах локализации и мгновенный вывод результатов в виде выпадающего списка под строкой поиска. Вот для этого найти решение было действительно непросто. В итоге нашёлся метод на каком-то китайском сайте с иероглифами. Хорошо, что код — он и в Африке (Китае) код.

Путём небольшой доработки напильником стало выглядеть вот так.

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

Почитать статью без картинок, зато с объемным кодом на C # можно на форуме.

Подписывайтесь, комментируйте, покупайте нашу игру.

Страница в Steam

ВКонтакте

Youtube

Сайт

#опыт #unity

Материал дополнен редакцией
{ "author_name": "Alex Smith", "author_type": "self", "tags": ["unity","\u043e\u043f\u044b\u0442"], "comments": 38, "likes": 37, "favorites": 40, "is_advertisement": false, "subsite_label": "gamedev", "id": 16172, "is_wide": false }
{ "id": 16172, "author_id": 45428, "diff_limit": 1000, "urls": {"diff":"\/comments\/16172\/get","add":"\/comments\/16172\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/16172"}, "attach_limit": 2, "max_comment_text_length": 5000 }

38 комментариев 38 комм.

Популярные

По порядку

Написать комментарий...
12

Пытаться делать в редакторе Юнити вещи, для которых он не предназначен - плохая идея. Вы гробите туеву хучу времени на борьбу с API и UI, вместо того, что бы задействовать сторонние тулсы, написав импорт данных в пару десятков строк.

На примере вашего локализатора - да боже мой, импорт из Excel был бы в 1000 раз удобнее.

Ответить
5

Только пожалуйста, не надо писать очередные десериализаторы XML, json и прочих сторонних форматов! И уж тем более, класть их потом в папку Resources (почему-то очень популярный у начинающих разработчиков варварский подход). ScriptableObject сериализируется в прекрасный и читаемый YAML, и большинство гейм-дизайнеров скажут вам только спасибо, если игровые данные можно будет отредактировать сразу в движке, не занимаясь никакими импортами – а удобство гейм-дизайнеров это всегда основная задача разработчиков тулзов.

Локализация это, пожалуй, единственный пример где импорта стороннего формата имеет смысл, ведь локализаторы это сторонняя компания. Но и при работе с локализацией гейм-дизайнеров должны писать исходные тексты на «основном» языке, пока контент не готов к отправке на перевод, прямо в движке.

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

Ответить
1

Я вас не совсем понимаю.
1) Кого класть? десериализаторы?
2) Про YAML не знал, загуглил. Спасибо.
3) Ну вроде кастомный редактор позволяет "никакими импортами" и не заниматься, в чем здесь проблема? :)

Ответить
1

1) ресурсы
2) так это же был не вам комментарий, как раз с кастомнымм редакторами я согласен

Ответить
0

А куда класть ресурсы, если не в папку Resources?
В ассет-бандлы?

Ответить
0

Вообще, в Unity есть три основных места: в Resources, просто в проект, и, наконец, в ассет-бандлы.

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

Ответить
0

Когда-то делал смену скинов, и приходилось в рантайме загружать-выгружать текстуры. Ассет бандлы я не вывез (дело было до 5.3) а ресурсы работали как часики.

Ответить
0

Если задача требует ручного управления загрузкой и выгрузкой ресурсов из памяти, то без нормального менеджера ассетов, который будет скрывать ассет бандлы под адекватным слоем абстракции никуда, по-моему.

Другое дело, что такие вещи нужны очень редко. Я сейчас работаю над проектом с открытым миром, где в принципе нет разделения на сцены - и автоматическое управление ресурсами, просто с общей выгрузкой неиспользуемых ассетов, работает на боевых загрузках очень адекватно. Даже отключение обджект пула, написанного предыдущего разработчиками, судя по всему, по инерции, не даёт никаких пенальти к производительности - зато прибавляет огромное количество очков стабильности.

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

Ответить
0

"На больших проектах лучше переключаться на ассет бандлы, чтобы билд не занимал пару часов."

А можете кинуть ссылку на реализацию такого метода работы с ресурсами?

Ответить
0

https://docs.unity3d.com/Manual/AssetBundlesIntro.html

У Юнити очень неплохая официальная документация, рекомендую.

Ответить
0

Спасибо

Ответить
5

И да, и нет. Основной вопрос - работа с ключами. Чтобы ключи не повторялись, чтобы для каждого языка был ключ, чтобы можно было удалить или добавить фразу на всех языках одновременно...
Я бы сказал, что расширения юнити и Excel должны дополнять друг друга. Например, чтобы дать текст на перевод стороннему человеку, а потом загрузить его обратно в игру. И при этом ничего не сломать. Нельзя недооценивать человеческий фактор)

Ответить
6

Вообще не понимаю нытье по этому поводу - у юнити лучшая кастомизация редактора что я видел. У меня на всех проектах весь пайплайн валидации контента которого много построен на редакторах и я не помню чтобы у меня что-то горело. Плюс не забывайте что они добавили UIElements и теперь все стало гораздо проще и быстрее писать (если конечно вы не делаете это в моем стиле)

Ответить
0

Ого, что-то новенькое. Пошел читать, спасибо за наводку.

Ответить
0

Прочитал вчера твой этот коммент, сегодня решил попробовать на очередной задаче, залез - ни документации, ни апи нормального, и вообще всё экспериментальное.

Нафиг-нафиг экспериментальщину на живом проекте трогать. Подождать пару лет, пока все схватят все шишки и апи стабилизируется, и потом уже перелазить. Для редактора старый добрый imGUI по-прежнему вполне рабочий.

Ответить
4

Для работы с Editor есть, кстати, очень неплохой плагин OdinInspector. У него там и сериализация своя есть, чтобы выводить в Инспекторе всё подряд, и очень много встроенных функций, чтобы некоторые штуки делать просто и легко через аттрибуты.

Ответить
0

Одной из основных фишек Одина, на мой вкус, получается что он позволяет не множить файлы и описывать функции кастомного эдитора прям в классе. Хотя возможно это идеологически не очень правильно. И все же Один значительно казуальнее родного механизма кастомизации интерфейсов. Ну и там есть много приятных вещей "из коробки"... сериализация Dictionary, удобное табличное отображение List, сразу содержащее механику ReorderableList, всякие подсказки, кастомные механики кнопок и прочего...

Ответить
0

+1 Подобные ассеты стоят своих денег!
Только я использовал не Odin, а EasyEditor или как-то так. Во первых можно в основном классе быстро докинуть пару кнопок в эдитор простой анотацией, во вторых для сложных вещей можно юзать их базовые эдитор-классы, это уже упрощает все в сотню раз.

Ответить
–9

Какой ад.
Вот поэтому мне ни один движок не нравится и пишу свой : )

Ответить
5

Я тоже свой писал когда-то, над XNA. Это, конечно, весело - но к готовой игре имеет мало отношения, к сожалению.

Ответить
4

Оп-па, некрокодер из 2000-х детектед :)

Ответить
1

на ассемблере, разумеется?)

Ответить
4

На своём языке программирования.

Ответить
0

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

Ответить
3

Локализацию делаете своими силами, а затем тратите тонну времени на "вбивание буковок"? А чтоб внешний райтер что либо отредактировал - ему нужно отправлять целиком проект или часть проекта?)
Не учите людей плохому, пожалуйста.

Рекомендую пользовать локализацию так: XLS (или CSV) файл, конвертер в бинарник, загружать на старте игры/редактора.
Доступ к строкам после выгрузки в бинарник можно получить нативными средствами.
Если не забыть слеши, то вообще красиво можно сделать редактор. А главное удобно.
PS: еще регексы в помощь, чтоб встраивать всякие разные штуки в текст, но для этого придется написать еще свой небольшой парсер.

Ответить
3

У меня вопрос не по теме: у вас в офисе такие флаги висят?

Ответить
0

Шутку понял, смешно))

Ответить
–2

Вся разработка игр такой ад? Я тогда даже лезть не буду. Может есть попроще что-то, понагляднее?

Ответить
5

Это ещё не ад, это ещё приятные и прямолинейные вещи.

Ад начнётся, когда вы полезете разбираться, как разные платформы разные семантики шейдеров воспринимают, например. Или как пробить NAT.

Ответить
3

Нет, просто в статье речь не про разработку игры, а про разработку дополнительных инструментов для разработки игры. Они не являются обязательными, и тут все зависит от проекта. А так Юнити вполне доступен для понимания.

Ответить
2

А вы думали разработчики только "текстурки рисуют и в игры играют"? :D
Это вы ещё не видели, как в крупных компаниях движки пишут

Ответить
0

всегда можно упростит задачу так чтобы избежать проблем, для решения которых еще нет готового средства. например "даже не лезть" :)

Ответить
1

Привет. Спасибо за текст, поправили чутка и закрыли редактирование, чтобы в соцсети вывести. Если что-то изменить захотите — пишите.

Ответить
0

Вообще смотрю на всё это и почему то в голове крутится мысль - что это всё не оправдано, на банальные вещи делать такие вот кастомизации, есть масса способов сделать это проще. На локализацию если хочется с ключами - можно тот же json сделать, и его могут править сами локализаторы. И можно будет сколько угодно раз заменить и добавить. Айтемы хоть из экселя, хоть опять же джейсон.

Ответить
0

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

Ответить
0

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

Ответить
0

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

Ответить
0

Да, зачем пилить UI на юнитевском imGUI когда есть божественный HTML.

Ответить
0

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fizc" } } }, { "id": 4, "label": "240х200_mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "flbq" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjog" } } }, { "id": 10, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-250597-0", "render_to": "inpage_VI-250597-0-1134314964", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=clmf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Плашка на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudo", "p2": "ftjf" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvc" } } } ]
Игру с лучшим стелсом никто не заметил
Подписаться на push-уведомления