Найти в Дзене
ZDG

Пишу онлайн-игру Color Lines: Рендерер, представление и эффекты

В предыдущей части более-менее закончили с эмуляцией сервера: Клиент будет сложнее, так как ему нужно обрабатывать пользовательские команды и рисовать интерфейс. Как бы ни хотелось сделать его побыстрее, необходимо структурно разделить его на следующие части: Он будет заниматься отрисовкой графических примитивов. Суть в том, что рендереры могут быть разные, от алфавитно-цифрового до OpenGL. И любой из них можно подсунуть так, чтобы не менять ничего в игре. Рендерер, независимо от реализации, должен предоставлять интерфейс для рисования графических примитивов. В нашем случае это будут четыре метода: В HTML можно отрисовать прямоугольники и шарики, используя исключительно CSS. Но такая реализация абсолютно костыльная и непереносимая на другие платформы, поэтому я остановлюсь на более традиционном рисовании пикселами на холсте. В HTML есть элемент <canvas>, получив доступ к которому, можно рисовать привычными примитивами. Соответственно рендерер будет называться CanvasRenderer: Аргументы
Оглавление

В предыдущей части более-менее закончили с эмуляцией сервера:

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

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

1. Рендерер

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

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

  • clear() – очищает область рисования
  • fillRect() – рисует закрашенный прямоугольник
  • drawImage() – рисует картинку
  • drawScaledImage() – рисует картинку с изменённым размером

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

В HTML есть элемент <canvas>, получив доступ к которому, можно рисовать привычными примитивами. Соответственно рендерер будет называться CanvasRenderer:

Аргументы w и h задают размеры рабочей области (т.е. холста), а ctx это контекст рисования, через который происходит доступ к холсту.

Реализация методов рендерера это по сути обёртка для встроенных методов контекста:

-2

2. Представление (View)

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

Представление конструируется с аргументами размеров рабочей области, рендерера и ассетов. Ассетом в нашем случае будет картинка с шариками и другими графическими элементами:

-3
-4

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

Метод для отрисовки одной клетки:

-5

На деле используются два метода: drawTileBack() рисует простой прямоугольник, который является задником клетки, а drawTile() рисует клетку с рельефной рамкой, используя светлый и тёмный цвета.

Метод drawField() рисует игровое поле поклеточно:

-6

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

-7

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

Есть следующие эффекты:

  • GrowEffect – шарик появляется на поле, вырастая в размерах. Используется при добавлении новых шариков.
  • TrailEffect – тень шарика, которая уменьшается и пропадает. Используется при движении шарика для формирования следа.
  • BounceEffect – подпрыгивающий на месте шарик, который выбран игроком
  • ShakeEffect – трясущийся шарик, который вот-вот исчезнет
  • RejectEffect – красный крестик, который появляется на клетке, в которую нельзя попасть

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

Как работают эффекты

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

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

Для всех эффектов можно сделать общий класс:

-8

Пераметр delay задаёт задержку между кадрами, а параметр endTick задаёт последний тик, до которого должен идти прогресс. Тик увеличивается при вызове метода update(), с учётом задержки:

-9

Чтобы получать текущее значение эффекта, используется метод getValue():

-10

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

Как теперь будет выглядеть класс GrowEffect:

-11

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

Аналогично делается RejectEffect:

-12

и TrailEffect:

-13

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

Более интересно выглядит BounceEffect:

-14

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

Кроме того, этот эффект работает бесконечно, пока не будет отменён, и поэтому наследуется от другого класса LoopedEffect:

-15

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

У ShakeEffect также переопределённый метод getValue(), который в каждом кадре меняет значение между -2 и 2.

-16

Как View использует эффекты

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

-17

Для удаления эффекта можно было просто записать null в позицию массива, но так как клетку нужно восстановить до нормального вида, в позицию помещается ClearEffect:

-18

В каждом кадре View обрабатывает активные эффекты:

-19

Метод проходит по массиву эффектов, пропуская пустые. Встретив ClearEffect, обрабатывает его и сразу же обнуляет. У остальных эффектов вызывает метод update(). Если метод вернул false, значит эффект уже отработал. Тогда вызывается метод removeEffect(). Иначе увеличиваем счётчик активных эффектов effectCnt – он будет нужен, чтобы быстро определить, остались ли активные эффекты. И отправляем эффект на обработку в processEffect():

-20

Первым делом надо отрисовать задник в drawTileBack(), чтобы скрыть следы предыдущих отрисовок. Затем проверяем тип эффекта. Если это BounceEffect, ClearEffect или ShakeEffect, то они влияют только на вертикальную позицию шарика. Используя картинку-ассет, отрисовываем шарик со смещением вертикальной позиции, полученным из эффекта (ClearEffect всегда возвращает 0 и поэтому будет отрисован шарик в стандартном положении).

Если это TrailEffect или RejectEffect, то рисуем нужный кадр из ассета в соответствии с прогрессом.

А вот если это GrowEffect, то рисуем шарик в отмасштабированном виде.

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

Читайте дальше: