Русский
Русский
English
Статистика
Реклама

Unity3d

Как проходить собеседования на Unity разработчика

21.04.2021 14:19:32 | Автор: admin

Вступление и личные наблюдения

Собеседование на юнити-разработчика состоит в основном из трёх частей. Процесс выглядит практически один в один как и на любую другую техническую специальность в IT. Сначала собеседование с HR или рекрутером, потом техническое интервью с Team Leader команды разработки. В конце, если предыдущие этапы успешно пройдены, вас ждет финальный босс - Project Manager(или Product Owner). Эта статья будет полезна для джунов и мидлов, а также людей которые недавно познакомились с Unity. Бородатые синьоры и лиды - буду рад увидеть от вас в комментариях ваш опыт.

Благодарности

Спасибо Никите и Денису за помощь в оформлении и составлении списка вопросов.

Первая часть - собеседование с рекрутером

Как правило занимает от 10 до 30 минут. На нём задача рекрутера дать предварительную оценку по кандидату. Обычно просят рассказать о себе.

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

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

Пример ответа на Расскажите о своем опыте.:

Разрабатываю игры со старшей школы как инди разработчик, участвовал в джемах и конкурсах. На первом курсе начал работать в гипер-казуальном стартапе. Разрабатывал проекты на Unity C# и Lens Studio JavaScript. Отвечал за полный цикл разработки и гейм дизайн, общался с заказчиком и т.д. Команда состояла из.... Потом принял решение расти как программист дальше, пошел работать в большую компанию для улучшения понимания процессов разработки и технических навыков. Там делал За время работы научился делать. На последнем месте работы делаю Удалось автоматизировать Предложил варианты решений для... Хочу сменить работу потому, что...

Часть вторая - техническое интервью

Вот мы и прошли скрининг. В целом, вроде бы не сумасшедший, какие-то слова из вашей речи рекрутер смог сопоставить с требованиями в вакансии, Лондон из зэ кэпитал оф Грейт Британ смогли из себя выдавить. Супер! Идём дальше!

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

Интервью обычно делится на такие части:

  • Общие вопросы по разработке ПО (OOP, algorithms, DI, SOLID, etc.).

  • Вопросы по C# (boxing/unboxing, GC, async/await, reference types, etc.).

  • Unity и опыт в конкретном игровом жанре(match 3, slots, AAA, FPS, etc.) или направлении(mobile, PC, consoles, AR/VR, etc.).

Общие вопросы по разработке

  • Принципы ООП. Рассказать про каждый. Как это реализовано в языке C#?Как применяли на практике?

  • SOLID. В чем смысл каждого принципа и как применяли на практике?

  • Структуры данных. Какие структуры данных вы знаете? Для каких задач лучше использовать ту или иную.

  • В чем разница между array и List?

  • Что такое хеш-таблица? Что такое хеш-функция? Как обрабатываются коллизии в словарях?

  • Алгоритмы. Поиск пути в графе, сортировки коллекций, поиск элемента в коллекции. Какие подходы в обработке коллизий объектов в 2д и 3д знаете?

  • Сложность алгоритма. Big O notation.

  • Шаблоны проектирования. Архитектурные шаблоны(MVC, MVP, MVVM, компонентный подход, ECS). Шаблоны для решения типовых задач(GoF, GRASP, Game Programming Patterns).

  • Dependency Injection. Что это за подход разработки и умеете ли работать с Zenject?

  • Реактивность. Что это за подход разработки и умеете ли работать с UniRx?

  • Клиент-серверные приложения. В чем основные принципы разработки клиент-серверных игр? Какие типы вы знаете и разрабатывали?

  • CI/CD окружение. Для чего используется? Есть ли опыт работы с ним?

Вопросы по C#

  • Что такое .NET? Что такое CLR? Что такое IL?

  • Чем отличается динамическая типизация от статической?

  • Значимые и ссылочные типы. Спецификаторы аргументов функций ref, out.

  • Boxing и unboxing. Что это и почему это плохо?

  • Строки. Операции над строками, StringBuilder.

  • Что такое класс? Что такое структура? В чем отличие между структурой и классом?

  • Модификаторы доступа.

  • Что такое интерфейс? Какие члены можно описывать в интерфейсе?

  • Отличие интерфейса и абстрактного класса.

  • Upcasting, downcasting.

  • Обработка исключений. Блок try, catch, finally. Порядок выполнения.

  • Что такое делегат? Ковариантность, контрвариантность.

  • Что такое замыкание? Привести пример с замыканием.

  • Может ли структура реализовывать интерфейс?

  • Что такое атрибут? Для каких целей используются атрибуты?

  • Что такое рефлексия? Для решение каких задач приходилось использовать?

  • LINQ. Extension syntax, query syntax.

  • Как работает сборщик мусора? Что происходит с объектами которые имеют циклические зависимости?

  • Есть ли опыт написания авто-тестов и юнит-тестов?

Вопросы по Unity

  • Игровой движок. Что собой представляет и какие проблемы решает?

  • Корутины. Что это? Работают в одном потоке или в разных? Какой механизм C# используется для реализации корутин в юнити? Можно ли запустить рутину не из MonoBehaviour? Какие типы yield инструкций вы знаете? Когда они вызываются?

  • Что такое Game Object? Что такое сцена?

  • Что такое MonoBehaviour? От чего он наследуется? Можно ли создать тип наследуемый от Component?

  • Жизненный цикл MonoBehaviour.

  • Порядок вызова Event функций в runtime режиме Unity.

  • Физика. Какие компоненты позволяют работать с физикой. Что такое rigid body? Что такое рейкаст? Отличие от лайнкаста?

  • NavMesh. Поиск пути.

  • Опыт работы с UI компонентами? Что такое канвас? Что такое панель? Чем плох и хорош канвас? Как верстать адаптивный интерфейс? Что такое LayoutGroup?

  • Камера. Типы камер, параметры для настройки. Скай бокс, occlusion culling.

  • Что такое deltaTime и fixedDeltaTime? Отличия между ними.

  • Аниматор. Можно ли дописывать логику к состояниям аниматора? Что такое Timeline и опыт работы с ним?

  • Опыт написания кастомного редактора, окна для инструментов, расширения для ускорение и автоматизации рутинных задач.

  • Ассет бандлы и адрессаблы. Для чего используются и есть ли опыт разработки с их использованием?

  • Батчинг и Draw calls. Что это? Какие подходы оптимизации вызовов отрисовки вы знаете?

  • Что такое mesh? Из чего состоит 3д модель?

  • Опыт работы с шейдерами. Приходилось ли писать шейдеры?

  • Профайлинг. Какие инструменты для диагностики проблем производительности вы знаете(profiler, deep profiling, frame debugger, memory profiling, profiling on device)?

  • Unity Web Requests. Что это? Приходилось ли работать с клиент-серверным взаимодействием?

  • Есть ли опыт работы с нативным слоем? Android Studio, XCode.

  • Опыт интеграции SDK(реклама, аналитика, конфиги, БД, пуш уведомления).

  • Test Runner. Опыт работы с тестами в движке.

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

Часть третья - финальный босс

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

Вопросы

  • Есть ли опыт провалившихся дедлайнов? Как справлялись с ситуацией?

  • Как решали задачи которые не могли решить самостоятельно?

  • Расскажите о самой сложной задаче.

  • Как вы оцениваете задачи по времени и сложности?

  • Есть ли опыт менторства? Как работали с джунами?

  • Приходилось ли работать в стрессовой обстановке перед релизом?

  • Как относитесь к овертаймам?

  • По какой методологии работали(agile, scrum, kanban)?

Подведение итогов

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

Как и любой другой навык, прохождение собеседований нарабатывается, как не удивительно, прохождением собеседований! Главное - покажите на максимум те навыки, которыми уже обладаете. И помните, если вы провалили собеседование или получили отказ, это может значить две вещи: или вам нужно еще поучиться, или вы банально не подходите этой компании, этому проекту, этой вакансии Это IT, слышал, тут так бывает. Удачи на собеседовании!

Подробнее..

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

28.05.2021 12:18:53 | Автор: admin

Привет, Хабр! Говорят, лень - это двигатель прогресса. Но никто не говорил, что этот самый двигатель, будет быстрым. Моя история о том, как я делал визуализацию голосового помощника Алисы с эффектом голограммы из подручных материалов.

Вступление

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

Например, у меня есть робот голосовой помощник "Vector" от Anki (сейчас им владеет Digital Dream Labs). Он отлично передает эмоции (радость, огорчение, злость и т.д.), когда с ним взаимодействуешь. Но его проблема в том, что программная часть голосового помощника Vector очень слабая по сравнению с такими гигантами как Alexa, Google Assistant, Siri, Алиса.

Робот Vector от AnkiРобот Vector от Anki

Недавно Яндекс выпустил умную колонку Яндекс.Станция Макс с LED-дисплеем. Через дисплей, голосовой помощник "Алиса" дополняет свои ответы анимацией и выражает "эмоции". И это уже хороший шаг в сторону визуализации голосового помощника, но все равно этого недостаточно для меня.

Яндекс.Станция Макс с LED-дисплеемЯндекс.Станция Макс с LED-дисплеем

Бороздя просторы интернета во время всеобщего карантина, я увидел пост о том, как Джарем Арчер сделал рабочий концепт голограммы голосового помощника Cortana от Microsoft. Я вдохновился этой идеей и захотел это повторить, только вместо Кортаны, взять Алису от Яндекса.

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

  • Старый монитор c TFT матрицей (BenQ GW2750HM)

  • Старый ноутбук (core 2 duo p7350, GeForce 9300M, 4Gb RAM)

  • 3D-принтер (Tevo Tarantula 2017)

  • RGB-светодиодная лента

  • Arduino Nano

Дисклеймер по качеству фото

Не сразу додумался фотографировать весь процесс создания, поэтому фото были сделаны на свой старенький Xiaomi

Корпус

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

Модель делал в Autodesk Fusion 360. Сама модель состоит из нескольких частей и ее, в теории, можно сделать под любой размер монитора.

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

Модель корпуса в Autodesk Fusion 360Модель корпуса в Autodesk Fusion 360

3D печать

Для печати корпуса выбрал ABS пластик т.к. он прочный и легко поддается обработке. Но есть и минусы: дико воняет и сильно чувствителен к сквозняку. На печать всех компонентов ушло дней 5 почти непрерывного печатания. Поэтому запах в доме еще долго не мог уйти.

Напечатанный корпусНапечатанный корпус

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

После печати деталей обработал их наждачной бумагой, нанес грунтовку и покрасил в черный матовый цвет.

Эффект голограммы

Эффект голограммы создается просто: сверху лежит монитор, изображение попадает на стекло, которое стоит под определенным углом.

У стекла есть эффект раздвоения изображения (особенно, если стекло толстое), когда изображение отображается на передней и задней стороне стекла. Чтобы это убрать, с обратной стороны наклеил антибликовую пленку.

Первая проверка работы "голограммы"Первая проверка работы "голограммы"

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

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

Полностью собранный корпусПолностью собранный корпус

Программная часть

Программная часть состоит из четырех частей: официальный, но уже устаревший desktop-клиент Алисы, Python-сервис для обработки сообщений, приложение на Unity для отображения модели и Arduino Nano для управления светодиодной лентой.

Общий принцип работы визуализации следующий: клиент Алисы передает текст команды от пользователя и ответ на Python-сервис. После обработки данных, сервис отправляет команду на вызов той или иной анимации в приложении на Unity и команды для управления светодиодной лентой на Arduino.

Чтобы перехватить запросы в клиенте Алисы, использовал реверс-инжиниринг. Для этого взял старую версию клиента Алисы. Данная версия работает на JS, но исходники были обработаны обфускатором, поэтому пришлось долго искать места, где можно перехватить команды. После этого вставил вызовы API моего сервиса для передачи данных.

После того, как сервис получил данные из клиента, он их обрабатывает. В зависимости от того, какие данные пришли, отправляет по MQTT сообщения: состояние (например, Алиса начала слушать пользователя), текст ответа на запрос и изображение ответа. Если с состоянием и текстом сообщения все легко и в сервис приходит простой JSON, то с изображением не так просто. Внутри клиента Алисы изображения строятся на основе сложного JSON, который приходит от сервиса Яндекса. Его нужно было бы обрабатывать и создавать изображение самому, а т.к. я ленивый человек, решил отправлять то, что клиент Алисы сам формирует (HTML блок + CSS). Далее сервис вставляет HTML блок в запущенный заранее веб-драйвер Chrome, делает скриншот и отправляет в MQTT JSON сообщение с изображением в Base64, высотой и шириной изображения для сохранения пропорций в Unity. Для включения/выключения светодиодной ленты, сервис отправляет по Serial порту сообщение/команду в Arduino, выбирая какую область (светодиод над моделью и/или заднюю и нижнюю светодиодную ленту) включить с каким RGB цветом и яркостью.

Есть еще одна функция. Клиент Алисы периодически опрашивает сервис на наличие новых команд для неё. Если есть новая команда, то клиент выполняет её таким образом, как это сделал бы пользователь через голос или текст. Например, если дать команду: "Алиса, повтори за мной: Перед уходом, не забудь покормить кота", то эту фразу она и повторит. Таким образом можно оповещать состояние датчиков.

Приложение на Unity принимает сообщения с MQTT для запуска анимации и отображения текста/изображения на специальных панелях. Модель отображают три камеры (каждое изображение попадает на свое стекло), на которых применен эффект "зеркала", чтобы после проецирования на стекло пользователь видел корректное изображение модели и текста.

Визуализация в UnityВизуализация в Unity

Т.к. голос Алисы бы сделан на основе голоса Татьяны Шитовой, которая озвучивает большинство героинь Скарлетт Йоханссон, то для модели Алисы я взял образ персонажа из комиксов Marvel "Черная вдова", что дало визуализации свой "шарм". Саму 3D-модель я взял из открытого доступа, а скелет и его анимацию сделал в Blender, визуальный эффект голограммы был применен на модели в самой Unity.

3D-модель Алисы3D-модель Алисы

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

Заключение

Огромное спасибо моей жене! Без её помощи, поддержки и терпения я бы забросил эту идею с самого начала.

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

Из минусов, найденных при эксплуатации:

  1. У меня настроена система "Умный дом", через Home Assistant. На нем работают как свои устройства (на esp8266/Arduino), так и производителей (в основном от Xiaomi). Когда я начал делать этот проект, то была возможность управлять всеми устройствами через Алису. И можно было бы не использовать Яндекс.Станцию мини, но в какой-то момент Алиса в клиенте перестала находить эти устройства, хотя управлять ими через станцию все еще можно. Скорее всего поменяли API, поэтому перестало работать, но есть идеи как это можно исправить

  2. Плохая идея использовать монитор с TFT матрицей

Еще нашел необычное применение для такого устройства: вместо Алисы, отображать время или таймер до какого-то события с включенной подсветкой.

Отсчет до нового годаОтсчет до нового года

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

Подробнее..

Разделяй и властвуй Использование FSM в Unity

23.04.2021 22:09:05 | Автор: admin

Грамотная архитектура играет ключевую роль при разработке любого программного продукта. Корни большинства распространенных проблем с производительностью, расширяемостью или понятностью кода растут именно из ее отсутствия. Отсутствие строго определенной структуры проекта лишает разработчиков возможности мыслить абстракциями, понимать написанный коллегой код с первого взгляда и предугадывать место возникновения ошибки. А в некоторых случаях человек может запутаться даже в своем собственном коде, перенасыщенном сущностями и компонентами. Но практически каждый программист рано или поздно, сам ли или с помощью умной книжки, знакомится с решениями, которые хороши вне зависимости от контекста. Они настолько эффективны и универсальны, что находят место в решении множества задач, и... Да, знаю, можно не продолжать, все уже и так поняли, что я о шаблонах проектирования. Одни на них молятся, другие находили среди них свои велосипеды. Некоторые утверждают на собеседовании, что изучили их вдоль и поперек и уличили в полной бесполезности. Но все, так или иначе, слышали о них. Сегодня речь пойдет об одном из паттернов - "Состояние". А точнее, о конечных автоматах. Даже если вы относитесь к последней из перечисленных выше групп, вы наверняка сталкивались со следующим инструментом:

Минимальный аниматор главного героя в платформереМинимальный аниматор главного героя в платформере

Аниматоры в Unity построены как раз на конечных автоматах. Каждая анимация группы объектов представлена в виде состояния. Условия и порядок переходов между ними определяется в аниматоре, который является конечным автоматом. Также, неоднократно поднималась тема использования конечных автоматов для описания логики работы объектов со сложным поведением. AI ботов, управление главным героем, вот это все.

Я бы хотел сделать акцент на менее изъезженной теме. Конечный автомат позволяет эффективно управлять состоянием игры в целом. Более того, с его помощью можно однажды описать бизнес-логику абстрактной игры, и отталкиваться в дальнейшем от этой заготовки при разработке прототипов. Так, многие ГК игры, построенные по такому принципу могут отличаться друг от друга на полсотни строк и структуру сцены, реализуя всю логику на автомате. А в сложнейших и запутанных проектах новый функционал может быть внедрен максимально просто путем добавления в существующий автомат нового состояния и добавления переходов в него, исключая возможность возникновения багов и неожиданного поведения. Также, среди плюсов построения бизнес-логики приложения на автоматах можно выделить следующее:

  • Каждое состояние описывает свою логику максимально просто. Нет никаких условных блоков, каждое состояние представляет собой последовательность действий на входе и выходе и список сигналов, которые автомат обрабатывает в текущем состоянии. Таким образом, мы отображаем джойстик на входе в состояние Play и скрываем на выходе. Но даже если каким-то чудом игрок увидел джойстик на экране паузы, он не сможет двигать персонажа. И не потому, что активен еще один флаг isPaused, а просто потому, что в состоянии паузы не предусмотрено принимать сигналы с джойстика.

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

  • В дополнение к предыдущему пункту, следует отметить, что это также очень удобно в контексте дебага. Посмотрев логи, мы сразу видим, что бот сломался потому, что был инициализирован не после старта уровня, а во время генерации сцены, когда еще не был готов контекст для работы его AI.

  • Многие баги становятся просто невозможны, потому что мы строго определяем условия переходов. Мы точно не попадем в состояние Play, пока состояние WaitMatch не получит сигнал "match_ready", а если мы захотим вернуться в лобби, мы сначала отправим серверу команду об этом, и только после сигнала "room_left" выполним переход.

  • Сама идея логики на автоматах максимально прозрачна. Видя ТЗ мы сразу понимаем, каким будет список состояний, логика, реализованная в каждом из них и граф переходов. Причем, как было отмечено выше, "внешняя" часть графа в большинстве игр будет оставаться неизменной.

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

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

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

FSM

AState

- public FSM(AState initState)

- public void Signal(string name, object data = null)

- private void ChangeState(AState newState)

- void Enter()

- void Exit()

- AState Signal()

Итак, мы имеем 2 сущности:

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

 public class FSM {   private AState currentState;   public FSM(AState initState) => ChangeState(initState);      private void ChangeState(AState newState)   {     if (newState == null) return;     currentState?.Exit();     currentState = newState;     currentState.Enter();   }   public void Signal(string name, object arg = null)   {     var result = currentState.Signal(name, arg);     ChangeState(result);   } }

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

public class AState{  public virtual void Enter() => null;  public virtual void Exit() => null;  public virtual AState Signal(string name, object arg) => null;}

А в самих состояниях мы просто описываем логику

public class SLoad : AState{    public override void Enter()    {        Game.Data.Set("loader_visible",true);        var load = SceneManager.LoadSceneAsync("SceneGameplay");        load.completed+=a=>Game.Fsm.Signal("scene_loaded");    }    public override void Exit()    {        Game.Data.Set("loader_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "scene_loaded")            return new SLobby();        return null;    }    }

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

public class SMessage : AState{    private string msgText;    private AState next;    public SMessage(string messageText, AState nextState)    {        msgText = messageText;        btnText = buttonText;        next = nextState;    }        public override void Enter()    {        Game.Data.Set("message_text", msgText);        Game.Data.Set("window_message_visible",true);    }    public override void Exit()    {        Game.Data.Set("window_message_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "message_btn_ok")             return next;        return null;    }}

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

...case "iap_ok":return new SMessage("Item purchased! Going back to store.", new SStore());...

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

public class ButtonFSM : MonoBehaviour, IPointerClickHandler{    public string key;        public override void OnPointerClick(PointerEventData eventData)    {        Game.Fsm.Signal(key);    }}

Иными словами, мы при клике по кнопке(на самом деле, любому CanvasRenderer) передаем соответствующий сигнал в автомат. При переходе между состояниями мы можем любым удобным нам способом включать и выключать разные Canvas, менять маски, используемые в Physics.Raycast и даже иногда менять Time.timeScale! Как бы ужасно и бескультурно это ни казалось на первый взгляд, пока сделанное в Enter отменяется в Exit, оно гарантированно не может доставить каких-либо неудобств, так что вперед! Главное - не переусердствуйте.

Подробнее..

Как обновить все сцены Unity-проекта в один клик

30.05.2021 12:21:38 | Автор: admin
Танюшка - автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутинуТанюшка - автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутину

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

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

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

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

А далее вы уже сможете использовать и модифицировать приведённый в примере код под свои нужды.

Зачем нужен такой инструмент

Представьте, что у вас есть 3 уровня в вашем проекте. Вы добавили новые скрипты, изменили интерфейс, обновили используемые 3D-модели. Но только в рамках одной сцены.

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

Но что будет, когда их станет 10? 20? 50?

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

Как эту проблему решить?

На самом деле, довольно просто!

Можно поступить несколькими способами. Например, использовать метод OnValidate() в классах, которые уже присутствуют на сцене. Но для этого нужно запустить каждую сцену и сохранить её вручную. Более того, не весь функционал изменения сцен нам будет доступен через OnValidate(), поскольку данный метод есть только у объектов, наследованных от MonoBehaviour.

Нам такой вариант не подходит. Но знать о нём тоже полезно.

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

Чтобы это сделать, мы создадим новый класс в папке Editor:

Пример возможной иерархии для расширений движкаПример возможной иерархии для расширений движка

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

Далее добавим необходимые пространства имён, а также укажем, что наследоваться будем от Editor Window (а не от MonoBehaviour, как происходит по умолчанию):

using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }        private void OnGUI()    {        if (GUILayout.Button("Update scenes"))    Debug.Log("Updating")    }}

С помощью атрибута [MenuItem("Custom Tools/Scene Updater")] мы создадим элемент меню с заданной иерархией в самом движке. Таким образом мы будем вызывать диалоговое окно будущего инструмента:

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

Далее мы добавим кнопку, с помощью которой будем запускать наш дальнейший код:

using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }        private void OnGUI()    {        if (GUILayout.Button("Update scenes"))    Debug.Log("Updating")    }}

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

Быстрое добавление компонентов к объектам

Для добавления компонентов к объектам с уникальными именами можно написать вот такую функцию:

/// <summary>/// Добавление компонента к объекту с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>/// <typeparam name="T"> тип компонента </typeparam>private void AddComponentToObject<T>(string objectName) where T : Component{    GameObject.Find(objectName)?.gameObject.AddComponent<T>();}

Использовать её можно вот так:

AddComponentToObject<BoxCollider>("Plane");AddComponentToObject<SampleClass>("EventSystem");

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

Быстрое удаление объектов по имени

Аналогично можно сделать и для удаления объектов:

/// <summary>/// Уничтожение объекта с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>private void DestroyObjectWithName(string objectName){    DestroyImmediate(GameObject.Find(objectName)?.gameObject);}

И использовать так:

DestroyObjectWithName("Sphere");

Перенос позиции, поворота и размера между объектами

Для компонентов Transform и RectTransform можно создать функции, с помощью которых будет происходить копирование локальной позиции, поворота и размера объекта (например, если нужно заменить старый объект новым или изменить настройки интерфейса):

/// <summary>/// Копирование позиции, поворота и размера с компонента Transform у одного объекта/// на такой же компонент другого объекта./// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,     bool copyPosition = true, bool copeRotation = true, bool copyScale = true){    var newTransform = objectToCopyFrom.GetComponent<Transform>();    var currentTransform = objectToPasteTo.GetComponent<Transform>();            if (copyPosition) currentTransform.localPosition = newTransform.localPosition;    if (copeRotation) currentTransform.localRotation = newTransform.localRotation;    if (copyScale) currentTransform.localScale = newTransform.localScale;}    /// <summary>/// Копирование позиции, поворота и размера с компонента RectTransform у UI-панели одного объекта/// на такой же компонент другого объекта. Не копируется размер самой панели (для этого использовать sizeDelta)/// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyRectTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,    bool copyPosition = true, bool copeRotation = true, bool copyScale = true){    var newTransform = objectToCopyFrom.GetComponent<RectTransform>();    var currentTransform = objectToPasteTo.GetComponent<RectTransform>();            if (copyPosition) currentTransform.localPosition = newTransform.localPosition;    if (copeRotation) currentTransform.localRotation = newTransform.localRotation;    if (copyScale) currentTransform.localScale = newTransform.localScale;}

Причём, благодаря тому, что есть переменные-условия, мы сможем контролировать, какие параметры мы хотим скопировать:

var plane = GameObject.Find("Plane");var cube = GameObject.Find("Cube");CopyTransformPositionRotationScale(plane, cube, copyScale:false);

Изменение UI-компонентов

Для работы с интерфейсом могут быть полезны функции, позволяющие быстро настроить Canvas, TextMeshPro и RectTransform:

/// <summary>/// Изменение отображения Canvas/// </summary>/// <param name="canvasGameObject"> объект, в компонентам которого будет производиться обращение </param>/// <param name="renderMode"> способ отображения </param>/// <param name="scaleMode"> способ изменения масштаба </param>private void ChangeCanvasSettings(GameObject canvasGameObject, RenderMode renderMode, CanvasScaler.ScaleMode scaleMode){    canvasGameObject.GetComponentInChildren<Canvas>().renderMode = renderMode;    var canvasScaler = canvasGameObject.GetComponentInChildren<CanvasScaler>();    canvasScaler.uiScaleMode = scaleMode;    // выставление стандартного разрешения    if (scaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)    {        canvasScaler.referenceResolution = new Vector2(720f, 1280f);        canvasScaler.matchWidthOrHeight = 1f;    }} /// <summary>/// Изменение настроек для TextMeshPro/// </summary>/// <param name="textMeshPro"> тестовый элемент </param>/// <param name="fontSizeMin"> минимальный размер шрифта </param>/// <param name="fontSizeMax"> максимальный размер шрифта </param>/// <param name="textAlignmentOption"> выравнивание текста </param>private void ChangeTMPSettings(TextMeshProUGUI textMeshPro, int fontSizeMin, int fontSizeMax, TextAlignmentOptions textAlignmentOption = TextAlignmentOptions.Center){    // замена стандартного шрифта    textMeshPro.font = (TMP_FontAsset) AssetDatabase.LoadAssetAtPath("Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset", typeof(TMP_FontAsset));    textMeshPro.enableAutoSizing = true;    textMeshPro.fontSizeMin = fontSizeMin;    textMeshPro.fontSizeMax = fontSizeMax;    textMeshPro.alignment = textAlignmentOption;}/// <summary>/// Изменение параметров RectTransform/// </summary>/// <param name="rectTransform"> изменяемый элемент </param>/// <param name="alignment"> выравнивание </param>/// <param name="position"> позиция в 3D-пространстве </param>/// <param name="size"> размер </param>private void ChangeRectTransformSettings(RectTransform rectTransform, AnchorPresets alignment, Vector3 position, Vector2 size){    rectTransform.anchoredPosition3D = position;    rectTransform.sizeDelta = size;    rectTransform.SetAnchor(alignment);}

Замечу, что для RectTransform я использую расширение самого класса, найденное когда-то давно на форумах по Unity. С его помощью очень удобно настраивать Anchor и Pivot. Такие расширения рекомендуется складывать в папку Utils:

Пример возможной иерархии для расширений стандартных классов Пример возможной иерархии для расширений стандартных классов

Код данного расширения оставляю для вас в спойлере:

RectTransformExtension.cs
using UnityEngine;public enum AnchorPresets{    TopLeft,    TopCenter,    TopRight,    MiddleLeft,    MiddleCenter,    MiddleRight,    BottomLeft,    BottomCenter,    BottomRight,    VertStretchLeft,    VertStretchRight,    VertStretchCenter,    HorStretchTop,    HorStretchMiddle,    HorStretchBottom,    StretchAll}public enum PivotPresets{    TopLeft,    TopCenter,    TopRight,    MiddleLeft,    MiddleCenter,    MiddleRight,    BottomLeft,    BottomCenter,    BottomRight,}/// <summary>/// Расширение возможностей работы с RectTransform/// </summary>public static class RectTransformExtension{    /// <summary>    /// Изменение якоря    /// </summary>    /// <param name="source"> компонент, свойства которого требуется изменить </param>    /// <param name="align"> способ выравнивания </param>    /// <param name="offsetX"> смещение по оси X </param>    /// <param name="offsetY"> смещение по оси Y </param>    public static void SetAnchor(this RectTransform source, AnchorPresets align, int offsetX = 0, int offsetY = 0)    {        source.anchoredPosition = new Vector3(offsetX, offsetY, 0);        switch (align)        {            case (AnchorPresets.TopLeft):            {                source.anchorMin = new Vector2(0, 1);                source.anchorMax = new Vector2(0, 1);                break;            }            case (AnchorPresets.TopCenter):            {                source.anchorMin = new Vector2(0.5f, 1);                source.anchorMax = new Vector2(0.5f, 1);                break;            }            case (AnchorPresets.TopRight):            {                source.anchorMin = new Vector2(1, 1);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.MiddleLeft):            {                source.anchorMin = new Vector2(0, 0.5f);                source.anchorMax = new Vector2(0, 0.5f);                break;            }            case (AnchorPresets.MiddleCenter):            {                source.anchorMin = new Vector2(0.5f, 0.5f);                source.anchorMax = new Vector2(0.5f, 0.5f);                break;            }            case (AnchorPresets.MiddleRight):            {                source.anchorMin = new Vector2(1, 0.5f);                source.anchorMax = new Vector2(1, 0.5f);                break;            }            case (AnchorPresets.BottomLeft):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(0, 0);                break;            }            case (AnchorPresets.BottomCenter):            {                source.anchorMin = new Vector2(0.5f, 0);                source.anchorMax = new Vector2(0.5f, 0);                break;            }            case (AnchorPresets.BottomRight):            {                source.anchorMin = new Vector2(1, 0);                source.anchorMax = new Vector2(1, 0);                break;            }            case (AnchorPresets.HorStretchTop):            {                source.anchorMin = new Vector2(0, 1);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.HorStretchMiddle):            {                source.anchorMin = new Vector2(0, 0.5f);                source.anchorMax = new Vector2(1, 0.5f);                break;            }            case (AnchorPresets.HorStretchBottom):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(1, 0);                break;            }            case (AnchorPresets.VertStretchLeft):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(0, 1);                break;            }            case (AnchorPresets.VertStretchCenter):            {                source.anchorMin = new Vector2(0.5f, 0);                source.anchorMax = new Vector2(0.5f, 1);                break;            }            case (AnchorPresets.VertStretchRight):            {                source.anchorMin = new Vector2(1, 0);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.StretchAll):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(1, 1);                break;            }        }    }    /// <summary>    /// Изменение pivot    /// </summary>    /// <param name="source"> компонент, свойства которого требуется изменить </param>    /// <param name="preset"> способ выравнивания </param>    public static void SetPivot(this RectTransform source, PivotPresets preset)    {        switch (preset)        {            case (PivotPresets.TopLeft):            {                source.pivot = new Vector2(0, 1);                break;            }            case (PivotPresets.TopCenter):            {                source.pivot = new Vector2(0.5f, 1);                break;            }            case (PivotPresets.TopRight):            {                source.pivot = new Vector2(1, 1);                break;            }            case (PivotPresets.MiddleLeft):            {                source.pivot = new Vector2(0, 0.5f);                break;            }            case (PivotPresets.MiddleCenter):            {                source.pivot = new Vector2(0.5f, 0.5f);                break;            }            case (PivotPresets.MiddleRight):            {                source.pivot = new Vector2(1, 0.5f);                break;            }            case (PivotPresets.BottomLeft):            {                source.pivot = new Vector2(0, 0);                break;            }            case (PivotPresets.BottomCenter):            {                source.pivot = new Vector2(0.5f, 0);                break;            }            case (PivotPresets.BottomRight):            {                source.pivot = new Vector2(1, 0);                break;            }        }    }}

Использовать данные функции можно так:

// изменение настроек отображения Canvasvar canvas = GameObject.Find("Canvas");ChangeCanvasSettings(canvas, RenderMode.ScreenSpaceOverlay, CanvasScaler.ScaleMode.ScaleWithScreenSize);// изменение настроек шрифтаvar tmp = canvas.GetComponentInChildren<TextMeshProUGUI>();ChangeTMPSettings(tmp, 36, 72, TextAlignmentOptions.BottomRight);// изменение RectTransformChangeRectTransformSettings(tmp.GetComponent<RectTransform>(), AnchorPresets.MiddleCenter, Vector3.zero, new Vector2(100f, 20f));

Аналогично, может пригодиться расширение для класса Transform для поиска дочернего элемента (при наличии сложной иерархии):

TransformExtension.cs
using UnityEngine;/// <summary>/// Расширение возможностей работы с Transform/// </summary>public static class TransformExtension{    /// <summary>    /// Рекурсивный поиск дочернего элемента с определённым именем    /// </summary>    /// <param name="parent"> родительский элемент </param>    /// <param name="childName"> название искомого дочернего элемента </param>    /// <returns> null - если элемент не найден,    ///           Transform элемента, если элемент найден    /// </returns>    public static Transform FindChildWithName(this Transform parent, string childName)    {        foreach (Transform child in parent)        {            if (child.name == childName)                return child;            var result = child.FindChildWithName(childName);            if (result)                return result;        }        return null;    }}

Для тех, кому хочется иметь возможность видеть событие OnClick() на кнопке в инспекторе - может быть полезна вот такая функция:

/// <summary>/// Добавление обработчика события на кнопку (чтобы было видно в инспекторе)/// </summary>/// <param name="uiButton"> кнопка </param>/// <param name="action"> требуемое действие </param>private static void AddPersistentListenerToButton(Button uiButton, UnityAction action){    try    {        // сработает, если уже есть пустое событие        if (uiButton.onClick.GetPersistentTarget(0) == null)            UnityEventTools.RegisterPersistentListener(uiButton.onClick, 0, action);    }    catch (ArgumentException)    {        UnityEventTools.AddPersistentListener(uiButton.onClick, action);    }}

То есть, если написать следующее:

// добавление события на кнопкуAddPersistentListenerToButton(canvas.GetComponentInChildren<Button>(), FindObjectOfType<SampleClass>().QuitApp);

То результат работы в движке будет таким:

Результат работы AddPersistentListenerРезультат работы AddPersistentListener

Добавление новых объектов и изменение иерархии на сцене

Для тех, кому важно поддерживать порядок на сцене, могут быть полезны функции изменения слоя объекта, а также создания префаба на сцене с возможностью присоединения его к родительскому элементу и установке в определённом месте иерархии:

/// <summary>/// Изменение слоя объекта по названию слоя/// </summary>/// <param name="gameObject"> объект </param>/// <param name="layerName"> название слоя </param>private void ChangeObjectLayer(GameObject gameObject, string layerName){    gameObject.layer = LayerMask.NameToLayer(layerName);}/// <summary>/// Добавление префаба на сцену с возможностью определения родительского элемента и порядка в иерархии/// </summary>/// <param name="prefabPath"> путь к префабу </param>/// <param name="parentGameObject"> родительский объект </param>/// <param name="hierarchyIndex"> порядок в иерархии родительского элемента </param>private void InstantiateNewGameObject(string prefabPath, GameObject parentGameObject, int hierarchyIndex = 0){    if (parentGameObject)    {        var newGameObject = Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)), parentGameObject.transform);                    // изменение порядка в иерархии сцены внутри родительского элемента        newGameObject.transform.SetSiblingIndex(hierarchyIndex);    }    else        Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)));}

Таким образом, при выполнении следующего кода:

// изменение тэга и слоя объектаvar cube = GameObject.Find("Cube");cube.tag = "Player";ChangeObjectLayer(cube, "MainLayer");               // создание нового объекта на сцене и добавление его в иерархию к существующемуInstantiateNewGameObject("Assets/Prefabs/Capsule.prefab", cube, 1);

Элемент встанет не в конец иерархии, а на заданное место:

Цикл обновления сцен

И наконец, самое главное - функция, с помощью которой происходит вся дальнейшая автоматизация открывания-изменения-сохранения сцен, добавленных в File ->Build Settings:

/// <summary>/// Запускает цикл обновления сцен в Build Settings/// </summary>/// <param name="onSceneLoaded"> действие при открытии сцены </param>private void RunSceneUpdateCycle(UnityAction onSceneLoaded){    // получение путей к сценам для дальнейшего открытия    var scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToList();    foreach (var scene in scenes)    {        // открытие сцены        EditorSceneManager.OpenScene(scene);                    // пометка для сохранения, что на сцене были произведены изменения        EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());                    // проведение изменений        onSceneLoaded?.Invoke();                    // сохранение        EditorApplication.SaveScene();                    Debug.Log($"UPDATED {scene}");    }}

А теперь соединим всё вместе, чтобы запускать цикл обновления сцен по клику на кнопку:

Полный код SceneUpdater.cs
#if UNITY_EDITORusing System;using UnityEditor.Events;using TMPro;using UnityEngine.UI;using System.Collections.Generic;using UnityEngine.SceneManagement;using UnityEditor;using UnityEditor.SceneManagement;using System.Linq;using UnityEngine;using UnityEngine.Events;/// <summary>/// Класс для обновления сцен, включённых в список BuildSettings (активные и неактивные)/// </summary>public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }    private void OnGUI()    {        // пример использования        if (GUILayout.Button("Update scenes"))            RunSceneUpdateCycle((() =>            {                // изменение тэга и слоя объекта                var cube = GameObject.Find("Cube");                cube.tag = "Player";                ChangeObjectLayer(cube, "MainLayer");                                // добавление компонента к объекту с уникальным названием                AddComponentToObject<BoxCollider>("Plane");                                // удаление объекта с уникальным названием                DestroyObjectWithName("Sphere");                                // создание нового объекта на сцене и добавление его в иерархию к существующему                InstantiateNewGameObject("Assets/Prefabs/Capsule.prefab", cube, 1);                // изменение настроек отображения Canvas                var canvas = GameObject.Find("Canvas");                ChangeCanvasSettings(canvas, RenderMode.ScreenSpaceOverlay, CanvasScaler.ScaleMode.ScaleWithScreenSize);                // изменение настроек шрифта                var tmp = canvas.GetComponentInChildren<TextMeshProUGUI>();                ChangeTMPSettings(tmp, 36, 72, TextAlignmentOptions.BottomRight);                // изменение RectTransform                ChangeRectTransformSettings(tmp.GetComponent<RectTransform>(), AnchorPresets.MiddleCenter, Vector3.zero, new Vector2(100f, 20f));                                // добавление события на кнопку                AddPersistentListenerToButton(canvas.GetComponentInChildren<Button>(), FindObjectOfType<SampleClass>().QuitApp);                // копирование настроек компонента                CopyTransformPositionRotationScale(GameObject.Find("Plane"), cube, copyScale:false);            }));    }    /// <summary>    /// Запускает цикл обновления сцен в Build Settings    /// </summary>    /// <param name="onSceneLoaded"> действие при открытии сцены </param>    private void RunSceneUpdateCycle(UnityAction onSceneLoaded)    {        // получение путей к сценам для дальнейшего открытия        var scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToList();        foreach (var scene in scenes)        {            // открытие сцены            EditorSceneManager.OpenScene(scene);                        // пометка для сохранения, что на сцене были произведены изменения            EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());                        // проведение изменений            onSceneLoaded?.Invoke();                        // сохранение            EditorApplication.SaveScene();                        Debug.Log($"UPDATED {scene}");        }    }    /// <summary>    /// Добавление обработчика события на кнопку (чтобы было видно в инспекторе)    /// </summary>    /// <param name="uiButton"> кнопка </param>    /// <param name="action"> требуемое действие </param>    private static void AddPersistentListenerToButton(Button uiButton, UnityAction action)    {        try        {            // сработает, если уже есть пустое событие            if (uiButton.onClick.GetPersistentTarget(0) == null)                UnityEventTools.RegisterPersistentListener(uiButton.onClick, 0, action);        }        catch (ArgumentException)        {            UnityEventTools.AddPersistentListener(uiButton.onClick, action);        }    }    /// <summary>    /// Изменение параметров RectTransform    /// </summary>    /// <param name="rectTransform"> изменяемый элемент </param>    /// <param name="alignment"> выравнивание </param>    /// <param name="position"> позиция в 3D-пространстве </param>    /// <param name="size"> размер </param>    private void ChangeRectTransformSettings(RectTransform rectTransform, AnchorPresets alignment, Vector3 position, Vector2 size)    {        rectTransform.anchoredPosition3D = position;        rectTransform.sizeDelta = size;        rectTransform.SetAnchor(alignment);    }    /// <summary>    /// Изменение настроек для TextMeshPro    /// </summary>    /// <param name="textMeshPro"> тестовый элемент </param>    /// <param name="fontSizeMin"> минимальный размер шрифта </param>    /// <param name="fontSizeMax"> максимальный размер шрифта </param>    /// <param name="textAlignmentOption"> выравнивание текста </param>    private void ChangeTMPSettings(TextMeshProUGUI textMeshPro, int fontSizeMin, int fontSizeMax, TextAlignmentOptions textAlignmentOption = TextAlignmentOptions.Center)    {        // замена стандартного шрифта        textMeshPro.font = (TMP_FontAsset) AssetDatabase.LoadAssetAtPath("Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset", typeof(TMP_FontAsset));        textMeshPro.enableAutoSizing = true;        textMeshPro.fontSizeMin = fontSizeMin;        textMeshPro.fontSizeMax = fontSizeMax;        textMeshPro.alignment = textAlignmentOption;    }    /// <summary>    /// Изменение отображения Canvas    /// </summary>    /// <param name="canvasGameObject"> объект, в компонентам которого будет производиться обращение </param>    /// <param name="renderMode"> способ отображения </param>    /// <param name="scaleMode"> способ изменения масштаба </param>    private void ChangeCanvasSettings(GameObject canvasGameObject, RenderMode renderMode, CanvasScaler.ScaleMode scaleMode)    {        canvasGameObject.GetComponentInChildren<Canvas>().renderMode = renderMode;        var canvasScaler = canvasGameObject.GetComponentInChildren<CanvasScaler>();        canvasScaler.uiScaleMode = scaleMode;        // выставление стандартного разрешения        if (scaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)        {            canvasScaler.referenceResolution = new Vector2(720f, 1280f);            canvasScaler.matchWidthOrHeight = 1f;        }    }         /// <summary>    /// Получение всех верхних дочерних элементов    /// </summary>    /// <param name="parentGameObject"> родительский элемент </param>    /// <returns> список дочерних элементов </returns>    private static List<GameObject> GetAllChildren(GameObject parentGameObject)    {        var children = new List<GameObject>();                for (int i = 0; i< parentGameObject.transform.childCount; i++)            children.Add(parentGameObject.transform.GetChild(i).gameObject);                return children;    }    /// <summary>    /// Копирование позиции, поворота и размера с компонента Transform у одного объекта    /// на такой же компонент другого объекта.    /// Для корректного переноса координат у parent root объеков должны быть нулевые координаты    /// </summary>    /// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>    /// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>    /// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>    private static void CopyTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,         bool copyPosition = true, bool copyRotation = true, bool copyScale = true)    {        var newTransform = objectToCopyFrom.GetComponent<Transform>();        var currentTransform = objectToPasteTo.GetComponent<Transform>();                if (copyPosition) currentTransform.localPosition = newTransform.localPosition;        if (copyRotation) currentTransform.localRotation = newTransform.localRotation;        if (copyScale) currentTransform.localScale = newTransform.localScale;    }        /// <summary>    /// Копирование позиции, поворота и размера с компонента RectTransform у UI-панели одного объекта    /// на такой же компонент другого объекта. Не копируется размер самой панели (для этого использовать sizeDelta)    /// Для корректного переноса координат у parent root объеков должны быть нулевые координаты    /// </summary>    /// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>    /// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>    /// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>    private static void CopyRectTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,        bool copyPosition = true, bool copyRotation = true, bool copyScale = true)    {        var newTransform = objectToCopyFrom.GetComponent<RectTransform>();        var currentTransform = objectToPasteTo.GetComponent<RectTransform>();                if (copyPosition) currentTransform.localPosition = newTransform.localPosition;        if (copyRotation) currentTransform.localRotation = newTransform.localRotation;        if (copyScale) currentTransform.localScale = newTransform.localScale;    }    /// <summary>    /// Уничтожение объекта с уникальным названием    /// </summary>    /// <param name="objectName"> название объекта </param>    private void DestroyObjectWithName(string objectName)    {        DestroyImmediate(GameObject.Find(objectName)?.gameObject);    }    /// <summary>    /// Добавление компонента к объекту с уникальным названием    /// </summary>    /// <param name="objectName"> название объекта </param>    /// <typeparam name="T"> тип компонента </typeparam>    private void AddComponentToObject<T>(string objectName) where T : Component    {        GameObject.Find(objectName)?.gameObject.AddComponent<T>();    }    /// <summary>    /// Изменение слоя объекта по названию слоя    /// </summary>    /// <param name="gameObject"> объект </param>    /// <param name="layerName"> название слоя </param>    private void ChangeObjectLayer(GameObject gameObject, string layerName)    {        gameObject.layer = LayerMask.NameToLayer(layerName);    }    /// <summary>    /// Добавление префаба на сцену с возможностью определения родительского элемента и порядка в иерархии    /// </summary>    /// <param name="prefabPath"> путь к префабу </param>    /// <param name="parentGameObject"> родительский объект </param>    /// <param name="hierarchyIndex"> порядок в иерархии родительского элемента </param>    private void InstantiateNewGameObject(string prefabPath, GameObject parentGameObject, int hierarchyIndex = 0)    {        if (parentGameObject)        {            var newGameObject = Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)), parentGameObject.transform);                        // изменение порядка в иерархии сцены внутри родительского элемента            newGameObject.transform.SetSiblingIndex(hierarchyIndex);        }        else            Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)));    }}#endif

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

Волшебная кнопкаВолшебная кнопка

Заключение

Имея в своём распоряжении такой инструмент, вы сможете делать всё, что вам угодно за считанные клики: сериализовать поля, менять иерархию на сценах, настраивать Fuse/IClone/DAZ и других персонажей, а также менять Build Pipeline, но об этом как-нибудь в другой раз.

Главное, не забывайте использовать систему контроля версий и проверять запуск ваших модификаций сперва на одной сцене (т.е. без использования RunSceneUpdateCycle).

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

Кстати, тех, кто планирует строить карьеру в IT, ябуду рада видеть на своёмYouTube-канале IT DIVA. Там вы сможете найти видео по тому, как оформлять GitHub, проходить собеседования, получать повышение, справляться с профессиональным выгоранием, управлять разработкой и т.д.

Спасибо за внимание и до новых встреч!

Подробнее..

Подпишись, чтобы не пропустить События

16.06.2021 20:20:54 | Автор: admin

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

В примерах в статье про автоматы я использовал следующую конструкцию:

Game.Event.Invoke("joystick_updated", input);

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

public static class Game{    public static FSM Fsm = new FSM();    public static EventManager Event = new EventManager();       public static ObservableData Data = new ObservableData();...

В этих примерах можно увидеть некоторые вольности в деталях реализации. При масштабировании проекта, например, придется отказаться от статического контекста и на основе класса Game реализовать компоненты, назовем их претенциозно MonoBehaviourPro с подобной структурой для сложных подсистем, и передавать ее в качестве контекста автомату и компонентам этих подсистем. Я намеренно сглаживаю эти углы для большей наглядности примера. Сегодня мы рассмотрим класс с многострадальным названием EventManager, так как он является зависимостью ObservableData и без него мы не сможем двинуться дальше. По ссылке можно увидеть полную реализацию класса EventManager, принцип его работы предельно прост. Мы храним список делегатов c произвольной сигнатурой, подписанных на события со строковым ключом.

Важно, что мы работаем с Generic-структурой, поэтому следует помнить о Type safety. Тип аргумента при отправке события должен соответствовать сигнатурам функций, подписанных на него. Также, можно заметить, что EventManager отдельно хранит binds и binds_global и имеет отдельный интерфейс для работы с ними. Это реализация, специфичная для Unity. Дело в том, что там существует система сцен, позволяющая подгружать или выгружать сцены и объекты. И разница между этими двумя словарями в том, что первый очищается при выгрузке сцены. В идеальном мире мы всегда подписываем объект в Awake и отписываем его в OnDestroy. В таком случае можно было бы обойтись одним binds, не очищая его никогда. Каждый объект подписывается и отписывается в рамках своего жизненного цикла и разве что при переходе между сценами происходило бы немного лишней работы над поштучным отписыванием выгружаемых объектов. Но такой подход не прощает ошибок, выгруженный подписчик в лучшем случае сразу сломает вызов делегата и будет найдена, а в худшем - станет причиной утечки памяти. Так что, в качестве "защиты от дурака" лучше при переходе явно отписывать все, что не было обозначено как Global.

Итак, интерфейс EventManager cводится к 5 методам:

        public void Bind<T>(string name, Action<T> ev)        public void BindGlobal<T>(string name, Action<T> ev)        public void Unbind<T>(string name, Action<T> ev)        public void UnbindGlobal<T>(string name, Action<T> ev)                  public void Bind(string name, Action ev)        public void BindGlobal(string name, Action ev)        public void Unbind(string name, Action ev)        public void UnbindGlobal(string name, Action ev)                  public void Invoke<T>(string name, T arg)                  public void Invoke(string name)

Мы можем подписываться на события и отправлять их. И все это с аргументом произвольного типа. В примере из статьи про FSM мы передавали ввод с джойстика в автомат и, если состояние предусматривает такую возможность, передавали в EventManager событие изменения положения джойстика , на которое может подписаться компонент, управляющий положением игрока(Или потомок MonoBehaviourPro, какой нибудь PlayerController, который передаст информацию о вводе в свой автомат, и если игрок в состоянии SPlayerDriving , будет передавать ввод с джойстика уже автомобилю, за рулем которого он сидит, а если в SPlayerClimbing, джойстик будет двигать игрока перпендикулярно нормали плоскости, по которой он движется, с соответствующей анимацией. Но это уже более сложные примеры, не будем на этом задерживаться). Или же, на входе в состояние игры SWin мы можем отправить событие level_done, а на него подписать анимацию экрана победы, конфетти, и чего там еще ваш ГД придумает.

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

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

Эта статья - вторая в серии:
- Разделяй и властвуй Использование FSM в Unity
- Подпишись, чтобы не пропустить События

Подробнее..

Перевод Скрываем воду внутри лодки

23.04.2021 10:10:48 | Автор: admin
При разработке игр про лодки, да и любых других игр с обширными водными поверхностями, существует проблема сокрытия поверхности воды, когда на ней что-то плавает. Я расскажу о решении, используемом в моей игре Sail Forth на движке Unity, но эта методика применима для любого другого движка.


Та самая проблема. Тащите ведро!

Так как в большинстве игр вода это просто большая плоскость, логично, что плавающие на ней объекты будут пересекаться с её поверхностью!

Как же нам устранить эту проблему? Мне известны две основные методики: одна основана на деформировании меша воды вокруг корпуса судна, вторая заключается в маскировании поверхности воды внутри судна. Я знаю, как использовать вторую методику, поэтому мы реализуем её.

Решение состоит из трёх компонентов:

  • Создание меша маски для каждого судна
  • Написание шейдера для меша маски
  • Изменение шейдера воды для использования маски

Меш маски


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


Меш маски воды

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

Шейдер маски


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

Shader "Custom/WaterMask"{  SubShader   {    Pass     {      // Render the mask after regular geometry, but before masked geometry and      // transparent things.      // You may need to adjust the queue value depending on your setup      Tags {"RenderType"="Opaque" "Queue"="Geometry+10" "IgnoreProjector"="True" }      // Don't draw in the RGBA channels; just the depth buffer      // This is important for making our mask mesh invisible      ColorMask 0      // We write to the depth buffer which will hide the water below our mask mesh      ZWrite On      // We don't want anything to draw in front of our mask,       // as it would allow the water to then be drawn on top of us      ZTest Off    }  }}

Шейдер для меша маски воды

Пока таким будет весь шейдер для маски воды! Краткое описание: он выполняет рендеринг после всей непрозрачной (opaque) геометрии (например, лодки) и до воды, и не записывает никаких цветов, зато выполняет запись глубин. Последнее означает, что мы не сможем его увидеть, но он перекрывает объекты за собой. Он не перекрывает саму лодку, потому что отрисовывается после лодки.


Включение и отключение меша маски

Если мы применим этот шейдер к мешу маски, при условии, что очередь рендеринга воды находится после очереди рендеринга маски, то вы увидите, что это уже работает!

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

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


Ужасно

Что здесь происходит? Лодка качается на волнах, и иногда часть волн поднимается над верхней частью лодки. Это означает, что вода ближе к камере, чем маска, поэтому проходит Z-тест и рендерится.

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

Стенсил-буфер


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

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

Shader "Custom/WaterMask"{  SubShader   {    Pass     {      // Render the mask after regular geometry, but before masked geometry and      // transparent things.      // You may need to adjust the queue value depending on your setup      Tags {"RenderType"="Opaque" "Queue"="Geometry+10" "IgnoreProjector"="True" }      // Don't draw in the RGBA channels; just the depth buffer      // This is important for making our mask mesh invisible      ColorMask 0      // Writing to z depth isn't necessarily required, but might hide       // any extra effects your water has like caustics when the boat interior is below the water surface      ZWrite On      // We don't want anything to draw in front of our mask,       // as it would allow the water to then be drawn on top of us      ZTest Off      // The real meat of the solution.       // Ref - The value this stencil operation is in reference to. I arbitrarily picked '1'.      // Comp - The comparison method for deciding whether to draw a pixel.       //        For drawing the mask, we always want it to render regardless of the       //        stencil state, so I chose 'always'      // Pass - What to do with the stencil state after drawing a pixel. I chose 'replace',       //        which means that whatever was in the stencil buffer will be replaced with '1'       //        where our mask is drawn.      Stencil       {        Ref 1        Comp always        Pass replace      }    }  }}

Шейдер остался точно таким же, добавился только блок со стенсил-буфером в конце. Синтаксис стенсила может быть сложно понять, поэтому вкратце опишу, что здесь происходит:

Stencil это просто означает, что на данном проходе шейдера мы будем выполнять операцию со стенсилом

Ref 1 значение стенсила, на которое мы будем ссылаться, равно 1

Comp always когда мы смотрим на текущее значение стенсила, шейдер всегда должен отрисовывать пиксель вне зависимости от значения стенсила.

Pass replace при отрисовке пикселя мы должны заменять текущее значение стенсила нашим значением, то есть 1.

Итак, результатом выполнения этого шейдера станет то, что стенсил-буфер будет содержать 1 в каждом пикселе, в котором отрисовывается объект.

Теперь нам нужно использовать эту информацию стенсила в шейдере воды.

Маскирование в шейдере воды


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

Pass{  ZWrite On  // Mask the water using the stencil buffer  Stencil   {    Ref 1    Comp notequal    Pass keep  }  CGPROGRAM  #pragma vertex waterVert  #pragma fragment waterFrag  ENDCG}

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

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

Поэтому мы делаем параметр Comp равным notequal, то есть сравнение будет выполняться на неравенство. Если значение стенсила не равно 1, то тест стенсила оказывается пройденным.

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


Вода, вода повсюду, но ни капли в нашей лодке!

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

При этом возникает ещё одна проблема: что если лодка потонет? Я решил отключать рендерер маски воды, как только лодка начинает тонуть. Во всех остальных случаях я предполагаю, что нет никаких причин отрисовывать воду поверх внутренностей лодки.

Ещё одна проблема, похожая на ситуацию с потоплением, рассмотрена в этой замечательной статье: https://simonschreibt.de/gat/black-flag-waterplane/. Высокая волна может встать между лодкой и камерой, которая в таком случае неправильно создаст стенсил-буфер, что приведёт к некрасивому артефакту. Конкретно в вашей игре такая ситуация может и не возникнуть, или наоборот, будет очень заметной.


Упс

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

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

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


Артефакт устранён
Подробнее..

Жидкий персонаж на Unity 3D

04.02.2021 18:19:53 | Автор: admin

Учебные материалы для школы программирования. Часть11

Spoiler

На сегодняшнем занятии мы познакомимся с физикой на джоинтах движка BOX 2D, на примере создания персонажа, похожего на главного героя Gish или Slime Laboratory.

Порядок выполнения

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

Для начала, создадим новую сцену и поместим в неё 10 сфер с радиусом 0.5, таким образом, чтобы получилась "ромашка":

Установим на каждую сферу Rigidbody2D массой 0.5 и CircleCollider2D. Центральная сфера имеет массу 0.05 и drag = 1 и не имеет коллайдера.

Добавим между соседними сферами по одному SpringJoint2D. Обратите внимание на параметр Frequency - это частота опроса пружины, чем частота выше, тем пружина будет сильнее возвращаться на своё место. Damping - демпфирование пружины (предотвращение колебаний, смягчающий эффект).

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

Для придания формы используем среднюю точку. От каждой, из 9-и окружающих её сфер, создадим ещё по одному джоинту.

Данная конструкция позволяет нам держать форму, при этом, она может сработать некорректно, если при сильном ударе центр вылетит за пределы круга. Более того, данная модель не учитывает поверхностное натяжение между удалёнными точками. Для этого, создадим систему из джоинтов по такому принципу:

Итоговая система джоинтов должна выглядеть примерно так:

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

Теперь придадим "лизуну" очертания. Для этого в ассет приложена 3д-модель со скелетом, каждая из 9-и костей которого управляет по одному вертексу по периметру.
Для неё написан скрипт Goo, который управляет привязкой и отвечает за перемещение нашего главного героя.

Закинем модель на сцену и назначим ей скрипт. Конфигурируем его таким образом. На сцене создадим уровень из спрайтов и назначим ему коллайдеры.

Разберём немного сам скрипт:

using System.Collections;using System.Collections.Generic;using UnityEngine; public class Goo: MonoBehaviour {    public ConstantForce2D ForceObject; // центральная точка как объект приложения силы    public float maxForce = 4f; // сила передвижения publicTransform[] bones; // массив костей    public Transform[] go; // массив сфер    public float sphereRadius; // радиус сферы, можно брать автоматически, но в данном случае выставляем вручную    public Transform center; // центральная точка как трансформ    ConstantForce2D[] frc; // все объекты приложения силы      // Use this for initialization    void Start() {        frc = newConstantForce2D[9]; // инициализируем        // находим все объекты приложения силы        for (int i = 0; i < 9; i++) {             frc[i] =go[i].GetComponent<ConstantForce2D>();        }    }    // Update is called once per frame     void Update() {        for (int i = 0; i < 9; i++) {             // выставляем кости по точкам с небольшим смещение            bones[i].position = go[i].position + (go[i].position - center.position).normalized * sphereRadius / 2f;            // добавляем всем точкам силу по горизонтали            frc[i].force = newVector2(Input.GetAxis("Horizontal") * maxForce, 0f);         }        // и центральной точке тоже        ForceObject.force = newVector2(Input.GetAxis("Horizontal") * maxForce, 0f);        // нажали пробел        if (Input.GetKeyDown(KeyCode.Space)) {            foreach(Transformrig ingo) {                rig.GetComponent<Rigidbody2D>().velocity += new Vector2(0, 6f); // прыжок            }        }    }}

После отключения MeshRenderer у сфер, можно увидеть, что всё адекватно работает. Для полноты картины, фон и камеру прикрепляем к центральному объекту. У риджитбади центрального объекта нужно запретить вращение по Z. Можно добавить, на свое усмотрение, пару глаз.

На серые объекты вешаем скрипт RandomColor и выставим ему палитру.

using System.Collections;using System.Collections.Generic;using UnityEngine; public class RandomColor: MonoBehaviour {    SpriteRenderer rndr;     public Color32[] colors;        // Use this for initialization    void Start() {        rndr = GetComponent<SpriteRenderer>();         rndr.color = colors[Random.Range(0, colors.Length)];    }}

Теперь при старте уровень приобретёт цвет.

На этом, сборка нашего проекта завершена!

Подробнее..

Стики и работа с Event System в Unity 3D

10.02.2021 22:13:33 | Автор: admin

Учебные материалы для школы программирования. Часть12

Предыдущие уроки можно найти здесь:

Этот материал состоит из двух частей:

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

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

Далее, перейдем ко второй, где создадим скрипт, реализующий доступ к другим объектам посредством Event System.

По ходу дела, попробуем свои силы в работе со static-переменными для реализации удобной имплементации модулей в проект, и узнаем о глобальных и локальных координатах RectTransform.

Обе части занятия являют собой продолжение работы над проектом "Жидкий персонаж".

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

Порядок выполнения

Создадим новую панель со следующими параметрами:

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

Внутри панели создадим 2 Image согласно иерархии на скриншотах - Joy и Mushroom Joy тело нашего стика, Mushroom его грибок.

Их параметры:

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

Создадим скрипт. Скрипт необходимо закинуть на панель. Его полный листинг выглядит так (если в таком формате совсем неудобно, пишите в комментах - перенаберу исходный код):

Разберём его подробнее
Для начала подключим пространство имён для обработки событий:

using UnityEngine.EventSystems;

За обработку нажатий отвечают методы OnPointerDown и OnPointerUp. Для их работы необходимы следующие интерфейсы: IpointerDownHandler и IpointerUpHandler.

Чтобы работать с информацией о конкретном нажатии (а в случае мультитача данных нажатий может быть несколько) объявляем поле private PointerEventData eventData;

При нажатии на экран вызывается OnPointerDown и складывает информацию о нажатии в eventData.

В дальнейшем это позволяет нам работать с eventData из метода Update().

Для того, чтобы понимать, актульна ли информация о нажатии, введена булева переменная OnScreen. Если мы нажали на экран, то переменная принимает значение true, объект Joy становится в точку нажатия и объекты Joy и Mushroom становятся видимыми.

Метод OnPointerUp отключает видимость Joy и Mushroom и переводит переменную OnScreenв false.

Остальная обработка возникает в Update().
Там мы выставляем Mushroom по глобальной точке нажатия и меряем её локальные координаты.

Принцип такой: нажали, грибок и джой выставились в току нажатия.
Передвинули палец/указатель и грибок сместился относительно джоя. Это смещение мы и берём из локальных координат. Его и используем как результат.

Теперь, в любом скрипте, который используем методы типа GetAxis строку типа Input.GetAxis("Horizontal")меняем наCustomStick.horizontal

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

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

Хочется напомнить, все материалы рассчитаны на использование в составе проекта с главным героем - желе.

Перейдем ко второй части.

Использование своих типов эвентов через код

Откроем проект Goo (жидкий желеобразный персонаж), созданный ранее. Проект используется как пример, можно использовать любой другой.

Создадим новый скрипт. Его листинг:

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

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

Рассмотрим пару вариантов использования этого скрипта. Вариант первый создание потайной двери-стены, открывающийся ключом. Для этого нам понадобится спрайт стены с обычным коллайдером и спрайт или модель ключа с коллайдером в режиме триггера.
Также можно добавить ещё один пустой объект и закинуть на него звук, создав тем самым AudioSource. Уберём у AudioSource галочку вопроизведения при старте и закинем в него ключ и стены.

Далее настроим сам ключ. В данной конфигурации ключ выключает стену, включает звук и выключает себя.

Это самый простой пример логики. Рассмотрим посложнее.

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

Создать её будет сложнее. Кратко, логику можно описать так: у нас есть стенка, которая включается, чтобы не дать игроку выйти. (изначально она выключена, чтобы игрок вошёл).

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

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

Создадим кнопку. Для этого импортируем приложенное извображение и разрежем на 2 спрайта.

Расположим их в мире в одной точке, зелёный выключим и назовём его "Вкл", Красный назовём "Выкл".

Создадим ещё один пустой объект, закинем на него коллайдер, выставим коллайдеру режим триггера и расположим на кнопке. Настроим следующим образом:

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

Сюда же можно добавить звук нажатия кнопки, закинув его на пустой объект или на сам спрайт зелёной кнопки и оставив галочку Play On Awake.

На этом этапе занятие можно считать завершённым.

Пишите комменты, делитесь полезными ссылками, как можно улучшить проект!

Пожалуйста, поддержите инициативу - нажимайте нравится и поделиться!

Подробнее..

Синтезатор на Unity 3D

09.04.2021 12:18:12 | Автор: admin

Учебные материалы для школы программирования. Часть13

Предыдущие уроки можно найти здесь:

При тематическом планировании уроков, мы сталкиваемся с интересной задачкой: в нашей группе учатся и мальчишки и девчонки. У них разные вкусы, разные любимые герои, жанры. И если все занятия будут на тему гонок или Silent Hill - ряды девочек на ваших занятиях, - поредеют. Эту ситуацию мы решаем двумя способами:

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

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

Сегодня мы сделаем проект из категории: спокойный, для девчат. Мальчишкам тоже хорошо заходит - особенно момент, когда в конце урока получается звуковая какофония =)

Рассмотрим следующие темы:

  • выставление вращения объектов в локальной системе координат посредством конверсии из углов Эйлера в кватернионы;

  • события объектов OnMouseEnter и OnMouseExit;

  • метод POW класса Mathf - возведение в степень;

  • парсинг float из имени объекта через системный метод Parse ;

  • стек постэффектов от Unity Technologies;

  • функция движка RequireComponent.

Особое внимание обратим на:

  • изменение скорости воспроизведения и высоты звука через Pitch;

  • использование аудиомикшера и постэффектов на мастер-канале микшера, в частности, реверберации и эмуляции комнаты.

Порядок выполнения

Создаётся новый проект, импортируется приложенный ассет, открывается сцена piano (в проекте заранее заготовлена сцена, ключевыми объектами которой являются клавиши, расположенные на сцене в "Пианино/Клавиши").
Клавиши пронумерованы в соответствии с полутонами, начиная с ноты "до" и заканчивается нотой "фа" следующей октавы, т.е. "до", "до диез", "ре", "ре диез", "ми", "фа" и т.д.

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

Весь проект умещается в один скрипт. Полный листинг содержит пояснения:

using System.Collections;using System.Collections.Generic;using UnityEngine;using System;[RequireComponent(typeof(AudioSource))] // необходимо для того, чтобы скрипт требовал установленный аудиосорсpublic class Piano : MonoBehaviour {public KeyCode Key; // энумератор для выбора клавиши клавиатуры, на которую реагирует скриптAudioSource src; // Аудиосорс, приват-переменная     void Start () {src = GetComponent<AudioSource>(); // получаем аудиосорс        src.pitch = Mathf.Pow(1.059462f, float.Parse(name) - 1f); // высота звука равна 1.059462f в степени (имя_клавиши - 1).}    void Update () { // Для уменьшения отклика стоит использовать FixedUpdateif (Input.GetKeyDown(Key)) { // если нажали клавишу.playNote(); // играем        }        if (Input.GetKeyUp(Key)) { // если отпустили клавишу.stopNote(); // не играем        }}    private void OnMouseEnter() { // если мышь над коллайдером клавиши        playNote(); // играем}    private void OnMouseExit() { // если мышь вышла из коллайдера клавиши         stopNote(); // не играем}    private void playNote() { // играемtransform.localRotation = Quaternion.Euler(-3, 0, 0); // ставим локальный угол поворота на -3 градуса по Хsrc.Play(); // Включаем звук с начала}    private void stopNote(){ // не играемtransform.localRotation = Quaternion.Euler(0, 0, 0); // ставим локальный угол поворота на 0 градусов по всем осям    src.Stop(); // Останавливаем звук    }}

Особое внимание стоит уделить строке:

src.pitch = Mathf.Pow(1.059462f, float.Parse(name) - 1f);//высота звука равна 1.059462f в степени (имя_клавиши - 1).

Число1.059462высчитано математически и является простой заменой логарифмической функции, делящей одну октаву на 12 полутонов. Таким образом, каждый последующий полутон в 1.059462 раза выше предыдущего по частоте, что при количестве 12 полутонов даёт умножение частоты на 2 с ошибкой в 0.00003 Гц на октаву. С учётом того, что динамический диапазон нашего пианино не превышает полторы октавы, звук практически не искажается.

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

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

А всем аудиосорсам в качестве output установлен мастер-канал аудиомикшера.

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

Далее, немного оформляем сцену и добавляем следующие эффекты:

  • SSAO - подчеркнёт тени между клавишами, добавит глубины картинке.

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

  • Антиалиасинг, чтобы убрать пикселизацию.

  • Винетка, чтобы затенить края, выделив основной объект.

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

Готово!

Подробнее..

Разработка своей Just Shapes amp Beats и как всё началось

12.04.2021 00:19:09 | Автор: admin

Немного о себе

Здравствуйте, мне 16 лет и я люблю играть в Just Shapes & Beats (JSAB). Одним прекрасным днём я узнал о такой игре, как JSAB. Я был очень поглощён геймплейной частью, разработчики создали больше 30 уровней из простых геометрических фигур - это же гениально! Но просто так играть мне не хотелось, мне хотелось создавать что-то своё. И так как у JSAB есть редактор уровней, но он находится в pre-alpha тестировании уже больше 2 лет, а уровни делать хочется, мною было принято решение создать свою JSAB. Теперь приступим к самому началу.

Самые начала начал

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

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

Техническая часть

Приступим к технической части игры, и начнём с создания объектов.

Создание объектов

Все объекты создавались через Instantiate, что, как позже выяснилось, очень сильно бьёт по оптимизации. Просто представьте такую ситуацию

public GameObject Obj;private void Start(){  for(int i = 0; i < 100; i++){   GameObject.Instantite(Obj);  }}

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

Также все объекты создавались просто в игровом мире, что конечно никуда не годилось. Позже я создал канвас, но об этом уже в другой части. Для создания объектов использовался JSON формат, в который я вручную записывал типы атак, время их появления и параметры. Файлик выглядел примерно так

{  attacks: [    {      "attackType": "DotCircle",      "time": "1,0828",      "dotCount": "20"    },    {      "attackType": "Beam",      "time": "3,06713",      "width": "50"    }  ]}

При запуске игры этот JSON файл парсился в массив и через Update брал время каждой атаки и сравнивал его с текущим временем музыкального трека.

Анимации

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

Animation anim = GetComponent<Animation>();AnimationCurve curve;// create a new AnimationClipAnimationClip clip = new AnimationClip();clip.legacy = true;// create a curve to move the GameObject and assign to the clipKeyframe[] keys;keys = new Keyframe[3];keys[0] = new Keyframe(0.0f, 0.0f);keys[1] = new Keyframe(1.0f, 1.5f);keys[2] = new Keyframe(2.0f, 0.0f); curve = new AnimationCurve(keys);clip.SetCurve("", typeof(Transform), "localPosition.x", curve);// update the clip to a change the red colorcurve = AnimationCurve.Linear(0.0f, 1.0f, 2.0f, 0.0f);clip.SetCurve("", typeof(Material), "_Color.r", curve);// now animate the GameObjectanim.AddClip(clip, clip.name);anim.Play(clip.name);

И здесь всего лишь прописана трансформация объекта по оси X и изменение его цвета.

Коллайдеры

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

Лирическое отступление

Я очень долго провозился с кругом из точек, я просто перелопатил массу материала, но не мог найти ответы. В итоге оказалось, что можно просто использовать синусы и косинусы, но тут тоже были подводные камни. C# в функцию синуса и косинуса принимает значения в радианах. Я очень долго не мог понять в чём же именно проблема, так как давал значение в градусах. Мои точки никак не хотели лететь туда, куда надо, но позже узнал что и как работает. Чтобы перевести градусы в радианы нужно нашу градусную меру умножить на и разделить на 180, но ещё позже я выяснил, что в Unity уже есть готовое решение. Нужно градусную меру (AngleInDegree) умножить на переменную.

public float AngleInDegree = 90f;private void Start(){  float cos = Mathf.Cos(AngleInDegree * Mathf.Deg2Rad);  float sin = Mathf.Sin(AngleInDegree * Mathf.Deg2Rad);}

Начало "новой эпохи"

В конце концов монохромная палитра надоела глазу и было принято решение использовать оригинальную розово-голубую палитру. Также вся механика была переработана, я отказался от использования JSON в качестве файла уровня и перешёл на использование встроенного юнитевского таймлайна. На нём можно было создавать кастомные маркеры (материал, который я использовал, если вдруг понадобится). И я создал несколько маркеров, отвечающих за свои атаки. Также связал Playable (компонент таймлайна, который отвечает за проигрывание и т.д.) с AudioSource'ом (музыка) и в итоге всё это было синхронизировано.

В итоге вышла такая вот штука:

Небольшой итог

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

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

Подробнее..

Регдоллы на Unity 3D

16.04.2021 18:11:03 | Автор: admin

Учебные материалы для школы программирования. Часть15

Предыдущие уроки можно найти здесь:

Регдоллы - физика тряпичных кукол, основная задача которых - реалистичное падение тел со скелетом. Регдоллы применяются везде - от шутеров (падающие враги) до гонок. Допустим, в Goat Simulator регдоллы являются важнои частью геимплея.

Goat SimulatorGoat Simulator

В данном занятии рассмотрены следующие аспекты:

  • работа со стандартным генератором регдоллов;

  • понимание скелета гуманоидных моделеи;

  • исправление неверно выставленных коллаидеров на Rigidbidy посредством дополнительных объектов в иерархии.

Порядок выполнения

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

Создадим новую сцену, установим на нее плеин или квад в качестве пола. На пол поставим лестницу, на лестницу - модель робота Каила.

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

Перед созданием регдолл-системы необходимо выставить модель в Т-позу. Поворачиваем 2 этих объекта в локальнои системе координат до нужного угла.

должно быть так:

Далее, нажимаем в окне объектов Create->Ragdoll и конфигурируем его следующим образом:

Жмем Create и упираемся в одну проблему. Как можно заметить, модель имеет неверные коллаидеры.

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

После всех этих манипуляции, модель должна падать корректно, а именно, мягко и естественно.

Готово!

Подробнее..

Судно на воздушной подушке на Unity 3D

16.04.2021 18:11:03 | Автор: admin

Учебные материалы для школы программирования. Часть14

Предыдущие уроки можно найти здесь:

Сегодня мы настроены на отдых и развлечения! Поэтому, этот урок будет простой и "короткий". Мы не будем работать с графикой (но вас никто не ограничивает в праве усовершенствовать проект), уделим внимание управлению и работе с физикои, на примере создания судна на воздушнои подушке.

Порядок выполнения

Создадим новыи проект, импортируем в него приложенныи ассет. В данном ассете содержится модели, звук и простая сцена.

Первое, что нам необходимо сделать - это установить на сцену модели карты и СВП, затем создать материал с нулевым трением и назначить его юбке СВП

На само судно устанавливаем Rigidbody со следующими параметрами:

Обратите внимание, что на коллаидерах установлена галочка Convex, а Rigidbody не имеет галочку использования гравитации. Вместо нее используется ConstantForce с довольно большим значением, направленная вниз.

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

Скрипт конфигурируется согласно позапрошлому скриншоту. Полныи листинг скрипта выглядит таким образом:

using UnityEngine;using System.Collections; public class Howercraft: MonoBehaviour {    public Rigidbody HowercraftRigidbody; // риджитбади     public Transform CenterOfMass; // центр масс    public float power = 25000; // мощность вперёд/назад    public float torque = 25000; // мощность влево/вправо    float finAngle; // угол отклонения лопаток     float pitch; // питч для звука    public Transform[] Fins; // массив с лопатками    public AudioSource mainEngine; // звук основного двигателя       public AudioSource pushEngine; // звук турбин     // Use this for initialization     void Start() {        HowercraftRigidbody.centerOfMass = CenterOfMass.position - HowercraftRigidbody.position; // устанавливаем центр масс    }     // Update is called once per frame    void Update() {                float inpFB = Input.GetAxis("Vertical"); // ввод вперёд/назад        float inpLR = Input.GetAxis("Horizontal"); // и влево/вправо              Vector3 vely = new Vector3(HowercraftRigidbody.transform.forward.x, 0, HowercraftRigidbody.transform.for ward.z); // находим вектор приложения силы          float gain = Mathf.Clamp01(HowercraftRigidbody.transform.up.y); // если перевёрнуты, силы будут равны нулю             HowercraftRigidbody.AddForce(vely * power * inpFB * gain, ForceMode.Force); // добавляем линейные силы              HowercraftRigidbody.AddRelativeTorque(0, torque * inpLR * inpFB * gain, 0, ForceMode.Force); // и поворот              finAngle = Mathf.Lerp(finAngle, -45 * inpLR, Time.deltaTime / 0.2f); // угол лопаток            foreach(Transform Fin in Fins) {            Fin.localEulerAngles = new Vector3(0, finAngle, 0); // выставляем угол         }        mainEngine.pitch = 0.9f + HowercraftRigidbody.velocity.magnitude / 60f; //высота звука основного двигателя               pitch = Mathf.Lerp(pitch, Mathf.Abs(inpFB) * 1.3f, Time.deltaTime / 0.5f); // высчитываем высоту звука турбины               pushEngine.pitch = 1f + 2f * pitch;        pushEngine.volume = 0.3f + pitch / 3f;    }}

При этом скрипт лучше давать последовательно, сначала физическии движок, потом звуковои.

Готово!

Подробнее..

Игровые механики на уроке геометрии или векторы на Unity 3D

20.04.2021 16:13:49 | Автор: admin

Учебные материалы для школы программирования. Часть16

Предыдущие уроки можно найти здесь:

В этой статье, мы обратим свой взор в прошлое, и вспомним, с чего начиналась детская школа программирования Step to Science. Первоначальная идея проекта состояла в том, чтобы быть не просто кружком технического творчества, а стать для детей ответом на вопрос, "зачем учиться в школе?"

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

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

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

Порядок выполнения

На примере создания 2D игры баскетбол, рассмотрим векторы (скорости, сил, локальнои и глобальнои систем координат). Разберем принципы представления систем координат и представления векторов. Также будет затронута работа с LineRenderer и многокамерность.

Поехали!

Создадим новыи проект и импортируем в него приложенныи ассет.
Ассет содержит в себе все ресурсы, необходимые для создания полноценного 2D приложения.

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

Конечно, необходимо выставить правильныи Order in layer у спраитов. Добавим мяч, применим к нему Circle collider и Rigidbody.

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

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

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Ball : MonoBehaviour {    public AudioSource hitSound;    public Rigidbody2D rig;    // Use this for initialization    void Start () {    }    // Update is called once per frame    void FixedUpdate() {    }    private void OnCollisionEnter2D(Collision2D other) {        if (other.relativeVelocity.magnitude > 1f) {            hitSound.Play();            hitSound.volume = Mathf.Clamp01(other.relativeVelocity.magnitude / 10);            rig.velocity *= 0.8f;        }    }}

В скрипте нет автопоиска Rigidbody, так что придется закинуть его руками. Если нажать на Play, наш мяч упадет, издавая звуки. Чтобы мяч отскакивал, создадим физическии материал и закинем его на коллаидер мяча.

Теперь подумаем о том, чтобы мяч показывал свое направление. Для этого создадим скрипт, которыи рисует стрелки: нам понадобятся два пустых объекта с LineRenderer, один в другом.

Создадим материал для стрелки:

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

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Arrow : MonoBehaviour {   public Vector3 showVector;    public LineRenderer lrenderer1;    public LineRenderer lrenderer2;    Transform myTransform;    // Use this for initialization    void Start () {        //lrenderer1 = GetComponent<LineRenderer>();        myTransform = transform;    }   // Update is called once per frame    void Update () {        showVector = new Vector3(showVector.x, showVector.y, 0f);        lrenderer1.SetPosition(0, myTransform.position);        lrenderer1.SetPosition(1, myTransform.position + showVector);          if (showVector.magnitude >= 2f) { // длинная стрелка            lrenderer2.SetPosition(0, myTransform.position + showVector - showVector.normalized);        } else {            lrenderer2.SetPosition(0, myTransform.position + showVector * 0.5f);        }        lrenderer2.SetPosition(1, myTransform.position + showVector);        if (showVector.magnitude < 0.1f) {            lrenderer1.enabled = lrenderer2.enabled = false;        } else {            lrenderer1.enabled = lrenderer2.enabled = true;        }    }}

Закинем скрипт на объект-родитель стрелки и настроим его.

Теперь надо написать скрипт, которыи будет вектор скорости передавать в наш скрипт "показывания" стрелки. Он очень простои:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class VectorVelocity : MonoBehaviour {    public Rigidbody2D rig;    public Arrow arrow;    // Use this for initialization    void Start () {     }    // Update is called once per frame    void Update () {        if (rig.bodyType == RigidbodyType2D.Dynamic) {            arrow.showVector =  rig.velocity / 5f;        }    }}

Закинем его на мяч, в скрипте укажем риджибади мяча и объект со скриптом стрелки.

Теперь вектор скорости показывается верно. Вектор скорости уменьшен в 15 раз, чтобы его было хорошо видно. А для того, чтобы было видно траекторию мяча - добавим ему Trail Renderer на любои привязанныи к мячу объект.

Теперь сделаем так, чтобы мяч можно было кидать. Для этого необходимо выставить ему тип Rigidbody как Kinematic и написать небольшои скрипт.

Листинг скрипта:

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.EventSystems;public class Spawner : MonoBehaviour {    public Rigidbody2D ball;    public TrailRenderer tr;    Quaternion oldRotation;    Vector3 oldPosition;    public bool readyToShoot = true;    // Use this for initialization    void Start () {        oldPosition = ball.transform.position;        oldRotation = ball.transform.rotation;    }    // Update is called once per frame    public void Respawn () {        ball.transform.position = oldPosition;        ball.transform.rotation = oldRotation;        ball.velocity = Vector3.zero;        ball.angularVelocity = 0;        ball.bodyType = RigidbodyType2D.Kinematic;        readyToShoot = true;        tr.Clear();    }    public void Shoot(Vector3 speed) {        if (!readyToShoot) {            return;        }        ball.bodyType = RigidbodyType2D.Dynamic;        ball.velocity = speed;        readyToShoot = false;    }}

Скрипт выкладываем на пустои объект в мире и устанавливаем ему наш мяч в качестве риджитбади и его треил.

Этот скрипт сам по себе ничего не делает. Чтобы он работал, необходимо организовать ввод. Создадим UI -> Panel на сцене, выставим панели нулевую альфу и установим на него скрипт TouchPanel.cs , приложенныи в проект.

Внутри панели должен лежать спраит со следующими параметрами (обратите внимание на привязку):

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

Для того, чтобы сделать включение/выключение стрелок, используется скрипт Toggle, которыи реализован через эвент-систему юнити. Его необходимо закинуть на кнопку и сконфигурировать следующим образом.

Готово!

P.S. Делитесь ссылкой на статью с коллегами, друзьями и любопытными учениками. Будет здорово, если вы попробуете провести один из уроков в своей школе или в кружке детского технического творчества, и напишите пару слов обратной связи о том, как прошел урок по Unity 3D. Успехов!

Подробнее..

Тир. Стрельба рейкастами на Unity 3D

06.05.2021 18:22:32 | Автор: admin

Учебные материалы для школы программирования. Часть17

Предыдущие уроки можно найти здесь:

В этом проекте рассмотрим процесс работы:

- с рейкастами и векторами;
- с методами других пользовательских классов;
- с AudioSource и с Rigidbody через код;
- три основных составляющих выстрела, психологически действующих на игрока (звук, свет и свечение, анимация и след от выстрела);
- инстанцирование префабов.

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

Порядок выполнения

Создаём новый проект, импортируем приложенный ассет.
Помимо стандартных ресурсов пакет имеет сторонний плагин для рисовки декалей. Его работа в контексте данного урока не рассматривается.

Проект урока разбит на 2 части - тир и гранаты.

Тир

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

Внутри проекта есть скрипт DecalShooter, который создаёт декали, и в котором расположен весь код стрельбы, включая рейкаст. В нём будет вводиться код взаимодействия с мишенью.
Для начала, необходимо подготовить саму мишень. Ею служит цилиндр, который необходимо уменьшить по Y до состоянии платины, удалить CapsuleCollider и поставить MeshCollider с галочкой Convex. Дополнительно, на цилиндр устанавливается текстура мишени, внутри цилиндра создаётся point light, подсвечивающий мишень, и объект с AudioSource для воспроизведения звука, а на сам цилиндр устанавливается Rigidbody с обработкой коллизий типа Continius Dynamic и галочкой isKinematik. У AudioSource не забудьте убрать галочку PlayOnAwake и закинуть звук попадания в мишень.

Следующим шагом становится создание скрипта Target, который сразу же накладываем на нашу мишень.

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

using System.Collections;using System.Collections.Generic;using UnityEngine; public class Target : MonoBehaviour {    public GameObject light;    public Rigidbody rig;    public AudioSource src;     bool enabled = true;     // Use this for initialization     void Start() {        rig = GetComponent<Rigidbody>();        src = GetComponent<AudioSource>();     }     // Update is called once per frame     void Update() {     }     public void shoot() {        if (!enabled) {           return;        }         rig.isKinematic = false;         light.SetActive(false);         src.Play();         enabled = false;    } }

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

   if (Input.GetKeyDown(KeyCode.Mouse0)) {            time = 0.3f;             ShootSource.Play();             anim.Play("fire");             Muzzleflash.SetActive(true);            // Сама стрельба             RaycastHit hitInfo;            Vector3 fwd = transform.TransformDirection(Vector3.forward);                         if (Physics.Raycast(transform.position, fwd, out hitInfo, 100f)) {                GameObject go = Instantiate(                    DecalPrefab,                    hitInfo.point,                     Quaternion.LookRotation(                        Vector3.Slerp(-hitInfo.normal, fwd, normalization)                     )                ) as GameObject;                go.GetComponent<DecalUpdater>().UpdateDecalTo(                    hitInfo.collider.gameObject,                     true                );                Vector3 explosionPos = hitInfo.point;                Target trg = hitInfo.collider.GetComponent<Target>();                                if (trg) {                    trg.shoot();                }                                Rigidbody rb = hitInfo.collider.GetComponent<Rigidbody>();                                if (rb != null) {                    rb.AddForceAtPosition(fwd * power, hitInfo.point, ForceMode.Impulse);                    Debug.Log("rb!");                }             }            // Сама стрельба         }

Данный код пытается получить компонент из объекта hitInfo и, если это удаётся, вызывает методshoot. Мишень падает, свет от мишени выключается, звук попадания воспроизводится. Далее, желательно дать группе свободное задание по кастомизации своего проекта. Как альтернативу, можно предложить изменить код таким образом, чтобы мишень меняла цвет при попадании. Делается это заменой в Target строк:

light.SetActive(false);

на

light.GetComponent<Light>().color = Color.red;

Таким образом, свет меняется и не удаляется.

Гранаты

Эта часть урока должна ещё больше закрепить понимание векторов и работы рейкастов.

Для начала, создадим другой скрипт - дальномер.

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI; public class Lenght :  MonoBehaviour {    public Text Dalnost;    float rasstoyanie = 0; // переменная для расстояния до цели     // Use this for initialization     void Start() {     }     // Update is called once per frame     void Update() {        RaycastHit hitInfo;                if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hitInfo, 200)) {            rasstoyanie = hitInfo.distance;            Dalnost.text = rasstoyanie.ToString();        }    }}

Скрипт закинем в FPScontroller/FirstPersonCharacter. В Canvas создадим текст, закинем его в скрипт.
В этом скрипте реализован простейший рейкаст, и на его примере мы разбираем, как рейкаст передаёт информацию в структуру и как нам получать из структуры эту информацию.
При срабатывании рейкаста мы выводим дальность на экран.

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

using System.Collections;using System.Collections.Generic; using UnityEngine;using UnityEngine.UI; public class Length :  MonoBehaviour {    public Text Dalnost;    float rasstoyanie = 0; // переменная для расстояния до цели     public GameObject sharik;     // Use this for initialization    void Start() {     }     // Update is called once per frame     void Update() {        RaycastHit hitInfo;                 if(Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), outhitInfo, 200)) {            rasstoyanie = hitInfo.distance;             Dalnost.text = rasstoyanie.ToString ();            if(Input.GetKeyDown(KeyCode.Mouse1)) {                GameObject go = Instantiate(                    sharik,                     transform.position + Vector3.Normalize(hitInfo.point - transform.position),                     transform.rotation                );                Rigidbody rig = go.GetComponent<Rigidbody>();                rig.velocity = Vector3.Normalize(hitInfo.point - transform.position) * 10;            }         }    } }

Сразу же возникнет вопрос - зачем такое решение? К тому же, при направлении в небо бросок не произойдёт. Сделано всё это в учебных целях. Т.к. в рамках урока код пишется поэтапно, на данном участке объясняется, как найти вектор направления на объект по конечным и начальным координатам. Теперь дело за самой гранатой. Создадим скрипт Grenade.

using System.Collections;using System.Collections.Generic;using UnityEngine; public class Grenade :  MonoBehaviour {    public Transform explosionPrefab;     void OnCollisionEnter(Collision collision) {        ContactPoint contact = collision.contacts[0];                // Rotate the object so that the y-axis faces along the normal of the surface        Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal);        Vector3 pos = contact.point;         Instantiate(explosionPrefab, pos, rot);        Destroy(gameObject);    }}

Закидываем на сцену гранату, на меш Body ставим коллайдеру Convex, добавляем гранате RIgidbody и наш скрипт. Получившуюся гранату добавляем в префаб и удаляем со сцены.

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

Создадим эффект взрыва. В нём должен быть свет от взрыва, AudioSource с галочкой PlayOnAwake и звуком взрыва, Spital Blend на 90 процентов переведённый в 3д и увеличенный радиус распространения звука.
Для правильной отработки всех эффектов и разлёта Rigidbody нужно создать ещё один скрипт. Его мы назовём Explosion:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Explosion :  MonoBehaviour {    public float radius = 5.0f;    public float power = 10.0f;    public GameObject svet;    void Start() {        Destroy(svet, 0.1f);        Vector3 explosionPos = transform.position;        Collider[] colliders = Physics.OverlapSphere(explosionPos, radius);        foreach(Collider hit in colliders) {            Rigidbodyrb = hit.GetComponent<Rigidbody>();            if (rb != null) {                rb.AddExplosionForce(power, explosionPos, radius, 3.0f);            }        }    }}

Его нужно закинуть на эффект взрыва и создать префаб.
Данный префаб и используется в скрипте гранаты для создания взрыва.

Готово!

Подробнее..

Менять одежду на персонажах из MakeHuman в Unity3d

25.02.2021 08:12:25 | Автор: admin

Цель статьи, перенести персонажа из Makehuman в Unity3d, так чтобы его одежду можно было снимать и надевать прямо во время игры.

Для этого нам понадобится Makehuman, Blender3d, Unity3d и его плагин UMA (они все бесплатные). Статья написана по мотивам этого ютуб канала, повторяя за автором я набил много шишек, и теперь готов изложить своё видине.

Этап 1: Создание персонажа в Makehuman

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

Но есть три важных момента. Во-первых: нужно добавить скелет к модели (добавляется во вкладке Pose/Animate). В этом туториале я использую риг Game Engine. Во-вторых, если на персонаже есть одежда, то нужно отключить опцию удаления полигонов под одеждой (это можно сделать во вкладке Geometris -> Clothes, и снять галочку Hide faces under clothes). В-третьих нужно экспортировать в fbx указать единицы метры. Так же, на всякий случай, можно сохранить модель.

Несколько дополнительных моментов

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

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

Этап 2: Обработка его в Blender3d

Тут нам нужно сделать три вещи:

  • Нормализовать модель

  • Добавить глобальную кость

  • Нарезать части тела

Запускаем Blender3d. Для начала нам нужно удалить свет, камеру и куб и импортировать нашу модель. Затем импортируем нашу модель: File -> Import -> fbx (и выбираем наш fbx файл)

Грабля на которую я наступил

Первый раз когда потратил время на создание персонажа makehuman, в блендере нажал на экспорт вместо импорта (в результате в мой fbx файл записалась пустая сцена, и пришлось заново создавать персонажа). Тоже самое касается импорта.

Нормализация

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

Очищаем трансформацию позы

Переходим в блендере в режим позы

Переход в режим позыПереход в режим позы

Выделяем всё, (клавиша a)

Затем нажимаем pose ->clear transform -> all

Очистка трансформацииОчистка трансформации

Чтобы проверить что трансформации очищены, можно перейти в режим редактирования. Модель не должна при этом измениться.

После этого персонаж развернулся на 90 градусов, нужно вернуть его обратно

Вращение персонажа в блендер

Сначала нужно перейти в объектный режим.

Переход в объектный режимПереход в объектный режим

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

И наконец, нужно очистить внутреннее масштабирование и вращение всех мешей.

Очистка масштабирования и вращения

Если мы, например, создадим два куба и первый сожмем в объектном режиме, а второй в режиме редактирования, то они сожмутся по разному. На панели трансформации в первом случае scale по всем осям будет 0.500, во втором 1.000. Это значит, что у первого куба меняется атрибут scale, а сам куб, как бы не меняется. Тоже самое с вращением.

Для модели нашего персонажа нам нужно добиться чтобы все его меши имели вращение 0 и масштабирование 1.000 (без изменения формы самих мешей естественно).
Для этого нужно, в объектном режиме выделять каждый меш (включая арматуру) нажимать на ctrl + a, и выбирать пункт rotation & scale.

Добавление глобальной кости

Теперь нам нужно добиться чтобы скелет был совместим с UMA. Для этого нужно добиться такой иерархии костей.

Global (Head= 0,0,0 Tail=0,0,0.1) // новая кость  Position (Head= 0,0,0 Tail=0,0,0.1) // бывшая  Root кость    весь старый скелет (кость pelvis)
Пошаговая инструкция как этого добиться

Выделим Root кость и переименуем её в Position. Затем выставляем у неё характеристики Transform так же как на картинке.

Далее выделяем в арматуру (у меня это Game_engine) и переходим в режим редактирования. Далее нажимаем на add -> single bone

У нас появится кость с именем bone, и её нужно переименовать в Global и выставить в ней те же характеристики, что и для кости Position.

Далее нужно сделать Global кость предком кости Position. Для этого нужно сначала свернуть список дочерних костей у кости Position.

Далее выделить кость Global, затем зажав shift, выделить кость Position. Перевести мышь на экран с 3d моделью и нажать на ctrl + p, в выпадающем меню выбрать keep offset.

Нарезка персонажа

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

В этом туториале мы разобъём персонажа на следующие слоты

  • Голова

  • Торс

  • Ноги (без ступней)

  • Ступни

  • Глаза

  • Волосы

Первом делом нам нужно создать сетку швов (seams mesh). Для того чтобы можно было менять те части, которые идут единой сеткой (например чтобы голова выглядела приделанной к туловищу, а не как отдельная моделька). Для этого нам нужно продублировать главную сетку.

Создание seams mesh

Выделяем главную сетку (в моём случае она выглядит вот так). Прочие сетки (глаза, волосы и одежду) я скрыл.

seams meshseams mesh

Нажимаем shift + d, затем RMB

В результате она должна продублироваться.

И называем её seams mesh (либо любым другим названием).

Теперь осталось добиться того, чтобы вместо старого меша были отдельные части.

Нарезка персонажа

Переходим в режим редактирования нашей главной сетки (не seams mesh).
В подрежим редактирование поверхностей.

Зажимаем alt и нажимаем на какой нибудь из прямоугольников на шее чтобы выделить полоску как на рисунке ниже (стараемся кликнуть ближе к вертикальному ребру чем к горизонтальному, иначе выделится вертикальная лента).

Далее входим в режим рентгеновского выделения

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

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

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

Ещё я нарезал одежду и переименовал части тела.

Таким образом, мы имеем seams mesh и множество мешей если собрать которые получиться модель персонажа. Каждый с одинаковой арматурой.

Теперь экспортируем персонажа в формате fbx (желательно в ту же папку куда экспортировали из makehuman потому что там должны лежать текстуры к нему)

Этап 3: Создание UMA расы

Подготовка сцены

Открываем unity, настраиваем сцену, скачиваем вот этот ассет и импортируем его в unity. Это ничто иное как UMA - Unity Multipurpose Avatar, фреймворк который позволяет кастомизировать персонажей. Перетаскиваем префаб UMA/Getting Started/UMA_GLIB в иерархию.

Импорт персонажа

Создаём папку в юнити в которой будем работать, я назвал её characters. Перетаскиваем туда папку с нашим персонажем (fbx и текстуры). Возможно появиться сообщение о том, что некоторые текстуры не помечены как карты нормалей. Можно нажать fix now.

Для удобства при работе с UMA я закидываю всё в одну папку (в characters), потому что при создании и настройки ассетов постоянно приходиться указывать в них ссылки на друг друга. А каждый раз лазить по папкам не всегда удобно. В любом случае, после того как мы всё настроем можно будет навести порядок.

Выделяем в менеджере проекта нашу fbx модельку, в инспекторе снимаем галочку Convert Units, и нажимаем Apply. Далее переходим во вкладку rig в animation type, выбираем Humanoid и так же нажимаем Apply.

Теперь нажимаем в верхнем меню UMA -> Extract T-Pose (в менеджере проекта наша fbx модель должна быть выделена). В результате должна появиться папка TPoses с экспортированной т-позой.

Создание слотов и оверлеев

В верхнем меню выбирам UMA -> Slot Builder, появившийся окно я перетаскиваю рядом со вкладкой иерархии. Нажимаем на стрелочку рядом с fbx моделью и находим там часть seams mesh и перетаскиваем её в поле seams mesh в Slot Builder. В UMAMaterial выбирем UMA_defuse (для простоты). В slot destination folder переместите папку куда будут генерироваться слоты.

Теперь перетаскиваем части которые должны выглядеть единым мешем в панель automatic Drag and Drop porcessig. Это голова, торс, ноги и ступни (но не глаза и волосы). После того как закончили, в seams mesh выберите None и подобным образом перетащите глаза, волосы и одежду.

Теперь нужно создать оверлеи. Оверлей это по сути текстура, созданная специально для UMA, которую можно комбинировать с другими такими текстурами, используется например для добавления шрамов, татуировок, макияжа, рисунка на щите (ещё можно добиться чтобы цвет этого рисунка на щите менялся программно). Итак, правый клик мыши в менеджере проекта, Create -> UMA -> Core -> Overlay Asset. В Overlay name впишите имя оверлея (например head), в material выберете тот материал который указывали при создание слотов (в нашем случае это UMA_defuse) в спойлере количество каналов укажите 1, и перетащите в появившиеся поле _MainTex соответствующую текстуру. Туже операцию проделайте для других текстур.

Создание TextRecipe и RaceData

Снова правый клик мыши в менеджере проекта Create -> UMA -> Core -> Race Data, затем Create -> UMA -> Core -> Text Recipe. Первый файл - это файл расы. Второй - это базовый набор слотов/оверлеев для неё. Придумайте название для них и переименуйте файлы соответствующим образом. Выделите расу, в поле Race Name введите её название, в TPose перетащите т-позу которую мы получили ранее. В Base Race Recipe перетащите рецепт который мы только что создали.

Теперь нужно назанчить Wardrobe Slots, это что-то типа ячейки инвенторя, куда можно надеть шмотку. В них помещается Wardrobe Recipe. Wardrobe Recipe по сути это сама шмотка. В чем его отличие Wardrobe Recipe от обычный слотов? Слот это сменяемая одна из частей модели, вардроп рецепт это информация о том как этот слот (шмотка) будет надеваться на тело. Например если есть шляпа, то слот шляпы будет содержать мешь шляпы, (и информацию о том, оверлеи с каким матерьялом с ним совместимы). А Wardrobe Recipe будет содержать ссылку на этот слот, нужные оверлеи, и информацию о том что нужно убрать волосы (и другую шляпу если она есть), и ещё скрыть часть головы, если её части проходят сквозь шляпу. Поэтому в Wardrobe Slots можно создать примерно вот такой список:

  • None

  • Hair

  • Shirt

  • Pants

  • Shoes

Теперь выделите файл с BaseRecipe, переключитесь на вкладку Slots. В поле Race Data перетащите наш файл расы (да, теперь файл расы и базовый рецепт ссылаются друг на друга). Теперь нужно перетащить слоты тела на соответствующую панель (слоты одежды и волос не обязательно перетаскивать). В результате должен получиться список из слотов. Теперь для каждого слота нужно перетащить соответствующий оверлей. В данном случае для всех частей тела используется один и тот же оверлей, и после перетаскивания он будет отображаться в панели shared overlays.

В верхнем меню нажмите UMA -> Global Library, и перетащите в левую панельку папку в которой мы работали.

Добавление персонажа на сцену

Всё что мы до этого делали было подготовкой к этому шагу. Перетаскиваем префаб UMA/Getting Started/UMADynamicCharacterAvatar в сцену (можно переименовать его). В инспекторе в компоненте Dynamic Character Avatar в поле Active Race выбираем нашу расу.

Добавляем анимацию

У меня по умолчанию не запустилась анимация (для базовых рас тоже, поэтому подозреваю что это недоработка). Глубоко этот вопрос пока не изучил, но сумел добиться чтобы она воспроизводилась. Для этого под спойлером Race Animation Controllers в списке Race Animators удаляем все элементы и добавляем новый, в Race указываем нашу расу, в Animator выбираем IdleTest-w-head.

Нажимаем на Play. У вас должен появиться персонаж которого мы собрали.

Добавление одежды

Теперь создадим одежду Create -> UMA -> DCS -> Waredrop Recipe, выделяем созданный ассет назначаем ему расу, задаём имя и назначаем Wardrobe Slot . Затем по аналогии с созданием базового рецепта, перетаскиваем на панель слот и оверлей той шмотки которую мы ходим создать. Затем перетаскиваем этот ассет в глобальную библиотеку, так же как мы делали ранее. Проделываем эту операцию для других шмоток и волос. Теперь выделяем нашего персонажа в иерархии, в спойлере Castumization -> Default Recipes перетаскиваем созданные вардроп рецепты на панельку.

Если тело проходит сквозь одежду, то нужно создать сетку сокрытия. Create -> UMA -> Misc -> Mash Hide Asset, указывам слот с частью тела, который нужно частично скрыть, нажимаем на кнопку Begin Editing и в редакторе выделяем те грани, которые будут скрываться при надевании этой шмотки. Далее в Waredrop Recipe нажимаем на кнопку Add Mesh Hide Asset, и выбираем сетку сокрытия.

Теперь можно надевать и снимать эту вещь. Для этого выбираем наш Dynamic Character Avatar, и в спойлере Customisation -> Default Waredrop Recipes перетаскивам на панельку наш вардроп рецепт.

Добавление и удаление одежны программно

Для этого нужно использовать методы компонента DynamicCharacterAvatar. Для добавления SetSlot и ClearSlot для удаления, после этого нужно вызвать метод BuildCharacter. Первый аргумент SetSlot это имя слота куда необходимо надеть шмотку. Полный список доступных слотов можно посмотреть в файле рассы. Второй аргумент это имя файла нашего Waredrop Recipe. В ClearSlot нужно использовать только имя слота.

Первый аргумент в SetSlot излишен (имхо)

На мой взгляд информация о том куда мы надеваем шмотку излишния. По сути значение первого аргумента SetSlot всегда совподает с полем Waredrop Slot соответвующего Waredrop Recipe.

Вот пример скрипта, вам остаеться куда нибдуь повесить методы AddWardrobe и RemoveWardrobe.

using System.Collections;using System.Collections.Generic;using UnityEngine;using UMA;using UMA.CharacterSystem;public class Program : MonoBehaviour{// сюда нужно перетащить наш Dynamic Character Avatarpublic GameObject DCA;private DynamicCharacterAvatar avatar;void Start(){avatar = DCA.GetComponent<DynamicCharacterAvatar>();}public void AddWardrobe(string wardrobeSlot, string wardrobeRecipe){avatar.SetSlot(wardrobeSlot,wardrobeRecipe);avatar.BuildCharacter();}public void RemoveWardrobe(string wardrobeSlot){avatar.ClearSlot(wardrobeSlot);avatar.BuildCharacter();}}

Заключение

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

Подробнее..
Категории: Gamedev , Unity , Unity3d

Опыт разработки первой мобильной игры на Unity или как полностью перевернуть свою жизнь

16.02.2021 16:13:42 | Автор: admin

От кого и для кого

Доброго времени суток! Меня зовут Николай, и я хочу рассказать свою историю и поделиться своим небольшим опытом в разработке своей первой игры. С чего начинал и какие трудности пришлось преодолеть на пути разработки. Статья ориентирована на тех, кто начинает, думает начать или уже разрабатывает свою первую игру. Зачем? Потому что на стадии разработки своей первой игры, сам не однократно читал статьи о подобном опыте, после прочтения которых "наматывал сопли на кулак" и продолжал разработку дальше. От идеи до выпуска в магазин.

Внимание! Статья получилось длинной, так что запаситесь чаем! Если не хочется долго читать, то выжимка из советов в концы статьи.

С чего все начиналось

Шел третий курс универа

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

Выбор направления

Появилась острая необходимость найти "дело", которое будет приносить удовольствие, не придется отрываться от современного мира на длительный срок и иметь финансовый достаток в перспективе сравнимый с моей по образованию профессией. Конец 4 курса универа и мой выбор пал на IT индустрию, а именно на python разработчика. Уделив 2 недели теории, в частности технической документации языка, я начал развивать логику и выполняя задачки каждый день на протяжении полугода, пока в конце декабря 2018 года не обнаружил геймдев.

А вот и Unity!

Выглядит комично или даже банально, но я повелся на клик-бэйт видео с подобным названием "Как сделать свою первую игру за 15 минут" или "Делаю крутую игру за 5 минут без регистрации и смс". Посмотрев данные материалы, в голове появилась мысль, выделить себе пару дней в своем графике, и утолить свое любопытство, установив данную среду разработки на свой компьютер. Потыкав разные кнопочки, и написав код методом "copy-paste", я пришел в неописуемый восторг! Моя творческая натура внутри меня ликовала. Ведь это было так приятно наблюдать за тем, что ты "сам" написал пару минут назад, сейчас заставляет кубик крутиться, перемещаться или менять цвет. Так уж вышло, что средой разработки установленной на мой компьютер оказалась Unity.

Почему Unity?

Он бесплатный, не такой сложный в освоении, большое сообщество и тонны ресурсов для самообучения, поэтому отлично подходит для начинающих разработчиков. Мобильный рынок заполнен проектами созданные на Unity. Даже такие крупные компании как Blizzard, Riot Games, CD Project RED выпустили всеми известные хиты как Hearthstone, Wild Rift и Gwent, используя эту платформу. Приняв волевое решение, я решил уйти в геймдев на пару с Unity.

Подготовка к разработке

Формирование идеи

Определившись с выбором рабочей платформы для разработки игры, я отправился читать статьи людей, у которых имелся уже хоть какой то опыт в данной сфере, чтобы выбрать нужное мне направление. ПК или смартфоны? 2Д или 3Д? Сингл или мультиплеер?
Прочитав большое количество статей и проанализировав их, все советы сходились к тому, чтобы:

  1. Проект не разрабатывать больше 2-х месяцев, иначе увеличивается вероятность потерять энтузиазм и он окажется на полке "потом доделаю" так не дойдя до выпуска;

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

Мой выбор

2Д мобильная аркада с сетевым режимом до 6 человек , рейтинговой системой и вознаграждением. Разработка, которой заняла отнюдь не 2 , а все "12 месяцев".

Аргументы "за":

  • Мне показалось заставлять двигаться объекты будет проще, чем те же 3Д;

  • Мобильный рынок огромен и его доля более половины всей игровой индустрии;

  • Писать сюжеты для игр я не умею, да и опыта в этом нет никакого, поэтому я решил сделать упор на веселье. А играть всегда веселее вместе! Поэтому сетевая;

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

Аргументы "против":

  • Игра уже становилась не так уж проста, как советовали более опытные коллеги;

  • Сложность и продолжительность разработки для не опытного "птенца" увеличивалась многократно.

Аргументы "за" были очень привлекательны и я решил рискнуть. Как говорится - "Чем чёрт не шутит" и "Была не была"!

Знакомство с Unity и его изучение

Учится чему то с нуля, это как учится кататься на велосипеде. Главное стараться и упорно трудиться, и тогда рано или поздно точно должно получится.
Начинал с малого, а именно перемещал разные объекты в пространстве, писав самый примитивный код.

Совет: знайте, чтобы вы не делали или не хотели начать делать, это с вероятность больше 90% уже было сделано до вас. Научитесь правильно искать нужную информацию!!!

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

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

Как только я накидал определенный план действий и уже собирался начать делать свой первый будущей "шедевр", встал серьезный вопрос

Где я возьму картинки, музыку и остальные элементы для своей будущей игры? Ведь я совершенно не умею сам это создавать

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

А ты сам все это нарисовал? А музыку ты писал тоже сам?

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

Совет: Не чурайтесь использовать чужие наработки или шаблоны, которые продают или прибегать к работе фрилансеров! Это взаимосвязанная выгода! Конечному пользователю все равно, сами вы рисовали самолетик несколько часов или потратили 10$ на его покупку в магазине, ведь главное результат!

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

Совет: Отслеживайте скидки на продаваемые ассеты в различным магазинах, особенно под новый год! Можно приобрести кучу ассетов по выгодной цене со скидкой до 90% в такое время.

Непосредственная разработка

Первые шаги

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

На этом этапе моя игра имела следующий вид:

Главное начатьГлавное начать

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

Совет: Не думайте, что сетевая игра, будет легче чем написать простенький сюжет. Это совершенно не так.

От простого к сложному

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

Эх, как же сильно была переработана финальная версия интерфейсаЭх, как же сильно была переработана финальная версия интерфейса

Интерфес и меню

Не малая важная часть разработки игры, о которой начинающие разработчики не задумываются это Меню, его визуальное оформление и работоспособность. Когда уже основная часть геймплея готова, появляется ложное чувство того, что проект уже готов, и можно завтра его готовить для публикации в магазине. К сожалению, это не делается за пару дней. Это тяжелая и рутинная работа, которая требует концентрации и повышенной ответственности. Когда что-то светиться, крутиться и перемещается в меню, это располагает к себе пользователя. Не зря же все крупные вендоры компьютерного желаза продают свой товар с большим акцентом на динамическую RGB подстветку, значит есть положительный результат!

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

На этом этапе разработки, многие проекты так и не доходят до выпуска в мир. Тут несколько факторов способствуют:

  • усталость

  • потеря интереса

  • неувереность в своих силах

  • все кажется адом и этому нет конца и края

Совет: Скажу то, что я прочитал когда сам проходил этот этап. НЕ СДАВАЙСЯ! Как бы не было сложно, ни в кое случае НЕ СДАВАЙСЯ и НИ ШАГУ НАЗАД! Дойдя до самого конца ты познаешь лавину экстаза и самоудовлетворения от того, что ты не бросил все! И разумеется бесценный опыт!!!

Однопользовательский режим

Сетевая игра это, конечно, хорошо, но что если у пользователя отсуствует подступ к Информационно-Телекоммуникационной Системе Общего Пользования?(Интернет)
Разумеется, было принято решение сделать простенький сингл режим, где игрок будет собирать определенный ресурс на время, а ему попутно будут мешаться ИИ, затруднив выполнение миссии.
Две недели работы, и первоначальный вид был такой:

Концептуальные различие с финальной версией отстствуютКонцептуальные различие с финальной версией отстствуют

Оптимизация

Фух! Оптимизация это такая штука, о которой ты начинаешь задумываться, когда твой проект на смартфоне выдает 10-15 кадров в секунду с фризами и просадками до 4 кадров секунду. Как только тестирования проекта доходит до сборки его на телефоне, все встает на свои места. При разработке прилождений на мобильные смартфоны, оптимизация имеет очень важную роль. Ведь в них не скрывается такая вычислительная мощность как на ПК.

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

  • картинки

  • материалы

  • звук

  • шейдеры

  • настройки камеры, рендеринга

  • интерфейс

  • скрипты

Это заняло у меня еще не меньше двух недель.

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

Одна голова хорошо, а несколько лучше

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

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

Реклама и внутриигровые покупки

Настал черёд встроить в свой проект рекламу и сделать магазин. Кто бы что не говорил, но на одном энтузиазме далеко не уедешь и святым духом сыт не будешь. Посему, это необходимый блок разработки. Тут главное грамотно подойти к этому делу, чтобы игрок не "плевался" и выгода была для обеих сторон!

Софта для рекламных интеграций имеется множетство, в том числе и от самой Unity, так называемая Unity Ads. Однако, мой выбор пал на Google AdMob. Почему не Unity Ads? Почитав обзоры, я узнал, что контент рекламы содержит казино, рулетки и ставки. Тут уже на вкус и цвет, как говорится, но я не хочу чтобы реклама была связана с подобного рода сервисами. Я использовал межстраничную и рекламу с вознаграждением.

Совет: Реклама с вознаграждением, намного лучше, ведь игрок сам нажимает на просмотр рекламы, чтобы получить какие-либо "плюшки" в игре. Разработчик и пользователь в плюсе!

Покупки в игре, я реализовал подобным образом:

Финальная версия игры

"12 месяцев" кропотливой работы , и финальная версия выглядит примерно так:

Меню игрыМеню игрыСетевой ГеймплейСетевой Геймплей

Казалось бы, все сделано и осталось дело за малым, а именно опубликовать игру. Но не все так просто как кажется!

Совет: Тут необходимо открыть еще одно "второе" дыхание , к ранее уже открытым +100500

Публикация игры

Большим плюсом выбора Unity - кроссплатформенность, что позволяет один проект выпустить на всех желаемых платформах (Android, iOS,PC,WebGl и др). К моменту написания статьи игра была опубликована только для Android в Google Play Market, но не за горами ios в Apple Store.

Какие "подводные камни" имеются?

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

Так в чем же проблема и где те самые "подводные камни"?

Политика конфиденциальности

Для публикации она не является необходимым элементом, однако, если в вашем приложении имеется реклама или внутриигровые покупки, то в кратчайшие сроки стоит обзавестись данной "бумажкой" и указать сссылку на неё в Google Play Console. К счастью, есть моножество ресурсов, которые генерирует данный документ за считанные секунды и сразу предлагают разместить на их сайте, удовлетворяя запросам гугла.
Если проигнорировать предупреждения от гугл, что у вас отсутствует политика конфидециальности, то приложение могут легко снять с публикации.

Совет: Не откладывайте на потом этот пункт, делайте его паралельно с публикацией!

Идентификатор клиента OAuth

Если у вас в игре имеется система достижений, рейтинга от гугл или вы хотя бы сохраняете данные игры в облаке от гугл, то необходимо, чтобы пользователь проходил процесс авторизации используя гугл аккаунт, а значит предоставлял некоторые разрешения на управления его данными. Теперь по порядку. При настройке игровых сервисов в Google Play Console, необходимо создать приложение для авторизации пользователя в Google Cloud Platforms, настроить учетные данные для идентификатора клиента OAuth, и Окно запроса доступа OAuth. Пожалуй это главный "подводный камень".
Сложность состоит не в его первоначальной настройке, чтобы сервисы исправно работали, а в том что приложение было опубликовано и не имело ограничений по количеству пользователей. Если вы намерены создавать крупнобюджетный проект, которые будет привлекать тысячи игроков, то вам придется обязательно пройти этот этап.

Сайт игры

Это не является обязательным пунктом, но лучше сделать сайт, где будут размещены новости вашего проекта, а так же политика конфиденциальности и прочие материалы для ознакомления. Оказывается в 2021 году сделать легкий и простой сайт достаточно просто. С шаблонами для разработки сайтов в Word Press, не долго думая, я останавливаюсь на нем. Для сайта необходим хостинг и собственный домен. Взвесив все "за" и "против", решил потратить пару тысяч рублей на его аренду, сроком на 48 месяцев и не "париться". В сети огромное количество предложений, так что проблем с этим тоже не было. Пару часов уходит на его настройку, и еще пару часов на наполнение его контентом. И вот уже есть свой собственный сайт для игры!

Совет: Чтобы получить заветную галочку во вкладке Окно запроса доступа OAuth в Google Cloud Platforms, иметь сайт игры и свой домен , где так же будет размещена политика конфиденциальности - является обязательным пунктом!

Совет: Так же, если используете рекламу от Google Admob, то сайт тоже необходим. В корневую папку вашего сайта добавляется файл app-ads.txt. Это позволяет рекламодателям понять, какие источники объявлений имеют право продавать рекламный инвентарь. Если не пройти авторизацию, то доход с рекламы будет сильно снижен!

GDPR

Еще одно бюрократическое препятствие осталось, на пути для публикации. Если ваше приложение имеет рекламу, то она может быть персонализированной, а значит ваше приложение собирает данные пользователей, чтобы успешно показывать рекламу. GDPR- (General Data Protection Regulation) -этозакон, принятый Европейским Парламентом, который описывает правила защиты данных для граждан ЕС. Это значит,чтобы показывать персональну рекламу, необходимо перед первым запуском вашей игры, пользователь должен принять соглашение, что ознакомлен с политикой конфиденциальности вашего приложения, а так же прочитать в каких целях будет использоваться его персональные данные и дать согласие/отказаться на их обработку. Разумееется это распространяется на резидентов из стран ЕС.

После выполнения всех выше изложенных пунктов, мое приложение успешно опубликовано в Google Play Market и не знает никаких проблем.

Краткая выжимка советов

  • Изучите рынок, и определитесь с направлением и жанром игры. Главное не стройте в начале "наполеоновские"планы, которые могут и не реализоваться!

  • Распишите план действий и пытайтесь четко следовать ему, попутно внося небольшие правки. Старайтесь укладываться в установленные планом сроки.

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

  • Не стесняйтесь приобретать ресурсы для ускорения разработки и повышения качества своего проекта.

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

  • Изучите базовые навыки работы с редактированием изображений и звуков.

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

  • Научитесь использовать на базовом уровне Git. Незаменимый помощник при разработке игры, чтобы контролировать внесенные изменения.

  • Последнее и наверно самое важное. Никогда не сдавайтесь , верьте в себя, упорно трудитесь и рано или поздно, но у вас все обязательно получится! Если получилось у меня и миллионов других начинающих разработчиков данного ремесла, то почему не должно получится и у вас!?

Заключение

Премного благодарен вам, что вы дочитали конца эту длинную статью! Надеюсь, что те кто только пробуют себя в роли разработчика, подчерпнут для себя некоторые полезные вещи и получат заряд мотивации, а "бывалые" найдут эту историю как минимум не скучной.

Не знаю, кто сказал, что разработка игр это веселое занятие. Это не разу не вёселое занятие, требующее повышенной концентрации, ответственности к деталям, высокого уровня самодисциплины и упорства. Интересно ли?! Разумеется да, иначе бы я не начинал этот путь. Играть в игры и делать их, это совершенно разные вещи. Но в любом случае, это безумно интересно! Сделанный выбор однажды в универе, полностью перевернул моё мышление и всю мою жизнь. Не бойтесь и дерзайте!

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

Чтобы не было недопониманий на счет даты релиза.

Впервые игра была опубликована 2 декабря 2019 года, и это было 10 месяцев разработки. После я был вынужден отдать долг своей родине. Срочную службу в армии я нес до 2 декабря 2020. После демобилизации, я сразу продолжил разработку. И 4 февраля 2021, после "12 месяцев" разработки, я выпустил проект.

Если Вам интересно посмотреть на результат моей работы, то вы можете найти в Google Play Market.

Название игры - Starlake

Подробнее..

Обзор технологий трекинга AR Маски

02.02.2021 02:11:38 | Автор: admin

Всём привет. Меня зовут Дядиченко Григорий, и я люблю трекинг. За последние годы технологии трекинга развивались семимильными шагами и становились всё более и более демократичными. Появилось много технологий самого разного плана. Мне повезло поработать с огромным количеством технологий разного рода, поэтому данные знания хочется несколько структурировать. По большей части мы будем разбирать технологии трекинга совместимые с Unity или Web. Так что, если эта тема вам интересна. Добро пожаловать под кат!

Трекинг в общем это очень объёмная тема. Существует множество технологий трекинга, про которые хочется рассказать в этой серии статей. Но сегодня хочется поговорить про Face Landmark Recognition. В целом в плане распознавания лица существует множество других прикольных технологий и задач, которые они решают, типа того же Face Recognition в FaceID и подобных кейсах безопасности, но в контексте работы с графикой и интерактивных визуализаций самой интересной, пожалуй, является именно технология определения ключевых точек на лице.

Применение AR масок

AR маски наверное самый распространённый вид применения дополненной реальности. Иногда даже складывается впечатление, что многие не воспринимают его, как AR. Первое же что приходит в голову это социальные сети. Маски с дополненной реальностью уже неотъемлемая часть того же Instagram и TikTok. Но это далеко не единственное использование технологии. Приведу ещё немного примеров.

Face Tracking в продакшене

Покупка Epic Games технологии Hyprsense.

В 2020 году у нас состоялась интересная сделка. В статье, конечно, говорится о том, что анимации в фортнайт. Это возможно, но есть и другая сторона вопроса. В игровой разработке и в видео продакшене многие пытаются перейти от дорогостоящего производства лицевых анимаций с помощью Motion Capture технологий, таких как Faceware, к более простым инструментам. FaceRig используется VTuberами. Да и во многих случаях не нужна точность, которую предоставляют дорогостоящие технологии захвата движений. Даже в проектах, которые мне когда-то нужно было разработать встречался такой подход, что часть лицевых анимаций записывались с помощью трекинга ARKit. Поэтому одно из применений это упрощённый демократичный путь производств, который подходит тем же инди студиям, у которых нет бюджета на закупку оборудования за несколько тысяч долларов. Хотя сравнительно недавно у Faceware появилось предложение для инди.

AR Маски в рекламе

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

https://player.vimeo.com/video/220504292

https://mixr.ru/2021/01/20/trollo/

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

Технологии

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

SparkAR

Цена: Бесплатно

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: Нет, но можно сделать ссылку в приложении

Поддержка устройств: Большое количество устройств

https://sparkar.facebook.com/ar-studio/

Говоря про маски, нельзя не упомянуть Spark. Спарк это прекрасная технология, которая позволяет создавать свои Instagram маски. Если вас устраивают ограничения политик фейсбука и маски ориентированы в первую очередь на шейринг это отличный выбор, так как в вашем приложении можно разместить ссылку на маску и дать её пользователя. В любом случае пользователи будут делиться вашей маской в инстаграм, фейсбук и т.п. Широкая поддержка устройств является основным плюсом. Кроме того, большое количество обучающих материалов по сбору масок.

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

ARFoundation (ARKit/ARCore)

Цена: Бесплатно

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: Есть

Вес SDK в билде: Около 2МБ

Поддержка устройств: Маленькое количетсво устройств

https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@4.1/manual/

В версии ARFoundation 4.1 Face tracking поддерживается уже и ARCore и ARKit (Достаточно долгое время ARFoundation поддерживал только ARCore). Отдельно на самих технологиях останавливаться не хочется, так как они имеют не так много различий и, по сути, объединены общим API в AR Foundation. Из плюсов встраиваемость в Unity приложение, стабильная работа, относительная простота интеграции и бесплатность. Основным же минусом является небольшое количество поддерживаемых устройств.

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

OpenCV + Dlib

Цена: Бесплатное/реализация для Unity 135$ (https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088 + https://assetstore.unity.com/packages/tools/integration/dlib-facelandmark-detector-64314 )

Поддерживаемые платформы: Android/IOS/WebGL/Win/Mac/Linux/Hololens/MagicLeap

Совместимость с Unity: Есть

Вес SDK в билде: около 9МБ (без классификаторов, но их можно скачать по сети)

Поддержка устройств: Зависит от требуемого функционала

https://opencv.org/

Последняя условно бесплатная технология. Условно бесплатная, потому что в целом можно взять бесплатные библиотеки, написанные на том же Python, развернуть сервер обработки и слать данные куда душе угодно. В случае локальной сети это даже возможно сделать в реальном времени. Что же касается реализации для Unity - если не писать порт самостоятельно, то она платная. Основной нюанс OpenCV и Dlib, что это не готовые технологии и с ними надо уметь работать. К плюсам можно отнести широкую поддержку платформ. Работает оно почти везде и умеет очень многое. Даёт больше контроля, позволяет самостоятельно настраивать качество классификаторов и многое другое. На Python огромное количество материалов для изучения, которые легко переносить в Unity поняв суть SDK.

Из минусов - очень высокий порог входа. Для того, чтобы комфортно работать на OpenCV в реалтайме и не иметь проблем с производительностью - нужно очень много знать. Во-первых, знать OpenCV достаточно хорошо. Во-вторых, знать, как работать с многопоточностью в Unity, да и в целом знать Unity достаточно глубоко. Cтоит ввязываться, только если интересно разобраться в теме компьютерного зрения или разработки своей технологии. Или не лезь, оно тебя сожрёт. Базовые примеры на андроид выдают 2 FPS, но я реализовывал и стабильные 30, и стабильные 60 на средних устройствах закапываясь в это SDK с головой.

XZIMG

Цена:от 2100

Поддерживаемые платформы: Android/IOS/WebGL/Win

Вес SDK в билде: около 2МБ

Совместимость с Unity: Есть

Поддержка устройств: Большое количество устройств

https://www.xzimg.com/Products?nav=product-XMF

Основной плюс данного SDK в коммерческих проектах при широкой поддержке устройств - что это не подписка, а лицензия за которую надо заплатить один раз. В ряде проектов при сметировании это в разы удобнее, чем учитывать подписку, которая ещё в свою очередь зависит от нагрузки. Хотя цена, конечно, немаленькая. По качеству трекинга работает хорошо, даже на слабых устройствах типа MOTOROLA G5S. Ну и Android 4.1 - это очень широкий спектр устройств. Судя что по сайту, что по проекту примеру - делали инженеры. Тут есть ряд доработок, которые сильно улучшат перфоманс этого решения. В целом без опыта работы в OpenCV неплохое коробочное решение.

ARGear

Цена: Бесплатно/от 25$ в месяц

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: Есть

Вес SDK в билде: около 9МБ

Поддержка устройств: Большое количество устройств

https://argear.io/

По ощущениям работает хуже, чем XZIMG. Подтормаживает даже на Xiaomi Redmi Note 8T. Проект пример на андроид плохо организован и без пляски с бубном не заводится. Но в целом с этим можно работать. В целом, как сервис по подписке выглядит неплохо, и не просит сразу платить за коммерческую лицензию несколько тысяч долларов, что думаю для кого-то будет плюсом. Больше всего расстроил, по сути, sample project для Unity, так как у меня sdk. которые не заводятся из коробки - не вызывают доверия.

BRFv5

Цена: Неизвестно

Поддерживаемые платформы: Web/Android(Chrome)/IOS(Safari)

Совместимость с Unity: Нет

https://github.com/Tastenkunst/brfv5-browser

Единственное целиком и полностью веб SDK с непонятным ценообразованием, которое в первую очередь понравилось мне своим качеством. Пример его работы можно посмотреть тут. Но как же оно хорошо работает. Я протестировал его и на своих тест устройствах на смартфонах (важно: на андроид работает именно в хроме) и на пк погонял, и для 68 лендмарок это прям круто. В плюс к тому это веб. Прям даже интригующе. Качество трекинга в разы лучше, чем у того же Media Pipe графа для построения меша лица.

В заключении

Из не вошедших, пожалуй, стоит упомянуть Banuba и Deepar. Бануба не вошла, так как у неё непонятное ценообразование и как бы там ни было сложная схема получения триала. А Deepar так как у него отсутствует SDK для Unity, и при этом он мне не показался особо интересным.

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

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

Подробнее..

Перевод Создаём 2,5D-игру жанра Dungeon Crawling в Unity

19.04.2021 10:09:21 | Автор: admin
В этой статье мы расскажем, как можно воссоздать старые игры жанра dungeon crawler в Unity.



Eye of the Beholder (1991 год) и SMT If (1994 год)


Базовый ассет карты для уровня нашего подземелья.

Большинство dungeon crawler-ов 90-х, в том числе и наш это листы спрайтов, выстроенные в виде коридора. Хотя с технической точки зрения мы используем 3D-движок, никакие меши не рендерятся. Подземелья это просто сетки, составленные из разных типов блоков, хранящиеся в карте вместе с данными монстров и объектов.

Чтобы отобразить любую перспективу для заданной координаты [x, y] сетки и направления взгляда игрока, мы можем вычислить, какие фрагменты стен нужно рендерить. Если блок находится непосредственно перед игроком, то нужно показать самую крупную плоскую стену. Если в двух квадратах вперёд и влево есть ещё одна стена, то нужно показать боковую панель, соответствующую этому расстоянию. Каждый блок в определённом радиусе вокруг игрока определяет, какие спрайты видимы.


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

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

Добавьте статический фон, и получите стены с потолком

Вот как сцена выглядит в Unity...


А вот как она выглядит через ортогональную камеру!

То есть воспользовавшись массивом спрайтов, системой рендеринга на основе сеток и ортогональной камерой, мы получили минималистичный dungeon crawler в 2.5D. Он очень плоский, а движение ограничено тайлами, между которыми игрок по сути телепортируется. Выглядит всё это не очень.

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


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

С ходьбой всё просто, но как насчёт поворотов? С ними труднее.

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


Префабы это ваши друзья.

Но помните, что мы используем очень точно направленную ортографическую камеру для рендеринга 2D-спрайтов в координатах экрана, поэтому не стоит ожидать, что их можно просто вращать в 3D. Вместо этого мы рендерим каждую структуру в Render Texture и выполняем поворот относительно них.


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

Отрендерим этот вид в ещё одну Render Texture и в UI/кадр, так и получим внешний вид игры. При помощи этой системы можно рендерить и всевозможные объекты, просто помещая их внутрь рендерящихся на экране блоков и создав спрайты для каждого из расстояний в блоках.

Подробнее..

Ремастеринг игрового контента, или как создать 800 единиц контента за семь месяцев

17.05.2021 12:14:39 | Автор: admin

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

Итак, перед нами стояла задача переработки почти всего контента в игре: графического пайплайна, роботов, пушек, визуальных эффектов, карт и т. д. Для этого нужно было внести множество технологических изменений в существующий код и поддержать эти изменения со стороны контента. Этот процесс занимает немало времени, но мы не могли остановить разработку самой игры для внесения всех этих изменений, ведь одно из требований к ремастеру параллельная разработка с основным продуктом. Поэтому в первую очередь нам необходимо было выстроить процесс работы программистов и художников команды War Robots Remastered в рамках основного проекта. И для решения этой проблемы мы пошли по пути итерационной разработки и системы обратной совместимости.

Организация технического процесса работы над ремастером

Разработка War Robots ведется в системе управления версиями git по модели git-flow. В проекте есть основная ветка (develop), в которую по мере готовности вливаются ветки фичей (features). Из основной ветки создаются релизы, которые затем уходят в прод.

В эту схему мы добавили три элемента:

  1. Ветку Remastered-develop, содержащую весь актуальный контент для проекта War Robots Remastered. Мы договорились о том, что эта ветка будет обратно совместимой с актуальной основной веткой игры. Это значит, что в этой ветке будут использоваться те же технологии и все фичи из ветки develop.

  2. Ветки фичей для ремастера: Remastered/feature/branch. В этих ветках ведется работа над фичами, относящимися к контенту и графическому пайплайну ремастера. Они создаются от Remastered-develop и вливаются в нее же.

  3. Ветки фичей для поддержания обратной совместимости feature/branch: в этих ветках ведется работа над технологиями, необходимыми в первую очередь для ремастера, но несовместимыми с основной веткой War Robots. К таким фичам относятся система загрузки ассетов, система управления качеством (Quality Manager) и т. д.

Процесс работы получался следующим:

  1. В Remastered-develop постоянно заливалась ветка develop с актуальным кодом и контентом основного проекта;

  2. Геймдизайнеры, художники и графические программисты, работающие над контентом для ремастера, работали в ветках Remastered/feature/branch;

  3. Все новые технологии, ломающие обратную совместимость, сначала попадали в develop War Robots, а потом уже в develop War Robots Remastered.

Такая схема позволила нам работать над проектом итеративно, не опасаясь того, что War Robots Remastered не будет содержать тех же фичей, что и основная версия игры.

Однако поддержание обратной совместимости имеет свою цену: все фичи, кардинально меняющие технологии на проекте например, система загрузки ассетов, необходимо вносить сначала в основную ветку War Robots, а потом уже в ремастер. Это нетривиальный процесс: раз эти фичи подают в develop, значит, они попадают и в релизы. А чтобы вывести фичу в релиз, ее необходимо согласовать с годовым релиз-планом проекта, выбрать релиз, в который ее сможет забрать и поддержать команда основного продукта, и полноценно протестировать силами QA-отдела. Это увеличивало время разработки ремастера. Однако, как плюс, мы получили то, что на момент релиза War Robots Remastered большинство технологий уже были обкатаны в продакшене, и мы снимали часть технических рисков с запуском проекта.

Как мы переделывали контент для трех качеств ремастера и чем нам помог переход на Unity 2018

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

За свою семилетнюю историю War Robots успела обрасти множеством фичей и еще большим количеством контента. К моменту релиза ремастера в игре существовали:

  • 81 робот;

  • 109 пушек;

  • 83 единиц эквипа: щиты, модули, встроенные абилки;

  • 10 дронов.

Весь контент необходимо было пересобрать в Unity: обновить материалы, анимации, VFX и подготовить все это в трех качествах: Ultra Low Definition (ULD), Low Definition (LD), High Definition (HD). А после пересборки еще и протестировать.

Итого мы имеем: 81 робота, 109 пушек, 83 эквипа, 10 дронов в трех качествах 849 единицы контента.

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

Сборка нового префаба робота из готовых ассетов в Unity может занимать от 1 до 3 дней, его полное тестирование, включая логику и визуал, от 2 до 3 дней. Несложно подсчитать, что на сборку и тестирование одних только роботов у нас ушло бы 11 месяцев, и это без учета рисков в виде задержек со стороны подготовки арта, багов и т.д. Это нас не устраивало, ведь мы хотели завершить проект менее, чем за год.

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

Нам необходимо было автоматизировать процесс сборки контента и облегчить процесс тестирования. К счастью, в момент старта проекта War Robots Remastered в релиз-плане ванильной War Robots был запланирован переход на новую на тот момент версию Unity 2018 LTS. Эта версия Unity добавляла в движок новую технологию Prefab Variants, которой мы и решили воспользоваться.

Наша идея заключалась в том чтобы создать единую базу для каждого робота (пушки, дрона и т. д.), содержащую всю игровую логику и общие компоненты для всех качеств: настройки абилок, звуков, точки креплений пушек и т. д., а также создать префаб-варианты для каждого пресета качества (ULD, LD, HD), которые будут содержать только визуальную составляющую: геометрию, материалы, VFX.

Для примера базовый префаб робота Cossack и его HD-префаб вариант:

Красным выделены объекты, присущие только HD-качеству: скелет, геометрия и материал, а также дополнительные звуковые эффекты.

Схема разделения префабов:

Заметим, что на схеме отображено четыре качества, а не три: Legacy, ULD, LD и HD. Качеством Legacy было принято называть контент из основного проекта War Robots. Также сам механизм разделения на базовые (base) префаб-варианты стал одной из фичей основной игры по поддержке обратной совместимости с War Robots Remastered.

Такая схема построения контента решала две проблемы:

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

  • Сокращается время тестирования единицы контента. В случае с четырьмя независимыми префабами QA необходимо было протестировать и логику, и визуал каждого префаба в отдельности. В новой же схеме тестирование логики проводится только один раз, что сокращает время тестирования единицы контента на 70%.

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

Как уже упоминалось ранее, создание нового префаба робота в War Robots может занимать от 1 до 3 дней работы геймдизайнера и это очень много, когда дело касается более 80 префабов роботов. Однако, благодаря общей базе и префаб-вариантам нам уже не нужно было создавать робота целиком необходимо было лишь заменить ему визуальные компоненты.

Префаб-варианты качеств ULD, LD и HD отличаются между собой всего несколькими элементами:

  • материалом (шейдером и набором текстур);

  • набором LOD-ов;

  • структурой VFX (набором систем частиц).

Замена этих компонентов легко поддается автоматизации.

HD, LD и ULD-вариант робота Griffin. Можно заметить различия в детализации и прорисовке тенейHD, LD и ULD-вариант робота Griffin. Можно заметить различия в детализации и прорисовке тенейHD, LD и ULD-вариант робота CossackHD, LD и ULD-вариант робота Cossack

Однако изначально у нас было только одно качество: Legacy, которое мы получили на выходе работы инструмента по разделению на префаб-варианты. Legacy-качество отличалось от ремастерных качеств, помимо прочего, еще и структурой скелета и анимациями. И если замену материалов, лодов и VFX-ов мы легко могли автоматизировать, то замена скелета анимаций требовала ручных усилий от геймдизайнеров для настройки новых точек креплений пушек, VFX и т. д. В результате мы создали два инструмента для геймдизайнеров: утилиту по портации префаба Legacy в качество ремастера и инструмент для генерации качеств HD, LD, ULD из готового качества ремастера.

Процесс создания ремастер-контента со стороны геймдизайнера теперь стал разделяться на следующие этапы:

  1. Геймдизайнер использует инструмент для портации Legacy-качества в ремастер-качество (обычно в LD). Этот инструмент заменяет скелет, анимации, материалы и VFX.

  2. Геймдизайнер вручную донастраивает ремастер-префаб: указывает новые ссылки на точки креплений пушек, VFX и т. д. На выходе этого этапа мы имеем один полноценный префаб-вариант для ремастер-качества.

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

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

Помимо этого, мы получили инструмент, который позволит нам в дальнейшем интегрировать ассеты любых качеств в пару кликов мыши например, Medium Definition (MD) и Ultra High Definition (UHD).

В текущем виде инструмент по созданию и инспектированию роботов выглядит так:

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

  • настройки освещения рендеров;

  • использование правильных материалов и текстур в префабах;

  • наличие самих префабов в билде;

  • настройки лодирования.

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

Суммарно внедрение процесса автоматизации сборки контента позволило нам собрать и протестировать более 800 единиц контента всего за 7 месяцев, а не порядка трех лет, как было оценено изначально.

Подводя итоги: какие практики хороши при переделке контента для крупного игрового проекта

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

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

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

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

Подробнее..

Обзор технологий трекинга AR маркеры

20.06.2021 10:05:00 | Автор: admin

Всем привет. Меня зовут Дядиченко Григорий, я СТО Foxsys, и я всё ещё люблю трекинг. Продолжим серию статей после долгого перерыва и поговорим про AR маркеры. Какие технологии есть, чем они отличаются, в чём плюсы и минусы каждой на данной момент. Если интересуетесь AR технологиями - доброе пожаловать под кат!

Что чаще всего подразумевают под AR маркером?

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

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

Применение AR меток и общие рекомендации

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

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

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

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

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

Целевые устройства важны, так как у многих различаются камеры и датчики, что так же влияет на трекинг. Для теста того же ARFoundarion (ARCore) или для сборки B2B проекта на аркоре, чтобы не было никаких вопросов с лицензией, по соотношению цена-качество я выделил для себя Redmi Note серию, нужно только внимательно смотреть, на каком устройстве есть поддержка аркор. Самый недорогой с достаточно качественной работой ARCore это восьмой ноут. Он сравнительно недорогой, но при этом проверять на нём качество трекинга данной технологии самое то. А так всё зависит от множества факторов. Бюджет, требования к системе, ограничения в плане покупке лицензий, какими устройствами обладает целевое устройства или бюджет на устройства для обустройства конкретной локации, и так далее.

Что-то мы глубоко ушли в нюансы работы с технологиями. Поговорим теперь про применение.

В рекламе

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

В благотворительности

По сути, очень похоже на рекламу, так как средства примерно такие же, только с целью осветить какую-то проблему.

В музеях и выставках

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

В целом применений технологии очень много, и их полное перечисление само по себе растянется на несколько статей. Если даже отойти от телефонов метки используют в навигации дронов в пространстве, как систему рекалибровки и релокализации, в робототехнике, в инженерных задачах. К любой технологии трекинга можно придумать множество применений, так как определение позиции объекта в пространстве позволяет сделать много очень интересных взаимодействий. Когда-то даже был очень интересный концепт оживления настольных игр на Playstaition с помощью Eye Toy. Но перейдём же к самим технологиям. Да в основном я буду разбирать те технологии, с которыми я работал и запускал коммерческие проекты. Итак, приступим!

ARUCo (OpenCV)

Цена: Бесплатное

Поддерживаемые платформы: Android/IOS/WebGL/Win/Mac/Linux/Hololens

Совместимость с Unity: есть

Поддержка устройств: Широкая

https://docs.opencv.org/4.5.2/d5/dae/tutorial_aruco_detection.html

Если вы когда-то видели такие странные квадраты - это Аруко. Это технология для хардкорщиков. Из коробки для AR работает она откровенно плохо. Очень много шума. Но всегда можно реализовать обвязку которая решит эти проблемы. Допустим хорошо тречится так называемый аруко борд. Плюс если реализовать фильтр Маджевика, то тогда и jitter (тряска) будет не так сильно мешать. Основные плюсы: бесплатное (правда на допил уйдёт вероятно больше бюджета), умеет одновременно отслеживать очень большое число маркеров. Минусы: нельзя использовать изображения, кроме квадратов, что подходит далеко не для всех применений.

Vuforia

Цена: зависит от применения

Поддерживаемые платформы: IOS/Android/Win/Hololens

Совместимость с Unity: есть

Поддержка устройств: широкая

https://www.ptc.com/en/products/vuforia

Какой разговор про AR метки без Вуфории. Можно перейти правда сразу к главному недостатку технологии. Правильно купить её из РФ довольно сложно, и стоит это очень дорого. В остальном отличная технология. Имеет определение качества в вебе (правда ему стоит доверять с оговорками, так как он не определяет те же самые симметрии нормально). Работает очень хорошо, имеет интересную технологию вумарок. Недостатком с точки зрения долго развиваемого продукта является только тот факт, что у команды вуфории нет привычки к обратной совместимости. То есть каждая большая (а иногда и не очень) новая версия сдк ломает проект чуть ли не целиком, и требует переписать большую часть модулей. Что означает, что в долгосрочной перспективе решение требует поддержки, и поддержка будет дороже других технологий.

EasyAR

Цена: $1400 на приложение /$39 в месяц на приложение

Поддерживаемые платформы: IOS/Android/Win/Mac

Совместимость с Unity: есть

Поддержка устройств: широкая

https://www.easyar.com/

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

XZIMG

Цена: бесплатно/2100 для использования в html5

Поддерживаемые платформы: IOS/Android/Win/Mac/Html5

Совместимость с Unity: есть

Поддержка устройств: широкая

https://www.xzimg.com/Products?nav=product-XAV

Одно из преимуществ технологии, одна из немногих коробочных технологий, работающих с WebGL и вебом в целом https://www.xzimg.com/Demo/AV. Работает неплохо в целом, но меньше спектр подходящих маркеров по сравнению с конкурентами. Бесплатное для коммерческого использования везде, кроме веба.

ARFoundation (aka ARCore/ARkit)

Цена: Бесплатно

Поддерживаемые платформы: IOS/Android

Совместимость с Unity: есть

Поддержка устройств: на Android порядка 20% устройств из общего числа моделей, IOS устройства с процессором A9+

Отличная технология для мобильных устройств. Бесплатная, хорошо работает и за счёт аппаратной поддержки тратит не так много ресурсов телефона. А недостатки, как всегда всё те же. Ограниченная поддержка на устройствах у пользователей. Соотношение постепенно растёт и в пределах нескольких лет возможно устройства будут у большего числа людей. Но пока по оценке данного ресурса (которая по методологии вызывает недоверие в плане точности) https://www.aronplatform.com/mobile-ar-penetration-2020/ AR supported устройства есть у 41.6% держателей смартфонов. Если посчитать точнее, думаю это число будет меньше. Но терять 60% аудитории в рамках акции или некоей платформы из-за технологии AR это должно быть весьма обоснованным решением.

SparkAR

Цена: Бесплатно

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: нет, но можно сделать ссылку в приложении

Поддержка устройств: широкая

https://sparkar.facebook.com/ar-studio/

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

Достойные упоминания

Это были не все технологии маркерного трекинга, так же на рынке присутствует Wikitude, ARmedia, MAXST - но с данными технологиями мне не довелось поработать, поэтому ничего не могу сказать про их качество работы, и какие-то тонкие моменты.

Спасибо за внимание! Если вам была интересна тема, то, когда у меня в следующий раз появится время постараюсь написать обзор на тему Hand Tracking или чего-нибудь её.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru