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

Блог компании pixonic

Как и почему мы стали делать 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.

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

Подробнее..

Избавляемся от мистических строк в системе реактивного связывания на Unity

17.12.2020 10:10:32 | Автор: admin
Любая система, которая часто используется в проекте, со временем обречена на эволюцию. Так случилось и с нашей системой реактивного связывания reactive bindings.

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



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

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

Два основных неудобства, с которыми мы столкнулись:

  • Строковые ключи в коде;
  • Нет проверки соответствия ключей в коде и ключей в модели.

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

Но обо всем по порядку.

В наших reactive bindings доступ к полям происходит по связке тип поля-строковый путь во ViewModel. Отсюда повсеместно мы имели подобный код:

Посмотреть код
public static class AbilitiesPresenter{  private static readonly PropertyName MechAbilities = "mech/abilities";  private static readonly PropertyName MechAbilitiesIcon = "mech/abilities/icon";  private static readonly PropertyName MechAbilitiesName = "mech/abilities/name";  private static readonly PropertyName MechAbilitiesDescription = "mech/abilities/description";  public static void Present(IViewModel viewModel, List<AbilityInfo> data)  {     var collection = viewModel.GetMutableCollection(MechAbilities);     collection.Fill(data, SetupAbilityItem);  }  private static void SetupAbilityItem(AbilityInfo data, IViewModel model)  {     model.GetString(MechAbilitiesIcon).Value = data.Icon;     model.GetString(MechAbilitiesName).Value = data.Name;     model.GetString(MechAbilitiesDescription).Value = data.Desc;  }}

То есть, посредством GetString/GetInteger/GetBoolean и т. д. мы получаем ссылку на поле в модели и пишем/читаем значения.

В чем проблема этой системы? А в том, что чем больше полей в модели тем больше строк в коде. Читать и поддерживать подобный стиль становится весьма проблематично.

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

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

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

Желаемый формат работы выглядел примерно так:

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

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

Теперь ближе к коду.

Раньше описание работы с элементами у нас было в следующем стиле:

Посмотреть код
public static class AbilitiesPresenter{  private static readonly PropertyName MechAbilities = "mech/abilities";  private static readonly PropertyName MechAbilitiesIcon = "mech/abilities/icon";  private static readonly PropertyName MechAbilitiesName = "mech/abilities/name";  private static readonly PropertyName MechAbilitiesDescription = "mech/abilities/description";  public static void Present(IViewModel viewModel, List<AbilityInfo> data)  {     var collection = viewModel.GetMutableCollection(MechAbilities);     collection.Fill(data, SetupAbilityItem);  }  private static void SetupAbilityItem(AbilityInfo data, IViewModel model)  {     model.GetString(MechAbilitiesIcon).Value = data.Icon;     model.GetString(MechAbilitiesName).Value = data.Name;     model.GetString(MechAbilitiesDescription).Value = data.Desc;  }}

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

Стало же все выглядеть так:

Посмотреть код
namespace DroneDetails{  public class DroneDetailScreenView : UIScreenViewWith3D<DroneViewUI3DScreen>  {     [ExpectReactiveContract(typeof(DroneInfoViewModel))] [ExpectNotNull] [SerializeField]     private ViewModel _droneInfoModel;     [ExpectReactiveContract(typeof(DroneScreenMainEventsModel))] [ExpectNotNull] [SerializeField]     private ViewModel _droneScreenMainEventsModel;     [ExpectReactiveContract(typeof(DroneScreenInfoModel))] [ExpectNotNull] [SerializeField]     private ViewModel _droneScreenInfoModel;     [ExpectReactiveContract(typeof(DroneSpawnInfoViewModel))] [ExpectNotNull] [SerializeField]     private ViewModel _droneSpawnInfoViewModel;     [ExpectReactiveContract(typeof(ScrollListViewModel))] [ExpectNotNull] [SerializeField]     private ViewModel _scrollListViewModel;//.   }}

ViewModel приписывается атрибут ExpectReactiveContract, который получает параметры контракта. Пример контракта выглядит следующим образом:

public struct ConnectionStatusViewModel : IBindViewModel{//пример описания полей  [Bind("connection/is-lost")]  public IMutableProperty<string>IsConnectionLost;  [Bind("mech/slots-count")]   public IMutableProperty<int> SlotsCount;//задание контракта для элементов вложенной коллекции  [Bind("current-drone-info/scheme-slots-info")]     [SchemaContract(typeof(SchemeSlotInfoViewModel))]  public IMutableCollection SchemeSlotsInfo;}

В этом варианте есть явное типизированное поле. Сверху атрибутом Bind описывается строка, которая связывает это поле с ViewModel.

private void OnPreviewDrone(int index){  _droneDetailModel.DroneScrollStateModel.SaveState(index);  var droneId = _dronesListModel.GetDroneIdByIndex(index);  _view.DroneInfoViewModel.DroneId.Value = droneId;  //...}

Способ использования теперь стал каноничным: мы берем структуру (контракт) и устанавливаем новое значение одному из полей (в примере это DroneId).

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

Для описания контракта используются два основных атрибута: Bind и SchemaContract. Bind отвечает за связывание поля структуры с полем во ViewModel. Атрибут получает ключ и опциональное поле IsRequired, говорящее о том, действительно ли во ViewModel необходимо иметь конкретный ключ или ничего не произойдет, если его упустить.

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

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |               AttributeTargets.GenericParameter)]public class BindAttribute : Attribute{  public string ViewModelPath { get; }  public bool IsRequired { get; }  public BindAttribute(string value, bool isRequired = true)  {     ViewModelPath = value;     IsRequired = isRequired;  }}

Атрибут SchemaContract служит с целью указания контракта для элементов коллекции:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |  AttributeTargets.GenericParameter)]public class SchemaContractAttribute : Attribute{  public System.Type[] BindViewModelContracts;  public SchemaContractAttribute(params System.Type[]contracts)  {     BindViewModelContracts = contracts;  }}

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

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

Резолверы имеют простую структуру и хорошо подходят для кодогенерации:

Посмотреть код
// ------------------------------------------------------------------------------// <auto-generated>//     This code was generated by ViewModelBindingsGenerator//     Changes to this file may cause incorrect behavior and will be lost if//     the code is regenerated.// </auto-generated>// ------------------------------------------------------------------------------using PS.ReactiveBindings;using Test;namespace BindViewModel{   public partial struct BindViewModelResolver   {       private static ConnectionStatusViewModel ResolveConnectionStatusViewModel(IViewModel viewModel)           => new ConnectionStatusViewModel           {               IsConnectionLost = LookupProperty<IMutableProperty<string>>(               "ConnectionStatusViewModel",               viewModel,                PropertyType.String,                "connection/is-lost",                true),               SomeCollection = LookupProperty<IMutableCollection>(               "ConnectionStatusViewModel",       viewModel,                PropertyType.Collection,                "mech/tempCollection",                true),           };   }}

Темплейт для генерации:

Посмотреть код
<#@ template debug="false" hostspecific="false" language="C#" #><#@ parameter name ="m_GenerationInfo" type="WarRobots.RBViewModelWrapperGenerator.BindViewModel.GenerationInfo"#>// ------------------------------------------------------------------------------// <auto-generated>//     This code was generated by ViewModelBindingsGenerator//     Changes to this file may cause incorrect behavior and will be lost if//     the code is regenerated.// </auto-generated>// ------------------------------------------------------------------------------using PS.ReactiveBindings;using <#=m_GenerationInfo.Namespace #>;namespace BindViewModel{   public partial struct BindViewModelResolver   {       private static <#=m_GenerationInfo.ClassName #> Resolve<#=m_GenerationInfo.ClassName #>(IViewModel viewModel)           => new <#=m_GenerationInfo.ClassName #>           {<#  foreach (var property in m_GenerationInfo.PropertiesInfo)  {     var requiredString = property.Required.ToString().ToLower();#>               <#=property.Name #> = LookupProperty<<#=property.PropertyTypeName #>>("<#=m_GenerationInfo.ClassName #>",viewModel, <#=property.ReactivePropertyTypeName #>, "<#=property.ViewModelPath #>", <#=requiredString #>),<#  }#>           };   }}

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

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

Посмотреть код
public partial struct BindViewModelResolver{  private static Dictionary<System.Type, IResolver> _resolvers;  static partial void InitResolvers();  public static T Resolve<T>(IViewModel viewModel) where T : struct, IBindViewModel  {     InitResolvers();     if (_resolvers != null && _resolvers.ContainsKey(typeof(T)))     {        var resolver = (Resolver<T>) _resolvers[typeof(T)];        return resolver.Resolve(viewModel);     }     return ResolveWithReflection<T>(viewModel);  }  private class CanNotResolvePropertyException : System.Exception  {     public CanNotResolvePropertyException(string message) : base(message)     {     }  }  private interface IResolver  {  }  private struct Resolver<T> : IResolver     where T : struct, IBindViewModel  {     public delegate T ResolveDelegate(IViewModel viewModel);     public ResolveDelegate Resolve;  }  private static Resolver<T> FromDelegate<T>(Resolver<T>.ResolveDelegate resolveDelegate)     where T : struct, IBindViewModel     => new Resolver<T> {Resolve = resolveDelegate};  private static T LookupProperty<T>(     string holderName,     IViewModel viewModel,     PropertyType type,     PropertyName id,     bool required)     where T : class, IReactive  {     T obj = viewModel.LookupProperty(id, type) as T;     if (obj == null)     {        if (required)        {           throw new CanNotResolvePropertyException(              $"{holderName} -> Can't resolve {id} path => \n PropertyType.{type} \n {id}"           );        }        Debug.LogWarning($"{holderName} -> Can't resolve {id} path => \n PropertyType.{type} \n {id}");     }     return obj;  }  private static T ResolveWithReflection<T>(IViewModel viewModel)  {     var type = typeof(T);     var fields = type.GetFields();     var resolvedStruct = System.Activator.CreateInstance(type);     foreach (var field in fields)     {        var bindAttribute = field.GetCustomAttribute<BindAttribute>();        if (bindAttribute != null)        {           var viewModelPath = bindAttribute.ViewModelPath;           var result = ResolveFieldValue(type.Name, field.FieldType, viewModelPath, viewModel, bindAttribute.IsRequired);           field.SetValue(resolvedStruct, result);        }     }     return (T) resolvedStruct;  }

Сами резолверы лежат в словаре по типам. Этот список резолверов и описан в сгенерированной части. А сама она выглядит так:

Посмотреть код
public partial struct BindViewModelResolver{   static partial void InitResolvers()   {        if (_resolvers != null) return;       _resolvers = new Dictionary<System.Type, IResolver>       {           {typeof(DroneInfoViewModel), FromDelegate(ResolveDroneInfoViewModel)},           {typeof(DroneSchemeMetaphorViewModel), FromDelegate(ResolveDroneSchemeMetaphorViewModel)},           {typeof(DroneScreenInfoModel), FromDelegate(ResolveDroneScreenInfoModel)},           {typeof(DroneScreenMainEventsModel), FromDelegate(ResolveDroneScreenMainEventsModel)},           {typeof(DroneSpawnInfoViewModel), FromDelegate(ResolveDroneSpawnInfoViewModel)},           {typeof(DroneStoreItemViewModel), FromDelegate(ResolveDroneStoreItemViewModel)},           {typeof(HangarSlotViewModel), FromDelegate(ResolveHangarSlotViewModel)},           {typeof(SchemeSlotInfoViewModel), FromDelegate(ResolveSchemeSlotInfoViewModel)},           {typeof(ScrollListViewModel), FromDelegate(ResolveScrollListViewModel)},           {typeof(StateItemViewModel), FromDelegate(ResolveStateItemViewModel)},           {typeof(ConnectionStatusViewModel), FromDelegate(ResolveConnectionStatusViewModel)},           {typeof(TitanStateViewModel), FromDelegate(ResolveTitanStateViewModel)},           {typeof(MechStateViewModel), FromDelegate(ResolveMechStateViewModel)},           {typeof(ChipOfferItemViewModel), FromDelegate(ResolveChipOfferItemViewModel)},           {typeof(DroneOfferItemViewModel), FromDelegate(ResolveDroneOfferItemViewModel)},       };   }}

Темплейт для генерируемой части:

Посмотреть код
<#@ template debug="false" hostspecific="false" language="C#" #><#@ parameter name ="m_GenerationInfos" type="System.Collections.Generic.List<WarRobots.RBViewModelWrapperGenerator.BindViewModel.GenerationInfo>"#><#@ import namespace="System.Collections.Generic" #><#@ import namespace="BindViewModel" #>// ------------------------------------------------------------------------------// <auto-generated>//     This code was generated by ViewModelBindingsGenerator//     Changes to this file may cause incorrect behavior and will be lost if//     the code is regenerated.// </auto-generated>// ------------------------------------------------------------------------------using System.Collections.Generic;<#List<string> namespaces = new List<string>();  foreach (var generationInfo in m_GenerationInfos)  {     if (!namespaces.Contains(generationInfo.Namespace))     {#>using <#=generationInfo.Namespace #>;<#        namespaces.Add(generationInfo.Namespace);     }  }#>namespace BindViewModel{   public partial struct BindViewModelResolver   {       static partial void InitResolvers()       {            if (_resolvers != null) return;           _resolvers = new Dictionary<System.Type, IResolver>           {<#  foreach (var generationInfo in m_GenerationInfos)  {#>               {typeof(<#=generationInfo.ClassName #>), FromDelegate(Resolve<#=generationInfo.ClassName #>)},<#  }#>           };       }   }}

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

Генератор проходится по assemblies и выискивает контракты-наследники IBindViewModel. Найдя контракт, он проходит по нему и заполняет информацию для генерации. Текущая информация состоит из имени переменной, типа, пути для связывания и прочего. Затем подготовленная информация передается непосредственно в T4-генератор.

Код для сбора информации:

Посмотреть код
List<GenerationInfo> generationInfos = new List<GenerationInfo>();Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();foreach (var assembly in assemblies){   var types = assembly.GetTypes();  var iBindViewModelType = typeof(IBindViewModel);  foreach (Type type in types)  {     if (type.IsValueType && iBindViewModelType.IsAssignableFrom(type))     {        GenerationInfo generationInfo = new GenerationInfo {ClassName = type.Name, Namespace = type.Namespace};        var props = new List<PropertyInfo>();        var fields = type.GetFields();        foreach (var field in fields)        {           var bindAttribute = field.GetCustomAttribute<BindAttribute>();           if (bindAttribute != null)           {              var propertyInfo = new PropertyInfo();              propertyInfo.Name = field.Name;              propertyInfo.ViewModelPath = bindAttribute.ViewModelPath;              var propertyTypeNames = GetPropertyTypeName(field.FieldType);              propertyInfo.ReactivePropertyTypeName = propertyTypeNames.ReactivePropertyTypeName;              propertyInfo.PropertyTypeName = propertyTypeNames.PropertyTypeName;              propertyInfo.ValueTypeName = propertyTypeNames.ValueTypeName;              propertyInfo.Required = bindAttribute.IsRequired;              props.Add(propertyInfo);           }        }        generationInfo.PropertiesInfo = props;        generationInfos.Add(generationInfo);     }  }}

Передача информации и запуск T4-генератора:

Посмотреть код
foreach (var gInfo in generationInfos){  var viewModelBindingsTemplateGenerator = new ViewModelBindingsTemplate  {     Session = new Dictionary<string, object> {["_m_GenerationInfoField"] = gInfo}  };  viewModelBindingsTemplateGenerator.Initialize();  var generationData = viewModelBindingsTemplateGenerator.TransformText();  File.WriteAllText(fullOutputPath + gInfo.ClassName + ".cs", generationData);}var viewModelResolverTemplateGenerator = new ViewModelResolverTemplate(){  Session = new Dictionary<string, object> {["_m_GenerationInfosField"] = generationInfos}};viewModelResolverTemplateGenerator.Initialize();var generationResult = viewModelResolverTemplateGenerator.TransformText();File.WriteAllText(fullOutputPath + "BindViewModelResolverGenerated.cs", generationResult);

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

Var DroneInfoViewModel = BindViewModelResolver.Resolve<DroneInfoViewModel>(_droneInfoModel);

Пример сгенеренного резолвера для DroneInfoViewModel:

Посмотреть код
public partial struct BindViewModelResolver{   private static DroneInfoViewModel ResolveDroneInfoViewModel(IViewModel viewModel)       => new DroneInfoViewModel       {           OnTopInfoClick = LookupProperty<IEvent>("DroneInfoViewModel",viewModel, PropertyType.Event, "current-drone-info/on-top-info-click", true),           OnBottomInfoClick = LookupProperty<IEvent>("DroneInfoViewModel",viewModel, PropertyType.Event, "current-drone-info/on-bottom-info-click", true),           DroneName = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/drone-name", true),           DroneTier = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/drone-tier", true),           VoltageCurrent = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/voltage-current", true),           VoltageMax = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/voltage-max", true),           VoltageRange = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/voltage-range", true),           SpawnChargeCost = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/spawn-charge-cost", true),           SpawnHardCost = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/spawn-hard-cost", true),           BuyCurrency = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/buy-currency", true),           BuyPriceValue = LookupProperty<IMutableProperty<int>>("DroneInfoViewModel",viewModel, PropertyType.Integer, "current-drone-info/buy-price-value", true),           SchemeSlotsInfo = LookupProperty<IMutableCollection>("DroneInfoViewModel",viewModel, PropertyType.Collection, "current-drone-info/scheme-slots-info", true),           DroneId = LookupProperty<IMutableProperty<string>>("DroneInfoViewModel",viewModel, PropertyType.String, "current-drone-info/drone-id", true),           InLoadingState = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/in-loading-state", true),           DroneExist = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-exist", true),           DroneNoSlot = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-no-slot", true),           DroneNoDrone = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-no-drone", true),           IsDroneBlueprint = LookupProperty<IMutableProperty<bool>>("DroneInfoViewModel",viewModel, PropertyType.Boolean, "drone-info/drone-blueprint", true),       };}

Напоследок в паре слов о валидаторах.

Чтобы включить валидацию для модели, нужно всего лишь прописать атрибут ExpectReactiveContract:

[ExpectReactiveContract(typeof(DroneInfoViewModel))]private ViewModel _droneInfoModel;

При наличии ошибок в редакторе будет выведено предупреждение вида:



Валидатор работает на основе рефлексии, пробегая по Bind-полям и проверяя их наличие в модели.

Наличие валидации принесло нам ряд преимуществ:

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

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

Перевод Трассировщик лучей с нуля за 100 строчек Python

10.03.2021 14:15:34 | Автор: admin
Рисунок 1Рисунок 1

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

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

Предпосылки

Нам понадобится только самая базовая векторная геометрия:

  • Пусть у нас есть две точки A и B независимо от размерности: 1, 2, 3,, n, тогда вектор, идущий от A к B, может быть найден путем вычисления B A (поэлементно);

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

  • Единичный вектор это вектор длины 1: ||v|| = 1;

  • Для данного вектора другой вектор, идущий в том же направлении, но имеющий длину 1, может быть найден путем деления каждого компонента первого вектора на его длину это называется нормализацией: u = v/||v||;

  • Точечное произведение для векторов вычисляется как: <v, v> = ||v||.

Алгоритм трассировки лучей

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

Чтобы объяснить работу этого алгоритма, сначала нужно настроить сцену:

  • Трехмерное пространство (мы собираемся использовать три координаты для позиционирования объектов в пространстве);

  • Объекты в этом пространстве (поскольку мы собираемся воспроизвести рис. 1, то возьмем в качестве объектов сферы);

  • Источник света (в нашем случае это одна точка, излучающая свет во всех направлениях);

  • Камера для наблюдения за сценой;

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

Рисунок 2Рисунок 2

Экран это некая определенная вами геометрическая фигура (например, прямоугольник 3x2). Но сами по себе цифры 3 и 2 ни о чем нам не говорят и действительно начинают приобретать какое-то значение только при сравнении их с размерами других объектов. Здесь важно то, как вы разделите ваш прямоугольник на более мелкие квадраты (пиксели), как на рисунке выше. Это определит размер конечного изображения. Другими словами, можно взять прямоугольник 3x2 и разделить его на 300x200 пикселей.

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

для каждого пикселя p(x, y, z) экрана:

ассоциировать черный цвет с p

если луч (линия), начинающийся от камеры и идущий к точке p, пересекает любой объект сцены, то:

вычислить точку пересечения с ближайшим объектом

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

рассчитать цвет точки пересечения

сопоставить цвет точки пересечения с p

Рисунок 3Рисунок 3

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

Настройка сцены

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

Рисунок 4Рисунок 4

Допустим, камера расположена в точке (x = 0, y = 0, z = 1), а экран является частью плоскости, образованной осями x и y. Теперь мы можем написать скелет нашего кода.

Посмотреть код
import numpy as npimport matplotlib.pyplot as pltwidth = 300height = 200camera = np.array([0, 0, 1])ratio = float(width) / heightscreen = (-1, 1 / ratio, 1, -1 / ratio) # слева, сверху, справа, снизуimage = np.zeros((height, width, 3))for i, y in enumerate(np.linspace(screen[1], screen[3], height)):  for j, x in enumerate(np.linspace(screen[0], screen[2], width)):    # image[i, j] = ...    print("progress: %d/%d" % (i + 1, height))plt.imsave('image.png', image)
  • Камера это просто точка, имеющая три координаты;

  • С другой стороны, экран определяется четырьмя числами (или двумя точками): слева, сверху, справа, снизу. Он находится в диапазоне от -1 до 1 в направлении x и от -1/ratio до 1/ratio в направлении y, где ratio ширина изображения, деленная на его высоту. Это вытекает из того, что мы хотим, чтобы экран имел такое же соотношение сторон, что и фактическое изображение. При такой настройке экрана будет получено соотношение сторон (ширина к высоте): 2 /(2/ratio) = ratio, которое и является соотношением сторон желаемого изображения 300x200;

  • Цикл состоит из разделения экрана на точки в направлениях x и y, затем вычисляется цвет текущего пикселя;

  • Полученный код создаст как и ожидалось на данном этапе черное изображение.

Пересечение лучей

Следующий шаг алгоритма: если луч (линия), начинающийся от камеры и проходящий к точке p, пересекает объект сцены, тогда...

Разобьем его на две части. И начнем с определения того, какой луч (линия) начинается от камеры и идет к точке p?

Определение луча

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

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

Помните, что камера и пиксель это 3D-точки. При t = 0 вы окажетесь в положении камеры, но с увеличением t будете удаляться от нее в направлении пикселя. Это параметрическое уравнение, которое даст точку вдоль линии для любого t.

Конечно, точно так же мы можем переписать уравнение и определить луч, который начинается в исходной точке (O) и идет к месту назначения (D) как:

Для удобства определим d как вектор направления.

Теперь мы можем добавить к нашему коду вычисление луча:

Посмотреть код
import numpy as npimport matplotlib.pyplot as pltdef normalize(vector):    return vector / np.linalg.norm(vector)width = 300height = 200camera = np.array([0, 0, 1])ratio = float(width) / heightscreen = (-1, 1 / ratio, 1, -1 / ratio) # слева, сверху, справа, снизуimage = np.zeros((height, width, 3))for i, y in enumerate(np.linspace(screen[1], screen[3], height)):    for j, x in enumerate(np.linspace(screen[0], screen[2], width)):        pixel = np.array([x, y, 0])        origin = camera        direction = normalize(pixel - origin)        # image[i, j] = ...    print("progress: %d/%d" % (i + 1, height))plt.imsave('image.png', image)
  • Мы добавили в код функцию normalize(vector), которая возвращает... собственно, нормализованный вектор;

  • Также мы добавили вычисление исходной точки и направления, которые вместе определяют луч. Обратите внимание, что пиксель имеет координату z = 0, поскольку он лежит на экране, который находится в плоскости, образованной осями x и y;

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

Определение сферы

Сфера довольно простой математический объект. Она определяется как набор точек, находящихся на одинаковом расстоянии r (радиус) от заданной точки (центра).

Следовательно, с учетом центра C сферы и ее радиуса r произвольная точка X лежит на сфере тогда, когда:

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

После этого мы сможем определить некоторые сферы сразу после объявления экрана:

objects = [   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7 },   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1 },   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15 }]

Теперь вычислим пересечение луча и сферы.

Пересечение со сферой

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

Это обычное квадратное уравнение, которое просто решается относительно t. Мы будем вызывать коэффициенты, связанные с t, t, t, a, b и c, соответственно. Вычислим дискриминант этого уравнения:

Поскольку направление d является единичным вектором, получим a = 1. После вычисления дискриминанта у нас есть три варианта:

Рисунок 5Рисунок 5

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

Посмотреть код
def sphere_intersect(center, radius, ray_origin, ray_direction):   b = 2 * np.dot(ray_direction, ray_origin  center)   c = np.linalg.norm(ray_origin  center) ** 2  radius ** 2   delta = b ** 2  4 * c   if delta > 0:       t1 = (-b + np.sqrt(delta)) / 2       t2 = (-b  np.sqrt(delta)) / 2       if t1 > 0 and t2 > 0:           return min(t1, t2)   return None

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

Ближайший пересекаемый объект

Пока мы все еще не выполнили инструкцию из псевдокода: если луч (линия), начинающийся от камеры и идущий к точке p, пересекает любой объект сцены, то [...]. Теперь нам нужно вычислить точку пересечения с ближайшим объектом.

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

Посмотреть код
def nearest_intersected_object(objects, ray_origin, ray_direction):   distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) for obj in objects]   nearest_object = None   min_distance = np.inf   for index, distance in enumerate(distances):       if distance and distance < min_distance:           min_distance = distance           nearest_object = objects[index]   return nearest_object, min_distance

При вызове функции, если nearest_object = None, луч не пересекает никакого объекта, иначе его значением является ближайший пересекаемый объект, и мы получаем min_distance расстояние от начала луча до точки пересечения.

Точка пересечения

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

nearest_object, distance = nearest_intersected_object(objects, o, d)if nearest_object:intersection_point = o + d * distance

В результате получаем следующий код:

Посмотреть код
import numpy as npimport matplotlib.pyplot as pltdef normalize(vector):   return vector / np.linalg.norm(vector)def sphere_intersect(center, radius, ray_origin, ray_direction):   b = 2 * np.dot(ray_direction, ray_origin  center)   c = np.linalg.norm(ray_origin  center) ** 2  radius ** 2   delta = b ** 2  4 * c   if delta > 0:       t1 = (-b + np.sqrt(delta)) / 2       t2 = (-b  np.sqrt(delta)) / 2       if t1 > 0 and t2 > 0:           return min(t1, t2)   return Nonedef nearest_intersected_object(objects, ray_origin, ray_direction):   distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) for obj in objects]   nearest_object = None   min_distance = np.inf   for index, distance in enumerate(distances):       if distance and distance < min_distance:           min_distance = distance           nearest_object = objects[index]   return nearest_object, min_distancewidth = 300height = 200camera = np.array([0, 0, 1])ratio = float(width) / heightscreen = (-1, 1 / ratio, 1, -1 / ratio) # слева, сверху, справа, снизуobjects = [   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7 },   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1 },   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15 }]image = np.zeros((height, width, 3))for i, y in enumerate(np.linspace(screen[1], screen[3], height)):   for j, x in enumerate(np.linspace(screen[0], screen[2], width)):       pixel = np.array([x, y, 0])       origin = camera       direction = normalize(pixel  origin)       # проверка пересечений       nearest_object, min_distance = nearest_intersected_object(objects, origin, direction)       if nearest_object is None:           continue       # вычисления пересечений между лучом и ближайшим объектом       intersection = origin + min_distance * direction       # image[i, j] = ...       print("%d/%d" % (i + 1, height))plt.imsave('image.png', image)

Пересечения света

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

У нас уже есть функция, которая нам может помочь: near_intersected_object(). И мы хотим знать, пересекает ли луч, который начинается в точке пересечения и идет к свету, объект сцены перед тем, как пересечь свет. Это практически та же задача, что мы решали раньше: нам просто нужно изменить начало и направление луча. Во-первых, нам нужно определить свет. Можно сделать это сразу вместе с объявлением объектов:

light = { 'position': np.array([5, 5, 5]) }

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

# ...intersection = origin + min_distance * directionintersection_to_light = normalize(light['position']  intersection)_, min_distance = nearest_intersected_object(objects, intersection, intersection_to_light)intersection_to_light_distance = np.linalg.norm(light['position']  intersection)is_shadowed = min_distance < intersection_to_light_distance

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

Рисунок 6Рисунок 6

Этот трюк используется не только для сфер, но и для любых объектов.

Следовательно, правильный код будет таким:

Посмотреть код
# ...intersection = origin + min_distance * directionnormal_to_surface = normalize(intersection  nearest_object['center'])shifted_point = intersection + 1e-5 * normal_to_surfaceintersection_to_light = normalize(light['position']  shifted_point)_, min_distance = nearest_intersected_object(objects, shifted_point, intersection_to_light)intersection_to_light_distance = np.linalg.norm(light['position']  intersection)is_shadowed = min_distance < intersection_to_light_distanceif is_shadowed:   continue

Модель отражения Блинна-Фонга

Итак, мы знаем, что луч света попал на объект, а отражение луча прямо в камеру. Вопрос: что увидит камера? На него и пытается ответить модель Блинна-Фонга.

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

Согласно этой модели, любой материал имеет четыре свойства:

  • Фоновый цвет (Ambient color): цвет, который имеет объект в отсутствие света;

  • Рассеянный цвет (Diffuse color): цвет, наиболее близкий к тому, что мы себе представляем;

  • Зеркальный цвет (Specular color): цвет блестящей части объекта, когда свет попадает на нее. В большинстве случаев это белый цвет;

  • Блеск (Shininess): коэффициент, показывающий, насколько блестящим является объект.

Примечание: Все цвета представлены в RGB в диапазоне 0 1.

Рисунок 7Рисунок 7

Добавим эти свойства к сферам:

objects = [   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7, 'ambient': np.array([0.1, 0, 0]), 'diffuse': np.array([0.7, 0, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100 },   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1, 'ambient': np.array([0.1, 0, 0.1]), 'diffuse': np.array([0.7, 0, 0.7]), 'specular': np.array([1, 1, 1]), 'shininess': 100 },   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15, 'ambient': np.array([0, 0.1, 0]), 'diffuse': np.array([0, 0.6, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100 }]

В нашем примере сферы имеют цвета красный, пурпурный и зеленый, соответственно.

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

light = { 'position': np.array([5, 5, 5]), 'ambient': np.array([1, 1, 1]), 'diffuse': np.array([1, 1, 1]), 'specular': np.array([1, 1, 1]) }

Учитывая эти свойства, модель Блинна-Фонга рассчитывает освещенность точки следующим образом:

где

  • ka, kd, ks фоновое, рассеянное, зеркальное свойства объекта;

  • ia, id, is фоновое, рассеянное, зеркальное свойства света;

  • L единичный вектор направления от точки пересечения к свету;

  • N единичный вектор нормали к поверхности объекта в точке пересечения;

  • V единичный вектор направления от точки пересечения к камере;

  • блеск объекта.

Посмотреть код
# ...if is_shadowed:   break# RGBillumination = np.zeros((3))# ambiantillumination += nearest_object['ambient'] * light['ambient']# diffuseillumination += nearest_object['diffuse'] * light['diffuse'] * np.dot(intersection_to_light, normal_to_surface)# specularintersection_to_camera = normalize(camera  intersection)H = normalize(intersection_to_light + intersection_to_camera)illumination += nearest_object['specular'] * light['specular'] * np.dot(normal_to_surface, H) ** (nearest_object['shininess'] / 4)image[i, j] = np.clip(illumination, 0, 1)

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

Запускаем код

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

Рисунок 8Рисунок 8

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

  • Серый пол отсутствует;

  • Отсутствие отражений.

Фейковая плоскость

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

Добавим эту сферу в список объектов и снова выполним рендеринг:

{ 'center': np.array([0, -9000, 0]), 'radius': 9000  0.7, 'ambient': np.array([0.1, 0.1, 0.1]), 'diffuse': np.array([0.6, 0.6, 0.6]), 'specular': np.array([1, 1, 1]), 'shininess': 100 }

Отражение

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

Каждый объект имеет коэффициент отражения в диапазоне от 0 до 1. Здесь 0 означает, что объект матовый, 1 что объект зеркальный. Добавим свойство отражения ко всем сферам:

{ 'center': np.array([-0.2, 0, -1]), ..., 'reflection': 0.5 }{ 'center': np.array([0.1, -0.3, 0]), ..., 'reflection': 0.5 }{ 'center': np.array([-0.3, 0, 0]), ..., 'reflection': 0.5 }{ 'center': np.array([0, -9000, 0]), ..., 'reflection': 0.5 }

Алгоритм

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

Рисунок 9Рисунок 9

Расчет цвета

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

где

  • c конечный цвет пикселя;

  • i освещенность, рассчитанная по модели Блинна-Фонга для точки пересечения;

  • r отражение от пересекаемого объекта.

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

Отраженный луч

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

где

  • R нормализованный отраженный луч;

  • L единичный вектор направления отражаемого луча;

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

Рисунок 10Рисунок 10

Добавим этот метод в начало кода вместе с функцией normalize():

def reflected(vector, axis):   return vector  2 * np.dot(vector, axis) * axis

Код

Посмотреть код
# глобальные переменныеmax_depth = 3# нужные данные для циклаcolor = np.zeros((3))reflection = 1for k in range(max_depth):   nearest_object, min_distance = # ...   # ...   illumination += # ...   # отражение   color += reflection * illumination   reflection *= nearest_object['reflection']   # начальная точка и направление нового луча   origin = shifted_point   direction = reflected(direction, normal_to_surface)image[i, j] = np.clip(color, 0, 1)

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

Вот и все. Запускаем код и наблюдаем результат:

Рисунок 11Рисунок 11

Окончательный код

Итоговый код состоит из всего порядка сотни строк:

Посмотреть код
import numpy as npimport matplotlib.pyplot as pltdef normalize(vector):    return vector / np.linalg.norm(vector)def reflected(vector, axis):    return vector - 2 * np.dot(vector, axis) * axisdef sphere_intersect(center, radius, ray_origin, ray_direction):    b = 2 * np.dot(ray_direction, ray_origin - center)    c = np.linalg.norm(ray_origin - center) ** 2 - radius ** 2    delta = b ** 2 - 4 * c    if delta > 0:        t1 = (-b + np.sqrt(delta)) / 2        t2 = (-b - np.sqrt(delta)) / 2        if t1 > 0 and t2 > 0:            return min(t1, t2)    return Nonedef nearest_intersected_object(objects, ray_origin, ray_direction):    distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) for obj in objects]    nearest_object = None    min_distance = np.inf    for index, distance in enumerate(distances):        if distance and distance < min_distance:            min_distance = distance            nearest_object = objects[index]    return nearest_object, min_distancewidth = 300height = 200max_depth = 3camera = np.array([0, 0, 1])ratio = float(width) / heightscreen = (-1, 1 / ratio, 1, -1 / ratio) # left, top, right, bottomlight = { 'position': np.array([5, 5, 5]), 'ambient': np.array([1, 1, 1]), 'diffuse': np.array([1, 1, 1]), 'specular': np.array([1, 1, 1]) }objects = [    { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7, 'ambient': np.array([0.1, 0, 0]), 'diffuse': np.array([0.7, 0, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 },    { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1, 'ambient': np.array([0.1, 0, 0.1]), 'diffuse': np.array([0.7, 0, 0.7]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 },    { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15, 'ambient': np.array([0, 0.1, 0]), 'diffuse': np.array([0, 0.6, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 },    { 'center': np.array([0, -9000, 0]), 'radius': 9000 - 0.7, 'ambient': np.array([0.1, 0.1, 0.1]), 'diffuse': np.array([0.6, 0.6, 0.6]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 }]image = np.zeros((height, width, 3))for i, y in enumerate(np.linspace(screen[1], screen[3], height)):    for j, x in enumerate(np.linspace(screen[0], screen[2], width)):        # экран в начальной точке        pixel = np.array([x, y, 0])        origin = camera        direction = normalize(pixel - origin)        color = np.zeros((3))        reflection = 1        for k in range(max_depth):            # проверка пересечений            nearest_object, min_distance = nearest_intersected_object(objects, origin, direction)            if nearest_object is None:                break            intersection = origin + min_distance * direction            normal_to_surface = normalize(intersection - nearest_object['center'])            shifted_point = intersection + 1e-5 * normal_to_surface            intersection_to_light = normalize(light['position'] - shifted_point)            _, min_distance = nearest_intersected_object(objects, shifted_point, intersection_to_light)            intersection_to_light_distance = np.linalg.norm(light['position'] - intersection)            is_shadowed = min_distance < intersection_to_light_distance            if is_shadowed:                break            illumination = np.zeros((3))            # ambiant            illumination += nearest_object['ambient'] * light['ambient']            # diffuse            illumination += nearest_object['diffuse'] * light['diffuse'] * np.dot(intersection_to_light, normal_to_surface)            # specular            intersection_to_camera = normalize(camera - intersection)            H = normalize(intersection_to_light + intersection_to_camera)            illumination += nearest_object['specular'] * light['specular'] * np.dot(normal_to_surface, H) ** (nearest_object['shininess'] / 4)            # reflection            color += reflection * illumination            reflection *= nearest_object['reflection']            origin = shifted_point            direction = reflected(direction, normal_to_surface)        image[i, j] = np.clip(color, 0, 1)    print("%d/%d" % (i + 1, height))plt.imsave('image.png', image)

Что дальше ?

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

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

  • То же самое и со светом. Добавить сюда POO и сделать так, чтобы можно было добавить несколько источников света в сцену;

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

  • Найти способ правильно расположить экран при любом положении и направлении камеры;

  • Смоделировать свет по-другому. В настоящее время это одна точка, поэтому тени от объектов жесткие или четко очерченные. Чтобы получить мягкие тени , нужно смоделировать источник света как 2D- или 3D-объект: диск или сферу.

Бонус

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

Код написан на Kotlin (можно оценить, насколько медленный по сравнению с ним Python) и доступен на GitHub.

Подробнее..

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • уклонению;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Подробнее..

Перевод Проблемы рейтрейсинга в играх нового поколения анализ трассировки лучей в ремастере Marvels Spider-Man

14.10.2020 12:12:47 | Автор: admin


По мере приближения запуска нового поколения консолей Insomniac Games начала публиковать больше материалов, раскрывающих подробности о Marvels Spider-Man и Marvel's Spider-Man: Miles Morales. Оба проекта задействуют технологию трассировки лучей. На взгляд Алекса Баттальи из Digital Foundry, выглядит она очень достойно, если не сказать, что превосходно. Для старта весьма недурно.

Тем не менее, читая комментарии в Интернете, он столкнулся с немалым количеством критики по отношению к реализации технологии, а также к частоте и разрешению кадров. Поэтому в новое видео Digital Foundry он решил представить своего рода пособие по трассировке лучей на консолях следующего поколения и объяснить, почему в Marvel's Spider-Man она выглядит именно так.

Итак, обо всем по порядку. Давайте обсудим, для чего именно нужна трассировка лучей в Marvels Spider-Man на PS5.

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



Или на этом скриншоте, где герой и город вдалеке отражаются в окне здания.



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



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



Отражения показываются в разрешении 1080p, тогда как остальная часть изображения в 4K. Комментаторы жалуются, например, на то, что листья в отражении более редкие либо вовсе отсутствуют по сравнению с теми, которые можно увидеть на реальном дереве. Или что некоторые внутриигровые объекты, такие как автомобили или пешеходы, могут не показываться в отражениях. Или на отсутствие теней. Или на пренебрежение мелкими деталями, например, на костюме Человека-паука.



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

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



Разделим процесс работы с трассировкой лучей на четыре шага. Допустим, нам нужно уложиться в 8 мс для создания отражений для игры на 30 FPS при разрешении 4K.

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

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



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

На следующем этапе происходит затенение попадания или выбор цвета пикселя.



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



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



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



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



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



Посмотрим на скриншот из демо The Ghost Runner с включенной трассировкой лучей с разрешением 4K на видеокарте RTX2060 Super. Здесь мы видим некоторые отличия по сравнению с Человеком-пауком. Unreal Engine 4, на котором создана игра, использует метод трассировки лучей чуть более дорогой по времени, но физически более точный, чем в Человеке-пауке.



RTX 2060 Super работает аналогично RTX 2070, имеет такую же пропускную способность шины памяти, как и у PS5, и такой же ее объем 8 ГБ. RTX 2060 Super, скорее всего, станет отличной отправной точкой для сравнения с графическим процессором PlayStation 5.



Когда сцена вместе с отражениями рендерится в 4K, а количество самих отражений на экране столь невелико, как в случае со статичной лужей выше, графический процессор едва укладывается в 30 FPS. Если бы в сцене было больше динамики, частота кадров определенно была бы меньше. Уменьшение осевого разрешения трассировки лучей на 50% и, таким образом, общего разрешения до 1080p увеличивает производительность примерно на 27%, оставляя свободные ресурсы в графическом процессоре для повышения частоты кадров до 30 FPS.



В других сценах можно увидеть более впечатляющие результаты. Как, например, здесь. Отражающая стеклянная поверхность занимает почти весь экран. В этом случае отражения с исходным разрешением 4К приводят к частоте кадров 21 FPS. При этом понижение разрешения отражений до 1080p увеличивает частоту кадров на 58% до 33 FPS при необходимых 30 FPS.

Таким образом, на графическом процессоре с такой же производительностью, что и у PS5, необходимо использовать отражения с трассировкой лучей с более низким разрешением для возможности поддерживать постоянной частоту кадров 30 FPS при общем разрешении 4К. Тогда становится понятным, почему для снижения стоимости трассировки лучей в Человеке-пауке будут использоваться отражения только при 1080p.



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

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



По опыту других разработчиков мы знаем, что прорисовка множества персонажей на экране может оказаться очень ресурсоемкой. Например, отрисовка некоторых сцен в Metro Exodus с большим количеством персонажей на RTX 2080 Ti занимает более 4 мс. В нашем случае это окажется половиной отведенного бюджета непозволительная для наш роскошь.

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



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

Например, взглянем на эти скриншоты Battlefield V.




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



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



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



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

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

image

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



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



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

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



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



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



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

Перевод Воксели против теневых карт выбор новой системы освещения для Roblox

20.11.2020 14:23:38 | Автор: admin


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

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

Примечание: Скриншоты в статье расположены так, чтобы слева всегда были показаны воксели, справа теневые карты.


Реализация: воксели


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

Данные о мире преобразуются в набор сеток вокселей: каждая сетка центрируется вокруг персонажа и может иметь размеры вокселей от 1 до 16 (всего 5 решеток). Каждый воксель содержит информацию о наполненности (occupancy) в диапазоне от 0 до 100%. Затем вычисляются данные освещения для каждого вокселя в каждой сетке на основе этой наполненности и информации об источниках/направлении света. Все вышеперечисленное происходит на графическом процессоре, так как центральный недостаточно быстр для обновления такого количества вокселей со столь высокой плотностью.

Система хранит все данные в вокселях, в частности для каждого имеющегося вокселя есть данные о:

  • Наполненности (несколько значений, описывающих, насколько заполнен каждый воксель);
  • Skylight (какая часть неба видна из вокселя);
  • Тени от солнца (какая часть солнца закрыта вокселем);
  • Цвета светового объекта/конуса (приближение цвета/конуса воздействия локальных источников света на воксель).

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


Реализация: теневые карты



image

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

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

Система хранит данные в двух структурах:

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

Цвет каждого пикселя вычисляется динамически и не хранится в явном виде. Части теневого атласа можно обновлять покадрово по мере перемещения источников света/объектов.


Производительность: воксели


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

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

Производительность вокселей вычисляется как O (G) + O (L) + O (P), где G количество треугольников (геометрическая сложность), L количество источников света, P количество пикселей.

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


Производительность: теневые карты


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

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

Кроме того, этот метод не позволяет отделить параметр разрешения от количества света: для каждого пикселя мы должны пересчитывать влияние всех источников света, которые его покрывают. Этот шаг также нельзя кэшировать, что приводит к проблемам с производительностью при высоких разрешениях в сильно освещенных сценах: 20 перекрывающихся источников света в комнате с разрешением 4K могут потребовать 160 миллионов оценок освещенности.

Производительность карт теней рассчитывается как O (GL) + O (LP), где G количество треугольников (сложность геометрии), L количество источников света, P количество пикселей.


Производительность: оценка


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


Париж (тени от солнца, очень мало источников света, не отбрасывающих тени)




  • Воксели: обновление теней 6 мс, рендеринг сцены 1,5 мс;
  • Теневые карты: обновление теней 1 мс, рендеринг сцены 2,4 мс;
  • Стоимость вычисления базовой воксельной тени выше, поскольку графическому процессору тяжелее ее обрабатывать.


Пещеры (много отбрасывающих тени источников)




  • Воксели: обновление теней 7 мс, рендеринг сцены 0,9 мс;
  • Теневые карты: обновление теней 10 мс, рендеринг сцены 2,1 мс;
  • Из-за большого количества геометрии и движущихся источников света обновление теневых карт обходится дорого.


Вестерн (много отбрасывающих тени источников)




  • Воксели: обновление теней 8 мс, рендеринг сцены 1 мс;
  • Теневые карты: обновление теней 15 мс, рендеринг сцены 2,5 мс;
  • С движущимися источниками света и большим количеством треугольников обновление карты теней оказывается дорогим.


1000 источников света без тени




  • Воксели: обновление света 20 мс, рендеринг сцены 0,5 мс;
  • Теневые карты: обновление света 0,5 мс, рендеринг сцены 5 мс;
  • Совокупный объем перекрытия света и вокселей в этом случае замедляет обновление вокселей. Кроме того, можно увидеть, что в ближнем каскаде приближение для одного источника света в каждом вокселе не выполняется.


Производительность: заключение


Теневые карты хорошо масштабируются для рабочей нагрузки, однако стоит учитывать два аспекта:
  • Стоимость каждого пикселя растет по мере увеличения разрешения, что делает это решение практичным только при среднем разрешении (1080p); выход за рамки 1080p требует наличия очень хорошего графического процессора.
  • Стоимость просчета тени растет очень быстро в случае сложной геометрии множества динамических источников света. Это может быть компенсировано лучшей отбраковкой, но на данном этапе пока остается фундаментальной проблемой.


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


Требования к памяти


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

В случае вокселей в памяти хранятся несколько текстур для каждого каскада, поэтому их совокупный размер зависит от количества каскадов и размера каждого из них. В настоящее время используется 4 каскада (с размерами вокселей 1..8) по 128x64x128 вокселей в каждом, что добавляет до 128 МБ используемой VRAM. Можно было бы завести еще 2 каскада (0,5 вокселя и 16 вокселей) или перенастроить имеющиеся, что увеличило бы это значение до 192 МБ. Можно, наоборот, уменьшить количество каскадов (удалив некоторые близкие каскады) в системах с ограниченным объемом памяти, и тогда минимальное воздействие на память может составить около 64 МБ с двумя каскадами (4..8) и около 96 МБ с тремя (4..16).

В случае теневых карт используются атлас теневых карт и фроксельная сетка. Последняя отчасти зависит от разрешения. Размер теневого атласа, в свою очередь, можно уменьшить, если нужно уменьшить качество теней для улучшения производительности/памяти. Текущая система использует 73 МБ видеопамяти, большую часть из которых (64 МБ) занимает теневой атлас. Можно уменьшить его и таким образом ограничить количество затемненных источников света или качество теней. Также можно рассмотреть некоторые варианты теневых карт, для которых требуется больше памяти для поддержки полупрозрачности, а значит, они займут больше места (до 130 МБ или более). Минимальное воздействие на память системы, вероятно, будет достигнуто, если уменьшить размер теневого атласа и воспользоваться его более простым вариантом, который будет занимать около 25 МБ.

Для сравнения: текущая система освещения имеет два режима: для высокого (ПК) и низкого качества (мобильные девайсы). Вариант для ПК занимает ~ 40 МБ (24 МБ RAM, 16 МБ VRAM); мобильный ~ 11 МБ (6 МБ RAM, 5 МБ VRAM).

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


Мобильная совместимость


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

Уже существующая воксельная система освещения отлично подходит для мобильных устройств: она поддерживает множество сложных функций освещения (light shadows, skylight и т. д.) и выполняет большинство сложных вычислений на ЦП, таким образом обеспечивая минимальные требования к производительности графического процессора и набору фич. Поскольку в обозримом будущем все равно придется поддерживать эту систему для недорогих мобильных устройств и ПК, возникло несколько вариантов поддержки большого пула устройств:

  • Сохранить существующую систему в мобильном виде, новая же будет только ПК/консольной. Это означает, что большой сегмент пользовательской базы не будет иметь доступа к новой системе.
  • Небольшое улучшение существующей системы для передовых мобильных устройств (слегка уменьшенный размер вокселей, улучшенное представление вокселей), использование новой системы для ПК/консолей.
  • Сохранить существующую систему на low-end девайсах, найти способ уменьшить масштаб новой системы, чтобы она могла работать на современных мобильных устройствах.

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


Качество: источники света


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

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



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

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



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


Качество: тени


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



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

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



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


Качество: skylight


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




Качество: геометрическая точность


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

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




Качество: light leaks


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



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

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

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


Качество: заключение


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

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


Видимость: полупрозрачность


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



Ниже представлено видео этого эффекта в движении:


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


Видимость: растительность


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




Видимость: self-illumination


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



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


Видимость: глобальное освещение


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

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

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

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


Резюме


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



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

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

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

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. Многим игрокам может показаться странным, что деньги здесь не являются основным ресурсом для победы. Действительно: в игре очень важно держать ваши финансы в хорошем состоянии, однако все высокоуровневые здания и сооружения открываются игроку только тогда, когда он набирает большое число здоровых, образованных, счастливых и финансово стабильных граждан. В некотором смысле это связано со следующей мыслью: настоящая мощь исходит от людей, а истинное мастерство от связи с ними. Будь то строительство мегаполиса или создание лучших игр.

Подробнее..

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

14.01.2021 14:20:31 | Автор: admin
image

MoonGun игра в жанре экшен/стратегия о защите лунной базы от астероидов. Игра была создана Ивенсом Серпой автором множества статей о геймдизайне в рамках Github Game Off Game Jam в ноябре 2020 года. Занимался он ей в одиночку в свободное от работы время. Игра получила немало положительных отзывов, так что ее создатель решил поделиться опытом ее разработки: описать весь процесс создания и поделиться методами, которые он использовал, включая этапы концепции, планирования, непосредственно разработки и релиза.

Прежде чем перейти к теме статьи, стоит упомянуть несколько важных замечаний: этот материал больше касается дизайна игры, чем технической стороны ее разработки. Проект был разработан при помощи Unity 3D (версия 2020.1), с которой автор уже довольно хорошо знаком и в которой уже писал другие игры. Несмотря на то, что над проектом он работал в одиночку, ему все равно помогали друзья, тестируя игру и делясь своими мыслями на этот счет. Большая часть ассетов либо была взята в Интернете (например, с сайта Kenney.nl), либо была сделана моделером Калео Мендесом.


Игровые джемы, тема и идеи


Игровой джем соревнование по разработке игр, в котором участники пытаются разработать игру с нуля за некий короткий промежуток времени обычно от 24 до 72 часов. В ноябре GitHub проводил Itch.IO Game Off Game Jam, который продлился весь ноябрь. У таких джемов обычно есть основная тема, которая направляет творческий процесс разрабатываемых игр. В 2020 году эта тема касалась Луны.

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

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

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

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

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

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

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

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

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

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

Ниже можно увидеть изображение первого рабочего прототипа:

image
MoonGun Version 0.1 не слишком красивый, но функциональный прототип


Прототип и гипотезы


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

Краткая версия этих гипотез звучала так:

  1. Стрелять в падающие астероиды интересно или увлекательно?
  2. Можно ли имитировать лунную поверхность с помощью большой 3D-сферы?
  3. Следует ли сделать управление пушкой с помощью клавиатуры или мыши?

Также нужно было проверить несколько технических гипотез, в основном связанных с выпуском игры в виде сборки WebGL с использованием Unity. В связи с пандемией имело смысл разместить игру прямо на Itch.io, чтобы облегчить тестирование и распространение ее версий. Беспокоило то, сколько времени потребуется, чтобы пройти весь цикл сборки-деплоя-тестирования на платформе Itch.io (в часах или минутах), а затем какого качества графики можно достичь, чтобы при запуске игры в браузере сохранялась приемлемая частота кадров.

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

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

image
Концепт-арт для игры на основе скриншотов прототипа

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

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

Скриншот из этой версии (V0.4 Лунные сооружения) можно увидеть ниже:

image


Тесты, стабильная версия и геймплей


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

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

image

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

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

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

image

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


Не (всегда) нужны сложные или дополнительные механики


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

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

image
Вариант основного квеста (Минеральный лазер) на объекте Лаборатория

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

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


Залп! Баланс и честность игры


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

image
MoonGun, версия 0.7 лазер

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

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

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

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


Лунные квесты


Фактически лазер был добавлен в версию 0.5. Тем не менее, его более стабильная реализация была представлена только в версии 0.7 под названием Лунные квесты, которая также включала:

  1. Дрожание камеры;
  2. Эффекты разрушения астероидов;
  3. Смену оружия (нажатием клавиши SHIFT);
  4. Изменение элементов управления для использования мыши и/или клавиатуры;
  5. Подзапросы на дополнительные ресурсы;
  6. Разнообразие астероидов (с разными скоростями, размерами и наградами за уничтожение);
  7. Улучшение средней частоты кадров.

image

Обратите внимание, что два пункта связаны с улучшением внешнего вида игры (1 и 2), два с добавлением/изменением элементов управления (3 и 4), еще два с дополнительными элементами игры (5 и 6). Один же предназначался для анализа и отладки. Это разделение было полностью преднамеренным.

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

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

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


Встречайте Титана и Луну S/2009 S1


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

Луна S/2009 S1 полна богатым минералом флеботием, и вы, игрок, отвечаете за защиту объектов станции во время его добычи. База покрыта полем, который может защитить ее от слабых астероидов. Однако по мере того, как продвигается добыча, ядро Луны становится все тяжелее, привлекая к себе более быстрые и опасные астероиды, которые могут повредить щит и прервать работу на объектах.

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

На изображении ниже показана первая версия введения с Титаном, новым UI-интерфейсом и на заднем плане щитом, покрывающим всю Луну.

image

Версия V0.8, Лунное меню, была выпущена 26 ноября всего за несколько дней до дедлайна. Она включала в себя существенную переработку UI, а также информацию о щите.

image

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

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


Финальная версия и ее проблемы


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

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

image
MoonGun, версия 1.0a


Отзывы и (будущие) улучшения


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

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

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

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

image


Процесс в ретроспективе


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

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

  1. Концепция и прототип главной механики;
  2. Улучшение опыта и создание стабильной игры;
  3. Тщательное тестирование и внедрение обратной связи;
  4. Развитие механики, повествования, экономики и эстетики;
  5. Полировка.

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

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

Также не стоит пренебрегать действенными инструментами из Unity и сторами ассетов, чтобы ускорить процесс разработки. В данном случае самыми полезными оказались DOTween, LeanPool и Cinemachine.


Постобработка (магия, которую вы заслужили)


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

image
Игра без постобработки (на оригинальной PlayStation) и с постобработкой (на ПК)

В частности, использовались следующие эффекты:

  1. Lift Gamma Gain: улучшение освещения;
  2. Искажение объектива (Lens Distortion): изображение, как если бы оно проходило через реальную камеру;
  3. Хроматическая аберрация: для создания атмосферы космического искажения;
  4. Зернистость пленки: добавление шума и динамики в сцену;
  5. Глубина резкости: размытие объектов вне фокуса для добавления реализма изображению;
  6. Виньетка: затемнение краев, чтобы игрок сосредоточился на центре изображения;
  7. Блум: чтобы свет выглядел волшебно!

Здесь вы можете увидеть, как эти эффекты накладываются друг на друга:

image


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


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

Более того, опыт работы с Unity 3D поможет быстрее перейти от прототипа к рабочему продукту за довольно короткое время. Во время разработки MoonGun в ход шли различные методы быстрого прототипирования для Unity 3D с использованием таких инструментов, как Unity Events, Scriptable Objects, и LINQ statements. Большинство материалов было сделано с помощью Unity Shader Graph и Prefab Variants.

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

Перевод NVIDIA Ampere против AMD RDNA 2 битва архитектур

05.03.2021 14:13:31 | Автор: admin

Спустя два года после запуска Turing в сентябре 2020 года NVIDIA сменила архитектуру своих видеокарт на Ampere. AMD не осталась в стороне и вскоре после этого тоже обновила архитектуру RDNA до второй версии.

Всем хотелось, чтобы новые видеокарты RX 6000 могли составить конкуренцию лучшим продуктам NVIDIA. И теперь, когда обе серии вышли в продажу, геймеры оказались избалованы выбором (по крайней мере, теоретически), куда вложить свои деньги.

Но что насчет чипов, лежащих в их основе? Какой из них лучше?

Размеры кристаллов

На протяжении долгих лет графические процессоры были больше центральных и продолжают неуклонно расти. Площадь последнего ГП AMD составляет примерно 520 мм2, что более чем в два раза больше их предыдущего чипа Navi. При этом он все еще не самый большой: эта честь принадлежит графическому процессору в новом ускорителе Instinct MI100 с площадью порядка 750 мм2.

В последний раз AMD производила игровой процессор размером примерно с Navi 21 для карт Radeon R9 Fury и Nano, которые имели архитектуру GCN 3.0 в чипе Fiji. Его площадь составляла 596 мм2.

С 2018 года AMD использует в работе 7-нм процесс от TSMC, и самым большим чипом из этой производственной линейки был Vega 20 (из Radeon VII) с площадью 331 мм2. Все графические процессоры Navi созданы на основе слегка обновленной версии этого процесса, называемой N7P.

И все же, что касается размеров кристалла, корона остается за NVIDIA, но не то чтобы это было хорошо. Последний чип на базе Ampere, GA102, имеет площадь 628 мм2. Это примерно на 17% меньше, чем у его предка, TU102: он имел ошеломляющую площадь кристалла в 754 мм2. Но все это ничто по сравнению с монструозным чипом NVIDIA GA100: используемый в ИИ и обработке данных, этот 7-нм графический процессор имеет площадь 826 мм2. Он наглядно показывает, каких размеров может достичь графический процессор.

По сравнению с ГП NVIDIA Navi 21 выглядит довольно стройно, хотя стоит помнить, что процессор это не только кристалл. GA102 содержит около 28,3 миллиарда транзисторов, тогда как новый чип AMD на 5% меньше 26,8 миллиарда.

Мы не знаем, из скольких слоев состоит каждый из этих ГП, поэтому все, что мы можем сравнить, это отношение транзисторов к площади кристалла, обычно называемое плотностью кристалла. Navi 21 имеет примерно 51,5 млн транзисторов на квадратный мм, в GA102 она заметно ниже 41,1 млн.

Navi 21 производится у TSMC в соответствии с процессом N7P, который дает небольшое увеличение производительности по сравнению с N7. Свои новые чипы GA102 NVIDIA предпочла производить у Samsung. В них используется модифицированная специально для NVIDIA версия так называемого 8-нм узла (обозначаемого как 8N или 8NN). Значения узлов 7 и 8 имеют мало общего с фактическим размером компонентов: это просто маркетинговые термины, используемые для различения производственных технологий.

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

Внутри кристаллов

Общая структура Ampere GA102 и RDNA 2 Navi 21

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

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

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

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

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

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

Например, Pascal GP106 (используемый в GeForce GTX 1060) был буквально вдвое меньше GP104 (из GeForce GTX 1070). В более ранней версии размер кристалла был больше, а кэш-память и контроллер располагались посередине. У младшего брата они переместились в сторону.

Pascal GP104 и GP106Pascal GP104 и GP106

Для всех предыдущих топовых ГП NVIDIA использовала классическую централизованную компоновку. Зачем же было менять подход? Интерфейсы здесь ни при чем, ведь контроллеры памяти и PCI Express работают на краю кристалла. С тепловыми проблемами это тоже не связано, ведь даже если кэш-часть или контроллер кристалла будут нагреваться сильнее, чем логические секции, вам наверняка захочется, чтобы посередине схемы было больше теплопоглощающего кремния. Хотя причина этого изменения не вполне понятна, есть подозрение, что она связана с реализацией блоков вывода рендеринга (ROP).

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

Прежде чем мы продолжим, стоит отметить инженерные изменения, реализованные AMD в компоновке Navi 21 по сравнению с Navi 10, установленном в Radeon RX 5700 XT. Несмотря на то, что новый чип в два раза больше предыдущего как по площади, так и по количеству транзисторов, разработчикам удалось улучшить тактовые частоты без значительного увеличения энергопотребления. Например, Radeon RX 6800 XT имеет базовую частоту и частоту разгона 1825 и 2250 МГц, соответственно, при TDP, равном 300 Вт. Те же показатели для Radeon RX 5700 XT: 1605 МГц, 1905 МГц и 225 Вт.

Исследование производительности на ватт карт Ampere и RDNA 2 показало, что оба производителя добились значительных улучшений в этой области, но AMD и TSMC достигли чего-то весьма примечательного сравните разницу между Radeon RX 6800 и Radeon VII на графике выше.

Radeon VII первая коллаборация AMD и TSMC с использованием 7-нм технологии, и менее чем за два года они увеличили производительность на ватт на 64%. Отсюда возникает вопрос: насколько лучше мог бы быть Ampere GA102, если бы NVIDIA осталась с TSMC.

Управление ГП

Как все устроено внутри чипов

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

Этот начальный этап управления инструкциями обрабатывается набором модулей в микросхеме. В RDNA 2 графические и вычислительные шейдеры маршрутизируются через отдельные конвейеры, которые планируют и отправляют инструкции остальной части микросхемы: первый называется Graphics Command Processor, второй асинхронными вычислительными блоками (ACE).

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

AMD называет это Shader Engine (SE), тогда как в NVIDIA они имеют название графических кластеров (GPC): названия разные, но суть одна.

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

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

  • Блок Primitive Setup подготавливает вершины к обработке, а также генерирует больше вершин (тесселяция) и отбраковывает их;

  • Растеризатор преобразует трехмерный мир треугольников в двухмерную сетку пикселей;

  • Блоки вывода рендеринга (ROP) считывают, записывают и смешивают пиксели.

Блок Primitive Setup работает с частотой 1 треугольник за такт. Параметр может показаться не очень большим, но не забывайте, что эти чипы работают на частотах между 1,8 и 2,2 ГГц, и эта настройка не должна оказываться узким местом ГП. Для Ampere этот блок находится на следующем уровне организации, и об этом мы еще поговорим позже.

Ни AMD, ни NVIDIA не особенно распространяются о своих растеризаторах. NVIDIA называют их Raster Engines. Мы знаем, что они обрабатывают 1 треугольник за такт, но больше никакой информации о них нет например, о субпиксельной точности.

Каждый SE в чипе Navi 21 содержит 128 ROP; GA102 от NVIDIA включает в себя 112 ROP. Может показаться, что у AMD здесь есть преимущество, ведь большее количество ROP означает, что за такт может обрабатываться больше пикселей. Но такие устройства нуждаются в хорошем доступе к кэш-памяти и локальной памяти, и мы поговорим об этом позже в этой статье. А пока давайте дальше рассмотрим на разделение SE/GPC.

Shader Engines AMD разделены на то, что они сами называют двойными вычислительными блоками (DCU), при этом чип Navi 21 использует десять DCU для каждого SE обратите внимание, что в некоторых документах они также классифицируются как Workgroup Processors (WGP). В случае Ampere и GA102 они называются кластерами обработки текстур (TPC), причем каждый графический процессор содержит 6 TPC. Они также работают со скоростью 1 треугольник за такт, и хотя графические процессоры NVIDIA работают на меньшей частоте, чем AMD, и у них намного больше TPC, чем у Navi 21 SE. Таким образом, при той же тактовой частоте GA102 имеет здесь заметное преимущество, поскольку весь чип содержит 42 блока Primitive Setup, тогда как новый RDNA 2 от AMD только 4. Но поскольку на один Raster Engine приходится шесть TPC, GA102 фактически имеет 7 систем примитивов, в то время как Navi 21 четыре. Кажется, что NVIDIA имеет здесь явное лидерство.

Последний уровень организации чипов вычислительные блоки (CU) в RDNA 2 и потоковые мультипроцессоры (SM) в Ampere производственные линии в наших ГП-заводах.

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

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

Но в случае с CU и SM AMD и NVIDIA используют разные подходы к обработке шейдеров. И пусть в некоторых областях у них много общего, но много и различий.

Подсчет ядер по методу NVIDIA

Если Turing имела множество существенных отличий от Pascal, то Ampere кажется довольно мягким обновлением предыдущей архитектуры по крайней мере, на первый взгляд. Впрочем, мы точно знаем, что по сравнению с Turing новая архитектура имеет более чем в два раза большее количество ядер CUDA в каждом SM.

В Turing потоковые мультипроцессоры содержат четыре раздела (иногда называемых блоками обработки), каждый из которых содержит логические блоки 16x INT32 и 16x FP32. Эти схемы предназначены для выполнения очень специфических математических операций с 32-битными значениями данных: блоки INT обрабатывают целые числа, а FP числа с плавающей запятой.

NVIDIA заявляет, что SM Ampere имеет в общей сложности 128 ядер CUDA, но, строго говоря, это неправда или с таким же успехом можно считать, что у Turing их было столько же. Блоки INT32 действительно могли обрабатывать значения с плавающей запятой, но только в очень небольшом количестве простых операций. Для Ampere NVIDIA увеличила поддерживаемый диапазон математических операций с плавающей запятой, чтобы соответствовать другим модулям FP32. Это означает, что общее количество ядер CUDA на SM действительно не изменилось, просто половина из них теперь имеет больше возможностей.

Поскольку блоки INT/FP могут работать независимо, SM Ampere может обрабатывать до 128 вычислений FP32 за цикл или 64 операций FP32 и 64 операций INT32 одновременно. Turing же умела делать только последнее.

Таким образом, новый графический процессор может потенциально вдвое увеличить производительность FP32 по сравнению с предшественником. Для вычислительных рабочих нагрузок это большой шаг вперед, но для игр польза окажется гораздо меньшей. Это стало очевидно после тестирования GeForce RTX 3080, в которой используется чип GA102 с 68 включенными SM.

Несмотря на то, что пиковая пропускная способность FP32 составляет 121% по сравнению с GeForce 2080 Ti, в среднем она увеличивает частоту кадров только на 31%. Так почему же вся эта вычислительная мощность тратится зря?

Простой ответ: зря она не тратится, просто игры не всегда запускают инструкции FP32.

Когда NVIDIA выпустила Turing в 2018 году, компания отметила, что в среднем около 36% инструкций, обрабатываемых графическим процессором, связаны с процедурами INT32. Эти вычисления обычно выполняются для определения адресов памяти, сравнения двух значений и логического управления.

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

Это означает, что GeForce RTX 3080 имеет только 11-процентное преимущество FP32 над 2080 Ti при работе в режиме INT+FP. Вот почему реальный прирост производительности в играх не так высок, как предполагают исходные данные.

Какие тут еще улучшения? На каждый SM приходится меньше тензорных ядер, но каждое из них оказывается намного более мощным, чем в Turing. Эти схемы выполняют очень специфические вычисления (например, умножают два значения FP16 и складывают ответ с другим FP16), и теперь каждое ядро выполняет 32 таких операции за цикл.

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

Ядра трассировки лучей также претерпели доработки: теперь они могут работать независимо от ядер CUDA, поэтому, пока они выполняют обход BVH или математику пересечения примитивов лучей, остальная часть SM все еще может обрабатывать шейдеры. Часть ядер трассировки лучей, отвечающая за проверку пересечений, также имеет вдвое большую производительность.

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

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

Что же насчет AMD что они сделали с вычислительными модулями в RDNA 2?

Трассировка лучей по-особенному

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

Эта часть вычислительных блоков выполняет проверки пересечения лучевого бокса или лучевого треугольника то же самое, что и ядра трассировки лучей в Ampere. Однако последние также ускоряют алгоритмы обхода BVH, тогда как в RDNA 2 это делается с помощью вычислительных шейдеров с использованием модулей SIMD 32.

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

Блоки Ray Accelerator находятся рядом с текстурными процессорами, поскольку они фактически являются частью одной и той же структуры. Хотя эта система действительно предлагает большую гибкость и устраняет необходимость в том, чтобы части кристалла занимались только трассировкой лучей и ничем другим одновременно с ней, первая реализация ее имеет некоторые недостатки. Наиболее примечательным из них является то, что текстурные процессоры могут обрабатывать только операции, связанные с текстурами или пересечениями примитивов лучей. Учитывая, что ядра трассировки лучей NVIDIA теперь работают полностью независимо от остальной части SM, это дает Ampere явное преимущество по сравнению с RDNA 2 в проработке структур ускорения и тестах пересечений, необходимых для трассировки лучей.

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

Например, в Gears 5 видеокарта Radeon RX 6800 (в которой используется вариант графического процессора Navi 21 с 60 CU) при включении трассировки лучей снизила частоту кадров только на 17%, тогда как в Shadow of the Tomb Raider аж на 52%. Для сравнения, у NVIDIA RTX 3080 (с использованием 68 SM GA102) средняя потеря частоты кадров в этих двух играх составила 23% и 40% соответственно.

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

Как упоминалось ранее, вычислительные блоки в RDNA 2 теперь поддерживают больше типов данных: наиболее заметными из них являются типы данных с низкой точностью, такие как INT4 и INT8. Они используются для тензорных операций в алгоритмах машинного обучения, и хотя AMD имеет отдельную архитектуру (CDNA) для ИИ и центров обработки данных, это обновление предназначено для использования с DirectML.

Этот API является недавним дополнением к семейству Microsoft DirectX 12. Комбинация аппаратного и программного обеспечения обеспечивает лучшее ускорение шумоподавления в алгоритмах трассировки лучей и временного масштабирования. В случае с последним у NVIDIA, конечно же, есть своя технология под названием DLSS. Она использует тензорные ядра в SM для выполнения части вычислений но учитывая, что аналогичный процесс может быть построен и через DirectML, может показаться, что эти компоненты в некоторой степени избыточны. Однако и в Turing, и в Ampere тензорные ядра также обрабатывают все математические операции, связанные с форматами данных FP16.

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

Какой же подход лучше?

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

Но если у NVIDIA весь SM может обрабатывать до 128 вычислений FMA FP32 за цикл , один вычислительный блок RDNA 2 производит только 64 таких вычисления. Использование FP16 увеличивает это значение до 128 FMA за цикл, что совпадает с тем, что делают тензорные ядра в Ampere при стандартных вычислениях FP16.

SM NVIDIA могут выполнять инструкции для одновременной обработки целочисленных значений и значений с плавающей запятой (например, 64 FP32 и 64 INT32) и имеют независимые блоки для операций FP16, тензорной математики и процедур трассировки лучей. Блоки управления AMD выполняют большую часть рабочей нагрузки блоков SIMD32, хотя у них есть отдельные скалярные блоки, которые поддерживают простую целочисленную математику.

Таким образом, может показаться, что у Ampere здесь преимущество: у GA102 больше SM, чем у Navi 21, и у них больше возможностей, когда дело доходит до пиковой пропускной способности, гибкости и предлагаемых функций. Но у AMD есть свой джокер в рукаве.

Система памяти и многоуровневые кэши

Давайте сначала взглянем на Ampere. В целом, внутри произошли некоторые заметные изменения: объем кэша 2-го уровня увеличился на 50% (Turing TU102 имел 4096 КБ, соответственно), а кэши 1-го уровня в каждом SM увеличились вдвое.

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

  • 64 КБ для данных и текстур;

  • 48 КБ для общей памяти;

  • 16 КБ для конкретных операций.

Остальная часть внутренней памяти осталась прежней, но за пределами графического процессора ждет приятный сюрприз. NVIDIA стала работать с Micron и теперь использует модифицированную версию GDDR6 для своих потребностей в локальной памяти. По сути, это тот же GDDR6, но шина данных полностью заменена. Вместо того, чтобы использовать обычную настройку 1 бит на вывод, при которой сигнал очень быстро колеблется между двумя значениями напряжения (PAM), GDDR6X использует четыре значения напряжения:

PAM2 в GDDR6 (сверху) и PAM4 в GDDR6X (снизу)PAM2 в GDDR6 (сверху) и PAM4 в GDDR6X (снизу)

Благодаря этому GDDR6X эффективно передает 2 бита данных на вывод за цикл, поэтому при той же тактовой частоте и количестве выводов полоса пропускания удваивается. GeForce RTX 3090 поддерживает 24 модуля GDDR6X, работающих в одноканальном режиме и рассчитанных на 19 Гбит/с, что дает пиковую пропускную способность 936 ГБ/с. Это на 52% больше, чем у GeForce RTX 2080 Ti. Таких показателей пропускной способности в прошлом можно было достигнуть только при помощи HBM2, реализация которого может быть куда более дорогостоящей, чем GDDR6.

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

Каждый шейдерный движок теперь имеет два набора кэшей первого уровня. Но как можно втиснуть в графический процессор 128 МБ кэш-памяти третьего уровня? Используя конструкцию SRAM для кэша L3, AMD встроила в чип два набора кэш-памяти высокой плотности объемом 64 МБ. Транзакции данных обрабатываются 16 наборами интерфейсов, каждый из них сдвигает 64 байта за такт.

Так называемый Infinity Cache имеет свой собственный тактовый домен и может работать на частоте 1,94 ГГц, что дает пиковую внутреннюю пропускную способность 1986,6 ГБ/с. И поскольку это не внешняя DRAM, задержки здесь исключительно низкие. Такой кэш идеально подходит для хранения структур ускорения трассировки лучей, и поскольку обход BVH включает в себя множество проверок данных, Infinity Cache должен в этом особенно помочь.

На данный момент не ясно, работает ли кеш третьего уровня в RDNA 2 так же, как в ЦП Zen 2: то есть, как кэш жертвы (victim cache) второго уровня. Обычно, когда необходимо очистить последний уровень кэша, чтобы освободить место для новых данных, любые новые запросы этой информации должны поступать в DRAM.

В кэше жертвы хранятся данные, помеченные для удаления из следующего уровня памяти, и имея под рукой 128 МБ, Infinity Cache потенциально может хранить 32 полных набора кэша L2. Эта система снижает нагрузку на контроллеры GDDR6 и DRAM.

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

Так что же лучше?

Использование GDDR6X дает GA102 огромную полосу пропускания для локальной памяти, а большие кэши помогают уменьшить влияние промахов кэша. Массивная кэш-память 3-го уровня Navi 21 позволяет реже использовать DRAM, при этом графический процессор может работать на более высоких тактовых частотах без дефицита данных.

Решение AMD придерживаться GDDR6 означает, что сторонним поставщикам доступно больше источников памяти, в то время как любая компания, производящая GeForce RTX 3080 или 3090, будет вынуждена использовать Micron. И хотя GDDR6 поставляется с модулями различной плотности, GDDR6X в настоящее время ограничен 8 Гб.

Система кэширования в RDNA 2, возможно, является лучшим подходом, чем та, что используется в Ampere, поскольку использование нескольких уровней встроенной SRAM всегда обеспечивает более низкие задержки и лучшую производительность для заданного диапазона мощности, чем внешняя DRAM, независимо от пропускной способности последней.

Пайплайны рендеринга

Обе архитектуры содержат множество обновлений для фронтэнда и бэкэнда пайплайнов рендеринга. Ampere и RDNA 2 полностью поддерживают mesh-шейдеры и variable rate-шейдеры в DirectX12 Ultimate, хотя чип NVIDIA обладает большей геометрической производительностью благодаря большему количеству процессоров для этих задач.

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

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

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

В Turing восемь блоков ROP были напрямую связаны с одним контроллером и фрагментом кэша размером 512 КБ. Добавление большего количества ROP представляется проблематичным, поскольку для этого требуется больше контроллеров и кэша, поэтому для Ampere ROP теперь полностью выделены для GPC. GA102 поддерживает 12 ROP на один GPC (каждый обрабатывает 1 пиксель за такт), что дает в общей сложности 112 блоков для всего чипа.

AMD следует системе, аналогичной старому подходу NVIDIA (т. е. привязке к контроллеру памяти и кэш-памяти L2), хотя их ROP в основном используют кэш первого уровня для чтения/записи и смешивания пикселей. В чипе Navi 21 ROP теперь обрабатывает 8 пикселей за цикл в 32-битном цвете и 4 пикселя в 64-битном.

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

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

Этот механизм по своей природе является последовательным: ЦП обрабатывает один запрос за раз. NVIDIA заявляет о таких цифрах, как 100-кратная пропускная способность и 20-кратное снижение нагрузки на ЦП, но до тех пор, пока система не будет протестирована в реальных условиях, их никак нельзя будет исследовать дальше.

Когда AMD представила RDNA 2 и новые видеокарты Radeon RX 6000, вместе с ними была представлена и так называемая Smart Access Memory. Это не ответ на RTX IO на самом деле, это даже не новая функция. По умолчанию контроллер PCI Express в ЦП может адресовать до 256 МБ памяти видеокарты для каждого отдельного запроса доступа. Это значение устанавливается размером регистра базового адреса (BAR), и еще в 2008 году в спецификации PCI Express 2.0 была дополнительная функция, позволяющая изменять его размер. Преимущество его состоит в том, что нет нужды обрабатывать большое количество запросов на доступ, чтобы адресовать всю DRAM карты.

Функция требует поддержки операционной системой, центральным процессором, материнской платой, графическим процессором и его драйверами. В настоящее время на ПК с Windows система ограничена определенной комбинацией процессоров Ryzen 5000, материнских плат серии 500 и видеокарт Radeon RX 6000.

Эта простая функция дала поразительные результаты на тестах: повышение производительности на 15% при разрешении 4K. Неудивительно, что вскоре NVIDIA заявила, что реализует эту функцию для RTX 3000 в ближайшем будущем.

Мультимедиа-движок и видеовыход

Обе архитектуры обеспечивают вывод изображения через HDMI 2.1 и DisplayPort 1.4a. Первый предлагает более широкую полосу пропускания сигнала, но оба они рассчитаны на 4K при 240 Гц с HDR и 8K при 60 Гц. Это достигается при помощи либо цветовой субдискретизации 4:2:0, либо DSC 1.2a. Это алгоритмы сжатия видеосигнала, обеспечивающие значительное снижение требований к полосе пропускания без большой потери визуального качества. Без них даже пиковой пропускной способности HDMI 2.1 в 6 ГБ/с было бы недостаточно для передачи изображений 4K с частотой 6 Гц.

48-дюймовый OLED-монитор LG CK: 4K при 120 Гц требует HDMI 2.148-дюймовый OLED-монитор LG CK: 4K при 120 Гц требует HDMI 2.1

Ampere и RDNA 2 также поддерживают системы с переменной частотой обновления (FreeSync для AMD, G-Sync для NVIDIA), в кодировании и декодировании видеосигналов между ними также нет заметной разницы.

Независимо от того, какой процессор рассматривать, в обоих есть поддержка декодирования 8K AV1, 4K H.264 и 8K H.265, хотя и еще не было тщательно изучено, насколько хорошо они оба работают в таких ситуациях. Ни одна из компаний не раскрывает подробностей о внутреннем устройстве в этих областях. Какими бы важными они ни были в наши дни, все внимание по-прежнему привлекают другие аспекты графических процессоров.

Созданы для вычислений, созданы для игр

Раньше AMD и NVIDIA использовали разные подходы к выбору архитектуры и конфигурации. Но по мере того, как 3D-графика набирала все большую популярность, они становились все более похожими.

На данный момент у NVIDIA есть три чипа, использующих технологию Ampere: GA100, GA102 и GA104.

GA104 используется в GeForce RTX 3060 TiGA104 используется в GeForce RTX 3060 Ti

Последний урезанная версия GA102. GA100 совсем другое дело.

В нем нет ядер трассировки лучей и CUDA с поддержкой INT32+FP32 вместо этого он содержит множество дополнительных модулей FP64, еще больше load/store систем и огромный объем кэш-памяти L1/L2. Все это объясняется тем, что он разработан для вычислений ИИ и анализа данных.

GA102/104, в свою очередь, должны охватывать все остальные рынки, на которые нацелена NVIDIA: геймеров, профессиональных графических художников и инженеров, а также маломасштабный ИИ и вычислительные системы. Ampere должен быть мастером на все руки а это задача не из легких.

Arcturus CDNA площадью 750 мм2Arcturus CDNA площадью 750 мм2

RDNA 2 была разработана только для игр на ПК и консолях, хотя могла с таким же успехом работать в тех же областях, что и Ampere. Однако AMD решила сохранить свою архитектуру GCN и обновить ее в соответствии с требованиями сегодняшних клиентов.

Там, где RDNA 2 породила Big Navi, можно сказать, что CDNA породила Big Vega: в Instinct MI100 находится их чип Arcturus 50-миллиардный транзисторный графический процессор с 128 вычислительными блоками.

Хотя NVIDIA в значительной степени доминирует на профессиональном рынке с моделями Quadro и Tesla, Navi 21 просто не нацелена на конкуренцию с ними. Ограничивает ли это каким-либо образом требование, чтобы Ampere вписалась на несколько рынков?

Правильный ответ: нет.

Скоро AMD выпустит Radeon RX 6900 XT, в котором используется полная версия Navi 21 (без отключенных CU), которая сможет работать так же хорошо, как GeForce RTX 3090 или даже лучше. Но GA102 на этой карте также не полностью включен, поэтому у NVIDIA всегда есть возможность обновить эту модель до супер-версии, как они это сделали с Turing в прошлом году.

Можно утверждать, что, поскольку RDNA 2 используется в Xbox Series X/S и PlayStation 5, разработчики игр будут отдавать предпочтение этой архитектуре для своих игровых движков. Но стоит просто вспомнить времена, когда GCN использовался в Xbox One и PlayStation 4, чтобы получить представление, как это, вероятно, будет происходить.

Первая версия, выпущенная в 2013 году, использовала графический процессор, построенный на архитектуре GCN 1.0, не появлявшийся в видеокартах для настольных ПК вплоть до следующего года. Xbox One X, выпущенный в 2017 году, использовал GCN 2.0, которому к тому времени было уже более 3 лет.

Так что же все игры, созданные для Xbox One или PS4, а затем портированные на ПК, по умолчанию лучше работали на видеокартах AMD? А вот и нет. Поэтому мы не можем предположить, что на этот раз с RDNA 2 все будет иначе, несмотря на впечатляющий набор функций.

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

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

Подробнее..

Перевод От int main() до BeginPlay как происходит инициализация Unreal Engine под капотом

24.03.2021 14:22:50 | Автор: admin

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

Но когда вы пишете игровой код на Unreal Engine, вы не имеете дело с игровым циклом напрямую. Вы не начинаете работать сразу с основной функцией сначала вы определяете подкласс GameMode и переопределяете функцию под названием InitGame. Или пишете одноразовые классы Actor и Component и переопределяете их функции BeginPlay или Tick для добавления собственной логики. Это самый минимум того, что вам нужно сделать: обо всем остальном движок позаботится за вас.

Unreal Engine также предлагает вам как программисту большую мощность и гибкость: конечно, он имеет открытый исходный код, но также возможно и расширение несколькими другими способами. Даже если вы только начинаете работать с этим движком, было бы не лишним получить представление о его GameFramework: о таких классах, как GameMode, GameState, PlayerController, Pawn и PlayerState.

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

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

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

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

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

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

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

Основной цикл движка реализован в классе FEngineLoop:

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

PreInit это место, где загружается большинство модулей. Когда вы создаете игровой проект или плагин с исходным кодом на C++, вы определяете один или несколько исходных модулей в файле .uproject или .uplugin и можете указать в LoadingPhase, когда нужно загрузить этот модуль.

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

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

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

Так что же происходит после загрузки вашего модуля?

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

Итак, если вы определили пользовательский тип Актер (Actor), или пользовательский игровой режим, или что-то еще, объявленное перед UCLASS, цикл движка выделяет экземпляр этого класса по умолчанию, затем запускает его конструктор, передавая CDO родительского класса в качестве шаблона. Это одна из причин, по которой конструктор не должен содержать никакого кода, связанного с геймплеем: на самом деле он предназначен только для установления универсальных деталей класса, а не для изменения какого-либо конкретного экземпляра этого класса.

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

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

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

Движок это программный продукт, содержащий исходный модуль под названием Engine. В этом модуле находится заголовок под названием Engine.h, а в этом заголовке определен класс UEngine, реализованный как в UEditorEngine, так и в UGameEngine.

На этапе инициализации игры FEngineLoop проверяет файл конфигурации движка, чтобы определить, какой класс GameEngine нужно использовать. Затем он создает экземпляр этого класса и закрепляет его как глобальный экземпляр UEngine, доступный через глобальную переменную GEngine, объявленную в Engine/Engine.h.

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

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

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

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

Класс UGameInstance был добавлен в Unreal 4.4 и выделен из класса UGameEngine для обработки некоторых функций, более специфичных для проекта, которые ранее обрабатывались в Engine.

Итак, после инициализации класса Engine у нас появляются GameInstance, GameViewportClient и LocalPlayer. Теперь игра готова к запуску: именно здесь происходит первоначальный вызов LoadMap. К концу вызова LoadMap у нас будет UWorld, содержащий всех актеров, сохраненных на нашей карте, а также несколько новых актеров, формирующих ядро GameFramework, включающее игровой режим, игровую сессию, состояние игры, диспетчер игровой сети, контроллер игрока, состояние игрока и пешку.

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

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

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

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

Короче говоря, к этому моменту World уже не будет. Однако у нас есть Контекст Мира (World Context). Этот объект создается экземпляром игры во время инициализации Engine, и по сути, это постоянный объект, который отслеживает, какой мир загружен в данный момент. Перед загрузкой чего-либо еще GameInstance может предварительно загрузить любые ассеты, которые ему могут понадобиться, но по умолчанию ничего не делает.

Далее нам нужно получить UWorld.

Если вы работаете с картой в редакторе, редактор загружает в память UWorld вместе с одним или несколькими ULevels, которые содержат размещенных вами Актеров. Когда вы сохраняете свой постоянный уровень, этот Мир, его Уровень и все его Актеры сериализуются в пакет карты, который записывается на диск в виде файла .umap. Во время LoadMap движок находит этот пакет и загружает его. На этом этапе мир, его постоянный уровень и актеры на этом уровне, а также WorldSettings загружаются обратно в память.

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

Движок дает миру ссылку на GameInstance, а затем инициализирует глобальную переменную GWorld. Затем мир устанавливается в WorldContext, ему присваивается тип в данном случае Game, и он добавляется в корневой каталог. InitWorld позволяет миру настраивать такие системы, как физика, навигация, искусственный интеллект и аудио.

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

Далее мы переходим к InitializeActorsForPlay. Это то, что Engine называет подведением мира к игре. Здесь World перебирает всех актеров в нескольких разных циклах. Первый цикл регистрирует все компоненты актеров в мире.

Происходит регистрация каждого компонента ActorComponent в каждом Actor, что делает для компонента три важных вещи:

  • Во-первых, мы получаем ссылку на мир, в который он был загружен;

  • Затем рпроисходит вызов функции компонента OnRegister, дающий ему возможность выполнить любую раннюю инициализацию;

  • В случае PrimitiveComponent после регистрации компонент будет иметь FPrimitiveSceneProxy, созданный и добавленный в FScene, являющийся версией потока рендеринга UWorld.

После регистрации компонентов World вызывает функцию InitGame GameMode. Это заставляет GameMode породить актера GameSession. После этого у нас есть еще один цикл, в котором мир проходит от уровня к уровню, и каждый уровень инициализирует всех своих актеров. Это происходит за два прохода. В первом проходе Уровень вызывает функцию PreInitializeComponents для каждого Актера. Это дает участникам возможность инициализироваться довольно рано после регистрации компонентов, но до их инициализации.

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

Наконец, мы повторяем цикл по всем актерам, на этот раз вызывая InitializeComponents, а затем PostInitializeComponents. InitializeComponents перебирает все компоненты актеров и проверяет две вещи:

  • Если в компоненте включена функция bAutoActivate, необходимо активировать компонент;

  • Если в компоненте включен bWantsInitializeComponent, произойдет вызов функции InitializeComponent.

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

На этом этапе наш вызов LoadMap почти завершен: все актеры загружены и инициализированы, мир готов для запуска в игре, и теперь у нас есть набор актеров, используемых для управления общим состоянием игры: GameMode определяет правила игры, он же порождает большинство актеров кор-геймплея. Это высший авторитет того, что происходит во время игры, и он существует только на сервере. GameSession и GameNetworkManager также работают только на сервере. Сетевой менеджер используется для настройки таких вещей, как обнаружение читов и предсказание движения. А для онлайн-игр GameSession одобряет запросы на вход и служит интерфейсом для онлайн-сервисов (например, Steam или PSN).

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

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

В этот момент LoadMap перебирает все LocalPlayers, присутствующие в нашем GameInstance: обычно таковой существует только один. Для конкретного LocalPlayer он вызывает функцию SpawnPlayActor. Обратите внимание, что PlayActor здесь взаимозаменяем с PlayerController: эта функция порождает PlayerController. LocalPlayer, как мы уже убедились, является представлением игрока в движке, а PlayerController представлением игрока в игровом мире.

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

Чтобы любой игрок мог присоединиться к игре независимо от того, локальная она или удаленная, он должен пройти процесс входа в систему. Этот процесс обрабатывается GameMode. Функция PreLogin в GameMode вызывается только для попыток удаленного подключения: она отвечает за утверждение или отклонение запроса на вход. Как только мы получаем добро на добавление игрока в игру, происходит вызов Login. Функция Login порождает актера PlayerController и возвращает его в мир.

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

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

После создания PlayerController World полностью инициализирует его для работы в сети и связывает с объектом Player. После этого вызывается функция PostLogin игрового режима, которая дает игре возможность выполнить любую настройку, которая должна произойти в результате присоединения этого игрока. По умолчанию игровой режим будет пытаться создать Pawn для нового PlayerController в PostLogin. Pawn это особый тип актера, которым может владеть Controller. PlayerController это специализация базового класса Controller. Есть еще один подкласс под названием AIController, использующийся для неигровых персонажей.

Это давнее соглашение в Unreal: если у вас есть актер, который перемещается по миру, руководствуясь собственным автономным процессом принятия решений будь то игрок-человек, принимающий решения и переводящий их в данные ввода, или ИИ, принимающий решения более высокого уровня о том, куда идти и что делать, обычно у вас есть два актера. Controller это тот, кто управляет актером, а Pawn представление актера в мире. Поэтому, когда к игре присоединяется новый игрок, GameMode по умолчанию порождает Pawn для нового PlayerController.

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

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

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

В любом случае, после создания Pawn будет связана с PlayerController, которому она принадлежит. Теперь, когда мы вернемся в LoadMap, у нас будет все готово для фактического запуска игры. Все, что осталось сделать это маршрутизировать событие BeginPlay. От Engine к World, от World к GameMode, от GameMode к WorldSettings, а WorldSettings, в свою очередь, перебирает всех актеров.

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

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

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

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

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

На этапе инициализации мы начинаем настройку самого движка. Короче говоря, мы создаем объект Engine, инициализируем его, а затем запускаем игру. Чтобы инициализировать движок, мы создаем GameInstance и GameViewportClient, а затем создаем LocalPlayer и связываем его с GameInstance. После этого мы можем начать загрузку игры.

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

Остальная часть нашего процесса запуска происходит в вызове LoadMap. Сначала мы находим наш пакет карты, затем загружаем его: он переносит в память всех актеров, помещенных на постоянный уровень, а также дает нам объекты World и Level. Мы находим наш мир, даем ему ссылку на GameInstance, инициализируем некоторые системы, а затем создаем актера GameMode. После этого мы полностью загружаем карту, добавляя все необходимые подуровни и ассеты. Когда все оказывается полностью загружено, мы начинаем подходить мир к игре. Сначала мы регистрируем все компоненты для каждого актера на каждом уровне, а затем инициализируем GameMode, который, в свою очередь, порождает актера GameSession. После этого мы инициализируем всех актеров в мире.

Сначала мы вызываем PreInitializeComponents для каждого актера на каждом уровне: когда это происходит для GameMode, он порождает GameState и GameNetworkManager, а затем инициализирует GameState. Затем в другом цикле мы инициализируем каждого актера на каждом уровне: происходит вызов InitializeComponent (и, возможно, Activate) для всех компонентов, которым это необходимо, после чего актеры уже формируются окончательно.

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

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

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

Мы рассмотрели здесь много вещей, поэтому выделим самое важное:

  • Мы рассматривали классы GameModeBase и GameStateBase, а не GameMode и GameState. Эти базовые классы были добавлены в Unreal 4.14, чтобы исключить некоторые функциональные возможности Unreal-Tournament из игрового режима. В то время как GameModeBase содержит все основные функции игрового режима, класс GameMode добавляет концепцию матча с изменениями состояния матча, которые происходят после BeginPlay. Это позволяет следить за потоком игры например, за готовностью всех игроков, за временем начала и окончания игры и переходом к новой карте для следующего матча.

  • Мы также рассмотрели класс Pawn, но помимо него GameFramework определяет класс Character, который является специализированным типом Pawn, включающим сразу несколько полезных функций. У класса Character есть капсула столкновения, которая используется в основном для движений, а также скелетная сетка, в связи с чем предполагается, что он является анимированным персонажем. Еще у него есть компонент CharacterMovementComponent, тесно связанный с классом Character и выполняющий несколько полезных вещей. Самым важным является то, что движение персонажа воспроизводится из коробки с предсказанием движения на стороне клиента. CharacterMovement реализует полный набор опций передвижения для ходьбы, прыжков, падений, плавания и полета.

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

Итак, вот все те классы, которые мы рассмотрели (за исключением UWorld и ULevel):

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

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

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

Подробнее..

Перевод Как и зачем Morrowind перезапускала оригинальный Xbox во время экрана загрузки

22.04.2021 12:11:11 | Автор: admin

Оригинальный Xbox известен тем, что имел всего 64 мегабайта оперативной памяти, чего даже в то время не всегда хватало играм. В недавнем подкасте о слиянии Bethesda и Xbox директор Bethesda Game Studios Тодд Говард рассказал о том, что именно из-за нехватки памяти и для ее освобождения Morrowind иногда перезагружала Xbox незаметно для пользователя. Долгие внутриигровые загрузки это как раз то, о чем идет речь.

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

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

Теперь настало время доказать это.

Для этого нам понадобятся три вещи:

  1. Копия игры;

  2. Комплект разработчика Xbox (XDK):

  3. Декомпилятор.

В качестве последнего будем использовать IDA Pro.

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

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

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

Отсюда вопрос: как все это работает?

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

Эта часть API Xbox XLaunchNewImage. Согласно документации, она перезагружает консоль для запуска другого файла XBE с DVD-диска.

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

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

Запустим игру на девките. В этом случае XLaunchNewImage должна перезапустить консоль. Теперь загрузим сохраненную игру с жесткого диска, но перед этим задействуем инструмент под названием Xbox File Event Viewer. Он будет отслеживать и выводить на экран создание каждого нового файла для чтения или записи.

