Итак, я продолжаю реализовывать обработчик столкновений. Напомню в чём проблематика вопроса: когда на экране игры есть некоторое количество объектов - они должны как-то взаимодействовать. По сути, это и является игрой.
На сегодняшний день есть довольно много алгоритмов, позволяющих эффективно обрабатывать взаимодействие сложных объектов. Но проблема в том, что такая сложность - избыточная. Она требует много ресурсов, но нисколько не добавляет увлекательности игре. Делает разнообразней, сложней и требующей больше ресурсов - да, увлекательней - нет. Увлекательность - это вообще не про технические аспекты реализации, а про фантазию игроделов. Так что пропустим спор.
На чём остановился
А остановился в прошлый раз на том, что шарик начал падать и собирать по пути (уничтожать то есть) все объекты на своём пути. И, как раз, столкнулся с необходимостью что-то делать с обработкой столкновений. По идее, если красный шарик проходит через синий квадрат - он тормозится; белый же является препятствием. Есть ещё бирюзовые шарики - это будущие противники, о них потом.
Итак, поехали
Первое, что я сделал - это обновил реакцию на пересечение. Обработка определения столкновений уже была, так что это проблемой не стало; стало быть, при пересечении с объектом типа "синий" - на героя накладывается штраф: предельная величина его скорости сокращается вдвое и остаётся таковой до тех пор, пока он не выйдет их зоны. Определятся выход очень просто: на каждой итерации проверяется, внутри ли зоны наш герой и, если он там - накладывается штраф, а, после рассчёта новых координат, штраф отменяется.
Но это так, пустячок. Следующий вопрос - это как нам обработать столкновения со стенами (белые квадратики).
И вот тут есть проблема: нам надо не просто обнаружить столкновение, но и не дать объекту пройти через такую стену.
Факт пересечения стены обнаружить просто, но что дальше? И дальше начинается раскрытие страшных тайн игроделов. Наверное.
Первое, что приходит на ум, это то, что нам надо откатиться. На координаты рядом находящегося пустого квадратика (с противоположной стороны).
Но тут есть сложность: мы не знаем куда двигался герой. То есть у нас есть смещения красного шарика, мы понимаем с какой стороны он врезался в стену, но... он врезался в неё сверху или справа? В какую сторону нужно подвинуть шарик.
Я понимаю, задача выглядит тривиальной. Но не торопитесь... обдумайте это.
Если коротко, то непонятно куда надо сдвинуть героя после столкновения.
О, знали бы вы, сколько я изрисовал листов бумаги, пытаясь определить, как же правильно отыграть эту задачу! На первый взгляд всё тривиально, но все варианты смещения давали элемент непредсказуемости каждый раз, как смещение (dx,dy) было таковым, что герой залезал на стену по двум координатам сразу (то есть, почти каждый раз).
Попытки смещать обратно его пропорционально "наезду" была провальной изначально, т.к. траектория движения могла быть таковой, что такое смещение переместит героя в неверное положение (он не должен был пролететь сквозь стену, а его подняло наверх или скинуло под неё).
Наверное, это объяснение выглядит несколько унылым и непонятным. Ну кроме как для тех, кто успел побороться с этим.
Но есть несколько факторов, которые позволяют разрешить этот вопрос крайне просто:
- Клеточки сетки не залазят друг на друга (т.е. при длине 20 пикселей, она начинается на 0, а заканчивается на 19, а не на 20) - это позволит не допустить случаев, когда мы сдвинули объект на соседнюю клетку, а он всё равно пересекается с текущей;
- У нас есть приоритет движения, то есть мы говорим, что у нас больше горизонтального или вертикального (как в моём случае) движения;
- Мы храним предыдущие координаты героя и перезаписываем их лишь тогда, когда новая точка смещения принята;
- Самое главное: мы сперва определяем факт пересечения в новой точке, затем пробуем сместить объект по основному направлению (у меня - по вертикали) и, если он опять натыкается на преграду - исправляем координаты смещения на "дельту заступа", а потом уже проверяем по второму направлению (у меня по горизонтали) и делаем то же самое.
Вот код, как это работает:
Я позже на эту тему напишу отдельную статью, т.к. она очень важная, но не совсем очевидная. А пока что, короткое резюме: пересечение объектов определить просто, а вот реакцию на него - нет; поэтому, для того, чтобы это сделать, надо отказаться от идеи "векторного" движения, а перейти на "движение лесенкой" (предполагается, что есть главное направление движение, смещение по нему выравнивается в первую очередь, а есть - второстепенное, по нему выравнивание происходит во вторую очередь).
Но всё вышеперечисленное, в большей степени, актуально для взаимодействия со стенами (полом, потолком). Это относительно просто, т.к. данные объекты имеют статичные координаты (x,y - всегда имеют одно и то же значение и пропорционально шагу карты).
Динамические объекты
Безусловно, типов объектов карты может быть существенно больше: разрушаемые блоки, блоки-двери, блоки одностороннего прохождения и много похожего. Но это всё - антураж. В большинстве игр главное игровое противодействие обеспечивают они, злодеи. Или противники игры (буду называть их злодеями, потому что мне от этого весело).
Итак, злодеи - они как герой, только плохие и мешают играть (собственно, делают игру интересней).
Условно, есть два типа перемещения объектов: по знакоместу (дёрганное такое переключение прыжками, зато все объекты всегда на каком-то определённом индексе матрицы карты) и плавное (то есть то, которым мы интересуемся), предполагающее, что герой может одновременно находиться на нескольких клетках (если размер героя 1 клетка - то он может пересекаться с координатами сразу 4 других клеток).
Так вот, плавное движение, всё же, требует соблюдения некоторых правил, нарушение которых приведёт к определённым трудностями. Главная из них: предельное смещение объекта за 1 шаг не может быть больше шага карты (про карту, шаг карты и габариты персонажей я ещё напишу позже, это важно).
Потому что, если шаг будет больше - герой (или злодей) начнёт успешно проходить сквозь стены (и, иногда, застревать в них, если стена достаточно толстая), т.к. его смещение не будет пересекаться с блоками карты.
А вот это уже влияет на предельную скорость. Простой расчёт: шаг карты - 20 пикселей (безопасное смещение - 19 пикселей), отрисовка - 60 кадров в секунду. То есть за 1 секунду персонаж может безопасно сместиться на 19 x 60 = 1140 пикселей. Значение довольно приличное, но, полагаю, не трудно придумать несколько примеров игры, когда объекты двигаются очень быстро и такой скорости явно недостаточно.
Но и завязываться на частоту кадров тоже нельзя: у кого-то слабая карта, выдающая лишь 30 кадров в секунду, а у кого-то - более 100.
Короче, думаю я довольно обстоятельно написал о проблеме скоростей объектов.
Но перейдём к динамическим объектам: они не привязаны к шагу карты, могут перемещаться (и перемещаются). Стало быть, использованный ранее подход, определяющий столкновение, для нас более неприемлем. Поэтому, появляются новые требования:
- Надо определить, с какими объектами вообще надо проводить определение пересечения (в интересах оптимизации, чтобы сравнивать лишь с теми объектами, что поблизости и игнорировать те, что далеко);
- Найти способ, как определить факт пересечения объектов (для этого будем использовать относительно простой подход, годящийся для объектов одного размера |x1 - x2| < {шаг клетки} && |y1 - y2| < {шаг клетки} - данные подход предполагает, что координаты обоих объектов достаточно близки, чтобы произошло пересечение; если объекты разные по размеру, скажем, герой и летящий в него заряд, то правила вычисления немного усложняются);
- Найти способ, как обрабатывать их взаимодействие (тут может быть довольно много разнообразных реакций: смена направления движения - "отброс" героя или злодея, "привязка" к объекту - актуально для движущихся платформ, наложение штрафов или бонусов и многое другое).
Тут самой большой сложностью является, как можно заметить, способ взаимодействия.
По правде сказать, она стала таковой именно потому, что изначально не были описаны правила и способы взаимодействия объектов. А сводиться это должно к довольно простым тезисам: любой динамичный объект может взаимодействовать (или не взаимодействовать) с прочими объектами карты (как динамичными, так и статичными); правила взаимодействия с объектами определяются в условном хранителе логики игры, который имеет доступ ко всем игровым сущностям и может ими управлять. Звучит немного напыщенно и предельно непонятно? Конечно. Если просто, то должен быть некий объект внутри приложения, который знает, как могут взаимодействовать динамичные и статичные объекты и обеспечивать это взаимодействие.
Например, герой зашёл в воду и получил штраф на скорость передвижения; штраф наложил тот самый условный хранитель логики, а вот снимется штраф автоматически, когда герой выйдет из воды. Можно сделать, чтобы штраф тоже снимался вручную, но это усложнит логику работы хранителя, так что этого лучше избегать.
Определение пересечения динамических объектов
Закончим с теорией, перейдём к практике. Для начала попробуем сделать обработку столкновений с динамичными объектами, чтобы вообще понимать, что предложенный алгоритм работает.
Добавим небольшой кусочек кода (по взаимодействию героя со злодеями).
Тут я решил воздержаться от оптимизации: злодеев на карте не так, чтобы много и мы потратим сопоставимое количество вычислительных ресурсов, проверяя, надо ли проводить определение пересечения.
При пересечении - выводим в консоль уведомление. Поскольку герой проходил через злодеев достаточно долго - уведомлений пришло несколько:
Отлично. Движемся дальше. Нам потребуется сделать какое-то более ощутимое воздействие на героя со стороны злодея. Пусть он накладывает на него штраф-вектор (отражение объекта в противоположном направлении). Заодно и эту функциональность проверим.
Тут, сразу, возникла некоторая сложность: до сих пор герой падал вниз засчёт внутренней убеждённости, т.е. падение было описано внутри объекта героя. Что не то, чтобы очень правильно. Но выяснилось это ровно в тот момент, когда нам потребовалось направить его в другом направлении: началась путаница с векторами. Вектор гравитации, вектор движения, вектор импульса: короче сумма векторов превратилась должна быть более понятна для героя. Он же герой, его задач - просто двигаться в заданном направлении, а вот думать за него должен основной модуль. Придётся немного переписать всё. Короче, тут подумать надо сперва, как сделать лучше. А пока вам вот образец кода и завтра продолжим.