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

Разработка игры RDS на языке Rust: Задачи солвера

В прошлом выпуске были сделаны коллайдер и солвер для обработки столкновений объектов. В этом выпуске остановимся подробнее на солвере. Я буду писать всё, что приходит в голову, и вносить исправления прямо по ходу. Возьмём реальные задачи, которые должен решать солвер. При попадании снаряда по парашютисту, вертолёту, самолёту или бомбе должно начисляться определённое количество очков. То есть солвер, обрабатывая столкновение снаряда и парашютиста, должен где-то там внутри игры увеличить переменную score на 5. Конечно, мы не будем вписывать 5 прямо в код, вместо этого сделаем конфиг, где укажем, сколько очков на что начислять. Значит, солвер должен иметь доступ ещё и к этому конфигу. Возьмём более сложный случай: справа или слева от пушки приземляется парашютист. Надо подсчитывать, сколько парашютистов с какой стороны пушки приземлилось. А если на стоящего парашютиста упал другой и они убились, опять же надо корректировать счётчик стоящих парашютистов. За счётчиком тоже надо лезть куда-
Оглавление

В прошлом выпуске были сделаны коллайдер и солвер для обработки столкновений объектов.

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

Возьмём реальные задачи, которые должен решать солвер. При попадании снаряда по парашютисту, вертолёту, самолёту или бомбе должно начисляться определённое количество очков.

То есть солвер, обрабатывая столкновение снаряда и парашютиста, должен где-то там внутри игры увеличить переменную score на 5. Конечно, мы не будем вписывать 5 прямо в код, вместо этого сделаем конфиг, где укажем, сколько очков на что начислять. Значит, солвер должен иметь доступ ещё и к этому конфигу.

Возьмём более сложный случай: справа или слева от пушки приземляется парашютист. Надо подсчитывать, сколько парашютистов с какой стороны пушки приземлилось. А если на стоящего парашютиста упал другой и они убились, опять же надо корректировать счётчик стоящих парашютистов. За счётчиком тоже надо лезть куда-то в игру.

Я хочу показать, что в общем случае мы заранее не знаем, к чему должен иметь доступ солвер, и насколько плотно он будет интегрирован в игровую логику.

Надо ли максимально отвязывать солвер от игры, или дать ему полный доступ? При осторожном подходе можно сделать солвер полностью интегрированным в игру. Но в случае с Rust это будет означать дополнительный слой передачи мутабельных ссылок, чем я рисковать уже заведомо не хочу.

Вот так Rust воспитывает!

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

Нас интересуют сбития вертолётов, самолётов и т.д., поэтому сделаем структуру, в которой хранятся счётчики каждого уничтоженного объекта:

-2

Солвер при обработке столкновения снаряда и вертолёта будет просто увеличивать счётчик shot_carriers и т.д.

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

Похоже, здесь все задачи покрыты, но что ещё нужно?

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

Для этого нужно двойное ограничение: во-первых, не выпускать более 10 вертолётов. Во-вторых, не начинать новую волну, пока на экране остаются ранее запущенные вертолёты.

В игре при инициализации волны мы должны сделать счётчик потраченных вертолётов.

Затем можно добавить в список объектов невидимый объект с поведением, которое через какие-то промежутки времени будет запускать новые вертолёты. Запустив 10 вертолётов, этот объект закончит существование. Получается довольно удобно, не правда ли? Можно не зашивать жёсткую логику в игру, а просто добавлять объект с нужным поведением, и всё будет работать само собой.

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

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

Когда вертолёт в целости и сохранности покидает экран, его тоже надо посчитать. Но солвер не сможет это сделать, потому что он обрабатывает только коллизии, а уход с экрана это не коллизия. Мы можем пойти на хитрость и разместить за пределами экрана невидимые прямоугольники. Вертолёты, вылетая за экран, будут сталкиваться с ними, и тогда солвер сможет это обработать. Но это просто забавный костыль, если нет другого выхода.

-3

Обратим внимание на то, что в процедуре обработки игровых объектов проверяется выход за пределы экрана и объекты сразу уничтожаются. А ведь эту задачу можно возложить на коллайдер. Если он обрабатывает столкновения объектов с объектами, он может обрабатывать и столкновения объектов с "ничем". И тогда не нужно их сразу уничтожать. Вместо этого они попадут в тот же самый список столкновений, который потом обработает солвер. Тогда солвер сможет сообщить игре и счётчик объектов, покинувших экран. А игра уже решит, что с этим делать.

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

Итак, убираем из процедуры обработки объектов проверку на выход за пределы экрана:

-4

И переносим эту проверку в коллайдер:

-5

Она будет делаться самой первой, и если объект ушёл с экрана, то дальше для него ничего не проверяется. Но нужно ещё решить два вопроса. Первый это использование ctx.stage.w и ctx.stage.h для получения размеров экрана. В коллайдер не передаётся ctx. Можем передать, но лучше передадим просто прямоугольник Rect, который описывает... прямоугольник, соответствующий экрану.

-6

Точно такая структура есть в SDL2, но я не хочу создавать зависимость от SDL2.

Далее, когда покинувший экран объект добавляется в список коллизий, солвер должен его обрабатывать по-особенному: отсутствует второй участник столкновения. Тут придётся кстати поле status, которое есть в структуре CollisionPair. Сейчас для него используются константы 1 и 0, так что сделаем расширенный набор статусов в виде перечисляемого типа:

-7

В метод коллайдера передадим Rect:

-8

И соответственно изменим проверку координат и формирование коллизионной пары:

-9

У такой пары src_index равен dst_index, типа объект столкнулся сам с собой, но это неважно, так как солвер будет обращать внимание на статус OFFSCREEN и обрабатывать только один объект.

Добавляем в метод солвера ссылку на структуру SolverEvents:

-10

Хотя нет, эту структуру всё равно надо каждый раз очищать, поэтому проще создавать её прямо в солвере и возвращать копированием. Сделаем конструктор пустой структуры:

-11

И вставим в метод солвера, не забыв, что он теперь возвращает SolverEvents:

-12

Теперь меняем логику солвера:

-13

Объекты со статусом CollideStatus::NONE он пропускает без обработки, а для объектов со статусом CollideStatus::OFFSCREEN он должен, к примеру, увеличить счётчик улетевших вертолётов, но как он определит, что это был именно вертолёт? На иллюстрации я использую проверку по маске CollideMаsk::AERIAL, но она неточная, так как такую маску могут иметь разные объекты. Проверять, например, тип поведения тоже ненадёжно. Тогда примем радикальное и простое решение: добавим в структуру GameObject перечисляемый тип, который можно назначать как угодно:

-14

Я назвал поле в структуре не type, а gmo_type, потому что type это зарезервированное слово. См. Проблемы именования переменных.

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

Хорошо, теперь солвер может проверять объекты непосредственно по типу:

-15

Здесь я получаю тип объекта-источника и сразу удаляю его (в нашем сценарии он всегда удаляется). И если это был внеэкранный объект, то второй объект из пары не удаляется (его сразу нет) и дальнейшие проверки не делаются.

В случае стандартного столкновения продолжаем проверки (сюда надо будет добавить подсчёт сбитых вертолётов и пр.):

-16

Что ж, надо поправить вызовы методов в других местах программы, и можно проверять, что получилось после текущих переделок.

Всё работает как и раньше, но уже с новыми обработками, и это радует:

-17

Добавлю теперь некоторые подсчёты в солвер:

-18

Тут я не проверяю досконально, кто в кого попал, потому что и так известно, что если один из участников столкновения это вертолёт, то другой это снаряд, и т.д.

Теперь применяю к подсчитанным данным логику со стороны игры (она пока кустарная прямо в главном цикле main()):

Здесь я начислил какое-то условное количество очков за вертолёты и парашютистов. Ну и вывел для демонстрации в консоль. Вот что получилось:

-20

Тяжёлый случай

Теперь рассмотрим ситуацию посложнее. Если снаряд попадает не в самого парашютиста, а в его парашют, мы должны не убивать парашютиста, а сделать его падающим.

Ок, что должен сделать солвер? Можно просто заменить объект "парашютист" на объект "падающий парашютист". Для этого у солвера должен быть доступ к списку объектов (он есть) и к фабрике объектов – а он тоже есть, через контекст ctx. Получается, мы можем это сделать.

А можем ли мы доверить это именно солверу? Конечно! Ведь он солвер, он решает, что делать. К тому же, когда мы сбиваем вертолёт, нужно добавить на сцену взрыв, и это тоже должен делать солвер. Именно в нём мы собираем правила, по которым должны трансформироваться игровые объекты.

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

Текущий код находится на гитхабе в ветке part3:

GitHub - nandakoryaaa/rds-game at part3

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