В предыдущей части была сделана система обработки поведения объектов, которая даже работает:
Из краеугольных требований осталось сделать определитель столкновений объектов.
Все объекты будут иметь прямоугольную область столкновения. Для данной игры это сойдёт, и переусложнять не будем.
Как всё происходит в теории: взяли список объектов, и каждый проверили с каждым. Если их прямоугольники пересекаются, то они столкнулись.
Такая проверка не очень хороша, так как требует O(N^2) нахождений пересечений. Нам на самом деле не нужно проверять каждый объект с каждым (естественно, сравнив А и Б, мы не будем сравнивать Б и А, но речь тут не об этом).
Снаряды поражают вертолёты, самолёты, парашютистов, падающих парашютистов и бомбы. Бомбы поражают пушку. Падающие парашютисты поражают стоящих парашютистов, если упадут на них. Все остальные объекты между собой не взаимодействуют – например, вертолёты могут пролететь друг сквозь друга. И снаряды уж точно не сталкиваются друг с другом.
Поэтому часть проверок можно заведомо не делать, для чего придумаем
Маски столкновений
Такая маска имеет два поля: источник и приёмник (т.е. источник сталкивается с приёмником).
Например, снаряд имеет маску, условно говоря, (SHOT, AERIAL). Вертолёты, самолёты имеют маску (AERIAL, NONE).
Тогда мы берём объект А из списка и смотрим на его маску: ага, это (SHOT, AERIAL). Мы пройдём по списку начиная со следующего объекта и найдём такие объекты Б:
- с маской (AERIAL, *). Это значит, что они принимают столкновения от объекта А.
- с маской (*, SHOT). Это значит, что они сами сталкиваются с объектом А.
Обнаружив такой объект, мы обработаем столкновение с ним.
Хотя сама проверка будет проводиться только при подходящей маске, сложность O(N^2) сохраняется, просто становится меньше операций.
Сделаю типы и структуру масок:
Обратим внимание на многочисленные #[derive]. Без них, оказывается, эти данные ни копировать, ни даже сравнивать между собой нельзя. Также, я закомментировал #[repr(u8)]. Это указание, что enum-тип должен иметь размер 8 бит, но Rust определяет это автоматически.
Теперь нужно добавить в GameObject поле collide_mask, ну и там, где мы порождаем объекты, указывать это поле:
Далее, что должен делать коллайдер:
Взять каждый объект из списка и сравнить со всеми, которые в списке после него, сначала по маске. Либо он стукается в кого-то, либо кто-то его стукает. Если таковой вариант нашёлся, то найдём пересечение двух прямоугольников. Если пересекаются, значит есть коллизия.
А дальше необходимо разрулить ситуацию со столкнувшимися объектами. Исходы могут быть такие:
- Оба объекта исчезли (в игре Paratrooper это единственный вариант)
- Один из объектов исчез
- Ни один из объектов не исчез, но поменялись их параметры
- Всё вышеперечисленное, плюс появились новые объекты
Очевидно, такие вещи лучше не решать "на лету", прямо во время обработки в коллайдере. Тут нужна игровая логика, которая определяет, что именно должно произойти. Коллайдер этого не знает, да и в любом случае проблема в том, что находясь в процессе перебора списка объектов, мы не не можем ни удалять, ни добавлять ничего в этот список.
Так что для начала коллайдер сформирует список результатов столкновений в виде отдельного вектора, где будет указано, какие пары объектов столкнулись друг с другом.
Структура для пары объектов содержит их индексы в списке, по которым их можно будет найти:
Solver
Это компонент, который будет решать, что делать со столкновениями. Можно сказать, что это обработчик игровой логики. Например, бомба и пушка столкнулись. Это не просто значит, что они должны разрушиться. Это значит, что игра окончена.
Поэтому Solver будет завязан на игру гораздо глубже. Сейчас будем просто уничтожать объекты, чтобы убедиться, что всё работает. Будем перебирать пары, которые нашёл коллайдер, и просто удалять объекты из списка. Как удалять? По индексам. И тут мы натыкаемся на старую проблему: если что-то удалить из вектора, индексы объектов изменятся. Значит, оставшиеся индексы для удаления станут невалидными.
Я рассмотрел несколько вариантов, в том числе сортировку индексов (если удалять в порядке убывания, всё будет хорошо) и копирование из вектора в вектор и и.д., но в результате никакого красивого решения не нашлось.
Тогда я просто заменил вектор на кладовку Pantry. Объекты будут храниться в кладовке, оттуда можно их удалять по индексу, и другие индексы двигаться не будут. Единственное, что придётся изменить итерацию по списку объектов, в кладовке она более кустарная, во всяком случае пока.
Методы стали больше, сюда их уже трудно вставлять, приведу кусок метода check() коллайдера:
Солвер:
Тут есть ещё один нюанс. Если часто выпускать снаряды, то два снаряда попадут в один вертолёт и получится две пары столкновений. Солвер обработает первую пару и удалит снаряд и вертолёт, затем перейдёт ко второй паре... а там тот же самый вертолёт, который уже удалён.
Поэтому солвер должен после обработки пары дополнительно пометить оставшиеся пары, которые нельзя обрабатывать, если объекты, указанные в них, уже удалены.
Ничего не поделаешь, дописываем поле status в CollidePair:
И переписываем часть кода Solver:
Далее, как именно солвер будет влиять на такие игровые события, как конец игры или начало следующего уровня? Ему нужно будет не просто удалять объекты, но и проверять их типы – кто с кем взаимодействовал. И принимать какое-то решение. Но как его реализовать, если для этого надо по сути иметь полный доступ к состоянию игры, что повлечёт передачу различных мутабельных ссылок, которые конечно не получится нормально использовать?
Думаю, тут будет ещё одна прокладка в виде кода события. Солвер будет возвращать только определённый код события, например GAME_OVER, а внешняя логика игры уже будет сама его обрабатывать.
Наверное, как-то так, дальше посмотрим.
Код из данной статьи находится в ветке part2 на гитхабе:
Читайте дальше: