Предыдущая часть:
Буду описывать довольно сложную проблему, поэтому вот сразу картинка того результата, который нас ожидает (а в конце статьи размещу ещё и видео):
Чтобы по экрану летали не унылые прямоугольники, я сделал новый тип отображения DrawableListRect, который содержит массив из 5 прямоугольников типа ColorRect:
Да, ровно 5, но за актуальное количество отвечает поле cnt, чтобы не возиться с динамическими размерами (это всё равно временное решение на коленке). Прямоугольник ColorRect имеет дополнительное поле color, чтобы можно было делать их разноцветными.
Из них, как из блоков Лего, можно построить более-менее различимые объекты:
А большего мне и не надо!
Замена объектов на лету (буквально)
Тут придётся позанудствовать, потому что задача оказалась с подвохом и я потратил немало времени, пробуя разные варианты, а потом переделывая их.
В оригинальной игре парашютист не сразу раскрывает парашют, а сначала пролетает в свободном падении какое-то случайное время. Без парашюта он летит быстро, а с парашютом медленно. Это не позволяет игроку заранее взять упреждение, потому что он не знает, в какой момент раскроется парашют. Так получается интереснее.
Это значит, что в общем случае игровой объект может поменяться на другой. У этого объекта будет и другая графика, и другое поведение, то есть менять надо комплексно.
Я задумался над тем, менять ли параметры прямо в текущем объекте, или просто удалить его и добавить новый.
Текущая схема обработки способствовала тому, чтобы старые объекты удалять, а новые добавлять в отдельный список, чтобы потом весь этот списк добавить к основному списку уже после всех обработок. Это выглядит наиболее надёжно, так как циклы обработки и добавления новых не пересекаются между собой.
Так и сделал, но потом понял, что это не совсем хорошо. Сам игровой объект GameObject неважно как добавлять, можно и всегда в конец списка. Но фабрика, производящая GameObject, одновременно производит и StageObject и сразу добавляет его на сцену, чтобы получить его сценический индекс, который должен сохраниться в GameObject.sto_index.
Добавленный на сцену объект окажется поверх всех остальных объектов, и это может привести к глюку в отображении. Ему надо оставаться в том слое, в каком был удалённый объект. Поэтому именно на сцене их надо именно заменять.
Пришлось изменить методы фабрики, порождающие GameObject. Они теперь ничего не добавляют на сцену и возвращают GameObject с индексом sto_index = 0, это значение пока что невалидно.
Я сделал ещё одну фабрику, которая порождает объекты StageObject для добавления на сцену. Но тоже не добавляет, а только возвращает. Используем принцип единственной ответственности.
Получив GameObject от одной фабрики и StageObject от другой фабрики, внешняя логика теперь сама может добавить StageObject на сцену и сохранить sto_index в GameObject. Получается немного больше работы, зато фабрики теперь заняты только своим делом и манипуляции с объектами становятся более гибкими.
В список новых я стал помещать не готовые объекты, а заготовки из GameObject и StageObject, которые собирались в актуальный объект и помещались на сцену уже непосредственно при переносе из нового списка в основной.
Также я добавил в заготовку признак того, должна ли она добавляться в конец списка или замещать какой-то существующий объект.
Задача, казалось, была решена. Теперь объекты работали, к примеру, так:
- Падающий парашютист решает, что пора раскрыть парашют, и формирует новую заготовку для раскрытого парашюта, добавляя её в список новых объектов. При этом сам он не удаляется, так как новый объект будет его замещать.
- Заготовка берётся из списка новых объектов, из неё собирается объект и замещает собой старый.
Ключевое здесь то, что старый объект не удаляется, а ждёт, когда на следующем этапе его заместит новый.
Что пошло не так
После обработки поведений объектов у нас в основном списке есть парашютист, который ожидает замену, и заготовка в списке новых.
Но дальше включается коллайдер и проверяет столкновения. Так как старый парашютист не удалён, он может столкнуться со снарядом. А солвер, обработав это столкновение, удалит его.
А дальше список новых переносится в список основных. Надо заместить старого парашютиста... А он уже удалён. Программа падает.
Чтобы выловить эту ошибку, нужно попасть снарядом в парашютиста ровно в том же кадре, в котором он раскрывает парашют. Невероятно подлый глюк.
Я пробовал менять порядок обработки, например, сначала добавить новые объекты, потом запустить коллайдер. Но тогда вылазят какие-то неудобства в других местах.
Поэтому я решил сделать настоящую замену объекта. Новые как и прежде добавляются в список новых, а вот замена происходит прямо по месту.
Для этого я изменил метод поведения, чтобы туда передавалась мутабельная ссылка на GameObject (раньше мне это кстати не удавалось, но вот уже приспособился), и через эту ссылку GameObject теперь может обновить сам себя.
Я сделал в нём метод update_from():
Туда передаются данные нового GameObject и нового StageObject. Текущий объект освобождает кладовку от данных своего поведения, обновляет StageObject на сцене, и копирует остальные поля из нового объекта. Вот, к примеру, поведение парашютиста, которое заменяет объект:
Я сделал для игры подобие автоуправления, чтобы она крутила пушкой и стреляла куда попало, и она работает уже часа три, сбив более 1000 объектов.
Поворот пушки
Пушка сделана гибридным способом из двух объектов. Первый объект это, так сказать, станина – обычный GameObject, у которого есть поведение (как я и говорил в прошлой части – запускать против самого себя вертолёты) и отображение. Он участвует в проверках коллайдера и в него может попасть бомба.
Второй объект это ствол пушки. Его нет в списке игровых объектов, это просто отдельно добавленный на сцену StageObject. Этот ствол нужно рисовать повёрнутым под разными углами, а что я могу сделать с этим сейчас?
Я добавил тип отображения DrawableRotRect, который рисует повёрнутые прямоугольники.
Это структура прямоугольника, в которую добавлены угол поворота angle и координаты центра поворота cx и cy.
DrawableRotRect, когда рисует сам себя, разбивает свой прямоугольник на два треугольника, поворачивает их вершины на нужный угол, и для каждого треугольника вызывает у рендерера метод растеризации, заимствованный прямо отсюда:
Рисовального кода получилось много и он неэффективный, но это абсолютно временное решение, ведь в итоге я всё равно перейду на использование изображений.
Чисто теоретически можно сделать графическую часть игры полностью на растеризуемых полигонах. Это даст определённые плюсы, например, масштабирование под любой экран, ну и своеобразный художественный стиль.
Аналогичным образом с помощью формул поворота я теперь рассчитываю точку, из которой должен вылететь снаряд, и направление полёта снаряда.
Унижение парашютистов
Проверку на попадание в купол парашюта можно было бы сделать, допустим, так. Купол отдельно, парашютист отдельно, как два отдельных объекта, и проверять можно стандартным образом каждый из них. Но тогда, если попало в купол, надо найти парашютиста и сделать его падающим, а если в парашютиста, то надо наоборот найти купол и удалить его вместе с парашютистом.
Это приводит к необходимости как-то поддерживать связь между двумя объектами. Я бы сделал это просто: они всегда добавляются подряд, поэтому следующий за куполом всегда парашютист, а предыдуший перед парашютистом всегда купол.
Но такая связь мне не нравится из-за того, что это очень специфичный случай. Поэтому, хотя её для этой конкретной игры можно спокойно сделать, я вместо этого расширю проверку в солвере. То есть коллайдер обеспечит грубое столкновение, а солвер будет считать более точное.
Вот как выглядит текущий обработчик в солвере:
Он просто увеличивает количество сбитых парашютистов, но обратим внимание на проверку типов объектов. В столкновении участвуют два объекта, один из них это парашютист, а вторым в данной версии может быть только снаряд. Я знаю индексы обоих объектов в паре, но не знаю, кто из них кто. Индексы следовало бы как-то нормализовать, ну а пока придётся просто ещё раз писать проверки:
Так я получил индексы и данные снаряда и парашютиста. Далее надо сравнить координаты снаряда и парашютиста. Так как это обработка коллизии, снаряд уже попал, вопрос только куда:
Если координата снаряда по вертикали находится не ниже 7 пикселов от верха парашютиста, значит попадание было в купол.
Если же снаряд попал ниже, надо дополнительно проверить попадание по горизонтальной координате. Оно должно быть более 4 пикселов дальше от левого и правого края, так как там пустое место.
И вот тут возникает новое обстоятельство. Если раньше солвер все столкнувшиеся объекты просто удалял, сейчас может быть ситуация, когда объекты формально столкнулись, но снаряд пока ещё не попал непосредственно в парашютиста. Поэтому удалять ни снаряд, ни парашютиста нельзя, для чего придётся ввести некий флаг удаления should_delete.
Солвер также заменяет парашютиста на падающего парашютиста прямо по месту:
Мокрое место
Когда парашютист без парашюта шлёпается о землю, от него должны полететь кровавые брызги. Ну как должны – просто я хочу сделать объект с ещё одним вариантом поведения.
Это движение с учётом (псевдо) гравитации:
Одна брызга будет красным квадратиком, который полетит вверх, замедлится и потом полетит вниз. Такой объект не будет участвовать в столкновениях, поэтому выход за пределы экрана он обрабатывает сам. Когда это случится, объект может просигнализировать о том, что он больше не нужен. Для этого поведение возвращает статус BhvStatus::END.
Обработчик объектов, получив статус END от поведения, просто удалит этот объект.
Теперь я для одного шлепка создам сразу несколько отдельных объектов-брызг, расположенных случайно в районе месте падения, и со случайными начальными параметрами.
И это работает:
Но лучше смотреть в движении.
Код лежит на гитхабе в ветке part4:
Читайте дальше: