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

Геймдев

Подкаст Хочу в геймдев 22 текстовая версия

26.03.2021 10:08:08 | Автор: admin

Двадцать второй выпуск подкаста - мы говорим о профессии 3D-художник (3D artist). Чем занимается 3D-художник, каковы особенности этой профессии? Что нужно делать, чтобы стать им? Об этом и не только вы узнаете в 22-м выпуске!

Гость подкаста: Алексей Назаров - Lead 3D artist и Team leader компании 1518 Studios.

Ведущие выпуска

Вячеслав Уточкин Олег Доброштан

Путь эксперта

Алексей: Мой путь начался с 2001 год. 3D я занимаюсь уже около 20 лет. Первый раз я познакомился с 3D Max, когда поступал в университет. С этого момента 3D стало частью моей жизни. Во время учебы в университете, у меня появилась возможность устроиться в местную Ульяновскую студию, которая тогда работала над ММОРПГ. В то время кстати многие работали над ММОРПГ по непонятной для меня причине. На этом проекте я получил золотой билет, потому что у меня было очень много практики. Команда была из 5 человек и тем не менее нам удалось достичь определенного результата. В 2008 году команду распустили и в тот же год мне написал на почту Максим Михеенко с предложением сделать пропсы и персонажей. С этого началось наше знакомство и продолжается до сих пор. До 2017 года я работал в качестве 3D-художника, а потом начал работать в качестве Team Lead и активно развиваюсь в этом направлении.

Чем занимается 3D-художник? Как выглядит рабочий день?

С проверки почты на новые задания. Хотя я могу написать задание еще и в скайпе. В задании содержится: имя таска, которое необходимо учитывать во всех файлах имя сцены, имя объектов, имя текстур, информация о количествах полигонов, разрешении текстур, могут быть дополнительно вводные как отдавать проект клиенту. После изучения задания художник может задать вопросы. Затем он приступает к выполнению задания. Можно начать с блэкаута, либо со скульптинга или хайполи. Представьте себе часы напольные: высокие, с грузами, с циферблатом. Художнику присылают задание с концептами и видами в перспективе, чтобы можно было максимально четко передать объект в 3D. В данном случае художнику блэкаут не нужен. Художник может сразу переходить к хайполи. Когда художник заканчивает скульп, он показывает его лиду, чтобы лид проверил все ли корректно и попал ли художник в стиль и дать фидбек. Иногда художники обижаются на фидбеки от лидов, поэтому задача лида найти корректный стиль общения. В нашей команде характер общения позитивный и очень вежливый.

Немного о разнице в задачах у 2D и 3D. У 3D-художника в отличие от 2D зачастую стоят конкретные задачи. Если 2D-художник может пустить в вольный поток идей, то у 3D-художника есть четкие референсы как просит клиент.

Про резюме, собеседование, портфолио и тестовые задания

Разумеется для 3D-художника самое важное - наличие портфолио. Хорошим плюсом будет наличие художественного образования: курсы или вуз не важно, но желательно понимать основы. Художник с образованием может заметить то, на что без образования художник получит фидбек. Поэтому если есть возможность, то лучше пройти хотя бы курсы. Тем более что в наши дни возможности огромные. То же касается и курсов по 3D: Scream School, XYZ, Smirnov School. Эти школы дают базис. К тому же нарабатываете портфолио во время учебы.

Что касается возрастных ограничений, то их нет. Художнику может быть хоть 65 лет, но если у него есть запал и желание развиваться, то мы его возьмем. Но конечно, таких случаев крайне мало. Игровая индустрия - индустрия молодых. Даже я уже играю в 3-4 крупных проекта в год и то в качестве анализа.

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

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

Зарплаты 3D-художников зависят от скила самого художника, единственное аниматоры получают больше, потому что их меньше. А вот у 3D и 2D художников примерно один уровень. Что касается России то зарплаты варьируются от 25000 до 150000 рублей в зависимости от позиции и региона.

Советы и рекомендации от эксперта

Художникам (всем) надо понять, что фидбеки дают не для того, чтобы как-то задеть или обидеть, а для того, чтобы сделать работу правильно.

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

Совет от Алексея к прочтению книг: М.Михеенко Миллионы миллиардов. Как попасть в игровую индустрию, Дж.Шелл Геймдизайн, М. Кадиков Проектирование виртуальных миров, Дж. Шрейер Кровь, пот и пиксели.

Полезные ссылки:

Личная страничка Олега Доброштана, куда можно написать вопросы для следующих выпусков: https://www.facebook.com/oleg.dobroshtan

Сайт подкаста, где можно послушать все выпуски: https://www.facebook.com/oleg.dobroshtan

Группа ВКонтакте, где публикуются новые выпуски: https://vk.com/hsbi_games

Контакт Вячеслава Уточкина в телеграмм: @viacheslavnu

Вакансии 101XP: https://corp.101xp.com/vacancies/

1518 Studios: https://1518studios.com/contact/

Полезные ресурсы для разработчиков игр: https://hsbi.hse.ru/programs/vocational_retraining/menedzhment-igrovykh-internet-proektov/useful-resources/

Программа повышения квалификации Геймдев по-американски: опыт Silicon Beach & Silicon Valley от Максим Михеенко: https://usagame.hse.ru/

Онлайн-День открытых дверей по программе Менеджмент игровых проектов 31.03.21: https://games.hse.ru/openday/

Список литературы: https://docs.google.com/document/d/1O6QpquHAJVCodKNVGPjQZ4kyoDEVgK16uqY03sgIMC0/edit?usp=sharing

Подробнее..

Подкаст Хочу в геймдев 23 текстовая версия

17.04.2021 00:07:00 | Автор: admin

Двадцать третий выпуск - мы говорим о профессии Левел-дизайнер. Чем занимается Левел-дизайнер, каковы особенности этой профессии? Какие навыки нужны левел-дизайнеру? Об этом и не только вы узнаете в 23-м выпуске!

Гость подкаста: Михаил Горанский - Level-дизайнер Mail.ru Group..

Ведущие выпуска

Вячеслав Уточкин Константин Сахнов

Гость подкаста

Михаил Горанский

Путь эксперта

Изначально Михаил шел работать в качестве геймдизайнера. И однажды он стал вести проект, где было много работы level-дизайнера и это стало для него самым интересным. Lead GD заметил это и когда ему понадобился level-дизайнер в проекте в Mail.ru Group, он пригласил Михаила. И вот уже три года он работает там.

Задачи level-дизайнера

Создание уровней, но при этом эксперт должен понимать математику, основы геймдизайна. К тому же у многих работодателей требования различаются: одни требуют навыки работы в Unity, другие навыки художника, третьи программирование и т.д. У тех же Ubisoft level-дизайнеры собирают локации, в то время как на проекте, в котором участвует Михаил level-дизайнеры уровень собирает на white-боксах, но при этом отвечает за расстановку персонажей и пр на локации, собирают триггеры, запускают все экшены через инструментарий, кат-сцены и пр.

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

Работа

Вячеслав: Какие вопросы могут спросить на собеседовании?

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

Михаил: Звуки тоже являются частью level-дизайна. В зависимости от того куда игрок идет звуковое сопровождение меняется, а это как раз часть создания уровня.

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

Вячеслав: какие тестовые задания могут быть для level-дизайнера?

Константин: мы говорим примерно: Вот есть игра в стиме. Спроектируйте ивент/локацию. А далее смотрим как и с помощью какого инструментария человек это сделал.

Михаил: дополню от себя. Есть хороший пример от крупных студий, когда они дают задание сделать макет уровня (документацию), который подходит под описание от компании. Например, сделать уровень для шутера от первого лица про войну в Ираке. Человек делает и отправляет все эту кипу документации. Или в России Owlcat давали задание собрать часть уровня в фентезийном мире.

Вячеслав: а что с зарплатами? Если посмотреть опросы, то средняя зарплата джуниора level-дизайнера выше, чем геймдизайнера. Почему так?

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

Константин: Согласен. А есть еще отдельная категория level-дизайнеров - дизайнер Match-3. Они получают на порядок выше всех других.

Константин: Михаил, а какие level-дизайнеры сейчас более востребованы? Match-3 или 3D, казуалки или инди?

Михаил: Сложный вопрос, с точки зрения того, что я три года уже работаю в одной компании. И поэтому не очень хорошо знаю рынок. Больше всего конечно будет предложений на Match-3 и фермы потому что его банально сейчас больше всего. Но есть перекос в различные изометрические проекты инди и полуинди. Стало появляться много хорошего мобильного мидкора. И всегда можно попасть в 3D проекты. Сейчас они стали появляться.

Что нужно знать и уметь, чтобы стать level-дизайнером?

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

О чем рассказывает Михаил на курсе

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

Советы от эксперта

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

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

Полезные ссылки

Питч игровых проектов регистрация зрителей

Список игровых компаний

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

Сайт подкаста, где можно послушать все выпуски

Группа ВКонтакте, где публикуются новые выпуски

Контакт Вячеслава Уточкина в телеграмм: @viacheslavnu

Вакансии 101XP

Полезные ресурсы для разработчиков игр

Программа профессиональной переподготовки Менеджмент игровых проектов

Дистанционная программа Основы создания игр

Книга "Хочу в геймдев! Основы игровой разработки для начинающих"

Настольная игра Game Dev Sim (симулятор игровой индустрии)

Подробнее..

Подкаст Хочу в геймдев 24 текстовая версия

06.05.2021 10:07:53 | Автор: admin

Двадцать четвертый выпуск - мы говорим о профессии Трафик-менеджер в игровой индустрии. Что это за профессия, насколько она востребована? Чем занимается Трафик-менеджер? Что нужно, чтобы устроиться на такую работу? Об этом и не только вы узнаете в 24-м выпуске! Гости подкаста: Алексей Смолдырев - руководит маркетинговым направлением 101XP и Максим Воробьёв - Digital Director CityAds Media.

Ведущие выпуска

Вячеслав Уточкин Олег Доброштан Константин Сахнов

Путь экспертов

Алексей: Я начал свой путь в 2009 году в интернет-маркетинге. В 2014 году я узнал, что можно рекламировать игры и тогда же начал заниматься арбитражем трафика. В 2016 году устроился в игровую компанию, сначала на позицию интернет-маркетолога. В 2019 году занялся мобильным трафиком, затем на время ушел в онлайн-кинотеатры и в 2020 году снова вернулся в игры в 101XP.

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

Рабочий день трафик-менеджера?

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

Глобальная цель трафик-менеджера

Принести компании деньги. Он приводит новых игроков, которые будут играть, смотреть рекламу, совершать внутриигровые покупки, делает свою core-аудиторию.

Каждый специалист в компании делает одну из двух основных задач:

  1. делает классный продукт

  2. привлекает игроков в этот продукт

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

Основные метрики для нас это:

  1. Окупаемость трафика (ROI). Смотрим сколько потратили на рекламу и сколько заработали с нее. И смотрим сколько принесла каждая когорта в определенные дни.

  2. Если приложение только запускается, то необходимо проверить Retention первого дня. И потом уже составлять метрики.

Различия между трафиком мобильных и PC-игр

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

Задача трафик-менеджера приводит пользователя, который удовлетворяет определенным требованиям. А этим требования спускаются с продуктовой части. Пример: средний пользователь в игре приносит 20р, следовательно приводить пользователя нужно за 20 рублей или дешевле. Когда пользователь начнет приносить 40р, то планка поднимается и приводить нужно уже пользователя за 40р и ниже. Когда растет ставка по пользователю, объемы трафика растут.

Есть в мобилках такая особенность-они используют в качестве систем аналитики Adjust, Appsflyer, которые позволяют смотреть все, у ПК таких систем нет. Хотя Steam запустил такое расширение.

Обязанности трафик-менеджера.

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

Основные обязанности это: оптимизация запущенных рекламных каналов, аналитика работы, разработка новых каналов, формулирование гипотез.

Сложно ли стать трафик-менеджером?

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

Что будет, если неэффективно потратить деньги на трафик?

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

Как принимают на эту работу Junior-специалистов, на какие навыки обращают внимание?

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

Как наработать портфолио трафик-менеджеру?

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

Про зарплаты и рост специалиста.

Рассмотрим по Москве. Джун это в районе 40-60т.р., мидл уже в районе 60-100т.р., сеньор 100-150т.р., лид 150-200 т.р., хэд 200-250 т.р., далее уже директора и там уже 300+тысяч. Развиваться есть куда. И один из плюсов позиции, что ты не привязан к офису.

Как разобраться в рекламном кабинете и насколько это сложно?

Главное не пугаться. Несмотря на огромное количество информации там, вы не будете использовать все. Вам нужно разобраться в том, что требуется конкретно вам. У некоторых есть статьи или курсы, как, например, Facebook и их курс Blueprint. И конечно гугл.

Советы экспертов

Максим: Тестировать, тестировать и еще раз тестировать! Пусть ROI растет.

Алексей: Желаю, чтобы люди поняли, что им нравится. И чтобы работа приносила удовольствие.

Константин: А я бы добавил слова Нерзула: Не ошибается тот, кто ничего не делает.

Полезные ссылки:

Средние показатели игр по жанрам: https://hsbi.hse.ru/programs/vocational_retraining/menedzhment-igrovykh-internet-proektov/w-average-game-pbg/

Список игровых компаний: https://hsbi.hse.ru/programs/vocational_retraining/menedzhment-igrovykh-internet-proektov/v-domestic-companies/

Личная страничка Олега Доброштана, куда можно написать вопросы для следующих выпусков: https://www.facebook.com/oleg.dobroshtan

Сайт подкаста, где можно послушать все выпуски: http://podcast.hsbi.ru/

Группа ВКонтакте, где публикуются новые выпуски: https://vk.com/hsbi_games

Контакт Вячеслава Уточкина в телеграмм: @viacheslavnu

Вакансии 101XP: https://corp.101xp.com/vacancies/

Полезные ресурсы для разработчиков игр: https://hsbi.hse.ru/programs/vocational_retraining/menedzhment-igrovykh-internet-proektov/useful-resources/

Программа профессиональной переподготовки Менеджмент игровых проектов: http://game.hsbi.ru/

Дистанционная программа Основы создания игр: http://egame.hsbi.ru/

Книга "Хочу в геймдев! Основы игровой разработки для начинающих": https://book24.ru/product/khochu-v-geymdev-osnovy-igrovoy-razrabotki-dlya-nachinayushchikh-5773179/

Настольная игра Game Dev Sim (симулятор игровой индустрии): https://gamedevsim.edinorog.org/

Подробнее..

Подкаст Хочу в геймдев 2 часть 2 текстовая версия

07.05.2021 10:12:10 | Автор: admin

Второй выпуск подкаста был разделен на несколько частей. Гость выпуска - Константин Сахнов. Мы говорим о том, что представляет собой профессия "Геймдизайнер", какие ключевые навыки ему необходимы, даем пример ТЗ, отвечаем на вопросы и многое другое!

Финальная часть!

Ведущие выпуска

Вячеслав Уточкин Олег Доброштан Александр Мураш

Приглашенный гость

Константин Сахнов

Разница между инди-командой и корпорацией

Для начала надо добавить третью категорию - это студии 60-200 человек, такой некий средний сегмент. Что касается инди-команд, то они более мобильны. У них не четкой структуры, проложенных рельсов. Если на рынке выпускается игра конкурент, то они могут изменить концепт, сделать новый USP, добавить другую фичу и выйти на рынок. Корпорации конечно не могут себе этого позволить. У них все поставлено на поток, они менее мобильны. Почему так? Потому что в корпорациях сидят люди с опытом, которые сталкивались с множество проблем и понимают, что главное выживание и поэтому нужно минимизировать риски. А все что касается непонятности, не прогрозируемости - это очень большие риски, а значит закрывают деньгами, процессами и т.д. Они нанимают больше кадры на резерв. Они рискуют миллиардами, а инди нет.

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

Есть ли какая-то вершина карьеры геймдизайнера? Как нужно действовать для ее достижения?

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

Поэтому нужно понять, что именно хочет человек. А единой вершины нет.

Зачем вообще идти в геймдев?

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

В России доминирует рынок мобильных игр, а как быть тем, кто хочет разрабатывать PC- и консольные игры?

Для начала попробовать хотя бы разрабатывать игры для мобильного рынка. Рынок растет, проекты делают новые и интересные.Что же касается ПК, то в России это конечно сложно. У нас это невыгодно. Специалистов мало, учить никто не хочет, нужно уже уметь. Разработка длится не полгода, а 2-3 года. И это дорого. Поэтому у нас этот сегмент очень мал.

PC-игры - это "отживающий" тренд?

Нет. ПК игр стало меньше потому что просто мобильный рынок вырос. А так он на высоком уровне. Если посмотреть на финансы, то по отчетам аналитиков, рынок ПК растет. Просто рынок стал уже.

Вопрос от слушателей: как вырваться из российского сегмента игровой индустрии в зарубежный? Можно ли сделать это без опыта?

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

Любимый геймдизайнер Константина

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

Еще мне нравится такой продюсер, креативный директор, нарративный геймдизайнер Крис Мэдсен. Один из разработчиков Варкрафта.

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

Любимые фильмы, книги и игры Константина

World of Warcraft, Герои Меча и Магии, мобильные батлеры, Darkest Dungeon. Из фильмов последних Холодное сердце, как странно бы не звучало, фэнтези, фантастику, фильмы по комиксам начал смотреть недавно. Что касается книг, то это 1984, Костя большой фанат антиутопии. Еще Атлант расправил плечи, книги по здоровью и физиологии и все, что связано с медициной.

Альянс или Орда?

Жизнь за Нерзула!

Полезные ссылки:

День открытых дверей на программу Менеджмент игровых проектов 18 мая:https://games.hse.ru/openday/

Онлайн-интенсив Хочу в геймдев 19-21 мая:https://hsbi.hse.ru/events/raznoe/besplatnyy-onlayn-intensiv...

Список игровых компаний:https://hsbi.hse.ru/programs/vocational_retraining/menedzhme...

Личная страничка Олега Доброштана, куда можно написать вопросы для следующих выпусков:https://www.facebook.com/oleg.dobroshtan

Сайт подкаста, где можно послушать все выпуски:http://podcast.hsbi.ru/

Группа ВКонтакте, где публикуются новые выпуски:https://vk.com/hsbi_games

Контакт Вячеслава Уточкина в телеграмм: @viacheslavnu

Вакансии 101XP:https://corp.101xp.com/vacancies/

Полезные ресурсы для разработчиков игр:https://hsbi.hse.ru/programs/vocational_retraining/menedzhme...

Программа профессиональной переподготовки Менеджмент игровых проектов:http://game.hsbi.ru/

Дистанционная программа Основы создания игр:http://egame.hsbi.ru/

Книга "Хочу в геймдев! Основы игровой разработки для начинающих":https://book24.ru/product/khochu-v-geymdev-osnovy-igrovoy-ra...

Настольная игра Game Dev Sim (симулятор игровой индустрии):https://gamedevsim.edinorog.org/

Подробнее..

Комьюнити менеджмент в GameDev это не про мемы, а про сервис, комфорт и вовлеченность

08.06.2021 20:05:53 | Автор: admin

Когда мы готовили it-конференцию DUMP, то предполагали, что секция о разработке игр вызовет интерес. Но то, что мест в зале не хватит, было неожиданностью. Мы решили продолжить обсуждение трендовых тем и пообщались с представителями игровой индустрии о комьюнити менеджменте - профессии, которая в России только набирает обороты. О том, как и для чего сочетать социологию, психологию и маркетинг - в нашем интервью c Александром Мартом, лид КМом, проработавшим в Targem Games без малого 6 лет.

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

Комьюнити-менеджмент это работа с пользователями, объединенными общими интересами, целями или болями.

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

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

Почему они нам необходимы? Есть группа игроков, которая помимо того, что находится на игре и получает эмоции от игры, за её пределами хочет сохранять эти эмоции, приумножать их. У них есть энергия на это, общие цели, привязанность к проекту. И чтобы энергию направить в мирное русло и построить более эффективную связь с игрой, повлиять на то, чтобы у игроков сохранилась желание возвращаться в игру и так далее, приходит КМ.

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

Откуда приходят в комьюнити менеджмент? Эти ребята чаще всего маркетологи?

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

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

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

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

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

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

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

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

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

Есть какой-то топ инструментов, который позволяет проблемы пользователей вытаскивать на поверхность?

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

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

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

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

Но мало быть игроком в своем проекте и понимать к какому типу относятся те или иные пользователи. Также важно понимать интересы аудитории в реальной жизни, говорить с ней на одном языке и на привычной площадке. Сейчас будет пример из мобильных игр. Там есть очень серьезная проблема в том, что многие игроки в социальных играх в кланы объединяются не в Telegram, Discord, VK или Facebook, а в WhatsApp или Viber. Это связано с возрастной категории пользователей. То есть когда играют мужики 30+ в каких-то условных викингах, им привычно пользоваться теми инструментами, которые у него всегда под рукой. Его подсадили на WhatsApp, он будет пользоваться WhatsApp. Туда приходит молодой КМ лет 20 и говорит, чтобы все пошли в Telegram. Игроки говорят: А не пошёл бы ты дальше. Поэтому иногда КМ-у нужно не только знать своего пользователя, но и найти его в привычной среде обитания.

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

Расскажи, какие KPI используются для оценки работы менеджера?

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

Поэтому часто образуется тандем из количественных показателей (сколько раз он отвечает, насколько активное сообщество, сколько людей в этом сообществе) и качественных (аналитика этих цифр, оценка специалиста, трактовка цифр, показатели удовлетворенности пользователей, выборочная проверка действий на форс-мажоры). Большинство действий КМа сложно оценить с количественной точки зрения. Это не СММ, которому ты можешь вменять количество показов или прямое количество лайков, это не таргетолог, с которого ты можешь спрашивать за переходы на сайт и так далее. Очень много действий, которые совершают КМы направлены на качественные вещи: как игроки себя ощущают, насколько полную они получают информацию. И тут возникает проблема: как отказаться от количественных показателей и метрик для оценки специалиста ? Надо искать баланс. Если вы не готовы мириться с мыслью, что отчёт о проделанной работе будет состоять наполовину из текстового вывода, внутренних ощущений КМ, вам, возможно, нужен не КМ. Вы не готовы так работать с настроением пользователей, его ощущениями и укреплять образующиеся между игроками связи и игрой.

Есть советы для тех, кто хочет стать комьюнити менеджером?

В первую очередь быть просто адекватными. Как бы абсурдно это не звучало. Осенью мы собеседовали порядка 15 человек, и только 2 из них прошли по критерию адекватности. Чего мы только не насмотрелись, хотя общие пожелания были минимальными: нам просто нужен был человек, которого мы всему научим. И таких вакансий на младшие позиции в геймдеве бывает достаточно много. Курсы, кстати, не могут являться гарантом трудоустройства, ни в одной компании, с которой я знаком, сейчас не практикуется просмотр курсов, на которых ты бывал. Корочки могут служить только для вашего собственного развития, но не бонусом при приёме на работу.

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

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

Подробнее..

Как я написал браузерный 3D FPS шутер на Three.js, Vue и Blender

07.05.2021 02:13:27 | Автор: admin
Стартовый экран игрыСтартовый экран игры

Мотивация

На пути каждого коммерческого разработчика (не только кодеров, но, знаю, у дизайнеров, например, также) рано или поздно встречаются топкие-болотистые участки, унылые мрачные места, блуждая по которым можно вообще забрести в мертвую пустыню профессионального выгорания и/или даже к психотерапевту на прием за таблетками. Работодатели-бизнес очевидно задействует ваши наиболее развитые скилы, выжимая по максимуму, стек большинства вакансий оккупирован одними и теми же энтерпрайз-инструментами, кажется, не для всех случаев самыми удачными, удобными и интересными, и вы понимаете что вам придется именно усугублять разгребать тонну такого легаси Часто отношения в команде складываются для вас не лучшим образом, и вы не получаете настоящего понимания и отдачи, драйва от коллег Умение тащить себя по-мюнхаузеновски за волосы, снова влюбляться в технологии, увлекаться чем-то новым [вообще и/или для себя, может быть смежной областью], имхо, не просто является важным качеством профессионала, но, на самом деле, помогает разработчику выжить в капитализме, оставаясь не только внешне востребованным, конкурентоспособным с наступающей на пятки молодежи, но, прежде всего, давая энергию и движение изнутри. Иногда приходится слышать что-нибудь вроде: а вот мой бывший говорил, что если бы можно было не кодить, он бы не кодил!. Да и нынешняя молодежь осознала что в сегодняшней ситуации честно и нормально зарабатывать можно только в айти, и уже стоят толпою на пороге HR-отдела... Не знаю, мне нравилось кодить с детства, а кодить хочется что-нибудь если не полезное, то хотя бы интересное. Короче, я далеко не геймер, но в моей жизни было несколько коротких периодов когда я позорно загамывал. Да само увлечение компьютерами в детстве началось, конечно же, с игр. Я помню как в девяностые в город завезли Спектрумы. Есть тогда было часто практически нечего, но отец все-таки взял последние деньги из заначки, пошел, отстоял невиданно огромную очередь и приобрел нам с братом нашу первую чудо-машину. Мы подключали его через шнур с разъемами СГ-5 к черно-белому телевизору Рекорд, картинка тряслась и моргала, игры нужно было терпеливо загружать в оперативную память со старенького кассетного магнитофона [до сих пор слышу ядовитые звуки загрузки], часто переживая неудачи... Несмотря на то что ранние программисты и дизайнеры умудрялись помещать с помощью своего кода в 48 килобайт оперативной памяти целые миры с потрясающим геймплеем, мне быстро надоело играть и я увлекся программированием на Бейсике)), рисовал спрайтовую графику (и векторная трехмерная тогда тоже уже была, мы даже купили сложную книжку), писал простую музыку в редакторе... Так вот, некоторое время назад мне опять все надоело, была пандемийная зима и на велике не покататься, рок-группа не репетировала Я почитал форумы и установил себе несколько более-менее свежих популярных игр, сделанных на Unity или Unreal Engine, очевидно. Мне нравятся РПГ-открытые миры-выживалки, вот это все... После работы я стал каждый вечер погружаться в виртуальные миры и рубиться-качаться, но хватило меня ненадолго. Игры все похожи по механикам, однообразный геймплей размазан по небольшому сюжету на кучу похожих заданий с бесконечными боями Но, самое смешное, это реально безбожно лагает в важных механиках. Лагают коммерческие продукты которые продают за деньги А любой баг, имхо, это сильное разочарование он мгновенно выносит из виртуальной среды, цифровой сказки в реальный мир Конечно, отличная графика, очень круто нарисовано. Но, утрируя, я понял что все эти поделки на энтерпрайзных движках, по сути даже не кодят. Их собирают менеджеры и дизайнеры, просто играясь с цветом кубиков, но сами кубики, при этом практически не меняются... Вообщем, когда стало совсем скучно, я подумал что а я ведь тоже так могу, да прямо в браузере на богомерзком непредназначенным для экономии памяти серьезного программирования джаваскрипте. Решил наконец полностью соответствовать тому что все время с умным видом повторяю сыну: уметь делать игры, намного интереснее чем в них играть. Одним словом, я задался целью написать свой кастомный браузерный FPS-шутер на открытых технологиях.

Итак, на данный момент, первый результат по этой долгоиграющей таски на самого себя можно тестить: http://robot-game.ru/

Стек и архитектура

Вполне может быть, что я не вкурсе чего-то (ммм на ум приходит что-нибудь вроде quakejs и WebAssembly), но, с основной технологией было, походу, особо без вариантов. Библиотека Three.js давно привлекала мое внимание. Кроме того, в реальной коммерческой практике, несколько раз, но уже приходилось сталкиваться с заказами на разработку с ее использованием. На ней я сделал собственно саму игру.

Очевидно, что нужно что-то вокруг для простого интерфейса пользователя: шкал, текстовых сообщений, инструкций, контролов настроек, вот этого всего. Я решил поленился, не усложнять себе жизнь и использовать любимый фреймворк Vue 2, хотя, надо было, конечно, писать на свежем, похожем по дизайну и еще более прогрессивном по сути молниеносном Svelte. Но так как хорошенько разобраться предстояло, прежде всего, с Three, думаю, это было правильное решение. Хорошо знакомый и предсказуемый, лаконичный, изящный, удобный и эффективный Vue, позволил практически не тратить время на внешний пользовательский интерфейс.

Когда-то давно я работал дизайнером на винде и достаточно бойко рисовал 2D в Иллюстраторе, но навыков 3D у меня никаких не было. А вот в процессе создания шутера пришлось пойти, скачать и установить одним кликом на свой нынешний Linux Blender. Я быстро научился рисовать с помощью примитивов мир, отдельные объекты, и даже научился делать UV-развертки на них. Но! В целях простоты, скорости работы и оптимизации объема ассетов в моей нынешней реализации не используются текстурные развертки. Я просто подгружаю чистые легковесные бинарные glTF: .glb-файлы и натягиваю на них всего несколько вариантов нескольких текстур уже в джаваскрипте. Это приводит к тому что текстуры на объектах искажаются в разных плоскостях, но на основном бетоне для стен, смотрится даже прикольно, такой разный, рваный ритм. Кроме того, сейчас персонажи не анимируются пока не было времени изучить скелетную анимацию. Одной из основных целей написания этой статьи является желание найти (по знакомым не получилось) специалиста который поможет довести проект до красоты (очень хочется) и согласится добавить совсем немного анимаций на мои .glb (об условиях договоримся). Тогда враги, будут погружаться в виде glTF со встраиванием: .gltf-файлов со встроенными текстурами и анимациями. Сейчас уже есть два вида врагов: ползающие-прыгающие наземные дроны-пауки и их летающая версия. Первых нужно научить шевелить лапками при движении и подбирать их в прыжке, а вторым добавить вращение лопастей.

Модель дрона-паука в BlenderМодель дрона-паука в Blender

Для того чтобы игру нельзя было тупо-легко прочитить через браузерное хранилище я добавил простенький бэкенд на Express с облачной MongoDB. Он хранит в базе данные о прогрессе пользователя по токену, который на фронте записывается в хранилище. Хотелось сделать не просто FPS-шутер, а привнести в геймплей элементы РПГ. Например, в нынешней реализации мир делиться на пять больших уровней-локаций между которыми можно перемещаться через перезагрузку. При желании локации можно быстро дорисовывать из уже имеющихся и добавлять в игру, указывая только двери входа и выхода, стартовую и конечную координату, хорошее направление камеры для них (при переходе живого персонажа через дверь текущее направление сохраняется-переносится). На каждом уровне есть только одна формальная цель найти и подобрать пропуск к двери на следующий уровень. Пропуски не теряются при проигрыше на локации (только при выборе перехода на стартовый уровень после выигрыша на последнем пятом). А вот враги и полезные предметы цветы и бутылки при переходе между локациями, проигрыше или перезагрузке страницы пока выставляются заново согласно основной glb-модели одновременно и схеме, и визуальной клетке локации об этом дальше. И тут вот первое важное про архитектуру: мой фронтенд это совсем примитивное SPA. Vue, например, ни для чего не нужен роутер. Вероятно, я получу негативную реакцию некоторых продвинутых читателей, после того, как сообщу что потратил кучу времени для того чтобы попробовать организовать перезагрузку-очистку сцены внутри системы и пока с самым провальным результатом. Вот к такой спорной мысли я пришел в процессе своих экспериментов: самый эффективный, простой, даже, в этой ситуации, правильный и при этом, конечно же, топорный подход, это нативный форс-релоад после того как мы сохраняем или обнуляем данные пользователя на бэкенде:

window.location.reload(true);

А потом просто дадада считываем их обратно )) и строим всю сцену заново, с чистого листа, так сказать. Тут, конечно, можно было бы улучшить прокидывать пользователя через хранилище вместо того чтобы ожидать разрешения запроса, но это не критично, в данном случае. Небольшое количество оптимизированных текстур (меньше полтора мегабайта сейчас), сильно компрессированного аудио (MP3, понятно: 44100Гц 16 бит, но с сильным сжатием 128 кбит/с меньше полтора мегабайта все вместе сейчас), основная модель-локация весящая около 100Кб и модели отдельных объектов каждая еще меньше... Я добился того что переход между локациями полная перезагрузка мира занимает вполне приемлемое время, судя по записи перфомансов примерно две с чем-то, три секунды. И это, кажется, меньше чем во всех шовных открытых мирах от энтерпрайза которые я видел. Продвинуто бесшовный я тоже один нашел и поиграл, но он лагал хуже всех, и когда сюжет наконец двинулся с мертвой точки вдруг перестали работать сейвы; тут я уже забил

Все использующиеся в игре текстурыВсе использующиеся в игре текстурыПерфомансПерфоманс

Хочется сразу сказать что техлиды и сеньоры с менторским тоном и заоблачной экспертизой в микробенчмаркинге в комментариях только приветствуются. Это же вообще самое забавное и интересное на Хабре когда лиды с сеньорами начинают рубиться в комментариях за стоимость операций в джаваскрипте и то, чей микробенчмаркинг заоблачнее! Остается только надеятся на то, что когда вы будете размазывать мой форсрелоад как дешёвое и сердитое средство изменения сцены вы обязательно продемонстрируете ваши работающие примеры в которых сцена Three с большим количеством разнообразных объектов на ней очищается и заново инициализируется через свои внутренние методы (например, без перезагрузки текстур и прочих ассетов, аудио). Я же не говорю что это невозможно, это очевидно дорого. Намного дороже чем просто сделать форсрелоад. Понятно что хороший проект это прежде всего кодовая база которая может и должна легко развиваться. Но невозможно прикрутить все фичи сразу, а использование дешевого релоада сейчас никак не блокирует добавление более сложного функционала в будущем. Да и кроме дешевизны более простой подход и идеологически привлекателен. Я убежден что хороший код это простой и понятный код, хороший подход простой подход, точно так же как и интерфейс который они предоставляют. Простое решение лучше сложного, особенно если мы только начинаем строить что-то.

Для того чтобы избежать лишних сложностей в моей реализации сцена практически неизменна. Она разворачивается, запускается и дальше функционирует в некотором постоянном виде [порождая и уничтожая только выстрелы и взрывы] пока не происходит переход в другую локацию (или проигрыш на этой). Конкретнее: cейчас я нигде кроме удаления не подлежащих внешнему учету выстрелов и взрывов не использую scene.remove(object.mesh) например при сборе героем полезных предметов, делая вместо этого:

// встроенное свойство на Object3D в Threeobject.mesh.visible = false;// кастомный флаг кастомного массива объектовobject.isPicked = true;

Поэтому мы, например, можем даже использовать свойство id: number mesh`ей вместо uuid: string для учета и идентификации объектов. Так как все подлежащие учету объекты всегда остаются на сцене мы можем быть уверены что Three не поменяет айдишники, сдвинув нумерацию под коробкой при удалении элемента (но если вы хотите все-таки удалять что-то такое просто опирайтесь на uuid при работе с этим).

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

Посмотрим на структуру проекта:

. /public // статические ресурсы   /audio // аудио     ...   /images // изображения     /favicons // дополнительные фавиконки для браузеров       ...     /modals // картинки для информационных панелей       /level1 // для уровня 1         ...       ...     /models       /Levels         /level0 // модель-схема Песочницы (скрытый уровень 0 - тестовая арена)           Scene.glb         ...       /Objects          Element.glb          ...     /textures        texture1.jpg        ...   favicon.ico // основная фавиконка 16 на 16   index.html // статичный индекс   manifest.json // файл манифеста   start.jpg // картинка для репозитория ) /src   /assets // ассеты сорцов     optical.png // у меня один такой )))   /components // компоненты, миксины и модули     /Layout // компоненты и миксины UI-обертки над игрой       Component1.vue // копонент 1       mixin1.js // миксин 1       ...     /Three // сама игра        /Modules // готовые полезные модули из библиотеки          ...        /Scene           /Enemies // модули врагов             Enemy1.js             ...           /Weapon // модули оружия             Explosions.js // взрывы             HeroWeapon.js // оружие персонажа             Shots.js // выстрелы врагов           /World // модули различных элементов мира             Element1.js             ...           Atmosphere.js // модуль с общими для всех уровней объектами (общий свет, небо, звук ветра) и проверками взаимодействия между другими модулями           AudioBus.js // аудио-шина           Enemies.js // модуль всех врагов           EventsBus.js // шина событий           Hero.js // модуль персонажа           Scene.vue // основной компонент игры           World.js // мир   /store // хранилище Vuex     ...   /styles // стилевая база препроцессора SCSS     ...   /utils // набор утилитарных js-модулей для различных функциональностей     api.js // интерфейс для связи с бэкендом     constants.js // вся конфигурация игры и тексты-переводы     i18n.js // конфигурация переводчика     screen-helper.js // модуль "экранный помощник"     storage.js // модуль для взаимодействия с браузерным хранилищем     utilities.js // набор полезных функций-атомов   App.vue // "главный" компонент   main.js // эндпоинт сорцов Vue ... // все остальное на верхнем уровне проекта, как обычно: конфиги, gitignore, README.md и прочее

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

Сейчас игра в спокойном состоянии когда потревоженных врагов нет или совсем мало, на компьютере с поддержкой GPU выдает практически коммерческие 60FPS в Google Chrome (ну или Yandex Bro). В Firefox игра запускается, но показатель производительности не менее чем в 2-3 раза ниже. А когда начинается мясо, появляется много потревоженных врагов, выстрелов и взрывов в Лисе процесс начинает лагать и может вообще повиснуть. Моя экспертиза в микробенчмаркинге сейчас пока не позволяет с умным видом рассуждать о причинах этой разницы. Будем считать что дело в более слабой поддержке WebGL и вычислительных способностях, что-то такое))...

Легенда

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

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

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

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

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

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

ДашбордДашборд

Если подойти к панели и нажать E открывается модаль с исторической справкой:

Рассказ о будущем внутриРассказ о будущем внутри

Это задел для дальнейшего развития в сторону РПГ. Например на таких дашбордах несложно организовать торговый ларек, в котором можно было бы менять поверженный и подобранный металлолом на полезные цветы и бутылки.

Геймплей

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

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

Цветы и бутылкиЦветы и бутылки

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

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

Уровни сложностиУровни сложности

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

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

  • Еще один вид бутылок с крепышом часовые мины: установил быстро отбегаешь. Будут полезны для разрушения Танков с огромным здоровьем или крупных скоплений любых врагов.

  • Новые типы врагов. Танки медленные, но очень живучие и с убойным выстрелом. Стационарные дроны-пушки умеющие стрелять не в горизонтальной плоскости навесом как делают дроны сейчас, а под разными углами и двойными зарядами. Рядовые бойцы Танцоры Роботы-Курицы мой барабанщик почему-то их именно так видит. В идеале они высаживаются как спезназ, приземляясь на челноке в центр третьей локации когда герой на нее заходит. В пятой локации может появиться босс: Робот-Блогер Финальный с ракетницей

  • Трубочных и двуполых Собутыльников нарисовать сложно, но в идеале было бы рассадить их по камерам Централа четвертой локации.

  • Можно добавить 2D-карту с врагами (внизу и по центру экрана)

Планов полно, но без скелетной анимации они бессмысленны, конечно

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

Конфигурация

Особенный кайф от написания кастомной игры в том, что после того как вы доставили новые фичи или любые изменения в код вам просто необходимо расслабиться и их честно искренне протестировать. Ручками. Сделать несколько каток, по любому. Тесты тут никак и ничем не помогут, даже, убежден, наоборот будут мешать прогрессу, особенно если вы не работаете по заранее известному плану, а постоянно экспериментируете. Браузерная игра на джаваскрипт это в принципе превосходный пример того, когда статическая типизация или разработка через тестирование будут только мешать добиться действительно качественного результата. (А на чем тут необходимо проверять типы, господа сеньоры? Я до сих пор в замешательстве от React c CSS Modules и просто Flow, а не TS даже в котором авторы маниакально проверяли что каждый, еще и передаваемый по цепочке компонент, класс модулей для оформления !!! это string А тут что будем маниакально типизировать, вектора?). И даже сам Роберт Мартин в Идеальном программисте делает несколько пассажей на тему бессмысленности TDD, когда говорит о рисках при разработке GUI. В моей игре можно сказать что и нет практически ничего кроме тонны двумерного и трехмерного GUI, ну и логики для него. Любая ошибка либо вызовет исключение, либо неправильное поведение во вьюхе и геймплее, которое может быть очень быстро обнаружено с помощью визуальной проверки, но очень сомнительно что вообще способно быть покрыто тестом.

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

Все настройки настройки и значения влияющие на геймплей и дизайн (константа DESIGN), а также весь текстовый контент-переводы у меня сосредоточены в constants.js.

Контрол

На сайте библиотеки Three представлено большое количество полезных примеров с демо-стендами, самых разных реализаций, функциональностей которые стоит изучить и по возможности к месту использовать. Я отталкивался в своих исследованиях, прежде всего, вот от этого примера. Это правильный, мягкий инерционный контрол от первого лица который математически обсчитывает столкновения с клеткой-миром gld-моделью с помощью октодерева. Проверять столкновения можно для капсулы (для героя или врагов) или обычных сферы Sphere и луча Ray от Three. Этого в принципе достаточно для чтобы сделать FPS-игру: сделать так чтобы герой и враги не сталкивались с миром и между собой, выстрелы взрывались при попадании в другие объекты и тд.

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

// Controls// In First Person...

Но! Тут нюанс браузеры обязательно оставляют путь для панического отступления пользователю и резервируют клавишу Esc для того чтобы пользователь всегда мог разлочить указатель. Это касается нашего UI/UX в игре необходима клавиша P ставящая мир на паузу. Когда указатель залочен то бишь запущен игровой процесс нажатие на Esc, как уже сказано вызовет паузу. Но если мы попытаемся добавить обработку отпускания по 27ому коду даже только для режима паузы, все равно очень быстро увидим в консоли:

ОшибкаОшибка

Поэтому: забудьте про Esc. Пауза по клавише P. Есть еще одно ограничение и проблема связанная с созданием хорошего FPS-контрола: оружие. Я так понял что в энтерпрайзных реализациях руки-оружие это отдельный независимый план наложенный поверх мира. С Three, насколько я понимаю, сделать так не получится. Поэтому мой пока единственный в арсенале грозный виномет с оптическим прицелом это объект сцены который приделан к контролу. Я копирую вектор направления камеры на него. Но около зенита и надира в результате его начинает штормить он не может однозначно определить позицию. При взгляде совсем под ноги я его просто скрываю, а вот стрелять наверх нужно. Что делать с этим небольшим и не особо заметным багом я пока не придумал.

Оптический прицел винометаОптический прицел винометаВыстрел вверхВыстрел вверх

Пытаясь сделать скоростной задорный шутер на Three мы можем сразу забыть о тенях или дополнительных источниках освещения, особенно движущихся. Да, я пытался запилить качающиеся на ветру лампы для особенного мрачняка и криповости, движущиеся тени от них. Нет никак нельзя даже статичные точечные источники света сильно просаживают производительность (а нам еще врагов гонять). По поводу света я пришел к простому компромиссу: чтобы картинка не выглядела совсем сухо и скучно приделать мощный фонарик к контролу, герою. Фонарик можно выключать клавиша T.

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

Сцена

Основной компонент Scene.vue предоставляет:

  • всю стандартную кухню Three: Renderer, Scene и ее туман, Camera и Audio listener в ней, Controls

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

  • переменные для хранения коллекций примитивных дополнительных объектов превдоmesh`ей по которым работает кастинг

  • в том числе и через используемые миксины все необходимые ему самому или его низовым модулям геттеры и экшены стора Vuex

  • обрабатывает большинство (кроме тех, что удобно ловить в логике героя) событий клавиатуры, мыши и так далее

  • инициализирует Аудиошину, Шину Событий и Мир

  • анимирует Шину Событий, Героя и Мир

  • в наблюдателях значений важных геттеров добавляет игровой логики

Весь код тут простой, прямо очевидный, практически не требующий дополнительных пояснений. Что-то мы будем рассматривать дальше, например что за такие превдоmesh`и для кастинга. Но стоит только остановить внимание на такой простой базовой сущности как переменные. Дело в том что существует еще один действительно важный аспект который позволит нам вытянуть эту задачу с шутером это тут можно начинать смеяться экономия памяти в джаваскрипте (господа техлиды-сеньоры?). Да не надо ни при каких обстоятельствах создавать переменные в анимационных циклах и проверках низовых модулей, нужно использовать только те что уже есть в основном компоненте (ну или модуле), созданы заранее. Контекст основного компонента можно передавать в публичные методы низовых модулей-функций.

Стандартный модуль героя, врагов, предмета или специфического объекта вроде двери или информационной панели в общем виде выглядит так:

import * as Three from 'three';import { DESIGN } from '@/utils/constants';function Module() {  let variable; // локальная переменная - когда очень удобна или необходима при инициализации или во всей логике    // ...  // Инициализация  this.init = (    scope,    texture1,    material1,    // ...  ) => {    // variable = ...    // ...  };  // Функция анимационного цикла для этого модуля - опционально (предметы, например, не нужно анимировать)  this.animate = (scope) => {    // А вот тут и в остальной логике стараемся использовать уже только переменные Scene.vue:    scope.moduleObjectsSore.filter(object => object.mode === DESIGN.ENEMIES.mode.active).forEach((object) => {      // scope.number = ...      // scope.direction = new Three.Vector3(...);      // variable = ... - так, конечно, тоже можно, главное не let variableNew;      // ...    });  };}export default Module;

Стор

Хранилище Vuex поделено на 3 простых модуля. layout.js отвечает за основные параметры игрового процесса: паузы-геймоверы и тд, взаимодействует с API-бекенда. В hero.js большое количество полей и их геттеров, но всего два экшена/мутации. Этот модуль позволяет в максимально унифицированной форме распространять изменения значений отдельных параметров, шкал, флагов на герое с помощью setScale или может пакетно установить эти значения через setUser.

Третий модуль совсем примитивный preloader.js и целиком состоит из однотипных boolean-полей с false по дефолту. Пока его поле isGameLoaded единственное в состоянии модуля с геттером с false не получает true при запуске или перезагрузке приложения пользователь будет видеть лоадер. Каждое из остальных полей обозначает подгрузку определенного ассета: текстуры, модели, аудио или постройку определенного типа объектов.

Если нам нужно подгрузить, например, текстуру песка:

import * as Three from 'three';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  this.init = (    scope,    // ...  ) => {    const sandTexture = new Three.TextureLoader().load(      './images/textures/sand.jpg',      () => {        scope.render(); // нужно вызвать рендер если объекты использующию эту текстуру заметны "на первом экране"          loaderDispatchHelper(scope.$store, 'isSandLoaded');      },    );  };}export default Module;
// В @/utils/utilities.js:export const loaderDispatchHelper = (store, field) => {  store.dispatch('preloader/preloadOrBuilt', field).then(() => {    store.dispatch('preloader/isAllLoadedAndBuilt');  }).catch((error) => { console.log(error); });};

Когда отправка сообщения о том что элемент подгружен разрешается функция-помощник из набора атомов-утилит отправляет экшен проверяющий все ли готово?.

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

Аудиошина

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

Аудио бывают:

1) Звучащие на контроле-герое и PositionalAudio на объектах

2) Луп или сэмпл

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

В Hero удобно записывать аудио в переменную чтобы можно было просто работать [в обход шины] с ними в специфической логике:

// В @/components/Three/Scene/Hero.js:import * as Three from "three";import {  DESIGN,  // ...} from '@/utils/constants';import {  loaderDispatchHelper,  // ...} from '@/utils/utilities';function Hero() {  const audioLoader = new Three.AudioLoader();  let steps;  let speed;  // ...  this.init = (    scope,    // ...  ) => {    audioLoader.load('./audio/steps.mp3', (buffer) => {      steps = scope.audio.addAudioToHero(scope, buffer, 'steps', DESIGN.VOLUME.hero.step, false);      loaderDispatchHelper(scope.$store, 'isStepsLoaded');    });  };  this.setHidden = (scope, isHidden) => {    if (isHidden) {      // ...      steps.setPlaybackRate(0.5);    } else {      // ...      steps.setPlaybackRate(1);    }  };  this.setRun = (scope, isRun) => {    if (isRun && scope.keyStates['KeyW']) {      steps.setVolume(DESIGN.VOLUME.hero.run);      steps.setPlaybackRate(2);    } else {      steps.setVolume(DESIGN.VOLUME.hero.step);      steps.setPlaybackRate(1);    }  };  // ...  this.animate = (scope) => {    if (scope.playerOnFloor) {      if (!scope.isPause) {        // ...        // Steps sound        if (steps) {          if (scope.keyStates['KeyW']            || scope.keyStates['KeyS']            || scope.keyStates['KeyA']            || scope.keyStates['KeyD']) {            if (!steps.isPlaying) {              speed = scope.isHidden ? 0.5 : scope.isRun ? 2 : 1;              steps.setPlaybackRate(speed);              steps.play();            }          }        }      } else {        if (steps && steps.isPlaying) steps.pause();        // ...      }    }  };}export default Module;

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

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

if (!isLoop) audio.onEnded = () => audio.stop();

Имейте ввиду!

import * as Three from "three";import { DESIGN, OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  const audioLoader = new Three.AudioLoader();  // ...  let material = null;  const geometry = new Three.SphereBufferGeometry(0.5, 8, 8);  let explosion;  let explosionClone;  let boom;  this.init = (    scope,    fireMaterial,    // ...  ) => {    // Звук наземных врагов - загружаем в инициализации на объекты через шину    audioLoader.load('./audio/mechanism.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isMechanismLoaded');      scope.array = scope.enemies.filter(enemy => enemy.name !== OBJECTS.DRONES.name);      scope.audio.addAudioToObjects(scope, scope.array, buffer, 'mesh', 'mechanism', DESIGN.VOLUME.mechanism, true);     });    // Звук взрыва - то есть - "добавляемой и уничтожаемой" сущности - загружаем и записываем в переменную    material = fireMaterial;    explosion = new Three.Mesh(geometry, material);    audioLoader.load('./audio/explosion.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isExplosionLoaded');      boom = buffer;    });  };  // ...  // ... где-то в логике врагов:  this.moduleFunction = (scope, enemy) => {    scope.audio.startObjectSound(enemy.id, 'mechanism');    // ...    scope.audio.stopObjectSound(enemy.id, 'mechanism');    // ...  };  // При добавлении взрыва на шину взрывов:  this.addExplosionToBus = (    scope,    // ...  ) => {    explosionClone = explosion.clone();    // ..    scope.audio.playAudioOnObject(scope, explosionClone, boom, 'boom', DESIGN.VOLUME.explosion);    // ..  };}export default Module;

Попробуйте подобрать и принять зеленый цветок запускающий машину времени в игре, классно? ))

Шина событий и сообщения

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

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

Мир

Модель первой локацииМодель первой локации

В инициализации модуля мира по порядку:

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

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

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

  4. Двери переносятся в отдельную группу из основной модели. Из этих двух групп очищенной сцены и все дверей создаются стартовые октодеревья.

  5. Инициализируются все остальные модули.

Я разбираю один файл glb и как совершенно необходимый такой игре редактор уровней и как готовую модель для построения стартовых октодеревьев мира, и, отдельно дверей в нем, и как почти готовую не текстурированную основу самого примитивного визуального мира. Различать примитивы можно с помощью специфических маркеров в их наименовании. Это не самое надежное соглашение, оно чревато ошибками, но они легко обнаруживаются визуально при ручном тестировании. Изменения можно вносить очень быстро. Тут уже все зависит от вашей фантазии и выдуманного с помощью нее дизайна и геймплея, ну и количества времени которые вы можете на это потратить. Например, я использую маркер Mandatory если хочу чтобы цветок или бутылка были обязательными, если его нет постройка зависит от рандома. Или для механики включения-выключения информационных панелей собирается отдельный массив с их комнатами параллелепипедами определяющими объем в котором панель реагирует на персонажа. Для геометрии такого объекта следует сделать при инициализации:

room.geometry.computeBoundingBox();

room.visible = false;

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

// В @/components/Three/Scene/World/Screens.js:this.isHeroInRoomWithScreen = (scope, screen) => {scope.box.copy(screen.room.geometry.boundingBox).applyMatrix4(screen.room.matrixWorld); if (scope.box.containsPoint(scope.camera.position)) return true;return false;};

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

Псевдообъект-помощник для двериПсевдообъект-помощник для двериДверь не закрываетсяДверь не закрывается

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

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

Кастинг

Вот мы и добрались до самого интересного: кастинг и столкновения. Как сделать так, чтобы предметы можно было собирать, а герой и враги не сталкивались с миром и друг-другом. Для обоих механик я использую дополнительные невидимые примитивы, псевдоmesh`и. Они инициализируются и записываются в абстрактные объекты которыми оперирует система вместе с основным видимым и всеми необходимыми им флагами-свойствами. Для движущихся врагов еще записывается коллайдер Sphere. Псевдомеши идут на кастинг (предметы) или построение и обновление октодеревьев (враги). А коллайдер для проверки столкновения с ними.

Псевдообъекты-помощники для предметовПсевдообъекты-помощники для предметов

Геометрия и материал готовиться в мире перед инициализацией всех вещей и надежнее сделать материал двусторонними так кастинг будет работать даже если герой оказался внутри псевдообъекта:

// В @/components/Three/Scene/World.js:const pseudoGeometry = new Three.SphereBufferGeometry(DESIGN.HERO.HEIGHT / 2,  4, 4); const pseudoMaterial = new Three.MeshStandardMaterial({ color: DESIGN.COLORS.white, side: Three.DoubleSide,});new Bottles().init(scope, pseudoGeometry, pseudoMaterial);

В модуле конкретной вещи:

