В этой статье мы расскажем о том, как вместе с «движковой» командой разработали наш внутренний редактор ICS и упростили процесс работы с проигрыванием кат-сцен. Иерархический объект (игровой объект) — объект, который может содержать элементы, являющиеся иерархическими объектами (дочерние объекты). Такой объект представляет собой дерево. У такого объекта есть трансформация (положение, ориентация, масштаб) относительно родительского объекта. Компонент (behaviour) — то, что определяет вид и поведение игрового объекта. Это может быть картинка, анимация, меняющая трансформацию объекта и т. д. Всё началось с того, что мы столкнулись с необходимостью проигрывания сценариев. В нашем случае под сценарием мы понимаем последовательность действий — это может быть затемнение экрана, появление текста, появление на экране персонажей или что-то похожее. Из таких действий нам нужно было собрать сценку, при этом некоторые действия должны были запускаться одновременно (например, проигрывание анимации персонажа и воспроизведение какого-нибудь эффекта), а после завершения всех этих действий (а продолжительность может быть разной) должно было происходить что-то ещё. Нам нужно было выстраивать такие действия друг за другом или параллельно, то есть организовывать их в виде графа. Параллельные цепочки должны были завершиться прежде, чем запустится следующее действие. Рассмотрим это на примере графа, представленного на картинке выше. Запускается действие 1. После завершения действия 1 одновременно запускаются действия 2 и 4. После завершения действия 2 запускается действие 3. Действие 5 запускается только после завершения действий 3 и 4. После завершения действия 5-я сценка заканчивается. Чтобы решить эту задачу, мы придумали редактор, в котором нужно было строить граф. В этом графе блоки, представляющие собой действия, соединялись связями. Такой редактор было сделать несложно, потому что мы уже решали похожую задачу. В результате мы разработали первую версию проигрывателя и редактора сценок, которые мы назвали кат-сценами. В таких сценках игрок должен иметь возможность прочитать текст, то есть нужно было приостанавливать сценку и ждать от пользователя какого-то действия. И это действие мы решили рассматривать точно так же, как и другие действия, то есть к нашему существующему набору действий (показать текст и т. д.) добавилось ещё одно — дождаться ввода от пользователя. И здесь в нашей системе — интерактивная кат-сцена (ICS). Добавление интерактивности позволило существенно расширить область применения этой системы. Мы получили довольно мощный инструмент для эффективного решения задач взаимодействия с интерфейсом, на которые раньше уходило много времени. Кроме работы с интерфейсами ICS нашёл множество других применений. Впоследствии для решения различных задач мы реализовали в ICS условные развилки, циклы, работу с переменными, промежуточные вычисления и многое другое. Interactive Cut Scenes (ICS) — это средство, предназначенное для создания и проигрывания интерактивных сцен, в соответствии с которыми в игре будут происходить различные события (при необходимости по заданным условиям). Они могут выполняться последовательно или параллельно, в зависимости от того, как мы соберём граф. Для создания сцен есть специальный редактор, в котором можно создать граф, определяющий порядок запуска действий. Для проигрывания используется специальный компонент (ICS controller behaviour), который добавляется к игровому объекту и управляет с помощью графа ICS его составляющими (другими компонентами, дочерними элементами и прочим). Рассмотрим подробнее устройство сцены ICS. Для того чтобы работать с ICS, нужно добавить проигрыватель ICS (ICS controller behaviour) к какому-нибудь игровому объекту. Этот объект содержит в себе интересующие нас элементы (такие как текстовая панель, фон и т. д.), они и будут участниками ICS (actor). В ICS есть участники, над которыми выполняются действия. Участник ICS — это игровой объект. Есть разные способы определения участника, один из них — это выбор дочернего элемента по пути в иерархии игрового объекта. На центральной панели находится граф, определяющий порядок выполнения действий. Действия можно выделять, создавать, удалять, можно создавать и удалять связи между действиями. На верхней панели расположены кнопки управления: воспроизведение, пауза, остановка, сохранение и открытие файла ICS. На панели слева указаны участники сцены. Ниже на панели слева можно настраивать параметры выбранного действия. На панели справа — переменные. У нас уже был собственный редактор графов — ASG, который мы упоминали в начале. В ICS всё сделано аналогично — есть стрелки между блоками, есть панель с действиями (слева), где мы видим данные действий, которые сохраняются в файле ICS. Это также уже ранее разработанная нами система с кодогенерацией, сериализацией и т. д., о которой мы рассказывали в статье «Как мы делали нашу маленькую Unity с нуля».  То есть мы создали наш редактор ICS на основе того, что у нас уже было, но со своей спецификой. Она заключается в том, что стрелки передают управление: сейчас воспроизводится одно действие, как только это действие закончится, то оно передаёт управление следующему. Управление определяют стрелки. Данные между действиями не передаются, каждое действие выполняется независимо. Но есть и общие на весь граф данные, они содержатся в участниках, которые представляют собой игровые объекты. Какие-то данные с них можно считывать, какие-то — на них назначать. Ещё у нас есть переменные, они также видны для всех действий и являются глобальными для всей сцены. Здесь мы можем сохранять промежуточные значения или брать игровые данные. Можно сказать, что это — контекст сцены, который может меняться во время воспроизведения и зависеть от внешних условий. Тут могут быть глобальные общие игровые данные, например, количество пройденных уровней. Подробнее о переменных и игровых объектах ниже. В ICS есть довольно большой набор действий. Ниже представлены наиболее часто используемые. Действие анимации применяется к участнику сценки (игровому объекту). Оно проигрывает анимацию, которая меняет объект (меняет его параметры). Действие заканчивается при завершении анимации. Действие звука проигрывает звук на игровом объекте. Привязка звука к объекту имеет значение, когда важно положение в пространстве источника звука. Действие ожидания нажатия на кнопку также применяется к игровому объекту. Игровой объект обычно представляет собой элемент интерфейса, на этом объекте должен быть специальный компонент — кнопка. Действие завершается, если кнопка была нажата во время выполнения этого действия. Действие ожидания. Это действие будет завершено по истечении заданного времени. Время может быть задано в виде константного значения, переменной или параметром игрового объекта. Действие ожидания true. В качестве параметра этого действия устанавливается выражение, результатом которого является true или false (примером такого выражения является сравнение). Источником значений, участвующих в выражении, могут быть числа, строки, переменные, параметры игровых объектов. Действие завершается, когда результат выражения становится true. Действие выбора имеет несколько выходов. Каждый выход для определённого ответвления. Это действие составное и содержит внутри несколько действий ожидания (например, несколько действий ожидания нажатия на кнопку). Каждому внутреннему действию ожидания соответствует определённый выход. Действие присвоения значения. В этом действии есть источник (source) данных и назначение (destination). В качестве источника могут быть параметры игрового объекта, переменные, константные данные (те, которые пользователь может ввести). Назначением может быть параметр игрового объекта и переменная. Тип источника и назначения должен совпадать. Действие выполняется мгновенно. Перед вами пример сценки ICS. Здесь мы видим несколько участников: фон, персонаж, панель с текстом, красная таблетка и синяя таблетка. Рассмотрим первую часть этого примера. Зелёный блок — это точка входа, это специальное действие, которое ничего не делает, оно запускается при запуске ICS и сразу завершается. После этого запускается действие, следующее за ним — анимация появления фона. В этом действии к выбранному участнику (объекту фона) применяется анимация появления, в которой меняется прозрачность объекта. Дальше запускается действие установки значения, которое применяется к панели с текстом. Здесь задаётся нужный текст. В этот момент панель с текстом ещё не видна, и можно внести желаемые изменения. Это действие выполняется мгновенно. Затем запускается анимация появления панели с текстом. Тут всё происходит так же, как в анимации фона. Последующее появление персонажа происходит так же. В оставшейся части примера мы видим развилки, где происходит выбор маршрута, по которому пойдёт развитие сценки. Как описано выше, действие запускается только после того, как закончат своё выполнение все его предшественники. Но в случае развилок это условие невыполнимо. Поэтому мы ввели понятие «пропускания» действия.  Теперь рассмотрим расширенную концепцию обхода графа, учитывающую возможность выбора. Введём понятие сигнала, передаваемого по связям от одних действий другим. Может быть два типа сигнала: «завершено» и «пропущено». Когда действие получит сигналы от всех своих предшественников, оно может быть либо запущено, либо пропущено. Если все предшественники были пропущены, то это действие пропускается и передаёт сигнал о том, что оно пропущено, следующим за ним действиям (их может быть несколько). Если хоть одно из действий-предшественников было завершено (не пропущено), то это действие начинает выполнение. При завершении действие передаёт своим действиям-последователям сигнал о завершении. Рассмотрим это подробнее на простом графе с развилкой. На рисунке выше изображён граф с развилкой. Сначала запускается и выполняется действие 1. После завершения действия 1 запускается действие 2. Из него стрелки выходят сверху и снизу (а не сбоку, как раньше) для обозначения развилки. При завершении этого действия происходит выбор маршрута. Предположим, что выбор был сделан в пользу действия 3. Действие 2 отправляет сигнал «завершено» действию 3 и сигнал «пропущено» действию 4. У действия 3 всего один предшественник, и он закончил выполнение. Поэтому действие 3 запускается. Действие 4 пропускается, так как все его предшественники были пропущены. Действие 5 дожидается сигналов от обоих предшественников. И один из них — «завершено», поэтому оно запускается. Действие 5 — это место схождения маршрутов. Теперь, возвращаясь к нашему примеру с выбором, мы можем понять, что там происходит. После завершения цепочки анимаций появления разных объектов сцены мы доходим до точки выбора. Это действие ждёт от пользователя нажатия на одну из кнопок. Если пользователь нажмёт на красную кнопку, мы пойдём по верхнему маршруту, если по синей, то по нижнему. В верхнем маршруте происходит смена текста в текстовой панели, затем предлагается тот же выбор (игрока переспрашивают, действительно ли он хочет выбрать красную таблетку). Если игрок настаивает на своём выборе и снова жмёт на красную кнопку, в панели устанавливается соответствующий текст, после чего мы приходим к точке схода всех маршрутов, от которой сценарий будет проходить независимо от принятых ранее решений. Для индикации пропущенные действия меняют свой цвет на чёрный. До сих пор мы рассматривали графы, в которых все действия выполнялись не более одного раза. С помощью представленной системы можно запускать цепочки действий с ветвлениями и выбором пути. В представленном примере пользователю дважды представляется один выбор, для этого в графе продублированы некоторые действия. Дублирование здесь оправдано, но если понадобится сделать этот повтор много раз, то придётся построить довольно сложный граф, в котором легко запутаться. А случай с неограниченным числом повторений вообще невозможно реализовать. Это привело к необходимости реализации циклов. На первый взгляд кажется, что нет ничего сложного — просто добавляем переход от конца к началу. Но это приведёт к тому, что в точке входа в цикл воспроизведение зависнет, то есть процесс воспроизведения не завершится, и при этом ничего происходить не будет. Причина такого поведения в том, что действие на точке входа в цикл будет ждать сигнала о завершении или пропускании от всех предшественников, но некоторые из предшественников будут запущены только после прохождения цепочки, идущей от этого самого действия, то есть от них сигнал никогда не придёт. На самом деле не нужно ждать сигнала от всех действий. Один раз при инициализации графа мы делаем анализ, в котором с помощью специального алгоритма определяются циклы. В результате сохраняются данные о том, какие из предшественников находятся внутри цикла, а какие снаружи. И если сигнал пришёл снаружи цикла, то мы не ждём сигналов изнутри, и наоборот. Есть ситуации, которые все равно будут приводить к зависанию — это некоторые случаи смежных циклов, но их можно обойти небольшой реорганизацией графа. Рассмотрим всё это подробнее на простом примере: Сначала выполняется действие 1. После завершения действия 1 мы приходим к точке входа в цикл. У действия 2 один предшественник снаружи цикла (действие 1), другой — изнутри (действие 4). Так как сигнал приходит снаружи, мы не ждём действия 4 и начинаем выполнение. Далее запуск и выполнение действий 3, 5 и 4 происходит по известной схеме. После завершения действия 4 мы возвращаемся к точке входа. В этом случае сигнал приходит изнутри, и мы не ждём сигнала от действия 1 и начинаем выполнение действия 2. Далее всё повторяется до бесконечности. А вот так выглядит цикл, который не поддерживается проигрывателем ICS. Здесь действие 3 будет ждать сигналов от действий 2 и 6. Оба эти действия находятся внутри цикла, но 6 будет выполнено только после выполнения 3. В итоге сигнал от него никогда не придёт. Поддержка циклов существенно расширяет возможности ICS. Теперь можно реализовывать процессы с повторяющимися действиями. Это открывает довольно большие возможности. Хороший пример — это реализация логики работы интерфейса, где каждое нажатие на интерактивные элементы приводит к запуску различных эффектов и анимаций. Рассмотрим пример: Здесь одновременно работают два независимых цикла. В одном запускаются анимации появления и исчезновения окна и кнопки, в другом — запуск эффекта. Для игры Wildscapes в ICS были добавлены свои участники — пешеходы. Для них доступны специальные действия, такие как передвижение от одной точки к другой, поворот, телепортация и т. д. С помощью ICS создаются сценки, в которых пешеходы могут общаться друг с другом, помахать рукой, сделать селфи или просто свободно ходить по миру в зависимости от состояния игры или действий игрока. В ICS есть специальное действие, которое осуществляет чтение каких-либо данных из одного места и их запись в другое. Например, можно взять позицию одного объекта и назначить ее другому. Точно так же можно перенести текст, цвет и т. д. Таким образом, можно читать и записывать данные по всей иерархии объекта и его компонент. Но, кроме этого, возникает необходимость сохранять некоторое значение, менять его и записывать куда-то ещё. Бывает, что кроме промежуточных значений, нужно получать данные из игры (например, информацию об уровне игрока или количестве у него денег). Для решения этих задач в ICS был введён контекст, который представляет собой переменные или игровые данные. Эти значения имеют тип (float, int, float2, float3, color, string) и имя. Нужно заранее определить переменные и задать им начальные значения. Для вывода игровых данных нужно написать класс-наследник базового класса контекста, в котором необходимо определить методы, предоставляющие доступ к игровым данным. Игровые данные можно только читать, а переменные можно менять, при завершении сценки ICS значения переменных будут сброшены к начальным. В примере с интерфейсом, приведённом выше, задана переменная типа Int. Её значение увеличивается на 1 при каждом прохождении цикла, по достижении значения 3 ей присваивается значение 0, а в зависимости от значения выбирается путь, в котором задаётся соответствующее значение цвета эффекта. Таким образом, при каждом третьем запуске эффекта его цвет меняется. Игровые данные можно использовать в разных сценках, например, ограничивая выбор игрока в зависимости от его уровня. В ICS можно проводить арифметические (сложение, вычитание, умножение, деление), логические (и, или, не) операции и сравнение. ICS работает со значениями разных типов (целые числа, дробные числа, строки, логические, двухмерный вектор, трехмерный вектор, цвет). В зависимости от типа значения над ним можно проводить определенные операции (арифметические, логические, сравнение). В качестве значения можно использовать любой параметр игрового объекта, переменную или константу (то, что вводится в редакторе). На основе вычислений можно делать развилки. Например, если у игрока денег больше какого-то значения, то мы идем по одному пути, если меньше, то по-другому. ICS посылает события о начале воспроизведения и остановке. Кроме этого, можно создавать свои события, которые будут срабатывать во время воспроизведения. Для этого мы разработали механизм триггеров. Триггер — это событие, имеющее имя. Триггеры задаются по имени в редакторе ICS. Для запуска такого события (триггера) есть специальное действие с единственным параметром — именем триггера. Действие выполняется мгновенно и посылает событие с именем триггера. В VSO есть механизм работы с событиями (Signal subscription), который позволяет совершать какие-нибудь действия при получении ожидаемого сигнала (например, запуск анимации). Источниками событий могут быть различные компоненты, например, анимация. А в анимации есть свои события, устанавливаемые на специальном треке событий. Ещё один пример — это кнопка. ICS тоже может быть источником событий, и события в ICS — это триггеры, а также начало и завершение ICS. Кроме этого, на события можно подписываться из кода. Обобщим все, что представляет собой ICS.  ICS управляет игровыми объектами. То есть ICS меняет различные параметры игровых объектов, таких как позиция, размер, цвет, текст. ICS может реагировать на внешние события (нажатие кнопки, изменение игровых данных и т. д.). В ICS есть участники — игровые объекты, к которым применяются действия. Действия чаще всего применяются к участникам или к переменным. Действия меняют данные (это может быть движение, изменение цвета) или ожидают от них чего-то (нажатие на кнопку, изменение параметра). Действия в ICS имеют длительность и выполняются по очереди или параллельно в зависимости от того, как построен граф. В графе могут быть развилки и циклы. В ICS есть различные типы сущностей, некоторые из них могут быть дополнены собственной реализацией. Можно создавать собственные действия (Action) и собственных участников ICS (Actor). Чтобы создать своё действие, нужно создать классы-наследники от базовых классов ActionData (данные действия) и Action (реализация действия — то, что действие делает). ActionData используется для отображения в редакторе параметров действия, а Action — это сама реализация, здесь нужно определить, что делается при запуске действия и что делается при обновлении (за один кадр). Например, мы можем создать действие, которое будет двигать объект из одной точки в другую. У неё будет два параметра — координаты первой точки и координаты второй точки. Функция Update будет медленно смещать объект от одной точки к другой и закончит своё выполнение, когда объект будет перемещён.  Еще есть участники, которые в базовой реализации представляют собой игровые объекты. Но если нам нужно, чтобы участник был чем-то большим, выходящим за рамки концепции игрового объекта, то мы можем сделать своего собственного участника и для него написать специфические действия, как это было в уже упомянутом проекте Wildscapes, в котором есть пешеходы. Это не игровые объекты, как в нашем движке, а некие более высокоуровневые сущности. Для них нам пришлось сделать своих участников, и над ними выполняются особые действия, такие как помахать рукой, перейти от одной точки к другой, обходя препятствия и тому подобное. Из-за интенсивного использования редактора ICS в какой-то момент мы задумались над улучшением его юзабилити. Если в первой версии ICS нужно было обязательно пользоваться левой панелью, чтобы настроить какое-то действие, в новой версии мы добавили возможность настраивать всё прямо внутри. Связи теперь можно по-разному двигать, настраивать их форму. В новой версии можно добавлять пометки, писать текст, делать рамки и подписывать их, чтобы потом, глядя на граф, было понятно, где что находится. Ещё можно делать группы — создавать блок, внутри которого делать какие-то действия и потом подключать его наружу. Таким образом, мы можем добавлять внутрь блока определённые логические структуры, чтобы улучшить читаемость.  Изначально мы разрабатывали ICS для создания кат-сцен, но со временем редактор стали использовать в разработке интерфейса и управлении игровыми персонажами. Теперь дизайнерам не приходится ждать доработок от программистов на каждом этапе выполнения задач — они могут экспериментировать и придумывать новые интересные решения, пользуясь возможностями редактора ICS.  От этого решения выиграли все — разработка ускорилась, качество улучшилось, а разработчики получили более интересные и приятные задачи.  Конечно, на этом работа не останавливается. Несмотря на все положительные стороны ICS, у него есть и свои недостатки, связанные с ограничениями, возникшими на начальном этапе, а также со спецификой первоначальной задачи. Например, в ICS нет путей передачи данных между блоками графа, а сам граф может быть использован только для выполнения действий, но не для вычислений.  В будущем мы планируем создать новый инструмент, в котором учтём эти недостатки. Кроме того, для графа будет генерироваться C++ код для более быстрой работы в релизе.  Следите за нашим блогом — и вы узнаете об этом первыми!