Случайны выбор дневника Раскрыть/свернуть полный список возможностей


Найдено 742 сообщений
Cообщения с меткой

unity - Самое интересное в блогах

Следующие 30  »
rss_rss_hh_new

Google Tango: управляем роботом в режиме дополненной реальности

Четверг, 29 Сентября 2016 г. 21:29 (ссылка)

Благодаря Хабрахабру и Google к нам в лабораторию робототехники Сколтеха на неделю попал дев-кит планшет проекта Google Tango. Мы не только протестировали данное устройство, но и написали небольшое демо по управлению мобильным роботом в режиме дополненной реальности. Но об этом чуть позже, сначала немного об устройстве и наших впечатлениях от него.



Статья автора Дмитрия Сенашенко, в рамках конкурса «Device Lab от Google».




Думаю многие из вас уже слышали о данном проекте и неплохо представляют что он из себя представляет. Если вкратце, то это платформа компьютерного зрения и локализации нацеленная на применение в мобильных устройствах. Используя данные с двух камер (широкоугольной и обычной), датчика глубины (по сути Kinect в миниатюре), акселерометров, гироскопов и барометра устройство проекта Google Tango способно воспринимать окружающее трёхмерное пространство и отслеживать своё положение в нём. Громадная заслуга группы инженеров ATAP (Advanced Technology and Projects) заключается не только в том, что они смогли уместить всё это оборудование в мобильном устройстве, но и в том что у них вышло разработать дружелюбное к разработчику высокоуровневое SDK, которое берёт на себя основную тяжёлую работу по обработке данных с сенсоров и проведению необходимых преобразований, позволяя разработчику работать с удобными абстракциями. Так же в лучших традициях Google нам доступна документация высокого качества, позволяющая достаточно быстро освоиться с устройством даже разработчикам без опыта разработки приложений под Android.



Об устройстве



Принцип работы







Устройство по сути имеет два основных режима локализации: с Area Learning и без него. В первом режиме мы предварительно сканируем помещение и строим его карту (к сожалению это делается offline, т.е. сначала обработка накопленных данных, потом использование результата в виде файла ADF — Area Description File), после чего мы можем весьма точно локализоваться в изученном помещении, компенсировать дрейф и справляться с проблемой временной потери трекинга. (например, при закрытии сенсоров рукой или другим слишком близко поднесённым объектом)



Второй режим позволяет нам проводить локализацию в пространстве и отслеживание движение устройства безо всякой предварительной подготовки. Работает он на основе совмещения данных со всех датчиков: IMU (Inertial Measurment Unit), визуальной одометрии по особым точкам изображения широкоугольной камеры, датчика глубины и т.д. Но т.к. нам неизвестны точки за которые мы могли бы зацепиться, в данном режиме координаты устройства будут подвержены дрейфу за счёт постоянно накапливающейся ошибки. (см. иллюстрацию) Кроме того имеется риск потери трекинга, корректное восстановление из которого в данном режиме в общем случае невозможно.



Пользуясь данными локализации (т.е. по сути зная с некоторой точностью координаты и ориентацию устройства относительно помещения) и имея трёхмерное облако точек с датчика глубины мы имеем возможность создавать приложения дополненной реальности ранее принципиально невозможные на мобильных устройствах. Логичным продолжением была бы установка Tango на очки дополненной реальности (следующая итерация Google Glass наподобии Hololens?), но пока мы можем воспользоваться эрзац-заменителем в виде Google Cardboard.



Немного о точности



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

Т.к. мы были весьма ограничены по времени, то решили всего лишь сделать кустарную оценку сверху используя в качестве инструмента стол с известными размерами. (два на три метра) Перемещая устройство из одного угла стола в другой по кривой траектории с отклонением от стола вплоть до двух метров мы записывали глобальные координаты (с и без Area Learning) в каждой из точек, после чего рассчитывали расстояние между данными точками и сравнивали с тем, что должно было получиться. Итоги следующие:




  • Среднее отклонение составило 2-3 см, в худших случаях вплоть до 5-6 см


  • Точность с Area Learning и без него на траекториях 15-20 метров кардинально не отличаются, что говорит о достаточно высоком качестве локализации по визуальной одометрии и IMU


  • Ориентация устройства влияет на координаты с ошибкой вплоть до 5 см (в т.ч. и с использованием Area Learning), т.е. если вернуть устройство в исходную точку, но повёрнутым, его координаты будут несколько иными




По понятным причинам точность сильно зависит от помещения в котором проводятся измерения. В помещении с хорошим освещением (устройство плохо относится к прямому солнечному свету) и большим количеством особых точек вполне реально добиться точности в пару сантиметров. Но в плохих условиях устройство быстро теряет трекинг, IMU конечно до поры до времени выручает, но его возможности весьма ограничены. Поэтому мы и бросили идею поставить танго на индустриальную робо-руку и измерить таким образом точность, ибо комната с роботом плохой пример «обычного» помещения.



Теперь о точности датчика глубины. Проверять его точность мы решили снимая облака точек для плоских объектов (пола, стен, столов) и анализируя насколько хорошо точки ложатся на плоскость. На оптимальной дистанции 0.5-4 м точность обычно составляла около 0.5 см, но на некоторых поверхностях точность падала в 2-3 раза, например на поле нашей лаборатории, покрытом черно-белым ковром в крапинку. Похоже текстура играла злую шутку с алгоритмами определения глубины основанными на структурированном ИК излучении.



Об SDK и API



Если кратко, то Google на высоте. Думаю как только Tango устройства попадут в широкую продажу, то отбоя от разработчиков не будет не только из-за уникальных возможностей устройства, но и из-за простоты программирования приложений для него. Фраза во вступлении, о том что с девайсом может освоиться даже разработчик без опыта программирования под Android — эксперементально подтверждённый факт, т.к. основной разработчик для Tango нашего демо управления роботом — Марко Симик (иностранный магистр нашей лаборатории), практически не имел опыта разработки под Android, но тем не менее смог за пару дней смог изучить инструменты и API в объёме достаточном для написания простеньких приложений.







Но хватит похвал. Tango Service представляет собой сервис работающий отдельным процессом. Общая структура программного стека показана на иллюстрации.



SDK предоставляет возможность работать с C++, Java и Unity. Порядок примерно соответствует их «высокоуровневости». Разумеется разработчики игр скорее всего оценят по достоинству возможность использования Unity и будут преимущественно выбирать данный вариант. Если же вы хотите работать напрямую с AIDL (Android Interface Definition Language) или другими Java приложениями, то Java API для вас. Разработчики же желающие разрабатывать приложения с Android NDK и иметь более полный контроль выберут C API.



Во всех трёх вариантах API практически идентично и предоставляет инструменты для съёма данных с устройства, управлением им и проведения необходимых преобразований из различных систем координат. (коих имеется аж 6 штук)



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



Ложка дёгтя



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



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



Кроме того не стоит ожидать чудес от построения карт. Примерно качество можно увидеть например в этом видео.







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



Демо: управляем роботом



Видео с конечным результатом недельного знакомства с Google Tango:







Исходный код скриптов для Unity опубликован на Гитхабе.



Идея



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



В нашем случае мы решили сделать виртуальный интерфейс для вполне реального мобильного робота. Т.к. мы были весьма ограничены по времени, то решили ограничиться лишь управлением роботом посредством задания точек куда ехать роботу в реальном пространстве. Разумеется данную идею можно продолжать множеством направлений: добавлять дополнительные интерфейсы для робота (например, «голограмму» человека над роботом, визуализировать обмен данными между роботами), создавать обратную связь (например, показывая запланированную траекторию робота), делать игры совмещающие реальное и виртуальное (например, собирать виртуальные объекты реальной машинкой) и тому подобное.



Но даже простое исполнении контроля робота через задание целевых точек может оказаться весьма полезной фичей, особенно на производствах в которых становится всё больше и больше роботов.

Не говоря уже о потенциальных возможностях развития идей использования Google Tango для контроля физических объектов.



Исполнение



Для реализации данной идеи мы решили использовать Unity API, как наиболее простое и лёгкое для построения демо приложения в виду своей высокоуровневости. Для повышения надёжности определения координат мы использовали локализацию с использованием Area Learning. (на практике, вероятнее всего, роботы будут использоваться в известных помещениях, промерить которые не составит труда). Конечно можно было обойтись и без него, но точность и надёжность значительно от этого пострадают.



Разумеется, что бы приложение заработало желательно, что бы робот имел собственные средства навигации в пространстве, иначе нам постоянно придётся держать робота в области видимости устройства, что согласитесь не очень удобно. В нашем мобильном роботе использовался двухмерный лазерный сканер Hokuyo-04LX и программное обеспечение реализующие SLAM (одновременная локализация и картографирование), на выходе которого мы получали карту занятости (occupancy grid) окружающей местности, используя которую мы уже можем планировать траекторию движения робота. (софт для робота был по большей части самописным, но всё то же самое можно сделать используя готовые модули в ROS).



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



Для того что бы реализовать данную задумку приложению необходимо было включить в себя три фичи: трекинг движения, Area Learning и получение карты глубин. (или иначе говоря трёхмерного облака точек) Использование приложения проходит по следующему пути:




  1. Записать Area Description File (ADF), т.е. провести Area Learning помещения в котором будет использоваться программа


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


  3. Отметить положение робота


  4. Отметить целевую точку и нажать кнопку исполнения команды




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



Код



Исходники в репозитории (ссылка выше) содержат лишь три скрипта: выбор файла ADF, сплеш-скрин инициализации (релокализации) после выбора ADF, основной скрипт управления и UI. Что бы воспользоваться этими скриптами достаточно добавить их в пример AreaLearning.



Unity скрипты исполняются определённым образом, имеется 3 главных коллбека, которые мы используем в нашем демо:




  • Start() запускается при запуске приложения


  • Update() запускается при каждом обновлении кадра


  • OnGUI() запускается несколько раз за кадр




В итоге у нас получилась следующая структура:




  • Start() назначает коллбеки к соответвующим событиям


  • Update() обрабатывает касания к экрану и при необходимости запускает корутину для нахождения глобальных координат соответствующих тапнутым координатам экрана, плюс рисует красный маркер при успешном выборе точки


  • OnTangoDepthAvailable, OnTangoPoseAvailable коллбеки ожидающие событий от Tango и устанавливающих соответствующие флаги при запуске


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


  • _WaitForDepth ожидает карту глубин (трёхмерное облако точек) и находит координаты точки в глобальной системе отсчёта для заданной координаты на экране


  • _WaitForRequest обрабатывает посылку команды роботу, в нашем случае это был простой GET запрос




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



Заключение



По итогам нашего знакомства могу с уверенностью сказать: Google Tango чрезвычайно многообещающее устройство, которое способно совершить в ближайшие года переворот в том, что мы считаем мобильными устройствами. В общем, весьма маловероятно что данный проект закончит как Glass и Wave. Разумеется на данный момент устройства не лишены детских болячек, и как любая технология имеют свои пределы и особенности, но первое будет поправлено, а ко второму, думаю, пользователи и разработчики постепенно привыкнут.



Таким образом, мы считаем, что давно навзревавший бум виртуальной реальности по видимому скоро раскроется в полную силу, и Google Tango явно намерен возглавить его посредством своей простоты программирования, дружелюбия к разработчикам, сложившейся Android экосистемы, в так же активного продвижения Google.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/309870/

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Победители Google Device Lab: исследуем Project Tango

Четверг, 22 Сентября 2016 г. 16:57 (ссылка)

Здравствуйте, коллеги! Наша небольшая компания Vizerra разрабатывает заказные проекты и собственные продукты в области дополненной и виртуальной реальности (далее ARVR) для самых разных заказчиков. Почти на каждом проекте нам требуется придумывать какое-то новое и неожиданное решение. ARVR рынок очень юн и многие решения создаются во время проектов. Таким образом происходит его развитие.



Статья-победитель автора Александра Лаврова, в рамках конкурса «Device Lab от Google».




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

Наше знакомство с Project Tango началось весной этого года, когда возникла идея попробовать безмаркерный трэкинг для ряда наших проектов.



Оказалось, что заполучить устройство для экспериментов очень непросто. Нам помог фонд Сколково познакомив с русским офисом Google. Ребята оказались очень отзывчивыми и выдали столь желаемое устройство на руки.

Мы смогли достаточно быстро достичь нужных нам результатов, показали наши тесты на Startup Village и с чувством выполненного долга вернули устройство в Google.



Пару месяцев назад мы узнали, что стартовал Google Device Lab, и мы решили, что нужно отплатить добром за добро — поделиться нашим опытом с другими разработчиками и сели писать данную статью. В процессе написания статьи её концепция несколько раз менялась. Хотелось как обычно сделать что-то полезное, а не описать наш очередной проект. В итоге посовещавшись мы решили, что наиболее полезным будет написать русскоязычный полный обзор по Project Tango с учетом всех набитых нами шишек. Тогда мы начали объединять разрозненные источники, что бы у читателя создалась достаточно полная картина по прочтении данной статьи.

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







Аппаратная часть



Для начала давайте посмотрим что же представляет из себя само устройство.



На момент тестирования существовало 2 доступные модификации:




  1. Планшет «Yellowstone». Обычно когда люди говорят про Tango, то подавляющее большинство представляет себе именно его.

  2. Телефон «Peanut». Честно говоря не знаем никого из тех, кто реально его держал в руках.







Нам достался для экспериментов 7 дюймовый планшет «Yellowstone», входящий в состав Project Tango Tablet Development Kit.



Сам базовый планшет в принципе удивления не вызывает. Устройство содержит 2.3 GHz quad-core Nvidia Tegra K1 processor, 128GB flash memory, 4 мегапиксельную камеру, сенсорный экран разрешением 1920x1200 и 4G LTE.



В качестве операционной системы используется Android 4.4 (KitKat). Мы уже порядком отвыкли от систем младше 5.0. Последний год минимальным требованием клиентов была поддержка устройств с Android 5.0.



Следует помнить, что устройство является частью Development Kit, а не устройством для конечного пользователя. Для прототипа оно выглядит отлично и даже удобно в использовании! Начинка устройства далеко не самая современная, но устройство отлично функционирует. Очевидно, что в серийных устройствах конечные производители, использующие платформу Tango, легко смогут использовать новейшее железо и ОС.



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




  • Fisheye камера для отслеживания движения

  • 3D датчики глубины

  • Акселерометр

  • Датчик окружающего света

  • Барометр

  • Компас

  • GPS

  • Гироскоп



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







Для любителей посмотреть «что там на самом деле у него внутри» прилагаем фотографию устройства в разобранном виде. Разбирали не сами. Спасибо, коллегам с портала slashgear.com за интересное фото.







Google предупреждает, что Tango и Android не являются аппаратными системами реального времени. Основной причиной является то, что Android Linux Kernel не может предоставить гарантий времени исполнения программы на устройстве. По этой причине Project Tango можно считать программной системой реального времени.



Прицип действия устройства



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



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



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



Tango дает мобильным устройствам возможность определения их положения в окружающем мире с использованием трех основных технологий: Motion Tracking (отслеживание движения), Area Learning (изучение областей пространства) и Depth Perception (восприятие глубины). Каждый из них по отдельности имеет свои уникальные возможности и ограничения. Все вместе же они позволяют достаточно точно позиционировать устройство в окружающем мире.





Motion Tracking



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

Результатом работы данной технологии являются относительная позиция и ориентация устройства в пространстве.



Ключевым термином в технологии Motion Tracking является Pose (поза) — сочетание позиции и ориентации устройства. Термин звучит, честно говоря, довольно специфически и, вероятно, Google решил подчеркнуть им свое стремление к совершенствованию человеко-машинного интерфейса. Обычно под позой подразумевается положение, принимаемое человеческим телом, положение тела, головы и конечностей по отношению друг к другу.



Tango API предоставляет положение и ориентацию устройства пользователя с шестью степенями свободы. Данные состоят из двух основных частей: вектор в метрах для перемещения и кватернион для вращения. Позы указываются для конкретной пары точек траектории. При этом используется система отсчёта — совокупность тела отсчёта, связанной с ним системы координат и системы отсчёта времени, по отношению к которым рассматривается движение каких-либо тел







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



Танго реализует Motion Tracking с помощью визуально-инерционной одометрии (visual-inertial odometry или VIO).

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







Изображения являются собственностью NASA.



Визуально-инерциальная одометрия дополняет визуальную одометрию инерциальными датчиками движения, способными отслеживать вращение устройства и его ускорение в пространстве. Это позволяет устройству рассчитать его ориентацию и движение в 3D-пространстве с еще большей точностью. В отличие от GPS, отслеживание движения с помощью VIO работает и в закрытом помещении. Что стало поводом для появления большого количества идей по созданию относительно недорогих indoor навигационных систем.



Визуально-инерциальная одометрия предоставляет улучшенное определение ориентации в пространстве по сравнению со стандартным Android Game Rotation Vector APIs. Она использует визуальную информацию, которая помогает оценить вращение и линейное ускорение более точно. Таким образом мы наблюдаем удачное комбинирование известных ранее технологий для устранения их известных проблем.



Все вышеперечисленное нужно для того, чтобы при объединении вращения и отслеживания положения в пространстве, мы смогли использовать устройство в качестве виртуальной камеры в соответствующем 3D-пространстве вашего приложения.







Motion tracking является базовой технологией. Она проста в использовании, но имеет ряд проблем:




  • Технология не разбирается сама по себе, что именно находиться вокруг пользователя. В Tango за эту задачу отвечают другие технологии: Area Learning и Depth Perception.

  • Motion Tracking не «помнит» предыдущие сессии. Каждый раз, когда вы начинаете новый сеанс отслеживания движения, отслеживание начинается снова и сообщает свою позицию по отношению к её стартовой позиции в текущей сессии.

  • Так же, судя по экспериментам, тряска устройства приводит к потере трэкинга. Это очень явно проявлялось при его использовании в автомобиле.

  • При перемещении устройства на большие расстояния и в длительные периоды времени происходит накопление мелких ошибок, что может привести в итоге к большим ошибкам в абсолютном положении в пространстве. Произойдет так называемый «drift» (дрейф). Это еще один термин заимствованный из реального мира. Обычно под этим требованием подразумевается отклонение движущегося судна от курса под влиянием ветра или течения, а также движение льдов, несомых течением. Для борьбы с дрейфом Tango использует Area Learning для коррекции данных ошибок. Этот аспект мы подробно рассмотрим в следующей части статьи.