Установим фильтр на названии morrowind.xbe. Если будет происходить запуск файла с таким именем, скорее всего, в этом вызове будет задействована XLaunchNewImage, которая и производит перезапуск консоли для исполнения другого файла XBE с диска.

Теперь запустим Xbox File Event Viewer и наш файл сохранения. Как мы видим, происходят множественные чтения файла morrowind.xbe. Это не обязательно говорит о перезапуске консоли, но наверняка указывает на то, что происходит активность XLaunchNewImage.

Теперь используем инструмент для реверс-инжиниринга и декомпилируем исполняемый файл morrowind.xbe. Для этого загрузим его в IDA Pro. И для начала поищем в нем непосредственно упоминание morrowind.xbe.

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

По-видимому, выделенный путь и исполняет XLaunchNewImage ведь, как мы видим в документации, один из требуемых для этого параметров строка для адреса XPE, а второй данные о запуске. Это вполне себе коррелирует с функцией sub_23AB90. Переименуем ее в XLaunchNewImage.

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

Если байт byte_3A3209 имеет значение true, выполняется один кусок кода, в противном случае другой, заканчивающийся исполнением команды XLaunchNewImage.

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

Перейдем к ней.

Один из двух байтов здесь говорит о том, чтобы если флаг No reboot on new game = 1 в файле Morrowind.ini, тогда этот байт равен true. Что касается второго, искомого байта, он становится true, если выполняется условие No reboot on load game = 1 в Morrowind.ini.

Если открыть Morrowind.ini, мы увидим, что оба значения No reboot on new game и No reboot on load game установлены в нуле, а значит игра будет перезагружаться на Xbox. Но на девкитах для отладки, когда игра разрабатывалась Bethesda, скорее всего, они имели значения единицы, поскольку те использовали 126 МБ вместо 64.

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

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

Мы убедились, что Xbox действительно перезагружается для очистки памяти во время экранов загрузки Morrowind, и вместо того, чтобы возвращаться к главному экрану игры, сразу переходит на экран загрузки и вызывает последнее сохранение. Достигается это с помощью процедуры под названием XLaunchNewImage части API Xbox, которую можно использовать для тихой перезагрузки консоли и запуска исполняемого файла. Но чего Говард не упомянул, так это того, что то же самое происходит при старте новой игры.

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

Подробнее..

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

21.05.2021 12:08:14 | Автор: admin

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

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

Итак, начнем

А начнем мы с того, какую роль в ПК выполняет материнская плата. По сути, она служит для:

  • Обеспечения компонентов электропитанием;

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

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

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

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

  • Standard ATX 12 9,6 дюйма (305 244 мм);

  • Micro ATX 9,6 9,6 дюйма (244 244 мм);

  • Mini ATX 5,9 5,9 дюйма (150 150 мм).

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

Но что же такое материнская плата?

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

Начнем разбор с типичной материнской платы ATX. Ниже изображена плата Asus Z97-Pro Gamer, внешне и функционально не сильно отличающаяся от десятков ей подобных.

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

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

Подключаем мозг к ПК

На схеме выше мы видим структуру, имеющую обозначение LGA1150. Это обозначение используется в Intel для разъема, предназначенного для подключения различных процессоров. LGA здесь означает Land Grid Array распространенный тип технологии упаковки центральных процессоров и других чипов.

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

Металлический фиксатор помогает закрепить ЦП на месте, но мешает рассмотреть контакты, так что уберем его:

Помните, что это? LGA1150. 1150 здесь указывает на количество выводов в разъеме, и у других материнских плат оно может отличаться. Чем выше производительность ЦП (с точки зрения количества ядер, объема кэш-памяти и т. д.), тем больше контактов будет в разъеме. Большая их часть используется для обмена данными со следующей важной частью материнской планы.

Большому мозгу большая память

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

В нашем примере ЦП в материнской плате имеет 2 контроллера памяти, каждый из которых оперирует 2 картами памяти, всего 4 слота DRAM. На материнской плате они окрашены так, чтобы вы знали, какие из них каким контроллером управляются. Обычно их называют каналами памяти.

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

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

При такой комбинации ЦП и материнской платы используются микросхемы DDR3 SDRAM (синхронная динамическая память с произвольным доступом с версией 3 двойной скорости передачи данных), и каждый слот может содержать один SIMM или DIMM. IMM обозначает X-рядный модуль памяти (In-line Memory Module); S и D указывают на то, заполнена ли чипами одна или две стороны модуля (Single или Dual, соответственно).

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

Одинарный модуль DIMM DDR3 SDRAMОдинарный модуль DIMM DDR3 SDRAM

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

Обычно производители придерживаются 240 контактов в модулях памяти, и нет никаких признаков того, что в ближайшее время это изменится. Чтобы улучшить производительность памяти, с каждой новой версией просто увеличивается скорость чипов. В нашем примере каждый из контроллеров памяти ЦП может отправлять и получать 64 бита данных за такт. Таким образом, с двумя контроллерами карты памяти будут иметь 128 контактов, предназначенных для обмена данными. Так почему же их именно 240?

Каждый чип в DIMM (всего их 16 по 8 на каждую сторону) может передавать 8 бит за такт. Это означает, что каждому чипу требуется 8 контактов только для передачи данных; однако пара чипов использует одни и те же выводы, поэтому только 64 из 240 отвечают за обмен данными. Остальные 176 контактов необходимы для синхронизации и передачи адресов данных (места расположения данных в модуле), управления микросхемами и обеспечения их электроэнергией. Так вы можете убедиться, что наличие более 240 контактов не обязательно улучшит ситуацию.

ОЗУ не единственное, что подключено к процессору

Для повышения производительности системная память подключена напрямую к центральному процессору, но на материнской плате есть и другие разъемы, подключенные примерно так же и в тех же целях. Они используют технологию подключения PCI Express (сокращенно PCIe), и каждый современный ЦП имеет встроенный контроллер PCIe.

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

На изображении ниже показаны 3 разъема: два верхних это PCI Express, нижний гораздо более старая система под названием PCI (родственная PCIe, но намного медленнее). Маленький слот сверху, обозначенный как PCIEX1_1, имеет одну линию; тот, что ниже, 16 линий.

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

  • 2 разъема PCI Express на 1 линию;

  • 3 разъема PCI Express на 16 линий;

  • 2 разъема PCI.

Но если у контроллера ЦП всего 16 линий, то что тогда происходит? Прежде всего, к процессору подключены только PCIEX16_1 и PCIEX16_2, а третий и два однолинейных подключены к другому процессору на материнской плате (подробнее об этом чуть позже). Кроме того, если задействованы оба разъема на 16 линий PCIe, ЦП выделит каждому только по 8 линий. Это типично для всех современных процессоров: количество линий у них ограничено, и по мере того, как к ЦП подключается все больше устройств, на каждое выделяется все меньшее число линий.

Различные конфигурации процессора и материнской платы имеют собственные способы решения этой проблемы. Например, материнская плата Gigabyte B450M Gaming имеет один разъем PCIe на 16 линий, один PCIe на 4 линии и один M.2, использующий 4 линии PCIe. Поскольку ЦП имеет всего 16 линий, использование любых двух разъемов одновременно приведет к тому, что самый большой слот, 16-тилинейный, будет ограничен всего до 8 линий.

Так что же использует такие разъемы? Наиболее распространенные варианты:

  • 16 линии видеокарта;

  • 4 линии твердотельные накопители (SSD);

  • 1 линия звуковые карты и сетевые адаптеры.

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

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

Теперь двинемся через мост на юг

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

На фото выше показана материнская плата ASRock 939SLI32, где четко видны микросхемы NB и SB. Они обе скрыты под алюминиевыми радиаторами, но ближайшая к разъему ЦП в середине изображения это и есть северный мост. Спустя несколько лет после выхода этой платы и Intel, и AMD откажутся от использования NB и выпустят продукты, в которых NB интегрирован в ЦП. Южный мост, однако, остается отдельным, и, скорее всего, это не изменится в обозримом будущем. Интересно, что оба производителя процессоров перестали называть его SB и теперь часто зовут его просто чипсетом (собственное название Intel PCH, блок контроллеров платформы) несмотря на то, что это один чип.

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

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

  • 8 линий PCI Express (версия 2.0 PCIe);

  • 14 портов USB (6 для версии 3.0, 8 для версии 2.0);

  • 6 портов Serial ATA (версия 3.0).

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

В данном случае это процессор, обрабатывающий слоты PCIe с одной линией, третий слот с 16 линиями и слот M.2. Как и многие новые чипсеты, он обрабатывает все эти различные соединения с помощью набора высокоскоростных портов, которые можно переключить на PCI Express, USB, SATA или сетевое соединение, в зависимости от того, что подключено в данный момент. Это, к сожалению, накладывает ограничение на количество устройств, подключенных к материнской плате, несмотря на наличие всех этих разъемов.

В случае нашей материнской платы Asus порты SATA (используемые для подключения жестких дисков, DVD-приводов и т. д.) из-за этого ограничения сгруппированы так, как показано выше. Блок из 4 портов посередине использует стандартные USB-соединения чипсета, тогда как два слева используются некоторые из этих высокоскоростных соединений. Так что, если вы используете те, что слева, на чипсете будет меньше соединений для других разъемов. То же самое и с портами USB 3.0. В них есть поддержка до 6 устройств, но 2 порта обязательно будут подключены к высокоскоростным соединениям.

Разъем M.2, используемый для подключения SSD-накопителя, также использует быструю систему (вместе с третьим слотом PCI Express на 16 линий на этой материнской плате); однако в некоторых комбинациях ЦП и материнской платы разъемы M.2 подключаются непосредственно к ЦП, поскольку многие новые продукты имеют более 16 линий PCIe.

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

  • Разъем PS/2 для клавиатур и мышей (сверху слева);

  • Разъем VGA для недорогих и устаревших мониторов (сверху посередине);

  • Порты USB 2.0 черные (снизу слева);

  • Порты USB 3.0 синие (снизу посередине).

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

Дополнительные фишки для дополнительной помощи

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

Рассматриваемая нами материнская плата Asus не исключение. Например, микросхема Nuvoton NCT6791D обрабатывает все маленькие разъемы для кулеров и датчики температуры на плате. Расположенный рядом процессор Asmedia ASM1083 управляет двумя устаревшими разъемами PCI, поскольку у чипа Intel Z97 такой возможности нет.

Хоть у чипсета Intel и есть встроенный сетевой адаптер, но он нагружает столь драгоценные высокоскоростные соединения, поэтому Asus добавила еще один чип Intel (I218V) для управления красным разъемом Ethernet, который мы видели в блоке ввода/вывода. На изображении выше не видно, насколько мал этот чип: его площадь составляет всего 0,24 дюйма (6 мм).

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

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

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

Эти маленькие микросхемы представляют собой переключатели PCI Express и помогают процессору и южному мосту управлять 16-линейными разъемами PCIe, когда им необходимо распределить полосы между несколькими устройствами.

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

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

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

Этот чип Winbond имеет объем памяти всего 8 МБ, но этого более чем достаточно для хранения всего необходимого ПО. Этот вид флэш-памяти имеет очень малое энергопотребление при использовании и надежно хранит данные в течение десятилетий.

При включении ПК содержимое флэш-памяти копируется непосредственно в кэш ЦП или системную память, а затем запускается оттуда. Единственное, с чем это не сработает, это время.

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

Кстати о питании для него тоже есть отдельные разъемы.

Игорь, принеси мне питание!

Чтобы обеспечить материнскую плату и подключенные к ней устройства необходимым напряжением, блок питания (PSU) имеет ряд стандартных разъемов. Основной из них 24-контактный разъем ATX12V версии 2.4.

Величина тока зависит от блока питания, но промышленные стандарты напряжения составляют +3,3, +5 и +12 В.

Основная часть питания ЦП поступает от 12-вольтных контактов, но для современных высокопроизводительных систем этого недостаточно. Чтобы обойти эту проблему, существует дополнительный 8-контактный разъем питания, обеспечивающий еще четыре 12-вольтных линии.

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

Линии +3,3, +5 и +12 В подают питание на различные компоненты на самой материнской плате, а также ЦП, DRAM и любые устройства, подключенные к разъемам, таким как USB-порты или PCI Express. Однако все, что использует порты SATA, требует питания непосредственно от блока питания, а разъемы PCI Express могут обеспечить напряжение только до 75 Вт. Если устройству требуется больше энергии, например, как многим видеокартам, то их также необходимо подключить напрямую к блоку питания.

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

Например, процессоры Intel, совместимые в нашей материнской платой Asus Z97, работают с напряжением от 0,7 до 1,4 В. Это не фиксированное напряжение, поскольку современные процессоры меняют его для экономии энергии и уменьшения нагрева, и в спящем режиме процессор может потреблять менее 0,8 В. Затем, когда все ядра полностью загрузятся и начнут свою работу, оно возрастает до 1,4 В и более.

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

Каждый VRM обычно состоит из 4 компонентов:

  • 2 MOSFET сильноточные управляющие транзисторы (синие);

  • 1 дроссель (фиолетовый);

  • 1 конденсатор (желтый).

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

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

Даже стандартный настольный процессор, такой как Intel i7-9700K, может потреблять ток более 100 А при полной загрузке. VRM очень эффективны, но они не могут изменять напряжение без потерь. Принимая во внимание большой ток потребления, вы получаете отличное устройство для гриля.

Если снова взглянуть на фотографию нашей платы, можно увидеть пару VRM для модулей DRAM, но, поскольку напряжения там гораздо меньше, чем на ЦП, они не так сильно нагреваются (и поэтому радиатор не нужен).

И прочие назойливые мелочи

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

Здесь у нас есть:

  • 1 мягкий выключатель питания;

  • 1 кнопка сброса;

  • 2 светодиодных разъема;

  • 1 разъем для динамика.

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

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

Большинство материнских плат имеет аналогичный набор дополнительных разъемов, как показано выше слева направо:

  • Разъем аудиопанели если в корпус ПК встроены разъемы для наушников/микрофона, то их можно подключить к встроенному звуковому чипу;

  • Цифровой аудиоразъем такой же, как и обычный аудиоразъем, но для S/PDIF;

  • Перемычка сброса BIOS позволяет сбросить BIOS до заводских настроек. Также за ним спрятан разъем термозонда;

  • Разъем Trusted Platform Module используется для повышения безопасности материнской платы и системы.

  • Разъем последовательного порта (COM) древний интерфейс. Кто-нибудь вообще им пользуется? Кто-нибудь?

Также наклеены, но не показаны, разъемы для кулеров и дополнительных USB-портов. Не каждая материнская плата поддерживает все это, но многие.

Соединяя все вместе

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

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

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

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

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

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

В заключение

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

Подробнее..

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

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

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

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

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

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

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

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

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

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

Так что же вызывает все эти проблемы?

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

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

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

Джиттер это колебания задержки, означающие, что пакеты отправляются и принимаются с разной скоростью. Это похоже на плохой frame pacing: то ваш пинг меняется с 20 миллисекунд до секунды, то с секунды до 90 миллисекунд, а затем возвращается к 30 миллисекундам, которые были когда-то уже давно.

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

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

Так почему же возникают подобные сбои?

Существуют три основных типа проблем с соединением:

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

  • проблемы средней мили, обусловленные перемещением данных по маршруту между вашим интернет-провайдером и игровым сервером;

  • проблемы последней мили, связанные непосредственно с игровым сервером.

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

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

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

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

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

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

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

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

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

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

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

Наконец, перейдем к последней миле в цепочке игровым серверам.

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

Client hosting это когда ПК одного из игроков сам по себе выступает в качестве сервера. Это хороший способ для разработчиков игр минимизировать затраты, но опыт каждого участника матча будет зависеть от качества соединения игрока-хоста. Таким образом, если такие игроки подключаются к сети через Wi-Fi или вовсе испытывают проблемы с подключением, другие игроки тоже столкнутся с лагами, джиттером и потерей пакетов.

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

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

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

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

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

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

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

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

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

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

Dead reckoning это, по сути, алгоритм оценки положения объекта в виртуальном мире на основе его предыдущего положения, направления движения, скорости, ускорения и других параметров. Получив первый блок данных протокола состояния (protocol data unit, PDU) для объекта (например, персонажа другого игрока), каждый клиент начинает перемещение этого объекта, применяя согласованный алгоритм dead reckoning. Его движение обновляется при получении последующих PDU. Если для пакетов, несущих PDU, возникнет увеличенная задержка или вовсе их потеря, каждая копия виртуального мира продолжит показывать движение объектов в соответствии с алгоритмом до тех пор, пока не получит следующее обновление. Кроме того, при несоответствиях между статусом сервера и предсказанным клиентом некоторые игры могут сделать переход к новому статусу менее резким, используя алгоритмы сглаживания.

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

Время выполнения команды = Текущее время сервера Задержка пакета Интерполяция представления клиента

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

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


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

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

Подробнее..

Перевод Рендеринг острова из Моаны менее чем за 10 000 строк кода на Swift

18.01.2021 14:21:57 | Автор: admin
Остров из Моаны (2048 858 пикселей, 64 spp), отрендеренный при помощи Gonzales в хранилище Google Cloud с 8 виртуальными ЦП и 64 ГБ памяти примерно за 26 часов. Остров из Моаны (2048 858 пикселей, 64 spp), отрендеренный при помощи Gonzales в хранилище Google Cloud с 8 виртуальными ЦП и 64 ГБ памяти примерно за 26 часов. В памяти он занимает около 60 ГБ. Удаление шума при помощи OpenImageDenoise.Остров из Моаны (2048 858 пикселей, 64 spp), отрендеренный при помощи Gonzales в хранилище Google Cloud с 8 виртуальными ЦП и 64 ГБ памяти примерно за 26 часов. Остров из Моаны (2048 858 пикселей, 64 spp), отрендеренный при помощи Gonzales в хранилище Google Cloud с 8 виртуальными ЦП и 64 ГБ памяти примерно за 26 часов. В памяти он занимает около 60 ГБ. Удаление шума при помощи OpenImageDenoise.

После того, как Walt Disney Animation Studios выложила в сеть описание сцены острова из Моаны, много кто пытался его отрендерить своими силами, исключающими оригинальный Hyperion. Это лишь малая часть списка таких движков:

Андреас Вендледер из Бабельсбергского киноуниверситета представил другой, написанный им рендерер Gonzales. Он в значительной степени вдохновлен PBRT, написан на Swift (с несколькими строками кода на C ++ для вызова OpenEXR и Ptex) и оптимизирован для проведения рендеринга в (сравнительно) разумные сроки на бесплатном хранилище Google Cloud (8 виртуальных ЦП, 64 ГБ RAM). Насколько автору известно, это единственный рендерер, написанный не на C/C++, способный на рендеринг этой сцены. Написан он с помощью vi и командной строки Swift в Ubuntu Linux и Xcode на macOS, так что скомпилировать его на этих платформах не должно составить труда.

Так почему именно Swift?

Как пишет Вендледер, ему всегда было неудобно работать с заголовочными файлами и препроцессором на C и C++. Однажды объявив и определив что-либо (переменную, функцию), нет нужды делать это повторно. Кроме того, текстовое включение заголовочных файлов приносит с собой множество проблем, таких как необходимость добавления деталей реализации в эти самые файлы (на ум приходят, например, шаблоны) или медленное время компиляции ввиду многократного включения заголовков и вытекающего отсюда комбинаторного взрыва. Когда он начинал работать на C++, модули там еще не были доступны, поэтому он перепробовал написание кода на Python (слишком медленный), Go (слишком похож на C) и некоторых других языках программирования, но в конце концов остановился Rust и Swift. В результате предпочтение отдал Swift из-за его удобочитаемости (просто не нравились fn main и impl trait). Тем более, тот факт, что он был написан разработчиками LLVM и Clang, вселил уверенность, что он а) не будет заброшен в будущем и б) будет соответствовать поставленным целям по производительности. Короче говоря, был нужен скомпилированный язык без указателей, модулей, концепций, диапазонов, читаемых шаблонов, и нужен он был прямо сейчас. Кроме того, компиляторы были изобретены, чтобы облегчить жизнь программистам, сделав программы более читабельными, и порой, смотря на код, основанный на шаблонах, у автора складывалось ощущение, что мы движемся назад во времени. Все любят, когда их код легко читается.

Кое-какие заметки

Парсинг пережил несколько воплощений. Сначала то была простая строка (file.availableData, кодировка: .utf8), но она оказалась слишком велика, чтобы поместиться в памяти. Данные здесь не использовались по аналогичным причинам. В то же время еще и Scanner переехал из Foundation. В конце концов Вендледер остановился на чтении InputStream в 64-КБ массиве UnsafeMutablePointer <UInt8>.

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

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

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

  • Valgrind Memcheck: показывает, куда пропала память. Например, анализ с помощью этого инструмента говорит о том, почему структура ускорения отделена от билдера этой структуры: память, потраченная на построение ограничивающей иерархии, просто никогда не освобождалась. Приятно не иметь указателей в Swift: никаких malloc или new и даже sharedpointers, но все же необходимо подумать о том, как используется память.

  • Профилирование с Xcode: в основном здесь использовались Time Profiler, Leaks и Allocations, которые дают примерно ту же информацию, что и Perf и Valgrind, но с другой точки зрения. Порой бывает полезно взглянуть на одно и то же с двух разных ракурсов. Это напоминает старые добрые времена, когда мы загружали наше ПО в три разных компилятора (Visual Studio, GCC и один из IRIX, как там его? MIPSPro?).

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

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

О программировании на основе протоколов: текущая версия Gonzales показывает 23 протокола, 57 структур, 47 заключительных классов и 2 незавершенных класса. Наследование здесь почти не используется. Два оставшихся незавершенных класса это TrowbridgeReitzDistribution и Texture: оба они самому Вендледеру не нравятся, и он думает о том, как изменить их в будущем. В целом программирование на основе протоколов приводит к хорошему коду: например, раньше в Gonzalez был класс Primitive, такой как в PBRT, но вскоре он изменился на протокол, унаследованный от таких протоколов, как Boundable, Intersectable, Emitting (его уже нет) и других. Теперь и его тоже нет: BoundingHierarchyBuild просто зависит от экзистенциального типа Boundable и возвращает иерархию Intersectables, которую использует BoundingHierarchy. Все примитивы теперь хранятся как массив экзистенциальных типов, состоящий из композиции протоколов Boundable и Intersectable (var primitives = Boundable&Intersectable).

С другой стороны, примитивы в BoundingHierarchy хранятся как [AnyObject&Intersectable]. На это есть две причины:

1. Тут требуются только пересечения;

2. AnyObject заставляет хранимые объекты быть ссылочными типами (или классами), что экономит память, поскольку макет протоколов как для структур, так и для классов (OpaqueExistentialContainer) использует 40 байтов ведь Swift пытается хранить встроенные структуры, тогда как протоколы только для классов (ClassExistentialContainer) используют всего 16 байт, ведь там должен храниться только указатель, как это можно увидеть в документации Swift или проверить здесь. Стоит подчеркнуть, что это не только академическое обсуждение: автор столкнулся с этим лично, ведь оно обнаружилось во время его работы в верхней части запуска проверки памяти.

Одна из причин, по которой вы можете отрендерить этот остров менее чем за 10 000 строк возможность писать компактный код на Swift. Один из таких примеров списки параметров. В PBRT вы можете прикреплять произвольные параметры к объектам, что приводит к примерно 1000 строкам кода в paramset.[h|cpp]. В Swift вы можете добиться того же примерно за три строчки:

protocol Parameter {}

extension Array: Parameter {}

typealias ParameterDictionary = Dictionary<String, Parameter>

На самом деле, это, конечно, не совсем так, но вы поняли суть. (Кроме того, скорее всего, это изменилось бы в PBRT-v4.)

О взаимодействии C ++ для поддержки Ptex и OpenEXR: Интегрирование с C ++ для Swift уже на подходе, но оно было недоступно, когда автор только начинал со всем этим заниматься, и все еще не доступно на данный момент. Поскольку он использует OpenEXR и Ptex только для чтения текстур и записи изображений, он прибег еще к extern "C". Одна карта модулей и несколько строк кода на C ++ (100 для Ptex, 82 для OpenEXR) и вас уже есть поддержка чтения и записи изображений OpenEXR и текстур Ptex.

Этот код публикуется сейчас потому, что получилось добиться его работы на Google Compute Engine с 8 виртуальными ЦП и 64 ГБ памяти, бесплатной в течение трех месяцев, поэтому, пожалуйста, загрузите код и получите учетную запись, чтобы запустить его. Тем не менее, предстоит еще многое сделать, поскольку сейчас он оптимизирован только для получения одного изображения. Ниже приводится большой список задач, отсортированный от легко реализуемых до крупных проектов, за которые автор мог бы или не мог бы взяться в будущем.

Список дел

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

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

  • Ускоренный парсинг: нужно встроить быстрый синтаксический анализатор pbrt Инго Вальда, который анализировал бы Моану за секунды, а не за полчаса. Или даже лучше: написать самому парсер для формата pbf на Swift.

  • Ускоренное создание иерархии: сейчас оно происходит медленнее, чем хотелось бы. Что с этим можно сделать?

  • Идея об ускоренном парсинге, генерации иерархии и форматах сцены. LLVM имеет три различных формата битового кода: in-memory, machine readable (двоичный) и human readable, и он может без потерь преобразовывать их между собой. Может ли у нас происходить то же самое? Подобно PBRT (читаемому человеком), PBF или USD (машиночитаемому) и BHF (формату двоичной иерархии), где ограничивающие иерархии уже созданы и могут быть просто отображены в памяти.

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

  • Bump mapping: должно оказаться довольно просто.

  • Displacement mapping: не так просто.

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

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

  • Шумоподавление: сейчас используется OpenImageDenoise, но, конечно, было бы неплохо иметь встроенный шумоподавитель в Swift. Кроме того, beauty, albedo и normal image сейчас пишутся отдельно, и это нужно переделать.

  • USD: написать парсер для Universal Scene Description Pixar.

  • Лучший сэмплинг: использовать discrepany-based sampling или multi-jittered sampling.

  • Трассировка пути: изучить PxrUnified и реализовать управляемую трассировку пути (автор уже смотрел ее, но выглядит она запутанно) и Manifold Next Event Estimation. Кажется, где-то была такая реализация, но уже забыл, где. (Эх, если бы только Weta последовала примеру Диснея и опубликовала голову Гэндальфа из той статьи!)

  • Подповерхностное рассеяние. Уже в PBRT.

  • Ускоренный рендеринг: у Embree есть трассировщик пути. Изучить его внимательно и постараться сделать Gonzales быстрее.

  • Рендеринг на ГП: достаточно объемная задача, и PBRT-v4, очевидно, делает это так же, как некоторые из упомянутых выше рендереров. Возможно, стоит просто сделать так же, как они, и использовать Optix для рендеринга на видеокарте, но лучше бы найти решение, не использующее закрытый исходный код. Это означало бы, что вам нужно реализовать свой собственный Optix. Но если посмотреть на то, как развиваются ГП и ЦП, в далеком будущем вполне возможно использовать один и тот же (Swift) код на них обоих; у вас могут быть экземпляры с 448 ЦП в облаке, а новейшие графические процессоры имеют несколько тысяч микро-ЦП, и они выглядят все более и более одинаково. Интересно, понадобится ли программирование для AVX в будущем, поскольку сейчас оно кажется все менее необходимым, ведь вы можете просто добавить больше ядер для решения проблемы. В то же время память становится все больше и больше похожей на NUMA, так что расположение ваших данных рядом с ALU становится все более важным. Может быть, однажды у нас появятся узлы рендеринга в облаке, каждый из которых будет отвечать за свою часть сцены: геометрически разбивать эту сцену и отправлять только части ее в ЦП. Затем возвращенные пересечения можно было бы просто отсортировать по значению t луча, что напоминает архитектуры сортировки по первому/среднему/последнему, такие как Chromium.

На этом пока все. Автор был бы очень рад получить комментарии о том, что можно было бы сделать лучше или более элегантно, отчеты об ошибках или даже пулл-реквесты. Также спасибо Мэтту Фарру и PBRT наиболее ценному ресурсу в известной вселенной (по крайней мере, касательно рендеринга)

Подробнее..

Самое важное с keynote-презентации WWDC21

07.06.2021 22:20:16 | Автор: admin

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

Выжимка самого важного из того, что объявили во время keynote-презентации в этом материале.

iOS 15

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

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

Также в FaceTime будет встроен портретный режим, размывающих фон за вами.

Одно из нововведений Facetime link. Это ссылки, позволяющие вести расписание ваших звонков, и делиться ими не только с владельцами iPhone, но и с пользователями Android через веб-браузер.

SharePlay позволяет слушать музыку, смотреть фильмы или просто расшарить экран вместе с другими людьми в звонке, при этом любой человек может остановить или возобновить воспроизведение медиафайла. При этом участники звонка смогут и переписываются одновременно с просмотром фильма или подслушиванием музыки. Функция будет поддерживаться множеством приложений, таких как HBOMax, Twich, TikTok, Disney+ и другими.

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

Новая функция Shared with you дает возможность закрепить то, что вам присылают в iMessage, и что вы хотели бы посмотреть это позже.

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

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

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

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

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

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

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

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

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

AirPods и Apple Music

AirPods Pro теперь смогут фокусироваться и усиливать голос говорящего рядом человека и тем самым работать как слуховой аппарат.

Siri сможет зачитывать важные уведомления через наушники.

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

Добавлено пространственное звучание на всех девайсах и на tvOS.

iPadOS 15

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

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

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

В Заметках появились упоминания и тэги.

Появилась функция Быстрых заметок для записи важной информации. Они будут собираться в отдельном хабе, и их можно создавать как на iPadOS, так и MacOS.

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

С помощью Swift Playgrounds теперь на iPad можно писать собственные приложения для iPhone и iPad.

Конфиденциальность, Siri и iCloud

Появилась защита конфиденциальности в Почте, которая может скрывать ваш IP-адрес или местоположение, а также время открытия письма. Также ваш IP не смогут отслеживать и трекеры в Safari.

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

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

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

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

Еще одна функция скрытие электронного адреса, а также встроенная поддержка HomeKit Secure Video.

Здоровье

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

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

watchOS 8

Обновленное приложение медитаций, возможность отслеживать изменения дыхания во сне и новый циферблат с 3D-портретами из фотографий, снятых на iPhone в обновленной системе watchOS 8. А также обновленное приложение для просмотра снимков, более похожее на таковое в смартфонах, и улучшенный набор текста с возможностью удаления слов и редактирования.

Умный дом

Теперь Apple TV сможет открывать ссылки на сериалы и фильмы, которые присылают пользователю в сообщениях.

HomePod mini можно будет использовать как динамики для телевизора.

В Apple TV+ появятся профили пользователей с возможностью отслеживать прогресс по просмотру сериалов.

Обновление получит и приложение Дом для Apple Watch. Теперь с его помощью можно будет следить за глазком и еще быстрее управлять функциями дома. А камеры смогут присылать пользователю уведомления о полученной посылке.

macOS Monterey

MacOS, как и iOS, тоже получит поддержку SharePlay для совместного просмотра фильмов, а также обновленные Сообщения, Фокусировку и Заметки.

Новая функциональность Universal Control, позволяющая управлять iPad и Mac с помощью одних и тех же клавиатуры и мыши. Так, если iPad поставить рядом с MacBook, курсор сможет автоматически перейти на экран планшета. Это относится как к управлению мышью, так и тачпадом. Таким образом можно переносить файлы с устройства на устройство или, например, использовать iMac в качестве динамиков для iPhone.

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

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

Технологии для разработчиков

Представлены новые API для изоляции голоса, Focus и SharePlay, а также функции для сканирования 3D-объектов для последующего переноса в AR.

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

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

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

TestFlight появится на Mac, и его закрытый тест начнется сразу на WWDC.


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

Подробнее..

Xcode Cloud, SharePlay, Focus самое важное с Keynote WWDC21

08.06.2021 00:14:55 | Автор: admin

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

Выжимка самого важного из того, что объявили во время keynote-презентации в этом материале.

iOS 15

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

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

Также в FaceTime будет встроен портретный режим, размывающих фон за вами.

Одно из нововведений Facetime link. Это ссылки, позволяющие вести расписание ваших звонков, и делиться ими не только с владельцами iPhone, но и с пользователями Android через веб-браузер.

SharePlay позволяет слушать музыку, смотреть фильмы или просто расшарить экран вместе с другими людьми в звонке, при этом любой человек может остановить или возобновить воспроизведение медиафайла. При этом участники звонка смогут и переписываются одновременно с просмотром фильма или подслушиванием музыки. Функция будет поддерживаться множеством приложений, таких как HBOMax, Twich, TikTok, Disney+ и другими.

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

Новая функция Shared with you дает возможность закрепить то, что вам присылают в iMessage, и что вы хотели бы посмотреть это позже.

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

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

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

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

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

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

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

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

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

AirPods и Apple Music

AirPods Pro теперь смогут фокусироваться и усиливать голос говорящего рядом человека и тем самым работать как слуховой аппарат.

Siri сможет зачитывать важные уведомления через наушники.

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

Добавлено пространственное звучание на всех девайсах и на tvOS.

iPadOS 15

Переработаны виджеты: теперь их можно размещать и на экране Домой, а не только в центре уведомлений.

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

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

В Заметках появились упоминания и тэги.

Появилась функция Быстрых заметок для записи важной информации. Они будут собираться в отдельном хабе, и их можно создавать как на iPadOS, так и MacOS.

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

С помощью Swift Playgrounds теперь на iPad можно писать собственные приложения для iPhone и iPad.

Конфиденциальность, Siri и iCloud

Появилась защита конфиденциальности в Почте, которая может скрывать ваш IP-адрес или местоположение, а также время открытия письма. Также ваш IP не смогут отслеживать и трекеры в Safari.

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

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

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

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

Еще одна функция скрытие электронного адреса, а также встроенная поддержка HomeKit Secure Video.

Здоровье

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

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

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

watchOS 8

Обновленное приложение медитаций, возможность отслеживать изменения дыхания во сне и новый циферблат с 3D-портретами из фотографий, снятых на iPhone в обновленной системе watchOS 8. А также обновленное приложение для просмотра снимков, более похожее на таковое в смартфонах, и улучшенный набор текста с возможностью удаления слов и редактирования.

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

Умный дом

Теперь Apple TV сможет открывать ссылки на сериалы и фильмы, которые присылают пользователю в сообщениях.

HomePod mini можно будет использовать как динамики для телевизора.

В Apple TV+ появятся профили пользователей с возможностью отслеживать прогресс по просмотру сериалов.

Обновление получит и приложение Дом для Apple Watch. Теперь с его помощью можно будет следить за глазком и еще быстрее управлять функциями дома. А камеры смогут присылать пользователю уведомления о полученной посылке.

macOS Monterey

MacOS, как и iOS, тоже получит поддержку SharePlay для совместного просмотра фильмов, а также обновленные Сообщения, Фокусировку и Заметки.

Новая функциональность Universal Control, позволяющая управлять iPad и Mac с помощью одних и тех же клавиатуры и мыши. Так, если iPad поставить рядом с MacBook, курсор сможет автоматически перейти на экран планшета. Это относится как к управлению мышью, так и тачпадом. Таким образом можно переносить файлы с устройства на устройство или, например, использовать iMac в качестве динамиков для iPhone.

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

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

Новинки для разработчиков

Представлены новые API для изоляции голоса, Focus и SharePlay, а также функции для сканирования 3D-объектов для последующего переноса в AR.

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

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

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

TestFlight появится на Mac, и его закрытый тест начнется уже на WWDC.


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

Подробнее..

Meta Gameplay Framework, или бэкенд без серверных разработчиков

12.11.2020 12:19:49 | Автор: admin


Привет! Меня зовут Кирилл, я руководитель отдела серверной разработки в Pixonic. Здесь я работаю уже более 5 лет. Долгое время Pixonic была компанией одной игры War Robots. Но однажды к нам пришло осознание, что так больше продолжаться не может, и мы начали работу над созданием новых проектов.

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

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

После долгих размышлений наши желания оформились в следующие требования:

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

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

  • Код пишется на JavaScript, что не дает возможности полноценного переиспользования в Unity 3D, и в любом случае приходится преобразовывать ответы сервиса в модели клиента. Получается, что разработчик должен знать два языка программирования и выполнять дополнительную работу, которой хотелось бы избежать.
  • Наша цель разработка хита, а значит речь идет о миллионах игроков в день. Стоимость работы этих сервисов на наших объемах становится значительно больше, чем стоимость самостоятельного владения подобным решением.

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

  • C# (.net core для сервера и клиента, .net 3.5, 4.X для клиента). Мы хотим, чтобы разработчик мог разрабатывать как клиентскую, так и серверную часть задачи. Уйти от Unity 3D мы не можем, а вот написать сервер на C# вполне.
  • Orleans фреймворк для построения распределенных, отказоустойчивых и масштабируемых систем в модели акторов от Microsoft (использовался в Halo). Использование этого фреймворка обусловлено тем, что с нашими задачами рано или поздно придется масштабироваться к тому же, хочется, чтобы решение было отказоустойчивым.
  • GRPC для общения сервисов между собой, так как в системе, кроме сервиса игроков, построенного на Orleans, существуют и другие: авторизация, загрузка каталогов и прочее в том числе и сервисы, которые ранее были написаны на Java и оказались по-настоящему автономны и независимы от того проекта, в котором используются.
  • Postgres для хранения данных игроков. Для масштабирования базы данных мы используем шардирование.
  • Docker с ним удобно разворачивать окружение локально и в тестовой среде. Таким образом, геймдизайнеры и разработчики могут работать с метой так, чтобы никому не мешать. Можно у себя ее локально поднять, проверить, что все работает, как нужно, и запушить уже измененный код в репозиторий.
  • Prometheus для мониторинга.
  • Event Sourcing парадигма, которую мы используем для хранения данных игроков. Она часто используется в банках. Подход здесь такой: когда мы работаем через Event Sourcing, все, что мы делаем, представлено в виде потока событий. Как упоминалось ранее, хотелось бы постоянно иметь историю, которая сохраняется в базе данных. Этот поток событий и есть наша история, по которой мы можем отслеживать, что происходило. Если что-то пошло не так, мы можем посмотреть интересующее нас событие в прошлом и полностью восстановить состояние игрока.

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

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

  • вкладки конфигурирования игровых предметов;
  • вкладки настройки экспериментов (A/B тестов);
  • вкладки настройки лутбоксов;
  • вкладки настройки игровых валют;
  • вкладки для хранения простых настроек, заданных в виде ключ-значение.



