Найти тему
Сделай игру

Графика на службе игрового процесса

Оглавление

Вместо вступления

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

Читал, как-то, статью о парне, который сам 5 лет писал игрушку. Классная игра вышла, только вот 5 лет срок немаленький и мало кто пойдёт на это.

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

Такое поведение было мной замечено в игре Ori and the Blind Forest в некоторых местах.

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

3d графика, фантазии ИИ
3d графика, фантазии ИИ

Плавно переходя к теме

Собственно тут, я хотел поговорить о графике. Как некоторые знают, мне нравится OpenGL, вернее его web реализация - WebGL. Главным образом потому, что это действительно восхитительный способ с минимальными затратами реализовывать восхитительную графику.

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

Есть немало "обёрток" для библиотеки, которые позволяют реализовывать большую часть требуемых функций через удобные методы и вызовы (например https://threejs.org/); однако любое упрощение сводится к двум проблемам:

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

Если вы посмотрите примеры, приведённые в библиотеки three.js, на которую я сослался ранее, то можете заметить, что количество кадров в секунду там весьма невысоко (у меня было примерно 40), притом, что ничего сложного и высокопроизводительного на экране не отображалось.

Ближе к делу

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

Графику буду делать на WebGL; сперва попробую что-то типа анимации спрайтов, настройка эффектов и тому подобное.

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

Кратко о WebGL

WebGL, кто не знает, это веб-версия OpenGL для встраиваемых систем. Относительно простая в использовании, но требующая понимания ряда пунктов:

  1. WebGL - это не про 3d графику, а в том числе и про 3d графику, однако всё строится вокруг изначально концепции: посередине точка 0, а дальше +/- 1 - правый (левый) и верхний (нижний) край, но это можно менять;
  2. Требуется программа отрисовки - это набор инструкций, как и что будет отрисовываться, отображаться и в какой последовательности (меня поначалу это слово "программа" сильно сбивала с толку, фактически - это просто набор инструкций, как всё должно выглядеть);
  3. WebGL, чаще всего, рисует треугольниками (то есть все формы, которые она обрабатывает - это, по-сути, фигуры, собранные из треугольников) - это несложно понять: прямоугольник - это два треугольника;
  4. Обычно, используется две программы: фрагментная (fragment) и вершинная (vertex). Вершинная - создаёт фигуру, фрагментная - закрашивает её. Эти программы называются затенителями или шейдерами (шейдерными программами);
  5. Библиотека работает примерно так: в буфере первой программой формируется некоторая фигура, которая затем раскрашивается второй программой (под раскраской может быть как наложение цвета, так и текстур);
  6. Помимо общего рисования, есть правила ранжирования фигур (z-Index), особенностей наложения и обработки;
  7. Также существует область видимости: всё то, что вне её - отрисовано не будет;
  8. Рисовать можно и 3d объекты, однако, для этого нужно перейти в специальный режим и это, надо заметить, требует определённой сноровки, т.к. все вычисления трёхмерности - сплошная математика и почти целиком она распределяется между вашим приложением и вершинной шейдерной программой;
  9. Тени, блики и многое другое - это не магия и не встроенные свойства, это - фрагментная шейдерная программа: фактически, все те интересные визуальные эффекты, что наблюдаются в играх - не какое-то встроенное свойство, а используемые или заново изобретённые наработки;
  10. Сама по себе WebGL - больше напоминает конечный автомат: вводится некоторое количество входных данных, переключаются состояния - и вуаля, отличный кадр.

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

Параллакс в играх

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

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

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

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

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

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

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

Получилось так
Получилось так

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

  • красная - самый дальний задний фон;
  • зелёная - ближний задний фон;
  • синяя - ближайший фон;

Вот тексты шейдерных программ:

Вершинный шейдер
Вершинный шейдер
Фрагментный шейдер
Фрагментный шейдер

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

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

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

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

Движение

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

Всё движется влево, хотя кажется, будто в разные направления
Всё движется влево, хотя кажется, будто в разные направления

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

Вершинный шейдер теперь такой
Вершинный шейдер теперь такой
А фрагментный - такой
А фрагментный - такой

Текстуры

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

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

Оказалось всё довольно тривиально: рассчитать верное отображение данных бывает непросто, поэтому разработчики установили маленькое условие для текстур: их размер должен быть кратен степени двойки: 2, 4, 8, 16, 32 и так далее. В принципе, после соблюдения этого условия, всё сразу начинает работать сильно лучше (надо сказать, что есть настройки, как гласят легенды, позволяющие наложить структуру произвольного размера, однако без танцев с бубном, особенно поначалу, обойтись не удастся). В общем, я не стал устраивать себе дополнительный аттракцион - обрезал структуру до чётных размеров и всё заработало (хотя фрагментный шейдер пришлось ещё немного огорчить операцией ветвления).

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

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

Например, как сделать так, чтобы текстура размножилась? Применить какой-нибудь хитрый фильтр? О нет, это было бы слишком тривиально! Нужно указать размер текстуры в N раз больше, чем он есть с тем, потому что если размер (0,0) - (1,1) - текстура будет натянута целиком, а вот если указать (0,0) - (10,10) - то текстура будет натянутся, теоретически, 10 раз, но как она будет отображаться - решат уже другие фильтры. Впрочем, не стоит слишком сильно на них рассчитывать с первого раза, там везде "есть нюанс".

Убрать лишний цвет

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

JPEG текстуры, разумеется, меня подвели
JPEG текстуры, разумеется, меня подвели

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

Подключение альфа-канала
Подключение альфа-канала
И вот, ничего лишнего, кроме артефактов по кромке
И вот, ничего лишнего, кроме артефактов по кромке

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

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

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

Заключение

Не обошлось, к слову, без казусов: firefox обрабатывает текстуры несколько иначе, нежели chrome, что привело к тому, что ядрёно-фиолетовая подложка под вторую текстуру получила иной код цвета и не обрабатывалась как надо; пришлось срочно допиливать, но чуть-чуть.

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