Area Learning



Люди умеют распознавать, где они находятся, заметив известные им признаки вокруг себя: дверной проем, лестницу, стол и т.д. Танго предоставляет мобильному устройству ту же способность. С помощью одной лишь технологии motion tracking, устройство через камеру «видит» визуальные особенности района, но не «помнит» их. С помощью area learning, устройство не только «помнит», что оно «увидело», но может также сохранять и использовать эту информацию. Результатом работы данной технологии являются абсолютные позиция и ориентация устройства в уже известном ему пространстве.



Tango использует метод одновременной локализации и построения карты (SLAM от англ. Simultaneous Localization and Mapping) — метод, используемый в мобильных автономных средствах для построения карты в неизвестном пространстве или для обновления карты в заранее известном пространстве с одновременным контролем текущего местоположения и пройденного пути.



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



В контексте Tango Area learning решает 2 ключевые задачи:




  1. Повышение точности траектории полученной с использованием motion tracking. Данный процесс получил название «Коррекция дрейфа».

  2. Ориентация и позиционирование самого устройства внутри ранее изученной области. Данный процесс назван «Локализация».



Давайте рассмотрим подробнее как именно решаются эти задачи.



Коррекция дрейфа



Как уже было сказано ранее, area learning запоминает визуальные признаки в областях реального мира, которые устройство «видело» и использует их, чтобы исправить ошибки в его позиции, ориентации и движении. Когда устройство видит место, которое оно видел ранее в вашей сессии, то оно понимает и корректирует свой путь, чтобы он соответствовал его предыдущим наблюдениям.

На приведенном ниже рисунке показан пример коррекции дрейфа.







Как только мы начнем ходить через известную Tango область, то мы получаем на самом деле две различные траектории, происходящие одновременно:




  • Путь по которому вы идете («Реальная траектория»)

  • Путь вычисляемый устройством («Вычисляемая траектория»)



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



Без коррекции дрейфа, игра или приложение с использованием виртуального 3D пространства в соответствии с реальным миром может столкнуться с неточностью в отслеживании движения после длительного использования. Например, если дверь в игровом мире соответствует дверной раме в реальном мире, то ошибки могут привести к тому, что в игре дверь появляться в стене, а не в реальной дверной коробке.



Локализация



После того, как мы прошли через нужную вам область с включенным area learning, мы можем сохранить в Area Description File (ADF) то, что устройство «увидело».

Изучение области и загрузка ее в качестве ADF имеет ряд преимуществ. Например, вы можете использовать его, чтобы выровнять систему координат устройства с ранее существовавшей системой координат, чтобы в игре или приложении вы всегда появлялись в том же физическом месте. На изображении ниже показан цикл поиска известной устройству области.







Существует два способа создания ADF. Проще всего использовать приложение TangoExplorer, а затем уже ваше приложение загружает полученный ADF.







Также мы можем использовать Tango API, для изучения пространства, сохранения и загрузки ADF в пределах нашего приложения.

Следует отметить ряд особенностей, о которых следует помнить при использовании area learning:




  • Tango зависит от визуального разнообразия области, которую мы собираемся локализовать. Если мы находимся в здании с множеством одинаковых комнат или в совершенно пустой комнате с пустыми стенами, то устройству будет трудно локализовать себя.

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

  • Поскольку среда измениться, мы можем создать несколько ADF для одного физического местоположения в различных условиях. Это дает нашим пользователям возможность выбрать файл, который наиболее точно соответствует их текущим условиям. Можно также добавить несколько сессий в один ADF, чтобы захватить визуальные описания окружающей среды из каждого положения, угла и при каждом изменении освещения или изменения окружающей среды.





Depth Perception



С помощью данной технологии устройство может понять форму нашего окружения. Это позволяет создавать «дополненную реальность», где виртуальные объекты не только является частью нашей реальной среды, они могут также взаимодействовать с этой средой. Например, многие уже пророчат компьютерные игры в реальном пространстве.







Tango API обеспечивает получение данных о расстоянии до объектов, которые снимает камера, в виде облака точек. Этот формат дает (х, у, z) координаты максимальному количеству точек в сцене, которое можно вычислить.







Каждое измерение представляет собой значение с плавающей точкой записывающее позицию каждой точки в метрах в системе отсчета камеры сканирующей глубину сцены. Облако точек ассоциировано с цветовой информацией, получаемой с RGB камеры. Эта ассоциация дает нам возможность использовать Tango в качестве 3D сканера и открывает много интересных возможностей для разработчиков игр и творческих приложений. Степень точности сканирования конечно нельзя сравнивать со специальными 3D сканерами, дающими точность в доли миллиметра. По опыту коллег максимальная точность Intel RealSense R200 (используется в Tango) составляет 2 миллиметра, но может быть значительно хуже, т.к. сильно зависит от расстояния до предмета, от условий съёмки, от отражающих свойств поверхности, от движения. Так же нужно понимать, что увеличение точности изображения глубины приводит к увеличению шумов в сигнале, это неизбежно.



Однако следует отметить, что для построения коллизий в игре, расстановки виртуальной мебели в реальной квартире, измерения расстояний текущей точности получаемого 3D облака точек вполне достаточно.







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







Описание каждой из них — это тема для отдельной большой статьи. Давайте пройдемся по ним вкратце, чтобы было понимание их роли в Tango и общих принципов их функционирования.



Structured Light



Структурированный свет осуществляет проецирование известного паттерна (обычно решетки или горизонтальных полос) на реальные объекты. Таким образом, что деформация этого известного паттерна при наложении на поверхность позволяет системе компьютерного зрения получить информацию о глубине и поверхности объектов в сцене относительно устройства. Широко известный пример использования структурированного света — это 3D-сканеры.







Невидимый структурированный свет используется, когда видимый паттерн создает помеху для остальных методов компьютерного зрения используемых в системе. Это как раз случай Tango, в котором используется инфракрасный излучатель.



Time of Flight



Данная технология базируется на определении времени, которое требуется инфракрасному излучению, чтобы долететь от источника излучения до объекта и вернуться в камеру. Чем дальше дистанция до поверхности, тем большее время требуется.







Stereo



Мы не нашли точной информации как именно устроена данная технология в Tango. Видимо реализация достаточно стандартная и разработчики не стали заострять внимание на этом. В один момент времени производиться 2 снимка, так называемая стереопара. На их основе при помощи фотограмметрии определяется расстояние до поверхностей. Таким образом, например, производится спутниковая съемка высоты рельефа Земли.







Важно иметь в виду, что Structured Light и Time of Flight требуют наличия в устройстве одновременно инфракрасного (ИК) излучателя и ИК-камеры. Stereo технологии достаточно 2х камер и не требуется ИК-излучатель.



Отметим, что текущие устройства фактически предназначены для работы в закрытом помещении при умеренных расстояниях до объекта сканирования (от 0,5 до 4 метров). Такая конфигурация дает достаточно большую глубину на среднем расстоянии, при этом не предъявляет излишних требований к питанию для ИК-излучателя и производительности самого устройства при обработке глубины. По этой причине не рассчитывайте на сканирование очень близких объектов и распознавание жестов.



Следует помнить, что для устройства, которое воспринимает глубину с помощью ИК-камеры есть некоторые ситуации, когда точная работа затруднена.




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

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

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





Наш проект с Tango



Принципы работы устройства мы рассмотрели достаточно подробно. Теперь давайте перейдем от теории к практике. Попробуем создать наш первый проект.



Танго предоставляет SDK для игрового движка Unity3D и поддерживает OpenGL и других 3D-системах через C или Java. Мы используем в своей работе в основном Unity3D и все примеры далее будет создаваться именно в его среде разработки.



Интерграция с unity3D



Для разработки приложений в Unity3D для Project Tango нам потребуются:


  • Установленный Android SDK 17+


  • Unity (5.2.1 или выше), со средой настроенной для Android разработки.


  • Tango Unity SDK.


  • Для ОС Windows следует установить Google USB Driver если устройство не было распознано автоматически.




Описание проекта



Мы решили реализовать расстановку миниатюр военной техники с использованием Tango. Была запланирована следующая не замысловатая последовательность действий:


  • Пользователь берет в руки планшет Tango;


  • На экране в тех местах, где возможно расставить технику — рисуется зеленый маркер, там, где невозможно — красный:


  • Предлагается следующий выбор военной техники:


  • Расставленную технику можно перемещать в плоскости экрана с помощью touch интерфейса, и она будет перемещаться по физической поверхности.


  • Можно удалить расставленную технику.


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




Интерфейс для демо решили сделать минималистичным, но понятным для пользователя.







Реализация



За основу мы взяли ресурсы из TangoSDK/Examples/AugmentedReality с github и наши модели из уже реализованного проекта.







Описание реализации



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



— Вывод изображения из камеры устройства на экран, в этом нам поможет Tango AR Camera







— Далее необходимо обеспечить повторение реального перемещения устройства в виртуальной сцене. За это отвечает технология Motion Tracking и в сцене юнити необходимо настроить Tango Application, включить галочку Enable Motion Tracking.







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







