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

Разработка игры RDS на языке Rust: Начало

В подготовительных выпусках я разобрался с механизмами хранения игровых объектов: Теперь можно начать делать конкретную игру, я назову её условно RDS. Это ремейк игры Paratrooper 1982 года (на БК-0010 была переделка под названием Diversant). Вкратце, игрок управляет неподвижной пушкой, у которой можно поворачивать ствол. По экрану летают вертолёты, которые сбрасывают парашютистов, и самолёты, которые бросают бомбы. Задача игрока, управляя пушкой, сбивать всё что движется. Если парашютисты приземлятся с любой стороны пушки в количестве более 3-х, то они взорвут пушку и игра закончится. Также нельзя допустить, чтобы в пушку попала сброшенная бомба. Игра хорошо подходит для демонстрации обработки списка объектов, потому что содержит разные объекты с разным поведением. Хотя объектов много, типов поведений меньше. Например, движение снаряда пушки, бомбы и парашютиста – это всё один тип поведения "движение", просто с разными параметрами. А вот поведения вертолёта и самолёта – разные, потому
Оглавление

В подготовительных выпусках я разобрался с механизмами хранения игровых объектов:

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

Это ремейк игры Paratrooper 1982 года (на БК-0010 была переделка под названием Diversant).

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

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

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

Создание объекта

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

Единицей взаимодействия в игре является объект типа GameObject. Чтобы его создать, требуются следующие компоненты:

  • GmoData – текущее состояние объекта. Не полиморфно, является частью структуры GameObject и поэтому внешнего выделения памяти не требует.
  • StageObject – объект отображения на сцене, который хранит сцено-специфичные координаты, размеры, видимость и т.п. Ввиду того, что объекты на сцене расположены в своей иерархии для отрисовки, они хранятся отдельным списком, а в GameObject находится индекс сценического объекта sto_index. Также GameObject может вообще не иметь отображения.
  • StageObject, в свою очередь, требует Drawable – трейт-объект, отвечающий за техническую процедуру отрисовки на экране. Полиморфен, хранится как ссылка в StageObject. Кладовка для него не нужна, про это напишу ниже.
  • Behaviour – трейт-объект обработки поведения. Полиморфен, хранитcя как ссылка, выделения памяти не требует, потому что не обладает собственными данными и ссылка получается статическая.
  • BhvData – данные для состояния поведения. У разных поведений разные наборы данных, поэтому в структуре хранить нельзя, хранятся в кладовке, ссылку хранить нельзя из-за конфликта владений, поэтому хранится их индекс в кладовке bhvd_index.

Почему объектам Drawable не нужно хранение в кладовке? Потому что их всех можно описать заранее. Например, объект для рисования снаряда пушки это белый квадрат размером 3*3 пиксела (да, пока я все объекты буду рисовать цветными прямоугольниками). Он не будет меняться, и если мы создадим 100 снарядов, все они будут ссылаться на один и тот же объект. Поэтому его можно сразу объявить статически:

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

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

-2

Посмотрим на сам метод spawn_shot():

-3

Он формирует объект StageObject и помещает его на сцену, сохраняя его индекс в sto_index. Помещает в кладовку bhv_data и сохраняет индекс в bhvd_index. Возвращает объект GameObject со всеми готовыми атрибутами.

Замечу, что в метод передаётся ссылка на структуру Context, которая содержит все нужные компоненты: сцену, хранилище и саму фабрику. А также вектор gmo_new_vec, до которого доберёмся позже.

-4

Я сделал структуру Context для того, чтобы не гонять много аргументов функций туда-сюда и не путаться в их порядке передачи.

Также отметим, что у StageObject есть экранные координаты x, y, но у GameObject они тоже есть. Зачем дублировать?

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

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

Удаление объекта

Перед тем как возрадоваться и добавить в список 100500 GameObject'ов, нужно сделать обработку случаев, когда они покидают пределы экрана. Тогда GameObject нужно удалять из списка, и также удалять со сцены связанный с ним StageObject, и данные поведения, которые хранятся в кладовке.

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

Поэтому в трейт поведения придётся добавить метод удаления данных free():

-5

А потом реализовывать этот метод в каждом конкретном поведении:

