Найти в Дзене
Сделай игру

Обработчик столкновений. День пятый

Оглавление

Итак, у нас уже всё работает, но только совсем всё не оптимально. Над этим и поработаю в этой главе.

Сегодня опорным персонажем будет он - мегамен
Сегодня опорным персонажем будет он - мегамен

Отрисовка

Дело в том, что в настоящий момент код отрисовки выглядит так:

Код отрисовки статики на карте
Код отрисовки статики на карте

Как нетрудно заметить, рисуется всё: и то, что видно, и то, что не видно. Непорядок.

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

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

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

Оптимизированная версия отрисовки видимой части экрана
Оптимизированная версия отрисовки видимой части экрана

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

Анимация

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

Спрайты мегамена
Спрайты мегамена

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

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

Задний фон - это некоторая общая картинка, без параллакса, небольшого размера, закрывающая весь "задник", не контрастная, оптимизированная для повторов по горизонтали и вертикали.

У пули два состояния, которые меняются довольно быстро (время подберу экспериментально). Злодей - имеет 4 состояния, 2 для движения влево и 2 - для движения вправо.

Герой имеет 5 состояний: покой или падение вниз, 2 для движения вправо и 2 - влево.

Вот такие исходные данные. К ним и приступим.

Задник

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

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

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

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

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

Добавили задний фон
Добавили задний фон

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

Время отсчёта для анимации

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

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

Код, генерирующий управляющие импульсы
Код, генерирующий управляющие импульсы

Можно было, конечно, использовать Math.ceil, но я решил оставить этот "+1" в самом конце, чтобы глядя в код всегда было понятно, что значение никогда не будет нулевым.

Мерцание заднего фона
Мерцание заднего фона

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

Перейдём к блокам: они также будут мерцать, но быстрее чем фон.

Мигает всё
Мигает всё

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

Выбор шага такта я делал так:

Каждая ...Fx переменная - это коэффициент для управления отображением 1..0..1
Каждая ...Fx переменная - это коэффициент для управления отображением 1..0..1

Подвижные герои, злодеи и пули

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

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

Искажение пули в полёте
Искажение пули в полёте

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

Мигание героя при простое и движении
Мигание героя при простое и движении

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

Злодеи тоже замигали в зависимости от направления движения
Злодеи тоже замигали в зависимости от направления движения

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

Оптимизация хранения данных

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

В чём трудность: данные карты хранятся в массиве длинной X записей. И это, вроде, хорошо. Однако, данный массив, во многом, заполнен нулями - пустым пространством, каждое из которых - это 4 байта данных. Посчитаем, карта 50 на 200 символов - это 10000 символов, по 4 байта. Итого, примерно 40 килобайт (поменьше конечно). Но это небольшая карта. Если карт несколько и они занимают большую площадь, то суммарная ёмкость может легко составить несколько мегабайт. А если добавить картинки, текстуры, звуки, а также используемую оперативную память - то довольно быстро становится ясно, что если уж оптимизировать, то всё, хотя бы чуть-чуть.

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

Хотя главным образом такая оптимизация создаётся, для двух вещей:

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

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

Приступим.

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

  • Столкновения героя со стенами - проверяет, находится ли по заданному индексу блок, отличный от "пусто", определяет столкновение с фиксированным объектом карты;
  • Определение направление столкновения: с какой стороны оно произошло с последующей реакцией;

И всё. Я думал будет больше.

Обращение, обычно, происходит по виду map.items[n] == 0, т.е. ничего в массив не записывается, а только проверяется. Важным фактором является адресация по индексу: индекс, учитывая то, что мы знаем габариты карты, позволяет определить координаты x и y блоков.

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

Теперь про "схлопывание" массива. Тут всё относительно просто:

  • Массив карты (в моём случае) - это набор значений, причём количество этих значений ограничено: всего 5 штук (0, 1, 2, 3, 4), но, думаю, число может увеличиться, но незначительно;
  • Всё упакуем в массив байт (Uint8Array);
  • 1 байт - это 8 бит или значения 0..255;
  • Поделим биты так: *****<a><b><c>, где a, b, c - 3 бита, зарезервированные под значения карты (8 состояний); 5 бит, помеченные звёздочками, это счётчик, который показывает, сколько таких элементов на карте будет в ряд (32 состояния);
  • Можно сделать счётчик по-другому, например, если первый бит байта начинается с 1 - значит это признак множителя и остальные 7 байт - количество, определяющее, сколько идущий следом байт надо повторить, но я от такого решил отказаться т.к. вероятность цепочки в 127 символов маловероятно, а работа с таким массивом сложней;
  • При разборе, индекс каждой новой горизонтальной линии сохраняется для упрощения навигации (чтобы не пришлось перебирать весь массив с самого начала в случае необходимости).
Код упаковки массива
Код упаковки массива

Результат упаковки оправдал все ожидание: количество элементов, хранящихся в массиве уменьшилось почти в 4 раза; из-за перехода к байтовой вместо 4-байтовой модели хранения данных - удалось сократить объём занимаемой памяти примерно в 16 раз.

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

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

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

Исправленный код
Исправленный код

Код подправить было несложно, после чего целостность данных восстановилась.

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

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

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

Иными словами, следующим шагом оптимизации будет развёртывание карты в полноценный массив карты, а также предоставление специального интерфейса для доступа к данным и обработки некоторых запросов к данным. Возможно, следует подумать про "переходные комнаты", вроде тех, которые использовались в Castlevania: SOTN.

Комната перехода; появляется при переходах среди зон игры
Комната перехода; появляется при переходах среди зон игры

Резюме

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

Вообще, структура файлов с обработчиками оказалась на удивление небольшой
Вообще, структура файлов с обработчиками оказалась на удивление небольшой

Отрисовка стала приятней, но не была особенно сильно проработана - только общие черты.

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

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

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