— Общее представление иерархии сцены должно выглядеть следующим образом:








  1. Нам потребуется интерфейс, поэтому разместим и настроим структуру интерфейса — объект UI, и для корректной работы интерфейса добавим EventSystem.


  2. Также нам понадобятся данные, которые мы будем расставлять, то есть непосредственно модели техники. Шаблоны мы разместили в сцене, в объекте Prefabs.


  3. Чтобы связать кнопки интерфейса с моделями, на каждую кнопку мы добавили скрипт



    ViecleSpawnScript 


    и сделали ссылку в скрипте на модель. Теперь наши кнопки «знают» какая модель с ними связана.



    — Теперь нам необходимо реализовать логику, которая позволит расставлять объекты, их перемещать, удалять. Логику мы реализовали в классе



    VehicleSpawnRoot


    Ниже вы можете увидеть его настройки:







    Instances Limit — максимальное количество расставленных моделей;

    Model Scale — масштаб моделей при расстановке;

    Point Cloud — ссылка на объект Tango Point Cloud;

    Placing Mark — ссылка на префаб маркера;

    Trace Layer Mask — настройка слоев, по которым возможна расстановка;

    Selection Info — ссылка на элемент интерфейса для отображения названия техники, которую выделили.



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



    — Нам необходимо иметь данные — точку пересечения луча и результат пересечения. Получение координаты и результата трейса мы сделали через асинхронное получение, запущена Coroutine с функцией



    LazyUpdate


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




Результат трейса может иметь следующие значения:



enum ETraceResults
{

Invalid, // результаты трейса недоступны

NotFound, // результаты трейса доступны, но пересечения с плоскостью не найдено

NotGround, // плоскость не выровнена по горизонтали.
ExistingObjectCrossing, // обнаружено пересечение с расставленным объектом дополненной реальности

Valid, // подходящее положение для расстановки
}



Чтобы модель по нажатию на кнопку ставилась на поверхность в классе



VehicleSpawnRoot


создадим функцию



public void Spawn( GameObject prefab )


В функцию передается префаб с моделью для расстановки.

В функции проверяется возможность расстановки и в случае успеха — создается модель в заданных координатах.



В целом все готово, можно запускать!



Более подробно можно ознакомиться с кодом проекта на gitHub.



Публикация приложения в Google play



Публикация приложений, использующих Tango framework требует совершения одного дополнительного действия в дополнение к стандартному процессу публикации Android приложений. Для того чтобы приложение могло быть установлено только на Tango совместимых устройствах необходимо добавить следующие строки в файл манифеста приложения.




...






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



Резюме



Вот мы и прошли вместе с вами путь от теоретических основ до публикации приложения.

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



К тому же начали выходить коммерческие устройства у крупных вендоров.

Единственная ложка дегтя в большой бочке мёда — это проблемы при работе устройства на ярком освещении. Очень хочется верить, что эту задачу Google и Intel будут решать в следующие годы или возможно уже решают.

Спасибо за внимание и надеемся, что статья оказалась для вас интересной.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/309876/

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

[Из песочницы] Создание меню для игры на Unity3D на основе State-ов

Суббота, 17 Сентября 2016 г. 12:58 (ссылка)

Всем доброго времени суток! Хотелось бы рассказать о том, как я реализовывал систему игрового UI в небольшом игровом проекте. Данный подход показался мне самым оптимальным и удобным во всех требуемых аспектах.



Вся система является довольно тривиальным представлением недетерминированного конечного автомата.

Для реализации нам понадобится: набор состояний, набор представлений состояний, стейт-свитчер, переключающая эти состояния.



Реализация сервиса управления меню



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



Опишем интерфейс состояния:



IState
public interface IState
{
void OnEnter(params object[] parameters);
void OnEnter();
void OnExit();
}




Метод OnEnter будет вызываться в момент перехода в состояние, а его перегрузка создана для того, чтобы передавать в состояние набор параметров. Тем самым можно передавать объекты, которые будут использованы внутри состояния — аргументы событий или, например, делегаты, которые будут вызываться из состояния при том или ином событии. В свою очередь, OnExit будет вызван при выходе из состояния.



Представление состояния:



У каждого состояния должно быть представление. Задача представления — выводить информацию в UI элементы и уведомлять состояние о пользовательских действиях, касающихся UI (Если такие предусмотрены конкретной страницей интерфейса).



IUIShowableHidable
public interface IUIShowableHidable
{
void ShowUI();
void HideUI();
}




ShowUI — метод, инкапсулирующий в себе методы, реализующие отображение (активацию) UI-элементов, относящихся к текущей странице меню.



HideUI — метод, позволяющий скрыть все элементы, к примеру, перед переходом на другую страницу.



Реализация состояний и их представлений:



Подразумевается, что IState и IUIShowableHidable работают в связке — в момент вызова OnEnter в стейте уже находится заинжекченый туда IUIShowableHidable. При переходе в состояние вызывается ShowUI, при выходе — HideUI. В большинстве случаев именно так и будет работать переход между состояниями. Исключения, вроде длительных анимаций переходов, при которых требуется задержка между HideUI предыдущей страницы и ShowUI новой страницы можно решить различными способами.



Учитывая факт, описанный выше, мной было решено для удобства и скорости создания новых стейтов сделать абстрактный класс, который будет иметь поле с «вьюшкой» и инкапсулировать показ и сокрытие UI в методы переходов.



UIState
public abstract class UIState : IState
{
protected abstract IUIShowableHidable ShowableHidable { get; set; }
protected abstract void Enter(params object[] parameters);
protected abstract void Enter();
protected abstract void Exit();

public virtual void OnEnter()
{
ShowableHidable.ShowUI();
Enter();
}

public virtual void OnExit()
{
ShowableHidable.HideUI();
Exit();
}

public virtual void OnEnter(params object[] parameters)
{
ShowableHidable.ShowUI();
Enter(parameters);
}
}




Так же имеются абстрактные методы Enter и Exit, которые будут вызваны после вызова соответсвующих методов IUIShowableHidable. Фактической пользы от них нет, так как можно было при надобности обойтись простым override-ом OnEnter и OnExit, однако мне показалось удобным держать в стейте пустые методы, которые в случае надобности будут заполнены.



Для большей простоты был реализован класс UIShowableHidable, который реализует IUIShowableHidable и избавляет нас от надобности, каждый раз реализовывать ShowUI и HideUI. Так же, в Awake элемент будет деактивирован, это сделано из соображений, что изначально, все элементы UI включены, с целью получения их инстансов.



UIShowableHidable
public class UIShowableHidable :  CachableMonoBehaviour, IUIShowableHidable
{
protected virtual void Awake()
{
gameObject.SetActive(false);
}

public virtual void ShowUI()
{
gameObject.SetActive(true);
}

public virtual void HideUI()
{
gameObject.SetActive(false);
}

protected bool TrySendAction(Action action)
{
if (action == null) return false;
action();
return true;
}
}




Приступим к проектированию «сердца» игрового меню:



Нам нужны три основных метода:




  • GoToScreenOfType — метод, который будет позволять переходить в состояние, передаваемое параметром. Имеет перегрузку, которая будет передавать набор object-ов в целевое состояние.


  • GoToPreviousScreen — будет возвращать нас на предыдущий «скрин».

  • ClearUndoStack — даст возможность очистить историю переходов между «скринами».




IMenuService
public interface IMenuService
{
void GoToScreenOfType() where T : UIServiceState;
void GoToScreenOfType(params object[] parameters) where T : UIServiceState;
void GoToPreviousScreen();
void ClearUndoStack();
}




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



StateSwitcher
public class StateSwitcher
{
private IState currentState;
private readonly List registeredStates;
private readonly Stack switchingHistory;
private StateSwitchCommand previousStateSwitchCommand;

public StateSwitcher()
{
registeredStates = new List();
switchingHistory = new Stack();
}

public void ClearUndoStack()
{
switchingHistory.Clear();
}

public void AddState(IState state)
{
if (registeredStates.Contains(state)) return;
registeredStates.Add(state);
}

public void GoToState()
{
GoToState(typeof(T));
}

public void GoToState(params object[] parameters)
{
GoToState(typeof(T), parameters);
}

public void GoToState(Type type)
{
Type targetType = type;
if (currentState != null)
if (currentState.GetType() == targetType) return;
foreach (var item in registeredStates)
{
if (item.GetType() != targetType) continue;
if (currentState != null)
currentState.OnExit();
currentState = item;
currentState.OnEnter();
RegStateSwitching(targetType, null);
}
}

public void GoToState(Type type, params object[] parameters)
{
Type targetType = type;
if (currentState != null)
if (currentState.GetType() == targetType) return;
foreach (var item in registeredStates)
{
if (item.GetType() != targetType) continue;
if (currentState != null)
currentState.OnExit();
currentState = item;
currentState.OnEnter(parameters);
RegStateSwitching(targetType, parameters);
}
}

public void GoToPreviousState()
{
if (switchingHistory.Count < 1) return;
StateSwitchCommand destination = switchingHistory.Pop();
previousStateSwitchCommand = null;
if (destination.parameters == null)
{
GoToState(destination.stateType);
}
else
{
GoToState(destination.stateType, destination.parameters);
}
}

private void RegStateSwitching(Type type, params object[] parameters)
{
if (previousStateSwitchCommand != null)
switchingHistory.Push(previousStateSwitchCommand);
previousStateSwitchCommand = new StateSwitchCommand(type, parameters);
}

private class StateSwitchCommand
{
public StateSwitchCommand(Type type, params object[] parameters)
{
stateType = type;
this.parameters = parameters;
}
public readonly Type stateType;
public readonly object[] parameters;
}
}




Тут все просто: AddState добавляет стейт в список стейтов, GoToState проверяет наличие требуемого стейта в списке, если находит его, то выполняет выход из текущего состояния и вход в требуемое, а так же регистрирует смену состояний, представляя переход классом StateSwitchCommand, добавляя ее в стек переходов, что позволит нам возвращаться на предыдущий экран.



Осталось добавить реализацию IMenuService



MenuManager
public class MenuManager : IMenuService
{
private readonly StateSwitcher stateSwitcher;

public MenuManager()
{
stateSwitcher = new StateSwitcher();
}

public MenuManager(params UIState[] states) : this()
{
foreach (var item in states)
{
stateSwitcher.AddState(item);
}
}

public void GoToScreenOfType() where T : UIState
{
stateSwitcher.GoToState();
}

public void GoToScreenOfType(Type type)
{
stateSwitcher.GoToState(type);
}

public void GoToScreenOfType(params object[] parameters) where T : UIState
{
stateSwitcher.GoToState(parameters);
}

public void GoToScreenOfType(Type type, params object[] parameters)
{
stateSwitcher.GoToState(type, parameters);
}

public void GoToPreviousScreen()
{
stateSwitcher.GoToPreviousState();
}

public void ClearUndoStack()
{
stateSwitcher.ClearUndoStack();
}
}
Конструктор принимает набор IState'ов, которые будут использованы в вашей игре.




Использование



Простой пример использования:



Пример состояния
public sealed class GameEndState : UIServiceState
{
protected override IUIShowableHidable ShowableHidable { get; set; }
private readonly GameEndUI gameEndUI;
private Action onRestartButtonClicked;
private Action onMainMenuButtonClicked;

public GameEndState(IUIShowableHidable uiShowableHidable, GameEndUI gameEndUI)
{
ShowableHidable = uiShowableHidable;
this.gameEndUI = gameEndUI;
}

protected override void Enter(params object[] parameters)
{
onRestartButtonClicked = (Action) parameters[0];
onMainMenuButtonClicked = (Action)parameters[1];
gameEndUI.onRestartButtonClicked += onRestartButtonClicked;
gameEndUI.onMainMenuButtonClicked += onMainMenuButtonClicked;
gameEndUI.SetGameEndResult((string)parameters[2]);
gameEndUI.SetTimeText((string)parameters[3]);
gameEndUI.SetScoreText((string)parameters[4]);
}

protected override void Enter()
{
}

protected override void Exit()
{
gameEndUI.onRestartButtonClicked -= onRestartButtonClicked;
gameEndUI.onMainMenuButtonClicked -= onMainMenuButtonClicked;
}
}


Конструктор требует входных IUIShowableHidable и, собственно, самого GameEndUI — представления состояния.



Пример представления состояния
public class GameEndUI : UIShowableHidable
{
public static GameEndUI Instance { get; private set; }

[SerializeField]
private Text gameEndResultText;
[SerializeField]
private Text timeText;
[SerializeField]
private Text scoreText;
[SerializeField]
private Button restartButton;
[SerializeField]
private Button mainMenuButton;

public Action onMainMenuButtonClicked;
public Action onRestartButtonClicked;

protected override void Awake()
{
base.Awake();
Instance = this;
restartButton.onClick.AddListener(() => { if(onRestartButtonClicked != null) onRestartButtonClicked(); });
mainMenuButton.onClick.AddListener(() => { if (onMainMenuButtonClicked != null) onMainMenuButtonClicked(); });
}

public void SetTimeText(string value)
{
timeText.text = value;
}

public void SetGameEndResult(string value)
{
gameEndResultText.text = value;
}

public void SetScoreText(string value)
{
scoreText.text = value;
}
}




Инициализация и переходы
 private IMenuService menuService;

private void InitMenuService()
{
menuService = new MenuManager
(
new MainMenuState(MainMenuUI.Instance, MainMenuUI.Instance, playmodeService, scoreSystem),
new SettingsState(SettingsUI.Instance, SettingsUI.Instance, gamePrefabs),
new AboutAuthorsState(AboutAuthorsUI.Instance, AboutAuthorsUI.Instance),
new GameEndState(GameEndUI.Instance, GameEndUI.Instance),
playmodeState
);
}

...

private void OnGameEnded(GameEndEventArgs gameEndEventArgs)
{
Timer.StopTimer();
scoreSystem.ReportScore(score);
PauseGame(!IsGamePaused());
Master.GetMenuService().GoToScreenOfType(
new Action(() => { ReloadGame(); PauseGame(false); }),
new Action(() => { UnloadGame(); PauseGame(false); }),
gameEndEventArgs.gameEndStatus.ToString(),
Timer.GetTimeFormatted(),
score.ToString());
}





Заключение



В итоге получается довольно практичная, на мой взгляд, и легко расширяемая система управления пользовательским интерфейсом.



Спасибо за внимание. Буду рад замечаниям и предложениям, способным улучшить описаный в статье способ.
Original source: habrahabr.ru.

https://habrahabr.ru/post/310236/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

[Из песочницы] Автоматизация поддержания соответствия между названиями слоев в редакторе и коде с помощью CodeDom

Суббота, 03 Сентября 2016 г. 11:46 (ссылка)

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



На первый взгляд, для решения этой проблемы логично использовать шаблоны T4, но они мне показались неудобными (по крайней мере, при использовании именно в Unity-проекте), поэтому я выбрал другой подход. Использование CodeDom для решения такой мелкой проблемы может показаться оверинжинирингом, но личный опыт доказал состоятельность этого подхода: я написал свой незамысловатый генератор кода больше года назад, и с тех пор, не внося в него изменения, успешно пользовался им, что сэкономило мне немало нервов и порядочно секунд времени.



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



Общий план действий таков:




  • Получить список названий слоев коллизий, имеющихся в проекте.

  • Сгенерировать сам код, содержащий класс с нужными константами.

  • Записать этот код в файл.

  • Побудить Unity немедленно скомпилировать добавленный/измененный файл.



Получаем список названий слоев коллизий



Тут все просто, если не бояться лезть в места, названные внутренними. Конкретнее, список имен слоев коллизий хранится как поле «внутреннего» класса.



        private static IEnumerable GetAllLayers()
{
return InternalEditorUtility.layers;
}


Генерируем сам код



У CodeGen слегка своя терминология (сравните, например, с терминологией в Roslyn), но, в целом, все соответствует синтаксическому дереву, присущему коду на C#. В порядке от корня к листьям, мы будем использовать следующее:




  1. CodeCompilationUnit — это сам генератор кода, который мы здесь, так сказать, конфигурируем.

  2. CodeNamespace — это пространство имен, в котором будет сидеть наш класс. Мы не будем оборачивать класс в явное пространство имен, но создать экземпляр CodeNamespace, все равно, придется.

  3. CodeTypeDeclaration — это сам класс.

  4. CodeMemberField — это член класса (в данном случае, объявление константы).

  5. CodePrimitiveExpression — это выражение с литералом (в данном случае, строка, который будет присваиваться константе).



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



private static CodeMemberField GenerateConstant(string name)
{
name = name.Replace(" ", "");

var @const = new CodeMemberField(
typeof(string),
name);

@const.Attributes &= ~MemberAttributes.AccessMask;
@const.Attributes &= ~MemberAttributes.ScopeMask;
@const.Attributes |= MemberAttributes.Public;
@const.Attributes |= MemberAttributes.Const;

@const.InitExpression = new CodePrimitiveExpression(name);
return @const;
}


Есть у CodeGen одно мелкое неудобство: он не умеет создавать статические классы. Связано это с тем, что он создавался на заре языка C#, когда в него еще не «завезли» статические классы. Будем выкручиваться: сымитируем статический класс запечатанным классом с приватным конструктором. Так поступали некоторые ранние пользователи C#, а использующие язык Java вынуждены и сейчас прибегать к этому.



private static void ImitateStaticClass(CodeTypeDeclaration type)
{
@type.TypeAttributes |= TypeAttributes.Sealed;

@type.Members.Add(new CodeConstructor {
Attributes = MemberAttributes.Private | MemberAttributes.Final
});
}


Наконец-то, соберем сам класс, с приватным конструктором и константами:



private static CodeCompileUnit GenerateClassWithConstants(
string name,
IEnumerable constants)
{
var compileUnit = new CodeCompileUnit();
var @namespace = new CodeNamespace();

var @class = new CodeTypeDeclaration(name);

ImitateStaticClass(@class);

foreach (var constantName in constants)
{
var @const = GenerateConstant(constantName);
@class.Members.Add(@const);
}

@namespace.Types.Add(@class);
compileUnit.Namespaces.Add(@namespace);

return compileUnit;
}


Записываем код в файл




private static void WriteIntoFile(string fullPath, CodeCompileUnit code)
{
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
using (var stream = new StreamWriter(fullPath, append: false))
{
var writer = new IndentedTextWriter(stream);
using (var codeProvider = new CSharpCodeProvider())
{
codeProvider.GenerateCodeFromCompileUnit(code, writer, new CodeGeneratorOptions());
}
}
}


Заставляем Unity немедленно «осознать» изменения



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




[MenuItem("Habr/Generate layers constants")]
private static void GenerateAndForceImport()
{
const string path = @"Auto/Layers.cs";

var fullPath = Path.Combine(Application.dataPath, path);
var className = Path.GetFileNameWithoutExtension(fullPath);

var code = GenerateClassWithConstants(className, GetAllLayers());
WriteIntoFile(fullPath, code);

AssetDatabase.ImportAsset("Assets/" + path, ImportAssetOptions.ForceUpdate);
AssetDatabase.Refresh();
}


Результат



Собираем все воедино:



Итоговый код генератора

namespace Habr
{
using Microsoft.CSharp;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

internal static class HabrCodeGen
{
[MenuItem("Habr/Generate layers constants")]
private static void GenerateAndForceImport()
{
const string path = @"Auto/Layers.cs";

var fullPath = Path.Combine(Application.dataPath, path);
var className = Path.GetFileNameWithoutExtension(fullPath);

var code = GenerateClassWithConstants(className, GetAllLayers());
WriteIntoFile(fullPath, code);

AssetDatabase.ImportAsset("Assets/" + path, ImportAssetOptions.ForceUpdate);
AssetDatabase.Refresh();
}

private static CodeCompileUnit GenerateClassWithConstants(
string name,
IEnumerable constants)
{
var compileUnit = new CodeCompileUnit();
var @namespace = new CodeNamespace();

var @class = new CodeTypeDeclaration(name);

ImitateStaticClass(@class);

foreach (var constantName in constants)
{
var @const = GenerateConstant(constantName);
@class.Members.Add(@const);
}

@namespace.Types.Add(@class);
compileUnit.Namespaces.Add(@namespace);

return compileUnit;
}

private static CodeMemberField GenerateConstant(string name)
{
name = name.Replace(" ", "");

var @const = new CodeMemberField(
typeof(string),
name);

@const.Attributes &= ~MemberAttributes.AccessMask;
@const.Attributes &= ~MemberAttributes.ScopeMask;
@const.Attributes |= MemberAttributes.Public;
@const.Attributes |= MemberAttributes.Const;

@const.InitExpression = new CodePrimitiveExpression(name);
return @const;
}

private static IEnumerable GetAllLayers()
{
return InternalEditorUtility.layers;
}

private static void ImitateStaticClass(CodeTypeDeclaration type)
{
@type.TypeAttributes |= TypeAttributes.Sealed;

@type.Members.Add(new CodeConstructor {
Attributes = MemberAttributes.Private | MemberAttributes.Final
});
}

private static void WriteIntoFile(string fullPath, CodeCompileUnit code)
{
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
using (var stream = new StreamWriter(fullPath, append: false))
{
var tw = new IndentedTextWriter(stream);
using (var codeProvider = new CSharpCodeProvider())
{
codeProvider.GenerateCodeFromCompileUnit(code, tw, new CodeGeneratorOptions());
}
}
}
}
}


Кладем нашу утилиту в папку Editor, нажимаем Habr -> Generate layers constants, получаем в проекте файл со следующим содержанием:




// ------------------------------------------------------------------------------
//
// This code was generated by a tool.
// Mono Runtime Version: 2.0.50727.1433
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//

// ------------------------------------------------------------------------------



public sealed class Layers {

public const string Default = "Default";

public const string TransparentFX = "TransparentFX";

public const string IgnoreRaycast = "IgnoreRaycast";

public const string Water = "Water";

public const string UI = "UI";

public const string Habr = "Habr";

private Layers() {
}
}


Дальнейшие действия



Полученной утилите не хватает следующих вещей:




  • Чуть более удобного интерфейса с чуть более гибкими настройками.

  • Устойчивости к невалидным в C# названиям.

  • Генерации аналогичным образом констант для названий сортировочных слоев, сцен, тэгов и осей ввода.



Чтобы не тратить время на написание своего «велосипеда», вы также можете воспользоваться моим «велосипедом».
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/309128/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

[Из песочницы] Unity с позиции художника при разработке кроссплатформенной игры

Среда, 31 Августа 2016 г. 14:24 (ссылка)

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



«Работать в Unity так приятно и удобно: много возможностей для самореализации» — говорили мне, но всё оказалось не так просто. В данной статье не будет петься хвалебных од Unity. Эта статья о суровых реалиях, ограничениях и сложностях, с которыми столкнулась наша маленькая команда инди-разработчиков при создании своей первой, но достаточно крупной игры Death Point.







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



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



Освещение



Наши силы были крайне ограничены и я выбрал тайловую систему. Гибкая, позволяющая достичь хорошего визуального разнообразия, ограниченными средствами. Глядя на свой блеклый уровень, я со слезами на глазах штудировал документацию по Unity в поисках ответа «настройка и работа системы освещения». Документация оказалась достаточно поверхностной и не давала полного понимания о работе системы. Спасла меня документация Unreal Engine, раскрывающая многие аспекты того, как это работает в игровом движке и дающая множество ответов на вопросы “как нужно делать”. Еще пара тройка хороших статей и я был готов творить.



Первым ударом стала невероятная разница между тем, что Unity отображает в редакторе при real-time render и bake. Красивые тени после запекания превращались в кашу, источники света забивали друг друга, их яркость и цвет менялись кардинально.



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



Затем, отсутствие HDR в вариации для мобильных устройств. Везде есть галочки позволяющие его включить, но в результате получалось только жуткое месиво цветов от источника света на объектах, Unity просто не понимает как сохранять light-map в HDR. Как следствие, текстура осветляется до уровня albedo, и на краях освещенной области появляется “радужный” градиент.





Самый ранний тест запеченного света



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





После множества манипуляций — красивый градиент с рисунком



В этой же картинке можно увидеть, что перед лампой стоит 3d объект с прозрачной текстурой, выполняющий роль маски. Все потому, что разработчики не добавили возможность выставлять для baked источника света текстурную маску cookies (форму светового пятна).



Отдельным пунктом можно упомянуть directional-lightmap – прекрасное техническое решение, но из-за мобильной направленности мы не стали его использовать. Сами lightmap не мало весят, а directional-lightmap удваивают это значение. Мы не могли жертвовать таким количеством ресурсов, так как игра должна была хорошо работать и на мобильных устройствах. Причина в том, что весь эффект directional-lightmap виден только в случае, когда источник света попадает в камеру. А таких моментов в top-down игре очень мало.



Размер имеет значение



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



Когда пришло время понять, на что же мы способны, был собран довольно внушительный уровень. И тут снова удар ниже пояса от разработчиков Unity – в консоль падают ошибки о том, что невозможно запечь lightmap, и уровень покрывается черными полосами. Слишком большой уровень и слишком высокие настройки освещения. Жаль.



Вот он, тот уровень, с хорошим освещением, настроенными reflection пробами, light-probe для освещения динамических объектов и с частично реализованным геймплем. Выглядит хорошо, играется отлично. Взгляд падает на количество drawcalls – провал, полный провал… их около двух тысяч.







Откуда такие цифры? На сцене всего сотня уникальных объектов, а клоны должны батчиться внутренней системой. Неделя поисков не увенчались успехом, несколько догадок и предположений, тонна тестов и ковыряние в настройках, – все тщетно. В какой-то момент, Unity напрочь отказывалась объединять одинаковые объекты при рендере, отрисовывая их по одному.



Наш гейм-дизайнер оказался толковым парнем и хорошим программистом. Всего за несколько недель он написали свою систему оптимизации, которая группировала и объединяла меши, создавала атласы из всех объектов используемых в игре и еще кучу разных вещей которых я не понимаю =). В итоге мы добились приемлемого для мобильного приложения количества drawcalls. При максимальной детализации сцены их было не более 250. Все еще много, но хороший телефон справляется на ура, выдавая те самые, заветные, 60 FPS.







После ввода системы оптимизации количество drawcalls стабильно держится в районе 100.



Собственный шейдер



И все же, с картинкой было что-то не так. Результат был далек от ожидаемого и, тем более, желаемого. Я не сразу увидел, что модели выглядят слишком разными в 3D-coat, где я делаю текстуры, и Unity полагая, что все дело в окружении этого объекта.

Несмотря на огромную работу проделанную разработчиками Unity по оптимизации стандартного шейдера, тесты на устройствах показали, что всё не очень хорошо. Да, быстро, но не так как хотелось бы. Да PRB (Physically based rendering), но выглядит странно. Неправильная работа reflection sphere на мобильных устройствах при использовании graphics API 2.0 стала одной из главных причин написания своего шейдера. Переключаться на 3.0 мы не хотели из-за провала в производительности.



Я не хотел использовать сферические отражения; выглядит странно и некрасиво, персонаж двигается и отражения двигаются вместе c ним. Когда в игре камера смотрит в землю, вариантов с украшением картинки мало, и отражения на поверхностях предметов играют действительно важную роль.



Вообще PRB в Unity это история PC игр, на котором он чувствует себя хорошо при realtime освещении. В случае с baked освещением и отсутствием направленных лайтмап, PRB становится грудой мусора, которая не понимает, как себя вести. Да, кто то может сказать “Почему нельзя было просто использовать мобильный шейдер в котором зашита cubemap?”. Ответ прост: у нас очень много отражающих поверхностей в закрытых помещениях. Отражения не просто красиво блестят, они подчеркивают форму и дают лучшее понимание и представление о свойствах материала из которого создан предмет, они задают правильную атмосферу того помещения, где находится персонаж. Как бонус, вы можете увидеть то, что не должны были бы видеть: фасады зданий (которые мы не отображаем в основной камере, что бы они не мешали геймплею), потолки помещений, да и просто различную фурнитуру и вывески, которые находятся в недоступном с этого ракурса камеры местах. Всё светится, блестит и отражается, создавая цельную атмосферу и оживляя этот цифровой мир.