Пример конфигурирования предмета

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

// Находим в каталоге предмет с идентификатором 'Reaver_1':ItemBlueprint itemBlueprint = catalog.GetItemBlueprint(ItemBlueprint.ValueOf("Reaver_1")); // Получаем проверенное значение из колонки с названием 'grade'. // Если поле отсутствует или имеет неверный тип, далее этот результат будет передан // в админку, где будет выведена информация о месте нахождения ошибки:Validated<int> grade = itemBlueprint.ShouldHasIntAttr("grade");

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

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

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

  • Лутбоксы. Сейчас без них немыслима ни одна free-to-play игра. Наше решение позволяет задавать различные варианты генерации контента для выдачи: от 100% гарантированного в этом случае лутбоксы можно использовать как обычные контейнеры, до полностью случайного.

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

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

// Описываем команду, представляющую из себя обычную DTO,// которая может быть сериализована в Json:public class DemoCommand : ICommand{    public string BlueprintId;    public int Value;} // Описываем обработчик команды:public class DemoHandler : HandlerBase, ICommandHandler<DemoCommand>{    public void Handle(ICommandContext context, DemoCommand command)    {        // Inventory  объявлен в HandlerBase.        // Создаем новый предмет по образцу из каталога:        var demoItem = Inventory.GrantItem(ItemBlueprintId.ValueOf(command.BlueprintId));         // Задаем предмету значение атрибута 'demo_value':        demoItem.Attributes["demo_value"] = command.Value;     }}

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

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

// Выполняем команду на сервере:var command = new DemoCommand { BlueprintId = "Reaver_1", Value = 777 };var commandResult = connection.Execute(command); // Из ответа получаем обновленный инвентарь игрока:var inventory = commandResult.Player.Inventory; // Получаем последний созданный предмет:var demoItem = inventory.FindItemsByBlueprint(ItemBlueprintId.ValueOf("Reaver_1")).Last(); // Выводим установленное значение:Console.WriteLine(demoItem.Attributes["demo_value"]);

Так при чем здесь Event Sourcing?

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

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

Ниже приведена упрощенная диаграмма того, что происходит при взаимодействии клиента и сервера:



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

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


Пример лога транзакций из реального проекта

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

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

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

О важности левел-дизайна, или как не сломать кор игры плохим лэйаутом

26.01.2021 10:19:42 | Автор: admin

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

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

Для начала немного об игре. War Robots это мобильный сессионный PvP-шутер про огромных роботов. Игроки здесь делятся на две команды и сражаются в одном из режимов: захват маяков или дезматч. Кор-геймплей формируют следующие механики:

  • Стрельба;

  • Перемещение;

  • Режимы боя.

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

Особенности стрельбы

Автоматическая вертикальная наводка

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

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

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

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

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

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

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

Еще одна проблема автоматической наводки положение точки сведения отличается от расположения пушек.

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

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

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

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

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

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

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

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

Самонаводящиеся ракеты и урон по площади

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

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

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

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

Разная дистанция стрельбы

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

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

Другой пример карта Factory. На ней есть зона для ближней (красная рамка) и средней (зеленая рамка) дистанции боя. То есть, акцент сделан только на две дистанции из трех. И так со всеми нашими картами.

Почему мы не используем три дистанции на каждой карте? Это обусловлено размером карт и совокупностью механик, которые не позволяют нам этот размер увеличить. Бой длится 10 минут, в нем участвует 12 игроков. Увеличивая размер карты, мы не сможем равномерно распределить геймплей по всем ее зонам и при этом сохранить длительность боя в пределах 10 минут.

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

Особенности перемещения

Управление

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

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

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

Низкая скорость

Большинство наших роботов имеет относительно низкую скорость в сравнении с дальностью пушек. Это повлияло на типовые размеры карт, предрасположенные к определенным дистанциям боя. Так, у нас есть маленькие карты для ближней дистанции боя (800х800 м), средние для средней (1000х1000 м) и большие для дальней (1200х1200 м). При этом карты максимального размера многие игроки уже воспринимают как слишком большие: они успевают заскучать на них, пока идут от спавна к точке столкновения.

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

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

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

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

Во-вторых, мы упрощаем заходы на возвышенности.

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

В-третьих, мы следим за частыми развязками движения.

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

Размеры роботов

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

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

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

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

Чтобы новые Титаны и роботы не испытывали проблем с перемещением в дальнейшем, мы ввели метрики высоты и ширины роботов: чтобы обычный робот помещался в низком проходе, он должен быть не выше 25 метров. В свою очередь, чтобы Титан там точно не застрял, он должен быть не ниже 28,5 метров.

Прыжки и полет

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

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

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

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

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

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

Килл-зоны на картах

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

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

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

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

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

Особенности режимов

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

Domination

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

  • На карте 5 маяков;

  • 2 команды по 6 игроков;

  • У каждой команды своя база со спавнами;

  • Контроль маяков приносит команде очки;

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

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

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

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

Зоны спавна команд мы размещаем по одной для каждой команды в разных концах карты.

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

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

Beacon Rush

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

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

  • Сократилось время сближения с врагом: игрокам теперь не нужно долго идти от стартового спавна до точки столкновения;

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

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

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

Team Deathmatch

Переходим к режимам на убийство игроков. Первый из них Team Deathmatch, классический режим для шутеров. Его особенности:

  • 2 команды по 6 игроков;

  • Цель уничтожение вражеских роботов;

  • Побеждает команда, уничтожившая всех роботов противника.

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

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

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

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

Free for All

Последний режим каждый сам за себя:

  • всего 6 игроков;

  • цель уничтожить вражеских роботов;

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

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

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

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

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


Итак, мы рассмотрели влияние на левел-дизайн основных механик кор-геймплея игры War Robots: стрельбы, перемещения, режимов боя. Учитывая их особенности при построении лэйаута, мы поддерживаем и дополняем механики игры, на выходе получая интересный геймплей на картах.

Подробнее..

Ремастеринг игрового контента, или как создать 800 единиц контента за семь месяцев

17.05.2021 12:14:39 | Автор: admin

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

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

Организация технического процесса работы над ремастером

Разработка War Robots ведется в системе управления версиями git по модели git-flow. В проекте есть основная ветка (develop), в которую по мере готовности вливаются ветки фичей (features). Из основной ветки создаются релизы, которые затем уходят в прод.

В эту схему мы добавили три элемента:

  1. Ветку Remastered-develop, содержащую весь актуальный контент для проекта War Robots Remastered. Мы договорились о том, что эта ветка будет обратно совместимой с актуальной основной веткой игры. Это значит, что в этой ветке будут использоваться те же технологии и все фичи из ветки develop.

  2. Ветки фичей для ремастера: Remastered/feature/branch. В этих ветках ведется работа над фичами, относящимися к контенту и графическому пайплайну ремастера. Они создаются от Remastered-develop и вливаются в нее же.

  3. Ветки фичей для поддержания обратной совместимости feature/branch: в этих ветках ведется работа над технологиями, необходимыми в первую очередь для ремастера, но несовместимыми с основной веткой War Robots. К таким фичам относятся система загрузки ассетов, система управления качеством (Quality Manager) и т. д.

Процесс работы получался следующим:

  1. В Remastered-develop постоянно заливалась ветка develop с актуальным кодом и контентом основного проекта;

  2. Геймдизайнеры, художники и графические программисты, работающие над контентом для ремастера, работали в ветках Remastered/feature/branch;

  3. Все новые технологии, ломающие обратную совместимость, сначала попадали в develop War Robots, а потом уже в develop War Robots Remastered.

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

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

Как мы переделывали контент для трех качеств ремастера и чем нам помог переход на Unity 2018

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

За свою семилетнюю историю War Robots успела обрасти множеством фичей и еще большим количеством контента. К моменту релиза ремастера в игре существовали:

  • 81 робот;

  • 109 пушек;

  • 83 единиц эквипа: щиты, модули, встроенные абилки;

  • 10 дронов.

Весь контент необходимо было пересобрать в Unity: обновить материалы, анимации, VFX и подготовить все это в трех качествах: Ultra Low Definition (ULD), Low Definition (LD), High Definition (HD). А после пересборки еще и протестировать.

Итого мы имеем: 81 робота, 109 пушек, 83 эквипа, 10 дронов в трех качествах 849 единицы контента.

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

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

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

Нам необходимо было автоматизировать процесс сборки контента и облегчить процесс тестирования. К счастью, в момент старта проекта War Robots Remastered в релиз-плане ванильной War Robots был запланирован переход на новую на тот момент версию Unity 2018 LTS. Эта версия Unity добавляла в движок новую технологию Prefab Variants, которой мы и решили воспользоваться.

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

Для примера базовый префаб робота Cossack и его HD-префаб вариант:

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

Схема разделения префабов:

Заметим, что на схеме отображено четыре качества, а не три: Legacy, ULD, LD и HD. Качеством Legacy было принято называть контент из основного проекта War Robots. Также сам механизм разделения на базовые (base) префаб-варианты стал одной из фичей основной игры по поддержке обратной совместимости с War Robots Remastered.

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

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

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

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

Как уже упоминалось ранее, создание нового префаба робота в War Robots может занимать от 1 до 3 дней работы геймдизайнера и это очень много, когда дело касается более 80 префабов роботов. Однако, благодаря общей базе и префаб-вариантам нам уже не нужно было создавать робота целиком необходимо было лишь заменить ему визуальные компоненты.

Префаб-варианты качеств ULD, LD и HD отличаются между собой всего несколькими элементами:

  • материалом (шейдером и набором текстур);

  • набором LOD-ов;

  • структурой VFX (набором систем частиц).

Замена этих компонентов легко поддается автоматизации.

HD, LD и ULD-вариант робота Griffin. Можно заметить различия в детализации и прорисовке тенейHD, LD и ULD-вариант робота Griffin. Можно заметить различия в детализации и прорисовке тенейHD, LD и ULD-вариант робота CossackHD, LD и ULD-вариант робота Cossack

Однако изначально у нас было только одно качество: Legacy, которое мы получили на выходе работы инструмента по разделению на префаб-варианты. Legacy-качество отличалось от ремастерных качеств, помимо прочего, еще и структурой скелета и анимациями. И если замену материалов, лодов и VFX-ов мы легко могли автоматизировать, то замена скелета анимаций требовала ручных усилий от геймдизайнеров для настройки новых точек креплений пушек, VFX и т. д. В результате мы создали два инструмента для геймдизайнеров: утилиту по портации префаба Legacy в качество ремастера и инструмент для генерации качеств HD, LD, ULD из готового качества ремастера.

Процесс создания ремастер-контента со стороны геймдизайнера теперь стал разделяться на следующие этапы:

  1. Геймдизайнер использует инструмент для портации Legacy-качества в ремастер-качество (обычно в LD). Этот инструмент заменяет скелет, анимации, материалы и VFX.

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

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

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

Помимо этого, мы получили инструмент, который позволит нам в дальнейшем интегрировать ассеты любых качеств в пару кликов мыши например, Medium Definition (MD) и Ultra High Definition (UHD).

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

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

  • настройки освещения рендеров;

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

  • наличие самих префабов в билде;

  • настройки лодирования.

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

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

Подводя итоги: какие практики хороши при переделке контента для крупного игрового проекта

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

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

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

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

Подробнее..

Тримы, тайлы и террейн как происходит ремастеринг игровых игровых карт

01.06.2021 12:20:22 | Автор: admin

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

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

Иными словами, нам нужно было:

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

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

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

Концепт-арт: формируем стиль обновленной игры

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

Для примера возьмем центральное здание карты Valley. Ниже показан оригинальный концепт и его реализация в ванильной версии War Robots:

А теперь посмотрим на концепт и 3D-модель той же самой постройки в War Robots Remastered:

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

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

Пример доски планирования текстурных массивов. Здесь мы видим сопоставление материалов на зданиях к слоям (0-15) в массиве текстур по цвету вершины.Пример доски планирования текстурных массивов. Здесь мы видим сопоставление материалов на зданиях к слоям (0-15) в массиве текстур по цвету вершины.

Быстрее, больше, мощнее: изменения в техническом пайплайне

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

По части кода мы переписали практически весь пайплайн рендеринга (Scriptable Render Pipeline, SRP), и теперь вместо классического рендера материалов (diffuse/normal/specular) у нас используется современный физически корректный рендеринг PBR (albedo/metallic/normal/smoothness) для более точной передачи физических свойств материалов. Но об этом мы еще расскажем подробнее в будущих статьях.

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

Что такое тайлы и тримы?

  • Тайловая текстура (Tile sheet) однородная текстура, которая может повторяться n-ое количество раз как по горизонтали, так и по вертикали без каких-либо видимых швов;

  • Трим-текстура (Trim sheet) тот же тайл, но повторяющийся в одном направлении: либо горизонтально, либо вертикально.

Основные ограничения по текстурам, накладываемые памятью лоу-энд девайсов, это один, максимум два массива из 16 текстур с одинаковым разрешением 512512px на основную геометрию уровня, которую можно и нужно текстурировать тайлами и тримами. Для HD-пресета мы можем использовать разрешение и 10241024px с более прогрессивными алгоритмами компрессии и это почти не увеличивает потребление памяти, но по факту визуально разница на экранах мобильных устройств невелика. Обычно мы используем по одному массиву текстур на геометрию уровня и для террейна, использующему каждый свои шейдеры для специфических задач. Например, на индустриальных объектах не нужен такой плавный переход между текстурами, как на террейне. Вообще любые плавные бленды сложнее по просчету для GPU, поэтому шейдер террейна мы используем только на природных объектах.

Атлас для карты Canyon Legacy (20482048px)Атлас для карты Canyon Legacy (20482048px)Набор из 16 текстур для карты Canyon Remastered (512512px)Набор из 16 текстур для карты Canyon Remastered (512512px)

Что касается полигонажа, здесь мы тоже шагнули далеко вперед: теперь для HD-пресета качества в нашем распоряжении до 500 тысяч треугольников, тогда как раньше было максимум 200-300 тысяч.

Раз уж речь зашла о террейнах

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

Итак, типичная игровая карта в War Robots состоит из:

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

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

Пример игровой статики здание с локации ValleyПример игровой статики здание с локации Valley
  • Крупной неигровой статики. Сюда относятся объекты, влияющие на визуальное восприятие локации, но не участвующие в игровом процессе и не допускающие физического взаимодействия: скалы в окружении локации, осветительные фермы за границей геймплейной зоны и т. д. Эти объекты нельзя отключить для увеличения производительности на слабых устройствах, а значит, они нуждаются в лодировании процессе подмены объектов разного уровня детализации (LOD) в зависимости удаленности их от игровой камеры.

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

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

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

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

А теперь, наконец, перейдем к основной артовой части.

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

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

Ниже представлен один из концептов для карты Canyon:

Что мы здесь видим?

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

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

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

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

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

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

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

Один из примеров атласов для карты PowerplantОдин из примеров атласов для карты Powerplant

Общую плотность текселей плотность пикселей текстуры на единицу масштаба объекта мы решили использовать в районе 128 px/m2. Этого вполне достаточно, чтобы прорисовать даже мелкие детали на дверях, окнах, решетках и подобных элементах, которые не поплывут после сжатия до 512512px ETC2.

Обычно мы создаем текстуры в связке 3ds Max/Maya + Substance Painter или Substance Designer + Substance Painter, но некоторые заимствуем из библиотеки Megascan от Quixel и редактируем под наши нужды. Пайплайн здесь несложный: моделируем геометрию в 3ds Max или Maya, разбиваем ее на ID Color, а потом запекаем все карты в Marmoset или в Substance Painter. После этого вся работа ведется уже в Substance Painter.

Другой вариант: в Substance Designer получаем все необходимые карты (height, normal, AO, curvature) и выгружаем в Substance Painter для текстурирования. А иногда текстурируем и сразу в Substance Designer кому как удобнее.

Моделирование и маппинг основной геометрии уровня

Как только готов набор текстур, можно приступать к моделированию основной геометрии уровня. Для этого мы берем *.obj-файл концепт-болванки (если есть), делаем из него оптимизированное лоу-поли, стараясь сохранить интересный силуэт. При этом следим за объемом, чтобы здание не выглядело слишком плоским и скучным. Где нужно, добавляем дополнительную геометрию с небольшими скосами. Там, где можно обойтись просто тримом нарезаем геометрию на полоски и мапим уже на них текстуры. Хотя зачастую при использовании технологии тайл- и трим-текстур это и не нужно: модель создается одновременно с текстурированием.

Уже знакомое нам главное здание с карты ValleyУже знакомое нам главное здание с карты Valley

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

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

В целом полигонаж на уровни детализации определяется следующим образом:

Здесь можно увидеть, как отличаются LOD между собой для здания с карты Powerplant, и исчезают мелкие деталиЗдесь можно увидеть, как отличаются LOD между собой для здания с карты Powerplant, и исчезают мелкие детали

В HD-пресете у нас используются только LOD0 и LOD1, в LD LOD3 и LOD2 как базовый меш, в ULD только LOD3 в качестве базового меша.

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

Наряду с постройками ведутся работы и по террейну. Базовый меш создается в World Machine или World Creator. Там же генерируются маски для SplatMap RGBA текстуры, показывающей, где какой тайл прорисовывать на геометрии террейна.

SplatMap для террейна Powerplant. В данном случае в канале R используется трава, G почва, B скальная порода, A песокSplatMap для террейна Powerplant. В данном случае в канале R используется трава, G почва, B скальная порода, A песок

Меш, как правило, впоследствии дорабатывается и оптимизируется руками, а SplatMap дорисовывается в Substance Painter или уже непосредственно в Unity с помощью плагина Splat Painter.

Так выглядит террейн с примененной SplatMap и массивом из четырех текстурТак выглядит террейн с примененной SplatMap и массивом из четырех текстур

Перед выгрузкой *.fbx в Unity мы назначаем цвета вершин на полигоны по ID в соответствии с нашей рабочей таблицей:

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

Одно из зданий на карте Powerplant: слева старое, справа новоеОдно из зданий на карте Powerplant: слева старое, справа новое

От доработки до релиза финальные этапы работы

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

Вот так выглядит карта до запекания света и заполнения объектами детализации:

А так после финальной полировки:

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

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

Так выглядят HD, LD и ULD пресеты карты:

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

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

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

До/после: карта из ванильной War Robots и War Robots Remastered:

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

  • Переход на новый графический пайплайн (SRP) позволил нам использовать PBR текстуры вместо классической модели diffuse/normal/specular. Это упрощает создание текстур, поскольку современные пакеты вроде Substance Painter/Designer, а также Quixel заточены именно под PBR.

  • Использование текстурных массивов (Texture Array) и тримов/тайлов предоставляет нам возможность более эффективно расходовать память на девайсе при текстурировании большого количества крупных однотипных моделей. Это также сокращает время создания более детализированных объектов.

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

  • Самописные инструментарии вроде Splat Painter упрощают рисование Splat-масок для террейна и не только.

  • Кастомные шейдеры и материалы под SRP дают возможность воплотить весь творческий потенциал, заложенный на этапе препродакшена карты.

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

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

Автор материала старший 3D-художник Pixonic Александр Коляда, также помогал Алексей Пастушков

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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