[Перевод] Пишем на Rust игры для Unreal Engine
Несколько месяцев назад я задался вопросом — что, если написать игру на 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
. Нам нужно каким-то образом удалить их, или в компонентах редактора будет храниться слишком много необязательных данных.
Кроме того, ко всем компонентам можно получить доступ через блюпринт:
Что, если мы удалим здесь поле 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.
Если хотите попробовать его, то здесь есть подсказки по началу работы.