Без отражений





С отражениями



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



При написании собственного шейдера мы, изначально, просто скопировали решение юнити – разбили шейдер на 2 части: «металик» и «спекулар». Я далеко не сразу понял, что это именно та часть, где все очень плохо. Это решение не позволяет создавать сложные объекты сочетающие в себе металлические и глянцевые поверхности. Что-то одно на выбор.



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



В Unity есть потрясающая возможность делать композитные шейдеры, которые в зависимости от входных параметров, выбрасывают неиспользуемые блоки. Т.е. если в шейдере есть 7 входных параметров, но используется только 3, то при компиляции он создаст шейдр-версию в которой будет только 3 параметра.



Было создано 2 версии шейдера, один debug, который оперировал всеми возможными текстурными картами по отдельности, и final, оперирующий четырьмя собранными текстурными атласами.





В итоге у нас получилось приличное количество входных данных на debug версии шейдера:



1. Выбор между прозрачность / непрозрачность

2. отключение Normal Map (ее отсутствие на одном объекте дает небольшой прирост производительности, но отключение в больших количествах дает заметный результат). В нашем случае это имеет смысл, так как камера расположена достаточно далеко и не всегда разумно ее использовать, часто добавление АО (ambient occlusion) в albedo показывало заметно лучший результат.

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

3. Albedo

4. Specular

5. Roughness

6. AO

7. Emission

8. Metal

9. Normal map



В процессе компиляции все это многообразие умещается в 4 текстуры.







Что облегчает мне жизнь как художнику – у меня есть гибкий инструмент, позволяющий получить мне любой желаемый результат и в то же время достаточно легкий в отношении производительности, особенно, если я использую не все возможности. Благодаря всему вышеперечисленному я мог позволить себе не задумываться о размерах текстур и, при необходимости, домножать значения цвета на текстуры, что ускоряло мою работу, ведь мне не надо было лезть в графический редактор и сохранять все заново.



Еще одним плюсом была система вычисления размера 3D модели в мире, что позволяло мне не задумываться о размерах текстур. Не надо было мучиться и выбирать между некачественной текстурой в 64px или излишне детализированной в 128px. Система просто ужимает текстуру до действительно необходимого размера и упаковывает ее в атлас. В итоге, для игрока, все имеет одинаково качественную детализацию.



Зоны vision персонажа. Туман войны



Расскажу об одном из самых из самых гениальных и простых решений которые мы использовали в нашем проекте.

Туман войны реализован через point-light, находящийся в персонаже и рендерящийся отдельной камерой при максимально простом шейдере, который учитывает только освещение. Результатом является буфер видимости персонажа использующийся для пост-процесса тумана войны и в качествемаски в других шейдерах. Например, в зоне видимости охранников.







Заключение



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



Есть мысль выгружать готовый уровень обратно в 3D редактор и уже там запекать свет. Это даст огромный прирост как в скорости подготовки сцены, так и в качественном результате. Поверьте, это будет удивительно. Мягкие и жесткие тени, подсветка, качественные градиенты. Думаете только FrostBite может показать все возможности красивого рендера на грани вашего железа? Наша команда можем Вам показать, как должны выглядеть действительно красивые игры на всех устройствах, при все тех же, смешных технических требованиях к железу. Но, сколько бы ни было идей, на их реализацию нужно время. Выход в STEAM может дать нам возможность реализовать все наши амбиции, но для этого нам нужна Ваша помощь!







подробнее с проектом можно ознакомиться тут:

steamcommunity.com/sharedfiles/filedetails/?id=752096160



Статья за авторством Кротова Сергея, при поддержке Челушкина Андрея и Ковалевского Виталия.
Original source: habrahabr.ru.

https://habrahabr.ru/post/308918/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

VA - Beatport Top 100 Techno [July] (2016)

Суббота, 13 Августа 2016 г. 08:41 (ссылка)
djmp-3.com/electronic/3571-...-2016.html

VA - Beatport Top 100 Techno [July] (2016)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

VA - Starseed (2016/FLAC)

Вторник, 09 Августа 2016 г. 21:49 (ссылка)
djmp-3.com/dts/3516-va-star...-flac.html

VA - Starseed (2016/FLAC)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Делаем работу нашего гейм-дизайнера в Unity удобнее и приятнее

Пятница, 05 Августа 2016 г. 15:03 (ссылка)

Добрый день, Хабр. В эфире снова я, Илья Кудинов. В свободное от основной работы время я занимаюсь разработкой игрушек на Unity 3D и решил в качестве эксперимента написать статью об одной из проблем, с которой столкнулась наша команда. Я являюсь основным разработчиком, и наш гейм-дизайнер в «гробу видал» копание в моем коде с какой бы то ни было целью (разделение труда — одно из величайших достижений цивилизации), значит, моя обязанность — предоставить ему все необходимые рычаги управления и настройки геймплея в виде удобных визуальных интерфейсов. Благо Unity сам по себе имеет достаточно удобные (кхе-кхе) готовые интерфейсы и ряд методов их расширения. И сегодня я расскажу вам о некоторых приемах, которые делают жизнь нашего гейм-дизайнера проще и удобнее, а мне позволяют не биться головой о клавиатуру после каждого его запроса. Надеюсь, они смогут помочь каким-нибудь начинающим командам или тем, кто просто упустил эти моменты при изучении Unity.



Сразу скажу, что наша команда все еще активно учится и развивается, хоть мы уже и выпустили дебютную игру. И если «дедлайны не горят», то я предпочитаю разбираться в каких-то вещах сам, а не обращаться к экспертам и различным best practices. Поэтому что-то из рассказанного мною может оказаться не оптимальным или банальным. Буду очень рад, если в таких случаях вы подскажете мне более удобные решения в комментариях и личных сообщениях. Ну и в целом информация здесь скорее базового уровня.



Код для Unity я пишу исключительно на C#, поэтому все выкладки в статье будут именно на этом языке.



Singleton-объекты



В архитектуре любой игры зачастую предусмотрены различные классы менеджеров, фабрик и хелперов, которым не нужны физические представления в игровом мире. В идеальном случае можно было бы реализовать их классами со статическими методами, не создавать в сцене никаких GameObject для их работы и спокойно пользоваться кодом вида GameController.MakeEverybodyHappy(). Однако у этого подхода есть два существенных минуса в нашем случае:


  • для изменения каких-либо параметров гейм-дизайнерам придется лазить напрямую в код, а они это очень не любят;

  • будет сложнее использовать ссылки на любые ассеты в Unity (префабы, текстуры и т.д.), так как придется загружать их через Resources.load(), а такой код поддерживать существенно труднее, чем те ссылки, которые можно создавать через интерфейс Unity.



Решение проблемы? Наследовать ваши классы от MonoBehaviour и создавать для каждого из них объект в сцене. Минусы этого подхода? При обращении к этим объектам придется пользоваться извращенными вызовами типа FindObjectOfType() или даже GameObject.Find(”GameController”).GetComponent(). Ни один уважающий себя разработчик делать так на каждом шагу не захочет. Дополнительные проблемы и возможные ошибки начинают возникать при необходимости переносить такие объекты между сценами или при возникновении нескольких объектов с таким классом (либо их полном отсутствии).

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

Наше решение выглядит следующим образом («костяк» класса я когда-то давно нашел на просторах интернета и слегка доработал для собственного удобства):



using UnityEngine;
public class Singleton : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;

public void Awake()
{
// Если в сцене уже есть объект с таким компонентом, то
// он пропишет себя в _instance при инициализации
if (!_instance) {
_instance = gameObject.GetComponent();
} else {
Debug.LogError("[Singleton] Second instance of '" + typeof (T) + "' created!");
}
}

public static T Instance
{
get
{
if (_instance == null) {
_instance = (T) FindObjectOfType(typeof(T));

if (FindObjectsOfType(typeof(T)).Length > 1) {
Debug.LogError("[Singleton] multiple instances of '" + typeof (T) + "' found!");
}

if (_instance == null) {
// Если в сцене объектов с этим классом нет - создаём
// новый GameObject и лепим ему наш компонент
GameObject singleton = new GameObject();
_instance = singleton.AddComponent();
singleton.name = "(singleton) " + typeof(T).ToString();
DontDestroyOnLoad(singleton);
Debug.Log("[Singleton] An instance of '" + typeof(T) + "' was created: " + singleton);
} else {
Debug.Log("[Singleton] Using instance of '" + typeof(T) + "': " + _instance.gameObject.name);
}
}
return _instance;
}
}
}




Как этим пользоваться? Наследуем свои классы от Singleton, указав самого себя в шаблоне:

 public class GameController : Singleton
В дальнейшем мы сможем обращаться к полям и методам нашего класса как GameController.Instance.MakeEverybodyHappy() в любом месте кода. Чтобы создавать ссылки на ассеты, достаточно добавить этот компонент на любой объект в сцене и настраивать его привычным образом (и сохранять в префаб для верности). Мы используем объект Library в корне сцены для хранения всех Singleton-классов, настройка которых может понадобиться нашему гейм-дизайнеру, чтоб ему не приходилось их искать по всей сцене.



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

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




