[Перевод] Пишем на Rust игры для Unreal Engine

image


Несколько месяцев назад я задался вопросом — что, если написать игру на Rust, но в качестве рендерера использовать Unreal? Поразмыслив, я пришёл к выводу, что раскрытие рендерера Unreal языку Rust при помощи FFI (Foreign function interface) языка C потребовало бы гораздо больше усилий, чем мне хотелось. Но что, если просто надстроить систему на Unreal? Я смогу просто перемещать акторов (gameobject из Unreal) при помощи Rust. Эта задача показалась гораздо более приемлемой, поэтому я приступил к работе.

Спустя неделю я раскрыл несколько функций, позволявших получать ввод, задавать и считывать позицию актора. Благодаря этому я наконец мог перемещать персонажа в Unreal.


Потрясающе, но наблюдать за этим довольно скучно. Может быть, добавить анимации?

Я изучил вопрос управления анимациями в Unreal. В этом примере персонаж уже имеет риг и анимации. Эти анимации управляются AnimationBlueprint. Мне достаточно лишь передать скорость, с которой бежит персонаж, а всё остальное сделает AnimationBlueprint. Я просто раскрыл ffi-функцию GetRustVelocity, после чего персонаж научился бегать.


Для меня это было настоящей магией. Не приложив практически никаких усилий, я мог перемещать персонажа в Unreal при помощи Rust.

Но мне хотелось чего-то большего, чем просто перемещаться. Я хотел проигрывать звуки, реализовать физику и поиск пути в 3D, создавать частицы и префабы, работать с сетью. Так я осознал, что на самом деле хотел использовать Unreal не просто в качестве рендерера, а как движок в целом. Зачем реализовывать его самому, если достаточно раскрыть несколько функций?

Так родился проект unreal-rust.


Если вкратце, unreal-rust позволяет писать на Rust игры для Unreal Engine.

unreal-rust — это интеграция бескомпромиссного Rust для Unreal. Для Rust важны монопольное использование, мутабельность и времена жизни. Строгое сопоставление концепций Unreal с концепциями Rust привело бы к куче проблем. Поэтому unreal-rust будет написан поверх AActor движка Unreal и раскроет его API для Rust удобным образом.

Первое большое изменение заключается в том, что unreal-rust будет использовать Entity Component System (ECS). Для unreal-rust я решил использовать bevy, а не писать свою собственную систему. Я всего лишь разработчик-одиночка, поэтому должен рассчитывать свои силы. Написание и поддержка ECS лишь отвлечёт меня от истинной работы. Разработчики bevy замечательно справились с удобной для пользователя реализацией ECS.

Я хочу глубокого интегрировать Rust в Unreal, чтобы доступно было всё. Пользователь может добавлять Component Rust к AActor в редакторе. Например, можно добавить CharacterConfigComponent к PlayerActor, доступ к которому потом можно будет получить из Rust. Это позволяет настраивать персонажа из Unreal, не прикасаясь к коду. Также можно получить доступ к Component Rust через блюпринт. Это позволяет управлять блюпринтами Animation или передавать данные в UI.

Rust связывается с Unreal через FFI языка C. Это немного выходит за рамки нашей статьи, поэтому я написал небольшую статью с объяснением того, как это работает, в wiki unreal rust.

Компоненты редактора и рефлексия


Чтобы сделать Component видимыми в редакторе/блюпринте, достаточно добавить производный Component, дать ему уникальный UUID/GUID (который может сгенерировать ваш редактор) и зарегистрировать его при помощи register_components. А если вы хотите, чтобы он отображался в редакторе, достаточно просто пометить его #[reflect(editor)].

#[derive(Debug, Component)]
#[uuid = "16ca6de6-7a30-412d-8bef-4ee96e18a101"]
#[reflect(editor)]
pub struct CharacterConfigComponent {
    pub max_movement_speed: f32,
    pub gravity_dir: Vec3,
    pub gravity_strength: f32,
    pub max_walkable_slope: f32,
    pub step_size: f32,
    pub walk_offset: f32,
    pub jump_velocity: f32,
    pub max_gliding_downwards_speed: f32,
    pub gliding_gravity_scale: f32,
    pub ground_offset: f32,
}

...

impl Plugin for MovementPlugin {
    fn build(&self, module: &mut Module) {
        register_components! {
            CharacterConfigComponent,
            => module
        };
    }
}


Компонент сразу окажется видимым в редакторе, и вы сможете добавить его к любому нужному актору. К сожалению, на данный момент он поддерживает только несколько примитивных типов наподобие Vec3, Quat, f32, bool и некоторые ассеты наподобие UClass и USound. Я планирую расширить его до определяемых пользователем структур, hashmap, массивов и так далее, а также добавить настраиваемые drawer, например, использование ползунка для f32 вместо поля ввода.
Теперь в актор в редакторе можно добавлять любой Component. По сути, это позволяет реализовать префабы. Можно создать PlayerActor, добавить ему нужные компоненты, сконфигурировать и при размещении его на уровне unreal-rust автоматически зарегистрирует его и добавит компоненты к Entity Rust.

Блюпринты


Blueprint — это визуальная система Unreal для скриптинга, активно используемая в движке. Через неё можно управлять анимациями, материалами, частицами, звуком и геймплеем. Важно, что в блюпринтах можно использовать и unreal-rust.

Я добавил новый нод под названием GetComponent(Rust), предоставляющий доступ ко всем компонентам Rust.