-6

Здесь BehaviuorMove знает, что его данные находятся в кладовке storage.pantry_bhvd_move.

Если будет 100 поведений, придётся написать 100 однотипных реализаций данного метода, либо придумывать что-то другое.

Теперь добавим метод освобождения в GameObject:

-7

Он удаляет связанный StageObject со сцены и связанные данные поведения через само поведение.

Цикл обработки

Теперь можно взять вектор gmo_vec, в котором лежат какие-то объекты, и отправить в процедуру обработки:

-8

Тут всё из прошлой части, с дополнениями:

  • Cделано обновление экранных координат после обработки объекта. Для этого мы получаем из stage ссылку на объект StageObject и меняем его атрибуты x и y.
  • Сделан механизм добавления новых объектов. Первоначально я планировал, что поведения, которые порождают новые объекты, будут размещать их в том же gmo_vec. Но так не вышло из-за перекрёстных заимствований, которые запрещает Rust. Поэтому здесь все новые объекты добавляются в отдельный вектор ctx.gmo_new_vec, который потом добавляется к основному вектору gmo_vec с помощью append(). Так оно и лучше, чтобы свежедобавленные объекты не обрабатывались сразу (они должны начать обрабатываться только в следующем кадре).

Далее я организую сворую из проекта Apple стандартный главный цикл программы на основе событий SDL2, убрав лишнее:

-9

И в обработку нажатой клавиши вставляю добавление снаряда в список:

-10

Теперь можно запустить программу и, нажимая на любую клавишу, "выстреливать" снаряды. Они будут лететь из координат посередине-снизу экрана (где типа находится пушка) вертикально вверх (пока без поворотов пушки). Цикл обработки работает, только надо следить за тем, чтобы не выпустить больше 128 снарядов одновременно – проверку на переполнение списка объектов я не сделал :)

Чтобы скриншот смотрелся лучше, я увеличил размер снаряда до 10*10:

-11

Теперь я сделаю статические Drawable для вертолёта и самолёта. Это будут прямоугольники жёлтого и синего цвета. Для парашютов сделаю зелёный цвет, для бомб красный:

-12

Пора заняться поведением вертолёта. Он летит по горизонтали и через какие-то интервалы бросает парашютистов. Данные поведения:

-13

Здесь есть смещение по горизонтали dx, интервал выброса парашютистов interval, и счётчик интервалов cnt.

Пишем метод update() для поведения:

-14

Здесь увеличивается счётчик cnt, и когда его значение достигает interval, мы обращаемся к методу фабрики spawn_chute(). Созданный объект помещается в ctx.gmo_new_vec.

Структуры нулевого размера

Стоит обратить внимание, что фабрика находится в контексте ctx, но мы не обращаемся к ней через ctx.factory. Уже пора привыкнуть, что Rust не даст вызвать метод ctx.factory.update() и передать в него &mut ctx, потому что ctx содержит factory и получается зацикленная ссылка.

Но можно получить копию фабрики:

let factory = ctx.factory

И вызвать update() у копии. Тогда зацикленной ссылки не получится. Это не тот же объект, это копия. Чтобы оно работало, нужно специальным способом пометить структуру GmoFactory:

-15

Как видно, сама структура GmoFactory не имеет никаких атрибутов и поэтому имеет нулевой размер. Да, буквально ноль. Поэтому копирование по факту ненастоящее. Копировать нечего, и компилятор как-то это уберёт совсем, ну а у нас зато всё работает.

Порождение парашютиста и вертолёта

Вот парашютист:

-16

Здесь порождается объект, аналогичный снаряду из пушки, просто у него другое отображение и другие данные движения (а поведение – то же самое BehaviourMove).

А вот вертолёт, у него уже поведение BehaviourCarrier:

-17

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

Пора проверять!

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

-18

И всё!

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

Пробуем:

-19

Ура! Заработало!

Скажу честно, очень приятно это видеть. Система работает буквально автономно.

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

Нас ждёт следующий трудный этап: проверка столкновений объектов.

Ссылка на гитхаб-репозиторий:

GitHub - nandakoryaaa/rds-game at part1

Все сделанные в этой части изменения находятся в ветке part1.