Кастомные поля инспектора класса



Итак, гейм-дизайнер получил свои визуальные интерфейсы и начал творить геймплей. И достаточно быстро начал ненавидеть меня, невинного разработчика, за все неудобства, которые на него наваливаются. Массив сериализованных объектов? Многомерные массивы? Почему это все настолько неудобно настраивать? Как бы вы ни старались сделать универсальную и расширяемую систему на стороне кода, ваш гейм-дизайнер предпочел бы видеть минимальное количество выпадающих списков и уж тем более массивов элементов с названиями вроде Element 73. Но разве мы можем что-то с этим поделать?

На самом деле можем. Предположим, в нашей игре появляются настройки сложности, и в данный момент вариантов три. Но, возможно, станет больше. Поэтому мы смотрим в будущее и для упрощения дальнейшего ВОЗМОЖНОГО увеличения количества сложностей создаем вот такой замечательный класс «зависящих-от-сложности-интов» и заменяем в нужных местах обычные инты на него:



[System.Serializable]
public class DifDepInt
{
public int[] values = {0, 0, 0};

static public implicit operator int (DifDepInt val)
{
return val.Get();
}

public int Get()
{
return values[GameConfig.Instance.difficulty];
}
}




DifDep означает Difficulty Dependant. Конечно, с точки зрения архитектуры лучше было бы сделать вместо этого специфического класса шаблон DifDep, принимающий любые типы данных, но, к сожалению, я не нашел способа создания кастомных полей редактора для шаблонов.



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





Да, однозначно, это не очень интуитивно и удобно. Давайте сделаем так, чтобы все выглядело иначе! Для этого мы воспользуемся вышеназванной «плюшкой» Unity — возможностью создавать кастомных инспекторов для отображения различных классов. Это достаточно хитрая система, позволяющая вам делать практически все что угодно, но в ней не так просто разобраться с первого взгляда (с самого начала она меня отпугнула, и поэтому какое-то время мы-таки страдали со стандартным инспектором, но в конце концов момент истины настал).

Итак, мы пишем следующий код:



#if UNITY_EDITOR
using UnityEditor;

[CustomPropertyDrawer(typeof(DifDepInt))]
public class DifDepIntDrawer : PropertyDrawer
{
int difCount = 3;

public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);

Rect contentPosition = EditorGUI.PrefixLabel(position, label);
contentPosition.width *= 1 / difCount;
float width = contentPosition.width;

SerializedProperty values = property.FindPropertyRelative ("values");

for (int i = 0; i < difCount; i++) {
EditorGUI.PropertyField (contentPosition, values.GetArrayElementAtIndex(i), GUIContent.none);
contentPosition.x += width;
}

EditorGUI.EndProperty();
}
}
#endif




Давайте разберемся, что тут происходит. Директива компилятора #if UNITY_EDITOR сообщает Unity, что она должна компилировать этот класс только во время разработки в редакторе. В противном случае она будет пытаться собрать этот код при сборке билда игры, а модуль UnityEditor там недоступен целиком, и это может вызвать сбивающие с толку ошибки.

[CustomPropertyDrawer(typeof(DifDepInt))] говорит Unity, что для отрисовки полей классов типа DifDepInt ей нужно использовать предоставленный ниже код вместо стандартного. Таких директив можно указать сколько угодно подряд для всех DifDep-классов, которые вам понадобятся — сам код кастомного редактора написан так, что примет любые классы, имеющие в себе массив элементов под названием values, поэтому этот класс у меня обслуживает и int, и float, и даже Sprite и GameObject.

Мы перегружаем метод OnGUI(), который и занимается отрисовкой области редактирования поля в инспекторе. Unity вызывает его иногда несколько раз за кадр — это нужно иметь в виду. Не забываем оставлять методы EditorGUI.BeginProperty() и EditorGUI.EndProperty(), без них корректно работать ваш код не будет.

Остальной код достаточно интуитивно понятен, если заглянуть в документацию Unity. Вместо магии с contentPosition можно использовать методы отрисовки из класса EditorGUILayout, а не EditorGUI, однако они не всегда ведут себя очевидным образом и в некоторых плохих случаях разбираться с ними себе дороже.

Ради чего же мы этим занимались? Смотрите, какая красота!





Это однозначно удобнее того, что было. Возможности, которые дает подобный функционал, практически безграничны — вы можете отображать самые сложные структуры максимально удобным для редактирования способом. Но не думайте, что гейм-дизайнер будет вам благодарен. Он примет это как должное, я вам гарантирую (:



Кастомные редакторы целого класса



Окей, красиво рисовать отдельные поля мы научились, а можем ли мы рисовать что-то, что охватывает весь класс? Конечно же да! Например, параметры всех грейдов отдельно взятого вида оружия мы задаем вот так:





Помимо редактирования полей здесь присутствует еще и калькулятор, значения в котором изменяются автоматически при изменении параметров оружия (на самом деле они read-only, вид инпутов они имеют только для консистентности и удобства выравнивания).

Как же сделать что-то подобное? Очень просто и схоже с тем, что мы делали до этого! Продемонстрирую на простом примере— добавлении простенького калькулятора DPS перед всеми остальными полями в классе поведения монстра:



#if UNITY_EDITOR
using UnityEditor;

[CustomEditor(typeof(EnemyBehaviour), true)]
public class EnemyCalculatorDrawer : Editor
{
public override void OnInspectorGUI() {
EnemyBehaviour enemy = (EnemyBehaviour)target;
float dps1, dps20;

dps1 = enemy.damageLeveling.Get(1) / enemy.getAttackDelay(1);
dps20 = enemy.damageLeveling.Get(20) / enemy.getAttackDelay(20);

GUIStyle myStyle = new GUIStyle ();
myStyle.richText = true;
myStyle.padding.left = 50;
EditorGUILayout.LabelField("Calculator", myStyle);
EditorGUILayout.LabelField("DPS on level 1: " + dps1.ToString("0.00"), myStyle);
EditorGUILayout.LabelField("DPS on level 20: " + dps20.ToString("0.00"), myStyle);
EditorGUILayout.Separator();

base.OnInspectorGUI();
}
}
#endif




Ситуация очень похожая: сначала мы сообщаем Unity о желании заменить отрисовщик для этого класса с помощью директивы [CustomEditor(typeof(EnemyBehaviour), true)]. Затем переопределяем метод OnInspectorGUI() (да, в этот раз не OnGUI(), потому что разработчик должен страдать), пишем в нем свою кастомную логику (унаследованное от класса Editor поле под названием target содержит в себе ссылку на отображаемый объект как на Object) и затем вызываем base.OnInspectorGUI(), чтобы Unity отрисовал все остальные поля так же, как и обычно. GUIStyle позволяет нам изменять внешний вид отображаемых данных. В этом случае я использовал методы из EditorGUILayout просто потому, что здесь совершенно не надо было беспокоиться о выравненном позиционировании.

Итог же выглядит так:



Соответственно, таким образом можно отрисовывать в инспекторе все что угодно, хоть графики зависимости урона и выживаемости от уровня и этапа игры.



Всякие мелочи



Конечно, можно делать огромное количество других вещей, чтобы спасти глаза и мозг ваших коллег. Unity предлагает целый набор директив для того, чтобы превратить простыню public-полей в структурированное целое. Самая важная из них, это, конечно, [HideInInspector], которая позволяет скрыть public-поле из инспектора. И больше не нужно вопить: «Пожалуйста, не трогайте эти галочки, они служебные!», и затем все равно часами разбираться, почему все монстры внезапно начали ходить задом наперед. Помимо этого есть еще приятные вещи вроде [Header(«Stats»)], которые позволяют отображать аккуратный заголовок перед блоком полей, и [Space], который просто делает небольшой отступ между полями, помогая разбивать их на смысловые группы. Все три эти директивы нужно писать непосредственно перед объявлением public-поля (если вы поставите [Header()] перед приватным полем, то ругаться Unity не станет, но никакого заголовка не отобразит).

Небольшая подсказка: если ваш сериализуемый объект имеет в себе string-поле под названием name, то когда вы засовываете несколько таких объектов в публичный массив, его “имя” будет отображаться вместо неинтуитивного Element X в инспекторе.

И ещё один полезный совет: даже если какой-то настраиваемый объект лежит у вас в сцене и является единственным представителем своего рода, все равно имеет смысл сделать из него префаб. Тогда, в случае совместной работы над проектом, не произойдут конфликты из-за одновременного редактирования сцены: правки, внесённые в инстанс префаба и применённые с помощью кнопки Apply, никак не аффектят файл сцены.



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



Товарищи разработчики-программисты, в совершенстве владеющие традиционным китайским языком, — глубокий поклон вам и извинения за такие предрассудки.
Original source: habrahabr.ru.

https://habrahabr.ru/post/307170/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Комментарии (0)КомментироватьВ цитатник или сообщество

Следующие 30  »

<unity - Самое интересное в блогах

Страницы: [1] 2 3 ..
.. 10

LiveInternet.Ru Ссылки: на главную|почта|знакомства|одноклассники|фото|открытки|тесты|чат
О проекте: помощь|контакты|разместить рекламу|версия для pda