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

Game development

Из песочницы Plague M.D. Я остался в России и меня поглотила Чума. Сопли и нытье прогера

28.09.2020 12:18:44 | Автор: admin
Мы с друзьями делаем игру. Так было не всегда, а я вообще жил в Австрии.


Plague M.D. игра, которую сделал я и мои друзья на Godot. Сейчас она доступна в на мобильных платформах, а 14 октября выйдет в steam. Как это произошло, я сам не понял.



Полгода назад я застрял в России из-за COVID после сокращения офиса в Австрии. Обучение в Вене было окончено, так что меня ждал огромный мир IT нашей прекрасной страны. Рынок вакансий предлагал карьеру в области веб, 8 часов в день, 5 дней в неделю и все в комфортном офисе, но Только после окончания пандемии. Именно тогда мои друзья предложили мне сделать игру. Кто откажется от такого?

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

И вот, что я запомнил за последние пять месяцев своей жизни в следующих 13 пунктах:

1. Есть всего один специалист это я


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



2. Я застрял в рамках инструмента


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

3. Одиночество с кодом


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



4. Большая нагрузка
Очевидно, вытекающая из предыдущего пункта проблема. В какой-то момент работы с Unity начинаешь встречать специалистов, с которыми можно и нужно советоваться. Кроме того, тему менторства никто не отменял любому IT комьюнити очень нужны новые специалисты. Увы, на Godot обученные работники хотят зарплату более 9000, а прочие ленятся переучиваться из-за боязни оказаться за бортом мейнстримной технологии.

5. Слишком много багов, слишком много лука


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



6. Чит-коды
Несмотря на то, что Годот очень удобен для теста игры и её модулей, иногда приходилось залипать в игровой процесс по несколько минут. Ради ускорения этого процесса пришлось идти на разные ухищрения, ведь классические методы тестирования не подходят для такого продукта.
Так для тестировщиков была придумана комбинация кликов на курицах в игровой деревне, которая переключала уровни. Но, как только был прикручен модуль сохранений, она начала выдавать совсем уже необычный результат. Этот чит-код все еще в игре, так что можете попытаться активировать его. Не удивляйтесь, если Жан умрет, все дело в курице:



7. Я сжимаю как батя. Оптимизация


Из-за того, что львиная доля изображений была нарисована художником и обработана в хайрез, а также из-за безумной любви дизайнера скидывать материал 5:1, постоянно приходится ужиматься, внося правки в размеры файлов, форматы (вместо wav ogg) и используя лаконичность мысли в коде.

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

8. Ад архивов и бэкапов


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

9. Где вообще я работаю?


Из-за ковида полпроекта пришлось проработать удаленно. Были моменты, когда мы собирались в офисе агентства, в котором раньше работали мои друзья.

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

Главный бородатый мужик помогал нам не умереть с голоду, когда было совсем тяжело, искренне верил в игру, любезно предоставил место в офисе для наших собраний и аккаунты разработчика в Google Play и App Store. Смена обстановки и режима работы помогала решать задачи.

10. И никому не объяснишь


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

11. Про команду и доверие


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

12. Про дедлайны


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



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

13. Если игра выпущена, это не значит, что ты свободен. У Добби будет еще много правок


Россия откроет границы в декабре 2020 года, я уеду в Австрию или нет.
Подробнее..

Как и почему мы стали делать UI в Figma вместо Photoshop

27.11.2020 12:11:35 | Автор: admin

По моему опыту, большинство игровых интерфейсов все еще делаются вAdobe Photoshopввиду их художественности и графической сложности, на которую не способны векторные редакторы. Однако и в игры пришел тренд плоских интерфейсов, и настало время пересесть с тяжелого Фотошопа на более простые редакторы интерфейсов, такие какFigma,Adobe XD,Sketchи прочие.

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

Зачем это было нужно

В 2018 году, когда наш проект Dino Squad только стартовал, наша команда работала по такому пайплайну:

  1. Проектируем логику интерфейса и делаем черновые интерактивные прототипы в Figma;

  2. Создаем макеты в высоком разрешении в Photoshop, затем передаем их в верстку;

  3. Снова переносим картинки в растровом формате из Photoshop в Figma для создания чистовых прототипов, которые необходимы для презентаций и геймдизайн-документации.

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

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

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

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

Технические особенности перехода

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

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

Что упростило задачу переноса

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

Еще одним упрощающим переход фактором стало то, что все элементы подчинялись одной сетке. Мы выбрали не самую обычную сетку кратностью 7 (потому что могли). Во всех экранах присутствовала модульность, которую могли наследовать новые экраны, поэтому все элементы интерфейса органично вписывались и в последующие макеты.

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

Перенос проекта из Photoshop в Figma

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

Для начала внутри проекта мы создали три файла:

  • В файлеTokensпредставлены все токены проекта;

  • ВLibraryхранятся все отрисованные элементы интерфейса и ассеты;

  • В файлеScreensнаходятся все актуальные экраны проекта.

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

Этап 1. Создание токенов

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

Как устроена система токенов в Figma

Фигма позволяет создавать Color Styles, Text Styles и Effects Styles. Подробнее про стили можно почитатьтут.

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

Цвета

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

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

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

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

Шрифты

Шрифтовая схема в проекте была более-менее систематизирована. Так как мы использовали кратную 7 сетку, размеры шрифтов тоже должны были оказаться кратными 7. Поэтому для каждого используемого начертания мы создали линейку шрифтов с шагом 7: 28, 35, 42, ..., 84.

Этап 2. Загрузка ассетов и графического контента

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

Иконки

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

Другие ассеты

Точно так же мы поступили и с остальными изображениями. Достаточно было вписать взаимозаменяемые ассеты во фреймы одинакового размера и обернуть в компоненты одной группы: либо сгруппировать в один фрейм, либо представить имя ассета в виде GroupName / AssetName.

Таким образом, в библиотеке ассеты будут храниться по следующему пути: Library / Assets / GroupName / AssetName.

Этап 3. Создание библиотеки компонентов

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

Как создать элемент:

  1. Определить иерархию элементов и их состояний;

  2. Начать с элементов в самом низу иерархии, отрисовать сперва их и все их состояния;

  3. Далее продолжить отрисовку элементов на ступень выше, используя элементы, созданные на предыдущем шаге;

  4. Действовать так до тех пор, пока не получится исходный элемент.

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

Library / Cards / Lots / _BaseLot

Состояния элемента

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

Library / Cards / Lots / LotHeart
Library / Cards / Lots / LotLeaf
Library / Cards / Lots / LotDrop

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

Можно оставить состояния переключаемыми компонентами, как мы это делали до появления функции вариантов в Figma, и переключать их в инспекторе.

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

Важное замечание

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

Библиотека

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

Мы вынесли для себя несколько правил для масштабирования и дополнения библиотеки:

  • Одинаковые элементы не должны повторяться;

  • Переиспользовать все, что можно переиспользовать, и не плодить сущности;

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

  • Использовать фреймы вместо групп и расставлять Constraints для адаптивности при создании прототипов под разные девайсы;

  • Не использовать не занесенные в стили цвета и шрифты.

Возникшие проблемы

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

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

  • Наложение в Photoshop действует внутри группы, а в Figma на все слои ниже в иерархии;

  • В Photoshop можно сделать внутреннюю обводку без наложения на заливку слоя, в Figma нет.

Как подружить Figma с Unity

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

Во-первых, Figma предоставляет API, чтобы файлы можно было легко спарсить в JSON. Бонусом вы получаете доступ к модификации самой Figma.

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

В-третьих, человеку, верстающему интерфейсы в Unity, даже не требуется установленная Figma и аккаунт в ней достаточно иметь токен UI-дизайнера и ссылку на файл.

В-четвертых, Figma поддерживает концепцию компонентов, которая очень схожа с концепцией Nested Prefabs в Unity.

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

using System.Net;using System.IO;using UnityEngine; ...private void FileRequest(){    var request = WebRequest.Create("<https://api.figma.com/v1/files/>" + /* file name */);    request.Headers["X-Figma-Token"] = /* figma token */;     var response = request.GetResponse();    var json = "";     using (var stream = response.GetResponseStream())    {        using (var reader = new StreamReader(stream))        {            string line;            while ((line = reader.ReadLine()) != null)            {                json += line;            }        }    }    Debug.Log(json);     response.Close();}

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

{  "components": {},  "document": {    "children": [      {        "backgroundColor": {          "a": 1,          "b": 0.8980392156862745,          "g": 0.8980392156862745,          "r": 0.8980392156862745        },        "children": [],        "exportSettings": [],        "id": "0:1",        "name": "Page 1",        "type": "CANVAS",        "visible": true      }    ],    "id": "0:0",    "name": "Document",    "type": "DOCUMENT",    "visible": true  },  "schemaVersion": 0}

Теперь разберемся, как получить имя файла и токен. Токен генерируется один раз и может быть найден вSettingsв Figma во вкладкеPersonal Access Tokens.

Название файла таится в его ссылке. То есть, если ссылка на файл выглядит вот так:www.figma.com/file/NNNN/FileName, его названием будетNNNN.

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

Результат

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

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

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

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

Подробнее..

Перевод Как мощность влияет на геймплей численный подход к дизайну игры для достижения ее честности

05.01.2021 12:12:21 | Автор: admin

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

Стоит заметить, что термин честность здесь более уместен, чем баланс, ведь последний чаще можно истолковать неверно. Честность подразумевается в том смысле, что опыт игроков должен быть выдержанным и правдоподобным. Честный геймплей должен быть приятным и увлекательным, даже если он не совсем четко сбалансирован. Идеальный баланс, в свою очередь, может сделать игру более скучной. По словам ведущего дизайнера Overwatch Джеффа Каплана:

Восприятие баланса гораздо важнее, чем сам баланс.

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

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

Во всех описанных случаях могут быть разные стратегии и отправные точки, которые можно использовать для достижения этой самой честности. Одна из них, на которой мы и сфокусируемся в данной статье, мощность (Power). Для ее определения прибегнем к термину, введенному Эрнестом Адамсом в Основах геймдизайна:

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

Другими словами, мощность это мера возможностей игрока.

Далее в статье мы:

  • обсудим, как можно измерить мощность и как это влияет на решения по геймдизайну;

  • проанализируем, как мощность зависит от ресурсов игры и как связана с игровой экономикой;

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

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

  • наконец, вкратце обсудим саму концепцию мощности.

Характеристика мощности через ресурсы

Концепция мощности очень субъективна не только для самой игры в целом, но и для ее цели. Для примера рассмотрим гипотетическую игру Run & Gun, в которой игроки сражаются на арене, гоняясь друг за другом на авто и стреляя друг в друга. Побеждает последний выживший, как это было в боевом режиме Mario Kart. В этой игре оружие и его свойства теснее связаны с концепцией мощности, чем скорость машины.

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

