Напомню, в прошлый раз мы остановились на том, что герой начал отскакивать от злодеев, меняя траекторию. Это было уже хорошо, но нашёлся целый ряд вопросов, которые надо было бы проработать. С них и начнём.
Движение злодеев
Вчера специально наблюдал за тем, как работают вражеские боты в играх (для рассмотрения взял Darkwing Duck от NES).
Сперва хочу заметить, что эта игра - настоящий шедевр. В ней прекрасно всё: сюжет, управляемость, противники, окружение и... музыка.
Ну так вот, наблюдая за злодеями я обнаружил следующее. Есть несколько типов злодеев. Первый тип - это злодей, связанный с окружением. То есть он взаимодействует со стенами, полом и прочим. Второй же тип - независим от окружения: его снаряды могут лететь сквозь стены, как и он сам. Также есть противники, не способные перемещаться.
Далее, момент появления: противники не присутствуют всё время, они появляются на экране лишь тогда, когда герой входит в некоторую "триггерную" зону. А правильней сказать - какая-то зона оказывается видимой на экране. Потому что нет смысла держать в памяти всех противников карты, а загружать их можно по мере необходимости.
Затем, действие: каждый противник изначально действует по-умолчанию: стоит или движется по заданной траектории. Но стоит герою попасть в некоторую "триггерную" зону, как противник начинает атаковать героя.
Тут, к слову, есть два разных подхода: триггерная зона привязана к координатам карты и триггерная зона привязана к координатам противника.
Короче говоря, вариантов действия противников довольно много. Поэтому мы пойдём по следующему пути, описывая работу злодея:
- Злодей перемещается слева-направо от края до края экрана;
- Если злодей встречает препятствие, пытается обойти его сверху или снизу (случайно);
- "Штрафные" зоны его не задерживают;
- Если обойти препятствие не получилось - он начинает двигаться в обратном направлении;
- Если на расстоянии 10 клеток (вверх) появляется герой - злодей меняет траекторию и движется к нему;
- В случае столкновения героя и злодея - герой отскакивает, а злодей выключается на N секунд (предположительно - две).
Короче говоря, список правил не то, чтобы очень большой. Но начнём с самого начала: заставим двигаться злодеев в горизонтали.
Ну что, противники теперь двигаются, каждый в свою сторону. При этом, начинают они движение только тогда, когда попадают в область видимости. Оптимизацию "добавления при появлении" я делать не стал, т.к. столь острого дефицита памяти у меня нет, однако двигаю их лишь тогда, когда они видны.
Как нетрудно заметить, злодеи улетают в край экрана и застревают там навсегда потому, что не умеют взаимодействовать с блоками. Пока что. Чтобы это исправить, надо дополнить класс обработки столкновений и добавить туда обработку столкновения злодеев со стенами.
Ну, это было не очень сложно. Просто пришлось переписать родительский класс Bot, а также изменить распределение методов внутри классов Hero и Enemy. Пустячок. Зато злодей теперь барражирует от стены к стене, что нам и требовалось. Падение на дно для героя стало сложней. Хотя казалось бы, что может быть проще падения... В обещем, сократил силу отскока вдвое: теперь реактивные разлёты исчезли.
Механизм штрафов
В настоящий момент есть только один тип штрафов: штраф при падении на непроходимый блок (пол короче говоря). Но это не совсем правильно. Дело в том, что есть, по меньшей мере, ещё несколько случаев, когда скорость должна гаситься:
- При столкновении с боковыми препятствиями, все "боковые" силы должны обнуляться (это касается сброса величины вектора управления и величины вектора отскока);
- Внутри блоков замедления гасится скорость гравитации, но должны гаситься все силы примерно вдвое;
- При встрече с расположенными выше блоками (удар в потолок), вектор отскока должен полностью удаляться;
Тут придётся существенно доработать систему определения столкновений.
Во-первых, надо понять, с какой стороны произошёл удар. Во-вторых, изменить работу системы штрафов: теперь она должна быть зависима от типа столкновения (в смысле с какого боку столкнулись).
На мой взгляд, тут ничего сложно нет. Логика реакций на столкновения выглядит примерно так:
А описание штрафов - так
Короче говоря, получилось более-менее прилично. Но всё же каких сил требует простая, на первый взгляд, настройка динамики управления. Что-то сделаешь не так - и герой уже начинает мельтешить по экрану сбивая всё настроение играть. Определённо, теперь управление стало плавней. Ещё за ним понаблюдаю, может ещё что-то исправить надо.
Новый тип блоков
Давно хотел добавить такой блок, который с одной стороны пропускает героя, а с другой - нет. В большинстве NES игр были платформы, по которым можно ходить, с которых можно спрыгивать вниз и на которые можно было запрыгнуть. Вот что-то такое мне и нужно. Почему нужно? Потому что добавление такого типа блоков заставит пересмотреть используемую систему взаимодействия с блоками и, возможно, улучшить архитектуру обработчика и осознание способа взаимодействия со статичными объектами.
Итак, цвет новых объектов будет зелёный и они будут изображаться в виде треугольника гранью вверх, через которую нельзя пролететь в падении; а сбоку или снизу вполне можно пройти сквозь объект.
Но с ними вышел казус. Дело в том, что существующий детектор столкновений работал на сам факт пересечения. И оказавшийся на площади зелёного треугольничка герой - автоматически перестаёт падать.
Происходит это потому, что детектор столкновений "заточен" на блок в целом и действовавшая ранее система, не позволявшая герою проходить сквозь стены - тут не работает. И не работает просто потому, что пока на "территории" нового типа блока находится герой - его состояние определяется как на момент столкновения со стеной. То есть он останавливается в падении. А находится, при этом, не сверху, а внутри блока, что отчасти и требовалось, но вот общий результат неудовлетворителен. Надо как-то сделать так, чтобы блок "работал" лишь тогда, когда герой сверху и переставал работать во всех прочих случаях.
Короче, намучился я с этим новым типом блоков: вроде всё должно работать, но нет, не работает. А всё, как обычно, в деталях: нижняя граница героя (которая, по сути, составляет 20 - 1 пиксель) считалась мной неверно и принималась за полновесные 20. Как следствие, когда герой оказывался своей нижней гранью прямо над новым блоком - он воспринимался как уже прошедший границу (а раз прошёл - значит можно смело падать дальше, потому что считается, что сверху пробиться невозможно, только сбоку если). И вот, 1 пиксель делал своё чёрное дело.
Поэтому надо использовать >= оператор. Хотя такой подход не то, чтобы совершенно верный, задачу решает и погрешность учитывает.
Тут ещё есть одна недосказанность. В идеале, конечно, иметь чёткий вектор движения героя (и он есть), но в текущем случае нахождение верхней грани над платформой уже говорит о том, что идёт падение вниз (т.к. лишь при пересечении нижних квадрантов с зоной платформы вызывают корректирующий эффект и наложение штрафов скорости).
Встряска
Во многих игрушках используется довольно любопытный эффект: при некоторых событиях (получение сильного повреждения, падения с большой высоты или столкновении со стеной на высокой скорости) - экран вздрагивает. Если есть контроллер с вибрацией - то он, обычно, ещё и вибрирует. Хочу добавить такое же.
Изначально, я хотел показать, как всё это выглядит, но из-за того, что количество кадров довольно мало при записи gif, увидеть это навряд ли получится. Ну ничего, потом посмотрите, когда закончу всё.
Теперь про техническую часть. Встряхнуть экран совсем несложно. Для этого я добавил 2 параметра: амплитуда (встряски экрана) и счётчик (фактически, величина смещения по оси X во время встряски). Для встряски счётчик устанавливается на 10 и постепенно уменьшается на 1 до достижении значения 0. Когда значение 0 - встряска отсутствует.
Направление же и величина встряски рассчитывается простым синусом:
На рисунке выше видно, что xShakeOffset - смещение от встряски по OX - вычисляется только при ненулевом значении величины (которая устанавливается в 10 при наложении штрафов, при условии, что скорость - смещение - была выше 5 пикселей за такт).
Такое решение позволяет несколько раз дёрнуть экран влево-вправо. Приемлемо. Если в какой-то момент встряска перестанет нравиться - можно изменить количество колебаний или изменить параметры функции.
Снаряды
Ну, кажется пришлось добавить ещё один тип злодеев: злодейские снаряды. И это новый вызов.
Прежде всего, снаряды - негабаритные. Они меньше чем объект карты. Значит и стандартные обработчики в них не работают. С блоками и со злодеями снаряды не взаимодействуют, а вот героя останавливают.
Исчезают они при выходе за границы экрана, либо при встрече с героем; стреляют ими специальные блоки (по прочим характеристикам как стена).
То есть у нас тут уже появляется новый тип объекта: статичный объект, который начинает действовать в тот момент, когда рядом герой и стреляющий в его сторону (летят по горизонтали) всегда.
Первое, что тут надо решить, это когда активировать стрелка. Пойдём по тому же пути, что и со злодеями: когда блок оказывается в зоне видимости - начинает стрелять. Может выпустить не более двух снарядов на экран с интервалом не менее трёх клеток.
Да, вот эти фиолетовые шарики, что летят - и есть снаряды. Пока они пролетают мимо всего, но должны будут тормозить главного героя при попадании в него. По сути, просто штраф скорости.
Чтобы их добавить, пришлось немного пересмотреть подход, используемый до настоящего момента:
- Все пушки пересчитаны и пока не появятся на экране - не стреляют;
- 1 выстрел в 1 секунду и не более 2 на экране за раз;
- Пока что снаряды стираются границами экрана;
- Учёт снарядов - отдельная сущность, считает их количество для каждой пушки, а также следит за интервалом выстрелов;
- Когда все снаряды, выпущенные одной пушкой, заканчиваются (например, пушка больше не видна) - запись о ней удаляется;
- Отрисовка ведётся стандартным методом;
- Стандартный обработчик столкновений тут не сработает - нужен немного другой. Им и займёмся.
Новый обработчик столкновений
Как вы, наверное, помните, для определения столкновения двух равновеликих объектов достаточно было просто вычесть последовательно x и y координаты их и модуль значения должен был быть меньше или равен шагу клетки. Но то для равновеликих объектов. Для разновеликих - всё немного иначе.
Принцип определения факта столкновения будет примерно тот же:
|X1 - X2| < Wлев
|Y1 - Y2| < Hверх
Проясню. Если в прошлой версии все блоки одного размера, то размер пули сильно меньше размера героя и, как следствие, возможны случаи, когда фактор столкновения будет обрабатываться неверно. Разберу на примере оси икс (для игрек ситуация такая же). Предусловие: объекты описываются рамками и по этим рамкам мы и определяем факт столкновения.
Итак, пуля слева (x1), герой справа (x2). Если от x1 вычесть x2 и взять по модулю - получится расстояние от начала одного объекта до начала другого. Если это расстояние меньше длины объекта (как было в предыдущей, упрощённой, версии), то всё однозначно. Но в текущем примере, размеры объектов разные.
Предположим, длина пули - 10, она находится на точке 0, длина героя - 20, он находится на точке 15. То есть пересечения нет. Вычисляем дельту: |0 - 15| = 15. Если мы сравним 15 с длиной героя, то координата явно попадает внутрь, а если с длиной пули - то нет. Стало быть, мы должны сравнивать с длиной объекта, находящегося слева. Иными словами, объекта, у которого координата X наименьшая.
В формуле есть два параметра: Wлев и Hверх. Они, как раз, и обозначают "левейший" и "наиверхнейший" объекты. Причём, в данном случае, размеры могут браться иначе: длина может браться от первого объекта, а высота - от второго.
Продолжение
Прошлый "генератор" пуль мною делался вечером, поздно уже было... короче наворотил всяких глупостей. Начал подготовку к столкновениям с того, что оптимизировал код: вынес "объект пуля" в отдельный класс и обрабатываться он теперь будет совсем по-другому. Проверил, всё работает.
Следующим шагом - добавил обработчик столкновений разновеликих объектов (только для пуль пока что). Собственно, именно ради более удобной работы с ним и затевался весь рефакторинг.
Собственно, на пути к обработке столкновения, было найдено несколько ошибок, сделанных (но не замеченных) ранее, а также выявлено попустительство, в результате которого пуля рисовалась в центре ячейки, а вот взаимодействовала уже с реальными координатами. Получалось, что с одной стороны она влетала внутрь героя, зато с другой не долетала.
Попадание пули делает две пакости: во-первых, сбрасываются все силы (гравитационные и силы отскока); а во-вторых, на полсекунды блокируется управление (падение продолжается, а вот управления - нет).
На изображении, кстати, видно немного встряхивание экрана, когда туда попадает пуля.
Ну, на этом данный этап можно считать пройденным. Но есть ещё кое-что, что хочется сделать:
- Оптимизация отрисовки: сейчас рисуется всё, и видимое, и невидимое;
- Анимация элементов карты: пускай герои, злодеи и окружение оживёт... ну или хотя бы начнёт мигать;
- Оптимизация работы с картой (тут всё очень непросто: хранить в памяти очень много однотипных данных - я про массив, например, пустых пространств - накладно, а всю карту, кроме той, что видна на экране, плюс-минус немного по краю - и вовсе не имеет смысле).
Этим и продолжим.