Первый опыт разработки игры на Rust

Рассказывает Olivia Ifrim

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

Почему Rust?

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

Почему именно игра и какая это игра?

Делать игры — весело! Хотелось бы, чтобы была более веская причина, но для любительских проектов я обычно предпочитаю вещи, которые весьма далеки от того, что я делаю ежедневно на работе. Так какая же игра? Я хотела сделать симулятор на тему тенниса с пиксельной графикой. Я ещё не всё продумала, но по сути это теннисная академия, куда люди приходят и играют в теннис.

Техническое исследование

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

Я нашла несколько полезных ресурсов, которыми хочу поделиться:

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

Архитектура игры

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

В этот момент у меня появилось достаточно идей, чтобы начать писать код.

Разработка игры

Начало: круги и абстракция

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

// Трейт игрового объекта
trait GameObject {
    fn update(&mut self, _ctx: &mut Context) -> GameResult<()>;
    fn draw(&mut self, ctx: &mut Context) -> GameResult<()>;
}

// определённый игровой объект - Круг
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(())
    }
}

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

impl event::EventHandler for MainState {
    fn update(&mut self, context: &mut Context) -> GameResult<()> {
        // обновляем все объекты
        for object in self.objects.iter_mut() {
            object.update(context)?;
        }

        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult<()> {
        graphics::clear(context);

        // Рисуем все объекты
        for object in self.objects.iter_mut() {
            object.draw(context)?;
        }

        graphics::present(context);

        Ok(())
    }
}

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

resources -> все ассеты находятся здесь (изображения)
src
-- сущности
---- game_object.rs
---- circle.rs
-- main.rs -> главный цикл

Люди, площадки и изображения

Следующим большим шагом было создание игрового объекта Person и загрузка изображений. Я решила, что все будет на основе плитки (на данный момент 32×32 плитки).

object Person

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

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

Есть 2 уникальных плитки: 1 и 2.

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

unique tiles

Основной режим сборки

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

Это было бы не очень удобно, так как вам нужно помнить, что такое 1 и 2. Поэтому я добавила вайрфрейм (от англ. wireframe – каркас) в режиме сборки, чтобы вы могли понять, какой у вас объект. Вот сборка в действии.

wireframe

Сомнения по поводу архитектуры и рефакторинга

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

Я начала подвергать сомнению архитектуру и смогла увидеть некоторые ограничения:

  • Наличие сущности, отображающей и обновляющей саму себя, было бы проблематичным — сущность не будет знать, должна она рендерить изображение или вайрфрейм.
  • У меня не было хорошего способа обмена свойствами и поведением между сущностями (такого как свойство is_build_mode или отрисовки поведения, например). Я могла бы использовать наследование (в Rust нет хорошего способа реализовать это), но что я действительно хотела, так это компоновку.
  • У меня не было хорошего способа взаимодействия сущностей между собой — что мне определенно понадобилось позже, чтобы назначать людей в корты.
  • Сущности смешивали данные и логику, что очень быстро выходило из-под контроля.

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

  • Отделять данные от логики.
  • Компоновка вместо наследования.
  • Дизайн, ориентированный на данные.

В терминологии ECS есть 3 основных понятия:

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

Чем больше я читала об этом, тем больше понимала, что это решит мои текущие проблемы:

  • Я могла бы использовать компоновку вместо наследования, чтобы раскладывать сущности более систематично.
  • Я могла бы использовать системы для управления поведением, не делая “спагетти” из кода.
  • Я могла бы делиться методами вроде is_build_mode и иметь логику вайрфрейма в одном месте (в системе рендеринга).

Вот что я получила после внедрения ECS (что, честно говоря, всё практически переписано).

resources -> 
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 -> глобальные фабричные функции
-- main.rs -> главный цикл

Назначение игроков на корты

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

Что было сделано:

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

Всё это можно увидеть в действии.

rust game

Заключение

Я действительно отлично провела время, создавая эту маленькую игру. Но я особенно рада, что написала её с помощью Rust, потому что:

  • Rust позволяет делать именно то, что вам нужно.
  • Он очень элегантен и имеет отличную документацию.
  • Неизменность по умолчанию прекрасна.
  • Никаких неловких движений, клонирования или копирования (как я часто делала в C++).
  • С Options работать очень комфортно и они делают обработку ошибок прекрасной.
  • Если проект скомпилировался, то 99 % случаев он работает так, как вы и ожидаете. А ошибки компилятора, вероятно, самые лучшие, которые я когда-либо видела.

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

Перевод статьи "24 HOURS OF GAME DEVELOPMENT IN RUST"