// В @/components/Three/Scene/World/Thing.js:import * as Three from 'three';import { GLTFLoader } from '@/components/Three/Modules/Utils/GLTFLoader';import { OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Thing() {  let thingClone;  let thingGroup;  let thingPseudo;  let thingPseudoClone;  this.init = (    scope,    pseudoGeometry,    pseudoMaterial,  ) => {    thingPseudo = new Three.Mesh(pseudoGeometry, pseudoMaterial);    new GLTFLoader().load(      './images/models/Objects/Thing.glb',      (thing) => {        loaderDispatchHelper(scope.$store, 'isThingLoaded'); // загружена модель        for (let i = 0; i < OBJECTS.THINGS[scope.l].data.length; i++) {          // eslint-disable-next-line no-loop-func          thing.scene.traverse((child) => {            // ... - тут "покраска" материалами частей вещи          });          // Клонируем объект и псевдо          thingClone = thing.scene.clone();          thingPseudoClone = thingPseudo.clone();          // Псевдо нужно дать правильное имя чтобы мы могли различать его при кастинге          thingPseudoClone.name = OBJECTS.THINGS.name;          thingPseudoClone.position.y += 1.5; // корректируем немного позицию по высоте          thingPseudoClone.visible = false; // выключаем рендер          thingPseudoClone.updateMatrix(); // обновляем          thingPseudoClone.matrixAutoUpdate = false; // запрещаем автообновление          // Делаем из обхекта и псевдо удобную группу          thingGroup = new Three.Group();          thingGroup.add(thingClone);          thingGroup.add(thingPseudoClone);          // Выставляем координаты из собранных из модели уровня данных          thingGroup.position.set(            OBJECTS.THINGS[scope.l].data[i].x,            OBJECTS.THINGS[scope.l].data[i].y,            OBJECTS.THINGS[scope.l].data[i].z,          );          // Записываем в "рабочие объеты" - по ним будем кастить и прочее          scope.things.push({            id: thingPseudoClone.id,            group: thingGroup,          });          scope.objects.push(thingPseudoClone);          scope.scene.add(thingGroup); // добавляем на сцену        }        loaderDispatchHelper(scope.$store, 'isThingsBuilt'); // построено      },    );  };}export default Thing;

Теперь мы можем тыкать направленным вперед лучом из героя в анимационном цикле Hero.js:

// В @/components/Three/Scene/Hero.js:import { DESIGN, OBJECTS } from '@/utils/constants';function Hero() {  // ...  this.animate = (scope) => {    // ...    // Raycasting    // Forward ray    scope.direction = scope.camera.getWorldDirection(scope.direction);    scope.raycaster.set(scope.camera.getWorldPosition(scope.position), scope.direction);    scope.intersections = scope.raycaster.intersectObjects(scope.objects);    scope.onForward = scope.intersections.length > 0 ? scope.intersections[0].distance < DESIGN.HERO.CAST : false;    if (scope.onForward) {      scope.object = scope.intersections[0].object;      // Кастим предмет THINGS      if (scope.object.name.includes(OBJECTS.THINGS.name)) {        // ...      }    }    // ...  };}export default Hero;

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

// В @/utils/utilities.js:// let arrowHelper;const fixNot = (value) => { if (!value) return Number.MAX_SAFE_INTEGER; return value;};export const isEnemyCanMoveForward = (scope, enemy) => { scope.ray = new Three.Ray(enemy.collider.center, enemy.mesh.getWorldDirection(scope.direction).normalize()); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); scope.resultEnemies = scope.octreeEnemies.rayIntersect(scope.ray); // arrowHelper = new Three.ArrowHelper(scope.direction, enemy.collider.center, 6, 0xffffff); // scope.scene.add(arrowHelper); if (scope.result || scope.resultDoors || scope.resultEnemies) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance), fixNot(scope.resultEnemies.distance));   return scope.number > 6; } return true;};

Для наглядной визуальной отладки подобных механик очень полезен объект Three ArrowHelper. Если мы включим его добавление на сцену в функции выше:

Отладка с включенными стрелочными помощникамиОтладка с включенными стрелочными помощниками

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

// В @/utils/utilities.js:export const isToHeroRayIntersectWorld = (scope, collider) => { scope.direction.subVectors(collider.center, scope.camera.position).negate().normalize(); scope.ray = new Three.Ray(collider.center, scope.direction); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); if (scope.result || scope.resultDoors) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance));   scope.dictance = scope.camera.position.distanceTo(collider.center);   return scope.number < scope.dictance; } return false;};

Враги

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

// В @/utils/constatnts.js:export const DESIGN = {  DIFFICULTY: {    civil: 'civil',    anarchist: 'anarchist',    communist: 'communist',  },  ENEMIES: {    mode: {      idle: 'idle',      active: 'active',      dies: 'dies',      dead: 'dead',    },    spider: {      // ...      decision: {        enjoy: 60,        rotate: 25,        shot: {          civil: 40,          anarchist: 30,          communist: 25,        },        jump: 50,        speed: 20,        bend: 30,      },    },    drone: {      // ...      decision: {        enjoy: 50,        rotate: 25,        shot: {          civil: 50,          anarchist: 40,          communist: 30,        },        fly: 40,        speed: 20,        bend: 25,      },    },  },  // ...};
// В @/components/Three/Scene/Enemies.js:import { DESIGN } from '@/utils/constants';import {  randomInteger,  isEnemyCanShot,  // ...} from "@/utils/utilities";function Enemies() {  // ...  const idle = (scope, enemy) => {    // ...  };  const active = (scope, enemy) => {    // ...    // Где-то в логике агрессивного режима: решение на выстрел (если отдыхает)    scope.decision = randomInteger(1, DESIGN.ENEMIES[enemy.name].decision.shot[scope.difficulty]) === 1;    if (scope.decision) {      if (isEnemyCanShot(scope, enemy)) {        scope.boolean = enemy.name === OBJECTS.DRONES.name;        scope.world.shots.addShotToBus(scope, enemy.mesh.position, scope.direction, scope.boolean);        scope.audio.replayObjectSound(enemy.id, 'shot');      }    }  };  const gravity = (scope, enemy) => {    // ...  };  this.animate = (scope) => {    scope.enemies.filter(enemy => enemy.mode !== DESIGN.ENEMIES.mode.dead).forEach((enemy) => {      switch (enemy.mode) {        case DESIGN.ENEMIES.mode.idle:          idle(scope, enemy);          break;        case DESIGN.ENEMIES.mode.active:          active(scope, enemy);          break;        case DESIGN.ENEMIES.mode.dies:          gravity(scope, enemy);          break;      }    });  };}export default Enemies;

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

Но! Самое важное на что нужно обратить внимание: в idle спокойном режиме полноценно двигается некоторое случайное время только один выбранный случайным образом враг. Остальные поворачиваются на месте + может и должна быть запущена анимация. Такая оптимизация позволяет действительно полноценно разгрузить систему.

Столкновения

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

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

В текущей реализации используются три октодерева: мир: 1) пол, бетонные блоки, трубы, стекла, а также 2) двери и 3) враги. Каждый из врагов обсчитывает свои столкновения с персональным октодеревом врагов собранным без него.

Мы можем и должны обновлять октодеревья. Если дверь была открыта-закрыта или враг передвинулся нам нужно пересобрать соответствующее октодерево. При открытии закрытии двери лучше всего сделать это два раза: когда дверь открылась или закрылась настолько что герой уже может или наоборот больше не может пройти через нее и когда процесс открытия/закрытия завершен.

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

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

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

// В @/utils/constatnts.js:export const DESIGN = {  OCTREE_UPDATE_TIMEOUT: 0.5,  // ...};
// В @/utils/utilities.js:// Обновить персональное октодерево врагов для одного врагаimport * as Three from "three";import { Octree } from "../components/Three/Modules/Math/Octree";export const updateEnemiesPersonalOctree = (scope, id) => {  scope.group = new Three.Group();  scope.enemies.filter(obj => obj.id !== id).forEach((enemy) => {    scope.group.add(enemy.pseudoLarge);  });  scope.octreeEnemies = new Octree();  scope.octreeEnemies.fromGraphNode(scope.group);  scope.scene.add(scope.group);};
// Столкновения враговconst enemyCollitions = (scope, enemy) => {  // Столкновения c миром - полом, стенами, стеклами и трубами  scope.result = scope.octree.sphereIntersect(enemy.collider);  enemy.isOnFloor = false;  if (scope.result) {    enemy.isOnFloor = scope.result.normal.y > 0;    // На полу?    if (!enemy.isOnFloor) {      enemy.velocity.addScaledVector(scope.result.normal, -scope.result.normal.dot(enemy.velocity));    } else {      // Подбитый враг становится совсем мертвым после падения на пол и тд      // ...    }    enemy.collider.translate(scope.result.normal.multiplyScalar(scope.result.depth));  }  // Столкновения c дверями  scope.resultDoors = scope.octreeDoors.sphereIntersect(enemy.collider);  if (scope.resultDoors) {    enemy.collider.translate(scope.resultDoors.normal.multiplyScalar(scope.resultDoors.depth));  }  // Делаем октодерево из всех врагов без этого, если давно не делали  if (scope.enemies.length > 1    && !enemy.updateClock.running) {    if (!enemy.updateClock.running) enemy.updateClock.start();    updateEnemiesPersonalOctree(scope, enemy.id);    scope.resultEnemies = scope.octreeEnemies.sphereIntersect(enemy.collider);    if (scope.resultEnemies) {      result = scope.resultEnemies.normal.multiplyScalar(scope.resultEnemies.depth);      result.y = 0;      enemy.collider.translate(result);    }  }  if (enemy.updateClock.running) {    enemy.updateTime += enemy.updateClock.getDelta();    if (enemy.updateTime > DESIGN.OCTREE_UPDATE_TIMEOUT && enemy.updateClock.running) {      enemy.updateClock.stop();      enemy.updateTime = 0;    }  }};

Своя атмосфера

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

Если вывалится за стену и забежать за край небаЕсли вывалится за стену и забежать за край неба

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

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

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

Да, это вам не React c TS и тестами в финтех и банки!

Выводы которые я могу сделать на основе практики создания браузерной FPS на Three:

  • Мы не можем использовать тени и множество источников света

  • Мы должны экономить память в анимационном цикле и использовать в нем только готовые переменные

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

  • Статическая типизация и юнит-тесты ничем не могут помочь в данном эксперименте

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

Подробнее..

Автоматизация или смерть как управлять тысячами единиц игрового контента с помощью гугл-таблиц

10.06.2021 18:12:02 | Автор: admin

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

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

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

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

На первых годах жизни Pixel Gun 3D использовалась простая схема: весь контент добавлялся и редактировался вручную. Нужно поменять урон пушке? Заходишь в Unity, открываешь нужный файл и правишь руками. Дело на пару минут.

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

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

Нужно было глобально что-то менять.

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

Из гугл-таблиц в Unity

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

Для получения данных с таблиц мы используем Google Apps Script. Первое время заводили отдельные скрипты на каждую таблицу, в которых обрабатывали данные в JSON. Затем, получая в редакторе JSON, применяли их по назначению.

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

Так выглядит наш скрипт:
function doGet(e){ if (e === undefined || e.parameter === undefined) {   return FailWithMessage("nullable parameters"); } var tableId = e.parameter["table"]; var listName = e.parameter["list"]; if (listName !== undefined && listName !== "" && listName !== "null") {   var startRow = parseInt(e.parameter["startRow"]);   var startColumn = parseInt(e.parameter["startColumn"]);   var numRow = parseInt(e.parameter["numRow"]);   var numColumn = parseInt(e.parameter["numCol"]);   return GetSigleList(tableId, listName, startRow, startColumn, numRow, numColumn); } else {   return GetAllTable(tableId); }}  function GetSigleList(tableId, listName, startRow, startColumn, numRow, numColumn){ var ss = SpreadsheetApp.openById(tableId); if (ss == null) {   return FailWithMessage("table with name: " + tableId + "not found"); } var sheet = ss.getSheetByName(listName); if (sheet == null) {   return FailWithMessage("list with name: " + listName + "not found"); }  if (numRow < 1) numRow = sheet.getLastRow(); if (numColumn < 1) numColumn = sheet.getLastColumn(); var range = sheet.getRange(startRow, startColumn, numRow, numColumn); var data = range.getValues(); var str = JSON.stringify(data);  var resultObject = {   "resultCode": 2,   "message": str }; var result = JSON.stringify(resultObject); return ContentService.createTextOutput(result);}  function GetAllTable(tableId){ var ss = SpreadsheetApp.openById(tableId); if (ss == null) {   return FailWithMessage("table with name: " + tableId + "not found"); }  var result = {};  var listModes = ss.getSheets(); for(var i = 0; i< listModes.length; i++) {   var sheet = listModes[i];   var sheetName = sheet.getSheetName();     var range = sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn());   var data = range.getValues();   result[sheetName] = data; }  var str = JSON.stringify(result);  var resultObject = {   "resultCode": 2,   "message": str };  var result = JSON.stringify(resultObject); return ContentService.createTextOutput(result);} function FailWithMessage(message){ var result = {   "resultCode": 1,   "message": message };   var str = JSON.stringify(result);  return ContentService.createTextOutput(str);}

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

После публикации получится ссылка такого формата:

https://script.google.com/macros/s/WwlCZODTDRXJaHhdjfwFRcKtHRQOHqzYisjndduZzDihMpXehLrNxdi/exec

Ее нужно использовать для запуска скрипта. Чтобы скрипт знал, с какой таблицы нужны данные, в get-запрос подставляем ID таблицы. Получить его можно из URL таблицы. Например, в https://docs.google.com/spreadsheets/d/example_habr/edit#gid=0, ID будет example_habr.

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

Полный запрос будет выглядеть так:

https://script.google.com/macros/s/WwlCZODTDRXJaHhdjfwFRcKtHRQOHqzYisjndduZzDihMpXehLrNxdi/exec?table=example_habr&list=MyList&startRow=1&startColumn=2&numRow=10&numCol=5

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

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

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

Но мы пошли дальше.

Из Unity в гугл-таблицы

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

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

Начали с простеньких задачек. Например, мы часто превышали лимит в 500 символов на описание апдейта в Google Play. Стор такое отклоняет, нужно переписывать и отправлять заново. Задались вопросом, а есть ли формула для подсчета символов в ячейке? Разумеется, в гугл-таблицах большой перечень базовых формул, которые можно комбинировать как угодно и решать практически любые задачи. Написали в ячейке, чтобы описание апдейта автоматически проверялось на количество символов =ДЛСТР(номер ячейки). Теперь проблемы нет.

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

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

Например, решили составить коллекции для галереи оружия (это такие подборки оружия по схожим признакам: цвет, сеттинг, сезон). Чтобы составить одну коллекцию, нужно оценить визуал, смотрятся ли они вместе, схожий ли у них класс и другие детали. Для этого сделали таблицу, в которую из базы данных выгрузили список всех пушек, написали формулу и подгрузили превьюшки, с которыми можно визуально работать. А уже из таблиц по балансу подтягиваем названия, теги и остальную информацию.

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

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

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

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

Где используем автоматизацию

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

Балансировка контента

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

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

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

Генерация сущностей

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

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

Подобным образом мы генерируем задачи и для клановых войн.

Пример формулы:

=ifs(I2="EndMatch";ifs(AE2<=TasksData!$O$34+TasksData!$N$34;"TeamDuel";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34;ЕСЛИ(A2=0;"TeamDuel";"Spleef");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34;"Duel";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34;ЕСЛИ(A2=0;"Duel";"BattleRoyale");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34;ЕСЛИ(A2=0;"TeamFight";"DeadlyGames");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34;"CapturePoints";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34;"FlagCapture";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34+TasksData!$G$34;"Deathmatch";AE2>TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34+TasksData!$G$34;"TeamFight"); I2="killPlayer";ifs(AE2<=TasksData!$N$35;"TeamDuel";AE2<=TasksData!$N$35+TasksData!$L$35;"Duel";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35;ЕСЛИ(A2=0;"TeamFight";"BattleRoyale");AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35;"DeadlyGames";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35;"CapturePoints";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35;"FlagCapture";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35+TasksData!$G$35;"Deathmatch";AE2>TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35+TasksData!$G$35;"TeamFight"); I2="killPet";ifs(AE2<=TasksData!$G$36;"Deathmatch";AE2>TasksData!$G$36;"TeamFight"); I2="killPlayerThroughWall";ifs(AE2<=TasksData!$I$37;"CapturePoints";AE2<=TasksData!$I$37+TasksData!$H$37;"FlagCapture";AE2<=TasksData!$I$37+TasksData!$H$37+TasksData!$G$37;"Deathmatch";AE2>TasksData!$I$37+TasksData!$H$37+TasksData!$G$37;"TeamFight"); I2="killPlayerFlying";ifs(AE2<=TasksData!$I$38;"CapturePoints";AE2<=TasksData!$I$38+TasksData!$H$38;"FlagCapture";AE2<=TasksData!$I$38+TasksData!$H$38+TasksData!$G$38;"Deathmatch";AE2>TasksData!$I$38+TasksData!$H$38+TasksData!$G$38;"TeamFight");I2="ramEscort";"Siege";I2="escortDestroyGate";"Siege";I2="winBrNoChest";"BattleRoyale";I2="crashChest";"BattleRoyale";I2="winBrInParty";"BattleRoyale";I2="flagCapture";"FlagCapture";I2="pointCapture";"CapturePoints")

Пример таблицы с вводными для генератора:

Пример формулы (в ней представлена часть с генерацией описаний задач, Key_номер это ключи локализаций, из которых составляются задачи):

=IFS(I2="endMatch";(ЕСЛИ(T2=0;"Key_7220";"Key_7234"));I2="killPet";ЕСЛИ(W2="None";"Key_7228";"Key_7224");I2="killPlayer";ЕСЛИ(Q2=1;"Key_7227";(ЕСЛИ(W2="NONE";ЕСЛИ(R2=1;"Key_7232";ЕСЛИ(S2=1;"Key_7233";"Key_7221"));"Key_7216")));I2="killPlayerFlying";"Key_7225";I2="killPlayerThroughWall";"Key_7226";I2="ramEscort";"Key_7235";I2="escortDestroyGate";"Key_7236";I2="winBrNoChest";"Key_7229";I2="crashChest";"Key_7230";I2="winBrInParty";"Key_7231";I2="flagCapture";"Key_7237";I2="pointCapture";"Key_7238";I2="";"")

И еще более эпичная, уже проходящая через ячейки параметров и рандомайзеров:

=ifs(L3="DeadlyGames";0;L3="BattleRoyale";0;L3="TeamDuel";0;1=1;ЕСЛИ(I3="killPlayer";ifs(A3=0;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38)*6;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47)*6;1;0);0);A3=1;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38)*6;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$47)*6;1;0);0);A3=2;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$C$47+TasksData!$C$48+TasksData!$D$34+TasksData!$D$36+TasksData!$D$38)*6;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$D$34+TasksData!$D$36+TasksData!$D$38+TasksData!$B$47+TasksData!$C$47+TasksData!$C$48+TasksData!$D$47)*6;1;0);0));0))

Симуляция процессов

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

Пример части вводных данных:

Пример использованных формул в ячейках:

=СЛУЧМЕЖДУ(1;100)
=СРЗНАЧ(13;15)*6
=СУММ(B4:F4)
=IFS($A32<=G32;"Mythic";$A32<=F32;"Legend";$A32<=E32;"Epic";$A32<=D32;"Rare";$A32<=C32;"Common")

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

Пример результатов дропа по номерам открытий:

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

Воркфлоу создания таблицы

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

Расскажу подробнее по шагам (цифры в примере изменены в рамках конфиденциальности):

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

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

3. Для удобства из другой таблицы с помощью формулы подтягиваются картинки пушек. Арты предварительно загружены на сервис для получения прямой ссылки в формате (важно именно расширение в конце ссылки):

http://адрес_сервиса/путь/номер/имя картинки.jpg

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

=IMAGE("https://files.fm/u/wdrhemgnk#/view/special_offer_pixelman_reward_big.png")

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

=ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!B:F);5;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!B:F);5;ЛОЖЬ); ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!C:F);4;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!C:F);4;ЛОЖЬ); ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!D:F);3;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!D:F);3;ЛОЖЬ); ЕСЛИ(ЕНД(ВПР(A4;importrange(имя_таблицы;Лист1!E:F);2;ЛОЖЬ))=ЛОЖЬ;ВПР(A4;importrange(имя_таблицы;Лист1!E:F);2;ЛОЖЬ);НИМА ТАКОГО))))

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

Важный момент: после подгрузки данных мы их вырезаем (ctrl+x) и вставляем без привязки к формуле (ctrl+x+v). Формулу затем удаляем, иначе после каждого обновления страницы она будет пересчитывать все строки. В данном случае более 800.

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

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

5. Также используем форматирование ячеек. Оно может выполнить множество полезных преобразований. Например, покрасить ячейки, в которых значение больше 2 или меньше 1.

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

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

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

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

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

График популярности оружияГрафик популярности оружия

Жизнь после автоматизации

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

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

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

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

P.S. Данный подход меняет сам принцип работы с различными инструментами. Например, в том же Slack можно видеть BB-коды наглядно с помощью простой команды #:

Полезные ссылки

Подробнее..

Обзор роликов об играх с Game Trailer Challenge и их разбор с Alconost

27.05.2021 20:23:24 | Автор: admin

Этой весной мы вошли в жюри Game Trailer Challenge, в рамках которого гейм-девелоперы создавали ролики о своих играх. Мы посмотрели трейлеры и тизеры, сделанные разработчиками, внимательно оценили каждое видео и хотим показать вам 10 наиболее впечатливших нас роликов. Расскажем, что именно, на наш взгляд, сделало их классными, и что могло бы помочь им стать ещё лучше.

Идейный вдохновитель и спонсор челленджа польская компания Games Operators, издатель игр. А проводили челлендж основатели Game Industry Conference мероприятия для разработчиков игр, которое проводится в польской Познани с 2014 года. В этом году конференция должна состояться 21-24 октября.

Почти в топе

4 ролика, которым не хватило совсем чуть-чуть

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

Подчеркнём: оценки здесь и далее баллы от Alconost. В жюри челленджа, помимо нас, было ещё 11 экспертов, и их оценки не обязательно совпадали с нашими.

Трейлер игры Carebotz: 7 баллов

Релиз этого космического шутера, разработанного Glasscannon Studio из Венгрии, состоялся в Steam 7 мая.

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

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

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

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

Тизер об игре Pale Night: 7 баллов

Этот минималистичный 2D-платформер от парижской инди-студии Pretty French Games, основанной бывшим инженером машинного обучения из Nvidia Симоном Андерсеном, должен выйти в Steam в сентябре 2021-го.

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

Но главная фишка игры раскрывается лишь на 17-й секунде видео. Все ли досмотрят до этого момента?

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

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

Трейлер Covert: 8 баллов

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

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

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

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

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

Трейлер для игры Wayfarers: Call of Osiris: 8 баллов

Студия ActaLogic из Любляны планирует выпустить этот археологический экшен для PlayStation 4, Xbox One и ПК (Steam) в 3-4 квартале 2021-го.

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

И обратите внимание, как здорово в плане драматизма работает пауза на 0:14: она погружает в сюжет и заставляет смотреть дальше. К слову, первые 15 секунд видео вполне можно использовать и как отдельный ролик-тизер.

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

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

Топ-5 игровых роликов по версии Alconost

Эти видео мы оценили на 9 и 10.

Трейлер игры words: 9 баллов

Смотрите этот ролик со звуком: музыка в нём очень цепляет. Анимация тоже на уровне: продуманные планы, просчитанные движения камеры. Комбинация быстрых движений и почти статичных кадров, как на 20-23-й секундах, подчёркивает ритм анимированных событий. При этом, в ролике нет хаотичного действия: весь моушен очень хорошо сбалансирован.

А ещё в ролике есть ярко выраженная кульминация. Это на 32-35-й секундах, когда предмет из окружения вдруг становится интерактивным, а напряжённый и интенсивный фрагмент саундтрека сменяется плавным перебором струн. Как будто мы полминуты бежали по тёмным комнатам вместе с невидимым героем ролика и наконец добрались до безопасного места, где можно выдохнуть и забыть о тревогах. Эта метаморфоза подчёркивается всеми возможными выразительными средствами, вплоть до смены характер освещения в сцене.

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

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

Трейлер игры EXISTENTIAL: 9 баллов

Игра, которой посвящён трейлер, создана инди-разработчиком из Турции в рамках 72-часового геймдев-челленджа Atom GameJam. Кстати, words (предыдущий пункт нашего рейтинга) тоже его работа, и в роликах об обеих играх можно заметить кое-что общее: гармоничное сочетание аудио- и видеоряда, синхронизация анимации с ритмом музыки, а главное только значимые фрагменты геймплея, которые действительно помогают понять контекст игровой ситуации. Этот роликдоказывает: ввести потенциального пользователя в курс дела, передать атмосферу игры и намекнуть, что именно нужно будет делать в ходе прохождения, можно быстрее, чем за 15 секунд.

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

Тизер для игры Sunfall: Children of Adiona: 9 баллов

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

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

О самой игре, которую разрабатывает Tractor Set GO! геймдев-студия из Румынии, пока известно не очень много. Если предположить, что в игре будет больше контента и, когда игра будет ближе к релизу, ролик решат немного дополнить думаем, он отлично справится с задачей мотивировать потенциальных пользователей установить игру.

Трейлер игры Under the Counter: 9 баллов

Если бы в рамках челленджа была номинация Приз симпатий жюри, наши сердечки улетели бы к этому видео. Поздравляем варшавскую студию Korba Games с отличной работой! Релиз их визуальной новеллы Under the Counter в Steam запланирован на 30 июня 2021.

Вот что нам особенно понравилось в этом видео:

  1. Голосовая озвучка. Это не просто начитка текста, это актёрская игра как раз то, что нужно трейлеру-истории. Манера чтения, логические паузы, смысловые акценты всё на месте, и всё дозировано: никакого переигрывания.

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

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

  4. Общая плавность анимации, её размеренность. Темп видеоряда соответствует темпу аудиоряда. События в кадре не мешают следить за сюжетом, не отвлекают от сути.

  5. Музыка и звуковые эффекты: всё уместно, уравновешенно и отлично сведено.

Из моментов, которые мы бы улучшили, отметим сцену на 0:33-0:47: анимация рук главного героя всё же могла бы быть немного живее. Но главное как нам не хватило акцента на 1:05, когда камера фирменным движением плывёт вниз и оказывается, что красотка за барной стойкой вовсе не та, за кого себя выдаёт! Мы почти почувствовали мурашки по коже в этот момент, и как уместно было бы небольшое зависание камеры на этом моменте, лаконичный анимированный эффект или особый звук! Не то чтобы это было обязательным: ролик и без того выглядит абсолютно целостным и захватывающим. Но такой приём мог бы стать той самой вишенкой на торте, которая дала бы зрителю ещё больше эмоций.

Трейлер для игры #DRIVE: 10 баллов

#DRIVE сингл-плеерная гонка от польской геймдев-студии Pixel Perfect Dude. Игра доступна не только в привычных Google Play и App Store, но ещё и в Nintendo eShop.

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

Бонус: отличный тизер к несуществующей игре

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

Тизер для игры Fast Like Hell: 10 баллов

Этот тизер выглядит так, будто он был сделан на одном дыхании. Оцените динамику действия, проработанность анимации, музыку, юмор всё сочетается отлично! Хотя мы бы немного больше внимания уделили дизайну слоганов (серые плашки и чёрный шрифт без изысков выглядят простовато, особенно в контексте сочного, глянцевого арта) и добавили бы звуковые эффекты (они просто просятся в ролик, особенно на 0:19-0:23), даже без этого тизер выглядит лёгким для восприятия, живым и очень естественным.

Кто выиграл челлендж

Помимо нас, в жюри было ещё 11 экспертов профессионалов в геймдеве. По общему мнению жюри, челлендж выиграли два видео: трейлер Under the Counter и тизер Fast Like Hell. Желаем ребятам из Korba Games успехов в развитии игры Under the Counter и надеемся, что положительный отклик жюри вдохновит студию Berdo Games на разработку Fast Like Hell!

Топ роликов по общему мнению жюри челленджа

А вот и рейтинг роликов согласно оценкам всех членов жюри.

Место

Игра

Ролик

1

Under the Counter

https://youtu.be/sT9cP0sbqeo

1

Fast Like Hell

https://youtu.be/hAG4CziFBzA

2

#DRIVE

https://youtu.be/yi4gOixZpck

3

Pale Night

https://youtu.be/wwbizUvY-5s

4

Wayfarers: Call of Osiris

https://youtu.be/IESRqhqGmPM

5

Sunfall: Children of Adiona

https://youtu.be/ExkVPoXxZRE

6

Carebotz

https://youtu.be/8jJNGujedXo

7

words

https://youtu.be/ACFS1eMockI

8

Existential

https://youtu.be/4xqYQKyMUwQ

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

Хотите сделать трейлер или тизер о своей игре?

Создание роликов для игр наша работа. Будем рады сделать эффектное видео о вашем проекте! А если вы хотите попробовать сделать ролик самостоятельно надеемся, вам помогут наши материалы:

Об авторе

Статья написана вAlconost. Мы уже 8 летсоздаём видеоролики: рекламные и обучающие, для игр и приложений, продуктов и компаний. Ещё мы занимаемсялокализацией игр,приложений и сервисовна 80+ языков.

Подробнее..

Как обновить все сцены 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, проходить собеседования, получать повышение, справляться с профессиональным выгоранием, управлять разработкой и т.д.

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

Подробнее..

Recovery mode Странные шахматы как тестовое задание

17.06.2021 22:13:59 | Автор: admin

Добрый вечер хаброжители!

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

Суть задачи, есть доска 8 на 8 клеток. У игрока есть 9 шашек они расположены в углу доски в квадрате 3 на 3, у противника тоже столько же шашек и они расположены симметрично по диагонали в другом углу в квадрате 3 на 3. Каждый игрок ходит по очереди, нужно дойти шашками на места изначального положения соперника через всю доску, кто первый дошел тот и победил. Ходить можно только на пустые клетки и только вверх, вниз, влево и вправо(по диагонали нельзя!).

Управление я добавил мышью, а играть против компьютерного алгоритма. Черные - человек, белые - ИИ.

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

Немного кода для наглядности:

Game::Game(){run = true;//флаг признак нажатия кнопки выхода F5Matrix = new int* [8];//Поле 64 ячейки - значения 0 - для пустой ячейки, для игрока каждая пешка-шашка от 1 до 9, для компьютера значения в матрице от 10 до 18for (int i = 0; i < 8; i++)Matrix[i] = new int[8];//Квадраты координат нужны чтобы программа знала какие ячейки над указателем мыши, 64 квадратаQuadCoorXleft = new int* [8];//каждой ячейки матрицы Matrix соответстует квадрат координат для мыши xleft означает левую координату xQuadCoorXright = new int* [8];//xright - правая xQuadCoorYdown = new int* [8];//верхняя y координатаQuadCoorYup = new int* [8];//нижняя y координатаfor (int i = 0; i < 8; i++){QuadCoorXleft[i] = new int[8];QuadCoorXright[i] = new int[8];QuadCoorYdown[i] = new int[8];QuadCoorYup[i] = new int[8];}//Координаты пешек для отрисовкиChessX = new double[18];//XChessY = new double[18];//Y//Выделяемая пешка ее координаты и значенияActiveX = -1;//XActiveY = -1;//YActive = -1;//Valuefirstplayer = true;//флаг того что можете игрок 1й ходитьsecondplayer = false;//флаг того что можете игрок 2й ходитьai = new bool[18];//ячейки флаги того что пешка на финишной позицииchessai tmp;for (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i+1;Ai.push_back(tmp);//Вектор с флагами финиша каждой пешки для искуственного интеллекта}}aicountfirstrow = 0;//счетчик кол-ва пешек ИИ(искуственного интеллекта) на верхней строчке(0-я)aicountsecondrow = 0;//счетчик кол-ва пешек ИИ на предверхней строчке(1-я)aicountthirdrow = 0;//счетчик кол-ва пешек ИИ на предпредверхней строчке(2-я)}

Для отрисовки и захвата мыши используется библиотеки OpenGL и SDL2.

void Draw_Circle(){//Отрисовка круга(пешек-шахмат) черногоfor (int i = 0; i <= 50; i++) {float a = (float)i / 50.0f * 3.1415f * 2.0f;glVertex2f(cos(a), sin(a));}}void Draw_Circle_Fill(){//Отрисовка круга(пешек-шахмат) белогоfor (int i = 0; i <= 50; i++) {float a = (float)i / 50.0f * 3.1415f * 2.0f;glVertex2f(0.0, 0.0);glVertex2f(cos(a), sin(a));}}

Самое удобное что можно использовать glTranslatef чтобы заполнить доску шашками с перемещениями

...for (int i = 0; i < 9; i++){glPushMatrix();glTranslatef(ChessX[i], ChessY[i], 0);glScalef(0.05, 0.05, 1);glBegin(GL_LINE_LOOP);Draw_Circle();glEnd();glPopMatrix();}//Рисуем белые пешки ИИfor (int i = 9; i < 18; i++){glPushMatrix();glTranslatef(ChessX[i], ChessY[i], 0);glScalef(0.05, 0.05, 1);glBegin(GL_LINES);Draw_Circle_Fill();glEnd();glPopMatrix();}...

Ходы игрока:

void Game::Move_Up(){//Ход игрока вверхif (Active > 0 && ActiveX != 0 && Matrix[ActiveX-1][ActiveY] == 0)//Если выделенная пешка и не самая верхняя строчка и ячейка выше пустая{Matrix[ActiveX-1][ActiveY] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке выше текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessY[Active-1] += 0.2;//перемещаем координату У пешки вверх для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << " Player MoveUp " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Down(){//Ход игрока внизif (Active > 0 && ActiveX != 7 && Matrix[ActiveX+1][ActiveY] == 0)//Если выделенная пешка и не самая нижняя строчка и ячейка ниже пустая{Matrix[ActiveX+1][ActiveY] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке ниже текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessY[Active-1] -= 0.2;//перемещаем координату У пешки вниз для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "Player MoveDown " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Right(){//Ход игрока вправоif (Active > 0 && ActiveY != 7 && Matrix[ActiveX][ActiveY+1] == 0)//Если выделенная пешка и не самая правая строчка и ячейка справа пустая{Matrix[ActiveX][ActiveY+1] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке справа текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessX[Active-1] += 0.2;//перемещаем координату Х пешки вправо для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "MoveRight " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}void Game::Move_Left(){//Ход игрока влево if (Active > 0 && ActiveY != 0 && Matrix[ActiveX][ActiveY-1] == 0)//Если выделенная пешка и не самая левая строчка и ячейка слева пустая{Matrix[ActiveX][ActiveY-1] = Matrix[ActiveX][ActiveY] ;//присваиваем ячейке слева текущюю(выделенную пешку)Matrix[ActiveX][ActiveY] = 0;//затираем старую ячейку на пустую ChessX[Active-1] -= 0.2;//перемещаем координату Х пешки влево для отрисовкиActiveX = -1;//стираем координаты выделенной пешкиActiveY = -1;//стираем координаты выделенной пешкиActive = -1;//делаем неактивной текущую выделенную фигуруstd::cout << "MoveLeft " << Active << std::endl;firstplayer = false;secondplayer = true;//меняем флаги хода от игрока к ИИ}}

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

void Game::ReccurentWalk(){//Реккурентный ход ИИcurrent = -1, currentI = -1, currentJ = -1;//изначально выделенная пешка не определенаfor (int i = 0; i < Ai.size(); i++)//поиск по массивуif (!Ai[i].ai)//если не завершены ходы для конкретных пешек{if (Check_MoveUp(Ai[i].value) || Check_MoveLeft(Ai[i].value))//Можно ли походить вверх или влево?{current = Ai[i].value;//запоминаем текущую пешкуbreak;}else{//Если походить нельзя стираем из массива ходов пешкуstd::vector<chessai>::iterator position = std::find_if(Ai.begin(), Ai.end(), find_s(Ai[i].value));if (position != Ai.end()) // == vector.end() means the element was not foundAi.erase(position);}}for (int i = 0; i < 8; i++)for (int j = 0; j < 8; j++)if (Matrix[i][j] == current)//ищем в матрице пешку и запоминаем индексы{currentI = i;currentJ = j;break;}if (currentI != -1 && currentJ != -1)//если какая либо найдена ходим либо вверх либо влево{if (!Move_UpAI(currentI, currentJ))if (!Move_LeftAI(currentI, currentJ)){ReccurentWalk();}}else{//если не найдена заполняем массив ходов снова пешкамиchessai tmp;for (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i + 1;Ai.push_back(tmp);}}//ищем ту которая может походить вправо или внизfor (int i = 0; i < Ai.size(); i++)if (!Ai[i].ai){if (Check_MoveRight(Ai[i].value) || Check_MoveDown(Ai[i].value)){current = Ai[i].value;break;}else{//если не может то стираем из массиваstd::vector<chessai>::iterator position = std::find_if(Ai.begin(), Ai.end(), find_s(Ai[i].value));if (position != Ai.end()) // == Vector.end() means the element was not foundAi.erase(position);}}//ищем ее индексы в матрицеfor (int i = 0; i < 8; i++)for (int j = 0; j < 8; j++)if (Matrix[i][j] == current){currentI = i;currentJ = j;break;}//ходим вправо или внизif(!Move_RightAI(currentI, currentJ))if (!Move_DownAI(currentI, currentJ)){std::cout <<"Artificial Intellegence asked: WTF?" << std::endl;}}chessai tmp;if(Ai.empty())//если список ходов пуст заполняем снова всемиfor (int i = 0; i < 18; i++){ai[i] = false;if (i > 8){tmp.ai = ai[i];tmp.value = i + 1;Ai.push_back(tmp);}}}

Ну и собственно опишу словами, что делает ИИ. Он собирает три пешки вверху на 0-ой строке и если ходить нельзя влево, то двигает вверх остальные 3 пешки на 1 строку, аналогично и на 2-ю строку. Если ходить влево и вверх нельзя, то он берет любую шашку и двигает либо вниз либо вправо если можно конечно, то есть стремится всегда двигать влево или вверх( при условии что выше меньше двух шашек в строке).

Ну собственно и геймплей:

https://youtu.be/XaQVeSKdQcs

И ссылка на исходный код:

https://github.com/Beginerok/DominiGames

Подробнее..

Препродакшн игровых проектов как оценить объем работ на старте и не сгореть к дедлайну

15.04.2021 12:10:33 | Автор: admin

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

Идея GaaS Game as a Service неразрывно связана с понятием MMO. Именно по этому принципу мы оперируем своим мобильным PvP-шутером War Robots: такие игры с обновлениями получают постоянный поток нового контента и фичей на основе обратной связи с комьюнити. Это позволяет игре оперативно учитывать пожелания игроков и задавать новые тренды. В то же время модель GaaS усложняет внедрение в игру глобальных изменений, таких как ремастеринг графики ведь продукт не прекращает жить своей жизнью, пока вы год готовите обновление. Тем не менее, такие изменения необходимы, когда речь идет об играх-долгожителях.

War Robots исполнилось семь лет. Это немало и для большого ПК-проекта, а в случае мобилок и вовсе ломает представления о типичной продолжительности их жизни. Когда игра только вышла, рынок был наводнен match 3 и фермами, и входить на него с mid-core шутером было весьма рисково. Но вот мы здесь: рынок давно изменился, а War Robots все еще остается лидером в своем сегменте.

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

Ремастер фича или проект со своей командой?

Итак, мы решили делать ремастер. Понятие это не новое, но к мобильным играм до этого не применявшееся. Но мы решили принять этот челлендж.

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

Первым делом возник вопрос, как мы хотим реализовать эту идею. Как большую, комплексную фичу? При таком подходе она будет в постоянном конфликте с другими фичами с более коротким time-to-market и более четким business value. Как следствие, команде будет некогда ей заниматься, и сроки будут постоянно сдвигаться. Так себе сценарий, которого хотелось бы избежать, если мы задались целью выпустить обновление графики в обозримом будущем, а не когда-нибудь через несколько лет.

В результате удалось убедить руководство студии и инициировать War Robots Remastered как отдельный проект со своей командой и целью:

Привлечь новую аудиторию к продукту War Robots, вернуть старую и повысить вовлечение текущей базы игроков. Для этого подготовить Remastered-версию продукта и раскатать релиз в продакшн на мобильных платформах App Store and Google Play до конца Q3 2020.

С целью проекта определились. Можно начинать собирать команду и приступать к оценке скоупа работ.

Как оценить объем работы, которую раньше никто не делал

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

Почему?Нельзя из старых технологий выжать крутую картинку на высоких FPS. Чтобы сделать level-up продукта, необходимо совершить апгрейд технологической базы. Невозможно создавать новых мехов с текстурами высокого разрешения и кучей полигонов на старой базе: ведь робот постоянно передвигается, ведет бой, на карте их 12 по 6 в каждой из соперничающих команд, и девайс должен просчитывать все их действия, эффекты и партиклы сражения. Если делать так, как описано выше, на выходе будет очень низкий фреймрейт даже на топовых девайсах то же самое, как если попытаться запустить на GeForce GTX 750 игру 2020 года.

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

Чтобы при ремастеринге меха не создавать трех разных роботов по одному на каждый пресет качества, нужно было изменить пайплайн, с помощью которого роботов в HD-качестве просто даунскейлить до MD и LD. А ведь таких роботов у нас 81 штука, не говоря уже дополнительно о 109 пушках и 83 элементах снаряжения.Мы хотим, чтобы наши игроки могли продолжить играть в War Robots а для этого нужно, чтобы на их текущих девайсах игра выглядела круто, и не было сильной просадки FPS. Мы довольно быстро стали понимать, что для этого нам необходим технологический стек, тянущий на AAA.

Так выглядели скриншоты Work in ProgressТак выглядели скриншоты Work in Progress

В результате нам нужно было проделать следующее:

  • Создание трех пресетов качества: LD низкое качество для low-end девайсов, MD среднее, HD высокое;

  • Рефакторинг роботов, пушек, карт и системы эффектов: переход на новый пайплайн создания и перевод единиц контента для разных пресетов качества;

  • Ремастеринг роботов, пушек и карт: пересборка всех существующих и создаваемых единиц контента в игре для каждого пресета качества, в том числе анимаций; полное пересоздание 4 из 13 карт, а впоследствии и остальных;

  • Освещение на старых картах: обновление технологий освещения и тюнинг оставшихся карт;

  • Оптимизация UI: рефакторинг UI-ассетов, интеграция упрощенных UI шейдеров;

  • Новые атласы текстур: рефакторинг атласов, реорганизация директорий в проекте;

  • Новые визуальные эффекты: создание новых VFX для пушек, мехов и карт, разные эффекты для разных пресетов качества; для VFX еще и новый движок;

  • Перепаковка всех ресурсов проекта для использования других механизмов дистрибуции продукта из сторов к игрокам;

  • Графический кодинг: внедрение стека новых технологий и выполнение работ по созданию и оптимизации крутого графония на экране как мобильного устройства, так и ПК; сердце всего ремастера.

Как оценить время, которое займет выбранный объем работ

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

Можно ли оценить весь ремастер на старте?

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

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

Поэтому действуем по следующей схеме:

  • Решаем, что мы хотим сделать и что получить на выходе;

  • Декомпозируем;

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

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

Допустим, после первичной оценки вы пересобрали на пробу 10 мехов, отлогировали время, аппроксимировали его. И. не попали в общую оценку.

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

Какое решение? Идти путем итераций.

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

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

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

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

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

  • уклонению;

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

  • передаче третьей стороне;

  • принятию (активному или пассивному).

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

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

Что делать, если со своим объемом работ вы не вписываетесь в заданный дедлайн

Легко попасть в ситуацию, когда планы монументальные, хотелок много, а времени мало. Что же делать в таком случае?

Например, учиться от чего-то отказываться.

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

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

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

Установка правильного контекста. Как постоянно менять план и не шокировать этим команду

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

Так, мы не планировали делать Ultra Low-пресет качества, но были вынуждены к нему прибегнуть, потому что тесты показали, что текущие лоу-энд девайсы не тянут даже LD. Новый пресет качества отдельная работа. Невозможно впихнуть ее в те же сроки, что и раньше. Что делать в таких случаях? Мы делаем этот пресет, но отказываемся от какой-то другой работы.Нужно изначально быть готовым к тому, что при всей точности оценок а это мало достижимо со 100% точностью, границы между обязанностями двух команд одного продукта могут размыться и требовать постоянного уточнения. При этом изменения в roadmap одного проекта будут влиять на roadmap другого.

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

Подводя итоги: что стоит учитывать на этапе предпродакшена

  • Необходимо закладывать буфер на неизвестность на старте.

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

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

  • Управлять рисками на стадии препрода еще при инициации проекта и продолжать на последующих этапах продакшн.

  • Стоит выделить одного ответственного человека за каждый фронт работ.

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

Автор материала Дмитрий Осипов, ведущий руководитель проектов War Robots и War Robots Remastered

Подробнее..

Программа для physics-based анимации персонажей Cascadeur вышла в ранний доступ

14.04.2021 20:10:55 | Автор: admin


Спустя 10 лет разработки и 2 года бета-тестирования Cascadeur, программа для создания физически корректной персонажной анимации, вышел в ранний доступ! Пользователям доступны 4 варианта подписки, один из которых совершенно бесплатный.

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


Данный релиз является итогом двухлетнего бета-тестирования, в котором приняли участие более 70.000 человек из индустрии по созданию компьютерной графики и визуальных эффектов. Однако на этом разработка Cascadeur не заканчивается.

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



Скачать программу можно на официальном сайте проекта cascadeur.com/ru. Здесь же вы найдете все необходимые материалы для того, чтобы начать анимировать прямо сейчас.

Узнать о Cascadeur больше:

Вложенность нейросетей инструмента автопозинга в Cascadeur
Почему 12 принципов Диснея недостаточно
Cascadeur: будущее игровой анимации
Подробнее..

Интеграция и серверная валидация инаппов для стора Google Play как защититься от читеров

25.05.2021 20:20:29 | Автор: admin

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

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

Как уже говорилось в блоге, наш флагманский проект это мобильный PvP-шутер с DAU около 1 млн пользователей, большинство из которых на Android. В игре сотни видов оружия и предметов. И чтобы защититься от взлома, естественно, нужна валидация покупок. Пойдем по порядку.

В Google Play наш проект использует consumable in-apps, которые после успешной покупки и валидации начисляются игроку и сразу потребляются. По историческим причинам для Google Play мы используем плагин от Prime31.

Отмечу, что если бы мы сегодня добавляли встроенные покупки с нуля на эти платформы, то взяли бы Unity IAP (а, например, на Huawei публиковались бы через Unity Distribution Portal).

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

Перейдем к коду покупки и валидации инаппов.

На старте приложения подписываемся на события покупки:

// GoogleIABManager  класс из плагина Prime31GoogleIABManager.purchaseSucceededEvent += HandleGooglePurchaseSucceeded;

Когда игрок нажимает на инапп в интерфейсе запускаем покупку:

// GoogleIAB  класс из плагина Prime31GoogleIAB.purchaseProduct(productId);

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

public interface IMarketPurchase{  string ProductId { get; }   string OrderId { get; }  string PurchaseToken { get; }  object NativePurchase { get; }}class GoogleMarketPurchase : IMarketPurchase{  internal GoogleMarketPurchase(GooglePurchase purchase)  {     _purchase = purchase;  }  public string ProductId => _purchase.productId;  public string OrderId => _purchase.orderId;  public string PurchaseToken => _purchase.purchaseToken;  public object NativePurchase => _purchase;  private GooglePurchase _purchase;}internal static class MarketPurchaseFactory{// GooglePurchase  класс из плагина Prime31  internal static IMarketPurchase CreateMarketPurchase(GooglePurchase purchase)  {     return new GoogleMarketPurchase(purchase);  }}private void IapManagerOnBuyProductSuccess(PurchaseResultInfo purchaseResult){  var purchaseData = new InAppPurchaseData(purchaseResult.InAppPurchaseData);  IMarketPurchase marketPurchase = MarketPurchaseFactory.CreateMarketPurchase(purchaseData);  ValidatePurchase( marketPurchase );}

Отправляем покупку на наш сервер на валидацию:

private void ValidatePurchase(IMarketPurchase purchase){  var request = new InappValidationRequest  {     orderId = purchase.OrderId,     productId = purchase.ProductId,     purchaseToken = purchase.PurchaseToken,     OnSuccess = () => ProvidePurchase(purchase),     OnFail = () => Consume(purchase)  };   WebSocketCallbacks.Subscribe(ServerEventNames.PurchasePrevalidate, PrevalidatePurchaseHandler);   Dictionary<object, object> data = new Dictionary<object, object>();  data.Add("orderId", request.orderId);  data.Add("productId", request.productId);  data.Add("data", request.purchaseToken);  int reqId = WebSocketManager.Instance.Send(ServerEventNames.PurchasePrevalidate, data);   _valdationRequests.Add(reqId, request);}

Если валидация проходит неуспешно потребляем (Consume) продукт без начисления пользователю.

Если все хорошо потребляем продукт с начислением пользователю:

void ProvidePurchase(IMarketPurchase purchase){  GiveInGameCurrencyAndItems(purchase);  Consume(purchase);}

Важный момент: метод Consume перед отправкой в магазин запроса на потребление запоминает, что мы уже начислили покупку игроку. Это нужно, если из-за проблем с сетью (или каких-то других) запрос на консьюм не дойдет до магазина. В таком случае, когда после перезапуска приложения нам придут незаконсьюмленные покупки, мы увидим, за какие из них уже начисляли игроку валюту и предметы.

Обработчик ответа с сервера:

private const int ERROR_CODE_SERVER_ERROR = 30;private const int ERROR_CODE_VALIDATION_ERROR = 31;private void PrevalidatePurchaseHandler(Dictionary<string, object> response){  int reqId = Convert.ToInt32(response["req_id"], CultureInfo.InvariantCulture);  _valdationRequests.TryGetValue(reqId, out InappValidationRequest request);  if (request == null)     return;  _valdationRequests.Remove(reqId);  if (response["status"].Equals("ok"))  {     request.OnSuccess();  }  else  {     int code = Convert.ToInt32(response["err_code"], CultureInfo.InvariantCulture);     switch (code)     {        case ERROR_CODE_VALIDATION_ERROR:           request.OnFail();           break;        case ERROR_CODE_SERVER_ERROR:           CoroutineRunner.DeferredAction(5f, () => TryValidateAgain());           break;        default:           // неизвестная ошибка, начисляем инапп (поступаем в пользу игрока)           request.OnSuccess(null);           break;     }  }}

В случае, если сервер вернул OK в статусе валидации, производим начисление и консьюм покупки. Если сервер вернул неизвестную ошибку, трактуем результат валидации в пользу игрока.

Для следующего раздела передаю слово нашему серверному программисту Ире Поповой.

Серверная валидация

Валидация на сервере состоит из двух этапов:

  • превалидация когда данные по платежу отправляются на сервер соответствующей платформы для проверки валидности;

  • начисление в случае успешно пройденной валидации купленных позиций.

Сначала сервер получает в качестве входных параметров данные, необходимые для проведения валидации. В Android это id позиции и токен. Методы валидации являются платформо-зависимыми. Но, как правило, включают в себя логику отправки данных на сервер валидации соответствующей платформы, обработку полученного результата и возврат соответствующего ответа на клиент. Дополнительно результат валидации записывается в redis для последующей быстрой проверки при начислении.

def validate_receipt(self, uid, data, platform):    InAppSlot = PlayerProgress.first(f"player_id={uid} AND slot_id='35'")    if not InAppSlot:        raise RuntimeError(f"Fail get slot purchases: not found player:{uid} data:{data}")    tid = data.get("tid")    params = []    orders_data = []    valid_orders = []    if not tid or tid in InAppSlot.content:        return False    params = str(tid).split(self.IN_APP_ID_SEPARATOR)    if platform == "ios":        transaction_id = params[0]        product_id = params[1]        orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id)        error("[VALIDATION] {} {} {}".format(transaction_id, product_id, orders_data))    elif platform == "android":        product_id = params[1]        purchase_token = data.get("data")        orders_data = self._get_receipt_android(product_id, purchase_token)    elif platform == "amazon":        receipt_sku = params[0]        user_id = params[1]        orders_data = self._get_receipt_amazon(user_id, receipt_sku)    elif platform == "huawei":        product_id = params[1]        orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""), data.get("account_flag", 0))    elif platform == "udp":        product_id = params[1]        orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", ""))    elif platform == "samsung":        product_id = params[1]        transaction_id = params[0]        product_id = params[1]        orders_data = self._get_receipt_samsung(data.get("data", ""), product_id)    else:        error("[InAppValidator] unknown platform")        return False    if not orders_data:        error(f"[InAppValidator] fail get receipt {platform} player:{uid} data:{data}")        return False    key = f"inapp:{uid}:{tid}"    for order in orders_data:        if not  order.is_success():            continue        valid_orders.append(order)        try:            self.inapp_redis.setex(key, order.to_json(), 86400)        except Exception as ex:            exception(f"[InAppValidator] fail save inapp to redis: {ex}")    if not valid_orders:        warning(f"[InAppValidator] not valid receipt {orders_data[0].order_id}")       return False    return True

Пример получения данных с соответствующего сервера валидации для Android. Для обращения к серверу Google были использованы пакеты Google для Python apiclient и oauth2client.

def _get_receipt_android(self, product_id, token):    if not self.android_authorized:        self._android_auth()    debug(f"[InAppValidator] android product_id: {product_id}, token: {token}")    try:        product = self.android_publisher.purchases().products().get(            packageName=config.GOOGLE_SERVICE_ACCOUNT['package_name'], productId=product_id, token=token).execute()            except client.AccessTokenRefreshError:        self.android_authorized = False        return self._get_receipt_android(product_id, token)    except google_errors.HttpError as ex:        if ex.resp.status == 401 or ex.resp.status == 503:            self.android_authorized = False            return self._get_receipt_android(product_id, token)        return False    if not product:        warning("[InAppValidator] android product is NONE")        return None    order_id = product.get('orderId')    if not order_id:        warning(f"order_id is NONE: {product}")        return None    return [Receipt(order_id, product.get('purchaseState', -1), product_id)]class Receipt:    def __init__(self, order_id, status, product_id, user_id=None, expire=0, trial=0, refund=0, latest_receipt=''):        self.order_id = order_id        self.status = status        self.product_id = product_id        self.user_id = user_id        self.expire = expire        if str(trial) == 'true':            self.trial = 1        else:            self.trial = 0        self.refund = refund        self.latest_receipt = latest_receipt    def is_success(self):        return self.status == 0    def is_canceled(self):        return self.status == 3    def is_valid(self):        return self.order_id and self.product_id    def to_dict(self):        return {"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt}    def to_json(self):        return json.dumps({"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt})

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

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

Команда валидации:

def validate_receipt(self, data):    neededSlotsNames = [self.slotName]    self.slots = self.get_slots_data(*neededSlotsNames)    InAppSlot = self.slots.get(self.slotName, [])    tid = data.get("tid")    platform = data.get("pl")    params = []    orders_data = []    valid_orders = []    if not tid:        self.ThrowFail("not found required parameter")    elif tid in InAppSlot:        self.ThrowFail("already in slot")    if not self.IsFail():        params = str(tid).split(self.IN_APP_ID_SEPARATOR)    if not self.IsFail():        inapp_storage = InappStorage.get_instance()        if inapp_storage.exists_transaction(self.platform, params[0]):            self.ThrowFail("already_purchased {0} d".format(params[0]),                           VALIDATOR_RESULT_CODE.ALREADY_PURCHASED)            self.FinalizeRequest({self.slotName: InAppSlot}, data)            return        # Try get from redis        player_platform = self.platform        if platform is not None and int(platform) == 4:            player_platform = "udp"        _prevalidate_order = self.inapp_redis.check_tid(self._player_id, tid)        if _prevalidate_order:            orders_data = Receipt.from_json(_prevalidate_order)        elif player_platform == "ios":            transaction_id = params[0]            product_id = params[1]            if not transaction_id or not product_id:                self.ThrowFail(f"fail get receipt {self.platform}")            else:                orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id)        elif player_platform == "android":            product_id = params[1]            purchase_token = data.get("data")            orders_data = self._get_receipt_android(product_id, purchase_token)        elif player_platform == "amazon":            receipt_sku = params[0]            user_id = params[1]            orders_data = self._get_receipt_amazon(user_id, receipt_sku)        elif player_platform == "huawei":            product_id = params[1]            orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""),                                                   data.get("account_flag", 0), data.get("subscribe"))        elif platform == "udp":            product_id = params[1]            orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", ""))        elif platform == "samsung":            product_id = params[1]            transaction_id = params[0]            product_id = params[1]            orders_data = self._get_receipt_samsung(data.get("data", ""), product_id)        else:            self.ThrowFail("unknown platform")    if not orders_data:        self.ThrowFail(f"fail get receipt {player_platform} {self.platform}")    if not self.IsFail():        for order in orders_data:            if order.is_success():                valid_orders.append(order)        if not valid_orders:            self.ThrowFail("already_purchased {0}".format(orders_data[0].order_id),                           VALIDATOR_RESULT_CODE.ALREADY_PURCHASED)        else:            InAppSlot.append(tid)            self.SetRequestSuccessful()    if self._player_id in LOG_PLAYER_IDS:        HashLog.error(f"[INAPP] id:{self._player_id} receipt:{data}")    self.FinalizeRequest({self.slotName: InAppSlot}, data)

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

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

Кроме того, в завершение начисления отправляется соответствующая статистика на сервер аналитики.

На что еще обратить внимание

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

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

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

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

Дополнительные ссылки

И последнее: когда мы реализовывали валидацию инаппов для Google Play несколько лет назад, нам оказалось полезной статья на Хабре, вам она тоже может пригодиться.

Также использовали решения, предложенные здесь и здесь. Ссылка на документацию по API серверной валидации Google здесь.

Подробнее..

Шахматы на Delphi. Как я изобретал велосипед

11.04.2021 00:10:54 | Автор: admin

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

Начало

Это случилось в 2009 году: я решил написать простую шахматную программу, чтобы попрактиковаться в разработке игрового AI. Сам я не шахматист и даже не сказать что любитель шахмат. Но задача для тренировки вполне подходящая и интересная. Кроме того, играя в шахматы на доске или в программе, мне всегда было любопытно, почему тот или иной ход сильнее другого. Хотелось возможности наглядно видеть всё дерево развития шахматной позиций. Такой фичи в других программах я не встречал - так почему бы не написать самому? Ну а раз уж это тренировка - то придумывать и писать нужно с нуля, а не изучать другие алгоритмы и писать их собственную реализацию. В общем, думаю, дня за три можно управиться и сделать какой-то рабочий вариант :-)

Первая версия

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

Надо сказать, что на тот момент у меня был 2-ядерный процессор с 2 или 4 Гб памяти (точно уже не помню), 32-битная винда и 32-битный компилятор Turbo Delphi Explorer. Так что если временем работы ещё можно было как-то пожертвовать, то доступная процессу память была ограничена 2Gb. Про PE flag, расширяющий user memory до 3Gb я тогда не знал. Впрочем, поскольку память кушают и система, и Delphi и другие программы - для шахмат, чтобы не уходить в своп, доступно менее гигабайта.

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

  • UI - основное окно, отрисовка доски с фигурами.

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

  • AI:оценка - оценочная функция позиции.

  • AI:перебор - поиск в ширину через очередь.

  • UI:браузер - окно визуализации дерева поиска, в котором можно наглядно изучать как всё работает.

Выяснилось что:

  • Поиск на глубину 3 полухода работает быстро - меньше секунды, и расходует немного памяти - 5-15 Мб. А вот поиск на глубину 4 полухода работает уже довольно долго и расходует большую часть доступной памяти. В отдельных ситуациях памяти и вовсе не хватает.

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

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

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

Оценочная функция

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

В итоге пришел к примерно такому алгоритму:

  • Для каждого игрока:

    • Подсчитать стоимость фигур: конь - 3, ладья - 5 и т.д. Начальная стоимость пешки - 1, но она растёт по мере её продвижения.

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

    • Определение какие поля находятся под боем и кем. Это медленная операция - основная часть времени выполнения оценочной функции тратится именно здесь. Зато польза от неё колоссальная! Незащищённая фигура под боем на ходу противника - минус фигура. Защищённая - минус разность стоимости фигур. Это позволяет получить эффект углубления поиска на 1-2 уровня.

    • Если остался один король: штраф за расстояние от центра доски и штраф за расстояние до короля противника. Такая формула в эндшпиле заставляет AI стремиться прижать короля противника к краю доски, т.е. получить позицию, из которой можно найти возможность поставить мат.

  • Итоговая оценка = (white_rate - black_rate) * (1 + 10 / (white_rate + black_rate)). Эта формула делает разницу более значимой в эндшпиле, заставляя отстающего игрока избегать размена фигур, а ведущего - наоборот, стремиться к размену.

Углубление поиска

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

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

В итоге алгоритм получился такой:

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

  2. Оценка дерева алгоритмом минимакс.

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

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

  5. Переход к следующей стадии: добавляем вес всем листьям дерева и продолжаем поиск. После завершения переходим к п 2.

Критерии принятия решения:

  • Осталась единственная ветка - выбора нет.

  • Одна из веток имеет оценку существенно более высокую чем у остальных - выбираем её.

  • Истекло время на ход - выбираем ветку с наилучшей оценкой.

Кэширование

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

Всё просто: нужно вычислить хэш от позиции и если в кэше уже есть оценка с таким хэшем - использовать её вместо вычисления. А если нет - вычислить и сохранить в кэше оценок. Основная проблема - нужна достаточно качественная и быстрая хэш-функция. После множества экспериментов получилось написать приемлемую функцию, вычисляющую 64-битный хэш. Общее количество возможных шахматных позиций значительно больше 264, но количество позиций, оцениваемых в ходе партии значительно меньше 232, поэтому более-менее качественная хэш-функция должна свести вероятность хэш-коллизий к минимуму.

Процент "попаданий" в кэш в ходе игры получился в районе 30-45%, но в эндшпиле достигает 80-90%, что даёт ускорение почти в 5-10 раз, а следовательно позволяет увеличить глубину поиска. Неплохой выигрыш!

Что получилось?

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

  • AI работает в один поток - ресурс CPU задействован не полностью.

  • А что если предоставить больше памяти?

  • AI думает только во время своего хода. Почему бы не использовать время соперника для просчёта наиболее вероятных продолжений?

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

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

Дальнейшее развитие

Недавно наткнувшись на этот заброшенный проект, решил всё-таки привести его код в порядок и доработать. Доработки сделал такие:

  • Многопоточность: сейчас у меня уже 8-поточный, а не 2-поточный CPU, поэтому многопоточный вариант даёт серьёзную прибавку в скорости.

  • 64-битный режим: кроме возможности использовать больше памяти, было любопытно, будет ли алгоритм работать быстрее на архитектуре x64. Как ни странно, оказалось что нет! Хотя отдельные функции на x64 работают быстрее, в целом версия x86 оказалась на 5-10% быстрее. Возможно 64-битный компилятор Delphi не очень хорош, не знаю.

  • Больше памяти: даже в 32-битном режиме за счёт PE-флага расширения адресного пространства доступной памяти стало больше. Однако практика показала, что больше 1 Гб памяти все-равно не нужно - разве что для хранения "обрезанных" ветвей дерева. К усилению игры увеличение памяти не приводит.

  • Непрерывность поиска: теперь дерево поиска не создаётся с нуля каждый ход, а создаётся лишь в начале партии. Когда делается ход - неактуальные ветви дерева обрезаются а поиск продолжается с того состояния, которое было. В том числе и во время хода соперника - идёт проработка перспективных продолжений, поэтому когда соперник сделает ход, поиск продолжится не с нуля. Быть может и вовсе получится сразу же сделать ход. Момент времени, когда игрок сделает ход - это фактор случайности, который делает игру недетерминированной. Теперь уже нельзя запомнить выигрышную последовательность ходов.

  • Оценка дерева. Ввёл новый атрибут узла - качество оценки. Качество равно сумме качества всех прямых потомков, умноженное на коэффициент затухания. Т.е. качество оценки показывает проработанность ветки.

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

  • База оценок и самообучение. В процессе игры какие-то позиции детально прорабатываются и получают оценку с высоким качеством. Почему бы не сохранять такие оценки для использования в других партиях? Это ещё одна фича, которая делает игру недетерминированной.

В результате этих доработок AI стал сильнее, и вполне уверенно обыгрывает старую версию игры. Я провел несколько партий против AI на chess.com и выяснил, что уровень моей программы примерно соответствует рейтингу 1800-1900. Прогресс есть, и это хорошо!

Программирование игрового AI - занятие чертовски затягивающее: всегда хочется добиться большего. И хотя у меня по прежнему есть масса идей для дальнейшего развития, наступает момент, когда надо остановиться. Думаю, он наступил. Однако если кто-либо желает - может взять мой код, побаловаться, поэкспериментировать, что-нибудь реализовать. Благо, сейчас Delphi доступен каждому благодаря бесплатной Community Edition, не говоря уже про бесплатный Free Pascal и Lazarus. Код проекта (а также скомпилированный exe-шник) можно взять тут: https://github.com/Cooler2/chess (для компиляции понадобится также кое что из https://github.com/Cooler2/ApusGameEngine). Спасибо всем, кто дочитал :-)

Подробнее..

Еще пять инструментов против читеров на мобильном проекте с DAU 1 млн пользователей

27.04.2021 20:11:49 | Автор: admin

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

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

  • Защита от измененных версий.

  • Photon Plugin.

  • Серверная валидация инаппов.

  • Защита от взлома оперативной памяти.

  • Собственная аналитика.

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

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

Решение 6. Защита от измененных версий

В дополнительные места мы расставили защиту от переподписывания версий, лаунчеров (на Android) и твиков (на iOS), спрятав уже в обфусцированном коде.

Проверка на твики (iOS)

На устройствах с Jailbreak с помощью Cydia пользователи могут устанавливать твики, которые способны внедрять свой код в системные и установленные приложения. Каждый твик имеет информацию (файл *.plist), с какими бандлами они должны работать.

Механизм детекта осуществляется проверкой этих файлов в папке /Library/MobileSubstrate/DynamicLibraries/ (на наличие внутри нашего бандла).

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

string finalPath = string.Empty;string substratePath = "/Library/MobileSubstrate/DynamicLibraries/";bool bySymlink = false;if (!Directory.Exists(substratePath)) //Если папки не существует (скрыт твиком xCon), то пытаемся получить доступ к файлам через созданный нами симлинк{string symlinkPath = CreateSymlimk(substratePath);if (!string.IsNullOrEmpty(symlinkPath)){bySymlink = true;finalPath = symlinkPath;}}else{finalPath = substratePath;}bool detected = false;string detectedFile = string.Empty;try{if (!string.IsNullOrEmpty(finalPath)){string[] plistFiles = Directory.GetFiles(finalPath, "*.plist"));foreach (var plistFile in plistFiles){if (File.Exists(plistFile)){StreamReader file = File.OpenText(plistFile);string con = file.ReadToEnd();string bundle = "app_bundle"; if (con.Contains(bundle)){detectedFile = plistFile;detected = true;break;}}}}}catch (Exception ex){Debug.LogError(ex.ToString());}

Но также есть твики, которые запрещают создание симлинков по проверяемому нами пути (KernBypass, A-Bypass). При их наличии мы не можем осуществить проверку, поэтому считаем это за возможное читерство.

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

Детект KernBypass (который был активен в отношении нашего бандла):

if (File.Exists("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist") {StreamReader file = File.OpenText("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist"); string con = file.ReadToEnd();if (con.Contains("app_bundle") {//detected}}

Определение запуска через лаунчер (Android)

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

При таком запуске у нас все равно сохраняется доступ ко всем файлам, к которым имеет доступ сам лаунчер. Самый простой способ детекта это проверить доступ к материнской папке от нашего приложения (dataDir в applicationInfo) через функцию access (в нативном коде). В обычном случае операционная система не предоставит доступ, а в случае лаунчера это будет папка, которая все еще находится внутри Persistent Data приложения.

Код для плагина на C:

JavaVM*java_vm;jint JNI_OnLoad(JavaVM* vm, void* reserved) {        java_vm = vm;    return JNI_VERSION_1_6;}int CheckParentDirectoryAccess(){    JNIEnv* jni_env = 0;    (*java_vm)->AttachCurrentThread(java_vm, &jni_env, NULL);    jclass uClass = (*jni_env)->FindClass(jni_env, "com/unity3d/player/UnityPlayer");    jfieldID activityID = (*jni_env)->GetStaticFieldID(jni_env, uClass, "currentActivity", "Landroid/app/Activity;");    jobject obj_activity = (*jni_env)->GetStaticObjectField(jni_env, uClass, activityID);    jclass classActivity = (*jni_env)->FindClass(jni_env, "android/app/Activity");        jmethodID mID_func = (*jni_env)->GetMethodID(jni_env, classActivity,                                                      "getPackageManager", "()Landroid/content/pm/PackageManager;");        jobject pm = (*jni_env)->CallObjectMethod(jni_env, obj_activity, mID_func);        jmethodID pmmID = (*jni_env)->GetMethodID(jni_env, classActivity,                                                      "getPackageName", "()Ljava/lang/String;");        jstring pName = (*jni_env)->CallObjectMethod(jni_env, obj_activity, pmmID);    jclass pm_class = (*jni_env)->GetObjectClass(jni_env, pm);    jmethodID mID_ai = (*jni_env)->GetMethodID(jni_env, pm_class, "getApplicationInfo","(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;");    jobject ai = (*jni_env)->CallObjectMethod(jni_env, pm, mID_ai, pName, 128);    jclass ai_class = (*jni_env)->GetObjectClass(jni_env, ai);        jfieldID nfieldID = (*jni_env)->GetFieldID(jni_env, ai_class,"dataDir","Ljava/lang/String;");    jstring nDir = (*jni_env)->GetObjectField(jni_env, ai, nfieldID);        const char *nDirStr = (*jni_env)->GetStringUTFChars(jni_env, nDir, 0);    char parentDir[200];    snprintf(parentDir, sizeof(parentDir), "%s/..", nDirStr);    if (access(parentDir, W_OK) != 0)    {         return 1;    }else{ return 0;}}

Защита от переподписи apk (Android)

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

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

Получение хеша подписи в С# через обращение в Java-код:

Lazy<byte[]> defaultResult = new Lazy<byte[]>(() => new byte[20]);            if (Application.platform != RuntimePlatform.Android)                return defaultResult.Value;#if UNITY_ANDROIDvar unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");if (unityPlayer == null)throw new InvalidOperationException("unityPlayer == null");var _currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");if (_currentActivity == null)throw new InvalidOperationException("_currentActivity == null");            var packageManager = _currentActivity.Call<AndroidJavaObject>("getPackageManager");            if (packageManager == null)                throw new InvalidOperationException("getPackageManager() == null");            // http://developer.android.com/reference/android/content/pm/PackageManager.html#GET_SIGNATURES            const int getSignaturesFlag = 64;            var packageInfo = packageManager.Call<AndroidJavaObject>("getPackageInfo", PackageName, getSignaturesFlag);            if (packageInfo == null)                throw new InvalidOperationException("getPackageInfo() == null");            var signatures = packageInfo.Get<AndroidJavaObject[]>("signatures");            if (signatures == null)                throw new InvalidOperationException("signatures() == null");            using (var sha1 = new SHA1Managed())            {                var hashes = signatures.Select(s => s.Call<byte[]>("toByteArray"))                    .Where(s => s != null)                    .Select<byte[], byte[]>(sha1.ComputeHash);                var result = hashes.FirstOrDefault() ?? defaultResult.Value;                return result;            }#else            return defaultResult.Value;#endif

Решение 7. Photon Plugin

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

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

Photon Plugin доступен на тарифе Enterprise Cloud и пишется на С#. Он запускается на серверах Photon и позволяет мониторить пересылаемый между пользователями игровой трафик, добавлять серверную логику, которая может:

  • блокировать или добавлять сетевые сообщения;

  • контролировать изменения свойств комнат и игроков;

  • кикать из комнаты;

  • взаимодействовать при помощи http-запросов со сторонними серверами.

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

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

Решение 8. Серверная валидация иннапов

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

Валидация на сервере состоит из двух этапов:

  1. Превалидация. Когда данные по платежу отправляются на сервер соответствующей платформы для проверки валидности.

  2. Начисление. В случае успешно пройденной валидации купленных позиций.

Сначала сервер получает в качестве входных параметров данные, необходимые для проведения валидации (например, на Android это id инаппа и токен).

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

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

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

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

Кроме того, в завершение начисления отправляется соответствующая статистика на сервер аналитики.

Подробнее про интеграцию инаппов и серверную валидацию вместе с кодом расскажем в отдельной статье.

Решение 9. Защита от взлома оперативной памяти

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

Для их защиты они засаливаются при помощи случайно сгенерированной соли:

 internal int Value{get { return _salt ^ _saltedValue; }set { _saltedValue = _salt ^ value; }}

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

private static int[] refNumbers;internal static void Start(){refNumbers = new int[1000];for (int i = 0; i < refNumbers.Length; i++) {refNumbers[i] = i;}}internal static bool Check(){for (int i = 0; i < 1000; i++) {if (!refNumbers [i].Equals(i))return true;}}

Решение 10. Собственная аналитика

Изначально мы пользовались платным решением от devtodev и бесплатным от Flurry. Основная проблема была в отсутствии детализации происходящих в игре событий. Мы собирали только агрегированные данные и поверхностные метрики.

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

Все пользовательские ивенты отправляются в одну большую SQL-евскую базу. Там есть как элементарные ивенты (игрок залогинился, сколько раз в день он залогинился и так далее), так и другие. Например, прилетает ивент, что игрок покупает оружие за столько-то монет, а вместо суммы написано 0. Очевидно, что он сделал что-то неправомерное.Большинство выгрузки с подозрительными действиями нарабатываются с опытом. Например, у нас есть скрипт, который показывает, что столько-то людей с конкретными id получили определенное большое количество монет. Но это не всегда читеры обязательно нужно проверять.

Также читеров опознаем по несоответствию значений начисления валют. Аналитик знает, что за покупку инаппа начисляется конкретное количество гемов. У читеров часто это количество бывает 9999 значит, что-то взломали в памяти. Еще бывают игроки с аномальными киллрейтами. По ним у нас тоже есть специально обученное поле, и когда появляется пользователь, у которого киллрейт 15 или 30, становится понятно, что, скорее всего, это читер.В основном отслеживанием занимается один скрипт, который пачкой прогоняет по детектам и сгружает все в таблицу. Аналитики получают id и видят игроков, которые залогинились утром с огромным количеством голды, в соседнем листе лежат игроки, открывшие 1000 сундуков, в следующем игроки с тысячей гач и так далее. Затем вариантов несколько.

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

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

Одновременный релиз всех решений

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

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

Всего на глобальный ввод большинства защит ушло около семи месяцев.

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

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

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

Подробнее..

Тренды для разработчиков игр зачем добавлять режим Among Us в свой проект

12.05.2021 20:07:31 | Автор: admin

В прошлом году мы добавили в мобильный PvP-шутер режим Imposter по мотивам игры Among Us переработанную и переосмысленную в 3D мини-игру с новыми механиками и фичами. Она стала популярным местом входа в проект даже среди тех, кто раньше не слышал про Pixel Gun. Другой пример: когда зарелизился Fortnite, мы за один день потеряли треть онлайна, и чтобы вернуть игроков, сделали свою реализацию батлрояля. Решение сработало оказалось, им просто не хватало королевской битвы. То есть тренды, это не просто желание хайпануть это возможность разговаривать с аудиторией на их языке.

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

Кому и зачем нужны тренды

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

Футуристичное обновление League of Legends в даты предполагаемого выхода Cyberpunk 2077Футуристичное обновление League of Legends в даты предполагаемого выхода Cyberpunk 2077

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

Добавив в игру некий тренд, вы увеличиваете шансы попасть в подборки сторов, привлечь органический трафик, улучшить ASO, познакомить новых пользователей с игрой и так далее.

Подборка игр в App StoreПодборка игр в App Store

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

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

Иногда тренд может спровоцировать появление целого жанра и тогда первые снимают все сливки. Причем не обязательно самые первые, а именно те, кто вовремя разглядел перспективное направление. Самый наглядный пример MOBA. Dota была небольшой кастомной игрой в WarCraft III, это был юзерский мод. Но когда его популярность стала расти, разработчики League of Legends вовремя среагировали и игра стала даже популярнее оригинала.

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

Из негативных примеров одно время был небольшой взлет мини-игры внутри Dota в стиле Tower Defence. Там был сложный гибрид из мультиплеера, co-op, башен и других механик, и могло показаться, что он выстрелит. Но, вероятно, ниша с таким сочетанием сразу нескольких кор-геймплеев и кооперативной составляющей оказалось слишком узкой, а потенциальный продакшн чрезмерно дорогим. Чудо не произошло, и нового жанра не случилось.

Еще несколько моментов:

1. Сам по себе тренд не сделаем вас популярным. Не все королевские битвы заходят игрокам в любом случае в основе должны лежать качественный продукт и команда (или человек-оркестр в случае некоторых инди-проектов).

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

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

Откуда берутся тренды

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

Например, в TikTok какой-то парень записал видео, как он едет на лонгборде и пьет сок под песню Dreams группы Fleetwood Mac. По неизвестным причинам ролик быстро набрал миллионы просмотров, а когда видео окончательно завирусилось, этот сок закончился в половине городов США. Потому что все блогеры пытались успеть поймать тренд и снять такие же видеоролики, а их подписчики, в свою очередь, хотели попробовать напиток.

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

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

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

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

Как не опоздать на поезд

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

Какого-то одного места для отслеживания трендов не существует. Если это культурное явление, мемы и так далее они скорее всего сначала завирусятся на Reddit. Если это про геймдев, то я бы добавил профильные сайты, топы сторов и даже топы Roblox (если идти глубже). Roblox, это платформа, где пользователи могут создавать свои собственные игры. Там встречаются очень интересные идеи, недавно видел симулятор супергероя, когда ты начинаешь простым каратистом и развиваешь свои навыки вплоть до телекинеза. Пока идея не выстрелила, но кто знает.

Roblox GamesRoblox Games

Вернемся к Among Us. Это далеко не свежая игра (релиз состоялся аж в середине 2018-го) и вообще аналог Мафии, но потом в 2020-м в нее поиграл кто-то из блогеров, произошел небольшой всплеск. Уже тогда разработчикам и геймдизайнерам стоило напрячься и начать следить за трендом куда он пойдет дальше. Потом в игру поиграл PewDiePie и это увидели сразу миллионы пользователей. А тысячи блогеров и стримеров были вынуждены теперь играть в нее, так как им надо делать актуальный контент для своих зрителей.

Даже ASMR-ролики снимают по Among Us, я не шучу:

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

Следующий шаг оценить, насколько тренд актуальный. Например, Among Us на пике трафика обошел даже Minecraft и сейчас уже идет вниз, но он настолько высоко прыгнул, что падать будет еще.

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

Кроме графика, есть и другие признаки, по которым видна живучесть тренда. Планы выпустить батлрояль по вселенным Vampire: The Masquerade и Resident Evil попали в сеть, так что за новостями следить тоже полезно. Может показаться, что все устали от жанра, но на самом деле многие с удовольствием играют и готовы к новым проектам. Самое время поговорить о негативе.

Как не сделать хуже

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

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

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

В любом случае, не пренебрегайте А/Б-тестами. Они нужны на постоянной основе, любая гипотеза сначала проверяется на части аудитории. Если метрики на этой аудитории себя оправдали запускайте изменения на всех. По опыту могу сказать, что далеко не все инди-разработчики так поступают. Иногда у этого есть объективные причины, например, размер команды. Но лучше задуматься об этом заранее и прописать, кто этим будет заниматься.

Эксперименты над игрой с устоявшимся кором, которой уже много лет это огромная тема. Кто-то улучшает графику, делает ее взрослее, чтобы привлечь более платежеспособную аудиторию, кто-то вводит новые режимы, фичи или VR просто, чтобы попасть в соответствующую подборку (когда виртуальная реальность была модной). Некоторые трендовые фичи реально помогают получить фичеринг в сторах. Например, если Apple презентует AR, подумайте, может стоит добавить возможность рассмотреть оружие из вашей игры с помощью новых технологий.

Ну и раз речь зашла про состоявшиеся проекты, поговорим про Pixel Gun 3D, который появился аж в 2013 году. С тех пор мы провели там десятки ивентов, добавили тонну контента и несколько новых режимов.

Кейсы

Один из трендов прошлого года это, конечно, взлет Among Us. Тогда мы решили добавить в игру режим Imposter это атмосфера и экспириенс игры Among Us, по-новому переработанные и переосмысленные в 3D, с новыми фишками, механиками и техническими решениями. Пришлось поработать со светом и добавить туман для ограничения обзора, также добавили многоуровневость (теперь можно падать на мирных членов экипажа сверху). Также продолжаем эксперименты с другими возможностями. Сам режим тестили сначала внутри команды, а потом тюнили по фидбеку от игроков.

У этого режима нет какой-то особой монетизации, его цель совсем в другом. Это мини-игра, которая очень полюбилась игрокам и привлекает новых пользователей, даже не знакомых с основным режимом. Но чтобы не отвлекать новичков от основного туториала (у нас все-таки PvP-шутер), режим доступен с четвертого уровня. В лобби появляется кнопка экстренного митинга, которая перебрасывает в мини-игру. То есть у нас не было цели переделать Pixel Gun 3D в новый жанр или продавать DLC. Это просто новый, классный экспириенс для старых игроков и точка входа в игру для части новых.

Imposter Mode в Pixel Gun 3DImposter Mode в Pixel Gun 3DImposter Mode в Pixel Gun 3DImposter Mode в Pixel Gun 3D

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

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

Из кейсов, которые не выстрелили, могу вспомнить питомцев. Мы хотели сделать что-то вроде Тамагочи внутри Pixel Gun 3D, попробовать возродить некогда популярный тренд. Игрок получает яйцо, но не знает, кто из него появится. Сейчас в игре десятки питомцев, они бегают, наносят урон, но в целом метрики от них не очень сильно растут, хотя мы еще экспериментируем.

Ну и еще из актуального сеттинг киберпанка. Очень многие в прошлом году усиленно готовились к выходу Cyberpunk 2077, мы не исключение. Даже хотели зарелизить скины и оружие примерно в одно время с игрой, но ее дважды переносили. Потом CD Projekt RED объявили, что Cyberpunk 2077 ушла на золото, мы не выдержали и выпустили апдейт. В итоге игру снова отложили, но, думаю, мы все равно попали в тренд, потому что пользователи очень сильно ждали что-то в киберпанк-стилистике.

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

Подробнее..

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

02.06.2021 12:04:43 | Автор: admin

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

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

Как попасть в геймдев

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

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

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

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

Например, гиперказуальные игры подразумевают короткий цикл разработки небольшой командой специалистов. Такой формат ближе к хакатону быстрой разработке, где широкие компетенции команды могут сужаться в одном человеке, делая его незаменимым дженералистом. Художник здесь выступает далеко за пределы рисования, и может брать на себя задачи концепт-артиста, UX/UI-дизайнера, верстальщика, художника по эффектам, аниматора и еще множества различных специальностей. Чем шире спектр навыков, тем лучше.

Cut the RopeCut the Rope

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

Resident Evil 3 RemakeResident Evil 3 Remake

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

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

Следить за новинками индустрии можно прямо на страницах Steam, Origin, Epic Game Store, на мобильном рынке в топах на App Store и Google Play. Новости и полезную информацию можно найти на множестве сайтов на том же Хабре или, например, на DTF (если говорим о русскоязычном комьюнити).

Новинки в SteamНовинки в Steam

4. Углубиться в стилистику, геймплейные особенности, жанры и сеттинги. Хорошо, когда художник и геймдизайнер понимают друг друга. Арт и ГД тесно взаимодействуют в процессе разработки и всегда дополняют друг друга.

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

5. Не зацикливаться. А что, если сейчас я хочу делать красивую архитектуру, а завтра рисовать 2D-персонажей?. Ответ: это нормально.

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

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

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

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

Design MastersDesign Masters

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

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

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

Узнать о предстоящих мероприятиях можно из календарей, например, здесь. Основные русскоязычные конференции это: DevGamm (Москва, Минск, Киев), Game Gathering (Киев), White Nights (Москва, СПб). Есть также специальные артовые ивенты, например, CG Event (Москва, СПб) и Artillery (СПб).

Портфолио

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

Куда выкладывать портфолио?

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

ArtStation

Если, по какой-то причине, вы не хотите выкладывать портфолио в общий доступ, то стоит загрузить работы на Google Drive с доступом по ссылке и пересылать ее рекрутерам (не забудьте корректно настроить доступы). Но в таком формате шанс удачно выступить существенно ниже, чем в тематических сайтах-портфолио.

Что там должно быть?

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

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

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

Примеры скетчейПримеры скетчей

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

Выпускная работа с арт-курса может хорошо дополнить портфолио, но там обязательно должны быть и другие, личные работы. Это демонстрирует, как художник самостоятельно применяет полученные знания на практике, без комментариев наставника. Курсовые работы размещают у себя, например, Smirnov School или Skills Up.

Работы выпускников Smirnov SchoolРаботы выпускников Smirnov School

Как много работ должно быть в портфолио?

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

Как часто надо обновлять портфолио?

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

Как компании работают с портфолио

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

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

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

Как-то мы нашли в соцсетях профиль художницы с релевантными местами предыдущей работы, но без портфолио в открытом доступе. Мы просмотрели ее видеозаписи в социалках и нашли ролик, где в кадр на мгновение попал монитор с открытым окном Photoshopa с 2D-персонажами. Бинго. В итоге мы списались и договорились о тестовом задании.

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

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

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

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

Design MastersDesign Masters

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

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

Первое общение

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

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

В сопроводительном письме или сообщении не нужно копировать часть резюме и перечислять заслуги или конкурсы. Достаточно даже просто написать приветствие, имя, специализацию (например, 3D-художник), ссылки на резюме/портфолио и контакты. Очень хорошо, если там будут не только почта, но мессенджеры (например, Telegram) и соцсети. Это даст рекрутеру альтернативный канал связи на случай, если ответное письмо случайно попадет в спам (его, кстати, полезно проверять на предмет таких вот осечек). Так мы сможем быстрее начать контактировать.

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

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

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

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

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

От джуна до сеньора

Джуны

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

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

Теперь несколько главных аспектов, о которых нужно помнить при поиске работы.

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

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

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

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

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

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

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

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

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

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

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

Мидлы и сеньоры

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

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

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

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

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

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

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

Важный момент: сеньор это не обязательно дженералист, он может быть сеньором в каком-то одном направлении.

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

  1. Первый контакт с рекрутером .

  2. Тестовое задание, оценка результата экспертной командой и фидбек.

  3. Интервью с рекрутером.

  4. Техническое интервью с профильным специалистом (арт-лид или арт-продюсер).

  5. Финальное решение.

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

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

Подробнее..

Открылся набор в Indie Games Accelerator и Indie Games Festival от Google Play

14.06.2021 12:13:27 | Автор: admin

Indie Games Accelerator и Indie Games Festival две программы для независимых (инди) разработчиков мобильных игр, организованных командой Google Play. Программы направлены на то, чтобы помочь небольшим игровым студиям и разработчикам стать популярнее в Google Play независимо от того, на какой стадии находятся их проекты.

В этом году обе программы пройдут в онлайн-формате, заявки принимаются до 1 июля подробности под катом.

Для нас важно поддерживать не только крупные международные компании, но и небольшие инди-команды благодаря своей креативности и увлеченности играми, они создают уникальные и интересные проекты. Если вы работаете над уникальным проектом и хотите, чтобы о нем узнал мир, предлагаем вам принять участие в одной (или обоих сразу) из наших программ Indie Games Accelerator и Indie Games Festival.

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

Indie Games Accelerator: обучение и менторская поддержка

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

Проекты, которые пройдут отбор и станут участниками акселератора, смогут присоединиться к 12-недельной образовательной программе, а также получат возможность поработать над своими проектами вместе с экспертами из Google, крупных игровых студий и венчурных фондов. Rovio, Game Insight, Zynga, Play Ventures, Unity Technologies, Belka Games с полным списком менторов и условиями участия можно ознакомиться здесь.

В этом году в акселерационной программе участвуют более 70 стран, заявки на Indie Games Accelerator из России, Украины и Беларуси будут приниматься впервые!

Indie Games Festival: промо-кампании для финалистов

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

Основные критерии отбора: инновационность, увлекательность и дизайн. Среди призов: фичеринг на Google Play и промо-кампании для 3 игр-победителей стоимостью 100 000 евро.

Условия участия: в программе участвуют 29 стран Европы, включая Россию, Украину и Беларусь; максимальное количество человек в команде 50, игра должна быть выпущена на Google Play не ранее 3 марта 2020 г. Подробнее с правилами участия и критериями отбора можно ознакомиться здесь.

В прошлом году в финал конкурса прошло три проекта из России: My Diggy Dog 2 от King Bird Games, Color Spots от UX Apps и Tricky Castle от Team Tricky подать заявку можно до 1 июля.

Подробнее..

Конкурс Питч игровых проектов

17.04.2021 14:23:59 | Автор: admin

Интересуетесь разработкой игр и хотите увидеть, как разработчики представляют свои проекты перед инвесторами и издателями? Или хотите узнать, какие разрабатываемые проекты стоят внимания и имеют шансы на успех? А может Вы издатель или инвестор и ищите новые дарования? Рады сообщить Вам, что Центр развития компетенций в бизнес-информатике Высшей школы бизнеса НИУ ВШЭ совместно с 1518 Studios, ZAVOD Games и Green Grey проводит уникальное мероприятие: Питч игровых проектов, в котором Вы сможете принять участие в качестве зрителя. Для этого необходимо зарегистрироваться на трансляцию и в день конкурса мы пришлем Вам ссылку трансляции.

Подробности на сайте

Подробнее..

Ремастеринг игрового контента, или как создать 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, декомпозиция задач и контента и работа над поддержкой обратной совместимости позволяет избежать множества конфликтов в ходе внесения изменений в проект несколькими независимыми командами. Но важно, чтобы все члены команды понимали схему работы над проектом, и лиды координировали свои действия при планировании задач на свои отделы.

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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