В показанном выше видео я раскрываю MovementComponent, который просто содержит несколько полей наподобие velocity, is_falling и так далее, после чего запись в него выполняется системой движения.

#[derive(Default, Debug, Component)]
#[uuid = "fc8bd668-fc0a-4ab7-8b3d-f0f22bb539e2"]
pub struct MovementComponent {
    pub velocity: Vec3,
    pub is_falling: bool,
    pub is_flying: bool,
    pub view: Quat,
}


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

Горячая перезагрузка


Горячая перезагрузка позволяет видеть изменения в коде в реальном времени без необходимости перезапуска редактора.

Эксперименты без вылетания редактора


C++ — серьёзный изъян Unreal. Можно легко вызвать вылет редактора при срабатывании assert или доступе к nullptr. Это сильно усложняет эксперименты с движком, особенно если вы новичок. Однако в Rust вылеты или panic чётко заданы и их можно перехватывать. Это значит, что при разворачивании Option::None или выполнении доступа за пределами unreal-rust просто перехватит панику, выйдет из игрового режима и выведет ошибку в консоль. При этом сбой редактора никогда не происходит.
В текущем состоянии проект ужасен. Почти ничто не реализовано правильно, поэтому пока не стоит использовать unreal-rust в реальном проекте. Ниже я перечислю часть проблем.

Отсутствие стабильной версионности/сохраняемости


Допустим, вы написали следующий компонент:

#[derive(Default, Component)]
#[uuid = "fc8bd668-fc0a-4ab7-8b3d-f0f22bb539e2"]
pub struct MovementComponent {
    pub gravity: f32,
}


Теперь вы добавляете этот компонент к актору в редакторе и сохраняете его. Но спустя несколько месяцев вы понимаете, что одного float для гравитации недостаточно и вам нужно ещё и направление. Поэтому вы меняете тип.

#[derive(Default, Component)]
#[uuid = "fc8bd668-fc0a-4ab7-8b3d-f0f22bb539e2"]
pub struct MovementComponent {
    pub gravity: Vec3,
}


Но теперь гравитация у всех акторов в редакторе имеет тип f32, а они должны превратиться в Vec3. Как их преобразовать?

Потом кто-то в вашем проекте переименует gravity в gravity_dir. Как сообщить редактору, что это поле переименовано?

Далее вы решите разделить гравитацию на направление и силу.

#[derive(Default, Component)]
#[uuid = "fc8bd668-fc0a-4ab7-8b3d-f0f22bb539e2"]
pub struct MovementComponent {
    pub gravity_strength: f32,
    pub gravity_dir: Vec3,
}


На каком этапе мы автоматически добавляем новое поле gravity_strength всем акторам в редакторе? Можно сделать это во время горячей перезагрузки. Мы добавляем поле, компилируем код, а затем внутри редактора обновляем все MovementComponent. Но спустя несколько минут мы понимаем, что это была плохая идея и возвращаемся к изначальному коду, удаляя поле gravity_strength. Однако все MovementComponent в редакторе по-прежнему будут иметь поле gravity_strength. Нам нужно каким-то образом удалить их, или в компонентах редактора будет храниться слишком много необязательных данных.

Кроме того, ко всем компонентам можно получить доступ через блюпринт:

5ed52ee2f73c36f0a65a76b1d3b8473a.png


Что, если мы удалим здесь поле is_falling? Удаляем ли мы связь is_fallingна лету? Если мы сделаем так, то поломаем все блюпринты, и в будущем, возможно, нам придётся восстанавливать связь. Если мы сохраним связь, то блюпринт откажется компилироваться.

Список можно продолжать, но общий смысл понятен. Все эти проблемы достаточно легко решить, но для этого потребуется поработать. А пока они не решены, я не рекомендую использовать unreal-rust в реальном проекте.

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

Отсутствие сериализации


В настоящий момент в проекте полностью отсутствует сериализация.

Обычно горячая перезагрузка реализуется следующим образом:

  • Компилируем код и создаём новую dll
  • Обнаруживаем изменение и инициируем горячую перезагрузку
  • Сериализуем текущее состояние игры
  • Загружаем только что скомпилированную dll
  • А затем восстанавливаем состояние игры с только что загруженной dll


Так как сериализации нет, при загрузке новой dll мы просто избавляемся от состояния игры.

Можно было бы просто использовать serde и на этом закончить. Однако serde существенно увеличивает время компиляции. Можно использовать bevy_reflect и/или mini_serde, но тогда теряются некоторые удобства serde, например, переименование полей и выполнение апгрейдов данных.

Прочие проблемы


  • Контроллер персонажа совершенно ужасен
  • Отсутствует множество API, например, для поиска пути, работы сети и так далее
  • Компоненты редактора приклеены поверх системы свойств Unreal


У проекта по-прежнему много проблем, но я хочу превратить unreal-rust в нечто реальное, для этого только потребуется время. У меня есть бесконечный список todo, но следующим серьёзным шагом будут примеры. Я хочу обкатать этот проект на реальных примерах, в которых воссоздам игровые механики, например, карточную игру из Inscryption, метание топора из God of War и так далее.

Для этого есть несколько причин:

  • Это позволит мне понять, раскрытию каких API нужно отдавать приоритет
  • Я выявлю проблемные места и устраню их на ранних этапах проекта
  • Упростится обучение unreal-rust на этих примерах


Если вы хотите следить за проектом, подпишитесь на меня в twitter или на проект на github.

Если хотите попробовать его, то здесь есть подсказки по началу работы.

© Habrahabr.ru