Найти в Дзене
ZDG

Разработка игры RDS на языке Rust: Замена объектов, поворот пушки и унижение парашютиста

Предыдущая часть: Буду описывать довольно сложную проблему, поэтому вот сразу картинка того результата, который нас ожидает (а в конце статьи размещу ещё и видео): Чтобы по экрану летали не унылые прямоугольники, я сделал новый тип отображения DrawableListRect, который содержит массив из 5 прямоугольников типа ColorRect: Да, ровно 5, но за актуальное количество отвечает поле cnt, чтобы не возиться с динамическими размерами (это всё равно временное решение на коленке). Прямоугольник ColorRect имеет дополнительное поле color, чтобы можно было делать их разноцветными. Из них, как из блоков Лего, можно построить более-менее различимые объекты: А большего мне и не надо! Тут придётся позанудствовать, потому что задача оказалась с подвохом и я потратил немало времени, пробуя разные варианты, а потом переделывая их. В оригинальной игре парашютист не сразу раскрывает парашют, а сначала пролетает в свободном падении какое-то случайное время. Без парашюта он летит быстро, а с парашютом медленно.
Оглавление

Предыдущая часть:

Буду описывать довольно сложную проблему, поэтому вот сразу картинка того результата, который нас ожидает (а в конце статьи размещу ещё и видео):

Чтобы по экрану летали не унылые прямоугольники, я сделал новый тип отображения DrawableListRect, который содержит массив из 5 прямоугольников типа ColorRect:

-2

Да, ровно 5, но за актуальное количество отвечает поле cnt, чтобы не возиться с динамическими размерами (это всё равно временное решение на коленке). Прямоугольник ColorRect имеет дополнительное поле color, чтобы можно было делать их разноцветными.

Из них, как из блоков Лего, можно построить более-менее различимые объекты:

-3

А большего мне и не надо!

Замена объектов на лету (буквально)

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

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

Это значит, что в общем случае игровой объект может поменяться на другой. У этого объекта будет и другая графика, и другое поведение, то есть менять надо комплексно.

Я задумался над тем, менять ли параметры прямо в текущем объекте, или просто удалить его и добавить новый.

Текущая схема обработки способствовала тому, чтобы старые объекты удалять, а новые добавлять в отдельный список, чтобы потом весь этот списк добавить к основному списку уже после всех обработок. Это выглядит наиболее надёжно, так как циклы обработки и добавления новых не пересекаются между собой.

Так и сделал, но потом понял, что это не совсем хорошо. Сам игровой объект GameObject неважно как добавлять, можно и всегда в конец списка. Но фабрика, производящая GameObject, одновременно производит и StageObject и сразу добавляет его на сцену, чтобы получить его сценический индекс, который должен сохраниться в GameObject.sto_index.

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

Пришлось изменить методы фабрики, порождающие GameObject. Они теперь ничего не добавляют на сцену и возвращают GameObject с индексом sto_index = 0, это значение пока что невалидно.

Я сделал ещё одну фабрику, которая порождает объекты StageObject для добавления на сцену. Но тоже не добавляет, а только возвращает. Используем принцип единственной ответственности.

Получив GameObject от одной фабрики и StageObject от другой фабрики, внешняя логика теперь сама может добавить StageObject на сцену и сохранить sto_index в GameObject. Получается немного больше работы, зато фабрики теперь заняты только своим делом и манипуляции с объектами становятся более гибкими.

В список новых я стал помещать не готовые объекты, а заготовки из GameObject и StageObject, которые собирались в актуальный объект и помещались на сцену уже непосредственно при переносе из нового списка в основной.

Также я добавил в заготовку признак того, должна ли она добавляться в конец списка или замещать какой-то существующий объект.

Задача, казалось, была решена. Теперь объекты работали, к примеру, так:

  1. Падающий парашютист решает, что пора раскрыть парашют, и формирует новую заготовку для раскрытого парашюта, добавляя её в список новых объектов. При этом сам он не удаляется, так как новый объект будет его замещать.
  2. Заготовка берётся из списка новых объектов, из неё собирается объект и замещает собой старый.

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

Что пошло не так

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

Но дальше включается коллайдер и проверяет столкновения. Так как старый парашютист не удалён, он может столкнуться со снарядом. А солвер, обработав это столкновение, удалит его.

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

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

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

Поэтому я решил сделать настоящую замену объекта. Новые как и прежде добавляются в список новых, а вот замена происходит прямо по месту.

Для этого я изменил метод поведения, чтобы туда передавалась мутабельная ссылка на GameObject (раньше мне это кстати не удавалось, но вот уже приспособился), и через эту ссылку GameObject теперь может обновить сам себя.

Я сделал в нём метод update_from():

-4

Туда передаются данные нового GameObject и нового StageObject. Текущий объект освобождает кладовку от данных своего поведения, обновляет StageObject на сцене, и копирует остальные поля из нового объекта. Вот, к примеру, поведение парашютиста, которое заменяет объект:

-5

Я сделал для игры подобие автоуправления, чтобы она крутила пушкой и стреляла куда попало, и она работает уже часа три, сбив более 1000 объектов.

Поворот пушки

Пушка сделана гибридным способом из двух объектов. Первый объект это, так сказать, станина – обычный GameObject, у которого есть поведение (как я и говорил в прошлой части – запускать против самого себя вертолёты) и отображение. Он участвует в проверках коллайдера и в него может попасть бомба.

Второй объект это ствол пушки. Его нет в списке игровых объектов, это просто отдельно добавленный на сцену StageObject. Этот ствол нужно рисовать повёрнутым под разными углами, а что я могу сделать с этим сейчас?

Я добавил тип отображения DrawableRotRect, который рисует повёрнутые прямоугольники.

-6

Это структура прямоугольника, в которую добавлены угол поворота angle и координаты центра поворота cx и cy.

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

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

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

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

Унижение парашютистов

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

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

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

Вот как выглядит текущий обработчик в солвере:

-7

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

-8

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

-9

Если координата снаряда по вертикали находится не ниже 7 пикселов от верха парашютиста, значит попадание было в купол.

-10

Если же снаряд попал ниже, надо дополнительно проверить попадание по горизонтальной координате. Оно должно быть более 4 пикселов дальше от левого и правого края, так как там пустое место.

-11

И вот тут возникает новое обстоятельство. Если раньше солвер все столкнувшиеся объекты просто удалял, сейчас может быть ситуация, когда объекты формально столкнулись, но снаряд пока ещё не попал непосредственно в парашютиста. Поэтому удалять ни снаряд, ни парашютиста нельзя, для чего придётся ввести некий флаг удаления should_delete.

Солвер также заменяет парашютиста на падающего парашютиста прямо по месту:

-12

Мокрое место

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

Это движение с учётом (псевдо) гравитации:

-13

Одна брызга будет красным квадратиком, который полетит вверх, замедлится и потом полетит вниз. Такой объект не будет участвовать в столкновениях, поэтому выход за пределы экрана он обрабатывает сам. Когда это случится, объект может просигнализировать о том, что он больше не нужен. Для этого поведение возвращает статус BhvStatus::END.

Обработчик объектов, получив статус END от поведения, просто удалит этот объект.

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

И это работает:

-14

Но лучше смотреть в движении.

Код лежит на гитхабе в ветке part4:

GitHub - nandakoryaaa/rds-game at part4

Читайте дальше: