Введение
Кто-то, прочитав заголовок темы, сразу подумает: «А смысл?» — и отчасти будет прав. Но только отчасти.
Я и сам некоторое время назад пользовался интегрированным гуем Юнити и не комплексовал по этому поводу. Ну, а что? Всё, что необходимо в работе для вывода информации, относительно легко и относительно же просто реализуется с его помощью. Для подавляющего большинства проектов его возможности покрывают потребности.
Но, как водится, тут есть ещё кое-что, маленькая ложка дёгтя в этой бочке мёда.
Для наглядности снабжу пример иллюстрацией из жизни. Вот есть у меня, как это модно говорить, проект, а в нём — сцена, а в сцене — компьютеры. С монитором, клавиатурой и мышкой — всё как положено. Стоят на столах и ждут. Предполагается, что игрок может подойти к любому и поработать на нём (например, выйти в интернет, в «Сапёра» потыкать или раскинуть «Косынку» — что-то одно или всё сразу — я ещё не определился).
И вот тут я словно наткнулся на стену. Оказалось, что с помощью интегрированного гуя Юнити невозможно реализовать трёхмерный интерфейс пользователя (ну да, открыл Америку). Почему и отчего так — этот вопрос адресуй к разработчикам Видимо, им и в голову не пришло при проектировании и разработке этой подсистемы движка, что иногда гуй нужен не только двумерный. К слову, в фич-риквест-листе запрос на трёхмерный гуй (на рендер гуя в текстуру, что в условиях моей задачи эквивалентно) находится уже больше года.
Как я стал решать означенную задачу.
Первым делом, конечно, начал искать способы произвести рендер гуя в текстуру (на тот момент я ещё не знал, что это принципиально невозможно). Полистал официальный форум, попробовал сделать самостоятельно и убедился, что данный процесс осуществить ну никак нельзя. Совсем-совсем. Даже если очень хочется.
Вторым моим шагом стала попытка прикрутить к Юнити существующий гуй. Был выбран gtk+, как имеющий байндинг для дотНета — gtk#. Попытка провалилась. И это было очевидно с самого начала эксперимента, так как gtk# использует для вывода собственную графическую библиотеку (или там враппер — это не суть важно) gdk и собственные средства для контроля устройств ввода. Сего факта Юнити пережить оказалась не в состоянии. Она просто молча падала при каждой попытке запуска основного цикла gtk# (хотя инициализация и создание виджетов никакой видимой реакции не вызывали).
Однако необходимо заметить, что теоретически вот этот вариант весьма и весьма правильный.
Что я хочу этим сказать. А то, что gtk легко доступен в исходниках и при наличии времени и упорства может быть немножко переписан (правда, легко может оказаться, что придётся переписывать очень даже «множко»). И в итоге получится гибкий и мощный гуй. На котором можно будет даже немного заработать (кажется, лицензия GNU LGPL позволяет, если ошибаюсь, прошу поправить).
Есть большое количество других открытых гуёв (хм... интересное словечко), с которыми при этом можно творить, что душа пожелает, и ничего тебе за это не будет. Но в большинстве своём написаны они или на чистом С или на С++ и при этом байндингов под дотНет не имеют. Это, конечно, не фатально, но объём работы (и, как следствие, временные затраты) в данном случае значительно увеличивается. А работать-то хочется прямо сейчас.
Есть, например, известный в определённых кругах CE GUI. Что я успел к данному моменту вычитать из документации по нему, так это то, что он позволяет создавать собственный рендер для вывода. Очень интересная возможность. Кроме того, существует байндинг к нему для c# (правда, он мёртв уже два года и выглядит и пахнет соответственно, то есть, как мертвец).
Возможности есть. Но неизвестно, что проще: адаптировать к условиям Юнити существующий гуй или написать с нуля новый. В этом свете закономерный мой третий шаг — это как раз и есть разработка собственной системы.
Ах да, вот только вспомнил, есть ведь EZ GUI (читается: изи гуй). Кто не видел его демы, рекомендую глянуть, впечатляет. Весьма и весьма. Отзывы о нём неплохие. Но есть у него один (это как минимум) большой и жирный минус — он денег стоит (в одно рыло — 50 баксов). Для меня сейчас проблема на автобус 10 рублей найти (ах, чёрт, с 1 ноября уже 12), так что этот вариант я даже не рассматривал.
Можно, конечно, найти EZ GUI где-нибудь на варезнике или спецфоруме (я своими глазами видел, как люди друг с другом им за спасибо делятся), но это же не комильфо, не по-пацански (кстати, у меня и Юнити бесплатная, а про рендер в текстуру там выше (фича про-версии) — это я так просто говорил).
Итак, переписывать существующий никакого желания не было и нет. Покупать — просто отсутствует необходимая денежная сумма. Решение очевидно — писать своё. Наверное, не очень умное. Мне самому с этой позиции его сейчас не оценить. Это надо со стороны смотреть. А если есть ещё какие-то выходы из ситуации, камрад, подскажи.
Аспекты реализации
Это была вводная часть, а теперь непосредственно перехожу к аспектам реализации собственного графического интерфейса пользователя.
Я видел перед собой необходимость разработки следующих частей гуя:
– подсистема обработки ввода и реализация на её основе подсистемы вызова событий гуя;
– подсистема графического отображения гуя;
– собственно иерархия классов для объектов гуя, так называемых виджетов, хотя мне привычнее называть их контролами (ударение на первый слог).
Если с виджетами всё должно быть ясно без дополнительных пояснений, то о вводе и рендеринге стоит высказать несколько дополнительных соображений.
В Юнити есть всё, что требуется самому взыскательному разработчику для отслеживания событий пользовательского ввода. Можно без труда получить состояние абсолютно любой клавиши на клавиатуре, кнопки мыши и даже кнопок джойстика (хотя ввиду отсутствия джойстика этого мне протестировать не удалось). Что интересно, координаты курсора мыши («in pixel coordinates» — цитата из Scripting Reference), которые отдаёт Юнити, имеют не целочисленный тип (в том числе и движение колеса), что несколько нелогично, так как дробного числа пикселов на экране просто физически быть не может.
Есть ещё один нюанс, который стоит учитывать при программировании пользовательского ввода и гуя: координаты нуля для экрана при вводе и рендеринге (например, с использованием интегрированного гуя) не совпадают. Для мыши нуль находится в левом нижнем углу экрана, а для гуя — в левом верхнем углу (что традиционно, привычно и понятно). Откуда взялось такое несоответствие, мне выяснить не удалось — могу только догадываться. Из этого следует, что для того, чтобы перевести координаты в одну систему отсчёта, необходимо, например, из координаты Y мыши вычитать высоту экрана:
Y = Y0 – Screen.height,
где
Y – координата Y мыши в системе гуя с нулём в левом верхнем углу экрана;
Y0 – координата мыши, которую отдаёт Input Юнити;
Screen.height – текущая высота экрана.
Ну, это очевидно.
Также необходимо заметить, что в Юнити проверка состояния клавиш и кнопок производится вызовом нескольких типов функций: проверка для виртуальных кнопок (функции GetButton это для проверки по псевдонимам клавиш), проверка для кнопок мыши (функции GetMouseButton) и проверка для всех клавиш и кнопок (функции GetKey).
Для организации корректной гуевой подсистемы ввода необходимо осуществлять диспетчеризацию всех событий ввода, а для этого надо знать состояние абсолютно всех клавиш на клавиатуре и кнопок мыши. А для этого требуется проверить их все путём вызова соответствующих функций. В данном контексте использование функций GetMouseButton не оптимально, так как определения для кнопок мыши входят в перечисление UnityEngine.KeyCode и прекрасно тестируются функциями GetKey (обычным циклом по перечислению).
Про рендер гуя говорить много не буду, так как, полагаю, что всё очевидно.
Рендер должен обеспечивать как можно менее затратный (в плане количества draw call'ов в терминологии Юнити) вывод. В идеале должен быть лишь один вызов для отрисовки всего гуя (собственно, эта задача проста до невозможности). Для этого необходимо работать с гуем в памяти, лишь при необходимости собирая его элементы в единую текстуру, которая затем отдаётся Юнити.
В реализации возможны два способа. К сожалению, у меня не было возможности сравнить их по затратам памяти и производительности. Первый способ: отрисовка элементов каждый раз по новой на единственной внеэкранной поверхности с последующим её копированием в текстуру. Второй способ: подготовка внеэкранной поверхности для каждого элемента гуя и отрисовка всего один раз (дальнейшая перерисовка требуется лишь при изменении каких-либо параметров элементов) и последующее копирование в текстуру Юнити (или другую внеэкранную поверхность — например, для создания какие-то эффектов — тогда уже её нужно будет копировать в текстуру).
Есть чисто умозрительный вывод, что первый способ будет менее расходовать память, чем второй, а по скорости работы они примерно одинаковы. Для создания же скинующегося интерфейса придётся пользоваться вторым способом, так как в этом случае затраты на масштабирование битмапов при отрисовке станут превышать затраты на копирование. Впрочем, может быть, на деле всё и не так будет. Ещё раз повторюсь: это всего лишь мысли, не подкреплённые опытными данными.
Собственно сама реализация гуя, если говорить об иерархии классов объектов, не представляет сложности даже для неопытного программиста.
Что сделано
– Подсистема обработки ввода Юнити.
Производит форматирование информации ввода в понятную контролам (виджетам) гуя, затем на основе этой информации диспетчер дёргает контролы за яй... э... осуществляет вызов соответствующих ивентов.
Обработка ввода пока происходит только в экранном пространстве.
– Иерархия классов объектов гуя.
Реализованы следующие классы: базовый контрол, кнопка, лэйбл (короткий текст), чекбокс, радиобатон, окно (не полностью). Контролы поддерживают объединение в контейнеры (по принципу один родитель— неограниченно детей). Вызываются следующие ивенты: получение и потеря фокуса ввода, нажатие, удержание и отпускание клавиши клавиатуры, нажатие, удержание и отпускание кнопки мыши, двойной клик мышью, движение колеса мыши, движение указателя мыши.
Этого минимума вполне достаточно для демонстрации принципов.
– Подсистема вывода изображения.
При подготовке гуя производится рендеринг элементов во внеэкранную поверхность, которая после необходимых преобразований (связанных с особенностями хранения битмапов в памяти) копируется в текстуру Юнити и рендерится. Для рисования элементов гуя используется библиотека векторного рисования cairo (если точнее, то её дотНет байнд Mono.Cairo). Для вывода текста используется она же (работает в UTF-8).
Cairo производит достаточно быстрый рендеринг, так что здесь единственное тонкое место — копирование битмапа из памяти в текстуру.
Что сделать
Безусловно, с настоящим наборов контролов интерфейс не может считаться полнофункциональным. Пока это всего лишь демонстрация. Необходимо ещё как минимум реализовать меню, текстовые поля для ввода и отображения больших объёмов текста, полосы прокрутки. Также необходимо реализовать возможность создания скинов для интерфейса.
Крайне важно обеспечить корректную работу ввода не только в экранных координатах, но и в координатах сцены.
Необходимо также уточнить, что представленное решение подходит пока что только для standalone-сборок, в браузере этот интерфейс работать не будет.
А нахрена?
Вроде я сказал всё, что хотел. Надеюсь, это небольшое эссе не утомило тебя, камрад.
А теперь самый главный вопрос: а для чего всё это? А вот для чего. Я догадываюсь, что не единственный такой на свете, кому вдруг понадобилась возможность рендерить гуй в текстуру. Ну, и вот, может быть, ты тоже как-то что-то похожее когда-нибудь делал в Юнити? Я хочу знать, что у тебя получилось, а что нет и почему?
Хочу обменяться опытом.
Да, демки пока нет, только скриншоты, и кода не привожу, поскольку его много, да и не нужен здесь он, разговор-то ведь скорее о принципах нежели. Но если попросишь, покажу код, естественно не весь (ещё раз: его реально много), а только интересующие места.