Игра на Rust за 24 часа: личный опыт разработки +31


image

В этой статье я расскажу о личном опыте разработки небольшой игры на Rust. На создание рабочей версии ушло около 24 часов (преимущественно я работала по вечерам или на выходных). Игра еще далека от завершения, но я думаю, что опыт будет полезным. Я расскажу, чему научилась, и о некоторых наблюдениях, сделанных при построении игры с нуля.

Skillbox рекомендует: Двухлетний практический курс «Я – веб-разработчик PRO».

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Почему Rust?


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

Почему именно игра и что за игра?


Создание игр — это весело! Я бы хотела, чтобы причин было больше, но для «домашних» проектов я выбираю темы, которые не слишком тесно связаны с моей обычной работой. Что за игра? Мне хотелось сделать что-то вроде теннисного симулятора, где сочетаются Cities Skylines, Zoo Tycoon, Prison Architect и собственно теннис. В общем, получилась игра об академии тенниса, куда люди приходят для игры.

Техническая подготовка


Мне хотелось использовать Rust, но я не знала точно, насколько «с нуля» понадобится начать работу. Я не хотела писать пиксельные шейдеры и использовать drag-n-drop, поэтому искала самые гибкие решения.

Нашла полезные ресурсы, которыми делюсь с вами:


Я изучила несколько игровых движков Rust, выбрав в конечном итоге Piston и ggez. С ними я сталкивалась при работе над предыдущим проектом. В итоге выбрала ggez, поскольку он показался более подходящим для реализации небольшой 2D-игры. Модульная структура Piston чересчур сложна для начинающего разработчика (или того, кто впервые работает с Rust).

Структура игры


Я потратила немного времени на размышления об архитектуре проекта. Первый шаг — сделать «землю», людей и теннисные корты. Люди должны перемещаться по кортам и ждать. У игроков должны быть навыки, совершенствующиеся с течением времени. Плюс ко всему должен быть редактор, который позволяет добавлять новых людей и корты, но это уже не бесплатно.

Продумав все, я приступила к работе.

Создание игры


Начало: окружности и абстракции

Я взяла пример из ggez и получила круг на экране. Удивительно! Теперь немного абстракций. Мне показалось, что неплохо абстрагироваться от идеи игрового объекта. Каждый объект должен быть отрендерен и обновлен, как указано здесь:

// the game object trait
trait GameObject {
    fn update(&mut self, _ctx: &mut Context) -> GameResult<()>;
    fn draw(&mut self, ctx: &mut Context) -> GameResult<()>;
}
 
// a specific game object - Circle
struct Circle {
    position: Point2,
}
 
 impl Circle {
    fn new(position: Point2) -> Circle {
        Circle { position }
    }
}
impl GameObject for Circle {
    fn update(&mut self, _ctx: &mut Context) -> GameResult<()> {
        Ok(())
    }
    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        let circle =
            graphics::Mesh::new_circle(ctx, graphics::DrawMode::Fill, self.position, 100.0, 2.0)?;
 
         graphics::draw(ctx, &circle, na::Point2::new(0.0, 0.0), 0.0)?;
        Ok(())
    }
}

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

mpl event::EventHandler for MainState {
    fn update(&mut self, context: &mut Context) -> GameResult<()> {
        // Update all objects
        for object in self.objects.iter_mut() {
            object.update(context)?;
        }
 
        Ok(())
    }
 
    fn draw(&mut self, context: &mut Context) -> GameResult<()> {
        graphics::clear(context);
 
        // Draw all objects
        for object in self.objects.iter_mut() {
            object.draw(context)?;
        }
 
        graphics::present(context);
 
        Ok(())
    }
}

main.rs необходим потому, что в нем — все строки кода. Я потратила немного времени, чтобы разделить файлы и оптимизировать структуру директорий. Вот как все стало выглядеть после этого:

resources -> this is where all the assets are (images)
src
— entities
— game_object.rs
— circle.rs
— main.rs -> main loop


Люди, этажи и изображения

Следующий этап — создание игрового объекта Person и загрузка изображений. Все должно строиться на основе плиток размером 32*32.



Теннисные корты

Изучив, как выглядят теннисные корты, я решила сделать их из плиток 4*2. Изначально можно было сделать изображение такого размера либо же составить вместе 8 отдельных плиток. Но потом я поняла, что необходимы лишь две уникальные плитки, и вот почему.

Всего у нас две таких плитки: 1 и 2.

Каждая секция корта состоит из плитки 1 или плитки 2. Они могут располагаться как обычно или быть перевернутыми на 180 градусов.



Основной режим строительства (сборки)

После того как получилось добиться рендеринга площадок, людей и карт, я поняла, что необходим еще и базовый режим сборки. Его реализовала так: когда нажата кнопка — объект выбран, а клик размещает его на нужном месте. Так, кнопка 1 дает возможность выбрать корт, а кнопка 2 позволяет выбрать игрока.

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


Вопросы по архитектуре и рефакторингу

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

Мне показалось, что нужно переосмыслить архитектуру так, чтобы выявились некоторые ограничения:

  • наличие сущности, которая отображает и обновляет себя саму, — это проблема, поскольку эта сущность не сможет «узнать», что она должна рендерить — изображение и вайрфрейм;
  • отсутствие инструмента обмена свойствами и поведением между отдельно взятыми сущностями (пример — свойство is_build_mode или же отрисовка поведения). Можно было бы использовать наследование, хотя в Rust нет нормального способа его реализации. Что мне действительно было нужно — это компоновка;
  • инструмент для взаимодействия сущностей между собой был нужен, чтобы назначать людей в корты;
  • сами сущности представляли собой смесь данных и логики, что очень быстро выходило из-под контроля.

Я провела дополнительное изучение и обнаружила архитектуру ECS — Entity Component System, которая обычно используется в играх. Вот преимущества ECS:

  • данные отделены от логики;
  • компоновка вместо наследования;
  • архитектура, ориентированная на данные.

Для ECS характерны три базовых концепта:

  • сущности — тип объекта, на который ссылается идентификатор (это может быть игрок, мячик или что-то еще);
  • компоненты — из них состоят сущности. Пример — компонент рендеринга, расположения и другие. Это хранилища данных;
  • системы — они используют как объекты, так и компоненты, плюс содержат поведение и логику, которые основываются на этих данных. Пример — система рендеринга, перебирающая все сущности с компонентами для рендеринга и занимающаяся отрисовкой.

После изучения стало ясно, что ECS решает такие проблемы:

  • применение компоновки вместо наследования для системной организации сущностей;
  • избавление от мешанины кода за счет систем управления;
  • использование методов вроде is_build_mode, чтобы хранить логику вайрфрейма в одном и том же месте — в системе рендеринга.

Вот что получилось после внедрения ECS.

resources -> this is where all the assets are (images)
src
— components
— position.rs
— person.rs
— tennis_court.rs
— floor.rs
— wireframe.rs
— mouse_tracked.rs
— resources
— mouse.rs
— systems
— rendering.rs
— constants.rs
— utils.rs
— world_factory.rs -> world factory functions
— main.rs -> main loop


Назначаем людей на корты


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

Что я сделала:

  • добавила данные о назначенных кортах в Person;
  • добавила данные о распределенных людях в TennisCourt;
  • добавила CourtChoosingSystem, позволяющую анализировать людей и площадки, обнаруживать доступные корты и распределять игроков на них;
  • добавила систему PersonMovementSystem, которая ищет людей, назначенных на корты, и если их там нет, то отправляет людей куда нужно.


Подводим итоги


Мне очень понравилось работать над этой простой игрой. Более того, я довольна, что использовала для ее написания Rust, поскольку:

  • Rust дает вам то, что необходимо;
  • у него отличная документация, Rust весьма элегантен;
  • постоянство — это круто;
  • не приходится прибегать к клонированию, копированию или другим подобным действиям, что я часто делала в С++;
  • Options очень удобны для работы, они также прекрасно обрабатывают ошибки;
  • если проект удалось скомпилировать, то в 99% он работает, причем именно так, как должен. Сообщения об ошибках компилятора, мне кажется, лучшие из тех, что я видела.

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

Skillbox рекомендует:





К сожалению, не доступен сервер mySQL