Концепцию мощности будет легче понять, если мы будем рассматривать факторы ее измерения как игровые ресурсы. Ресурсы это любые элементы, которые можно измерить численно. В Run & Gun, у нас есть следующие ресурсы (или атрибуты):

  1. V Здоровье транспортного средства: сколько очков XP есть у вашего авто;

  2. F Сила оружия: сколько урона наносит ваша пушка одним выстрелом;

  3. A Количество боеприпасов: сколько раз вы можете выстрелить до перезарядки;

  4. S Скорость: насколько быстро вы движетесь;

  5. T Время перезарядки: сколько времени длится перезарядка.

Имея данные об этих ресурсах, можно определить формулы, по которым рассчитывается мощность транспортного средства. Суммирование всех этих ресурсов то есть, V + F + A + S + T является исходным (наивным) подходом. Проблема в том, что он предполагает, что более высокое время перезарядки приводит к более высокой мощности, хотя это, конечно же, не так. Самое простое решение этой проблемы принять максимальное время перезарядки к примеру, 60 секунд, и использовать его для инвертирования значения времени перезарядки. Например:

Поскольку 60 секунд максимальное время перезарядки, если T = 60, его вклад в значение мощности будет минимален: 60/60 = 1. С другой стороны, если T окажется совсем небольшим например, 1 или 2, его вклад в значение мощности будет 60 либо 30, что определенно гораздо больше. В дальнейшем мы будем использовать термин вклад для обозначения того, насколько атрибут увеличивает или уменьшает расчетное значение мощности.

В качестве полного примера предположим, что автомобиль Fusca имеет следующие атрибуты: V = 10, F = 2, A = 3, S = 10, T = 30. Тогда мощность Fusca равна 10 + 2 + 3 + 10 + 2 = 27. Для сравнения возьмем другой автомобиль Hilux со следующими атрибутами: V = 15, F = 3, A = 1, S = 7, T = 60. Отсюда мощность Hilux равна 15 + 3 + 1 + 7 + 1 = 27. Хотя эти две машины имеют разные характеристики, мощность их одинакова.

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

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

Как мощность влияет на экономику

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

Игровая экономика обычно связана с четырьмя столпами: Источниками (Sources), которые создают ресурсы; Стоками (Drains), которые уничтожают ресурсы; Преобразователями (Converters), конвертирующими одни ресурсы в другие; и Обмениками (Traders), которые обменивают ресурсы между различными объектами в игре, такими как игроки или NPC.

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

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

На диаграмме ниже показан черновик игровой экономики для Run & Gun, где Fusca противостоит Hilux:

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

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

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

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

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

Мощность, сложность и прогресс

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

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

Более современные игры, такие как Hellblade: Senua's Sacrifice, предлагают автоматический выбор сложности в зависимости от прогресса игрока. По мере того, как игрок путешествует по адскому скандинавскому ландшафту, игра автоматически адаптируется в соответствии с навыками и способностями игрока, чтобы поддерживать необходимый уровень стресса и азарта.

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

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

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

Предположим, что в Run & Gun есть однопользовательский режим кампании, в котором игрок сталкивается с последовательностью испытаний, чтобы стать королем арены. Если у всех машин стандартная начальная мощность будет 27 (как рассчитано ранее), начальные испытания должны либо иметь более низкое значение мощности, либо равное ему.

Например, первой задачей может быть победа над Pontiac, имеющим атрибуты V = 5, F = 1, A = 1, S = 5, T = 60 и мощность, равную 13. То есть, она составляет почти половину мощности стандартного автомобиля. Это должно помочь новым игрокам познакомиться с игрой и изучить элементы управления без чувства непосредственной угрозы или разочарования.

Однако по мере приближения к титулу короля арены игрок должен столкнуться с противниками с врагами с более высокими характеристиками, что способствует более сложному опыту. Конечным боссом может быть Volkswagen Braslia с атрибутами V = 30, F = 5, A = 3, S = 10, T = 10 и мощностью, равной 54. То есть, мощность босса в два раза превышает мощность стандартного автомобиля. Преимущество использования этого подхода заключается в том, что, если задача оказывается практически невыполнимой, мы можем прибегнуть к корректировке характеристик и затем еще раз сравнить значения мощности.

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

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

  1. Линейный: мощность увеличивается на одно и то же значение на каждом уровне.

  2. Нелинейный: мощность увеличивается нелинейно на каждом уровне например, с умножением на некий коэффициент. На графике каждый уровень умножает предыдущее значение мощности на 1,25 (эквивалент увеличения на 25%).

  3. Скачкообразный (Steps): мощность увеличивается произвольно, включая повторение одной и той же мощности между уровнями в соответствии с определенным дизайном игры. На графике мощность увеличивается только после двух последовательных уровней. Этот подход помогает упростить прохождение игры.

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

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

Процесс регулировки мощности с относительной сложностью

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

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

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

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

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

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

Мощность исходит от людей

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

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

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

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

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

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

Пошаговый процесс вычисления мощности

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

  1. Составьте исходное уравнение для расчета мощности (например, суммирование).

  2. Рассчитайте мощность элементов на уровне, включая мощность игрока.

  3. Протестируйте уровень (как можно большее число раз).

  4. Проанализируйте сложность игры и трудности, с которыми сталкивается игрок.

  5. Если игроки считают, что уровень был честным (не обязательно сбалансированным), вы на верном пути, иначе:

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

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

В данном случае мощность Fusca станет 10 + 2 * 2 + 3 + 10 + 2 = 29, а мощность Hilux увеличится до 15 + 2 * 3 + 1 + 7 + 1 = 30. Из-за этого Hilux теперь немного превосходит Fusca. Мы можем уравновесить их, увеличив боезапас Fusca с 3 до 4 или уменьшив время перезарядки с 30 до 20.

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

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

Мощность через эмпатию

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

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

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

Самый важный навык для геймдизайнера умение слушать.

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

Хотя в этой статье был рассмотрен пример боевой системы, то же самое можно применить к системе экономики таких игр, как Rise of the Industry, или, скажем, освоения космоса в Spore.

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

Подробнее..

Перевод Библиотека Oboe для высокопроизводительного аудио в играх и приложениях

21.04.2021 10:22:09 | Автор: admin

Мы добавили в Android Game SDK библиотеку Oboe C++ для работы со звуком. Она позволяет разрабатывать высокопроизводительные аудиоприложения с низкой задержкой для максимального спектра устройств Android. Эта библиотека также отлично подойдет большинству разработчиков игр. О ней и о том как с ней работать в Android Game SDK мы и хотим рассказать в этой статье.

Один API

Библиотека Oboe использует усовершенствованный интерфейс AAudio с расширенными функциями на устройствах под управлением Android 8.1 (API уровня 27) или более поздней версии, а также обеспечивает обратную совместимость (через OpenSL ES) с Android 4.1 (API уровня 16) или более поздней версии. В дополнение к API платформы библиотека Oboe предлагает разработчикам аудиоприложений ключевые функции для комфортной работы, такие как ресемплинг, преобразование форматов и динамическая корректировка задержек. При необходимости она позволяет преобразовывать аудиоданные, например конвертировать число каналов, чтобы повышать производительность выбранных устройств. Также библиотека предлагает обходные решения для других особенностей работы конкретных устройств, что повышает эффективность кода для обработки звука. В двух словах, теперь библиотека Oboe считается рекомендуемым решением при написании кода для работы со звуком на C/C++ для платформы Android.

Интеграция Oboe

Встроить элементы кода на базе Oboe в проект можно двумя основными способами. Если вы используете плагин Android Gradle 4.1.0 или более поздней версии вместе с CMake, а также используете (или можете подключить) общую библиотеку STL, для включения библиотеки Oboe достаточно добавить ее в список зависимостей Gradle, включить объекты prefab и добавить несколько строк кода в файл CMakeLists.

Интегрировать Oboe также можно с помощью статического связывания при помощи Android Game SDK. Сначаласкачайте библиотекуи зарегистрируйте ее в системе управления версиями. Работать нужно с minSdkVersion 16 или более поздней версии, а также с NDK 18 или более поздней версии. Затем укажите версию игрового SDK для привязки, которая скомпилирована для заданной комбинации ABI, уровня API, NDK и STL, добавив путь к компилятору в следующем формате:

gamesdk/libs/[architecture]_API[apiLevel]_NDK[ndkVersion]_[stlVersion]_ReleaseExample: gamesdk/libs/arm64-v8a_API24_NDK18_cpp_static_Release

Затем добавьте ключ-loboe_staticв команду компоновщика. Поскольку включать общую библиотеку liboboe.so не требуется, статическое связывание позволяет сократить размер кода. Если для комбинации ABI, уровня API, NDK и STL нет предварительно скомпилированной версии SDK под ваши настройки, можно выполнить связывание с общей библиотекой. Дополнительные указания, в том числе о настройке CMake для статических библиотек, см. вдокументации для разработчиков.

Основы Oboe

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

oboe::AudioStreamBuilder builder;builder.setPerformanceMode(oboe::PerformanceMode::LowLatency)  ->setSharingMode(oboe::SharingMode::Exclusive)  ->setDataCallback(myCallback)  ->setFormat(oboe::AudioFormat::Float);

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

class MyCallback : public oboe::AudioStreamDataCallback {public:    oboe::DataCallbackResult    onAudioReady(oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) {        // We requested AudioFormat::Float        auto *outputData = static_cast<float *>(audioData);        // TODO: populate audioData here        return oboe::DataCallbackResult::Continue;    }};

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

Обратная связь

Если вы столкнулись с проблемами, сообщите нам о нихчерез GitHub.

Подробнее..

PVS-Studio и Continuous Integration TeamCity. Анализ проекта Open RollerCoaster Tycoon 2

20.07.2020 18:22:00 | Автор: admin

Один из самых актуальных сценариев использования анализатора PVS-Studio его интеграция с CI системами. И хотя анализ проекта PVS-Studio практически из-под любой continuous integration системы можно встроить всего в несколько команд, мы продолжаем делать этот процесс ещё удобнее. В PVS-Studio появилась поддержка преобразования вывода анализатора в формат для TeamCity TeamCity Inspections Type. Давайте посмотрим, как это работает.

Информация об используемом ПО


PVS-Studio статический анализатор С, С++, C# и Java кода, предназначенный для облегчения задачи поиска и исправления различного рода ошибок. Анализатор можно использовать в Windows, Linux и macOS. В данной статье мы будем активно использовать не только сам анализатор, но и некоторые утилиты из его дистрибутива.

CLMonitor представляет собой сервер мониторинга, который осуществляет отслеживание запусков компиляторов. Его необходимо запустить непосредственно перед началом сборки вашего проекта. В режиме отслеживания сервер будет перехватывать запуски всех поддерживаемых компиляторов. Стоит отметить, что данную утилиту можно использовать только для анализа C/С++ проектов.

PlogConverter утилита для конвертации отчёта анализатора в разные форматы.

Информация об исследуемом проекте


Давайте попробуем данную функциональность на практическом примере проанализируем проект OpenRCT2.

OpenRCT2 открытая реализация игры RollerCoaster Tycoon 2 (RCT2), расширяющая её новыми функциями и исправляющая ошибки. Игровой процесс вращается вокруг строительства и содержания парка развлечений, в котором находятся аттракционы, магазины и объекты. Игрок должен постараться получить прибыль и поддерживать хорошую репутацию парка, сохраняя при этом гостей счастливыми. OpenRCT2 позволяет играть как в сценарии, так и в песочнице. Сценарии требуют, чтобы игрок выполнил определенную задачу в установленное время, в то время как песочница позволяет игроку построить более гибкий парк без каких-либо ограничений или финансов.

Настройка


В целях экономии времени я, пожалуй, опущу процесс установки и начну с того момента, когда у меня на компьютере запущен сервер TeamCity. Нам нужно перейти: localhost:{указанный в процессе установки порт}(в моём случае, localhost:9090) и ввести данные для авторизации. После входа нас встретит:

image3.png

Нажмём на кнопку Create Project. Далее выберем Manually, заполним поля.

image5.png

После нажатия на кнопку Create, нас встречает окно с настройками.

image7.png

Нажмём Create build configuration.

image9.png

Заполняем поля, нажимаем Create. Мы видим окно с предложением выбора системы контроля версий. Так как исходники уже лежат локально, жмём Skip.

image11.png

Наконец, мы переходим к настройкам проекта.

image13.png

Добавим шаги сборки, для этого жмём: Build steps -> Add build step.

image15.png

Тут выберем:

  • Runner type -> Command Line
  • Run -> Custom Script

Так как мы будем проводить анализ во время компиляции проекта, сборка и анализ должны быть одним шагом, поэтому заполним поле Custom Script:

image17.png

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

Последнее, что нам нужно сделать, установить переменные окружения, которыми я обозначил некоторые пути для улучшения их читабельности. Для этого перейдём: Parameters -> Add new parameter и добавим три переменные:

image19.png

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

Непосредственно скрипт


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

choco install pvs-studio -y

Далее запустим утилиту отслеживания сборки проекта CLMonitor.

%CLmon% monitor -attach

Потом произведём сборку проекта, в качестве переменной окружения MSB выступает путь к нужной мне для сборки версии MSBuild

%MSB% %ProjPath% /t:clean%MSB% %ProjPath% /t:rebuild /p:configuration=release%MSB% %ProjPath% /t:g2%MSB% %ProjPath% /t:PublishPortable

Введём логин и ключ лицензии PVS-Studio:

%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%

После завершения сборки ещё раз запустим CLMonitor для генерации препроцессированных файлов и статического анализа:

%CLmon% analyze -l "c:\ptest.plog"

После воспользуемся ещё одной утилитой из нашего дистрибутива. PlogConverter преобразует отчёт из стандартного в специфичный для TeamCity формат. Благодаря этому мы сможем посмотреть его прямо в окне сборки.

%PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"

Последним действием выведем форматированный отчёт в stdout, где его подхватит парсер TeamCity.

type "C:\temp\ptest.plog_TeamCity.txt"

Полный код скрипта:

choco install pvs-studio -y%CLmon% monitor --attachset platform=x64%MSB% %ProjPath% /t:clean%MSB% %ProjPath% /t:rebuild /p:configuration=release%MSB% %ProjPath% /t:g2%MSB% %ProjPath% /t:PublishPortable%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%%CLmon% analyze -l "c:\ptest.plog"%PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"type "C:\temp\ptest.plog_TeamCity.txt"

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

image21.png

Теперь кликнем на Inspections Total, чтоб перейти к просмотру отчёта анализатора:

image23.png

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

Просмотр результатов работы анализатора


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

Предупреждение N1

V773 [CWE-401] The exception was thrown without releasing the 'result' pointer. A memory leak is possible. libopenrct2 ObjectFactory.cpp 443

Object* CreateObjectFromJson(....){  Object* result = nullptr;  ....  result = CreateObject(entry);  ....  if (readContext.WasError())  {    throw std::runtime_error("Object has errors");  }  ....}Object* CreateObject(const rct_object_entry& entry){  Object* result;  switch (entry.GetType())  {    case OBJECT_TYPE_RIDE:      result = new RideObject(entry);      break;    case OBJECT_TYPE_SMALL_SCENERY:      result = new SmallSceneryObject(entry);      break;    case OBJECT_TYPE_LARGE_SCENERY:      result = new LargeSceneryObject(entry);      break;    ....    default:      throw std::runtime_error("Invalid object type");  }  return result;}

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

Предупреждение N2

V501 There are identical sub-expressions '(1ULL << WIDX_MONTH_BOX)' to the left and to the right of the '|' operator. libopenrct2ui Cheats.cpp 487

static uint64_t window_cheats_page_enabled_widgets[] = {  MAIN_CHEAT_ENABLED_WIDGETS |  (1ULL << WIDX_NO_MONEY) |  (1ULL << WIDX_ADD_SET_MONEY_GROUP) |  (1ULL << WIDX_MONEY_SPINNER) |  (1ULL << WIDX_MONEY_SPINNER_INCREMENT) |  (1ULL << WIDX_MONEY_SPINNER_DECREMENT) |  (1ULL << WIDX_ADD_MONEY) |  (1ULL << WIDX_SET_MONEY) |  (1ULL << WIDX_CLEAR_LOAN) |  (1ULL << WIDX_DATE_SET) |  (1ULL << WIDX_MONTH_BOX) |  // <=  (1ULL << WIDX_MONTH_UP) |  (1ULL << WIDX_MONTH_DOWN) |  (1ULL << WIDX_YEAR_BOX) |  (1ULL << WIDX_YEAR_UP) |  (1ULL << WIDX_YEAR_DOWN) |  (1ULL << WIDX_DAY_BOX) |  (1ULL << WIDX_DAY_UP) |  (1ULL << WIDX_DAY_DOWN) |  (1ULL << WIDX_MONTH_BOX) |  // <=  (1ULL << WIDX_DATE_GROUP) |  (1ULL << WIDX_DATE_RESET),  ....};

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

Предупреждения N3

V703 It is odd that the 'flags' field in derived class 'RCT12BannerElement' overwrites field in base class 'RCT12TileElementBase'. Check lines: RCT12.h:570, RCT12.h:259. libopenrct2 RCT12.h 570

struct RCT12SpriteBase{  ....  uint8_t flags;  ....};struct rct1_peep : RCT12SpriteBase{  ....  uint8_t flags;  ....};

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

Предупреждение N4

V793 It is odd that the result of the 'imageDirection / 8' statement is a part of the condition. Perhaps, this statement should have been compared with something else. libopenrct2 ObservationTower.cpp 38

void vehicle_visual_observation_tower(...., int32_t imageDirection, ....){  if ((imageDirection / 8) && (imageDirection / 8) != 3)  {    ....  }  ....}

Давайте разберёмся поподробнее. Выражение imageDirection / 8 будет false в том случае, если imageDirection находится в диапазоне от -7 до 7. Вторая часть: (imageDirection / 8) != 3 проверяет imageDirection на нахождение вне диапазона: от -31 до -24 и от 24 до 31 соответственно. Мне кажется довольно странным проверять числа на вхождение в определённый диапазон таким способом и, даже если в данном фрагменте кода нет ошибки, я бы рекомендовал переписать данные условия на более явные. Это существенно упростило бы жизнь людям, которые будут читать и поддерживать этот код.

Предупреждение N5

V587 An odd sequence of assignments of this kind: A = B; B = A;. Check lines: 1115, 1118. libopenrct2ui MouseInput.cpp 1118

void process_mouse_over(....){  ....  switch (window->widgets[widgetId].type)  {    case WWT_VIEWPORT:      ebx = 0;      edi = cursorId;                                 // <=      // Window event WE_UNKNOWN_0E was called here,      // but no windows actually implemented a handler and      // it's not known what it was for      cursorId = edi;                                 // <=      if ((ebx & 0xFF) != 0)      {        set_cursor(cursorId);        return;      }      break;      ....  }  ....}

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

Предупреждение N6

V1004 [CWE-476] The 'player' pointer was used unsafely after it was verified against nullptr. Check lines: 2085, 2094. libopenrct2 Network.cpp 2094

void Network::ProcessPlayerList(){  ....  auto* player = GetPlayerByID(pendingPlayer.Id);  if (player == nullptr)  {    // Add new player.    player = AddPlayer("", "");    if (player)                                          // <=    {      *player = pendingPlayer;       if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)       {         _serverConnection->Player = player;       }    }    newPlayers.push_back(player->Id);                    // <=  }  ....}

Данный код поправить довольно просто, нужно или третий раз проверять player на нулевой указатель, либо внести его в тело условного оператора. Я бы предложил второй вариант:

void Network::ProcessPlayerList(){  ....  auto* player = GetPlayerByID(pendingPlayer.Id);  if (player == nullptr)  {    // Add new player.    player = AddPlayer("", "");    if (player)    {      *player = pendingPlayer;      if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)      {        _serverConnection->Player = player;      }      newPlayers.push_back(player->Id);    }  }  ....}

Предупреждение N7

V547 [CWE-570] Expression 'name == nullptr' is always false. libopenrct2 ServerList.cpp 102

std::optional<ServerListEntry> ServerListEntry::FromJson(...){  auto name = json_object_get(server, "name");  .....  if (name == nullptr || version == nullptr)  {    ....  }  else  {    ....    entry.name = (name == nullptr ? "" : json_string_value(name));    ....  }  ....}

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

std::optional<ServerListEntry> ServerListEntry::FromJson(...){  auto name = json_object_get(server, "name");  .....  if (name == nullptr || version == nullptr)  {    name = ""    ....  }  else  {    ....    entry.name = json_string_value(name);    ....  }  ....}

Предупреждение N8

V1048 [CWE-1164] The 'ColumnHeaderPressedCurrentState' variable was assigned the same value. libopenrct2ui CustomListView.cpp 510

void CustomListView::MouseUp(....){  ....  if (!ColumnHeaderPressedCurrentState)  {    ColumnHeaderPressed = std::nullopt;    ColumnHeaderPressedCurrentState = false;    Invalidate();  }}

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

Вывод


Как мы видим, интегрировать статический анализатор PVS-Studio в свой проект на TeamCity довольно просто. Для этого достаточно написать всего один маленький файл конфигурации. Проверка кода же позволит выявлять проблемы сразу после сборки, что поможет устранять их тогда, когда сложность и стоимость правок ещё малы.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Stolyarov. PVS-Studio and Continuous Integration: TeamCity. Analysis of the Open RollerCoaster Tycoon 2 project.
Подробнее..

PVS-Studio Анализ pull request-ов в Azure DevOps при помощи self-hosted агентов

27.07.2020 18:22:43 | Автор: admin


Статический анализ кода показывает наибольшую эффективность во время внесения изменений в проект, поскольку ошибки всегда сложнее исправлять в будущем, чем не допустить их появления на ранних этапах. Мы продолжаем расширять варианты использования PVS-Studio в системах непрерывной разработки и покажем, как настроить анализ pull request-ов при помощи self-hosted агентов в Microsoft Azure DevOps, на примере игры Minetest.

Вкратце о том, с чем мы имеем дело


Minetest это открытый кроссплатформенный игровой движок, содержащий около 200 тысяч строк кода на C, C++ и Lua. Он позволяет создавать разные игровые режимы в воксельном пространстве. Поддерживает мультиплеер, и множество модов от сообщества. Репозиторий проекта размещен здесь: https://github.com/minetest/minetest.

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

PVS-Studio статический анализатор кода на языках C, C++, C# и Java для поиска ошибок и дефектов безопасности.

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

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

  • У локального хоста может быть больше ресурсов, чем у ВМ Azure;
  • Агент не "исчезает" после выполнения своей задачи;
  • Возможность прямой настройки окружения, и более гибкое управление процессами сборки;
  • Локальное хранение промежуточных файлов положительно влияет на скорость сборки;
  • Можно бесплатно выполнять более 30 задач в месяц.

Подготовка к использованию self-hosted агента


Процесс начала работы в Azure подробно описан в статье "PVS-Studio идёт в облака: Azure DevOps", поэтому перейду сразу к созданию self-hosted агента.

Для того, чтобы агенты имели право подключиться к пулам проекта, им нужен специальный Access Token. Получить его можно на странице "Personal Access Tokens", в меню "User settings".

image2.png

После нажатия на "New token" необходимо указать имя и выбрать Read & manage Agent Pools (может понадобиться развернуть полный список через "Show all scopes").

image3.png

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

image4.png

В качестве агента будет использован Docker контейнер на основе Windows Server Core. Хостом является мой рабочий компьютер на Windows 10 x64 с Hyper-V.

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

В Windows для этого нужно модифицировать файл 'C:\ProgramData\Docker\config\daemon.json' следующим образом:

{  "registry-mirrors": [],  "insecure-registries": [],  "debug": true,  "experimental": false,  "data-root": "d:\\docker",  "storage-opts": [ "size=40G" ]}

Для создания Docker образа для агентов со сборочной системой и всем необходимым в директории 'D:\docker-agent' добавим Docker файл с таким содержимым:

# escape=`FROM mcr.microsoft.com/dotnet/framework/runtimeSHELL ["cmd", "/S", "/C"]ADD https://aka.ms/vs/16/release/vs_buildtools.exe C:\vs_buildtools.exeRUN C:\vs_buildtools.exe --quiet --wait --norestart --nocache `  --installPath C:\BuildTools `  --add Microsoft.VisualStudio.Workload.VCTools `  --includeRecommendedRUN powershell.exe -Command `  Set-ExecutionPolicy Bypass -Scope Process -Force; `  [System.Net.ServicePointManager]::SecurityProtocol =    [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `  iex ((New-Object System.Net.WebClient)    .DownloadString('https://chocolatey.org/install.ps1')); `  choco feature enable -n=useRememberedArgumentsForUpgrades;  RUN powershell.exe -Command `  choco install -y cmake --installargs '"ADD_CMAKE_TO_PATH=System"'; `  choco install -y git --params '"/GitOnlyOnPath /NoShellIntegration"'RUN powershell.exe -Command `  git clone https://github.com/microsoft/vcpkg.git; `  .\vcpkg\bootstrap-vcpkg -disableMetrics; `  $env:Path += '";C:\vcpkg"'; `  [Environment]::SetEnvironmentVariable(    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine); `  [Environment]::SetEnvironmentVariable(    '"VCPKG_DEFAULT_TRIPLET"', '"x64-windows"',  [System.EnvironmentVariableTarget]::Machine)RUN powershell.exe -Command `  choco install -y pvs-studio; `  $env:Path += '";C:\Program Files (x86)\PVS-Studio"'; `  [Environment]::SetEnvironmentVariable(    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine)RUN powershell.exe -Command `  $latest_agent =    Invoke-RestMethod -Uri "https://api.github.com/repos/Microsoft/                          azure-pipelines-agent/releases/latest"; `  $latest_agent_version =    $latest_agent.name.Substring(1, $latest_agent.tag_name.Length-1); `  $latest_agent_url =    '"https://vstsagentpackage.azureedge.net/agent/"' + $latest_agent_version +  '"/vsts-agent-win-x64-"' + $latest_agent_version + '".zip"'; `  Invoke-WebRequest -Uri $latest_agent_url -Method Get -OutFile ./agent.zip; `  Expand-Archive -Path ./agent.zip -DestinationPath ./agentUSER ContainerAdministratorRUN reg add hklm\system\currentcontrolset\services\cexecsvc        /v ProcessShutdownTimeoutSeconds /t REG_DWORD /d 60  RUN reg add hklm\system\currentcontrolset\control        /v WaitToKillServiceTimeout /t REG_SZ /d 60000 /fADD .\entrypoint.ps1 C:\entrypoint.ps1SHELL ["powershell", "-Command",       "$ErrorActionPreference = 'Stop';     $ProgressPreference = 'SilentlyContinue';"]ENTRYPOINT .\entrypoint.ps1

В результате получится сборочная система на основе MSBuild для C++, с Chocolatey для установки PVS-Studio, CMake и Git. Для удобного управления библиотеками, от которых зависит проект, собирается Vcpkg. А также скачивается свежая версия, собственно, Azure Pipelines Agent.

Для инициализации агента из ENTRYPOINT-а Docker файла вызывается PowerShell скрипт 'entrypoint.ps1', в который нужно добавить URL "организации" проекта, токен пула агентов, и параметры лицензии PVS-Studio:

$organization_url = "https://dev.azure.com/<аккаунт Microsoft Azure>"$agents_token = "<token агента>"$pvs_studio_user = "<имя пользователя PVS-Studio>"$pvs_studio_key = "<ключ PVS-Studio>"try{  C:\BuildTools\VC\Auxiliary\Build\vcvars64.bat  PVS-Studio_Cmd credentials -u $pvs_studio_user -n $pvs_studio_key    .\agent\config.cmd --unattended `    --url $organization_url `    --auth PAT `    --token $agents_token `    --replace;  .\agent\run.cmd} finally{  # Agent graceful shutdown  # https://github.com/moby/moby/issues/25982    .\agent\config.cmd remove --unattended `    --auth PAT `    --token $agents_token}

Команды для сборки образа и запуска агента:

docker build -t azure-agent -m 4GB .docker run -id --name my-agent -m 4GB --cpu-count 4 azure-agent

image5.png

Агент запущен и готов выполнять задачи.

image6.png

Запуск анализа на self-hosted агенте


Для анализа PR создается новый pipeline со следующим скриптом:

image7.png

trigger: nonepr:  branches:    include:    - '*'pool: Defaultsteps:- script: git diff --name-only    origin/%SYSTEM_PULLREQUEST_TARGETBRANCH% >    diff-files.txt  displayName: 'Get committed files'- script: |    cd C:\vcpkg    git pull --rebase origin    CMD /C ".\bootstrap-vcpkg -disableMetrics"    vcpkg install ^    irrlicht zlib curl[winssl] openal-soft libvorbis ^    libogg sqlite3 freetype luajit    vcpkg upgrade --no-dry-run  displayName: 'Manage dependencies (Vcpkg)'- task: CMake@1  inputs:    cmakeArgs: -A x64      -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake      -DCMAKE_BUILD_TYPE=Release -DENABLE_GETTEXT=0 -DENABLE_CURSES=0 ..  displayName: 'Run CMake'- task: MSBuild@1  inputs:    solution: '**/*.sln'    msbuildArchitecture: 'x64'    platform: 'x64'    configuration: 'Release'    maximumCpuCount: true  displayName: 'Build'- script: |    IF EXIST .\PVSTestResults RMDIR /Q/S .\PVSTestResults    md .\PVSTestResults    PVS-Studio_Cmd ^    -t .\build\minetest.sln ^    -S minetest ^    -o .\PVSTestResults\minetest.plog ^    -c Release ^    -p x64 ^    -f diff-files.txt ^    -D C:\caches    PlogConverter ^    -t FullHtml ^    -o .\PVSTestResults\ ^    -a GA:1,2,3;64:1,2,3;OP:1,2,3 ^    .\PVSTestResults\minetest.plog    IF NOT EXIST "$(Build.ArtifactStagingDirectory)" ^    MKDIR "$(Build.ArtifactStagingDirectory)"    powershell -Command ^    "Compress-Archive -Force ^    '.\PVSTestResults\fullhtml' ^    '$(Build.ArtifactStagingDirectory)\fullhtml.zip'"  displayName: 'PVS-Studio analyze'  continueOnError: true- task: PublishBuildArtifacts@1  inputs:    PathtoPublish: '$(Build.ArtifactStagingDirectory)'    ArtifactName: 'psv-studio-analisys'    publishLocation: 'Container'  displayName: 'Publish analysis report'

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

image8.png


image9.png

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

Если сборка прошла успешно, запускается анализ изменившихся файлов (флаг '-f diff-files.txt'), игнорируя созданные CMake вспомогательные проекты (выбираем только нужный проект флагом '-S minetest'). Для ускорения поиска связей между заголовочными и исходными C++ файлами создается специальный кэш, который будет храниться в отдельной директории (флаг '-D C:\caches').

Таким образом, мы теперь можем получать отчеты об анализе изменений в проекте.

image10.png


image11.png

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

image13.png

Некоторые ошибки, найденные в Minetest


Затирание результата

V519 The 'color_name' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 621, 627. string.cpp 627

static bool parseNamedColorString(const std::string &value,                                  video::SColor &color){  std::string color_name;  std::string alpha_string;  size_t alpha_pos = value.find('#');  if (alpha_pos != std::string::npos) {    color_name = value.substr(0, alpha_pos);    alpha_string = value.substr(alpha_pos + 1);  } else {    color_name = value;  }  color_name = lowercase(value); // <=  std::map<const std::string, unsigned>::const_iterator it;  it = named_colors.colors.find(color_name);  if (it == named_colors.colors.end())    return false;  ....}

Эта функция должна производить разбор названия цвета с параметром прозрачности (например, Green#77) и вернуть его код. В зависимости от результата проверки условия в переменную color_name передается результат разбиения строки либо копия аргумента функции. Однако затем в нижний регистр переводится не сама полученная строка, а исходный аргумент. В результате её не удастся найти в словаре цветов при наличии параметра прозрачности. Можем исправить эту строку так:

color_name = lowercase(color_name);

Лишние проверки условий

V547 Expression 'nearest_emergefull_d == 1' is always true. clientiface.cpp 363

void RemoteClient::GetNextBlocks (....){  ....  s32 nearest_emergefull_d = -1;  ....  s16 d;  for (d = d_start; d <= d_max; d++) {    ....      if (block == NULL || surely_not_found_on_disk || block_is_invalid) {        if (emerge->enqueueBlockEmerge(peer_id, p, generate)) {          if (nearest_emerged_d == -1)            nearest_emerged_d = d;        } else {          if (nearest_emergefull_d == -1) // <=            nearest_emergefull_d = d;          goto queue_full_break;        }  ....  }  ....queue_full_break:  if (nearest_emerged_d != -1) { // <=    new_nearest_unsent_d = nearest_emerged_d;  } else ....}

Переменная nearest_emergefull_d в процессе работы цикла не меняется, и её проверка не влияет на ход выполнения алгоритма. Либо это результат неаккуратного copy-paste, либо с ней забыли провести какие-то вычисления.

V560 A part of conditional expression is always false: y > max_spawn_y. mapgen_v7.cpp 262

int MapgenV7::getSpawnLevelAtPoint(v2s16 p){  ....  while (iters > 0 && y <= max_spawn_y) {               // <=    if (!getMountainTerrainAtPoint(p.X, y + 1, p.Y)) {      if (y <= water_level || y > max_spawn_y)          // <=        return MAX_MAP_GENERATION_LIMIT; // Unsuitable spawn point      // y + 1 due to biome 'dust'      return y + 1;    }  ....}

Значение переменной 'y' проверяется перед очередной итерацией цикла. Последующее, противоположное ей, сравнение всегда вернет false и, в целом, не влияет на результат проверки условия.

Потеря проверки указателя

V595 The 'm_client' pointer was utilized before it was verified against nullptr. Check lines: 183, 187. game.cpp 183

void gotText(const StringMap &fields){  ....  if (m_formname == "MT_DEATH_SCREEN") {    assert(m_client != 0);    m_client->sendRespawn();    return;  }  if (m_client && m_client->modsLoaded())    m_client->getScript()->on_formspec_input(m_formname, fields);}

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

Бит или не бит?

V616 The '(FT_RENDER_MODE_NORMAL)' named constant with the value of 0 is used in the bitwise operation. CGUITTFont.h 360

typedef enum  FT_Render_Mode_{  FT_RENDER_MODE_NORMAL = 0,  FT_RENDER_MODE_LIGHT,  FT_RENDER_MODE_MONO,  FT_RENDER_MODE_LCD,  FT_RENDER_MODE_LCD_V,  FT_RENDER_MODE_MAX} FT_Render_Mode;#define FT_LOAD_TARGET_( x )   ( (FT_Int32)( (x) & 15 ) << 16 )#define FT_LOAD_TARGET_NORMAL  FT_LOAD_TARGET_( FT_RENDER_MODE_NORMAL )void update_load_flags(){  // Set up our loading flags.  load_flags = FT_LOAD_DEFAULT | FT_LOAD_RENDER;  if (!useHinting()) load_flags |= FT_LOAD_NO_HINTING;  if (!useAutoHinting()) load_flags |= FT_LOAD_NO_AUTOHINT;  if (useMonochrome()) load_flags |=     FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_RENDER_MODE_MONO;  else load_flags |= FT_LOAD_TARGET_NORMAL; // <=}

Макрос FT_LOAD_TARGET_NORMAL разворачивается в ноль, и побитовое "или" не будет устанавливать никакие флаги в load_flags, ветка else может быть убрана.

Округление целочисленного деления

V636 The 'rect.getHeight() / 16' expression was implicitly cast from 'int' type to 'float' type. Consider utilizing an explicit type cast to avoid the loss of a fractional part. An example: double A = (double)(X) / Y;. hud.cpp 771

void drawItemStack(....){  float barheight = rect.getHeight() / 16;  float barpad_x = rect.getWidth() / 16;  float barpad_y = rect.getHeight() / 16;  core::rect<s32> progressrect(    rect.UpperLeftCorner.X + barpad_x,    rect.LowerRightCorner.Y - barpad_y - barheight,    rect.LowerRightCorner.X - barpad_x,    rect.LowerRightCorner.Y - barpad_y);}

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

Подозрительная последовательность операторов ветвления

V646 Consider inspecting the application's logic. It's possible that 'else' keyword is missing. treegen.cpp 413

treegen::error make_ltree(...., TreeDef tree_definition){  ....  std::stack <core::matrix4> stack_orientation;  ....    if ((stack_orientation.empty() &&      tree_definition.trunk_type == "double") ||      (!stack_orientation.empty() &&      tree_definition.trunk_type == "double" &&      !tree_definition.thin_branches)) {      ....    } else if ((stack_orientation.empty() &&      tree_definition.trunk_type == "crossed") ||      (!stack_orientation.empty() &&      tree_definition.trunk_type == "crossed" &&      !tree_definition.thin_branches)) {      ....    } if (!stack_orientation.empty()) {                  // <=  ....  }  ....}

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

Неправильная проверка выделения памяти

V668 There is no sense in testing the 'clouds' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. game.cpp 1367

bool Game::createClient(....){  if (m_cache_enable_clouds) {    clouds = new Clouds(smgr, -1, time(0));    if (!clouds) {      *error_message = "Memory allocation error (clouds)";      errorstream << *error_message << std::endl;      return false;    }  }}

В случае, если new не сможет создать объект, будет брошено исключение std::bad_alloc, и оно должно быть обработано try-catch блоком. А проверка в таком виде бесполезна.

Чтение за границей массива

V781 The value of the 'i' index is checked after it was used. Perhaps there is a mistake in program logic. irrString.h 572

bool equalsn(const string<T,TAlloc>& other, u32 n) const{  u32 i;  for(i=0; array[i] && other[i] && i < n; ++i) // <=    if (array[i] != other[i])      return false;  // if one (or both) of the strings was smaller then they  // are only equal if they have the same length  return (i == n) || (used == other.used);}

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

for (i=0; i < n; ++i) // <=  if (!array[i] || !other[i] || array[i] != other[i])    return false;

Другие ошибки

Данная статья посвящена анализу pull request-ов в Azure DevOps и не ставит целью провести подробный обзор ошибок проекта Minetest. Здесь выписаны только некоторые фрагменты кода, которые мне показались интересными. Предлагаем авторам проекта не руководствоваться этой статьёй для исправления ошибок и выполнить более тщательный анализ предупреждений, которые выдаст PVS-Studio.

Заключение


Благодаря гибкой конфигурации в режиме командной строки, анализ PVS-Studio может быть встроен в самые разнообразные сценарии CI/CD. А правильное использование доступных ресурсов окупается увеличением производительности.

Нужно отметить, что режим проверки pull request-ов доступен только в Enterprise редакции анализатора. Чтобы получить демонстрационную Enterprise лицензию, укажите это в комментарии при запросе лицензии на странице скачивания. Более подробно о разнице между лицензиями можно узнать на странице заказа PVS-Studio.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Alexey Govorov. PVS-Studio: analyzing pull requests in Azure DevOps using self-hosted agents.
Подробнее..

Как статический анализ кода помогает в сфере GameDev

30.11.2020 12:14:44 | Автор: admin
image1.png

Игровая индустрия не стоит на месте и с каждым днём развивается всё быстрее и быстрее. Вместе с ростом индустрии растёт и сложность разработки: кода становится больше и багов в нём тоже становится больше. Поэтому современные игровые проекты нуждаются в особом внимании к качеству кода. Сегодня мы расскажем об одном из способов сделать ваш код лучше статическом анализе, а также о том, как PVS-Studio на практике помогает в разработке крупных (и не только) игровых проектов.

"The most important thing I have done as a programmer in recent years is to aggressively pursue static code analysis. Even more valuable than the hundreds of serious bugs I have prevented with it is the change in mindset about the way I view software reliability and code quality." Джон Кармак

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

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

Естественно, не обошлось и без интересных историй. О нескольких таких историях и пойдёт речь в этой статье.

PVS-Studio и Unity


image2.png

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

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

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

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

Однако написание статей это далеко не всё. Мы продолжаем работать над PVS-Studio, и GameDev является для нас одним из важнейших направлений для развития. Поэтому мы хотим, чтобы разработчики игр на Unity могли получить как можно более качественный анализ своих проектов.

Одним из шагов по улучшению качества анализа Unity-проектов для нас стало написание аннотаций для методов, определённых в Unity Scripting API.

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

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

Нами уже написано огромное множество различных аннотаций (например, для методов из пространства имён System), и мы были рады пополнить их аннотациями методов из Unity Scripting API.

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

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

  • UnityEngine.Vector3
  • UnityEngine.Mathf
  • UnityEngine.Debug
  • UnityEngine.GameObject
  • UnityEngine.Material
  • UnityEditor.EditorGUILayout
  • UnityEngine.Component
  • UnityEngine.Object
  • UnityEngine.GUILayout
  • UnityEngine.Quaternion
  • ...

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

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

MeshRenderer renderer = cube.GetComponent<MeshRenderer>();Material m = renderer.material;List<int> outNames = null;m.GetTexturePropertyNameIDs(outNames);

крашится непосредственно сам редактор Unity (по крайней мере, в версии 2019.3.10f1). Вряд ли, конечно, кто-то станет писать подобный код, но интересен сам факт того, что редактор Unity можно "повалить" запуском подобного скрипта.

Итак, аннотации написаны. После запуска анализа мы сразу обнаружили новые срабатывания. Например, анализатор обнаружил странный вызов метода GetComponent:

void OnEnable(){  GameObject uiManager = GameObject.Find("UIRoot");  if (uiManager)  {    uiManager.GetComponent<UIManager>();  }}

Предупреждение анализатора: V3010 The return value of function 'GetComponent' is required to be utilized. ADDITIONAL IN CURRENT UIEditorWindow.cs 22

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

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

Если вам хочется подробнее прочитать про нашу работу с аннотациями для Unity-методов, вы можете сделать это в нашей статье: Как анализатор PVS-Studio стал находить ещё больше ошибок в проектах на Unity.

Unreal Engine 4


image3.png

Когда в далёком 2014-м году разработчики Unreal Engine 4 выложили исходный код движка в открытый доступ, мы просто не смогли обойти этот проект стороной и также написали о нём статью. Разработчикам движка понравилась статья и они исправили найденные нами ошибки. Но нам было этого мало, и мы решили попробовать продать компании Epic Games лицензию на наш анализатор.

Компания Epic Games была заинтересована в улучшении своего движка с помощью PVS-Studio, поэтому мы сошлись на соглашении: мы своими силами исправляем код Unreal Engine так, чтобы анализатор не выдавал на него ни одного предупреждения, а ребята из Epic Games покупают нашу лицензию и дополнительно вознаграждают нас за проделанную работу.

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

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

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

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

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

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

Этот сценарий, конечно, удобен, но разработчики из Epic Games хотели, чтобы их код был исправлен сразу, и передали это дело нам.

И мы приступили к работе. Проверив код проекта, мы обнаружили 1821 одно предупреждение уровней Level_1 и Level_2. Разбор такого объёма предупреждений требует серьезной работы, и, чтобы облегчить весь этот процесс, мы настроили непрерывный анализ кода на нашем CI-сервере.

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

Весь процесс занял у нас 17 рабочих дней. График исправления предупреждений получился следующий:

image4.png

На самом деле, этот график не полностью отражает нашу работу. После того, как мы исправили все предупреждения, мы еще в течение двух дней ждали, когда они примут наши последние пулл-реквесты. Все это время у нас продолжала работать автоматическая проверка последней версии Unreal Engine, который, в свою очередь, продолжал пополняться новым кодом. И что бы вы думали? За эти два дня PVS-Studio обнаружил в коде еще целых четыре ошибки! Одна из них была особенно серьезной и потенциально могла привести к неопределённому поведению.

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

Подробнее о том, как мы работали над кодом Unreal Engine, вы можете прочитать в официальном блоге Unreal Engine или на нашем сайте.

Анализ различных игр


image5.png

Я уже говорил, что мы проверяем различные открытые проекты и пишем о них статьи? Так вот, подобных статей про игровые проекты у нас накопилась просто целая уйма! Мы писали про такие игры, как VVVVVV, Space Engineers, Command & Conquer, osu! и даже (очень древняя статья) Doom 3. А еще мы составили топ 10 самых интересных программных ошибок из видеоигровой индустрии.

Мы также проверили, пожалуй, большинство известных движков с открытым исходным кодом. Помимо Unity и Unreal Engine 4, под наш прицел попали такие проекты, как Godot, Bullet, Amazon Lumberyard, Cry Engine V и многие другие.

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

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

Заключение


На этом моя небольшая статья подходит к концу. Желаю вам чистого и корректно работающего кода без багов и ошибок!

Заинтересовала тема статического анализа? Хочется проверить свой проект на наличие ошибок? Попробуйте PVS-Studio.



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: George Gribkov. How static code analysis helps in the GameDev industry.
Подробнее..

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

19.06.2020 10:21:33 | Автор: admin


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

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

Используем ее и мы в своем мобильном мультиплеерном шутере Dino Squad.

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

В паре слов о нашем коре и технологиях.

Dino Squad сетевой мобильный PvP-шутер. Игроки управляют динозаврами, обвешанными разнообразным вооружением, и сражаются друг с другом командами 6 на 6.

И клиент, и сервер у нас на Unity. Архитектура довольно классическая для шутеров: сервер авторитарный, а на клиентах работает клиентское предсказание. Игровая симуляция написана с использованием in-house ECS и используется как на сервере, так и на клиенте.

Если вы впервые услышали про лагокомпенсацию, вот краткий экскурс в проблематику.

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

Если на локальных сетях эта задержка (в народе именуемая input lag) может быть незаметна, то при игре через интернет она создает ощущение скольжения по льду при управлении персонажем. Эта проблема вдвойне актуальна для мобильных сетей, где случай, когда у игрока пинг составляет 200 мс, считается еще отличным соединением. Часто пинг бывает и 350, и 500, и 1000 мс. Тогда уже играть с инпут лагом в быстрый шутер становится практически невозможно.

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

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

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

Вооружившись этими знаниями, мы начали внедрять серверную лагкомпенсацию в Dino Squad. Прежде всего предстояло понять, как вообще восстановить на сервере то, что видел клиент? И что конкретно нужно восстанавливать? В нашей игре попадания оружия и способностей рассчитываются через рейкасты и оверлапы то есть, через взаимодействия с физическими коллайдерами противника. Соответственно, то положение этих коллайдеров, которое видел игрок локально, нам и требовалось воспроизвести на сервере. На тот момент мы использовали Unity версии 2018.x. API физики там статический, физический мир существует в единственном экземпляре. Возможности сохранить его состояние, чтобы потом его восстановить из коробки, нет. Так что же делать?

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

  1. Про каждого клиента нам нужно знать, в каком времени он видел противников, когда нажимал на клавиши. Мы уже писали эту информацию в пакет инпута и использовали ее для корректировки работы клиентского предсказания.
  2. Нам нужно уметь хранить историю состояний игры. Именно в ней мы будем держать позиции противников (а значит, и их коллайдеров). На сервере история состояний у нас уже была, мы использовали ее для построения дельт. Зная нужное время, мы легко могли бы найти нужное состояние в истории.
  3. Теперь, когда у нас на руках есть состояние игры из истории, нам нужно уметь синхронизировать данные об игроках с состоянием физического мира. Существующие коллайдеры передвинуть, недостающие создать, лишние уничтожить. Эта логика у нас тоже уже была написана и состояла из нескольких ECS систем. Использовали мы ее для того, чтобы держать несколько игровых комнат в одном Unity-процессе. И поскольку физический мир один на процесс, его приходилось переиспользовать между комнатами. Перед каждым тиком симуляции мы сбрасывали состояние физического мира и заново инициализировали его данными для текущей комнаты, пытаясь по максимуму переиспользовать игровые объекты Unity через хитрую систему пулов. Оставалось вызвать эту же логику для игрового состояния из прошлого.

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

public class TimeMachine : ITimeMachine{     //История игровых состояний     private readonly IGameStateHistory _history;     //Текущее игровое состояние на сервере     private readonly ExecutableSystem[] _systems;     //Набор систем, расставляющих коллайдеры в физическом мире      //по данным из игрового состояния     private readonly GameState _presentState;     public TimeMachine(IGameStateHistory history, GameState presentState, ExecutableSystem[] timeInitSystems)     {         _history = history;          _presentState = presentState;         _systems = timeInitSystems;       }     public GameState TravelToTime(int tick)     {         var pastState = tick == _presentState.Time ? _presentState : _history.Get(tick);         foreach (var system in _systems)         {             system.Execute(pastState);         }         return pastState;     }}

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

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

Но в Dino Squad таких механик очень мало! Большая часть оружия в игре создает проджектайлы долгоживущие пули, которые летят несколько тиков симуляции (в некоторых случаях десятки тиков). Как быть с ними, в каком времени они должны лететь?

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

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

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


На картинке игрок в 30-ом тике стреляет ракетой на упреждение: он видит, в каком направлении бежит противник, и знает примерную скорость ракеты. Локально он видит, что попал в цель в 33-ем тике. Благодаря лагкомпенсации попадет он и на сервере

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

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



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

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

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

На этом этапе мы получили в целом рабочую систему. Ее код в несколько упрощенном виде:

public sealed class LagCompensationSystemGroup : ExecutableSystem{     //Машина времени     private readonly ITimeMachine _timeMachine;     //Набор систем лагкомпенсации     private readonly LagCompensationSystem[] _systems;          //Наша реализация кластеризатора     private readonly TimeTravelMap _travelMap = new TimeTravelMap();    public LagCompensationSystemGroup(ITimeMachine timeMachine,         LagCompensationSystem[] lagCompensationSystems)     {         _timeMachine = timeMachine;         _systems = lagCompensationSystems;     }     public override void Execute(GameState gs)     {         //На вход кластеризатор принимает текущее игровое состояние,         //а на выход выдает набор корзин. В каждой корзине лежат энтити,         //которым для лагкомпенсации нужно одно и то же время из истории.         var buckets = _travelMap.RefillBuckets(gs);         for (int bucketIndex = 0; bucketIndex < buckets.Count; bucketIndex++)         {             ProcessBucket(gs, buckets[bucketIndex]);         }         //В конце лагкомпенсации мы восстанавливаем физический мир          //в исходное состояние         _timeMachine.TravelToTime(gs.Time);     }     private void ProcessBucket(GameState presentState, TimeTravelMap.Bucket bucket)     {         //Откатываем время один раз для каждой корзины         var pastState = _timeMachine.TravelToTime(bucket.Time);         foreach (var system in _systems)         {               system.PastState = pastState;               system.PresentState = presentState;               foreach (var entity in bucket)               {                   system.Execute(entity);               }          }     }}

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

1. Понять, насколько сильно ограничивать максимальную дальность перемещения во времени.

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

2. Определить, какие объекты можно перемещать во времени, а какие нет.

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

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

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

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

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

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

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

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

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

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

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

Помимо этого, мы провели небольшое исследование на тему того, можно ли использовать физические сцены для хранения истории физического мира. То есть, условно, выделить каждой комнате не одну сцену, а 30 сцен, и сделать из них циклический буфер, в котором и хранить историю. В целом вариант оказался рабочим, но внедрять мы его не стали: он не показал какого-то сумасшедшего прироста производительности, но требовал довольно рискованных изменений. Сложно было предсказать, как поведет себя сервер при длительной работе с таким количеством сцен. Поэтому мы последовали правилу: If it ain't broke, don't fix it.
Подробнее..

Перевод Охотники, щелкуны и Элли как устроен игровой искусственный интеллект в The Last of Us

20.06.2020 12:14:32 | Автор: admin
Вчера состоялся релиз сиквела The Last of Us игры, уже семь лет являющейся одним из наиболее узнаваемых эксклюзивов PlayStation. Это кинематографическая история о человеческих жизнях в бесчеловечной реальности мира, разрушенного современной чумой. В то время, когда игроки берут на себя управление циничным и озлобленным главным героем Джоэлом, искусственный интеллект разыгрывает других персонажей, будь то союзник, враг или зараженный.

На фоне выхода второй части игры рассказываем о том, почему игрокам так понравился оригинал. В этой переводной статье поговорим о философии дизайна The Last of Us, касающейся всех аспектов искусственного интеллекта.



Предупреждение: речь в статье идет только об оригинальной игре 2013 года.


Об игре


The Last of Us игра от третьего лица в жанре action-adventure с фокусом на механиках стелса и динамической системе укрытий. В мире постапокалиптической Америки игроки вынуждены противостоять двум типам противников: охотникам людям, контролирующим и патрулирующим отдельные территории по всей стране, и зараженным существам, в которых превращаются люди, заболевшие грибковой чумой.
Когда мы только начинали создавать прототипы ИИ в случае, когда врагом является человек, первый вопрос, которым мы задались, был следующий: как заставить игрока поверить в реальность своих противников, чтобы ему не хотелось их убивать? Ответ на этот вопрос и стал основой дизайна вражеского ИИ. Для ответа на него потребовалось нечто большее, чем просто нанять лучших актеров озвучки, лучших 3D-художников и аниматоров, хотя все это тоже было немаловажным. Прежде всего нам требовалось решить проблему самого искусственного интеллекта. Потому что, если мы не сможем заставить игрока поверить, будто эти отряды выживших мыслят и взаимодействуют, как настоящие люди, никакие передовые мокапы не удержат игрока в потоке.

Трэвис Макинтош, Вражеский ИИ в The Last of Us, Game AI Pro, том 2, глава 34, 2015

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

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


Архитектура ИИ


Начнем с объяснения основ архитектуры искусственного интеллекта различных персонажей в игре и того, почему Naughty Dog построили ее именно таким образом.

ИИ в The Last of Us строится на использовании конечных автоматов (КА) давно устоявшегося подхода к созданию искусственного интеллекта. Популяризовала его Half-Life еще в 1998 году. Его суть состоит в том, что персонаж может иметь несколько сменяющих друг друга состояний: к примеру, он будет атаковать цель либо искать ее местоположение до тех пор, пока не произойдет событие-триггер, которое переведет его из одного состояния в другое.


Простой КА с двумя состояниями

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

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

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



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

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

Марк Ботта, ИИ зараженных в The Last of Us, Game AI Pro, том 2, глава 33, 2015

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

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




Охотники


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

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

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

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

Изначально в The Last of Us они были такими же, как в другой серии игр Naughty Dog Uncharted. Но со своей функцией они справлялись плохо: игрока слишком быстро замечали как на близком, так и на дальнем расстоянии. Они не соответствовали темпу игры, а потому были видоизменены и имели уже не конусообразную форму. Теперь поле зрения NPC стало напоминать контур замочной скважины, тем самым обеспечивая более широкое периферическое зрение и более узкий обзор на расстоянии.


Поле зрения NPC в Uncharted


Поле зрения NPC в The Last of Us

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

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



У охотников есть множество навыков. Как мы видим, большинство из них строится вокруг боя: ближнего, дальнего, при прямом наступлении или подходе с фланга. Большинство последовательностей боевых действий в The Last of Us начинается с того, что игрок, пребывавший в невидимом режиме, оказывается обнаружен. Поэтому сейчас мы сосредоточимся на двух навыках, наиболее критических в режиме стелса: на расследовании и поиске.

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

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

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

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

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

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

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


Часть территории не видно, но ее можно проверить

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

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


Выбор наилучшего укрытия с учетом позиции игрока

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

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

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

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



Зараженные


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

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

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

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

Давайте посмотрим на то, как работает звук в игре.

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


Радиус распространения звука

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

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



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

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



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

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

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

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

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

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

image

Элли


Теперь, когда мы изучили философию искусственного интеллекта противника, осталась еще одна неохваченная тема: ИИ союзника и, в частности, Элли.

Одна из первых демонстраций игры в январе 2013 года показывала то, как игроки покидают карантинную зону Бостона. Тэсс и Элли под предводительством Джоэла пробирались сквозь небоскреб, переполненный бегунами и щелкунами. В этом демо персонажи-компаньоны должны были держаться поодаль от места чистки от зараженных, но в то время их ИИ был еще сырым и не соответствовал задуманному Naughty Dog.

Итак, давайте поговорим о приоритетах ИИ в случае с Элли:

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

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

Чтобы Элли могла следовать за Джоэлом, игра создает область вокруг игрока, которой также стараются придерживаться Тэсс, Билл, Генри и Сэм хотя обычно они держатся на большем расстоянии от Джоэла, нежели Элли. Как только эта область выстроена, игра отправляет рейкасты параллельно навигационной сетке в целью поиска подходящих позиций для размещения компаньонов. Как и в случае системы очков укрытий, каждая позиция оценивается по таким параметрам, как расстояние до врагов и союзников, угол относительно позиции игрока, не блокирует ли геометрия взгляд на Джоэла или движение вперед. Решить эту проблему правильно невероятно трудно и все же, пусть система неидеальна, гораздо чаще ей удается разместить персонажа правильно, чем не удается.


Определение допустимых позиций для Элли на основе положения Джоэла в случае, когда они прячутся за укрытием

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

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

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


Режим следования в укрытии


Режим защиты в укрытии

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

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

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

И остается еще один немаловажный приоритет.

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

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

Заключение


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

Немного о нашей безымянной студии и о том, что мы делаем

31.01.2021 22:19:51 | Автор: admin

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

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

Начало

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

Работу мы разделили примерно пополам ну или делали кто что может. С одной стороны, если в команде 2 человека, то делить работу можно пополам! Удобно же :) И на контроль за результатами работы всех разработчиков не так много времени уходит как, скажем, ушло бы если бы в нашей команде было 30 человек.

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

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

Не поверите, но у меня набралось 2 папки со скетчами по проекту и иногда мне все еще приходится набрасывать картинки/перебирать идеи/сопоставлять обьекты. Скетчи, которые я набрасывал выглядят примерно так:

На данный момент мы опубликовали большую часть скетчей в твиттере / инстаграме.

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

Кстати, игра на тот момент выглядела так:

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

Еще несколько скетчей:

Коротко о некоротком, о сети

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

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

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

Лучше синица в руках, чем журавль в небе

Попытки расширить команду

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

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

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

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

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

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

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

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

А вот и ссылки на нас:

https://twitter.com/CGAleksey

https://www.instagram.com/cgaleksey

https://vk.com/treload

Подробнее..

Wild Horizon. Обзор наших первых изменений

02.06.2021 00:12:39 | Автор: admin

Очередной выпуск "игровой газеты". Изменения на первой полосе. Ответы на вопросы.

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

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

Проект находится на стадии доработки.

Краткие ответы на ваши вопросы:

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

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

На каком движке игра? Unity. Работали на нем ранее, полностью устраивает.

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

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

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

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

Samsung а51Samsung а51

Будут ли еще добавлены ресурсы? Да, раз в неделю добавляем по одной шахте для разных металлов.

ШахтыШахты

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

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

Работаешь ли ты еще где то? Да, безусловно, дизайн мое всё, а проект это что-то вроде хобби, которое в будущем хотелось бы превратить в работу.

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

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

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

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

Любите игры, играйте в игры!

Подробнее..

Как внезапно для себя написать игру за день

30.12.2020 00:08:02 | Автор: admin

Компьютер осветил глубокую темноту комнаты белым ярким светом. На часах в углу экрана цифра 8 сменила цифру 7, и часы показали 5:58 AM.

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

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

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

Дело вот в чем: среди просмотренных в 6 утра картинок на фейсбуке промелькнула одна, которая меня заинтересовала. На ней Санта Клаус иолень стояли в боевых позициях друг напротив друга, согнув руки в локтях и держа кулаки на уровне головы. Над противниками виднелись две красные полоски и между ними яркая надпись Fight!

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

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

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

Готовый дед без рукГотовый дед без рук

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

Несколько строк кода, и вот картинки уже двигаются по прямой по нажатию стрелок. Нужно анимировать удары! Ох! Руки!

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

Олень с рукамиОлень с руками

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

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

Перспективный художникПерспективный художник

Больше всех над моей поделкой смеялась моя супруга, которая вообще холодна к данному жанру, но в этот раз (такой уж выдался год) отметелила Санту около пяти раз, после чего предложила историю, которая была единогласно принята, и в нынешней версии предваряет рождественское мордобитие. Поучаствовать в нем можно и после Католического Рождества https://helprudy.com.

Говорят (говорят!), что существует специальная, новогодняя версия: Дед Мороз против Санта Клауса. Говорят, что нужно, чтобы этот пост набрал определенное количество плюсов, чтобы открыть Деда Мороза. Помогите проверить! Без вашей помощи никуда!!!

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

Подробнее..

Из песочницы Как мы сделали VR-игру о космической катастрофе

27.06.2020 14:19:52 | Автор: admin

Этапы, ошибки, выводы


Я пришёл в BeaversBrothers год назад. Меня позвали, когда компания запускала новую услугу разработку VR/AR-решений для бизнеса. Чтобы отработать технологии и понять наши возможности, мы решили сделать виртуальную игру. Ведь в игре есть всё: завораживающий мир, яркий сюжет, увлекающий сценарий, интерактив с пользователем, сложная техническая реализация. Поэтому такой пилот был интересен нам вдвойне.

Мы разрабатывали игру полгода. Это был первый командный проект по виртуальной реальности, и мы наступили на все грабли, на которые могли наступить. В итоге мы получили колоссальный опыт, отработали все возможные ошибки, но всё-таки зарелизили игру Astro Collapse в магазине Oculus.

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

Об игре


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


Цель игры: спасти как можно больше кораблей и защитить планету

Поиск идеи


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

Тюбинг


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


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

Картинг


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


В прототипе игры была простая механика: объезжать коробки и не врезаться в стены.

Спасение Земли


Здесь совсем просто. Я читал новости в интернете и заметил, что сейчас много говорят о космосе: Илоне Маске, жизни на Марсе, происхождении Вселенной. С этого началась история Astro Collapse.


Первый прототип космического шутера: поначалу он был медленным и скучным

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

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

Сценарий игры


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

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

image

Наш редактор тестирует первую версию игры:

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

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

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


Сцены из второго прототипа игры

Мы выдохнули. По нашему мнению, игра получилась захватывающей и интересной! Но не всё так просто

Разгромное тестирование на детях и геймерах


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

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

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


Фразы взрослых во время тестирования: Почему такие быстрые астероиды? Почему их так много? А это что? А это кто? А это зачем?

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

image
Фразы детей: Почему так мало астероидов? Где босс-астероид? Разве это взрыв?

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

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

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


Трейлер игры

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

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

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

Главная ошибка напоследок


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

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

Подведём итоги


Что важно знать, когда вы разрабатываете VR-игру:

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

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

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

А пока сыграйте в Astro Collapse и спасите планету от астероидов! Для игры вам понадобятся очки Samsung Gear VR. Они работают со смартфонами: Samsung Galaxy Note 4, 5, 7, S6/S6 Edge/S6 Edge+, S7/S7 Edge, S8/S8+.

До скорого!
Подробнее..
Категории: Ar и vr , Игры , Game development , Ar vr , Oculus vr

Перевод Пулинг объектов в Unity 2021

03.06.2021 18:21:18 | Автор: admin

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

  • A: уменьшить количество пуль до 20

  • B: реализовать свою собственную пулинговую систему

  • C: заплатить 50 долларов за пулинговую систему в Asset Store

  • D: использовать новый Pooling API Unity, представленный в 2021 году

(СМОТРИТЕ ВИДЕО В ОРИГИНАЛЕ СТАТЬИ)

В этой статье мы рассмотрим последний вариант.

Сегодня вы узнаете, как использовать новый Pooling API, представленный в 2021 году.

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

Готовы узнать о них побольше?

Когда вам нужен пул?

Начнем с самого главного вопроса: когда вам нужен пул?

Я задаю его, потому что пулы не должны быть вашим дежурным решением.

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

Но мы рассмотрим это позже.

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

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

  • Вы часто аллоцируете и высвобождаете объекты, хранящиеся в куче (вместо их повторного использования). Это относится и к коллекциям C#.

Эти операции вызывают много аллокаций, следовательно вы сталкиваетесь с:

  • Избыточным расходом тактов процессора на операций создания и уничтожения (или new/dispose).

  • Преждевременной сборкой мусора, вызывающей фризы, которые ваши игроки не оценят.

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

Не кажется ли вам, что эти проблемы могут представлять для вас угрозу?

(Если сейчас - нет, то они могут позже)

Но давайте продолжим.

Итак, что же такое это (объектный) пулинг в Unity в конце-то концов?

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

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

Сущность может быть чем угодно: игровым объектом, инстансом префаба, словарем C# и т. д.

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

Допустим, вам нужно завтра утром пойти за продуктами.

Что вы обычно берете с собой, кроме кошелька и ключей?

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

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

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

Это и есть пул.

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

Вам нужна сумка?

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

Поняли в чем соль?

Вот основные детали юзкейса пулинга:

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

  • Глобальная цель для всех этих элементов, например, перенос продуктов, стрельба пулями и т.д. ...

  • Функции, которые вы выполняете над пулом и его элементами: Take (взять), Return (вернуть), Reset (сбросить).

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

  • Вы создаете тысячу пуль и помещаете их в пул.

  • Каждый раз, когда вы стреляете из своего оружия, вы берете пулю из этого пула.

  • Когда пуля попадает во что-то и исчезает, вы возвращает ее обратно в пул.

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

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

Когда следует отказаться от использования пула?

У техники пулинга есть несколько (потенциальных) проблем:

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

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

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

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

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

Помните: самое главное это частота ваших операций создания и уничтожения.

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

Позже мы рассмотрим больше проблем с пулами.

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

Пулы объектов в Unity 2021: ваши варианты

Если вы хотите добавить пул объектов в свой проект Unity, у вас есть три варианта:

  • Создать свою собственную систему

  • Купить стороннюю систему пулинга

  • Импортировать UnityEngine.Pool

Давайте рассмотрим их.

A) Создаем свою собственную систему пулинга

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

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

  • Создать и удалить пул (Create & dispose)

  • Взять из пула (Take)

  • Вернуться в пул (Return)

  • Операции сброса (Reset)

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

  • Типобезопасности

  • Управление памятью и структурах данных

  • Пользовательской аллокации/высвобождении объектов

  • Потокобезопасности

Это уже больше похоже на головную боль? Чувствую, ваше лицо побледнело...

Предлагаю не изобретать велосипед (если только это не учебное упражнение).

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

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

B) Сторонние системы пулинга объектов

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

  • The Unity Asset Store

  • Github

  • Друг или член семьи

Давайте рассмотрим несколько примеров:

Pooling Toolkit

13Pixels Pooling

Pure Pool

Pro Pooling

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

Сторонние инструменты могут творить чудеса и обладают множеством фич.

Но у них есть недостатки:

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

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

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

  • Они могут быть достаточно дорогими (и по деньгам и по времени).

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

И в настоящее время осталось еще меньше причин для использования сторонних решений, поскольку Unity втихаря зарелизила новый API для пулинга в Unity 2021.

И это основная тема этой статьи.

C) Новый Pooling API от Unity

Начиная с версии 2021 года, Unity зарелизила несколько механизмов пулинга C#, которые помогут вам во множестве юзкейсов.

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

Огромный плюс у вас есть доступ к их исходному коду.

И я должен отметить, что их реализации довольно просты. Это приятное вечернее чтиво.

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

Как использовать новый Object Pooling API в Unity 2021

Первый шаг убедиться, что вы используете Unity 2021+.

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

После этого, это просто вопрос знания Unity Pooling API:

  • Операции пулинга

  • Различные контейнеры пулов

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

(СМОТРИТЕ ВИДЕО В ОРИГИНАЛЕ СТАТЬИ)

1. Построение вашего пула

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

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

createFunc

Вызывается для создания нового экземпляра вашего объекта, например () => new GameObject(Bullet) or () => new Vector3(0,0,0)

actionOnGet

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

actionOnRelease

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

actionOnDestroy

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

collectionCheck

True, если вы хотите, чтобы Unity проверяла, что этот элемент еще не был в пуле, когда вы пытаетесь его вернуть (только в редакторе).

defaultCapacity

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

maxSize

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

Вот как вы можете создать пул GameObjects:

_pool = new ObjectPool<GameObject>(createFunc: () => new GameObject("PooledObject"), actionOnGet: (obj) => obj.SetActive(true), actionOnRelease: (obj) => obj.SetActive(false), actionOnDestroy: (obj) => Destroy(obj), collectionChecks: false, defaultCapacity: 10, maxPoolSize: 10);

Я оставил названия параметров для наглядности; не стесняйтесь пропускать их в вашем коде.

И, конечно же, это всего лишь пример с GameObject. Вы можете использовать его с любым типом, с которым захотите.

Хорошо, теперь у вас есть пул _GameObject_ов.

Как нам им пользоваться?

2. Создание элементов пула

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

Мы уже указали это в конструкторе, поскольку передали функцию createFunc в качестве первого параметра конструктору пула.

Каждый раз, когда вы захотите взять GameObject из пустого пула, Unity создаст его для вас и отдаст вам.

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

А как нам взять GameObject из пула?

3. Извлечение элемента из пула

Теперь, когда ссылка на пул хранится в _pool, вы можете вызвать его функцию Get:

GameObject myGameObject = _pool.Get();

Вот и все.

Теперь вы можете использовать объект по своему усмотрению (в определенных рамках).

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

4. Возврат элемента в пул

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

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

Как это сделать? Легко:

_pool.Return(myObject);

Тогда пул:

  1. Вызовет функцию actionOnRelease, которую вы предоставили с этим элементом качестве аргумента, чтобы деактивировать его, остановить систему частиц и т.д. ...

  2. Проверит, есть ли достаточно места в своем внутреннем списке/стеке на основе MaxSize

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

  4. Если свободного места нет, то он уничтожит объект, вызвав actionOnDestroy.

Вот и все.

А теперь об уничтожении элементов.

5. Уничтожение элемента из вашего пула

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

И делает это он, вызывая функцию actionOnDestroy, которую вы передали в его конструкторе.

Эта функция может быть совершенно пустой или вызывать Destroy(myObject), если мы говорим об объектах, управляемых Unity.

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

6. Очистка и утилизация вашего пула

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

_pool.Dispose();

Вот это собственно и есть вся функциональность пула. Но нам все еще не хватает одного важного момента.

Не все пулы созданы для одних и тех же юзкейсов

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

Типы пулов в Unity 2021+

LinkedPool и ObjectPool

Первая группа пулов это те, которые охватывают обычные объекты C# (95%+ элементов, которые вы, возможно, захотите поместить в пул).

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

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

ObjectPool просто использует Stack C#, который использует массив C# под капотом:

private T[] _array;

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

Наихудший случай наличие 0 элементов (длина = 0) в большом пуле (емкость = 100000). Там у вас будет большой кусок зарезервированной памяти, который вы не используете.

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

Подсказка: вы можете избежать изменения размера стека, играя с параметром конструктора maxCapacity.

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

internal class LinkedPoolItem { internal LinkedPool<T>.LinkedPoolItem poolNext; internal T value; }

Используя LinkedPool, вы используете память только для элементов, которые фактически хранятся в пуле.

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

Итак, давайте поговорим о следующей категории классов пулов в Unity.

ListPool, DictionaryPool, HashSetPool, CollectionPool

Теперь мы поговорим о пулах коллекций C# в Unity.

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

И достаточно часто вам нужно часто создавать/уничтожать эти коллекции.

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

Вот в чем собственно дело.

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

  • Аллоцируете и высвобождаете коллекцию плюс ее внутренние структуры данных.

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

Таким образом, решение, которое помогает с некоторыми из этих рантайм аллокаций в Unity, - это пулинг коллекций.

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

Вот пример:

var manuallyReleasedPooledList = ListPool<Vector2>.Get();manuallyReleasedPooledList.Add(Random.insideUnitCircle);// Use your pool// ...ListPool<Vector2>.Release(manuallyReleasedPooledList);

А вот другая конструкция, которая освобождает для вас пул коллекций:

using (var pooledObject = ListPool<Vector2>.Get(out List<Vector2> automaticallyReleasedPooledList)){   automaticallyReleasedPooledList.Add(Random.insideUnitCircle);   // Use your pool   // ...}

Каждый раз, когда вы выходите за пределы этого using блока, Unity будет возвращать список в пул за вас.

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

ListPool, DictionaryPool и HashSetPool - это особые пулы для соответствующих типов коллекций.

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

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

Наконец, давайте посмотрим на других плохишей: GenericPool и его близнеца UnsafeGenericPool.

Они, как и описывают их названия, являются пулами общих объектов. Но в них есть кое-что особенное.

GenericPool и UnsafeGenericPool

Итак, что такого особенного с этими пулами объектов?

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

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

Вы просто используете их, когда и где бы (и кем бы) вы ни находились.

var pooledGameObject = GenericPool<GameObject>.Get(); pooledGameObject.transform.position = Vector3.one; GenericPool<GameObject>.Release(pooledGameObject);

Вот так просто.

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

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

БАБАХ!

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

Подводя итоги различий:

GenericPool использует статический ObjectPool с collectionCheck = true

UnsafeGenericPool использует статический ObjectPool с collectionCheck = false

Хорошо, как вы видели, не все в пулах красиво и аккуратно. Но вотрем еще немного соли в рану.

Проблемы с пулами (почему вы не должны ими злоупотреблять)

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

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

Вот некоторые из проблем, с которыми вы можете столкнуться при использовании пулов:

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

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

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

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

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

Хорошая пища для размышлений.

Так, что еще?

Пулы отличные инструменты для снижения:

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

  • давления, которое вы оказываете на бедный сборщик мусора;

А с Unity 2021+ теперь стало проще, чем когда-либо, принять пул как образ жизни разработчика, поскольку теперь у нас есть встроенное pooling API.

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

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


Перевод материала подготовлен в рамках курса "Unity Game Developer. Professional". Если вам интересно узнать о курсе подробнее, приглашаем на день открытых дверей: на нем преподаватель расскажет о формате и особенностях обучения, о программе и выпускном проекте.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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