В этой статье я расскажу о личном опыте разработки небольшой игры на Rust. На создание рабочей версии ушло около 24 часов (преимущественно я работала по вечерам или на выходных). Игра еще далека от завершения, но я думаю, что опыт будет полезным. Я расскажу, чему научилась, и о некоторых наблюдениях, сделанных при построении игры с нуля.
Напоминаем:для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».
Почему Rust?
Я выбрала этот язык, поскольку слышала о нем много хорошего и вижу, что он становится все более популярным в сфере разработки игр. До написания игры у меня был небольшой опыт разработки простых приложений на Rust. Этого было как раз достаточно, чтобы почувствовать определенную свободу во время написания игры.
Почему именно игра и что за игра?
Создание игр — это весело! Я бы хотела, чтобы причин было больше, но для «домашних» проектов я выбираю темы, которые не слишком тесно связаны с моей обычной работой. Что за игра? Мне хотелось сделать что-то вроде теннисного симулятора, где сочетаются Cities Skylines, Zoo Tycoon, Prison Architect и собственно теннис. В общем, получилась игра об академии тенниса, куда люди приходят для игры.
Техническая подготовка
Мне хотелось использовать Rust, но я не знала точно, насколько «с нуля» понадобится начать работу. Я не хотела писать пиксельные шейдеры и использовать drag-n-drop, поэтому искала самые гибкие решения.
Нашла полезные ресурсы, которыми делюсь с вами:
Are we game yet — список нужных для разработки игр элементов Rust;
Я изучила несколько игровых движков 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 для всех. Поэтому я смотрю на будущее языка с оптимизмом, с нетерпением ожидая результатов нашей общей работы.