Introduction
Rusteal lets you write Unreal Engine 5.8 games in Rust. Your gameplay code is a Rust crate, compiled into a library that a small Unreal plugin loads. The classes you declare in it are real Unreal classes: they show up in the editor, they can be placed in a level, Blueprints can derive from them, and they are saved, cooked and packaged like classes written in C++.
This book is for the game developer. It shows how to use Rusteal to make a game: how to create a project, how to write actors, characters and systems in Rust, how to work with the editor and Blueprints, how to iterate without restarting the editor, and how to ship. It does not explain how Rusteal itself is built.
What stays in the editor, what goes in Rust
Rusteal does not replace the editor. A Rusteal game is an Unreal project like any other, and the work is split the way a C++ project splits it:
| In Rust | In the editor |
|---|---|
| Gameplay classes: actors, characters, controllers, game modes, components, subsystems | Levels, lighting, materials, meshes, animations |
| Their properties, components and default values | Blueprint children that assign assets to those properties |
| Rules, state, AI logic, systems | UMG widget layouts, input actions and mapping contexts |
| Console commands, settings, save data | Data tables and data assets filled with content |
A Rust class declares a property, and a Blueprint child of that class picks the mesh, the sound or the input action that goes into it. That is how Epic’s own C++ templates work, and Rusteal’s templates are those same templates with the C++ replaced by Rust.
How the book is organized
- Getting started installs the tools and creates a first project.
- The rusteal CLI covers every command you run while making the game.
- The iteration loop covers hot reload,
rusteal watch, the console commands and testing. - Classes covers what you write: classes, properties, components, functions, events, interfaces and structs.
- The engine API covers how Rust code talks to the engine: object references, spawning, assets, timers, traces and delegates.
- Gameplay recipes are complete use cases taken from the templates: movement, cameras, combat, AI, UI and more.
- Systems covers subsystems, settings, data assets, console commands and background work.
- Plugins covers gameplay and editor plugins written in Rust.
- Shipping covers packaging the game.
- Reference collects the attributes, the types, the event names, the known limits and the common problems.
Status
Rusteal is in early development. Its API changes between versions, and many engine features are not covered yet. Each chapter says what works today, and Known limits lists what does not.
How to read this book
Most pages are written as use cases. Each one starts from something you want to do in a game and shows how to do it with Rusteal:
- The goal: what the use case achieves in the game.
- The code: the Rust you write, complete enough to compile in a project made by
rusteal new. - In the editor: what you do in Unreal Editor, step by step, with the names the editor shows.
- Notes: limits, alternatives and what to watch for.
Code conventions
The examples import from two places:
rusteal_runtime, the runtime crate: the attributes (uclass,uclass_impl,ustruct), the logging macro and the types every project uses (UObjectRef,RustealResult,OwnedStruct…).bindings, the crate Rusteal generates from your engine: one Rust type per engine class, struct and enum, grouped by engine module (bindings::engine,bindings::enhanced_input,bindings::ai…).bindings::preludebrings the common ones and the helper functions (bind_action,create_widget_of_class,FVector::from_dvec3…).
Engine names become Rust names in a fixed way. Classes lose their C++ prefix (AActor is Actor, UCharacterMovementComponent is CharacterMovementComponent); structs and enums keep it (FVector, ETriggerEvent); functions and properties are in snake_case (K2_GetActorLocation is k2_get_actor_location). A class’s functions come from a trait named after it with Ext at the end (ActorExt, CharacterMovementComponentExt), which you import with the class.
Optional parameters of engine functions, the ones with a default value in C++, are Options in Rust: Some(value) passes a value, None keeps the default.
Screenshots
Some steps are easier to follow with a picture of the editor. Where a picture belongs, the page has a placeholder like this one:
Screenshot: what the picture shows. (images/example.png)
The file name in parentheses is where the image goes, under src/images/.
Prerequisites
Rusteal needs three things on the machine: Unreal Engine 5.8, Rust 1.88 or newer, and the C++ toolchain the engine builds with. The versions below come from the engine’s own requirements.
Linux (x64)
| Dependency | Version | Where from | What for |
|---|---|---|---|
| A C toolchain | any | build-essential (Debian, Ubuntu), gcc (Fedora), base-devel (Arch) | the linker Rust uses (cc) |
| Rust | stable, 1.88 or newer | rustup | installing the rusteal CLI, building your game crate |
| Unreal Engine | 5.8 | Epic’s Linux build (unrealengine.com/linux) or built from source | the game |
The engine brings the rest: the clang toolchain the Unreal Build Tool compiles with and the .NET SDK that runs it.
Windows (x64)
| Dependency | Version | Where from | What for |
|---|---|---|---|
| Visual Studio | Community, Professional or Enterprise; 2022 17.8 or newer, or 2026 18.0 or newer | visualstudio.microsoft.com | the C++ compiler the engine uses, and the linker Rust uses |
| Rust | stable, 1.88 or newer, x86_64-pc-windows-msvc | rustup (rustup-init.exe) | installing the rusteal CLI, building your game crate |
| Unreal Engine | 5.8 | the Epic Games Launcher | the game |
Install Visual Studio before Rust: rustup uses its build tools for the MSVC toolchain.
Visual Studio workloads
In the Visual Studio Installer (Modify on an existing install), the Workloads tab needs two workloads:
| Workload | Section | What the engine takes from it |
|---|---|---|
| Desktop development with C++ | Desktop & Mobile | MSVC, the C++ compiler and linker; the Windows SDK |
| .NET desktop development | Desktop & Mobile | the .NET Framework SDK |
Screenshot: the Workloads tab of the Visual Studio Installer with both workloads checked. (images/getting_started/vs_workloads.png)
Then, in the Individual components tab, check that these are selected:
- MSVC v143 x64/x86 build tools 14.44 for Visual Studio 2022, or MSVC x64/x86 build tools 14.50 for Visual Studio 2026. The engine refuses 14.39 to 14.43, 14.44 before 14.44.35211 and 14.50 before 14.50.35723;
- Windows 11 SDK 10.0.22621 (10.0.19041 at least);
- .NET Framework 4.6.2 targeting pack.
Screenshot: the Individual components tab with the MSVC build tools, the Windows SDK and the .NET Framework targeting pack selected. (images/getting_started/vs_components.png)
The Rust host
Rust must use the MSVC host. rustup show prints:
Default host: x86_64-pc-windows-msvc
If your default toolchain is a -windows-gnu one, for another project, it can stay. Set the default host and install the MSVC toolchain:
rustup set default-host x86_64-pc-windows-msvc
rustup toolchain install stable-x86_64-pc-windows-msvc
Then pin it in your game project with a rust-toolchain.toml, or for the directory that holds your projects:
rustup override set stable-x86_64-pc-windows-msvc --path <dir>
macOS
Rusteal has not been tested on macOS yet.
Installing the CLI
Everything you do with Rusteal goes through one command, rusteal. Install it from crates.io:
cargo install rusteal
The binary carries the Unreal plugins and the project templates inside it: there is nothing else to download.
Telling Rusteal where the engine is
The first command that needs the engine asks where it is installed:
Unreal Engine root (the directory with Engine/ and Templates/):
Answer with the directory that holds Engine/, for example the engine’s install directory from the Epic Games Launcher on Windows, or the directory you extracted or built the engine in on Linux. Rusteal checks that the directory has Engine/Build/BatchFiles and asks again if it does not.
The answer is saved in the CLI’s configuration file, ~/.config/rusteal/config.toml on Linux (the platform’s configuration directory elsewhere), under [engine].path. It is the only setting Rusteal keeps per machine: everything else lives in the project. To point Rusteal at another engine, edit that file.
Checking the version
rusteal --version
A project is tied to the Rusteal version that created it. See Versions and upgrades.
Your first project
The goal: a new Unreal project with a Rust actor that writes to the log when the game starts.
Creating the project
rusteal new MyGame --dir ~/Projects
rusteal new does everything a first build needs:
- creates the Unreal project in
~/Projects/MyGame, from the engine’s Blank template; - installs the Rusteal plugins into its
Plugins/; - writes the Rust workspace in
Rust/, with your game crate and aHelloActor; - runs the whole build pipeline: it builds the project with the Unreal Build Tool, generates the Rust bindings for your engine, builds the game crate and deploys the library.
The first build takes a while: it compiles the engine module and generates bindings for the whole engine API. The builds after it only compile your crate.
rusteal new does not run git init, but it writes a .gitignore that covers Unreal’s and Cargo’s build output, so the right files are picked up when you do.
The Rust code
The game crate is in Rust/my-game/. Its src/lib.rs declares the library and its modules:
#![allow(unused)]
fn main() {
rusteal_runtime::entry!();
mod hello;
}
entry!() is required once per library: it is the function the Unreal plugin calls when it loads the library. Every other file is a normal Rust module.
src/hello.rs declares the actor:
#![allow(unused)]
fn main() {
use bindings::prelude::*;
use rusteal_runtime::{uclass, uclass_impl};
#[uclass(parent = Actor)]
pub struct HelloActor {}
#[uclass_impl]
impl HelloActor {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
log::info!("[MyGame] Hello from Rust");
}
}
}
#[uclass(parent = Actor)]makesHelloActoran Unreal class whose parent isAActor.#[uclass_impl]marks the block where its Unreal functions are.#[ufunction(Override)]onreceive_begin_playimplements the engine’sReceiveBeginPlayevent, the one a Blueprint’s Event BeginPlay node implements.log::info!writes to the Output Log (see Logging).
In the editor
- Open
MyGame.uproject. - In the main menu, open Window > Place Actors.
- In the Place Actors panel, type
HelloActorin the search box. - Drag HelloActor from the panel into the level viewport.
- Press Play in the toolbar.
- Open the Output Log tab (Window > Output Log if it is not open).
The log shows:
LogRusteal: [MyGame] Hello from Rust
Screenshot: the Place Actors panel with HelloActor found by the search, and the Output Log showing the message. (images/getting_started/hello_actor.png)
Changing the code
Change the message in hello.rs, then build the crate again:
cd ~/Projects/MyGame
rusteal build
The editor reloads the library as soon as the build deploys it, without a restart, and shows a notification. Press Play again to see the new message. The iteration loop explains what a reload changes, and how rusteal watch builds on every save.
The flags of rusteal new
| Flag | What it does |
|---|---|
--dir <path> | The directory the project is created in (the current one by default). |
--template <name> | The template to start from, blank by default. See Templates. |
--variant <name> | The template’s variant. |
--no-build | Writes the project and stops; run rusteal build --all later. |
Project layout
A Rusteal project is an Unreal project with a Cargo workspace inside it. Everything it needs lives in its directory; the only thing outside is the engine path in the CLI’s configuration.
MyGame/
├── MyGame.uproject
├── rusteal.toml # Rusteal's project configuration
├── Source/MyGame/ # the C++ module the Unreal Build Tool needs
├── Content/ # the project's assets
├── Config/ # the project's ini files
├── Plugins/Rusteal/ # the runtime plugin and the generated C++ wrappers
├── Plugins/RustealGenerator/ # the plugin that exports the engine's reflection
├── Intermediate/Rusteal/uht/ # the exported reflection (build output)
└── Rust/
├── Cargo.toml # the workspace
├── my-game/ # your game crate
└── bindings/ # the generated bindings crate
What to version
Version everything except the build output, which the .gitignore written by rusteal new already leaves out. That includes Rust/bindings/ and the generated C++ wrappers in Plugins/Rusteal/: they are generated, but a clone of the project builds without generating them again.
The workspace
Rust/Cargo.toml lists the crates and pins the Rusteal crates to the version that created the project:
[workspace]
members = ["my-game", "bindings"]
resolver = "3"
[workspace.dependencies]
rusteal-runtime = "=x.y.z"
rusteal-core = "=x.y.z"
rusteal-ffi = "=x.y.z"
glam = "0.34"
log = "0.4"
It also holds the build profiles, which only a workspace’s Cargo.toml can set. See Development and release builds.
The game crate
Rust/my-game/Cargo.toml makes the crate a dynamic library and depends on the runtime and the bindings:
[package]
name = "my-game"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
rusteal-runtime = { workspace = true }
bindings = { path = "../bindings" }
glam = { workspace = true }
log = { workspace = true }
Add any crate from crates.io to [dependencies] as in any Rust project: serde, rand, rayon, a physics or a pathfinding library. They are compiled into your game’s library.
Your code goes in Rust/my-game/src/. lib.rs starts with #![deny(unsafe_code)], calls rusteal_runtime::entry!() once and declares the modules:
#![allow(unused)]
#![deny(unsafe_code)]
fn main() {
rusteal_runtime::entry!();
mod hello;
}
A game written with Rusteal never needs unsafe. Everything a game does, Rusteal’s API does in safe Rust, and the code its macros generate is accepted by #![deny(unsafe_code)]; the line makes the compiler refuse an unsafe that slips in, so keep it. If something seems to need unsafe, look for the safe call in this handbook; if there is none, it is a gap in Rusteal to report, not something to work around.
How you split the rest into modules is up to you; how you split the rest into modules is up to you. The templates keep each class in its own file, and the rules that do not touch the engine in a model.rs next to it, where cargo test can test them (see Testing without the engine).
The bindings crate
Rust/bindings/ is generated from the engine you build with, and only for the engine modules rusteal.toml lists. Do not edit it: the next generation overwrites it. To bind more of the engine (AI, Niagara, StateTree, Gameplay Tags…), see Binding more of the engine.
Where the library goes
rusteal build deploys the library to Plugins/Rusteal/Binaries/<Platform>/: librusteal.so on Linux, rusteal.dll on Windows. The plugin loads it from there when the editor or the game starts.
Starting from a template
The goal: a playable project in a genre, with its gameplay already written in Rust, to build your game on.
rusteal new MyGame --template third-person
rusteal new MyGame --template third-person --variant combat
A template is one of the engine’s templates with its gameplay written in Rust. The C++ classes are gone, the Rust crate in Rust/ takes their place, and their Blueprint children, with the same names, have Rust parents. The engine’s Blueprint-only templates (vr, ar, intro) keep their Blueprints, reparented to Rust classes and holding only data: meshes, materials, effects, input assets, widget layouts and Anim Blueprint graphs. Open the project and press Play: it plays like the engine’s template.
The engine’s templates come with variants, genre starting points that the C++ version puts all in one project. In Rusteal each variant is its own project: --variant picks one, and without it the project is the template alone. A variant project opens on the variant’s level.
| Template | Variant | What it is |
|---|---|---|
blank (default) | The engine’s Blank template and a HelloActor in Rust. | |
first-person | The First Person template: its character, camera manager, game mode and player controller in Rust (keyboard, mouse, gamepad, touch). | |
first-person | horror | A dark level explored with a flashlight, and a sprint that runs on stamina, its meter on screen. |
first-person | shooter | An arena shooter: weapon pickups from a data table, projectiles and grenades, team scores, and NPCs run by a StateTree that sense, snipe and shoot. |
top-down | The Top Down template: its character, game mode and point and click player controller in Rust (mouse, touch). | |
top-down | strategy | A real-time strategy start: units selected with clicks, boxes and touches, sent along the navigation mesh by EnvQueries, and interacting with the units they reach. |
top-down | twin-stick | A twin stick shooter: projectiles, dashes and area attacks against waves of enemies run by a StateTree, pickups and a combo score. |
intro | The Intro To Unreal template: its guided tour level, whose stations are the four features (see Adding features), and its first person character in Rust. Follow the tour from the first station. | |
third-person | The Third Person template: its character, game mode and player controller in Rust (keyboard, mouse, gamepad, touch). | |
third-person | combat | Melee combat: combo and charged attacks, enemies run by a StateTree, spawners, checkpoints, damageable props. |
third-person | platforming | Double jump, wall jump, coyote time, a dash, local multiplayer and respawn. |
third-person | side-scrolling | A side view platformer: soft platforms, jump pads, moving platforms, pickups with a counter and an NPC run by a StateTree. |
vehicle | The Vehicle template: a sports car and an offroad car on Chaos Vehicles, their game mode, player controller and speed and gear HUD in Rust (keyboard, gamepad, steering wheel, touch). | |
vehicle | offroad | The offroad car on an offroad level, its game mode ready for local split screen players. |
vehicle | time-trial | A race track with a start countdown, gates to pass in order, laps and the best lap time. |
ar | The Handheld AR template on ARCore and ARKit: its pawn (touch gestures to pick a detected plane, place the object on it, move, rotate and scale it; light estimation), plane and placeable actors and its styled menus in Rust. It runs in the editor; Rusteal does not build for Android or iOS yet. | |
vr | The Virtual Reality template on OpenXR: its pawn (teleport, snap turn, hand poses), grabbable objects, pistol, 3D menu, passthrough and spectator camera in Rust. Play it with VR Preview. |
Names are matched loosely (side-scrolling or SideScrolling), and rusteal new with an unknown template or variant lists them all, with the features any template can take (see Adding features).
Each template has a page of its own, linked from the table: how to play it, its Rust classes and its variants.
Reading a template
The templates are the largest body of Rusteal code there is, and the Gameplay recipes in this book are taken from them. In a template project:
- the Rust classes are in
Rust/<crate>/src/, one per file, with a variant’s classes in a module named after it (src/combat/,src/shooter/); - the tunable numbers and the rules that do not call the engine are in the
model.rsfiles, with their tests; - the Blueprint children that assign meshes, animations, input actions and widgets are in
Content/, under the same names as in the engine’s template (BP_ThirdPersonCharacter,BP_ThirdPersonPlayerController…).
To see where a Rust class is used, open its Blueprint child in the editor: Class Settings shows the Rust class as the Parent Class, and the Details panel shows the properties the Rust class declares.
Screenshot: BP_ThirdPersonCharacter open in the Blueprint editor, with Class Settings showing ThirdPersonCharacter as the Parent Class. (images/getting_started/bp_parent_class.png)
blank
The engine’s Blank template and one Rust class, HelloActor, to see the whole loop work before anything else. It is the default: rusteal new MyGame makes it.
rusteal new MyGame --template blank
From TP_Blank, the engine template it is made of.
Playing it
Open <Name>.uproject, drag a HelloActor from the Place Actors panel into the level and press Play: the Output Log shows [<Name>] Hello from Rust.
Screenshot: the blank template’s HelloActor message in the Output Log. (images/templates/blank.png)
Its classes
The class is in Rust/<crate>/src/hello.rs, the place to start writing your own.
| Class | Parent | File |
|---|---|---|
HelloActor | Actor | src/hello.rs |
first-person
The engine’s First Person template, playable as it comes with keyboard and mouse, gamepad or touch: its character with first person arms and camera, camera manager, game mode and player controller are Rust classes, and their Blueprint children keep the meshes, animations and input assets.
rusteal new MyGame --template first-person
From TP_FirstPerson, the engine template it is made of.
Playing it
Open the project and press Play: WASD / left stick move, mouse / right stick look, Space / A jump.
Its classes
The Rust classes are in Rust/<crate>/src/; their Blueprint children, under the engine template’s names, are in Content/.
| Class | Parent | File |
|---|---|---|
FirstPersonCameraManager | PlayerCameraManager | src/camera_manager.rs |
FirstPersonCharacter | Character | src/character.rs |
FirstPersonGameMode | GameModeBase | src/game_mode.rs |
FirstPersonPlayerController | PlayerController | src/player_controller.rs |
Variants
Each variant is a project of its own: the template plus the variant, opening on the variant’s level.
horror
A dark level explored with a flashlight, and a sprint that runs on stamina, its meter on screen.
rusteal new MyGame --template first-person --variant horror
It opens /Game/Variant_Horror/Lvl_Horror. To play: WASD / left stick move, mouse / right stick look, Space / A jump, hold Shift / LB sprint.
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
HorrorCharacter | FirstPersonCharacter | src/horror/character.rs |
HorrorGameMode | GameModeBase | src/horror/game_mode.rs |
HorrorPlayerController | PlayerController | src/horror/player_controller.rs |
HorrorUI | UserWidget | src/horror/ui.rs |
shooter
An arena shooter: weapon pickups described by a data table, projectiles and grenades, team scores, and NPCs run by a StateTree that sense their enemies, snipe and shoot.
rusteal new MyGame --template first-person --variant shooter
It opens /Game/Variant_Shooter/Lvl_Shooter. To play: WASD / left stick move, mouse / right stick look, Space / A jump, left click / RT shoot, Left Shift / Y switch weapons.
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
ShooterAIController | AIController | src/shooter/ai_controller.rs |
ShooterCharacter | FirstPersonCharacter | src/shooter/character.rs |
EnvQueryContextTarget | EnvQueryContext_BlueprintBase | src/shooter/env_query.rs |
ShooterGameMode | GameModeBase | src/shooter/game_mode.rs |
ShooterNPC | FirstPersonCharacter | src/shooter/npc.rs |
ShooterNPCSpawner | Actor | src/shooter/npc_spawner.rs |
ShooterPickup | Actor | src/shooter/pickup.rs |
WeaponTableRow | a struct | src/shooter/pickup.rs |
ShooterPlayerController | PlayerController | src/shooter/player_controller.rs |
ShooterProjectile | Actor | src/shooter/projectile.rs |
StateTreeLineOfSightToTargetCondition | StateTreeConditionBlueprintBase | src/shooter/state_tree.rs |
StateTreeFaceActorTask | StateTreeTaskBlueprintBase | src/shooter/state_tree.rs |
StateTreeFaceLocationTask | StateTreeTaskBlueprintBase | src/shooter/state_tree.rs |
StateTreeSetRandomFloatTask | StateTreeTaskBlueprintBase | src/shooter/state_tree.rs |
StateTreeShootAtTargetTask | StateTreeTaskBlueprintBase | src/shooter/state_tree.rs |
StateTreeSenseEnemiesTask | StateTreeTaskBlueprintBase | src/shooter/state_tree.rs |
ShooterUI | UserWidget | src/shooter/ui.rs |
ShooterBulletCounterUI | UserWidget | src/shooter/ui.rs |
ShooterWeapon | Actor | src/shooter/weapon.rs |
Recipes taken from it: Character movement and input, Cameras, Projectiles and weapons.
third-person
The engine’s Third Person template, playable as it comes with keyboard and mouse, gamepad or touch: its character with its spring arm camera, game mode and player controller are Rust classes.
rusteal new MyGame --template third-person
From TP_ThirdPerson, the engine template it is made of.
Playing it
Open the project and press Play: WASD / left stick move, mouse / right stick look, Space / A jump.
Its classes
The Rust classes are in Rust/<crate>/src/; their Blueprint children, under the engine template’s names, are in Content/.
| Class | Parent | File |
|---|---|---|
ThirdPersonCharacter | Character | src/character.rs |
ThirdPersonGameMode | GameModeBase | src/game_mode.rs |
ThirdPersonPlayerController | PlayerController | src/player_controller.rs |
Variants
Each variant is a project of its own: the template plus the variant, opening on the variant’s level.
combat
Melee combat: combo and charged attacks driven by animation notifies, enemies run by a StateTree, enemy spawners, checkpoints and respawn, damageable props and a lava floor.
rusteal new MyGame --template third-person --variant combat
It opens /Game/Variant_Combat/Lvl_Combat. To play: WASD / left stick move, mouse / right stick look, left click / RB combo attack, hold right click / RT charged attack, R / D-pad down switch the camera side.
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
CombatAIController | AIController | src/combat/ai_controller.rs |
AnimNotifyDoAttackTrace | AnimNotify | src/combat/anim_notifies.rs |
AnimNotifyCheckCombo | AnimNotify | src/combat/anim_notifies.rs |
AnimNotifyCheckChargedAttack | AnimNotify | src/combat/anim_notifies.rs |
CombatCharacter | Character | src/combat/character.rs |
CombatDamageableBox | Actor | src/combat/damageable_box.rs |
CombatDummy | Actor | src/combat/dummy.rs |
CombatEnemy | Character | src/combat/enemy.rs |
CombatEnemySpawner | Actor | src/combat/enemy_spawner.rs |
EnvQueryContextDanger | EnvQueryContext_BlueprintBase | src/combat/env_query.rs |
EnvQueryContextPlayer | EnvQueryContext_BlueprintBase | src/combat/env_query.rs |
CombatGameMode | GameModeBase | src/combat/game_mode.rs |
CombatLavaFloor | Actor | src/combat/lava_floor.rs |
CombatLifeBar | UserWidget | src/combat/life_bar.rs |
CombatPlayerController | PlayerController | src/combat/player_controller.rs |
StateTreeCharacterGroundedCondition | StateTreeConditionBlueprintBase | src/combat/state_tree.rs |
StateTreeIsInDangerCondition | StateTreeConditionBlueprintBase | src/combat/state_tree.rs |
StateTreeComboAttackTask | StateTreeTaskBlueprintBase | src/combat/state_tree.rs |
StateTreeChargedAttackTask | StateTreeTaskBlueprintBase | src/combat/state_tree.rs |
StateTreeWaitForLandingTask | StateTreeTaskBlueprintBase | src/combat/state_tree.rs |
StateTreeFaceActorTask | StateTreeTaskBlueprintBase | src/combat/state_tree.rs |
StateTreeFaceLocationTask | StateTreeTaskBlueprintBase | src/combat/state_tree.rs |
StateTreeSetCharacterSpeedTask | StateTreeTaskBlueprintBase | src/combat/state_tree.rs |
StateTreeGetPlayerInfoTask | StateTreeTaskBlueprintBase | src/combat/state_tree.rs |
CombatActivationVolume | Actor | src/combat/volumes.rs |
CombatCheckpointVolume | Actor | src/combat/volumes.rs |
platforming
Double jump, wall jump, coyote time, a dash, local multiplayer and respawn.
rusteal new MyGame --template third-person --variant platforming
It opens /Game/Variant_Platforming/Lvl_Platforming. To play: WASD / left stick move, mouse / right stick look, Space / A jump (again in the air: double jump, against a wall: wall jump), Shift / B dash.
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
AnimNotifyEndDash | AnimNotify | src/platforming/anim_notify_end_dash.rs |
PlatformingCharacter | Character | src/platforming/character.rs |
PlatformingGameMode | GameModeBase | src/platforming/game_mode.rs |
PlatformingPlayerController | PlayerController | src/platforming/player_controller.rs |
side-scrolling
A side view platformer: soft platforms to drop through, jump pads, moving platforms, pickups with a counter, interactables and an NPC run by a StateTree.
rusteal new MyGame --template third-person --variant side-scrolling
It opens /Game/Variant_SideScrolling/Lvl_SideScrolling. To play: A/D / left stick move, Space / A jump (again in the air: double jump, against a wall: wall jump), S / stick down + Space drop through a platform, F / X interact.
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
SideScrollingAIController | AIController | src/side_scrolling/ai_controller.rs |
SideScrollingCameraManager | PlayerCameraManager | src/side_scrolling/camera_manager.rs |
SideScrollingCharacter | Character | src/side_scrolling/character.rs |
SideScrollingGameMode | GameModeBase | src/side_scrolling/game_mode.rs |
SideScrollingJumpPad | Actor | src/side_scrolling/jump_pad.rs |
SideScrollingMovingPlatform | Actor | src/side_scrolling/moving_platform.rs |
SideScrollingNPC | Character | src/side_scrolling/npc.rs |
SideScrollingPickup | Actor | src/side_scrolling/pickup.rs |
SideScrollingPlayerController | PlayerController | src/side_scrolling/player_controller.rs |
SideScrollingSoftPlatform | Actor | src/side_scrolling/soft_platform.rs |
StateTreeGetPlayerTask | StateTreeTaskBlueprintBase | src/side_scrolling/state_tree.rs |
SideScrollingUI | UserWidget | src/side_scrolling/ui.rs |
Recipes taken from it: Character movement and input, Health, damage and death, Platforming, AI.
top-down
The engine’s Top Down template, point and click with the mouse or touch: its character, game mode and player controller are Rust classes. The engine’s own version is Blueprint-only at its base; here its Blueprints are children of the Rust classes.
rusteal new MyGame --template top-down
From TP_TopDown, the engine template it is made of.
Playing it
Open the project and press Play: click (or hold) / tap where the character should go.
Its classes
The Rust classes are in Rust/<crate>/src/; their Blueprint children, under the engine template’s names, are in Content/.
| Class | Parent | File |
|---|---|---|
TopDownCharacter | Character | src/character.rs |
TopDownGameMode | GameModeBase | src/game_mode.rs |
TopDownPlayerController | PlayerController | src/player_controller.rs |
Variants
Each variant is a project of its own: the template plus the variant, opening on the variant’s level.
strategy
A real-time strategy start: units selected with clicks, boxes and touches, sent along the navigation mesh by EnvQueries, and interacting with the units they reach.
rusteal new MyGame --template top-down --variant strategy
It opens /Game/Variant_Strategy/LVL_Strategy. To play: left click / drag selects units (Shift adds, double click selects all), right click sends them, WASD / right drag pans, wheel / Q E zoom.
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
EnvQueryContextMoveGoal | EnvQueryContext_BlueprintBase | src/strategy/env_query.rs |
StrategyGameMode | GameModeBase | src/strategy/game_mode.rs |
StrategyHUD | HUD | src/strategy/hud.rs |
StrategyPawn | Pawn | src/strategy/pawn.rs |
StrategyPlayerController | PlayerController | src/strategy/player_controller.rs |
StrategyTouchControls | UserWidget | src/strategy/touch_controls.rs |
StrategyUI | UserWidget | src/strategy/ui.rs |
StrategyUnit | Character | src/strategy/unit.rs |
twin-stick
A twin stick shooter: projectiles, dashes and area attacks against waves of enemies run by a StateTree, pickups and a combo score.
rusteal new MyGame --template top-down --variant twin-stick
It opens /Game/Variant_TwinStick/LVL_TwinStick. To play: WASD / left stick move, mouse / right stick aim, left click / RT shoot, Left Shift / LB dash, right click / LT area attack.
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
TwinStickAIController | AIController | src/twin_stick/ai_controller.rs |
TwinStickAoEAttack | Actor | src/twin_stick/aoe_attack.rs |
TwinStickCharacter | Character | src/twin_stick/character.rs |
TwinStickGameMode | GameModeBase | src/twin_stick/game_mode.rs |
TwinStickNPC | Character | src/twin_stick/npc.rs |
TwinStickNPCDestruction | Actor | src/twin_stick/npc_destruction.rs |
TwinStickPickup | Actor | src/twin_stick/pickup.rs |
TwinStickPlayerController | PlayerController | src/twin_stick/player_controller.rs |
TwinStickProjectile | Actor | src/twin_stick/projectile.rs |
TwinStickSpawner | Actor | src/twin_stick/spawner.rs |
StateTreeGetPlayerTask | StateTreeTaskBlueprintBase | src/twin_stick/state_tree.rs |
TwinStickUI | UserWidget | src/twin_stick/ui.rs |
Recipes taken from it: Point and click, AI, Projectiles and weapons.
vehicle
The engine’s Vehicle template on Chaos Vehicles: a sports car and an offroad car, with their wheels, the game mode, the player controller and the speed and gear HUD in Rust. It plays with keyboard, gamepad, steering wheel or touch.
rusteal new MyGame --template vehicle
From TP_VehicleAdv, the engine template it is made of.
Playing it
Open the project and press Play: W / right trigger throttle, S / left trigger brake and reverse, A D / left stick steer, Space / left shoulder handbrake, mouse / right stick look around, Tab toggles the camera, Backspace resets the car.
Screenshot: the vehicle template’s sports car on the track. (images/templates/vehicle.png)
Its classes
The Rust classes are in Rust/<crate>/src/; their Blueprint children, under the engine template’s names, are in Content/.
| Class | Parent | File |
|---|---|---|
VehicleAdvGameMode | GameModeBase | src/game_mode.rs |
VehicleAdvOffroadCar | VehicleAdvPawn | src/offroad_car.rs |
VehicleAdvOffroadWheelFront | VehicleAdvWheelFront | src/offroad_car.rs |
VehicleAdvOffroadWheelRear | VehicleAdvWheelRear | src/offroad_car.rs |
VehicleAdvPawn | WheeledVehiclePawn | src/pawn.rs |
VehicleAdvPlayerController | PlayerController | src/player_controller.rs |
VehicleAdvSportsCar | VehicleAdvPawn | src/sports_car.rs |
VehicleAdvSportsWheelFront | VehicleAdvWheelFront | src/sports_car.rs |
VehicleAdvSportsWheelRear | VehicleAdvWheelRear | src/sports_car.rs |
VehicleAdvUI | UserWidget | src/ui.rs |
VehicleAdvWheelFront | ChaosVehicleWheel | src/wheels.rs |
VehicleAdvWheelRear | ChaosVehicleWheel | src/wheels.rs |
Variants
Each variant is a project of its own: the template plus the variant, opening on the variant’s level.
offroad
The offroad car on an offroad level, its game mode ready for local split screen players.
rusteal new MyGame --template vehicle --variant offroad
It opens /Game/Variant_OffRoad/Maps/Lvl_Offroad. To play: W / right trigger throttle, S / left trigger brake and reverse, A D / left stick steer, Space / left shoulder handbrake, mouse / right stick look around, Tab toggles the camera, Backspace resets the car.
Screenshot: the vehicle template’s offroad variant in play. (images/templates/vehicle-offroad.png)
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
OffroadGameMode | GameModeBase | src/offroad/game_mode.rs |
time-trial
A race track with a start countdown, gates to pass in order, laps and the best lap time.
rusteal new MyGame --template vehicle --variant time-trial
It opens /Game/Variant_TimeTrial/Maps/Lvl_Timetrial. To play: W / right trigger throttle, S / left trigger brake and reverse, A D / left stick steer, Space / left shoulder handbrake, mouse / right stick look around, Tab toggles the camera, Backspace resets the car.
Screenshot: the vehicle template’s time-trial variant in play. (images/templates/vehicle-time-trial.png)
Its own classes, next to the template’s, in a module named after it:
| Class | Parent | File |
|---|---|---|
TimeTrialGameMode | GameModeBase | src/time_trial/game_mode.rs |
TimeTrialPlayerController | PlayerController | src/time_trial/player_controller.rs |
TimeTrialTrackGate | Actor | src/time_trial/track_gate.rs |
TimeTrialUI | UserWidget | src/time_trial/ui.rs |
TimeTrialStartUI | UserWidget | src/time_trial/ui.rs |
Recipes taken from it: Vehicles.
intro
The engine’s Intro To Unreal template: a guided tour level whose stations show the editor and the engine at work. The stations are the four features any template can take, mannequin, checkpoints, interactables and tour, which rusteal new adds by itself; the level’s Blueprints are children of their Rust classes. The first person character is the first-person template’s, with IntroGameMode as the level’s game mode; the checkpoints’ subsystem brings the player back to the last spawn point.
rusteal new MyGame --template intro
From TP_UEIntro_BP, the engine template it is made of.
Playing it
Open the project and press Play: WASD / left stick move, mouse / right stick look, Space / A jump; follow the tour from the first station.
Screenshot: the intro template’s tour level, at its first station. (images/templates/intro.png)
Its classes
The Rust classes are in Rust/<crate>/src/; their Blueprint children, under the engine template’s names, are in Content/.
| Class | Parent | File |
|---|---|---|
FirstPersonCameraManager | PlayerCameraManager | src/camera_manager.rs |
FirstPersonCharacter | Character | src/character.rs |
FirstPersonGameMode | GameModeBase | src/game_mode.rs |
IntroGameMode | GameModeBase | src/game_mode.rs |
FirstPersonPlayerController | PlayerController | src/player_controller.rs |
| Recipes taken from it: Saving the game, Game flow, Animation. |
ar
The engine’s Handheld AR template on ARCore and ARKit: its pawn reads touch gestures to pick a detected plane, place the object on it, then move, rotate and scale it, and applies the light estimation; the plane and placeable actors and the styled menus are Rust classes. It runs in the editor; Rusteal does not build for Android or iOS yet (see Known limits).
rusteal new MyGame --template ar
From TP_HandheldARBP, the engine template it is made of.
Playing it
Open the project and press Play: tap Begin Scan, tap a plane, tap it again to place the object, drag with one finger to move it, twist or pinch with two to rotate or scale it.
Screenshot: the ar template’s start menu in the editor. (images/templates/ar.png)
Its classes
The Rust classes are in Rust/<crate>/src/; their Blueprint children, under the engine template’s names, are in Content/.
| Class | Parent | File |
|---|---|---|
CapsuleButton | StylizedUI | src/buttons.rs |
ToggleButton | StylizedUI | src/buttons.rs |
ARGameMode | GameModeBase | src/game_mode.rs |
ARPlayerController | PlayerController | src/game_mode.rs |
StartMenu | StylizedUI | src/menus.rs |
BottomMenu | StylizedUI | src/menus.rs |
InfoMenu | StylizedUI | src/menus.rs |
OptionMenu | StylizedUI | src/menus.rs |
MainMenu | UserWidget | src/menus.rs |
InteractionHUD | UserWidget | src/menus.rs |
ARPawn | Pawn | src/pawn.rs |
ProductAssetRef | ActorComponent | src/placeable.rs |
Placeable | Actor | src/placeable.rs |
Plane | Actor | src/plane.rs |
StylizedUI | UserWidget | src/styles.rs |
StructIcon | a struct | src/styles.rs |
StructStyle | a struct | src/styles.rs |
| Recipes taken from it: Augmented reality. |
vr
The engine’s Virtual Reality template on OpenXR: its pawn teleports along the navigation mesh, snap turns and poses the hands; grabbable objects, a pistol and its projectiles, a 3D menu, passthrough and a spectator camera are Rust classes. Play it with Play > VR Preview and an OpenXR headset.
rusteal new MyGame --template vr
From TP_VirtualRealityBP, the engine template it is made of.
Playing it
Open the project and press Play: right thumbstick forward aims a teleport, left thumbstick snaps the view, grips grab, triggers shoot the pistol, the menu button opens the menu.
Screenshot: the vr template’s hands and teleport arc in VR Preview. (images/templates/vr.png)
Its classes
The Rust classes are in Rust/<crate>/src/; their Blueprint children, under the engine template’s names, are in Content/.
| Class | Parent | File |
|---|---|---|
XRGameMode | GameModeBase | src/game_mode.rs |
GrabComponent | SceneComponent | src/grab_component.rs |
GrabbableSmallCube | Actor | src/grabbable.rs |
MannequinsXR | SkeletalMeshComponent | src/hand.rs |
HandAnimInstance | AnimInstance | src/hand.rs |
InputModifierXAxisPositiveOnly | InputModifier | src/input_modifier.rs |
Menu | Actor | src/menu.rs |
MenuWidget | UserWidget | src/menu_widget.rs |
Passthrough | Actor | src/passthrough.rs |
XRPawn | Pawn | src/pawn.rs |
Pistol | Actor | src/pistol.rs |
Projectile | Actor | src/projectile.rs |
VRSpectator | Actor | src/spectator.rs |
TeleportVisualizer | Actor | src/teleport_visualizer.rs |
| Recipes taken from it: Virtual reality. |
Adding features
A feature is a piece of gameplay any template can take: a Rusteal plugin with its Rust classes, and the engine content they need. --feature adds it when the project is created, as many times as wanted:
rusteal new MyGame --template third-person --feature mannequin
| Feature | Plugin | What it is |
|---|---|---|
mannequin | Mannequin | CopycatAnimInstance, a parent for Animation Blueprints that copies a character’s movement, and BP_CopycatMannequin, the Intro To Unreal template’s mannequin that walks, runs and jumps with the player; ReachingMannequin, whose right hand reaches for a ReachTarget placed in the level. |
checkpoints | Checkpoints | BP_SpawnPoints that save where the player got to, and a subsystem that starts the level there. |
interactables | Interactables | A trigger (BP_DoorTrigger) and what it drives: lights, a door it unlocks, a confetti burst; a trigger that plays a level sequence, a MetaSound with a seed, and blocks dangling from physics constraints. |
tour | Tour | The pieces of the Intro To Unreal tour: texts that change in game, station titles, animated keyboard keys and mouse, gizmo demos, template cubes, decal arrows along a spline and editor panels shown in game. |
Each feature becomes a plugin in Plugins/<Plugin>/, enabled in the .uproject and built with the project: its Rust classes are in Plugins/<Plugin>/Rust/, and rusteal build builds them with the game’s. When the project lacks the engine content the feature uses, it is copied in: mannequin brings the Characters pack (Content/Characters/Mannequins), which the third person and first person templates already have, and interactables and tour the LevelPrototyping pack. Each feature’s Blueprints are in its plugin’s content: in the Content Browser, click Settings and check Show Plugin Content, then open the plugin’s folder, Plugins > Mannequin Content for the mannequin feature.
The intro template is made of all four: rusteal new MyTour --template intro adds them by itself, and the Blueprints of its tour level are children of their Rust classes.
An existing project takes a feature as a plugin:
rusteal plugin new Mannequin --template mannequin
A feature whose plugin carries assets keeps its plugin name, since its assets live under /<Plugin>/.
Features that add files
Some features add files to the project instead of a plugin: the setup of a tool around it.
| Feature | Adds | What it is |
|---|---|---|
rustrover | Rust/.run/ | RustRover run configurations, stored as project files: Rusteal Watch and Rusteal Test, the same on Linux and Windows. |
mannequin
Mannequins that move like a character: CopycatAnimInstance is a parent for Animation Blueprints that copies a character’s movement (ground speed, direction, moving, falling), the player’s unless told which, and BP_CopycatMannequin is the Intro To Unreal template’s mannequin that walks, runs and jumps with the player. ReachingMannequin reaches for a ReachTarget placed in the level with its right hand, a two-bone IK solved in Rust.
rusteal new MyGame --template third-person --feature mannequin
rusteal plugin new Mannequin --template mannequin # in an existing project
It becomes the Mannequin plugin, in Plugins/Mannequin/. It brings the engine’s Characters pack (Content/Characters/Mannequins) when the project lacks it; the third person and first person templates already have it.
Trying it
Place a BP_CopycatMannequin, and a ReachingMannequin with a ReachTarget as its Target (Plugins > Mannequin Content, Place Actors), then press Play.
Screenshot: the mannequin feature in play. (images/features/mannequin.png)
Its classes
The Rust classes are in Plugins/Mannequin/Rust/mannequin/src/:
| Class | Parent | File |
|---|---|---|
CopycatAnimInstance | AnimInstance | src/copycat.rs |
CopycatMannequin | Actor | src/reach.rs |
ReachTarget | Actor | src/reach.rs |
ReachingMannequin | Actor | src/reach.rs |
Its Blueprints, ABP_Copycat and BP_CopycatMannequin, children of those classes holding the meshes, materials and widget layouts, are in the plugin’s content: in the Content Browser, click Settings and check Show Plugin Content, then open Plugins > Mannequin Content.
A mannequin that copies the player
- Create a project with
--feature mannequin, or add the plugin to an existing one. - In the Content Browser, right-click and choose Animation > Animation Blueprint; pick the SK_Mannequin skeleton and, as the Parent Class, CopycatAnimInstance.
- Open it. In the My Blueprint panel, click the gear icon and check Show Inherited Variables: Ground Speed, Direction, Should Move, Is Falling, Velocity and Character appear under Copycat.
- In its AnimGraph, blend the idle, walk and run animations by Ground Speed and Direction, and switch to a jump loop when Is Falling is set.
- Drag SKM_Manny_Simple from
Content/Characters/Mannequins/Meshesinto the level. - In its Details panel, set Animation Mode to Use Animation Blueprint.
- Set Anim Class to the new Animation Blueprint.
- Press Play and walk: the mannequin walks, runs and jumps with you.
Screenshot: a copycat mannequin running beside the player in the third person level. (images/getting_started/feature_copycat.png)
The copying is plain Rust in Plugins/Mannequin/Rust/mannequin/src/copycat.rs: the speed ignores the vertical axis, the mannequin moves only when the speed is above 0.01 and the character accelerates, and the direction is kept within 45 degrees when the character turns toward its movement.
Recipes that use it: Animation.
checkpoints
Spawn points that save the player’s progress: walking past a BP_SpawnPoint writes it to a save (CheckpointSaveData), and a world subsystem, CheckpointSubsystem, brings the player back to the last one when the level starts, or when RespawnPlayer is called.
rusteal new MyGame --template third-person --feature checkpoints
rusteal plugin new Checkpoints --template checkpoints # in an existing project
It becomes the Checkpoints plugin, in Plugins/Checkpoints/. It needs no engine content of its own.
Trying it
Place BP_SpawnPoint actors (Plugins > Checkpoints Content) along the level, press Play, walk past one, stop and play again: you start there.
Screenshot: the checkpoints feature in play. (images/features/checkpoints.png)
Its classes
The Rust classes are in Plugins/Checkpoints/Rust/checkpoints/src/:
| Class | Parent | File |
|---|---|---|
CheckpointSaveData | SaveGame | src/save_data.rs |
SpawnPoint | Actor | src/spawn_point.rs |
CheckpointSubsystem | RustealWorldSubsystem | src/subsystem.rs |
Its Blueprint, BP_SpawnPoint, a child of those classes holding the meshes and materials, is in the plugin’s content: in the Content Browser, click Settings and check Show Plugin Content, then open Plugins > Checkpoints Content.
Recipes that use it: Saving the game.
interactables
A trigger and what it drives: BP_DoorTrigger is a switch that broadcasts its Trigger event, which toggles BP_Lights and unlocks a BP_DoorFrame_Unlockable. Around them: BP_ConfettiTrigger, a Niagara burst; BP_SequenceTrigger, which plays a level sequence; BP_Metasound, a MetaSound with a seed parameter; BP_DangleBlocks, cubes hanging from physics constraints.
rusteal new MyGame --template third-person --feature interactables
rusteal plugin new Interactables --template interactables # in an existing project
It becomes the Interactables plugin, in Plugins/Interactables/. It brings the engine’s LevelPrototyping pack (door meshes, cubes) when the project lacks it.
Trying it
Place a BP_DoorTrigger and a BP_DoorFrame_Unlockable (Plugins > Interactables Content), set the door’s Trigger to the trigger, press Play and step on it.
Screenshot: the interactables feature in play. (images/features/interactables.png)
Its classes
The Rust classes are in Plugins/Interactables/Rust/interactables/src/:
| Class | Parent | File |
|---|---|---|
ConfettiTrigger | Actor | src/confetti_trigger.rs |
DangleBlocks | Actor | src/dangle_blocks.rs |
UnlockableDoor | Actor | src/door.rs |
TriggerLight | Actor | src/light.rs |
SeededSound | Actor | src/seeded_sound.rs |
SequenceTrigger | Actor | src/sequence_trigger.rs |
TriggerSwitch | Actor | src/trigger.rs |
Its Blueprints, BP_ConfettiTrigger, BP_DangleBlocks, BP_DoorFrame_Unlockable, BP_DoorTrigger, BP_Light, BP_Metasound and BP_SequenceTrigger, children of those classes holding the meshes, materials and widget layouts, are in the plugin’s content: in the Content Browser, click Settings and check Show Plugin Content, then open Plugins > Interactables Content.
Recipes that use it: Game flow, Physics, Effects and sound.
tour
The pieces of the Intro To Unreal tour: BP_TextSwitcher, text that changes in game; BP_Titles, the station numbers; animated BP_KeyboardKeys and BP_MouseKeys; BP_GizmoDemo, the move, rotate and scale gizmos at work; BP_TemplateCube; BP_SplinePath, decal arrows along a spline; and BP_UI_Update, editor panels shown in game.
rusteal new MyGame --template third-person --feature tour
rusteal plugin new Tour --template tour # in an existing project
It becomes the Tour plugin, in Plugins/Tour/. It brings the engine’s LevelPrototyping pack when the project lacks it.
Trying it
Place a BP_KeyboardKey (Plugins > Tour Content), set its Key, and a BP_GizmoDemo with a Selection, then press Play.
Screenshot: the tour feature in play. (images/features/tour.png)
Its classes
The Rust classes are in Plugins/Tour/Rust/tour/src/:
| Class | Parent | File |
|---|---|---|
EditorHintWidget | UserWidget | src/editor_hint.rs |
TourSubsystem | RustealWorldSubsystem | src/editor_hint.rs |
EditorHintTrigger | Actor | src/editor_hint.rs |
GizmoDemo | Actor | src/gizmo_demo.rs |
KeyboardKeyWidget | UserWidget | src/keys.rs |
MouseWidget | UserWidget | src/keys.rs |
KeyboardKey | Actor | src/keys.rs |
MouseKey | Actor | src/keys.rs |
SplinePath | Actor | src/spline_path.rs |
TemplateCube | Actor | src/template_cube.rs |
TextSwitcher | Actor | src/texts.rs |
StationTitle | Actor | src/texts.rs |
Its Blueprints, BP_GizmoDemo, BP_KeyboardKey, BP_MouseKey, BP_SplinePath, BP_TemplateCube, BP_TextSwitcher, BP_Titles and BP_UI_Update, children of those classes holding the meshes, materials and widget layouts, are in the plugin’s content: in the Content Browser, click Settings and check Show Plugin Content, then open Plugins > Tour Content.
Recipes that use it: UI.
rustrover
The goal: a new project that opens in RustRover with its run configurations ready, the same on Linux and Windows.
rustrover is a feature that adds files to the project instead of a plugin: two RustRover run configurations, stored as project files in Rust/.run/:
rusteal new MyGame --template third-person --feature rustrover
| Run configuration | Runs | For |
|---|---|---|
| Rusteal Watch | cargo rusteal watch | Building and reloading the library every time a Rust file is saved, with the editor open. |
| Rusteal Test | cargo test -p <game crate> | The game crate’s unit tests (see Testing without the engine). |
- In RustRover, choose File > Open and open the project’s
Rustfolder. - Pick Rusteal Watch or Rusteal Test in the run configurations next to the Run button, and click Run.
They are files of the project, so they go into version control with it and the rest of the team gets them too.
Screenshot: RustRover’s run configurations list with Rusteal Watch and Rusteal Test. (images/getting_started/rustrover_features.png)
Adding Rusteal to an existing project
The goal: Rust gameplay in a project that already exists, made from the editor or with C++.
rusteal setup /path/to/YourProject
rusteal setup installs the Rusteal plugins into the project’s Plugins/ and writes a starter rusteal.toml. If the project already has a rusteal.toml, it keeps it.
It does not write the Rust workspace. Create it as rusteal new does (see Project layout):
Rust/Cargo.toml, the workspace, listing your crate andbindings, and pinning the Rusteal crates to the CLI’s version (rusteal --version);Rust/<crate>/Cargo.toml, acdylibdepending onrusteal-runtime,bindingsandglam;Rust/<crate>/src/lib.rs, withrusteal_runtime::entry!();;- in
rusteal.toml,[project] crate = "<crate>", the package name of your crate.
The quickest way is to create a blank project with rusteal new next to it and copy its Rust/ directory and the profiles of its Rust/Cargo.toml.
Then build everything once:
cd /path/to/YourProject
rusteal build --all
The project needs a C++ module (Source/<Project>/) for the Unreal Build Tool to build it. A project made from the editor’s Blueprint templates has none: add one from the editor (Tools > New C++ Class, any class) before running the build.
Your existing C++ and Blueprint classes stay as they are, and work next to the Rust ones: Blueprints derive from Rust classes, call their functions and bind their delegates the way they do with C++ classes.
Versions and upgrades
A project is tied to one Rusteal version: Rust/Cargo.toml pins rusteal-runtime, rusteal-core and rusteal-ffi to it ("=x.y.z"), and the plugins in Plugins/ carry it.
Before doing anything, rusteal build, rusteal generate and rusteal setup compare it with the CLI’s own version:
-
The CLI is newer. Upgrade the project:
rusteal upgradeIt moves the pins, replaces
Plugins/RustealandPlugins/RustealGeneratorwith the CLI’s, and runs a full build. -
The project is newer. Install the CLI version the project uses:
cargo install rusteal@x.y.z
The plugin checks the library as well: a library built against another Rusteal version is refused when it loads, and the Output Log shows both versions.
Before upgrading
Rusteal is in early development, and its API changes between versions. Commit your project before running rusteal upgrade, so the changes it makes, and the compile errors your code may get from a changed API, can be reviewed in a diff.
Changing the engine version
The bindings are generated from the engine you build with. After installing another engine version, point the CLI at it (see Installing the CLI) and regenerate everything:
rusteal build --all
The rusteal CLI
Every Rusteal task is a rusteal command. Commands that act on a project take its directory as an argument, or find it by walking up from the current directory, so inside a project you can leave the path out.
| Command | What it does | Page |
|---|---|---|
rusteal new <Name> | Creates a project from a template and builds it. | Your first project |
rusteal setup <path> | Installs the Rusteal plugins into an existing project. | Adding Rusteal to an existing project |
rusteal build | Builds the Rust libraries and deploys them. | Building |
rusteal build --all | Runs the whole pipeline: engine build, bindings, Rust. | Building |
rusteal watch | Builds every time a Rust source file changes. | rusteal watch |
rusteal generate | Regenerates the bindings crate and the C++ wrappers. | Building |
rusteal upgrade | Moves the project to the CLI’s Rusteal version. | Versions and upgrades |
rusteal package | Builds the release libraries and packages the game. | Packaging a game |
rusteal plugin new <Name> | Creates a Rust plugin in the project. | A new plugin |
rusteal plugin package <Name> | Copies a plugin out, ready for another project. | Packaging a plugin |
rusteal help <command> and rusteal <command> --help list the flags of each command.
A day with the CLI
A typical session, once the project exists:
cd ~/Projects/MyGame
rusteal watch
Open the editor, change Rust code, save: rusteal watch builds, the editor reloads the library, and you press Play. When you change rusteal.toml or the engine version, stop the watch and run rusteal build --all once. When the game is ready to be tried outside the editor, run rusteal package.
Use case: running the CLI from the IDE
The goal: start rusteal watch from the IDE, with one run configuration that works on Linux and on Windows.
Every command is also a Cargo subcommand: cargo rusteal watch is rusteal watch (cargo install rusteal installs both). In RustRover:
- Open Run > Edit Configurations….
- Click + and choose Cargo.
- Name it
rusteal watch, and set Command torusteal watch. - Set Working directory to the project’s
Rustdirectory: the command finds the project by walking up from it. - Tick Store as project file to share it with the rest of the team, then click OK.
Screenshot: RustRover’s Run/Debug Configurations with a Cargo configuration whose command is rusteal watch. (images/cli/rustrover_watch.png)
Building
A Rusteal build has five steps:
| Step | Name | What it does |
|---|---|---|
| 1 | UE build | Builds the project with the Unreal Build Tool, which exports the engine’s reflection to Intermediate/Rusteal/uht/. |
| 2 | Codegen | Reads the reflection and generates the bindings crate and the C++ wrappers. |
| 3 | UE rebuild | Builds the generated C++ wrappers into the Rusteal plugin. |
| 4 | Cargo build | Builds your game crate (and each Rust plugin’s) with Cargo. |
| 5 | Deploy | Copies the libraries to the plugins’ Binaries/<Platform>/, where the editor loads them. |
Which build to run
| You changed | Run |
|---|---|
| Rust code in your crate | rusteal build |
| Rust code, many times while you work | rusteal watch |
rusteal.toml (engine modules, blocklist) | rusteal build --all |
| The engine version | rusteal build --all |
| A cloned project, built for the first time on this machine | rusteal build --all |
rusteal build runs steps 4 and 5: that is all a change to Rust code needs, and it takes seconds. rusteal build --all runs the five, and takes as long as the engine build and the bindings generation take.
Flags
rusteal build
rusteal build --all
rusteal build --from 2
rusteal build --step 3
rusteal build --release
rusteal build --plugin Inventory
rusteal build /path/to/MyGame
| Flag | What it does |
|---|---|
--all | Runs every step. |
--from <n> | Runs step n and every step after it. |
--step <n> | Runs only step n. |
--release | Builds the libraries with the release profile, for shipping. |
--plugin <Name> | Limits codegen, Cargo and deploy to one Rust plugin of the project. |
--all cannot be combined with --from or --step.
Generating the bindings alone
rusteal generate
Regenerates the bindings crate and the C++ wrappers from the reflection already exported, without building anything.
When the build fails
The build prints the output of the tool that failed: Cargo’s errors for your code, the Unreal Build Tool’s for the engine steps. A Rust compile error leaves the library the editor runs as it was: fix the code and build again.
The Unreal Build Tool runs one build at a time per engine. If another build is running, from the editor or from another project, wait for it to finish.
If rusteal build says the bindings are missing, the project has never been built on this machine, or Rust/bindings/ was deleted: run rusteal build --all.
Binding more of the engine
The goal: use an engine module the project does not bind yet, such as AI, Niagara, StateTree or Gameplay Tags.
The bindings crate only has the engine modules rusteal.toml asks for. A new project binds the core of the engine, input, UMG and Enhanced Input. The other modules are listed in rusteal.toml, each behind a feature, and a feature is turned on by adding it to features.
The file
[project]
crate = "my-game"
[codegen]
features = ["core", "engine", "input", "slate", "umg", "enhanced-input"]
[codegen.modules]
CoreUObject = { module = "core_ue", feature = "core" }
Engine = { module = "engine", feature = "engine" }
AIModule = { module = "ai", feature = "ai" }
GameplayTasks = { module = "gameplay_tasks", feature = "ai" }
NavigationSystem = { module = "navigation", feature = "ai" }
StateTreeModule = { module = "state_tree", feature = "state-tree", plugin = "StateTree" }
GameplayStateTreeModule = { module = "gameplay_state_tree", feature = "state-tree", plugin = "GameplayStateTree" }
Niagara = { module = "niagara", feature = "niagara", plugin = "Niagara" }
| Key | What it is |
|---|---|
[project] crate | The package name of your game crate in Rust/. |
[codegen] features | The features whose modules are bound. |
[codegen.modules] | Each engine module (by its package name), the Rust module it becomes in bindings, the feature it belongs to, and the engine plugin it comes from when it is not part of the engine itself. |
[codegen.blocklist] | Engine classes, structs and functions left out of the bindings, because the generated code cannot handle them. |
Turning a module on
-
Add the feature to
features:features = ["core", "engine", "input", "slate", "umg", "enhanced-input", "ai", "state-tree"] -
Run the whole pipeline:
rusteal build --all -
Use the module from
bindings:#![allow(unused)] fn main() { use bindings::ai::{AIController, AIControllerExt}; use bindings::navigation::NavigationSystemV1; }
When a module comes from an engine plugin (plugin = "StateTree"), the build enables that plugin for Rusteal, so the engine loads it with your game.
The modules a new project lists
| Feature | Modules | What it brings |
|---|---|---|
core | core_ue | UObject, math structs (FVector, FRotator, FTransform…) |
engine | engine | actors, components, characters, game framework, gameplay statics, physics, audio, animation |
physics-core | physics_core | physical materials |
input | input_core | keys (FKey) |
enhanced-input | enhanced_input | input actions, mapping contexts, triggers |
slate, umg | slate_core, slate, umg | UMG widgets |
ai | ai, gameplay_tasks, navigation | AI controllers, perception, behavior trees, EQS, navigation |
state-tree | state_tree, gameplay_state_tree | StateTree components, tasks, conditions |
gameplay-tags | gameplay_tags | gameplay tags and containers |
niagara | niagara | Niagara systems and components |
chaos-vehicles | chaos_vehicles | Chaos wheeled vehicles: pawns, movement, wheels, engine and gearbox setups |
gameplay-abilities | gameplay_abilities | the Gameplay Ability System |
level-sequence, cinematic, movie | level_sequence, cinematic_camera, movie_scene, movie_scene_tracks | Sequencer, cinematic cameras |
engine needs input, slate and umg, because the Rusteal plugin links them: keep them in the list.
Binding another module
Any reflected engine module can be added to [codegen.modules] the same way, with a feature of your choice:
DeveloperSettings = { module = "developer_settings", feature = "engine" }
The blocklist
A few engine functions are reflected but not exported by the engine, so their wrapper compiles but does not link. The blocklist a new project gets already lists the ones known for the engine version Rusteal targets. If a rusteal build --all fails at link time on a wrapper of a new module, add the function to [codegen.blocklist] functions as "Class.Function" and build again.
Bigger bindings, longer builds
Every module added makes the bindings crate larger, and its first build longer. The bindings are compiled once and kept, so a change to your own code does not rebuild them. Bind what the game uses.
Development and release builds
The game library is built with Cargo’s dev profile by default, and with the release profile when --release is given. The two want opposite things, and the projects Rusteal creates set them up for that in Rust/Cargo.toml, the only place Cargo reads profiles from (in the game crate’s own Cargo.toml they are ignored with a warning).
dev: iterating
rusteal build
Rebuilding after a change must be fast, and the game must still run well in the editor:
- your game crate, the one that changes, is barely optimized, so it compiles in about a second;
- the Rusteal crates and the generated bindings, which compile once, are fully optimized;
- your crate has full debug info, for the debugger; the dependencies and the bindings keep only file and line, enough for backtraces, so the library stays small enough to reload quickly.
release: shipping
rusteal build --release
The fastest library possible, however long it takes to build: one codegen unit per crate and link-time optimization across crates, which also drops the parts of the bindings the game does not use. rusteal package always builds this profile.
Tuning
The profiles are yours to change, in Rust/Cargo.toml. The optimization levels follow Bevy’s recommendations for game projects:
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3
debug = "line-tables-only"
[profile.dev.package.bindings]
opt-level = 3
debug = "line-tables-only"
[profile.release]
codegen-units = 1
lto = "thin"
Every crates.io dependency you add is covered by "*": optimized, with file and line only. To step into one of them in the debugger, give it full debug info alone:
[profile.dev.package.rapier3d]
debug = true
Keep the bindings at line-tables-only: with full debug info, the library grows past a hundred megabytes, and every hot reload loads it again.
Platform optimizations
The goal: engine settings that one platform needs, applied on that platform only, on every machine you work on and in the game you ship, without you having to remember them.
Some engine settings fix or tune one platform and have no business on the others. Rusteal keeps them in rusteal.toml, per platform, and writes them into the engine’s own per-platform config files for you.
What every project gets
The templates ship one Linux setting:
[optimizations.linux.Engine.ConsoleVariables]
"r.Vulkan.CpuWaitForFence" = 0
On Linux the engine renders with Vulkan. Whenever the swapchain goes out of date (a window is resized or closed, PIE stops, the game switches to fullscreen, alt-tab), the engine recreates it and, by default, waits for the next image with a CPU fence. NVIDIA’s Linux driver can crash inside vkWaitForFences right then: the editor closes with a segfault in the driver a second or two after AcquireNextImage() failed due to the outdated swapchain in the log. With 0, the wait is done on the GPU with a semaphore, which never calls the crashing function. It costs nothing measurable, and Epic ships the same value on Android.
The crash is not the editor’s alone: a packaged game runs the same Vulkan code, so a player on Linux with an NVIDIA card can hit it when resizing or alt-tabbing. That is why the setting ships with the game too. On Windows the engine uses DirectX 12, and the setting is never written there.
Where it goes
The table’s path names the platform, the config file and its section: [optimizations.<platform>.<File>.<Section>]. The platforms are linux, windows and mac. Rusteal writes the table of the platform it runs on into Config/<Platform>/<Platform><File>.ini of your project, here Config/Linux/LinuxEngine.ini, between two comment lines of its own:
; Rusteal optimizations, written by rusteal from rusteal.toml [optimizations]: begin
[ConsoleVariables]
r.Vulkan.CpuWaitForFence=0
; Rusteal optimizations: end
That file is the engine’s: it reads it on Linux only, for the editor and for the packaged game alike. Rusteal only rewrites what is between its two lines:
- what you write elsewhere in the file stays as it is;
- a key you set yourself in that file, outside the Rusteal lines, keeps your value, and Rusteal leaves it out of its block.
Commit the file with the project: every Linux machine writes the same block, so it does not change from one developer to the next.
While you develop
rusteal watch
rusteal new writes the optimizations into the new project, and rusteal watch writes them again each time it starts. You may work on several machines, Linux and Windows, or a teammate on another one: whoever starts rusteal watch on Linux has the settings, and nobody has to copy anything. The Rusteal CLI built for Windows has no Linux settings in it at all; it is compiled per platform.
The editor reads config files when it starts: after the first rusteal watch of a new setting, restart the editor once. Check that it took in Saved/Logs/<Project>.log:
LogConfig: Set CVar [[r.Vulkan.CpuWaitForFence:0]]
To work without them, for instance to see whether a problem comes from one of them:
rusteal watch --optimizations off
That removes the block, and the file too if Rusteal had created it; the next rusteal watch writes it back. --optimizations 0 is the same. Do not commit the file in that state.
When you ship
rusteal package --shipping
rusteal package writes the optimizations of the platform it packages for, so the game carries them, then puts the project’s config files back exactly as they were before the package. Packaging on Windows for Windows, nothing of Linux goes in: the engine stages Config/Linux for Linux builds only, and the Windows CLI does not write it anyway.
To ship a game without them:
rusteal package --shipping --optimizations off
Your own
Add keys and tables to rusteal.toml, in the same shape. If the Vulkan crash still showed up, the next console variable to try is the one that recreates the swapchain from scratch instead of reusing the old one:
[optimizations.linux.Engine.ConsoleVariables]
"r.Vulkan.CpuWaitForFence" = 0
"r.Vulkan.KeepSwapChain" = 0
Any config file and section works: [optimizations.windows.Game.<Section>] goes into Config/Windows/WindowsGame.ini, [optimizations.mac.Engine.<Section>] into Config/Mac/MacEngine.ini. Keys with dots, and section names with dots or slashes such as "/Script/Engine.RendererSettings", go in quotes. Values are strings, numbers or booleans (true is written True). The next rusteal watch or rusteal package writes them; removing a line from rusteal.toml removes it from the .ini the same way.
- A project with no
[optimizations]table, one made before Rusteal had them, gets the defaults above. - An empty table turns them all off for good:
[optimizations]
In the engine itself
Some crashes happen before any project is open: the Project Browser runs the same Vulkan code, and a project’s config does not apply there. For those, the setting can go into the engine install, in the [ConsoleVariables] section that Engine/Config/Linux/LinuxEngine.ini already has:
[ConsoleVariables]
; larger timeout since drivers may take longer time
g.TimeoutForBlockOnRenderFence=60000
memory.MemoryPressureCriticalThresholdPercent=10
; Use semaphores instead of fences for swapchain image wait (avoids NVIDIA driver crash in vkWaitForFences after an out-of-date swapchain)
r.Vulkan.CpuWaitForFence=0
Know what that means before doing it:
| The project’s file (Rusteal) | The engine’s file | |
|---|---|---|
| Applies to | that project, its editor and its game | every project and every game packaged with that engine install, and the Project Browser |
--optimizations off | leaves it out | does nothing: the engine’s file is staged into every package |
| Survives an engine upgrade or reinstall | yes, it is in the project | no: a new install replaces Engine/Config, and the line must be added again |
| On another machine, or on CI | yes, written by rusteal watch and rusteal package | only if that engine install was changed too |
The project’s file is the one that travels with the game: keep it even when the engine has the line. Use Engine/Config/ConsoleVariables.ini only for the editor’s own debugging settings; its [Startup] section is ignored by Shipping builds, so a fix that the game needs does not belong there.
Hot reload
The goal: change Rust code and see it in the running editor, without restarting it.
rusteal build
When the build deploys the library, the editor reloads it on its own, and a notification says so. Press Play and the new code runs. Nothing else is needed: no button in the editor, no restart.
Screenshot: the editor notification shown after a reload. (images/iteration/reload_notification.png)
What a reload keeps and what it changes
| You changed | After the reload |
|---|---|
| The body of a function or event | The new code runs. Classes, objects and their properties stay as they are. |
A #[uproperty]: added, removed, renamed, retyped | The class is created again with the new properties, and its objects move to it, keeping the values of the properties that still exist. |
A #[component] | The class is created again; its objects move to it. |
A #[ufunction] or #[udelegate]: added, removed, signature changed | The class is created again; Blueprints that use it are recompiled. |
A class’s parent, its implements or its config | The class is created again. |
A new #[uclass] | The class appears, ready to be placed or derived from. |
| A Rust-only field (no attribute) | Starts over from its Default value, on every reload, changed or not. |
A #[uinterface] (after 0.7.0): a function added, removed, signature changed | The interface keeps its class and gets the new functions; the Blueprints implementing or calling it are recompiled, and the Rust classes implementing it are created again. |
A #[ustruct] | Logged as changed; the struct keeps its old fields until the editor restarts. |
When a class is created again, every object of it moves to the new class: the actors placed in the level, the Blueprint children of the class, and the Rust classes that derive from it. Their property values are kept.
Blueprints that use a changed class
A Blueprint that calls a function you removed, or reads a property you removed, gets a compile error on that node after the reload, as it would after the same change to a C++ class. Open the Blueprint, and either fix the node or put the function back and build again: the error clears.
Screenshot: a Blueprint graph with a node in error after its Rust function was removed. (images/iteration/blueprint_node_error.png)
Rust-only fields start over
A field with no attribute is Rust’s alone, kept outside the Unreal object, and a reload drops it. Keep what must survive a reload in #[uproperty] fields: they live in the object’s memory, and the reload keeps them. A Rust-only field is for state that can be rebuilt (a cache, a path, a timer you set again in receive_begin_play).
When a restart is needed
- A changed
#[ustruct]: the editor keeps the old struct until it restarts. - A change to
rusteal.toml, or a new engine version: runrusteal build --all, then restart the editor. - A new Rusteal version after
rusteal upgrade: restart the editor. - Outside the editor (a game started with
-game), a changed class or interface cannot be replaced: it keeps its old shape, and the log asks for a restart. Function bodies still update.
Reloading by hand
The editor also reloads on request, from its console. See Console commands.
A reload during Play
The safest moment to reload is between Play sessions. A reload during Play drops the Rust-only fields of the objects in the game, and moves the objects of a class that is created again to the new class in the middle of the game.
How long it takes
A reload that only changes code takes a few milliseconds. A reload that creates classes again takes longer, around a tenth of a second for a few classes, which is the engine moving the objects to the new classes. The Output Log ends each reload with how long it took.
rusteal watch
The goal: save a Rust file and have the change in the editor, with nothing else to run.
cd ~/Projects/MyGame
rusteal watch
rusteal watch builds the Rust libraries every time a source file changes, the same build as rusteal build. With the editor reloading each library when it is deployed, saving is the whole loop: save, build, deploy, reload.
rusteal watch: watching
/home/me/Projects/MyGame/Rust
Each change builds and deploys the libraries (rusteal build); Ctrl+C stops.
Leave it running in a terminal next to the editor. Ctrl+C stops it.
What it watches
The .rs and .toml files of the project’s Rust workspace, and of each Rust plugin’s workspace. It does not watch target/ or the generated bindings/.
It waits until the files have settled before building, so saving several files at once, or a formatter rewriting them, makes one build.
When the build fails
The build’s errors are printed, and the watch goes on:
rusteal watch: the build failed (above), waiting for changes
The editor keeps running the last library that built. Fix the error and save: the next build deploys and reloads.
One plugin
rusteal watch --plugin Inventory
Watches and builds only that plugin’s library, which is what working on a plugin needs.
What it does not do
rusteal watch runs steps 4 and 5 of the build. A change to rusteal.toml or to the engine version needs rusteal build --all: stop the watch, run it, and start the watch again.
Console commands
Rusteal adds two commands to the editor’s console.
Opening the console
Either of these works:
- press the
`key (the one underEsc) with the mouse over the level viewport; - type in the Cmd box at the bottom of the Output Log tab.
Screenshot: the Cmd box at the bottom of the Output Log tab with
Rusteal.Reloadtyped in. (images/iteration/output_log_cmd.png)
The commands
| Command | What it does |
|---|---|
Rusteal.Reload | Reloads every Rust library of the project: the game’s and each Rust plugin’s. |
Rusteal.Reload <Name> | Reloads one library: the game’s is named after the project, a plugin’s after the plugin. |
Rusteal.AutoReload 0 | Stops reloading a library when rusteal build deploys it again. Rusteal.Reload still works. |
Rusteal.AutoReload 1 | Reloads a library when it is deployed again. This is the default. |
Use case: reloading when you choose
The goal: build several times without the editor reloading after each build, for example while a Play session must not be interrupted.
- In the console, run
Rusteal.AutoReload 0. - Build as many times as you want with
rusteal build. - When you are ready, run
Rusteal.Reload. - Run
Rusteal.AutoReload 1to go back to reloading on every build.
Use case: reloading one plugin
The goal: reload a plugin’s library and leave the game’s alone.
Rusteal.Reload Inventory
Your own commands
A game can add its own console commands and variables from Rust. See Console commands and variables.
Logging
The goal: print messages from Rust code to the Output Log.
#![allow(unused)]
fn main() {
log::info!("[MyGame] wave {wave} started");
log::warn!("[MyGame] no spawn points in the level");
log::error!("[MyGame] could not load the save: {e}");
}
Rusteal logs through the log crate, the logging facade of the Rust ecosystem: the projects Rusteal creates depend on it (log = { workspace = true } in the game crate), and the library routes it to the Output Log when it loads. Its messages go under the LogRusteal category, with the level’s color: plain for info!, yellow for warn!, red for error!.
LogRusteal: [MyGame] wave 3 started
LogRusteal: Warning: [MyGame] no spawn points in the level
debug! and trace! are off by default. Turn them on, for the whole library, where it starts:
#![allow(unused)]
fn main() {
fn verbose_logs() {
log::set_max_level(log::LevelFilter::Debug);
}
rusteal_runtime::on_load!(verbose_logs);
}
They show at the display level, like info!. The crates your game depends on that log through log (most do) show in the Output Log the same way.
A project made before Rusteal routed log has no log dependency: add log = "0.4" to [workspace.dependencies] in Rust/Cargo.toml and log = { workspace = true } to the game crate’s [dependencies]. Its ulog! calls keep working.
Filtering
In the Output Log tab, Filters > Categories can show LogRusteal alone. A prefix of your own at the start of each message ([MyGame], [Inventory]) makes them easy to find with the search box as well.
Screenshot: the Output Log filtered to the LogRusteal category. (images/iteration/output_log_filter.png)
Errors that reach the log by themselves
Some failures are logged without any code of yours:
- A panic in your code is caught where the engine called into Rust, and logged as an error. The editor keeps running; the function that panicked stops there.
- A function bound by name (an input action, a delegate, a timer) that does not exist is logged with its name.
- A library built against another Rusteal version is refused, with both versions in the log.
Logging a RustealResult
Engine calls that can fail return a RustealResult. A common pattern is to put the work in a method that returns one, and log the error in the event that calls it:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let Err(e) = self.setup() {
log::warn!("[MyGame] setup failed: {e}");
}
}
}
See Errors.
Testing without the engine
The goal: test gameplay rules with cargo test, without opening Unreal.
Code that calls the engine needs the engine to run. Code that only computes does not: damage formulas, combo windows, stamina, inventories, wave schedules, movement math. Keep that code in plain Rust functions and types, apart from the classes, and test it like any Rust code.
The templates do this with a model.rs file next to each class: the tunable numbers and the rules live there, and the class calls them.
Example
src/character/model.rs:
#![allow(unused)]
fn main() {
use glam::DVec3;
pub const MAX_WALK_SPEED: f32 = 500.0;
pub const CAMERA_BOOM_LENGTH: f32 = 400.0;
pub fn ground_axes(yaw_degrees: f64) -> (DVec3, DVec3) {
let (sin, cos) = yaw_degrees.to_radians().sin_cos();
(DVec3::new(cos, sin, 0.0), DVec3::new(-sin, cos, 0.0))
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: DVec3, b: DVec3) -> bool {
(a - b).length() < 1e-9
}
#[test]
fn ground_axes_follow_control_yaw() {
let (forward, right) = ground_axes(90.0);
assert!(close(forward, DVec3::Y));
assert!(close(right, -DVec3::X));
}
}
}
The class uses it:
#![allow(unused)]
fn main() {
let yaw = controller.checked()?.get_control_rotation().to_rotator().yaw;
let (forward, right) = model::ground_axes(yaw);
}
Running the tests
From Rust/:
cargo test -p my-game
The tests compile your crate as a normal Rust program, without the engine. A test that calls an engine function cannot run this way: keep the engine calls in the classes and the rules in the model.
What else works
Everything Rust offers for testing works here: property tests (proptest), benchmarks (criterion), snapshot tests. A rules crate of its own, with no Rusteal dependency at all, can be shared with a dedicated server, a balancing tool or a web page.
Declaring a class
The goal: a new Unreal class, written in Rust, that the editor and Blueprints treat like any other.
#![allow(unused)]
fn main() {
use bindings::engine::{StaticMeshComponent, SceneComponent};
use bindings::prelude::*;
use rusteal_runtime::{uclass, uclass_impl};
#[uclass(parent = Actor)]
pub struct Barrel {
#[component(root)]
root: SceneComponent,
#[component(attach = "root")]
mesh: StaticMeshComponent,
#[uproperty(EditAnywhere, default = 50)]
health: i32,
}
#[uclass_impl]
impl Barrel {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
log::info!("[MyGame] crate with {} health", self.health());
}
}
}
A class is two parts:
- The struct, marked
#[uclass(parent = ...)]: its fields are the class’s properties (#[uproperty]), its components (#[component]) and fields only Rust sees (no attribute). - The impl block, marked
#[uclass_impl]: its methods marked#[ufunction]are the class’s Unreal functions and events, and#[class_defaults]sets its default values. A class can split them over several#[uclass_impl]blocks, in several files. Plainimplblocks next to them hold the methods only Rust calls.
When the library loads, Rusteal creates the class in the engine under the struct’s name: Barrel above. It can be placed in a level, used as a Blueprint’s parent and picked in any class field of the editor.
Choosing a parent
The parent is an engine class from the bindings, or another Rust class. What the class is follows from it:
| Parent | What the class is |
|---|---|
Actor | Anything placed in or spawned into a level: props, pickups, volumes, spawners, projectiles. |
Pawn, Character | Something a player or an AI controls. Character brings a capsule, a skeletal mesh and walking movement. |
PlayerController, AIController | What possesses a pawn: input for a player, logic for an AI. |
GameModeBase, GameStateBase, PlayerState | The rules of a level and their state. |
PlayerCameraManager | How the player’s camera is placed every frame. |
HUD | A canvas drawn over the game. |
ActorComponent, SceneComponent | Behavior added to actors: an inventory, a health system. |
UserWidget | A UMG widget whose layout a Widget Blueprint child draws. |
AnimNotify | Code run at a frame of an animation. |
SaveGame | Data written to and read from a save slot. |
PrimaryDataAsset | A kind of asset, created and edited in the Content Browser. |
DeveloperSettings | A page of Project Settings. |
StateTreeTaskBlueprintBase, StateTreeConditionBlueprintBase | StateTree tasks and conditions. |
EnvQueryContext_BlueprintBase | An EQS context. |
RustealGameInstanceSubsystem, RustealWorldSubsystem… | A subsystem. See Subsystems. |
The engine classes keep their C++ names without the prefix (AActor is Actor), and come from the module they belong to (bindings::engine::Character, bindings::ai::AIController, bindings::umg::UserWidget).
Reaching the object
Inside the class, self is the Rust side of the object. Three methods connect it to the engine:
| Method | What it returns |
|---|---|
self.as_ref() | A reference to the object as its parent class (UObjectRef<Actor> for Barrel), to call engine functions on it. |
MyClass::from_obj(object) | The Rust class of any object reference, or an error if the object is not of that class. |
MyClass::get_default() | The class default object, which every instance starts from. |
#![allow(unused)]
fn main() {
let me = self.as_ref().checked()?;
let location = me.k2_get_actor_location().to_dvec3();
}
#![allow(unused)]
fn main() {
if let Ok(mut barrel) = Barrel::from_obj(other_actor) {
barrel.set_health(barrel.health() - 10);
}
}
from_obj is how one Rust class reaches another: an overlap gives an UObjectRef<Actor>, and from_obj turns it into the Rust class, with its getters, setters and methods, when that is what it is. See Object references.
Where the class lives
The game’s classes are in the /Script/Rusteal package: /Script/Rusteal.Barrel is the class path of Barrel, the path an ini file, a redirect or editor Python uses. A plugin’s classes are in its own package (see Plugins).
What a class cannot do yet
- Replicate properties or call RPCs over the network:
Replicatedand the RPC specifiers are not accepted yet.
See Known limits.
Properties
The goal: values that designers tune in the editor, that Blueprints read and write, and that the engine saves with the object.
#![allow(unused)]
fn main() {
use bindings::engine::{AnimMontage, SoundBase};
use bindings::prelude::*;
use rusteal_runtime::runtime::{FName, SubclassOf, UObjectRef, UeArray};
use rusteal_runtime::{uclass, uclass_impl};
#[uclass(parent = Actor)]
pub struct Turret {
#[uproperty(EditAnywhere, category = "Combat", default = 25.0)]
damage: f32,
#[uproperty(EditAnywhere, category = "Combat", default = 3)]
burst_size: i32,
#[uproperty(EditAnywhere, BlueprintReadWrite, default = true)]
b_active: bool,
#[uproperty(EditAnywhere, category = "Effects")]
fire_sound: UObjectRef<SoundBase>,
#[uproperty(EditAnywhere, category = "Effects")]
fire_montage: UObjectRef<AnimMontage>,
#[uproperty(EditAnywhere, category = "Combat")]
projectile_class: SubclassOf<Actor>,
#[uproperty(EditAnywhere)]
target_tags: UeArray<FName>,
#[uproperty]
current_target: UObjectRef<Actor>,
shots_fired: u32,
}
#[uclass_impl]
impl Turret {}
}
A field marked #[uproperty] is a real Unreal property. It lives in the object’s memory, the editor shows it according to its specifiers, Blueprints reach it, it is saved with the level or the Blueprint, and object references in it keep the object alive for the garbage collector.
Getters and setters
Each property gets a getter named after the field and a setter with set_:
#![allow(unused)]
fn main() {
let damage = self.damage();
self.set_burst_size(5);
self.set_current_target(target);
}
The getter of a scalar, an object, a class or a name returns a copy. The getter of a UeArray returns a view of the array inside the object, changed in place:
#![allow(unused)]
fn main() {
let tags = self.target_tags();
tags.push(&FName::new("Player"))?;
let all: Vec<_> = tags.to_vec()?;
}
The getter of a struct property (OwnedStruct<FVector>) returns a copy; change it and set it back.
Specifiers
| Specifier | What it does |
|---|---|
EditAnywhere | Editable in the Details panel, on the class defaults and on each instance in a level. |
EditDefaultsOnly | Editable on the class defaults only (a Blueprint’s Class Defaults), not per instance. |
VisibleAnywhere | Shown, read-only, in the Details panel. |
BlueprintReadWrite | Blueprints get Get and Set nodes for it. |
BlueprintReadOnly | Blueprints get a Get node only. Rust code still has the setter. |
SaveGame | Written by a save game archive. See Saving the game. |
Config | Read from and saved to the ini files of the class’s config category. See Project settings. |
Several specifiers can be combined. A property with none (#[uproperty]) is hidden from the editor and from Blueprints, but is still a real property: use it for object references the class holds, so the garbage collector sees them.
Any other argument is a compile error.
Named arguments
| Argument | What it does |
|---|---|
default = <value> | The property’s default value, for scalars and engine enums: default = 25.0, default = true, default = ECollisionChannel::ECC_Pawn, or a constant (default = model::MAX_HP). |
category = "..." | The category it is listed under in the Details panel. `“Combat |
name = "..." | The Unreal name, when the field’s name in PascalCase is not the one you want. |
default is for scalars and enums. Objects, classes, arrays and structs get their defaults in Class defaults or in a Blueprint child.
Names
The Unreal name is the field’s name in PascalCase: burst_size is BurstSize. A bool whose field starts with b_ follows Unreal’s b convention: b_active is bActive. When PascalCase gets it wrong, name fixes it:
#![allow(unused)]
fn main() {
#[uproperty(EditAnywhere, name = "MaxHP", default = 100.0)]
max_hp: f32,
#[uproperty(EditAnywhere, name = "NPCClass")]
npc_class: SubclassOf<Actor>,
}
The Unreal name is what Blueprints, ini files, data tables and editor Python see. Changing it on a class that already has Blueprint children or placed instances loses the values they saved under the old name.
Types
| Rust type | Unreal type |
|---|---|
bool, u8, i32, i64, f32, f64 | bool, uint8, int32, int64, float, double |
String | FString |
FName | FName |
An engine enum (ECollisionChannel, ETraceTypeQuery…) or a #[uenum] of yours | that enum |
UObjectRef<T> | TObjectPtr<T>: an object or an asset |
SubclassOf<T> | TSubclassOf<T>: a class, such as a Blueprint to spawn |
SoftObjectRef<T> | TSoftObjectPtr<T>: an asset by path, loaded on demand |
OwnedStruct<T> | a struct: FVector, FLinearColor, FTransform, FDataTableRowHandle, a #[ustruct] of yours |
UeArray<T> of the scalars, objects, classes, FName or structs (UeArray<OwnedStruct<FLinearColor>>) | TArray<T> |
Maps and sets (TMap, TSet) cannot be declared yet; two arrays side by side, or a Rust-only HashMap, do the job meanwhile.
Fields only Rust sees
A field without an attribute is Rust’s alone: any Rust type, kept outside the Unreal object. It gets the same getter and setter, plus a <field>_mut() that changes it in place:
#![allow(unused)]
fn main() {
self.set_shots_fired(self.shots_fired() + 1);
self.cooldowns_mut().insert(weapon, 2.0);
}
The getter of a Rust-only field returns a clone. Use these fields for state the editor does not need: caches, timers, a state machine, a HashMap. Two things to keep in mind:
- They start over from
Defaulton every hot reload. - An
UObjectRefkept only there does not keep its object alive: the garbage collector does not see it. Hold the objects you need to keep in a#[uproperty], or check them withchecked()before use (see Object references).
The templates keep a class’s transient state in one plain struct field, read, changed and written back:
#![allow(unused)]
fn main() {
#[derive(Clone, Copy, Default)]
pub struct MoveState {
pub has_double_jumped: bool,
pub last_fall_time: f64,
}
let mut state = self.state();
state.has_double_jumped = true;
self.set_state(state);
}
In the editor
Open the class’s Blueprint child, click Class Defaults in the toolbar, and the Details panel lists the EditAnywhere and EditDefaultsOnly properties under their categories. Select an instance in the level to see the EditAnywhere ones per instance.
Screenshot: the Details panel of a Blueprint child showing the Combat and Effects categories of a Rust class. (images/classes/details_categories.png)
Components
The goal: an actor made of components, a mesh, a collision shape, a camera on a spring arm, created with every instance, as a C++ constructor’s CreateDefaultSubobject creates them.
#![allow(unused)]
fn main() {
use bindings::engine::{
BoxComponent, CameraComponent, ProjectileMovementComponent, SceneComponent,
SpringArmComponent, StaticMeshComponent
};
use rusteal_runtime::{uclass, uclass_impl};
#[uclass(parent = Actor)]
pub struct Door {
#[component(root)]
root: SceneComponent,
#[component(attach = "root")]
frame: StaticMeshComponent,
#[component(attach = "frame", socket = "Hinge")]
panel: StaticMeshComponent,
#[component(attach = "root", name = "Trigger Box")]
trigger: BoxComponent,
}
#[uclass_impl]
impl Door {}
}
| Argument | What it does |
|---|---|
root | The component is the actor’s root component. |
attach = "<field>" | The component it is attached to: one declared before it in the class, or an inherited one by its field-style name. |
socket = "<name>" | A socket on the parent component (a bone, a spring arm’s end). |
name = "..." | The component’s name in the editor, when it is not the field’s. |
A component that is not a scene component (a movement component, a perception component, a StateTree component) has no transform: it takes neither root nor attach.
#![allow(unused)]
fn main() {
#[component(name = "Projectile Movement")]
projectile_movement: ProjectileMovementComponent,
}
Attaching to inherited components
A Character already has a capsule (its root), a skeletal mesh and a movement component. Attach to them by their field-style names:
#![allow(unused)]
fn main() {
#[uclass(parent = Character)]
pub struct Hero {
#[component(attach = "root_component")]
camera_boom: SpringArmComponent,
#[component(attach = "camera_boom", socket = "SpringEndpoint")]
follow_camera: CameraComponent,
}
}
| Inherited component | Field-style name |
|---|---|
| The root component of any actor | root_component |
| A character’s skeletal mesh | mesh |
| A component of a Rust parent class | its field’s name (first_person_camera_component) |
Reaching a component
Each component gets a getter that returns a reference to it:
#![allow(unused)]
fn main() {
let boom = self.camera_boom()?.checked()?;
boom.set_target_arm_length(400.0);
}
The getter returns a RustealResult, because the component may not exist on the object at that moment (an object being destroyed). Inherited engine components come from the engine getters: me.get_mesh(), me.get_capsule_component(), me.get_character_movement().
Names and Blueprints
A component’s name is its field’s name, or name. A Blueprint child’s changes to an inherited component, a mesh assigned to it, a collision preset, are saved under that name. Keep it stable once Blueprints use the class: renaming the field without name loses those changes. When you port a C++ class, give each component the name the C++ CreateDefaultSubobject used, and the Blueprint children keep their data.
Configuring components
The values a component starts with go in Class defaults:
#![allow(unused)]
fn main() {
use bindings::engine::{BoxComponentExt, PrimitiveComponentExt};
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let trigger = self.trigger()?.checked()?;
trigger.set_box_extent(&FVector::from_dvec3(DVec3::new(100.0, 100.0, 120.0)), Some(false));
trigger.set_collision_profile_name(FName::new("OverlapAllDynamic").handle(), Some(true));
Ok(())
}
}
Adding a component at runtime
A component created during the game, for example on an actor the class does not own, is added through the engine:
#![allow(unused)]
fn main() {
let class = SubclassOf::<InventoryComponent>::base().upcast_to::<ActorComponent>();
let identity = FTransform::from_transform(Transform::IDENTITY);
let component = actor.checked()?.add_component_by_class(class, false, &identity, false);
}
See the inventory recipe.
In the editor
The Blueprint child’s Components panel lists the Rust components as inherited components: select one to change its mesh, material or collision in the Details panel.
Screenshot: the Components panel of a Blueprint child listing the components of a Rust class. (images/classes/components_panel.png)
Class defaults
The goal: set what a C++ constructor sets: inherited properties, component settings, object defaults, classes to use.
#![allow(unused)]
fn main() {
#[uclass_impl]
impl Hero {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
me.set_use_controller_rotation_yaw(false);
let movement = me.get_character_movement().checked()?;
movement.set_orient_rotation_to_movement(true);
movement.set_max_walk_speed(500.0);
movement.set_jump_z_velocity(700.0);
let boom = self.camera_boom()?.checked()?;
boom.set_target_arm_length(400.0);
boom.set_use_pawn_control_rotation(true);
Ok(())
}
}
}
A method marked #[class_defaults] in the #[uclass_impl] block runs once, on the class default object, when the class is created. Every instance and every Blueprint child starts from that object, so what it sets is the class’s default. It returns RustealResult<()>, so ? works, or nothing.
What goes there
| You want | In class defaults |
|---|---|
| A default for an inherited property | me.set_use_controller_rotation_yaw(false) |
| A component’s settings | self.mesh()?.checked()?.set_simulate_physics(true) |
A default for one of your properties that default = cannot hold | self.set_ragdoll_profile(FName::new("Ragdoll")) |
| The class an engine object uses | me.set_player_camera_manager_class(SubclassOf::<MyCameraManager>::base().upcast_to()) |
| An AI controller for a pawn | me.set_ai_controller_class(SubclassOf::<MyAIController>::base().upcast_to()) |
| A subsystem that ticks | self.as_ref().checked()?.set_wants_tick(true) |
An actor needs nothing to tick: overriding receive_tick turns its tick on.
What does not go there
Class defaults run on the class default object, not on an instance in a level, and before any game starts. Do not spawn actors, look for other actors or read the world there: do that in receive_begin_play.
Blueprint children win
A Blueprint child that changes a value in its Class Defaults keeps its value: class defaults set the starting point, and the Blueprint’s changes are saved on top of it. When a change in class_defaults does not show on a Blueprint child, check whether that Blueprint overrides the same value (a value changed from its default shows a Reset to Default arrow next to it in the Details panel).
A Rust parent’s defaults
A class whose parent is a Rust class starts from the parent’s defaults: the parent’s class_defaults runs first, then the child’s.
Engine events
The goal: run Rust code when the engine says something happened: the game started, a frame passed, the actor was hit, a controller possessed the pawn.
#![allow(unused)]
fn main() {
#[uclass_impl]
impl Hero {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
}
#[ufunction(Override)]
fn receive_tick(&mut self, delta_seconds: f32) {
}
#[ufunction(Override)]
fn receive_end_play(&mut self, end_play_reason: u8) {
}
#[ufunction(Override)]
fn on_landed(&mut self, hit: UStructRef<FHitResult>) {
}
}
}
#[ufunction(Override)] implements an event of the parent class: one of the functions a Blueprint implements as an Event node (Event BeginPlay, Event Tick, Event Hit). The method is named after the event in snake_case and takes the event’s parameters, in order.
C++ virtuals are events
A C++ class overrides virtual functions (BeginPlay, Tick, NotifyHit). A Rust class, like a Blueprint, overrides the events the engine raises for them:
| C++ virtual | Event | Rust method |
|---|---|---|
BeginPlay | ReceiveBeginPlay | receive_begin_play(&mut self) |
EndPlay | ReceiveEndPlay | receive_end_play(&mut self, end_play_reason: u8) |
Tick | ReceiveTick | receive_tick(&mut self, delta_seconds: f32) |
Destroyed | ReceiveDestroyed | receive_destroyed(&mut self) |
TakeDamage | ReceiveAnyDamage | receive_any_damage(&mut self, damage: f32, damage_type: UObjectRef<DamageType>, instigated_by: UObjectRef<Controller>, damage_causer: UObjectRef<Actor>) |
NotifyHit | ReceiveHit | receive_hit(&mut self, my_comp, other, other_comp, self_moved, hit_location, hit_normal, normal_impulse, hit) |
NotifyActorBeginOverlap | ReceiveActorBeginOverlap | receive_actor_begin_overlap(&mut self, other_actor: UObjectRef<Actor>) |
NotifyActorEndOverlap | ReceiveActorEndOverlap | receive_actor_end_overlap(&mut self, other_actor: UObjectRef<Actor>) |
Landed | OnLanded | on_landed(&mut self, hit: UStructRef<FHitResult>) |
OnMovementModeChanged | K2_OnMovementModeChanged | see below |
NotifyControllerChanged | ReceiveControllerChanged | receive_controller_changed(&mut self, old: UObjectRef<Controller>, new: UObjectRef<Controller>) |
SetupPlayerInputComponent | ReceiveRestarted | receive_restarted(&mut self): the input component exists once a local player possesses the pawn |
APlayerController::SetupInputComponent | ReceiveBeginPlay | receive_begin_play(&mut self) |
OnPossess | ReceivePossess | receive_possess(&mut self, possessed_pawn: UObjectRef<Pawn>) |
ChoosePlayerStart | ChoosePlayerStart | choose_player_start(&mut self, player: UObjectRef<Controller>) -> UObjectRef<Actor> |
DrawHUD | ReceiveDrawHUD | receive_draw_hud(&mut self, size_x: i32, size_y: i32) |
UpdateCamera | BlueprintUpdateCamera | see Cameras |
UAnimNotify::Notify | Received_Notify | see Animation |
Overriding receive_tick turns the actor’s tick on: nothing else is needed.
Parameter types
| The event takes | The Rust method takes |
|---|---|
| A number or a bool | the Rust scalar (f32, i32, bool…) |
| An object | UObjectRef<T> |
| A class | SubclassOf<T> |
A struct (FHitResult, FVector) | UStructRef<T>, a reference to the struct inside the call; to_owned() keeps a copy |
| An enum | the enum type or its u8 value |
| An output struct | UStructRef<T>, written in place |
| An output scalar or object | OutRef<T>, written with set |
An event that returns a value returns it from the method:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn choose_player_start(&mut self, _player: UObjectRef<Controller>) -> UObjectRef<Actor> {
let starts = GameplayStatics::get_all_actors_of_class(
self.as_ref().upcast_to(),
SubclassOf::<PlayerStart>::base().upcast_to(),
);
starts.first().copied().unwrap_or_default()
}
}
Events whose name is not the method’s
When the event’s name is not the method’s name in PascalCase, name gives it:
#![allow(unused)]
fn main() {
const MOVE_FALLING: u8 = 3;
#[ufunction(Override, name = "K2_OnMovementModeChanged")]
fn on_movement_mode_changed(
&mut self,
_prev_movement_mode: u8,
new_movement_mode: u8,
_prev_custom_mode: u8,
_new_custom_mode: u8,
) {
if new_movement_mode == MOVE_FALLING {
}
}
}
Finding an event’s name and parameters
An event is any function the parent class lets Blueprints implement. To find one:
- in a Blueprint child of the parent class, right-click the Event Graph and look under Add Event: the node’s title is the event, and hovering it shows its name;
- in the engine’s C++ headers, events are the
UFUNCTION(BlueprintImplementableEvent)andUFUNCTION(BlueprintNativeEvent)declarations of the class, many namedReceive...orK2_...; - in the generated bindings (
Rust/bindings/src/), the class’s module lists them with their parameters.
A Blueprint child must not implement the same event
A Blueprint child that implements the same event in its graph replaces the Rust one: the engine runs the Blueprint’s graph instead. Keep the Blueprint children of Rust classes for data (meshes, sounds, input actions), and if a Blueprint needs to react too, give it an event of its own (see Functions, BlueprintImplementableEvent).
Functions
The goal: functions Blueprints can call, events Blueprints implement, and functions the engine calls by name (input, timers, delegates).
#![allow(unused)]
fn main() {
#[uclass_impl]
impl Turret {
#[ufunction(BlueprintCallable)]
fn fire(&mut self, target: UObjectRef<Actor>) {
}
#[ufunction(BlueprintPure)]
fn is_reloading(&self) -> bool {
self.reload_left() > 0.0
}
#[ufunction(BlueprintImplementableEvent)]
fn on_fired(&self, muzzle_location: &OwnedStruct<FVector>) {}
}
}
A method marked #[ufunction] is a real Unreal function of the class. It is BlueprintCallable unless it says otherwise:
| Kind | What it is |
|---|---|
#[ufunction] or #[ufunction(BlueprintCallable)] | A function with execution pins in Blueprints, and a function the engine can call by name. |
#[ufunction(BlueprintPure)] | A function without execution pins: a getter, a query. |
#[ufunction(BlueprintImplementableEvent)] | An event the Blueprint child implements. The Rust body is empty; calling the method runs the Blueprint’s graph. |
#[ufunction(Override)] | Rust code for an event of the parent class. See Engine events. |
#[ufunction(Exec)] | A console command too. See Console commands and variables. |
name = "..." gives the Unreal name when the method’s name in PascalCase is not it. Any other argument is a compile error.
Parameters and return values
A function takes scalars (bool, u8, i32, i64, f32, f64), enums (engine enums and #[uenum]s), objects (UObjectRef<T>), classes (SubclassOf<T>) and structs (UStructRef<T>, a reference into the call’s parameters; to_owned() keeps a copy), and returns a scalar, an enum, an object or a class. A BlueprintImplementableEvent takes structs as &OwnedStruct<T>. An Override of an engine event that returns a struct, such as an input modifier’s ModifyRaw, returns an OwnedStruct<T>.
Strings and arrays are not function parameters yet. A Blueprint that needs to pass text to Rust can set a String property and call a function that reads it.
Names
The Unreal name is the method’s name in PascalCase: fire is Fire, do_jump_start is DoJumpStart. A method named after a Rust keyword uses a raw identifier: fn r#move is Move, fn r#use is Use. When PascalCase gets it wrong, name fixes it:
#![allow(unused)]
fn main() {
#[ufunction(name = "SpawnNPC")]
fn spawn_npc(&mut self) {
}
}
Functions the engine calls by name
Several engine features call a function by its Unreal name: an input action bound with bind_action, a timer set with k2_set_timer, a delegate bound with add_ufunction. Any #[ufunction] works for them:
#![allow(unused)]
fn main() {
#[ufunction]
fn respawn(&mut self) {
}
KismetSystemLibrary::k2_set_timer(self.as_ref().upcast_to(), "Respawn", 3.0, false, None, None, None);
}
A name that matches no function is logged with that name. See Timers and Delegates.
Use case: letting the Blueprint play the effects
The goal: Rust decides when something happens; the Blueprint child decides how it looks and sounds.
#![allow(unused)]
fn main() {
#[uclass(parent = Actor)]
pub struct Pickup {
#[component(root)]
root: SceneComponent,
}
#[uclass_impl]
impl Pickup {
#[ufunction(BlueprintImplementableEvent, name = "BP_OnPickedUp")]
fn bp_on_picked_up(&self) {}
#[ufunction(Override)]
fn receive_actor_begin_overlap(&mut self, other_actor: UObjectRef<Actor>) {
if other_actor.cast::<Character>().is_err() {
return;
}
if let Ok(me) = self.as_ref().checked() {
me.set_actor_enable_collision(false);
}
self.bp_on_picked_up();
}
}
}
In the editor:
- Open the Blueprint child of
Pickup(see Blueprint children). - Right-click the Event Graph and search for BP On Picked Up.
- Add the Event BP On Picked Up node, and from it play a sound, spawn a Niagara system, hide the mesh.
- Click Compile, then Save.
Screenshot: the Event Graph of BP_Pickup with the Event BP On Picked Up node playing a sound. (images/classes/implementable_event.png)
The templates name these events BP_... so they are easy to tell apart from the engine’s, and use them for every effect: hit sparks, death animations, UI updates.
Use case: a getter for an Animation Blueprint
The goal: an Animation Blueprint reads gameplay state from the Rust character.
#![allow(unused)]
fn main() {
#[ufunction(BlueprintPure)]
fn has_double_jumped(&self) -> bool {
self.state().has_double_jumped
}
}
In the Animation Blueprint’s Event Graph, cast Try Get Pawn Owner to the character’s class, and call Has Double Jumped on it. The pure function has no execution pins, so it can feed a variable directly.
Blueprint children
The goal: a Blueprint whose parent is a Rust class, to assign the assets the class needs (meshes, sounds, input actions, widget classes) and to tune its values, without touching Rust.
This is how every Rusteal template works: the Rust class declares the properties, and a Blueprint child named BP_<Class> fills them in. The game then uses the Blueprint, not the Rust class directly.
Creating the Blueprint
- In the Content Browser, open the folder the Blueprint goes in.
- Right-click an empty area and choose Blueprint Class.
- In the Pick Parent Class window, expand All Classes.
- Type the Rust class’s name in the search box (
Turret). - Select the class in the list and click Select.
- Name the new asset
BP_Turretand press Enter.
Screenshot: the Pick Parent Class window with All Classes expanded and the Rust class found by the search. (images/classes/pick_parent_class.png)
Filling in the properties
- Double-click BP_Turret to open it.
- Click Class Defaults in the toolbar.
- In the Details panel, find the Rust class’s properties under their categories, and set them: pick the sound in Fire Sound, the projectile Blueprint in Projectile Class.
- Click Compile, then Save.
Screenshot: the Class Defaults of BP_Turret with Fire Sound and Projectile Class assigned. (images/classes/class_defaults_assigned.png)
The components the Rust class declares are in the Components panel: select one to give it a mesh or a material.
Using the Blueprint
Place BP_Turret in the level by dragging it from the Content Browser into the viewport, or spawn it from Rust through a SubclassOf<T> property that holds it (see Spawning actors).
Game mode, pawn and controller
A game mode names the pawn and the controller the game uses. With all three in Rust:
- Create a Blueprint child of each:
BP_MyGameMode,BP_MyCharacter,BP_MyPlayerController. - Open BP_MyGameMode, click Class Defaults, and in the Details panel, under Classes, set Default Pawn Class to BP_MyCharacter and Player Controller Class to BP_MyPlayerController. Compile and Save.
- Open the level, and in the World Settings tab (Window > World Settings), set GameMode Override to BP_MyGameMode.
Or, for every level of the project: Edit > Project Settings, Maps & Modes, Default GameMode.
Screenshot: the World Settings tab with GameMode Override set to the game mode Blueprint. (images/classes/world_settings_game_mode.png)
Rules for Blueprint children
- Data, not logic, for what Rust does. A Blueprint child that implements an event the Rust class implements replaces the Rust code for it. Keep the Blueprint’s graph for the
BlueprintImplementableEvents the Rust class declares for it. - Values changed in the Blueprint win. A value set in the Blueprint’s Class Defaults overrides the Rust default. The arrow next to a changed value resets it.
- Hot reload keeps them. When the Rust class changes shape, its Blueprint children move to the new class with their values. See Hot reload.
Rust classes deriving from Rust classes
The goal: a class that extends another Rust class: a horror character built on the first-person character, an elite enemy built on the enemy.
#![allow(unused)]
fn main() {
use crate::character::FirstPersonCharacter;
#[uclass(parent = FirstPersonCharacter)]
pub struct HorrorCharacter {
#[component(attach = "first_person_camera_component", name = "SpotLight")]
spot_light: SpotLightComponent,
#[uproperty(EditAnywhere, category = "Sprint", default = 600.0)]
sprint_speed: f32,
}
}
The child has the parent’s properties, components and functions, and adds its own:
- its components can attach to the parent’s by their field names;
- its class defaults start from the parent’s: the parent’s
class_defaultsruns first; - Blueprints see one class with everything.
Calling the parent
An event both classes override runs the child’s code. To run the parent’s too, as C++ calls Super::, reach the parent class with from_obj and call it:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let Ok(mut parent) = FirstPersonCharacter::from_obj(self.as_ref()) {
parent.receive_begin_play();
}
}
}
The parent’s methods must be visible to the child’s module: declare the ones the child calls pub(crate).
#![allow(unused)]
fn main() {
#[ufunction(Override)]
pub(crate) fn receive_begin_play(&mut self) {
}
}
The same reaches the parent’s components and Rust methods. The templates keep a helper for it:
#![allow(unused)]
fn main() {
fn base(&self) -> RustealResult<FirstPersonCharacter> {
FirstPersonCharacter::from_obj(self.as_ref())
}
let camera = self.base()?.first_person_camera_component()?.checked()?;
}
Replacing a parent’s function
A function the child declares again with the same name replaces the parent’s, for Blueprints and for the engine:
#![allow(unused)]
fn main() {
#[ufunction(BlueprintCallable)]
fn do_move(&mut self, right: f32, forward: f32) {
if !self.is_dead()
&& let Ok(mut parent) = FirstPersonCharacter::from_obj(self.as_ref())
{
parent.do_move(right, forward);
}
}
}
Hot reload
When a parent class changes shape, its Rust children are created again with it, and their objects move to the new classes. See Hot reload.
Interfaces
Two different tools answer “different classes that respond to the same call”: Unreal interfaces, which Blueprints understand, and Rust traits, which only Rust code sees. A #[uinterface] trait is both.
Use case: an interface declared in Rust
Not released yet.
#[uinterface]is in development and comes in a release after 0.7.0. Until then, a Rust class implements the interfaces declared in C++ or as Blueprint Interface assets, by class path: see implementing a Blueprint Interface.
The goal: doors, chests and levers the player interacts with, some written in Rust and some in Blueprints, and one call that reaches any of them.
In Rust
#![allow(unused)]
fn main() {
use rusteal_runtime::{uclass, uclass_impl, uinterface};
#[uinterface]
pub trait Interactable {
fn interact(&mut self, instigator: UObjectRef<Actor>) -> bool;
fn highlight(&mut self, on: bool);
}
}
#[uinterface] makes the trait an Unreal interface, Interactable, whose functions are Blueprint events: Interact returns a Boolean, Highlight returns nothing. Its functions take &self or &mut self, then what a #[ufunction] takes (see Functions), and return what it returns. They have no body: each class implementing the interface writes its own.
A Rust class implements it with #[uclass_impl] on the trait’s implementation. Its methods are the interface’s functions, with no #[ufunction]:
#![allow(unused)]
fn main() {
#[uclass(parent = Actor)]
pub struct Door {
#[uproperty(EditAnywhere)]
b_locked: bool,
#[uproperty(VisibleAnywhere)]
b_open: bool,
}
#[uclass_impl]
impl Interactable for Door {
fn interact(&mut self, _instigator: UObjectRef<Actor>) -> bool {
if self.b_locked() {
return false;
}
self.set_b_open(!self.b_open());
true
}
fn highlight(&mut self, on: bool) {
log::info!("Door highlighted: {on}");
}
}
}
The class keeps its other #[uclass_impl] block for its own functions and events.
In the editor
A Blueprint implements it as it implements a Blueprint Interface:
- Open the Blueprint (a chest, say), and click Class Settings in the toolbar.
- In the Details panel, under Interfaces, click Add next to Implemented Interfaces and pick Interactable.
- In the My Blueprint panel, under Interfaces, double-click Interact: a function graph opens, since it returns a value. Connect its logic to the Return Node.
- Right-click the Event Graph, search for Event Highlight, and place it: functions with no return value are events.
- Click Compile, then Save.
Screenshot: a chest Blueprint’s Class Settings, with Interactable under Implemented Interfaces and the Interact function in My Blueprint. (images/classes/uinterface_bp.png)
A Blueprint child of Door has the interface too: double-clicking Interact in its My Blueprint panel overrides Door’s, as a C++ BlueprintNativeEvent is overridden, and Add call to parent function in the graph’s context menu runs the Rust one.
A Rust class that leaves the functions to its Blueprint children lists the interface instead of implementing it:
#![allow(unused)]
fn main() {
#[uclass(parent = Actor, implements = [Interactable])]
pub struct Lever {}
}
Calling it
From Rust, on any object, whichever language implements it:
#![allow(unused)]
fn main() {
if let Ok(mut target) = hit_actor.interface::<dyn Interactable>() {
target.interact(self.as_ref().upcast_to());
}
}
interface::<dyn Interactable>() turns an object into the interface, as C++’s Cast<IInteractable>: an error when its class does not implement it. implements::<dyn Interactable>() only tells. The ScriptInterface<dyn Interactable> it gives (C++’s TScriptInterface) calls the object’s own implementation, Rust or Blueprint; a function the object’s class does not implement returns the default value (false, 0, a null reference), as a Blueprint’s interface message does.
From a Blueprint: the Interact (Message) node on any actor, or Interact on an Interactable reference; Does Implement Interface with Interactable finds out whether an actor has it.
Screenshot: a Blueprint graph calling Interact (Message) on a line trace’s Hit Actor. (images/classes/uinterface_message.png)
Notes
- The interface is
/Script/Rusteal.Interactable, a plugin’s/Script/<Plugin>.Interactable. The classes of another library (the game, for a plugin’s interface) implement it by that path, as below. - Changing it during a hot reload keeps the interface and gives it the new functions; the Blueprints implementing or calling it are recompiled, as when a Blueprint Interface changes. See Hot reload.
Use case: implementing a Blueprint Interface
The goal: a Rust door that a Blueprint can Use through a Blueprint Interface, like any Blueprint door.
In the editor
- In the Content Browser, right-click an empty area and choose Blueprint > Blueprint Interface.
- Name it
BPI_Usableand open it. - In the My Blueprint panel, rename the first function to
Use. - With
Useselected, in the Details panel, add an Input namedInstigatorof type Actor (Object Reference), and an Output namedReturnValueof type Boolean. - Click Compile, then Save.
Screenshot: BPI_Usable open, with the Use function’s Instigator input and ReturnValue output in the Details panel. (images/classes/bpi_usable.png)
In Rust
#![allow(unused)]
fn main() {
#[uclass(parent = Actor, implements = ["/Game/Interaction/BPI_Usable.BPI_Usable_C"])]
pub struct Door {
#[uproperty(EditAnywhere)]
b_locked: bool,
}
#[uclass_impl]
impl Door {
#[ufunction]
fn r#use(&mut self, _instigator: UObjectRef<Actor>) -> bool {
!self.b_locked()
}
}
}
implements lists interfaces by class path:
- a Blueprint Interface asset: its path with
_Cafter the asset name,/Game/Interaction/BPI_Usable.BPI_Usable_CforBPI_UsableinContent/Interaction/; - a C++ interface:
/Script/<Module>.<Interface>without theU,/Script/Engine.NavAgentInterface.
Each interface function is a #[ufunction] named as the interface’s function (in snake_case, or with name), taking the same parameters in the same order and returning its return value.
Calling it
A Blueprint calls it with the Use (Message) node on any actor, and Does Implement Interface finds out whether an actor has it. C++ calls it with IUsable::Execute_Use. They reach the Rust class as they reach a Blueprint that implements the interface.
Notes
- The interface loads once the engine is initialized, so the class implements it from then on.
- An interface whose functions are not Blueprint events (one marked
CannotImplementInterfaceInBlueprint) can only be implemented in C++, and is refused, as Blueprints refuse it. - To declare the interface in Rust instead, see the first use case.
Use case: Rust traits between Rust classes
The goal: several Rust classes take damage, and the code that deals it does not care which one it hit.
When only Rust code calls it, a Rust trait is the lighter tool. The templates declare the trait, implement it on each class, and turn an actor into the trait with from_obj:
#![allow(unused)]
fn main() {
pub trait Damageable {
fn apply_damage(&mut self, damage: f32, causer: UObjectRef<Actor>);
}
impl Damageable for Enemy {
fn apply_damage(&mut self, damage: f32, _causer: UObjectRef<Actor>) {
self.set_current_hp(self.current_hp() - damage);
}
}
impl Damageable for DamageableBox {
fn apply_damage(&mut self, damage: f32, _causer: UObjectRef<Actor>) {
}
}
pub fn damageable(actor: UObjectRef<Actor>) -> Option<Box<dyn Damageable>> {
if let Ok(enemy) = Enemy::from_obj(actor) {
Some(Box::new(enemy))
} else if let Ok(damageable_box) = DamageableBox::from_obj(actor) {
Some(Box::new(damageable_box))
} else {
None
}
}
}
The caller:
#![allow(unused)]
fn main() {
if let Some(mut target) = damageable(other_actor) {
target.apply_damage(self.damage(), self.as_ref().upcast_to());
}
}
A trait is invisible to Blueprints: a Blueprint class cannot implement it, and a Blueprint cannot call it. When a Blueprint must take part, put #[uinterface] on the trait: the classes implement it with #[uclass_impl], and interface::<dyn Damageable>() replaces the damageable function.
Structs and data tables
The goal: a struct declared in Rust that Unreal understands: the row of a data table, or a group of values a class’s property holds.
#![allow(unused)]
fn main() {
use bindings::engine::StaticMesh;
use rusteal_runtime::runtime::{SoftObjectRef, SubclassOf};
use rusteal_runtime::ustruct;
#[ustruct]
pub struct WeaponTableRow {
#[uproperty(EditAnywhere)]
static_mesh: SoftObjectRef<StaticMesh>,
#[uproperty(EditAnywhere)]
weapon_to_spawn: SubclassOf<Weapon>,
#[uproperty(EditAnywhere)]
damage: f32,
}
}
A #[ustruct] is Unreal’s USTRUCT(). Every field is a #[uproperty], of the kinds a class’s properties are, without default: a struct starts zeroed. Its memory belongs to Unreal (a data table’s row, a property of an object, an OwnedStruct), so the Rust type itself is a marker, and the fields are read and written through a trait generated next to it, WeaponTableRowExt, on UStructRef<WeaponTableRow> and OwnedStruct<WeaponTableRow>:
#![allow(unused)]
fn main() {
use super::pickup::WeaponTableRowExt;
let damage = row.damage();
row.set_damage(30.0);
}
Use case: a data table of weapons
The goal: designers list the weapons in a table, and a pickup shows and grants the one it is set to.
In the editor
- In the Content Browser, right-click an empty area and choose Miscellaneous > Data Table.
- In the Pick Row Structure window, choose WeaponTableRow and click OK.
- Name it
DT_WeaponDataand open it. - Click Add to add a row, name it (
Pistol), and fill in Static Mesh, Weapon To Spawn and Damage in the Row Editor. - Click Save.
Screenshot: DT_WeaponData open, with a Pistol row filled in the Row Editor. (images/classes/data_table_rows.png)
In Rust
The pickup holds a row handle, which the editor shows as a table and a row name:
#![allow(unused)]
fn main() {
use bindings::engine::{FDataTableRowHandle, StaticMeshComponent, StaticMeshComponentExt};
use bindings::prelude::*;
#[uclass(parent = Actor)]
pub struct WeaponPickup {
#[component(root)]
mesh: StaticMeshComponent,
#[uproperty(EditAnywhere, category = "Pickup")]
weapon_type: OwnedStruct<FDataTableRowHandle>,
#[uproperty]
weapon_class: SubclassOf<Weapon>,
}
#[uclass_impl]
impl WeaponPickup {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
let weapon_type = self.weapon_type();
let Some(row) = weapon_type.get_row::<WeaponTableRow>() else {
log::warn!("[MyGame] pickup without a weapon type");
return;
};
if let Ok(static_mesh) = row.static_mesh().load_synchronous()
&& let Ok(mesh) = self.mesh().and_then(|m| m.checked())
{
mesh.set_static_mesh(static_mesh);
}
self.set_weapon_class(row.weapon_to_spawn());
}
}
}
get_row::<T>() is C++’s FindRow<T>: it returns None when the handle points to no table, to a missing row, or to a table of another row struct. A row can also be found by table and name:
#![allow(unused)]
fn main() {
let row = find_data_table_row::<WeaponTableRow>(table, FName::new("Pistol"));
}
Then, on each WeaponPickup in the level (or on its Blueprint child), set Weapon Type: the Data Table to DT_WeaponData and the Row Name to Pistol.
Screenshot: the Details panel of a WeaponPickup with Weapon Type set to DT_WeaponData and the Pistol row. (images/classes/row_handle.png)
Use case: a struct property
A class can hold your struct as a property:
#![allow(unused)]
fn main() {
#[uproperty(EditAnywhere)]
stats: OwnedStruct<WeaponTableRow>,
}
The getter returns a copy: change it and set it back.
#![allow(unused)]
fn main() {
let stats = self.stats();
stats.as_ref().set_damage(stats.as_ref().damage() * 2.0);
self.set_stats(&stats);
}
An array inside a struct, such as a gearbox’s gear ratios, is a UeArray view into the struct it belongs to: change it on the copy, then set the copy back.
#![allow(unused)]
fn main() {
let transmission = movement.get_transmission_setup();
transmission.as_ref().forward_gear_ratios().set_all(&[4.25, 2.52, 1.66, 1.22, 1.0])?;
movement.set_transmission_setup(&transmission);
}
Hot reload
A changed #[ustruct] keeps its old fields until the editor restarts: the Output Log says so when it happens. Classes reload without a restart; structs do not, yet.
Enums
The goal: an enum declared in Rust that Unreal understands: a property a designer picks from a dropdown, a function parameter a Blueprint passes, a value Blueprints switch on.
#![allow(unused)]
fn main() {
use rusteal_runtime::uenum;
#[uenum]
pub enum EGrabType {
None,
Snap,
Custom = 3,
Free,
}
}
A #[uenum] is Unreal’s UENUM(BlueprintType). Its variants carry no data, and their values go up to 254, as a Blueprint enum’s uint8 does (Unreal adds the _MAX entry after the last). Values follow Rust’s rules: the first is 0, each next one adds 1, and = 3 sets one. The macro sets the representation, u8, and derives Clone, Copy, PartialEq, Eq, Hash and Debug, so the enum is a plain Rust value too:
#![allow(unused)]
fn main() {
match self.grab_type() {
EGrabType::Free | EGrabType::Snap => self.attach_to(controller),
EGrabType::Custom => self.set_b_is_held(true),
EGrabType::None => {}
}
}
Use case: a grab type designers choose
The goal: a grab component whose kind of grab each object sets in its Blueprint, from a dropdown.
#![allow(unused)]
fn main() {
#[uclass(parent = SceneComponent)]
pub struct GrabComponent {
#[uproperty(EditAnywhere, BlueprintReadWrite, category = "Grab", default = EGrabType::Free)]
grab_type: EGrabType,
}
}
default takes a variant, as it does for engine enums. A Rust class that places the component can choose it in its class defaults:
#![allow(unused)]
fn main() {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
GrabComponent::from_obj(self.grab_component_snap()?)?.set_grab_type(EGrabType::Snap);
Ok(())
}
}
In the editor
- Open the Blueprint that has the component (a
BP_Pistolchild of the Rust pistol). - In the Components panel, select GrabComponentSnap.
- In the Details panel, under Grab, Grab Type lists None, Snap, Custom and Free.
Screenshot: the Details panel of a grab component, with the Grab Type dropdown open. (images/classes/uenum_dropdown.png)
In a Blueprint graph, the enum has the usual nodes: Switch on EGrabType, == (Equal), Literal enum EGrabType.
Enums in functions
#[uenum]s and engine enums are #[ufunction] parameters and return values:
#![allow(unused)]
fn main() {
#[ufunction(BlueprintPure)]
pub fn get_held_by_hand(&self) -> EControllerHand {
match model::hand_of_motion_source(&self.motion_source_name()) {
model::Hand::Left => EControllerHand::Left,
model::Hand::Right => EControllerHand::Right,
}
}
#[ufunction(BlueprintCallable)]
pub fn set_vr_spectator_mode(&mut self, new_vr_spectator_mode: EVRSpectatorMode) {
self.set_spectator_mode(new_vr_spectator_mode);
}
}
The enum must be a one-byte one, as every Blueprint enum is; an engine enum stored on more bytes is refused when the crate compiles.
Notes
- The enum’s Unreal name is the Rust name. Its entries are
EGrabType::Snapto C++ and ini files, Snap in the editor. - An enum created by a hot reload keeps its first values: adding a variant takes an editor restart, as changing a
#[ustruct]does. - An enum asset made in the editor (Blueprint > Enumeration) is not a Rust type. To use one from Rust, declare it again with
#[uenum]and point the Blueprints at the Rust one.
Object references
Every Unreal object Rust code touches, an actor, a component, an asset, a class’s default object, is reached through a reference. Unreal owns the objects and destroys them when its garbage collector decides; Rust code holds references to them and checks them before use.
UObjectRef
#![allow(unused)]
fn main() {
let target: UObjectRef<Actor> = self.current_target();
}
UObjectRef<T> is a handle to an object of class T: eight bytes, Copy, cheap to pass around and to store. It does not keep the object alive, and the object may be destroyed while you hold it.
| Method | What it does |
|---|---|
is_valid() | Whether the handle points to an object (it can still be pending destruction). |
checked() | Verifies the object is alive and gives access to its functions, or an ObjectDestroyed error. |
cast::<U>() | The same object as class U, checked at runtime, or an InvalidCast error naming the object and the class expected. |
upcast_to::<U>() | The same object as an ancestor class U, checked at compile time. |
is_a::<U>() | Whether the object is of class U or derives from it. |
get_name() | The object’s name. |
pin() | A Pinned<T> that keeps the object alive. |
UObjectRef::null() | A handle to nothing, also its Default. |
Calling functions: checked
Engine functions are called on a checked reference:
#![allow(unused)]
fn main() {
let me = self.as_ref().checked()?;
let location = me.k2_get_actor_location().to_dvec3();
me.k2_destroy_actor();
}
checked() verifies the object is still alive once, and the calls after it go straight to the engine. A call on a destroyed object would crash the engine in C++; in Rust, checked() returns an error first, and the code decides what to do:
#![allow(unused)]
fn main() {
let Ok(target) = self.current_target().checked() else {
return;
};
}
Casting
#![allow(unused)]
fn main() {
if let Ok(character) = other_actor.cast::<Character>() {
}
let as_object: UObjectRef<Object> = actor.upcast_to::<Object>();
}
cast is C++’s Cast<T>: a runtime check. upcast_to is free and cannot fail, because the compiler knows Character is an Actor. The engine functions that take a world context or a base class (UObject* WorldContextObject, AActor*) take the upcast:
#![allow(unused)]
fn main() {
let world_context = self.as_ref().upcast_to();
let player = GameplayStatics::get_player_pawn(world_context, 0);
}
An object’s class and outer come typed as well: actor.class()? is a SubclassOf<T> of the reference’s own type, and component.outer::<Actor>()? is the outer, checked to be an actor.
For your own Rust classes, MyClass::from_obj(reference) does the cast and gives the Rust class with its getters and methods. See Declaring a class.
Keeping an object alive
The garbage collector keeps an object alive while something it knows about references it. A UObjectRef in a Rust-only field or a local variable is invisible to it. To keep an object:
-
Hold it in a
#[uproperty]. The usual way, and what C++ does withUPROPERTY()pointers.#![allow(unused)] fn main() { #[uproperty] spawned_weapon: UObjectRef<Weapon>, } -
Pin it.
Pinned<T>is a garbage collection root: the object lives until thePinnedis dropped. For objects Rust code alone keeps, outside any class.#![allow(unused)] fn main() { let pinned: Pinned<Actor> = actor.pin()?; let location = pinned.k2_get_actor_location(); } -
Watch it.
TWeakObjectPtr<T>does not keep the object, and tells you when it is gone:#![allow(unused)] fn main() { let weak = TWeakObjectPtr::from_ref(&actor); if let Some(actor) = weak.get() { } }
Classes: SubclassOf
A class is not an object reference. SubclassOf<T> is Unreal’s TSubclassOf<T>, a class that is T or derives from it:
#![allow(unused)]
fn main() {
#[uproperty(EditAnywhere)]
projectile_class: SubclassOf<Projectile>,
}
| Expression | What it is |
|---|---|
SubclassOf::<Projectile>::base() | The Projectile class itself. |
class.upcast_to::<Actor>() | The same class, as a SubclassOf<Actor>, for engine functions that take any actor class. |
class.is_null() | Whether no class is set (a Blueprint child that did not fill the property). |
Engine functions that take a class filter (get_all_actors_of_class, get_overlapping_actors) take a SubclassOf:
#![allow(unused)]
fn main() {
let units = GameplayStatics::get_all_actors_of_class(
self.as_ref().upcast_to(),
SubclassOf::<Unit>::base().upcast_to(),
);
}
Threads
References are Send, so a background thread can carry them, but Unreal objects belong to the game thread: call the engine only from there. See Work off the game thread.
Calling the engine
The bindings give Rust every function Blueprints can call, for the engine modules the project binds. This page covers how they look from Rust.
Finding a function
A Blueprint node is a function of some class. Its Rust name is its C++ name in snake_case, on the class’s Ext trait:
| Blueprint node | C++ | Rust |
|---|---|---|
| Get Actor Location | AActor::K2_GetActorLocation | actor.k2_get_actor_location() (ActorExt) |
| Set Max Walk Speed | UCharacterMovementComponent::MaxWalkSpeed | movement.set_max_walk_speed(600.0) (CharacterMovementComponentExt) |
| Launch Character | ACharacter::LaunchCharacter | character.launch_character(&v, true, true) (CharacterExt) |
| Get Player Controller | UGameplayStatics::GetPlayerController | GameplayStatics::get_player_controller(ctx, 0) |
The C++ name is in the node’s tooltip in the Blueprint editor, and in the engine’s headers. Many Blueprint nodes are K2_ functions in C++ (K2_GetActorLocation, K2_SetTimer, K2_DestroyActor).
Import the class and its Ext trait from its module:
#![allow(unused)]
fn main() {
use bindings::engine::{Character, CharacterExt, CharacterMovementComponentExt};
}
rust-analyzer completes the methods on a checked reference, which is the quickest way to browse what a class offers.
Static functions
Function libraries (UGameplayStatics, UKismetSystemLibrary, UKismetMathLibrary) are called on the class, as in C++:
#![allow(unused)]
fn main() {
let now = GameplayStatics::get_time_seconds(self.as_ref().upcast_to());
let roll = KismetMathLibrary::random_integer_in_range(0, 100);
}
Properties of engine classes
The properties Blueprints can read have a getter, get_<name>(), and the ones they can write a setter, set_<name>(): get_max_walk_speed(), set_max_walk_speed(600.0). An array property’s getter is the property’s name alone (tags()). Private engine properties that Blueprints can read (ACharacter’s Mesh, CharacterMovement and CapsuleComponent) have getters, without setters when they are read-only:
#![allow(unused)]
fn main() {
let mesh = me.get_mesh().checked()?;
let movement = me.get_character_movement().checked()?;
let tags = me.tags();
}
An array property returns a view of the array inside the object (UeArray), changed in place:
#![allow(unused)]
fn main() {
me.tags().push(&FName::new("Dead").handle())?;
}
Optional parameters
A parameter with a default value in C++ is an Option in Rust. None keeps the default:
#![allow(unused)]
fn main() {
anim_instance.montage_play(montage, Some(1.0), None, Some(0.0), Some(true));
capsule.set_capsule_size(42.0, 96.0, Some(false));
}
Out parameters
A function with out parameters returns them in a tuple, after its return value:
#![allow(unused)]
fn main() {
let (hit, out_hit) = KismetSystemLibrary::line_trace_single(...);
let (projected, screen_position) = controller.project_world_location_to_screen(&location, Some(true));
let (found, x, y) = controller.get_mouse_position();
}
Names, strings and text
| C++ | Rust argument | Rust return value |
|---|---|---|
FName | FName::new("Socket").handle() | FNameHandle, FName::from(handle) |
FString | &str | String |
#![allow(unused)]
fn main() {
let socket = FName::new("HandGrip_R");
let location = mesh.get_socket_location(socket.handle()).to_dvec3();
}
Math
Engine structs (FVector, FRotator, FTransform, FQuat, FLinearColor) are Unreal’s memory, held in an OwnedStruct. Rust math uses glam (DVec3, DQuat, DVec2) and Rusteal’s own Rotator, Transform and LinearColor. Convert at the boundary:
| From | To |
|---|---|
FVector returned by the engine | .to_dvec3() |
a DVec3 passed to the engine | &FVector::from_dvec3(v) |
FRotator | .to_rotator(), FRotator::from_rotator(Rotator::new(pitch, yaw, roll)) |
FTransform | .to_transform(), FTransform::from_transform(Transform::new(rotation, translation, scale)) |
FQuat | .to_dquat(), FQuat::from_dquat(q) |
FVector2D | .to_dvec2(), FVector2D::from_dvec2(v) |
FLinearColor | FLinearColor::from_linear_color(LinearColor::new(r, g, b, a)) |
#![allow(unused)]
fn main() {
let me = self.as_ref().checked()?;
let forward = me.get_actor_forward_vector().to_dvec3();
let target = me.k2_get_actor_location().to_dvec3() + forward * 500.0;
me.k2_set_actor_location(&FVector::from_dvec3(target), false, true);
}
The conversions and FVector, FRotator, FTransform come with use bindings::prelude::*. Unreal’s units are kept: centimeters, degrees for rotators, Z up.
Structs: reading and writing fields
An engine struct has a getter and a setter per field, on its Ext trait (FHitResultExt, FVectorExt):
#![allow(unused)]
fn main() {
use bindings::engine::{ActorComponentExt, FHitResultExt};
let hit = out_hit.as_ref();
let impact = hit.get_impact_point().to_dvec3();
let normal = hit.get_impact_normal().to_dvec3();
let other = hit.get_component().checked()?.get_owner();
}
OwnedStruct<T> is a struct Rust owns; as_ref() gives the UStructRef<T> the field accessors work on. OwnedStruct::new() is a struct with Unreal’s default values, the usual way to pass “no options”:
#![allow(unused)]
fn main() {
subsystem.add_mapping_context(context, 0, &OwnedStruct::new());
}
To change a struct an engine object holds, read it, change the copy and write it back:
#![allow(unused)]
fn main() {
let nav = movement.get_nav_agent_props();
nav.as_ref().set_agent_radius(42.0);
movement.set_nav_agent_props(&nav);
}
Enums
Engine enums are Rust enums with the C++ names: ECollisionChannel::ECC_Pawn, ETriggerEvent::Triggered, EAttachmentRule::SnapToTarget. Events receive some enums as their u8 value (receive_end_play(&mut self, end_play_reason: u8)). Your own enums are declared with #[uenum]; see Enums.
Functions that are not in the bindings
A function that reflection exposes but the bindings leave out (a Blueprint’s own function, a module the project does not bind) can still be called by name. See Dynamic calls.
Spawning actors
The goal: put actors into the level from code: projectiles, enemies, pickups, effects.
Spawning a Blueprint chosen in the editor
The usual case: the class to spawn is a Blueprint child (BP_Projectile), picked in a SubclassOf property.
#![allow(unused)]
fn main() {
use bindings::engine::{ESpawnActorCollisionHandlingMethod, GameplayStatics, KismetMathLibrary};
use bindings::prelude::*;
#[uclass(parent = Character)]
pub struct Shooter {
#[uproperty(EditAnywhere, category = "Projectile")]
projectile_class: SubclassOf<Projectile>,
}
impl Shooter {
fn spawn_projectile(&self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
let location = me.k2_get_actor_location().to_dvec3()
+ me.get_actor_forward_vector().to_dvec3() * 100.0;
let transform = KismetMathLibrary::make_transform(
&FVector::from_dvec3(location),
&me.k2_get_actor_rotation(),
&FVector::from_dvec3(DVec3::ONE),
);
let world = self.as_ref().get_world()?;
world.spawn_actor_of_class(self.projectile_class(), &transform)?;
Ok(())
}
}
}
get_world() gives the world an object lives in, and spawn_actor_of_class spawns an actor of a class held in a SubclassOf. It returns an UObjectRef<T> to the new actor, or an error if the class is not set or the spawn failed.
Deferred spawn: setting values before BeginPlay
To set an actor up before its BeginPlay runs (an owner, an instigator, a team), spawn it deferred and finish the spawn after:
#![allow(unused)]
fn main() {
let spawned = GameplayStatics::begin_deferred_actor_spawn_from_class(
me.as_ref().upcast_to(),
self.projectile_class().upcast_to(),
&transform,
Some(ESpawnActorCollisionHandlingMethod::AlwaysSpawn),
Some(me.as_ref().upcast_to()),
None,
);
if let Ok(projectile) = spawned.checked() {
projectile.set_instigator(me.as_ref().upcast_to());
}
let spawned = GameplayStatics::finish_spawning_actor(spawned, &transform, None);
if let Ok(projectile) = Projectile::from_obj(spawned) {
projectile.set_damage(self.damage());
}
}
The fifth argument is the owner. The same, with the world and typed parameters:
#![allow(unused)]
fn main() {
let params = SpawnParams {
owner: Some(me.as_ref().upcast_to()),
instigator: Some(pawn),
collision: SpawnCollisionMethod::AlwaysSpawn,
};
let projectile = world.spawn_actor_deferred_of_class(self.projectile_class(), &transform, params)?;
Projectile::from_obj(projectile)?.set_damage(self.damage());
world.finish_spawning(&projectile.upcast_to(), &transform)?;
}
spawn_actor_deferred_of_class returns the actor already typed as the class’s T; SpawnParams::default() has no owner, no instigator and the class’s own collision handling. The collision handling method decides what happens when the spawn point is blocked:
| Method | What happens |
|---|---|
AlwaysSpawn | Spawns where asked, overlapping or not. |
AdjustIfPossibleButAlwaysSpawn | Moves the actor out of the way if it can, spawns anyway. |
AdjustIfPossibleButDontSpawnIfColliding | Moves the actor out of the way, or does not spawn. |
DontSpawnIfColliding | Does not spawn if the point is blocked: the returned reference is not valid. |
Spawning a Rust class directly
A Rust class with no Blueprint child, or an engine class, is spawned by type:
#![allow(unused)]
fn main() {
let transform = FTransform::from_transform(Transform::new(DQuat::IDENTITY, location, DVec3::ONE));
let gem: UObjectRef<Gem> = world.spawn_actor(&transform)?;
}
Without a Blueprint child, no assets are assigned: give the actor its mesh in code, or prefer a SubclassOf property and a Blueprint.
An actor class without a root component ignores the spawn transform; Rusteal logs a warning when that happens.
Destroying
#![allow(unused)]
fn main() {
me.k2_destroy_actor();
me.set_life_span(5.0);
}
k2_destroy_actor destroys the actor at the end of the frame. set_life_span destroys it after that many seconds.
Finding actors
#![allow(unused)]
fn main() {
let world_context = self.as_ref().upcast_to();
let enemies = GameplayStatics::get_all_actors_of_class(
world_context,
SubclassOf::<Enemy>::base().upcast_to(),
);
let starts = GameplayStatics::get_all_actors_of_class_with_tag(
world_context,
SubclassOf::<PlayerStart>::base().upcast_to(),
FName::new("Player1").handle(),
);
let player_pawn = GameplayStatics::get_player_pawn(world_context, 0);
let player_controller = GameplayStatics::get_player_controller(world_context, 0);
let game_mode = GameplayStatics::get_game_mode(world_context);
}
get_all_actors_of_class walks every actor of the level: fine on a key press or at the start of a level, too slow every frame for a big level. Keep the actors you need in a property, or let them register themselves with a subsystem.
Your Rust game mode, from any actor:
#![allow(unused)]
fn main() {
if let Ok(mut game_mode) = MyGameMode::from_obj(GameplayStatics::get_game_mode(world_context)) {
game_mode.add_score(10);
}
}
Assets and objects
Assets assigned in the editor
Most assets reach Rust code through properties a Blueprint child fills: a UObjectRef<StaticMesh>, a UObjectRef<SoundBase>, a SubclassOf<Actor>. That is the recommended way: the cooker sees the reference and packages the asset.
#![allow(unused)]
fn main() {
#[uproperty(EditAnywhere)]
explosion_sound: UObjectRef<SoundBase>,
}
Loading an asset by path
#![allow(unused)]
fn main() {
let mesh: UObjectRef<StaticMesh> = load_object("/Game/Props/SM_Crate.SM_Crate")?;
let cube: UObjectRef<StaticMesh> = load_object("/Engine/BasicShapes/Cube")?;
}
A class, a Blueprint’s included, loads with load_class, checked against the class it must derive from:
#![allow(unused)]
fn main() {
let class: SubclassOf<AnimInstance> = load_class("/Game/Characters/ABP_Hero.ABP_Hero_C")?;
mesh.set_anim_instance_class(class.upcast_to());
}
A Blueprint’s class path ends in _C. load_object loads the asset if it is not loaded yet, and returns an error if there is none at that path. find_object finds it only if it is already loaded. The path is the asset’s object path, which Copy Reference in the Content Browser’s right-click menu puts on the clipboard.
An asset that only load_object uses is not found by the cooker, because nothing references it: list its directory in Project Settings > Packaging > Additional Asset Directories to Cook, or the packaged game will not have it. See Packaging a game.
Soft references: loading on demand
#![allow(unused)]
fn main() {
#[uproperty(EditAnywhere)]
boss_music: SoftObjectRef<SoundBase>,
}
SoftObjectRef<T> is Unreal’s TSoftObjectPtr<T>: an asset picked in the editor but not loaded with the object. It keeps big or rarely used assets out of memory until needed:
#![allow(unused)]
fn main() {
let music = self.boss_music().load_synchronous()?;
let loaded_already = self.boss_music().get();
}
load_synchronous loads it now (a hitch for a large asset); get returns it only if it is already loaded, or a null reference.
Creating objects
#![allow(unused)]
fn main() {
use bindings::manual::world_ext::new_object;
let entry = new_object::<MenuEntry>(&self.as_ref())?;
}
new_object::<T>(outer) creates an object of class T inside outer, as C++’s NewObject<T>(Outer). For actors, spawn instead (see Spawning actors); for widgets, see UI.
Keep the new object in a #[uproperty], or the garbage collector may collect it.
Timers
The goal: run something later, once or repeatedly: a respawn in three seconds, a weapon’s refire, a stamina tick.
#![allow(unused)]
fn main() {
use bindings::engine::KismetSystemLibrary;
#[uclass_impl]
impl Enemy {
#[ufunction]
fn remove_from_level(&mut self) {
if let Ok(me) = self.as_ref().checked() {
me.k2_destroy_actor();
}
}
#[ufunction(Override)]
fn receive_end_play(&mut self, _end_play_reason: u8) {
KismetSystemLibrary::k2_clear_timer(self.as_ref().upcast_to(), "RemoveFromLevel");
}
}
impl Enemy {
fn die(&mut self) {
KismetSystemLibrary::k2_set_timer(
self.as_ref().upcast_to(),
"RemoveFromLevel",
5.0,
false,
None,
None,
None,
);
}
}
}
k2_set_timer is the Blueprint node Set Timer by Function Name: it calls a function of the object, by its Unreal name, after the given seconds. The function is any #[ufunction] of the class: remove_from_level is RemoveFromLevel.
| Argument | What it is |
|---|---|
| object | the object whose function is called, as UObjectRef<Object> (self.as_ref().upcast_to()) |
| function name | the Unreal name of the function |
| time | seconds until the call |
| looping | true to call it every time seconds |
| the rest | initial start delay and variance, None for the defaults |
k2_clear_timer stops a timer by the same object and name. Clear the timers of an object in its receive_end_play, so none fires on an object being destroyed. Setting a timer that is already running restarts it.
Use case: a looping timer at a fixed rate
The goal: update stamina thirty times a second, whatever the frame rate.
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
KismetSystemLibrary::k2_set_timer(
self.as_ref().upcast_to(),
"SprintFixedTick",
1.0 / 30.0,
true,
None,
None,
None,
);
}
#[ufunction]
fn sprint_fixed_tick(&mut self) {
}
}
Use case: refire with a cooldown
The goal: an automatic weapon fires every refire_rate seconds while the trigger is held; a semi-automatic one fires once and waits.
#![allow(unused)]
fn main() {
#[ufunction]
fn fire(&mut self) {
if !self.is_firing() {
return;
}
let next = if self.b_full_auto() { "Fire" } else { "FireCooldownExpired" };
KismetSystemLibrary::k2_set_timer(
self.as_ref().upcast_to(),
next,
self.refire_rate(),
false,
None,
None,
None,
);
}
}
Timers and the game’s time
Timers follow the game: they pause when the game is paused, and slow down with time dilation. To know the current game time:
#![allow(unused)]
fn main() {
let now = GameplayStatics::get_time_seconds(self.as_ref().upcast_to());
}
Comparing it with a stored time is the other way to handle cooldowns, without a timer:
#![allow(unused)]
fn main() {
if now - self.last_attack_time() > f64::from(self.cooldown()) {
self.set_last_attack_time(now);
}
}
Every frame without an actor
receive_tick runs every frame on an actor. Code that is not an actor, or that must also run in the editor outside Play, can register a frame callback. See Work off the game thread, task::on_tick.
Traces and overlaps
The goal: ask the world what is in a line, a sphere or an area: what the player aims at, what a sword swing hits, what is caught in an explosion.
All of them are functions of KismetSystemLibrary, the Blueprint nodes Line Trace By Channel, Sphere Trace For Objects, Sphere Overlap Actors and the others.
Line trace: what the player aims at
#![allow(unused)]
fn main() {
use bindings::engine::{EDrawDebugTrace, ETraceTypeQuery, FHitResultExt, KismetSystemLibrary};
fn aim_point(&self, start: DVec3, direction: DVec3) -> DVec3 {
let end = start + direction * 10_000.0;
let me = self.as_ref().upcast_to::<Actor>();
let (hit, out_hit) = KismetSystemLibrary::line_trace_single(
me.upcast_to(),
&FVector::from_dvec3(start),
&FVector::from_dvec3(end),
ETraceTypeQuery::TraceTypeQuery1,
false,
&[me],
EDrawDebugTrace::None,
true,
&OwnedStruct::new(),
&OwnedStruct::new(),
None,
);
if hit {
out_hit.as_ref().get_impact_point().to_dvec3()
} else {
end
}
}
}
| Argument | What it is |
|---|---|
| world context | any object in the world |
| start, end | the line |
| trace channel | TraceTypeQuery1 is Visibility, TraceTypeQuery2 is Camera |
| trace complex | true traces against the mesh’s triangles instead of its collision |
| actors to ignore | &[] for none |
| draw debug type | EDrawDebugTrace::ForDuration draws the trace in the viewport while you work on it |
| ignore self | true ignores the actor the context belongs to |
| trace color, hit color | &OwnedStruct::new() for the defaults |
| draw time | None for the default |
It returns whether something was hit and the FHitResult. Read the hit with as_ref(): get_impact_point(), get_impact_normal(), get_location(), get_component(), get_blocking_hit().
#![allow(unused)]
fn main() {
use bindings::engine::ActorComponentExt;
let hit_actor = out_hit.as_ref().get_component().checked()?.get_owner();
}
Sphere trace for objects: a sword swing
The goal: everything a swing touches between two points, pawns and movable props, not the floor.
#![allow(unused)]
fn main() {
use bindings::engine::{ECollisionChannel, EDrawDebugTrace, KismetSystemLibrary};
fn sweep(&self, start: DVec3, end: DVec3, radius: f32) -> Vec<OwnedStruct<FHitResult>> {
let object_types: Vec<_> = [ECollisionChannel::ECC_Pawn, ECollisionChannel::ECC_WorldDynamic]
.into_iter()
.filter_map(object_type_query)
.collect();
let (_, hits) = KismetSystemLibrary::sphere_trace_multi_for_objects(
self.as_ref().upcast_to(),
&FVector::from_dvec3(start),
&FVector::from_dvec3(end),
radius,
&object_types,
false,
&[],
EDrawDebugTrace::None,
true,
&OwnedStruct::new(),
&OwnedStruct::new(),
None,
);
hits
}
}
“For objects” traces filter by object type, the Object Type of a collision preset, instead of a trace channel. object_type_query (from the prelude) turns a collision channel into the object type the function takes.
The single, multi, line, sphere, box and capsule variants (line_trace_multi, sphere_trace_single, box_trace_single_for_objects, capsule_trace_single…) follow the same pattern.
Overlap: an explosion
The goal: every actor within a radius.
#![allow(unused)]
fn main() {
let object_types: Vec<_> = [ECollisionChannel::ECC_Pawn, ECollisionChannel::ECC_PhysicsBody]
.into_iter()
.filter_map(object_type_query)
.collect();
let ignored = [self.as_ref().upcast_to::<Actor>()];
let (_, actors) = KismetSystemLibrary::sphere_overlap_actors(
self.as_ref().upcast_to(),
&FVector::from_dvec3(center),
500.0,
&object_types,
SubclassOf::<Actor>::base().upcast_to(),
&ignored,
);
for actor in actors {
}
}
sphere_overlap_components returns the components instead, useful to push the physics bodies hit.
Overlaps of a component
A component with overlap events lists what overlaps it now:
#![allow(unused)]
fn main() {
let sphere = self.collision_sphere()?.checked()?;
for actor in sphere.get_overlapping_actors(Some(SubclassOf::<Enemy>::base().upcast_to())) {
}
}
To react when something starts overlapping, bind the component’s delegate. See Delegates.
What is under the mouse
#![allow(unused)]
fn main() {
let controller = self.as_ref().checked()?;
let (hit, out_hit) = controller.get_hit_result_under_cursor_by_channel(ETraceTypeQuery::TraceTypeQuery1, true);
if hit {
let point = out_hit.as_ref().get_location().to_dvec3();
}
}
get_hit_result_under_finger_by_channel(ETouchIndex::Touch1, ...) is the same for touch.
Delegates
Delegates are how the engine says “this happened” to whoever is listening: an actor overlapped, a component was hit, a montage ended, a pawn was destroyed. Rust code listens with a closure or with a function of its class, and Rust classes declare their own delegates for Blueprints and other Rust code to listen to.
Use case: reacting to an overlap with a function
The goal: a jump pad launches any character that steps on it.
#![allow(unused)]
fn main() {
use bindings::engine::{
BoxComponent, Character, CharacterExt, ECollisionChannel, ECollisionEnabled,
ECollisionResponse, PrimitiveComponentExt, SceneComponent
};
#[uclass(parent = Actor)]
pub struct JumpPad {
#[component(root)]
root: SceneComponent,
#[component(attach = "root")]
r#box: BoxComponent,
#[uproperty(EditAnywhere, default = 1000.0)]
z_strength: f32,
}
#[uclass_impl]
impl JumpPad {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let r#box = self.r#box()?.checked()?;
r#box.set_collision_enabled(ECollisionEnabled::QueryOnly);
r#box.set_collision_response_to_all_channels(ECollisionResponse::ECR_Ignore);
r#box.set_collision_response_to_channel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
Ok(())
}
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
let me = self.as_ref();
if let Err(e) = me
.checked()
.and_then(|actor| actor.on_actor_begin_overlap().add_ufunction(&me, "BeginOverlap"))
{
log::warn!("[MyGame] jump pad overlap: {e}");
}
}
#[ufunction]
fn begin_overlap(&mut self, _overlapped_actor: UObjectRef<Actor>, other_actor: UObjectRef<Actor>) {
let Ok(character) = other_actor.cast::<Character>().and_then(|c| c.checked()) else {
return;
};
let launch = DVec3::Z * f64::from(self.z_strength());
character.launch_character(&FVector::from_dvec3(launch), false, true);
}
}
}
add_ufunction(&target, "Function") binds a #[ufunction] of target by its Unreal name, as C++’s AddDynamic does. The function takes the delegate’s parameters, in order. Binding the same function again does nothing, so it is safe in receive_begin_play.
For an actor’s own overlaps, overriding receive_actor_begin_overlap is simpler still (see Engine events); delegates are for a component’s overlaps and for another object’s events.
Use case: a component’s overlap
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
let me = self.as_ref();
let bound = self.trigger().and_then(|trigger| {
trigger.checked()?.on_component_begin_overlap().add_ufunction(&me, "OnTriggerOverlap")
});
if let Err(e) = bound {
log::warn!("[MyGame] trigger overlap: {e}");
}
}
#[ufunction]
#[allow(clippy::too_many_arguments)]
fn on_trigger_overlap(
&mut self,
_overlapped_component: UObjectRef<PrimitiveComponent>,
other_actor: UObjectRef<Actor>,
_other_comp: UObjectRef<PrimitiveComponent>,
_other_body_index: i32,
_from_sweep: bool,
_sweep_result: UStructRef<FHitResult>,
) {
}
}
The common component delegates and their parameters:
| Delegate | Parameters |
|---|---|
on_component_begin_overlap | overlapped component, other actor, other component, other body index, from sweep, sweep result |
on_component_end_overlap | overlapped component, other actor, other component, other body index |
on_component_hit | hit component, other actor, other component, normal impulse, hit |
on_actor_begin_overlap, on_actor_end_overlap | overlapped actor, other actor |
on_destroyed | destroyed actor |
Use case: listening with a closure
The goal: the player controller respawns the player when its pawn is destroyed.
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_possess(&mut self, possessed_pawn: UObjectRef<Pawn>) {
let controller: UObjectRef<PlayerController> = self.as_ref();
let bound = possessed_pawn.checked().and_then(|pawn| {
pawn.on_destroyed().add(move |_destroyed_actor| {
if let Ok(mut me) = MyPlayerController::from_obj(controller) {
me.respawn();
}
})
});
match bound {
Ok(binding) => binding.detach(),
Err(e) => log::warn!("[MyGame] cannot watch the pawn: {e}"),
}
}
}
add(closure) binds a Rust closure, which receives the delegate’s parameters. It returns a binding:
- dropping the binding unbinds the closure;
detach()keeps the closure bound for as long as the object that raises the delegate lives.
The closure captures UObjectRefs, which are Copy, and turns them back into Rust classes with from_obj when it runs: by then the object may be gone, and from_obj says so.
Closures and add_ufunction both work on any delegate the engine exposes to Blueprints: animation (on_montage_ended), AI perception (on_target_perception_updated), movement (on_projectile_stop), AI moves (receive_move_completed), EQS (on_query_finished_event). See the Gameplay recipes.
Use case: a delegate of your own
The goal: a health component announces every change, and a UMG widget, a Blueprint or other Rust code updates itself.
#![allow(unused)]
fn main() {
#[uclass(parent = ActorComponent)]
pub struct Health {
#[uproperty(EditAnywhere, default = 100.0)]
max_health: f32,
#[uproperty(VisibleAnywhere)]
current: f32,
}
#[uclass_impl]
impl Health {
#[udelegate]
pub fn on_health_changed(&self, current: f32, max: f32) {}
#[ufunction]
pub fn take_damage(&mut self, amount: f32) {
self.set_current((self.current() - amount).max(0.0));
self.on_health_changed(self.current(), self.max_health());
}
}
}
#[udelegate] declares a multicast delegate, C++’s DECLARE_DYNAMIC_MULTICAST_DELEGATE with a BlueprintAssignable property. The method’s body stays empty; calling it broadcasts to every listener. The macro adds two methods for Rust listeners:
#![allow(unused)]
fn main() {
health.on_health_changed_add(|current, max| {
})?.detach();
health.on_health_changed_add_ufunction(&hud.as_ref(), "RefreshHealth")?;
}
Its parameters are scalars, objects, classes and structs (as &OwnedStruct<T>; a closure receives an OwnedStruct<T>). #[udelegate(name = "...")] gives the Unreal name when PascalCase is not it.
In the editor
In a Blueprint that holds the component, select the component in the Components panel; in the Details panel, the Events section lists On Health Changed with a + button that adds the bound event to the graph. Or, from any reference to the component, drag a wire and search for Bind Event to On Health Changed.
Screenshot: the Events section of the Details panel listing On Health Changed for a Rust component. (images/engine_api/udelegate_events.png)
Delegates that cannot be bound
Only delegates exposed to Blueprints can be bound. Some engine delegates exist only in C++ (UPathFollowingComponent::OnRequestFinished, for one); for those, look for a Blueprint-exposed delegate raised for the same thing (the AI controller’s ReceiveMoveCompleted).
Dynamic calls
The goal: call a function the bindings do not have: one a Blueprint declares, one of a module the project does not bind, one of a plugin’s Rust class.
#![allow(unused)]
fn main() {
use rusteal_runtime::runtime::DynamicCall;
let mut call = DynamicCall::new(&door, "OpenDoor")?;
call.set("Speed", 2.0f32)?;
let result = call.call()?;
let opened: bool = result.get("ReturnValue")?;
}
DynamicCall calls any reflected function by name, with the engine’s reflection, as a Blueprint node does:
| Method | What it does |
|---|---|
DynamicCall::new(&object, "Function") | Finds the function on the object’s class, or Err(FunctionNotFound). |
call.set("Param", value) | Sets a parameter by its Unreal name. |
call.set_struct("Param", &owned_struct) | Sets a struct parameter. |
call.call() | Calls it, and returns the result. |
result.get::<T>("Name") | Reads the return value ("ReturnValue") or an out parameter. |
Values are passed as they are in memory: bool, numbers, f32 for float, f64 for double, an object’s handle (object.raw()), an FName’s handle.
Use case: a function of a Blueprint
The goal: Rust code calls a function a designer wrote in a Blueprint.
#![allow(unused)]
fn main() {
let mut call = DynamicCall::new(&actor, "PlayIntro")?;
call.set("Delay", 0.5f32)?;
call.call()?;
}
The function name and the parameter names are the ones the Blueprint shows. A Blueprint function renamed in the editor breaks the call at runtime, with a FunctionNotFound error, not at compile time.
When to prefer something else
Dynamic calls skip the compiler’s checks: a wrong name or a wrong type is found when the game runs. Prefer:
- the bindings, for engine functions (bind the module in
rusteal.tomlif it is missing; see Binding more of the engine); - a
BlueprintImplementableEvent, when Rust calls into a Blueprint child of a Rust class (see Functions); - an interface, when the object may be one of several classes (see Interfaces).
Errors
The engine fails in ways C++ code tends to crash on: an object destroyed while something still points to it, a cast to the wrong class, a function that is not there. In Rusteal those failures are values: RustealResult<T>, an alias of Result<T, RustealError>.
What an error says
A RustealError says what went wrong, on which Unreal object, why Unreal refused when it says, and where in your Rust code:
invalid cast: expected Pawn (object StaticMeshActor_0 of class StaticMeshActor)
at my-game/src/turret.rs:42:23
while aiming at the target
at my-game/src/turret.rs:30:14
- What: its kind, and the name involved (the property, the function, the class a cast expected).
- On what: the object’s name and its class, when the error came from an object.
- Why, says Unreal: when the engine side knows more than the kind, its reason, after
Unreal::type mismatch: Unreal: Ammo (IntProperty of Turret) is not a BoolProperty,index out of range: Unreal: index 5 past the 3 elements of Targets (ArrayProperty of Turret). - Where: the file and line of the call that failed in your code, not inside Rusteal: the functions that return a
RustealResultpass their caller’s location along. - While doing what: the contexts your code added on the way up (below).
- Caused by: the error underneath, when it came from another library (a file that did not open, a number that did not parse).
It prints three ways:
| Format | Gives | For |
|---|---|---|
{e} | One line: the contexts, then the error, then the object. | Logs, UI, to_string(). |
{e:#} | The same line, then the causes and the location. | A log line with everything. |
{e:?} | The report above, one item per line. | Debugging; it is also what unwrap() prints when it panics. |
e.log(LOG_WARNING) reports it (see In the editor).
In the editor
A reported error goes to two places:
- the Output Log, as the report above, followed by the Blueprint call stack when a Blueprint was running;
- the Rusteal tab of the Message Log (Window > Message Log, then Rusteal in its list), one line per error. Its object is a link that selects it; an object of Play In Editor, once Play has stopped, selects the same actor in the level. Its Rust file and line are a link that opens the file at that line in the IDE set in Editor Preferences > General > Source Code. An error also shows a notification, at most once every few seconds.
Screenshot: the Message Log’s Rusteal tab with an invalid cast error, its object link and its errors.rs:42 link. (images/engine_api/message_log.png)
Panics are reported the same way, with the file and line of the panic!, the unwrap() or the index out of bounds.
Use case: a function that can fail
The goal: a Blueprint calls a Rust function that can fail, and the failure is a Blueprint runtime error on the node that called it, not a silent default.
A #[ufunction] (or an Override) returns a RustealResult<T> when it can fail. Unreal sees a function returning T (nothing for RustealResult<()>):
#![allow(unused)]
fn main() {
#[ufunction]
fn reload(&mut self) -> RustealResult<i32> {
let magazine = self.magazine().checked()?;
Ok(magazine.rounds())
}
#[ufunction(Override)]
fn receive_begin_play(&mut self) -> RustealResult<()> {
self.setup()
}
}
An Err is reported with the function’s name (Turret::Reload failed: ...) and the function returns the default value (0, false, a null reference). Called from a Blueprint, it is also a Blueprint runtime error: in Play In Editor the Message Log opens on it with a link to the node, as for an Accessed None, and with Blueprint Exceptions breakpoints on, the debugger stops there.
Screenshot: a Blueprint runtime error raised by a Rust function, with the link to the calling node in the Message Log. (images/engine_api/blueprint_error.png)
The same panic, unwrap or ? inside a function that does not return a RustealResult is still caught: a panic is reported as above, and the function returns the default value.
Use case: Result inside, log at the edge
The goal: a turret’s setup can fail in several ways, and the log says which, where and on what.
Events and functions the engine calls cannot return an error to anyone. Put the work in a method that returns RustealResult, use ? in it, and log the error in the event:
#![allow(unused)]
fn main() {
use rusteal_runtime::runtime::{ErrorContext, LOG_WARNING, RustealResult};
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let Err(e) = self.setup() {
e.log(LOG_WARNING);
}
}
impl Turret {
fn setup(&mut self) -> RustealResult<()> {
let mesh = self.mesh()?.checked()?;
let target = self.target().cast::<Pawn>().context("aiming at the target")?;
let range: f32 = self.range_text().parse().context("reading the range")?;
Ok(())
}
}
}
.context("...") adds what the code was doing to the error, with its own line; contexts read outermost first in {e}. It works on any Result whose error is a Rust error (a RustealError, an std::io::Error, a serde_json::Error…), and on Option, where None becomes an error with that message:
#![allow(unused)]
fn main() {
let spawn = self.find_spawn_point().context("no spawn point in the level")?;
}
Errors from other libraries
? turns the standard library’s errors (std::io::Error, ParseIntError, ParseFloatError, Utf8Error…) into a RustealError by itself, keeping them as the cause and recording the line of the ?. Another crate’s error goes through .context("..."), or .map_err(RustealError::other) with no context.
RustealError implements std::error::Error, so it goes the other way too: into anyhow::Error, a Box<dyn Error>, or a thiserror enum of your own.
Telling errors apart
kind() gives an ErrorKind to match on:
#![allow(unused)]
fn main() {
match other_actor.cast::<Character>() {
Ok(character) => self.hit(character),
Err(e) if e.is(ErrorKind::InvalidCast) => {}
Err(e) => return Err(e),
}
}
ErrorKind | When |
|---|---|
ObjectDestroyed | checked() on an object that is gone or was never set. |
InvalidCast | cast or from_obj to a class the object is not. |
NullArgument | A null class or object where one is required (an unset SubclassOf given to spawn_actor_of_class). |
FunctionNotFound | A function bound or called by a name the class does not have. |
PropertyNotFound | A property or a parameter reached by a name the class or the function does not have. |
TypeMismatch | A value of the wrong type for a property or parameter. |
IndexOutOfRange | An array index past the end. |
InvalidOperation | An engine call that refused: a spawn that failed, an asset that could not be loaded. |
Internal | Something Rusteal itself did not expect. |
Other | An error of your own, or from another library. |
Where a failure is a normal case (no target, the player is not a character), let else or if let say it without an error:
#![allow(unused)]
fn main() {
let Ok(character) = other_actor.cast::<Character>() else {
return;
};
}
Your own errors
#![allow(unused)]
fn main() {
use rusteal_runtime::runtime::{ErrorKind, RustealResult, bail, ensure};
fn spend(&mut self, cost: i32) -> RustealResult<()> {
ensure!(cost >= 0, "a cost of {cost}");
if cost > self.gold() {
bail!(ErrorKind::InvalidOperation, "{cost} gold, {} in the purse", self.gold());
}
self.set_gold(self.gold() - cost);
Ok(())
}
}
bail! returns an error with a message, of the kind given first or Other without one; ensure! does when its condition is false. RustealError::msg("..."), RustealError::with_message(kind, "...") and the constructors named after each kind (RustealError::null_argument(), RustealError::invalid_operation("...")) make one to return yourself, and .on(&actor) names the object it concerns.
Panics
A panic in your code does not crash the editor: it is caught where the engine called into Rust, reported as an error with its file and line (see In the editor), and the call returns. The object may be left half updated, so a panic is a bug to fix, not a way to handle errors. unwrap() on a RustealResult is a panic waiting for an object to be destroyed at the wrong moment: prefer ?, let else or unwrap_or_default().
Gameplay recipes
Complete use cases, each one taken from a Rusteal template, where it runs as part of a playable game. To see one working, create the template it comes from and play it:
| Recipe | Template it comes from |
|---|---|
| Character movement and input | every template |
| Cameras | third-person, first-person, third-person --variant side-scrolling |
| Animation | third-person --variant combat, third-person --variant platforming, vr, intro and the mannequin feature |
| Health, damage and death | first-person --variant shooter, third-person --variant combat |
| Projectiles and weapons | first-person --variant shooter, top-down --variant twin-stick |
| Pickups | first-person --variant shooter, third-person --variant side-scrolling |
| Platforming | third-person --variant platforming, third-person --variant side-scrolling |
| Physics | third-person --variant combat, intro and the interactables feature |
| UI | first-person --variant horror, top-down --variant strategy, third-person --variant combat, vr, ar |
| AI | first-person --variant shooter, third-person --variant combat, top-down --variant strategy |
| Point and click | top-down |
| Effects and sound | top-down, intro and the interactables feature |
| Vehicles | vehicle, vehicle --variant time-trial |
| Virtual reality | vr |
| Augmented reality | ar |
| Game flow | every template; intro and the interactables feature for triggers and doors |
| Saving the game | intro and the checkpoints feature |
| An inventory | the inventory plugin template |
rusteal new CombatLab --template third-person --variant combat
The code in these recipes is trimmed to what the use case needs. The template’s source, in Rust/<crate>/src/, has the rest.
Character movement and input
The goal: a character that walks, looks around and jumps with the keyboard and mouse, a gamepad or touch, with Enhanced Input.
Input in Rusteal works as in the engine’s C++ templates:
- the player controller adds the Input Mapping Contexts when the game starts;
- the pawn binds each Input Action to one of its functions once its input component exists;
- a Blueprint child of each assigns the assets.
The player controller
#![allow(unused)]
fn main() {
use bindings::engine::{ControllerExt, PlayerController};
use bindings::enhanced_input::{EnhancedInputLocalPlayerSubsystemExt, InputMappingContext};
use bindings::prelude::*;
use rusteal_runtime::runtime::{OwnedStruct, RustealResult, UObjectRef, UeArray };
use rusteal_runtime::{uclass, uclass_impl};
#[uclass(parent = PlayerController)]
pub struct MyPlayerController {
#[uproperty(EditAnywhere)]
default_mapping_contexts: UeArray<UObjectRef<InputMappingContext>>,
}
#[uclass_impl]
impl MyPlayerController {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let Err(e) = self.add_mapping_contexts() {
log::warn!("[MyGame] input mapping contexts failed: {e}");
}
}
}
impl MyPlayerController {
fn add_mapping_contexts(&self) -> RustealResult<()> {
let me: UObjectRef<PlayerController> = self.as_ref();
if !me.checked()?.is_local_controller() {
return Ok(());
}
let subsystem = enhanced_input_subsystem(me)?.checked()?;
for context in self.default_mapping_contexts().to_vec()? {
subsystem.add_mapping_context(context, 0, &OwnedStruct::new());
}
Ok(())
}
}
}
enhanced_input_subsystem(controller) is the controller’s Enhanced Input Local Player Subsystem, where mapping contexts are added. A player controller binds its own actions in receive_begin_play too, which is where C++ does it (SetupInputComponent).
The character
#![allow(unused)]
fn main() {
use bindings::engine::{
Character, CharacterExt, CharacterMovementComponentExt, ControllerExt, PawnExt
};
use bindings::enhanced_input::{ETriggerEvent, FInputActionValue, InputAction};
use bindings::prelude::*;
use rusteal_runtime::runtime::{RustealResult, UObjectRef, UStructRef };
use rusteal_runtime::{uclass, uclass_impl};
#[uclass(parent = Character)]
pub struct MyCharacter {
#[uproperty(EditAnywhere, category = "Input")]
jump_action: UObjectRef<InputAction>,
#[uproperty(EditAnywhere, category = "Input")]
move_action: UObjectRef<InputAction>,
#[uproperty(EditAnywhere, category = "Input")]
look_action: UObjectRef<InputAction>,
}
#[uclass_impl]
impl MyCharacter {
#[ufunction(Override)]
fn receive_restarted(&mut self) {
if let Err(e) = self.bind_input() {
log::warn!("[MyGame] input binding failed: {e}");
}
}
#[ufunction]
fn r#move(&mut self, value: UStructRef<FInputActionValue>) {
let axis = value.axis2d();
let Ok(me) = self.as_ref().checked() else {
return;
};
let Ok(controller) = me.get_controller().checked() else {
return;
};
let yaw = controller.get_control_rotation().to_rotator().yaw.to_radians();
let forward = DVec3::new(yaw.cos(), yaw.sin(), 0.0);
let right = DVec3::new(-yaw.sin(), yaw.cos(), 0.0);
me.add_movement_input(&FVector::from_dvec3(forward), Some(axis.y as f32), Some(false));
me.add_movement_input(&FVector::from_dvec3(right), Some(axis.x as f32), Some(false));
}
#[ufunction]
fn look(&mut self, value: UStructRef<FInputActionValue>) {
let axis = value.axis2d();
if let Ok(me) = self.as_ref().checked() {
me.add_controller_yaw_input(axis.x as f32);
me.add_controller_pitch_input(axis.y as f32);
}
}
}
impl MyCharacter {
fn bind_input(&self) -> RustealResult<()> {
let me = self.as_ref();
let pawn = me.checked()?;
if !pawn.is_player_controlled() || !pawn.is_locally_controlled() {
return Ok(());
}
bind_action(&me, self.jump_action(), ETriggerEvent::Started, "Jump")?;
bind_action(&me, self.jump_action(), ETriggerEvent::Completed, "StopJumping")?;
bind_action(&me, self.move_action(), ETriggerEvent::Triggered, "Move")?;
bind_action(&me, self.look_action(), ETriggerEvent::Triggered, "Look")?;
Ok(())
}
}
}
receive_restartedis where a pawn binds input: it runs when a controller possesses the pawn, once the input component exists. It replaces C++’sSetupPlayerInputComponent.bind_action(&pawn, action, trigger, "Function")binds an action’s trigger event to a function by its Unreal name. Engine functions work as well:"Jump"and"StopJumping"areACharacter’s.- The value of an action reads as
get_bool(),axis1d(),axis2d()oraxis3d(), by its Value Type.
What a bound function can take
The function takes nothing, or a leading part of these, in order:
| Parameter | Type | What it is |
|---|---|---|
| value | UStructRef<FInputActionValue> | the action’s value |
| elapsed seconds | f32 | how long the action has been evaluated |
| triggered seconds | f32 | how long it has been triggered |
| source action | UObjectRef<InputAction> | the action itself |
#![allow(unused)]
fn main() {
#[ufunction]
fn charge(&mut self, _value: UStructRef<FInputActionValue>, elapsed_seconds: f32) {
let charged = elapsed_seconds >= 1.5;
}
}
In the editor
The engine templates already have the input actions (IA_Move, IA_Look, IA_Jump) and a mapping context (IMC_Default). For a project without them:
- In the Content Browser, right-click and choose Input > Input Action. Create
IA_Moveand, in its Details, set Value Type to Axis2D (Vector2D). Do the same forIA_Look, and createIA_Jumpwith Digital (bool). - Right-click and choose Input > Input Mapping Context. Name it
IMC_Default, open it, and add a mapping for each action with the + next to Mappings:IA_Jumpto Space Bar,IA_Lookto Mouse XY 2D-Axis,IA_Moveto W, S, A and D with the Swizzle Input Axis Values and Negate modifiers as the engine templates do. - Create a Blueprint child of
MyPlayerController(see Blueprint children), and in its Class Defaults addIMC_Defaultto Default Mapping Contexts. - Create a Blueprint child of
MyCharacter, and in its Class Defaults, under Input, set Jump Action, Move Action and Look Action. - Set both Blueprints in the game mode (see Game flow).
Screenshot: the Class Defaults of the character Blueprint with the three input actions assigned. (images/recipes/input_actions_assigned.png)
Screenshot: IMC_Default with the W, A, S, D mappings and their modifiers. (images/recipes/imc_default.png)
If nothing moves, the Output Log usually says why: no mapping contexts added, or a function name that does not exist.
Touch controls
The templates add a virtual joystick widget on touch screens:
#![allow(unused)]
fn main() {
use rusteal_runtime::runtime::input::should_display_touch_interface;
if should_display_touch_interface() {
let widget = create_widget_of_class(&me, self.mobile_controls_widget_class())?;
widget.checked()?.add_to_player_screen(Some(0));
}
}
should_display_touch_interface() is the engine’s own check for a touch screen. The widget class is a SubclassOf<UserWidget> property, which the template’s player controller Blueprint sets to the engine template’s mobile controls widget.
Movement settings
The movement itself is the character movement component’s, tuned in Class defaults:
#![allow(unused)]
fn main() {
let movement = me.get_character_movement().checked()?;
movement.set_orient_rotation_to_movement(true);
movement.set_max_walk_speed(500.0);
movement.set_jump_z_velocity(500.0);
movement.set_air_control(0.35);
}
Cameras
Use case: a third person camera on a spring arm
The goal: a camera behind the character that orbits with the mouse and pulls in when a wall is in the way.
#![allow(unused)]
fn main() {
use bindings::engine::{CameraComponent, CameraComponentExt, SpringArmComponent, SpringArmComponentExt};
#[uclass(parent = Character)]
pub struct Hero {
#[component(attach = "root_component")]
camera_boom: SpringArmComponent,
#[component(attach = "camera_boom", socket = "SpringEndpoint")]
follow_camera: CameraComponent,
}
#[uclass_impl]
impl Hero {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
me.set_use_controller_rotation_pitch(false);
me.set_use_controller_rotation_yaw(false);
me.set_use_controller_rotation_roll(false);
let boom = self.camera_boom()?.checked()?;
boom.set_target_arm_length(400.0);
boom.set_use_pawn_control_rotation(true);
self.follow_camera()?.checked()?.set_use_pawn_control_rotation(false);
Ok(())
}
}
}
The spring arm follows the controller’s rotation, which the look input changes; the character does not turn with it, and the movement component turns it toward where it walks (set_orient_rotation_to_movement(true)).
A smoother camera adds lag: boom.set_enable_camera_lag(true) and boom.set_camera_lag_speed(10.0).
Use case: a first person camera
The goal: a camera at the head, with arms only the player sees and a body only others see.
#![allow(unused)]
fn main() {
use bindings::engine::{
CameraComponent, CameraComponentExt, EFirstPersonPrimitiveType, PrimitiveComponentExt,
SceneComponentExt, SkeletalMeshComponent
};
#[uclass(parent = Character)]
pub struct FirstPersonHero {
#[component(attach = "mesh", name = "First Person Mesh")]
first_person_mesh: SkeletalMeshComponent,
#[component(attach = "first_person_mesh", socket = "head", name = "First Person Camera")]
first_person_camera: CameraComponent,
}
#[uclass_impl]
impl FirstPersonHero {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let arms = self.first_person_mesh()?.checked()?;
arms.set_only_owner_see(true);
arms.set_first_person_primitive_type(EFirstPersonPrimitiveType::FirstPerson);
arms.set_collision_profile_name(FName::new("NoCollision").handle(), Some(true));
let camera = self.first_person_camera()?.checked()?;
camera.set_use_pawn_control_rotation(true);
camera.set_enable_first_person_field_of_view(true);
camera.set_first_person_field_of_view(70.0);
let body = self.as_ref().checked()?.get_mesh().checked()?;
body.set_owner_no_see(true);
body.set_first_person_primitive_type(EFirstPersonPrimitiveType::WorldSpaceRepresentation);
Ok(())
}
}
}
The Blueprint child assigns the arms’ and the body’s skeletal meshes and Animation Blueprints.
Use case: limiting how far the player looks up and down
The goal: a camera manager that clamps the pitch.
#![allow(unused)]
fn main() {
use bindings::engine::{PlayerCameraManager, PlayerCameraManagerExt};
#[uclass(parent = PlayerCameraManager)]
pub struct MyCameraManager {}
#[uclass_impl]
impl MyCameraManager {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
me.set_view_pitch_min(-70.0);
me.set_view_pitch_max(80.0);
Ok(())
}
}
}
The player controller uses it, set in its own class defaults:
#![allow(unused)]
fn main() {
self.as_ref().checked()?.set_player_camera_manager_class(
SubclassOf::<MyCameraManager>::base().upcast_to(),
);
}
Use case: a side scroller camera computed every frame
The goal: a camera that follows the character along X, stays within the level’s bounds, and moves up and down smoothly.
A camera manager decides the camera’s location, rotation and field of view every frame in BlueprintUpdateCamera:
#![allow(unused)]
fn main() {
#[uclass(parent = PlayerCameraManager)]
pub struct SideCameraManager {
#[uproperty(EditAnywhere, default = 400.0)]
current_zoom: f32,
#[uproperty(EditAnywhere, default = -1000.0)]
camera_x_min_bounds: f32,
#[uproperty(EditAnywhere, default = 1000.0)]
camera_x_max_bounds: f32,
}
#[uclass_impl]
impl SideCameraManager {
#[ufunction(Override)]
fn blueprint_update_camera(
&mut self,
camera_target: UObjectRef<Actor>,
new_camera_location: UStructRef<FVector>,
new_camera_rotation: UStructRef<FRotator>,
new_camera_fov: OutRef<f32>,
) -> bool {
let Ok(target) = camera_target.checked() else {
return false;
};
let location = target.k2_get_actor_location().to_dvec3();
let x = location.x.clamp(f64::from(self.camera_x_min_bounds()), f64::from(self.camera_x_max_bounds()));
new_camera_location.set_x(x);
new_camera_location.set_y(location.y + f64::from(self.current_zoom()));
new_camera_location.set_z(location.z);
new_camera_rotation.set_pitch(0.0);
new_camera_rotation.set_yaw(-90.0);
new_camera_rotation.set_roll(0.0);
new_camera_fov.set(65.0);
true
}
}
}
The event writes the camera into the structs it receives (UStructRef, written in place) and the field of view into an OutRef, and returns true to use them. The side-scrolling template adds smoothing with the engine’s interpolation formulas, written as plain Rust in its model.rs and tested there.
Animation
Use case: playing a montage and knowing when it ends
The goal: an attack plays its animation montage, and the character can attack again once it ends.
#![allow(unused)]
fn main() {
use bindings::engine::{AnimInstance, AnimInstanceExt, AnimMontage, Character, CharacterExt, SkeletalMeshComponentExt};
#[uclass(parent = Character)]
pub struct Fighter {
#[uproperty(EditAnywhere, category = "Combat")]
attack_montage: UObjectRef<AnimMontage>,
is_attacking: bool,
montage_end_bound: bool,
}
impl Fighter {
fn attack(&mut self) -> RustealResult<()> {
if self.is_attacking() {
return Ok(());
}
let anim_instance = self.as_ref().checked()?.get_mesh().checked()?.get_anim_instance();
self.bind_montage_end(anim_instance)?;
anim_instance.checked()?.montage_play(self.attack_montage(), Some(1.0), None, Some(0.0), Some(true));
self.set_is_attacking(true);
Ok(())
}
fn bind_montage_end(&mut self, anim_instance: UObjectRef<AnimInstance>) -> RustealResult<()> {
if self.montage_end_bound() {
return Ok(());
}
let character: UObjectRef<Character> = self.as_ref();
anim_instance
.checked()?
.on_montage_ended()
.add(move |montage, _interrupted| {
if let Ok(mut me) = Fighter::from_obj(character)
&& montage == me.attack_montage()
{
me.set_is_attacking(false);
}
})?
.detach();
self.set_montage_end_bound(true);
Ok(())
}
}
}
montage_play(montage, play_rate, return_value_type, time_to_start, stop_all_montages)plays it;Nonekeeps a default.on_montage_endedis the anim instance’s delegate for every montage that ends, interrupted or not: the closure checks which one ended.- A Rust-only flag keeps the closure from being bound twice.
Other montage controls on the anim instance:
#![allow(unused)]
fn main() {
anim_instance.montage_jump_to_section(FName::new("Attack2").handle(), Some(montage));
anim_instance.montage_stop(0.1, Some(montage));
}
Use case: code at a frame of an animation
The goal: the sword’s damage trace runs at the exact frame of the swing, set by an animator in the montage.
#![allow(unused)]
fn main() {
use bindings::engine::{ActorComponentExt, AnimNotify, AnimSequenceBase, FAnimNotifyEventReference, SkeletalMeshComponent};
#[uclass(parent = AnimNotify)]
pub struct AnimNotifyAttackTrace {
#[uproperty(EditAnywhere)]
attack_bone_name: FName,
}
#[uclass_impl]
impl AnimNotifyAttackTrace {
#[ufunction(Override, name = "Received_Notify")]
fn notify(
&self,
mesh_comp: UObjectRef<SkeletalMeshComponent>,
_animation: UObjectRef<AnimSequenceBase>,
_event_reference: UStructRef<FAnimNotifyEventReference>,
) -> bool {
let Ok(owner) = mesh_comp.checked().map(|mesh| mesh.get_owner()) else {
return false;
};
if let Ok(mut fighter) = Fighter::from_obj(owner) {
fighter.do_attack_trace(&self.attack_bone_name().to_string_lossy());
}
true
}
}
}
A class whose parent is AnimNotify is a notify animators place on a timeline. It receives the skeletal mesh component that plays the animation; its owner is the character.
In the editor
- Open the montage (
AM_Attack). - In the Notifies track, right-click at the frame of the hit and choose Add Notify > Anim Notify Attack Trace.
- Select the notify, and in the Details panel set Attack Bone Name (
hand_r). - Save.
Screenshot: a montage’s Notifies track with the Rust notify placed at the hit frame, and its Attack Bone Name in the Details panel. (images/recipes/anim_notify.png)
Use case: gameplay state for the Animation Blueprint
An Animation Blueprint reads the character’s state through BlueprintPure functions:
#![allow(unused)]
fn main() {
#[ufunction(BlueprintPure)]
fn has_double_jumped(&self) -> bool {
self.state().has_double_jumped
}
}
In the Animation Blueprint’s Event Graph, cast Try Get Pawn Owner to the character’s class, call Has Double Jumped on it, and store the result in a variable the AnimGraph uses.
Use case: the Animation Blueprint’s logic in Rust
The goal: the logic of an Animation Blueprint’s Event Graph (reading the owner, easing values toward their targets) in Rust, and the AnimGraph left to the Animation Blueprint, which reads the values the Rust class keeps.
The Rust class is the Animation Blueprint’s parent, an AnimInstance:
#![allow(unused)]
fn main() {
use bindings::engine::{AnimInstance, AnimInstanceExt, SkeletalMeshComponent};
#[uclass(parent = SkeletalMeshComponent)]
pub struct MannequinsXR {
#[uproperty(EditAnywhere, BlueprintReadWrite, category = "Hand", default = false)]
b_mirror: bool,
}
#[uclass(parent = AnimInstance)]
pub struct HandAnimInstance {
#[uproperty(BlueprintReadWrite, category = "Pose", default = 0.0)]
pose_alpha_point: f32,
#[uproperty(BlueprintReadOnly, category = "Pose", default = 0.0)]
current_pose_alpha_point: f64,
#[uproperty(BlueprintReadOnly, category = "Pose", default = false)]
b_mirror: bool,
}
#[uclass_impl]
impl HandAnimInstance {
#[ufunction(Override)]
fn blueprint_initialize_animation(&mut self) {
let mirror = self
.as_ref()
.checked()
.ok()
.and_then(|anim| MannequinsXR::from_obj(anim.get_owning_component()).ok())
.is_some_and(|hand| hand.b_mirror());
self.set_b_mirror(mirror);
}
#[ufunction(Override)]
fn blueprint_update_animation(&mut self, delta_time_x: f32) {
self.set_current_pose_alpha_point(model::f_interp_to(
self.current_pose_alpha_point(),
f64::from(self.pose_alpha_point()),
f64::from(delta_time_x),
13.0,
));
}
}
}
blueprint_initialize_animation runs once, when the mesh creates the instance; blueprint_update_animation every animation update, on the game thread, with the frame’s time. What the AnimGraph reads are the Rust properties, BlueprintReadOnly or BlueprintReadWrite. The pawn sets the targets on the instance its hand mesh has:
#![allow(unused)]
fn main() {
let anim = hand.checked()?.get_anim_instance();
if let Ok(mut hand_anim) = HandAnimInstance::from_obj(anim) {
hand_anim.set_pose_alpha_point(1.0);
}
}
In the editor
- Open the Animation Blueprint (
ABP_MannequinsXR), and in the toolbar click Class Settings. - In the Details panel, set Parent Class to HandAnimInstance.
- In the My Blueprint panel, Variables lists the Rust properties under Pose; drag Current Pose Alpha Point into the AnimGraph and connect it where the old variable was.
- Delete the old variables and the Event Graph nodes the Rust class replaces, then click Compile.
Screenshot: the AnimGraph of ABP_MannequinsXR, with Current Pose Alpha Point coming from the Rust parent. (images/recipes/anim_rust_parent.png)
The variable types follow the Blueprint’s: a Blueprint Float that is double precision is f64, one stored in single precision is f32. When they match, the AnimGraph’s nodes take the Rust property in place of a variable of the same name.
Template: vr.
Use case: a mannequin that copies the player
The goal: a mannequin in the level that walks, runs, jumps and lands when the player does, as the Intro To Unreal template’s copycat.
The mannequin feature’s CopycatAnimInstance reads player 0’s character every animation update and keeps what an AnimGraph needs:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn blueprint_update_animation(&mut self, _delta_time_x: f32) {
if !self.character().is_valid() || !self.movement_component().is_valid() {
self.find_character();
}
let (Ok(anim), Ok(character), Ok(movement)) = (
self.as_ref().checked(),
self.character().checked(),
self.movement_component().checked(),
) else {
return;
};
let velocity = movement.get_velocity();
let ground_speed = ground_speed(velocity.to_dvec3());
let direction = f64::from(anim.calculate_direction(&velocity, &character.k2_get_actor_rotation()));
self.set_ground_speed(ground_speed);
self.set_direction(clamp_direction(direction, movement.get_orient_rotation_to_movement()));
self.set_should_move(should_move(ground_speed, movement.get_current_acceleration().to_dvec3()));
self.set_is_falling(movement.is_falling());
}
}
ground_speed, should_move and clamp_direction are plain functions, tested with cargo test: the speed ignores the vertical axis, the mannequin moves only above 0.01 cm/s and while the character accelerates, and the direction stays within 45 degrees when the character turns toward its movement. ABP_Copycat is a child of CopycatAnimInstance whose AnimGraph blends idle, walk and run by Ground Speed and switches to a jump on Is Falling. Setting the instance’s Source Character copies another character than the player.
Any skeletal mesh of the SK_Mannequin skeleton takes it: Manny, Quinn, or a mannequin of your own on that skeleton.
Use case: a hand that reaches for a target
The goal: a mannequin plays its idle and, when a target comes within arm’s length, its right hand goes to it.
The animation plays on a hidden skeletal mesh; every tick, a poseable mesh copies that pose and bends the arm with a two-bone IK solved in Rust:
#![allow(unused)]
fn main() {
let pose = self.pose()?.checked()?;
pose.copy_pose_from_skeletal_component(self.body()?);
let chain = Chain {
root: location("upperarm_r"),
joint: location("lowerarm_r"),
end: location("hand_r"),
};
let solved = ik::reach(chain, target, down);
for (bone, rotation, location) in [
("upperarm_r", solved.root_delta * rotation("upperarm_r"), chain.root),
("lowerarm_r", solved.joint_delta * solved.root_delta * rotation("lowerarm_r"), solved.joint),
("hand_r", rotation("hand_r"), solved.end),
] {
pose.set_bone_transform_by_name(
FName::new(bone).handle(),
&FTransform::from_transform(Transform::new(rotation, location, DVec3::ONE)),
EBoneSpaces::WorldSpace,
);
}
}
ik::reach keeps both bone lengths, places the elbow in the plane of its current bend (nudged by a pole, here down), and returns the rotations that carry each bone onto the solution. Each bone gets its whole world transform: set_bone_rotation_by_name would read the bone’s location from before its parent moved, and pull the elbow off the arm.
The hidden mesh keeps animating with Visibility Based Anim Tick Option set to Always Tick Pose and Refresh Bones, and plays an animation asset without an Animation Blueprint:
#![allow(unused)]
fn main() {
body.play_animation(self.idle_animation().upcast_to(), true);
}
In the editor: place a ReachingMannequin and a ReachTarget from the Place Actors panel, select the mannequin and set its Target to the target, then move the target around it while playing.
Screenshot: the reaching mannequin with its right hand on the target sphere. (images/recipes/reach_target.png)
Health, damage and death
Use case: health with the engine’s damage system
The goal: a character that loses health when anything damages it, through the engine’s ApplyDamage, which Blueprints and other systems also use.
The dealer applies damage:
#![allow(unused)]
fn main() {
use bindings::engine::{DamageType, GameplayStatics, PawnExt};
let instigator_controller = me.get_instigator().checked().map(|pawn| pawn.get_controller()).unwrap_or_default();
GameplayStatics::apply_damage(
hit_actor,
25.0,
instigator_controller,
me.as_ref().upcast_to(),
SubclassOf::<DamageType>::base(),
);
}
The damaged character overrides ReceiveAnyDamage, the event the engine raises for it:
#![allow(unused)]
fn main() {
use bindings::engine::{Controller, DamageType};
#[uclass(parent = Character)]
pub struct Soldier {
#[uproperty(EditAnywhere, name = "MaxHP", category = "Health", default = 100.0)]
max_hp: f32,
#[uproperty(name = "CurrentHP")]
current_hp: f32,
}
#[uclass_impl]
impl Soldier {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
self.set_current_hp(self.max_hp());
}
#[ufunction(Override)]
fn receive_any_damage(
&mut self,
damage: f32,
_damage_type: UObjectRef<DamageType>,
_instigated_by: UObjectRef<Controller>,
_damage_causer: UObjectRef<Actor>,
) {
if self.current_hp() <= 0.0 {
return;
}
self.set_current_hp(self.current_hp() - damage);
if self.current_hp() <= 0.0 {
self.die();
}
}
#[ufunction(BlueprintImplementableEvent, name = "BP_OnDeath")]
fn bp_on_death(&self) {}
}
}
For damage in an area, GameplayStatics::apply_radial_damage damages every actor in a radius that is not blocked by a wall. The shooter template does it by hand instead, with an overlap and apply_damage per actor, to push physics bodies too (see Projectiles).
Use case: dying as a ragdoll
The goal: on death the character stops, falls as a ragdoll, and is removed a few seconds later.
#![allow(unused)]
fn main() {
use bindings::engine::{
CharacterExt, CharacterMovementComponentExt, ECollisionEnabled, KismetSystemLibrary,
MovementComponentExt, PrimitiveComponentExt
};
impl Soldier {
fn die(&mut self) {
let Ok(me) = self.as_ref().checked() else {
return;
};
let _ = me.tags().push(&FName::new("Dead").handle());
if let Ok(movement) = me.get_character_movement().checked() {
movement.stop_movement_immediately();
movement.disable_movement();
}
if let Ok(capsule) = me.get_capsule_component().checked() {
capsule.set_collision_enabled(ECollisionEnabled::NoCollision);
}
if let Ok(mesh) = me.get_mesh().checked() {
mesh.set_collision_profile_name(FName::new("Ragdoll").handle(), Some(true));
mesh.set_simulate_physics(true);
}
self.bp_on_death();
KismetSystemLibrary::k2_set_timer(me.as_ref().upcast_to(), "RemoveBody", 5.0, false, None, None, None);
}
}
}
RemoveBody is a #[ufunction] that calls k2_destroy_actor. The Blueprint child implements BP On Death for the sound and the effects.
Use case: respawning the player
The goal: when the player’s character is destroyed, a new one spawns at a player start and the controller possesses it.
#![allow(unused)]
fn main() {
use bindings::engine::{ControllerExt, Pawn, PlayerController, PlayerStart};
#[uclass(parent = PlayerController)]
pub struct SoldierController {
#[uproperty(EditAnywhere)]
character_class: SubclassOf<Soldier>,
}
#[uclass_impl]
impl SoldierController {
#[ufunction(Override)]
fn receive_possess(&mut self, possessed_pawn: UObjectRef<Pawn>) {
let controller: UObjectRef<PlayerController> = self.as_ref();
let bound = possessed_pawn.checked().and_then(|pawn| {
pawn.on_destroyed().add(move |_destroyed| {
if let Ok(mut me) = SoldierController::from_obj(controller)
&& let Err(e) = me.respawn()
{
log::warn!("[MyGame] respawn failed: {e}");
}
})
});
match bound {
Ok(binding) => binding.detach(),
Err(e) => log::warn!("[MyGame] cannot watch the pawn: {e}"),
}
}
}
impl SoldierController {
fn respawn(&mut self) -> RustealResult<()> {
let me = self.as_ref();
let starts = GameplayStatics::get_all_actors_of_class(
me.upcast_to(),
SubclassOf::<PlayerStart>::base().upcast_to(),
);
let Some(start) = starts.first() else {
return Ok(());
};
let transform = start.checked()?.get_transform();
let character = me.get_world()?.spawn_actor_of_class(self.character_class(), &transform)?;
me.checked()?.possess(character.upcast_to());
Ok(())
}
}
}
The Blueprint child of the controller sets Character Class to the character’s Blueprint.
Use case: damage between your own classes
When only Rust classes deal and take the damage, and they need more than a number (a hit location, a knockback impulse), a Rust trait is simpler than the engine’s damage system. See Interfaces, “Rust traits between Rust classes”, and the combat variant of third-person, whose interfaces.rs does exactly this.
Projectiles and weapons
Use case: a projectile
The goal: a bullet that flies, bounces, damages what it hits, and is removed after a while.
#![allow(unused)]
fn main() {
use bindings::engine::{
Character, DamageType, ECollisionEnabled, ECollisionResponse, FHitResult, FHitResultExt,
GameplayStatics, PawnExt, PrimitiveComponent, PrimitiveComponentExt,
ProjectileMovementComponent, ProjectileMovementComponentExt, SphereComponent,
SphereComponentExt
};
#[uclass(parent = Actor)]
pub struct Bullet {
#[component(root, name = "Collision Component")]
collision: SphereComponent,
#[component(name = "Projectile Movement")]
movement: ProjectileMovementComponent,
#[uproperty(EditAnywhere, category = "Hit", default = 25.0)]
hit_damage: f32,
#[uproperty(EditAnywhere, category = "Hit", default = 100.0)]
physics_force: f32,
hit: bool,
}
#[uclass_impl]
impl Bullet {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let collision = self.collision()?.checked()?;
collision.set_sphere_radius(16.0, Some(true));
collision.set_collision_enabled(ECollisionEnabled::QueryAndPhysics);
collision.set_collision_response_to_all_channels(ECollisionResponse::ECR_Block);
let movement = self.movement()?.checked()?;
movement.set_initial_speed(3000.0);
movement.set_max_speed(3000.0);
movement.set_should_bounce(true);
self.as_ref().checked()?.set_initial_life_span(5.0);
Ok(())
}
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let (Ok(collision), Ok(me)) = (self.collision().and_then(|c| c.checked()), self.as_ref().checked()) {
collision.ignore_actor_when_moving(me.get_instigator().upcast_to(), true);
}
}
#[ufunction(Override)]
#[allow(clippy::too_many_arguments)]
fn receive_hit(
&mut self,
_my_comp: UObjectRef<PrimitiveComponent>,
other: UObjectRef<Actor>,
other_comp: UObjectRef<PrimitiveComponent>,
_self_moved: bool,
_hit_location: UStructRef<FVector>,
_hit_normal: UStructRef<FVector>,
_normal_impulse: UStructRef<FVector>,
hit: UStructRef<FHitResult>,
) {
if self.hit() {
return;
}
self.set_hit(true);
let Ok(me) = self.as_ref().checked() else {
return;
};
if other.cast::<Character>().is_ok() {
let instigator = me.get_instigator().checked().map(|pawn| pawn.get_controller()).unwrap_or_default();
GameplayStatics::apply_damage(other, self.hit_damage(), instigator, me.as_ref().upcast_to(), SubclassOf::<DamageType>::base());
}
if let Ok(component) = other_comp.checked()
&& component.is_simulating_physics(None)
{
let direction = -hit.get_impact_normal().to_dvec3();
component.add_impulse_at_location(
&FVector::from_dvec3(direction * f64::from(self.physics_force())),
&FVector::from_dvec3(hit.get_impact_point().to_dvec3()),
None,
);
}
self.bp_on_hit(&hit.to_owned());
me.k2_destroy_actor();
}
#[ufunction(BlueprintImplementableEvent, name = "BP_OnProjectileHit")]
fn bp_on_hit(&self, hit: &OwnedStruct<FHitResult>) {}
}
}
- The
ProjectileMovementComponentmoves it; the root sphere collides. set_initial_life_spanremoves it after five seconds if it hits nothing.- The Blueprint child gives it a mesh, and implements BP On Projectile Hit for the impact effect and sound.
The weapon spawns it deferred, so the instigator is set before BeginPlay runs (see Spawning actors).
Use case: a weapon with a magazine and a refire rate
The goal: a weapon that fires while the trigger is held (or once per press), counts its bullets, and tells the HUD.
The shooter template splits it in two: the rules in model.rs, tested without the engine, and the weapon class that applies them.
#![allow(unused)]
fn main() {
pub enum TriggerPull {
FireNow,
FireIn(f32),
Wait,
}
pub fn pull_trigger(time_since_last_shot: f32, refire_rate: f32, full_auto: bool) -> TriggerPull {
if time_since_last_shot > refire_rate {
TriggerPull::FireNow
} else if full_auto {
TriggerPull::FireIn(refire_rate - time_since_last_shot)
} else {
TriggerPull::Wait
}
}
pub fn bullets_after_shot(bullets: i32, magazine_size: i32) -> i32 {
let left = bullets - 1;
if left <= 0 { magazine_size } else { left }
}
}
#![allow(unused)]
fn main() {
pub fn start_firing(&mut self) {
self.set_is_firing(true);
let now = GameplayStatics::get_time_seconds(self.as_ref().upcast_to()) as f32;
match model::pull_trigger(now - self.time_of_last_shot(), self.refire_rate(), self.b_full_auto()) {
TriggerPull::FireNow => self.fire(),
TriggerPull::FireIn(delay) => self.set_refire_timer("Fire", delay),
TriggerPull::Wait => {}
}
}
pub fn stop_firing(&mut self) {
self.set_is_firing(false);
KismetSystemLibrary::k2_clear_timer(self.as_ref().upcast_to(), "Fire");
}
}
fire spawns the projectile, plays the firing montage on the holder, adds recoil (add_controller_pitch_input), updates the bullet count and sets the timer for the next shot. See shooter/weapon.rs in a first-person --variant shooter project.
Use case: attaching a weapon to a hand
The goal: the weapon actor follows the character’s hand.
#![allow(unused)]
fn main() {
use bindings::engine::{EAttachmentRule, SceneComponentExt};
let snap = EAttachmentRule::SnapToTarget;
weapon.checked()?.k2_attach_to_actor(me.as_ref().upcast_to(), FName::NONE.handle(), snap, snap, snap, false);
weapon_mesh.checked()?.k2_attach_to_component(
character_mesh.upcast_to(),
FName::new("HandGrip_R").handle(),
snap,
snap,
snap,
false,
);
}
The weapon is spawned with the character as its owner, and destroys itself with it:
#![allow(unused)]
fn main() {
let weapon = self.as_ref();
me.get_owner()
.checked()?
.on_destroyed()
.add(move |_destroyed| {
if let Ok(weapon) = weapon.checked() {
weapon.k2_destroy_actor();
}
})?
.detach();
}
Use case: telling the AI a shot was heard
#![allow(unused)]
fn main() {
me.make_noise(
Some(1.0),
Some(pawn_owner),
&pawn.k2_get_actor_location(),
Some(3000.0),
Some(FName::new("Shot").handle()),
);
}
make_noise is heard by AI perception’s hearing sense, with the tag the AI filters on. See AI.
Pickups
Use case: a coin that counts
The goal: the player walks into a coin, the game mode counts it, the UI shows the count, and the Blueprint plays the effect.
#![allow(unused)]
fn main() {
use bindings::engine::{
ECollisionChannel, ECollisionEnabled, ECollisionResponse, GameplayStatics,
PrimitiveComponentExt, SceneComponent, SphereComponent, SphereComponentExt
};
#[uclass(parent = Actor)]
pub struct Coin {
#[component(root)]
root: SceneComponent,
#[component(attach = "root", name = "Collision")]
sphere: SphereComponent,
}
#[uclass_impl]
impl Coin {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let sphere = self.sphere()?.checked()?;
sphere.set_sphere_radius(60.0, Some(false));
sphere.set_collision_enabled(ECollisionEnabled::QueryOnly);
sphere.set_collision_response_to_all_channels(ECollisionResponse::ECR_Ignore);
sphere.set_collision_response_to_channel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
Ok(())
}
#[ufunction(Override)]
fn receive_actor_begin_overlap(&mut self, other_actor: UObjectRef<Actor>) {
if let Err(e) = self.pick_up(other_actor) {
log::warn!("[MyGame] pickup failed: {e}");
}
}
#[ufunction(BlueprintImplementableEvent, name = "BP_OnPickedUp")]
fn bp_on_picked_up(&self) {}
}
impl Coin {
fn pick_up(&mut self, other_actor: UObjectRef<Actor>) -> RustealResult<()> {
let Ok(character) = other_actor.cast::<Character>() else {
return Ok(());
};
if !character.checked()?.is_player_controlled() {
return Ok(());
}
let game_mode = GameplayStatics::get_game_mode(self.as_ref().upcast_to());
if let Ok(mut game_mode) = CoinGameMode::from_obj(game_mode) {
game_mode.add_coin()?;
}
self.as_ref().checked()?.set_actor_enable_collision(false);
self.bp_on_picked_up();
Ok(())
}
}
}
The collision only overlaps pawns, so the coin does not block anything. The Blueprint child gives it a mesh, and its BP On Picked Up graph plays a sound and hides or destroys it. The game mode keeps the count and updates the UI: see UI, “A counter on screen”.
Use case: a weapon pickup that respawns
The goal: a pickup gives its weapon to whoever walks in, hides, and comes back after a few seconds.
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_actor_begin_overlap(&mut self, other_actor: UObjectRef<Actor>) {
let Some(mut holder) = weapon_holder(other_actor) else {
return;
};
holder.add_weapon_class(self.weapon_class());
let Ok(me) = self.as_ref().checked() else {
return;
};
me.set_actor_hidden_in_game(true);
me.set_actor_enable_collision(false);
KismetSystemLibrary::k2_set_timer(me.as_ref().upcast_to(), "RespawnPickup", self.respawn_time(), false, None, None, None);
}
#[ufunction]
fn respawn_pickup(&mut self) {
if let Ok(me) = self.as_ref().checked() {
me.set_actor_hidden_in_game(false);
me.set_actor_enable_collision(true);
}
}
#[ufunction(Override)]
fn receive_end_play(&mut self, _end_play_reason: u8) {
KismetSystemLibrary::k2_clear_timer(self.as_ref().upcast_to(), "RespawnPickup");
}
}
weapon_holder turns the actor into a Rust trait that both the player and the NPCs implement (see Interfaces). Which weapon the pickup gives comes from a data table row: see Structs and data tables.
Platforming
The platforming and side-scrolling variants of third-person are full platformer controllers. Their rules are in model.rs, tested without the engine, and their classes apply them.
Use case: double jump and coyote time
The goal: a second jump in the air, and a jump that still works a moment after walking off a ledge.
The rules, plain Rust:
#![allow(unused)]
fn main() {
#[derive(Clone, Copy, Default)]
pub struct MoveState {
pub has_double_jumped: bool,
pub last_fall_time: f64,
}
pub enum AirJump {
Coyote,
Double,
None,
}
pub fn air_jump(state: &MoveState, now: f64, max_coyote_time: f32) -> AirJump {
if now - state.last_fall_time < f64::from(max_coyote_time) {
AirJump::Coyote
} else if !state.has_double_jumped {
AirJump::Double
} else {
AirJump::None
}
}
}
The character records when it starts falling, applies the rules on the jump press, and resets them on landing:
#![allow(unused)]
fn main() {
use bindings::engine::{CharacterExt, GameplayStatics, NavMovementComponentExt};
const MOVE_FALLING: u8 = 3;
#[uclass(parent = Character)]
pub struct Platformer {
#[uproperty(EditAnywhere, category = "Jump", default = 0.16)]
max_coyote_time: f32,
state: MoveState,
}
#[uclass_impl]
impl Platformer {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
me.set_jump_max_hold_time(0.4);
me.set_jump_max_count(3);
Ok(())
}
#[ufunction(Override, name = "K2_OnMovementModeChanged")]
fn on_movement_mode_changed(&mut self, _prev_mode: u8, new_mode: u8, _prev_custom: u8, _new_custom: u8) {
if new_mode == MOVE_FALLING {
let mut state = self.state();
state.last_fall_time = GameplayStatics::get_time_seconds(self.as_ref().upcast_to());
self.set_state(state);
}
}
#[ufunction]
fn jump_pressed(&mut self) {
let Ok(me) = self.as_ref().checked() else {
return;
};
let Ok(movement) = me.get_character_movement().checked() else {
return;
};
if !movement.is_falling() {
me.jump();
return;
}
let mut state = self.state();
let now = GameplayStatics::get_time_seconds(self.as_ref().upcast_to());
match air_jump(&state, now, self.max_coyote_time()) {
AirJump::Coyote => me.jump(),
AirJump::Double => {
state.has_double_jumped = true;
me.jump();
}
AirJump::None => {}
}
self.set_state(state);
}
#[ufunction(Override)]
fn on_landed(&mut self, _hit: UStructRef<FHitResult>) {
let mut state = self.state();
state.has_double_jumped = false;
self.set_state(state);
}
}
}
The movement mode arrives as its u8 value: 3 is MOVE_Falling in the engine’s EMovementMode.
Use case: a wall jump
The goal: jumping while touching a wall bounces off it.
#![allow(unused)]
fn main() {
let me = self.as_ref().checked()?;
let start = me.k2_get_actor_location().to_dvec3();
let end = start + me.get_actor_forward_vector().to_dvec3() * 100.0;
let (hit, out_hit) = KismetSystemLibrary::sphere_trace_single(
self.as_ref().upcast_to(),
&FVector::from_dvec3(start),
&FVector::from_dvec3(end),
40.0,
ETraceTypeQuery::TraceTypeQuery1,
false,
&[],
EDrawDebugTrace::None,
true,
&OwnedStruct::new(),
&OwnedStruct::new(),
None,
);
if hit {
let normal = out_hit.as_ref().get_impact_normal().to_dvec3();
let impulse = normal * 800.0 + DVec3::Z * 900.0;
me.launch_character(&FVector::from_dvec3(impulse), true, true);
}
}
launch_character(velocity, xy_override, z_override) replaces the character’s velocity with the launch: the bounce does not depend on how fast it was falling.
Use case: a dash
The goal: a quick dash in the air, driven by a montage with root motion, with gravity off while it lasts.
#![allow(unused)]
fn main() {
use bindings::engine::{AnimInstanceExt, MovementComponentExt, SkeletalMeshComponentExt};
fn start_dash(&mut self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
let movement = me.get_character_movement().checked()?;
movement.set_gravity_scale(0.0);
movement.set_velocity(&FVector::from_dvec3(DVec3::ZERO));
let anim_instance = me.get_mesh().checked()?.get_anim_instance();
anim_instance.checked()?.montage_play(self.dash_montage(), Some(1.0), None, Some(0.0), Some(true));
Ok(())
}
pub fn end_dash(&mut self) {
if let Ok(movement) = self.as_ref().checked().and_then(|me| me.get_character_movement().checked()) {
movement.set_gravity_scale(1.0);
}
}
}
end_dash runs from an anim notify at the end of the dash montage (see Animation), and from the montage’s end as a fallback.
Use case: a jump pad
See Delegates: a box that overlaps pawns and calls launch_character on whoever steps on it.
Use case: a platform you can drop through
The goal: a platform the character lands on, and drops through when pressing down and jump.
The platform’s mesh blocks a custom object channel; the character ignores that channel while dropping:
#![allow(unused)]
fn main() {
pub fn set_soft_collision(&self, enabled: bool) -> RustealResult<()> {
let response = if enabled { ECollisionResponse::ECR_Ignore } else { ECollisionResponse::ECR_Block };
self.as_ref()
.checked()?
.get_capsule_component()
.checked()?
.set_collision_response_to_channel(self.soft_collision_object_type(), response);
Ok(())
}
}
soft_collision_object_type is an EditAnywhere ECollisionChannel property, set in the Blueprint child to the channel the platforms use. The platform’s own receive_actor_end_overlap turns the collision back on once the character has passed.
In the editor
- Edit > Project Settings, Collision, New Object Channel: name it
SoftPlatform, Default Response Block. - In the platform Blueprint, set its mesh’s Collision Presets to Custom and its Object Type to SoftPlatform.
- In the character Blueprint’s Class Defaults, set Soft Collision Object Type to the same channel.
Screenshot: the Collision page of Project Settings with the SoftPlatform object channel. (images/recipes/soft_platform_channel.png)
Use case: a moving platform
The goal: a platform that moves to a target when the player interacts with it.
The side-scrolling template leaves the motion to the Blueprint, where a Timeline is the natural tool, and keeps the rules in Rust:
#![allow(unused)]
fn main() {
#[uclass(parent = Actor)]
pub struct MovingPlatform {
#[component(root)]
root: SceneComponent,
#[uproperty(EditAnywhere, BlueprintReadOnly)]
platform_target: OwnedStruct<FVector>,
#[uproperty(EditAnywhere, BlueprintReadOnly, default = 2.0)]
move_duration: f32,
moving: bool,
}
#[uclass_impl]
impl MovingPlatform {
#[ufunction(BlueprintCallable)]
fn reset_interaction(&mut self) {
self.set_moving(false);
}
#[ufunction(BlueprintImplementableEvent, name = "BP_MoveToTarget")]
fn bp_move_to_target(&self) {}
}
impl MovingPlatform {
pub fn interact(&mut self) {
if self.moving() {
return;
}
self.set_moving(true);
self.bp_move_to_target();
}
}
}
The Blueprint child implements BP Move To Target with a Timeline that moves the platform to Platform Target over Move Duration, and calls Reset Interaction when it finishes.
To move it from Rust instead, interpolate in receive_tick with k2_set_actor_location, using the engine’s interpolation as plain Rust (current + (target - current) * (delta * speed).clamp(0.0, 1.0)).
Physics
Use case: a prop that reacts to hits
The goal: a box that simulates physics, is pushed where it is hit, and breaks after enough damage.
#![allow(unused)]
fn main() {
use bindings::engine::{ECollisionChannel, KismetSystemLibrary, PrimitiveComponentExt, StaticMeshComponent};
#[uclass(parent = Actor)]
pub struct Crate {
#[component(root)]
mesh: StaticMeshComponent,
#[uproperty(EditAnywhere, name = "CurrentHP", default = 30.0)]
current_hp: f32,
}
#[uclass_impl]
impl Crate {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let mesh = self.mesh()?.checked()?;
mesh.set_collision_profile_name(FName::new("BlockAllDynamic").handle(), Some(true));
mesh.set_simulate_physics(true);
mesh.set_can_ever_affect_navigation(false);
Ok(())
}
}
impl Crate {
pub fn hit(&mut self, damage: f32, location: DVec3, impulse: DVec3) {
self.set_current_hp(self.current_hp() - damage);
if let Ok(mesh) = self.mesh().and_then(|m| m.checked()) {
let scaled = impulse * f64::from(mesh.get_mass());
mesh.add_impulse_at_location(&FVector::from_dvec3(scaled), &FVector::from_dvec3(location), None);
if self.current_hp() <= 0.0 {
mesh.set_collision_object_type(ECollisionChannel::ECC_Visibility);
}
}
}
}
}
add_impulse_at_location pushes and spins the body from the point that was hit. Multiplying by the mass makes the push the same for a light and a heavy prop; add_impulse(impulse, bone, vel_change = Some(true)) does that for you.
Use case: knockback on a character
A character’s capsule does not simulate physics; its movement component takes the impulse:
#![allow(unused)]
fn main() {
if let Ok(movement) = me.get_character_movement().checked() {
movement.add_impulse(&FVector::from_dvec3(knockback), Some(true));
}
}
Or replace its velocity at once with launch_character.
Use case: a hit reaction with partial ragdoll
#![allow(unused)]
fn main() {
let mesh = me.get_mesh().checked()?;
mesh.set_physics_blend_weight(0.5);
mesh.set_all_bodies_below_simulate_physics(FName::new("pelvis").handle(), true, Some(false));
}
The combat template blends the ragdoll in when hit and back out on landing (set_physics_blend_weight(0.0) in on_landed).
Use case: a physics constraint
The goal: a training dummy that wobbles on its base.
#![allow(unused)]
fn main() {
use bindings::engine::{PhysicsConstraintComponent, PhysicsConstraintComponentExt};
#[uclass(parent = Actor)]
pub struct Dummy {
#[component(root)]
root: SceneComponent,
#[component(attach = "root", name = "Base Plate")]
base_plate: StaticMeshComponent,
#[component(attach = "root")]
dummy: StaticMeshComponent,
#[component(attach = "root", name = "Physics Constraint")]
constraint: PhysicsConstraintComponent,
}
#[uclass_impl]
impl Dummy {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
self.dummy()?.checked()?.set_simulate_physics(true);
Ok(())
}
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let (Ok(constraint), Ok(base_plate), Ok(dummy)) =
(self.constraint().and_then(|c| c.checked()), self.base_plate(), self.dummy())
{
constraint.set_constrained_components(
base_plate.upcast_to(),
FName::NONE.handle(),
dummy.upcast_to(),
FName::NONE.handle(),
);
}
}
}
}
The constraint’s limits (swing, twist) are set on the component in the Blueprint child’s Details panel.
A chain is the same thing repeated: the interactables feature’s BP_DangleBlocks has four cubes and four constraints, the first holding the top cube to the world (its Component Name 1 names no component) and each other one a cube to the one above, with where each piece sits and how far each joint swings. Its Rust parent, DangleBlocks, has no code: the Blueprint is all data.
Collision presets and channels
| Call | What it does |
|---|---|
set_collision_profile_name(FName::new("BlockAllDynamic").handle(), Some(true)) | Applies a preset from Project Settings > Collision. |
set_collision_enabled(ECollisionEnabled::QueryOnly) | Overlaps and traces, no physics. |
set_collision_object_type(ECollisionChannel::ECC_WorldDynamic) | What the component is, for others’ responses. |
set_collision_response_to_all_channels(ECollisionResponse::ECR_Ignore) | Ignores everything… |
set_collision_response_to_channel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap) | …then overlaps pawns. |
A component meant to fire overlap events also needs Generate Overlap Events on, which most presets have.
UI
Rusteal UI follows one pattern: a Rust class whose parent is UserWidget declares what the widget can be told, as BlueprintImplementableEvents; a Widget Blueprint child of it draws the layout in the UMG designer and implements those events; the game creates the widget from a class a Blueprint assigns.
Use case: a counter on screen
The goal: a pickups counter that appears with the first pickup and updates with each one.
The widget class:
#![allow(unused)]
fn main() {
use bindings::umg::UserWidget;
#[uclass(parent = UserWidget)]
pub struct CoinsUI {}
#[uclass_impl]
impl CoinsUI {
#[ufunction(BlueprintImplementableEvent)]
pub fn update_coins(&self, amount: i32) {}
}
}
The game mode creates it and tells it the count:
#![allow(unused)]
fn main() {
use bindings::engine::{GameModeBase, GameplayStatics};
use bindings::umg::UserWidgetExt;
#[uclass(parent = GameModeBase)]
pub struct CoinGameMode {
#[uproperty(EditAnywhere)]
user_interface_class: SubclassOf<CoinsUI>,
#[uproperty]
user_interface: UObjectRef<CoinsUI>,
#[uproperty(BlueprintReadOnly)]
coins: i32,
}
#[uclass_impl]
impl CoinGameMode {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let Err(e) = self.create_user_interface() {
log::warn!("[MyGame] UI: {e}");
}
}
}
impl CoinGameMode {
fn create_user_interface(&mut self) -> RustealResult<()> {
let player = GameplayStatics::get_player_controller(self.as_ref().upcast_to(), 0);
let widget = create_widget_of_class(&player, self.user_interface_class())?;
self.set_user_interface(widget);
Ok(())
}
pub fn add_coin(&mut self) -> RustealResult<()> {
let coins = self.coins() + 1;
self.set_coins(coins);
let ui = self.user_interface();
if coins == 1 {
ui.upcast_to::<UserWidget>().checked()?.add_to_viewport(Some(0));
}
CoinsUI::from_obj(ui)?.update_coins(coins);
Ok(())
}
}
}
create_widget_of_class(&owning_player, class)is C++’sCreateWidget<T>(PlayerController, Class).add_to_viewportshows it over the whole screen;add_to_player_screenover the owning player’s part of a split screen;remove_from_parenthides it again.CoinsUI::from_obj(widget)reaches the Rust class of the widget, whose events run the Widget Blueprint’s graph.
In the editor
- In the Content Browser, right-click and choose User Interface > Widget Blueprint, and name it
UI_Coins. - Open it, switch to Graph (top right), click Class Settings in the toolbar, and in the Details panel set Parent Class to CoinsUI.
- Switch to Designer, add a Canvas Panel and a Text inside it. Select the Text, check Is Variable in the Details panel and name it
CoinsText. - Switch to Graph. Right-click and add Event Update Coins. From it, get CoinsText, call SetText (Text), and convert Amount to text with To Text (Integer).
- Compile and Save.
- Open the game mode’s Blueprint child, and in Class Defaults set User Interface Class to UI_Coins.
Screenshot: UI_Coins’s Graph with Event Update Coins setting the CoinsText text. (images/recipes/ui_update_coins.png)
Use case: a widget that listens to the character
The goal: a stamina meter that updates whenever the character’s stamina changes.
The horror template has the player controller create the widget on possess, and hand the character to it:
#![allow(unused)]
fn main() {
#[uclass(parent = UserWidget)]
pub struct HorrorUI {}
#[uclass_impl]
impl HorrorUI {
#[ufunction(BlueprintImplementableEvent, name = "BP_SprintMeterUpdated")]
pub fn bp_sprint_meter_updated(&self, percent: f32) {}
#[ufunction(BlueprintImplementableEvent, name = "BP_SprintStateChanged")]
pub fn bp_sprint_state_changed(&self, sprinting: bool) {}
}
}
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_possess(&mut self, possessed_pawn: UObjectRef<Pawn>) {
let me: UObjectRef<PlayerController> = self.as_ref();
if !self.horror_ui().is_valid()
&& let Ok(widget) = create_widget_of_class(&me, self.horror_ui_class())
&& let Ok(checked) = widget.checked()
{
checked.add_to_player_screen(Some(0));
self.set_horror_ui(widget);
}
}
}
The character then calls the widget’s events when the stamina changes. A delegate of your own (#[udelegate]) on the character does the same job and lets any number of widgets, Blueprint or Rust, listen.
Use case: a health bar above a character
The goal: a bar that floats over each enemy.
#![allow(unused)]
fn main() {
use bindings::umg::{WidgetComponent, WidgetComponentExt};
#[uclass(parent = Character)]
pub struct Enemy {
#[component(attach = "root_component")]
life_bar: WidgetComponent,
#[uproperty]
life_bar_widget: UObjectRef<LifeBar>,
}
#[uclass_impl]
impl Enemy {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let Ok(widget) = self
.life_bar()
.and_then(|bar| bar.checked())
.and_then(|bar| bar.get_user_widget_object().cast::<LifeBar>())
{
self.set_life_bar_widget(widget);
}
}
}
}
The Blueprint child sets the Widget Class of the Life Bar component to a Widget Blueprint whose parent is LifeBar, and its Space to Screen. Updating it is LifeBar::from_obj(self.life_bar_widget())?.set_life_percentage(hp / max_hp).
Use case: the buttons of a Widget Blueprint
The goal: a menu whose layout a designer draws in the UMG designer, and whose buttons run Rust functions.
#![allow(unused)]
fn main() {
use bindings::umg::{Button, ButtonExt, UserWidget};
#[uclass(parent = UserWidget)]
pub struct MenuWidget {
#[uproperty(BindWidget, BlueprintReadOnly)]
restart_button: UObjectRef<Button>,
#[uproperty(BindWidget, BlueprintReadOnly)]
exit_button: UObjectRef<Button>,
}
#[uclass_impl]
impl MenuWidget {
#[ufunction(Override)]
fn construct(&mut self) {
let me = self.as_ref().upcast_to::<UserWidget>();
for (button, handler) in [(self.restart_button(), "Restart"), (self.exit_button(), "Exit")] {
if let Ok(button) = button.checked() {
let _ = button.on_clicked().add_ufunction(&me, handler);
}
}
}
#[ufunction(BlueprintCallable)]
pub fn restart(&mut self) {
let me = self.as_ref();
let level = GameplayStatics::get_current_level_name(me.upcast_to(), Some(true));
GameplayStatics::open_level(me.upcast_to(), FName::new(&level).handle(), Some(true), None);
}
#[ufunction(BlueprintCallable)]
pub fn exit(&mut self) {
KismetSystemLibrary::quit_game(self.as_ref().upcast_to(), UObjectRef::null(), EQuitPreference::Quit, false);
}
}
}
A BindWidget property is C++’s UPROPERTY(meta = (BindWidget)): when the widget is created, it holds the layout’s widget of the same name (RestartButton for restart_button). The Widget Blueprint must have it, with that name and a compatible class, or it does not compile; BindWidgetOptional lets the layout leave it out, and the property is then null.
In the editor
- Create a Widget Blueprint whose parent is MenuWidget, or open an existing one, click Class Settings and set Parent Class to MenuWidget.
- In the Designer, add two Buttons and name them
RestartButtonandExitButtonin the Details panel. - Click Compile. A missing or misnamed button is reported by the compiler.
Screenshot: the UMG designer with RestartButton and ExitButton, and the compiler results naming a missing BindWidget. (images/recipes/bind_widget.png)
Template: vr (the 3D menu).
Use case: playing a Widget Blueprint’s animations
The goal: a flash plays over the screen when the player takes a screenshot, an animation drawn in the UMG designer’s Animations panel and started from Rust.
#![allow(unused)]
fn main() {
use bindings::umg::{EUMGSequencePlayMode, UserWidget, UserWidgetExt, WidgetAnimation};
#[uclass(parent = UserWidget)]
pub struct MainMenu {
#[uproperty(BindWidgetAnim, BlueprintReadOnly, name = "Screenshot_Show")]
screenshot_show: UObjectRef<WidgetAnimation>,
}
#[uclass_impl]
impl MainMenu {
#[ufunction(BlueprintCallable)]
pub fn take_screenshot(&mut self) {
let me = self.as_ref().upcast_to::<UserWidget>();
if let Ok(widget) = me.checked() {
widget.play_animation(
self.screenshot_show(),
Some(0.0),
Some(1),
Some(EUMGSequencePlayMode::Forward),
Some(1.0),
Some(false),
);
}
}
}
}
BindWidgetAnim binds the Widget Blueprint’s animation of the same name, as BindWidget binds a widget; the property is made Transient, since the engine sets it on each created widget. To run code when the animation ends, set a timer for get_end_time() seconds with KismetSystemLibrary::k2_set_timer.
Template: ar (the main menu’s screenshot flash and tutorial).
Use case: drawing on the HUD
The goal: a selection box drawn while dragging the mouse.
#![allow(unused)]
fn main() {
use bindings::engine::{HUD, HUDExt};
#[uclass(parent = HUD)]
pub struct StrategyHUD {
#[uproperty(EditAnywhere)]
selection_box_color: OwnedStruct<FLinearColor>,
drawing: bool,
box_start: DVec2,
box_size: DVec2,
}
#[uclass_impl]
impl StrategyHUD {
#[ufunction(Override)]
fn receive_draw_hud(&mut self, _size_x: i32, _size_y: i32) {
if !self.drawing() {
return;
}
let Ok(me) = self.as_ref().checked() else {
return;
};
let (start, size) = (self.box_start(), self.box_size());
me.draw_rect(&self.selection_box_color(), start.x as f32, start.y as f32, size.x as f32, size.y as f32);
}
}
}
receive_draw_hud runs every frame; draw_rect, draw_text, draw_line and draw_texture draw on the canvas. get_actors_in_selection_rectangle returns the actors inside a screen rectangle, which is how the strategy template selects units with a box. The game mode’s Blueprint child sets HUD Class to the HUD’s Blueprint.
AI
AI in Rusteal uses the engine’s tools: AI controllers, StateTree, EQS, AI perception and the navigation mesh. Rust writes the controller, the StateTree tasks and conditions, and the EQS contexts; the StateTree and the EQS queries are assets made in the editor.
The project needs the ai feature, and state-tree for StateTree, in rusteal.toml (see Binding more of the engine):
features = ["core", "engine", "input", "slate", "umg", "enhanced-input", "ai", "state-tree"]
Use case: an AI controller that runs a StateTree
The goal: an enemy’s controller that starts its StateTree when it possesses the enemy.
#![allow(unused)]
fn main() {
use bindings::ai::{AIController, AIControllerExt, BrainComponentExt};
use bindings::engine::{ControllerExt, Pawn};
use bindings::gameplay_state_tree::{StateTreeAIComponent, StateTreeComponentExt};
#[uclass(parent = AIController)]
pub struct EnemyAIController {
#[component(name = "StateTreeAI")]
state_tree_ai: StateTreeAIComponent,
}
#[uclass_impl]
impl EnemyAIController {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
me.set_start_ai_logic_on_possess(false);
me.set_attach_to_pawn(true);
self.state_tree_ai()?.checked()?.set_start_logic_automatically(false);
Ok(())
}
#[ufunction(Override)]
fn receive_possess(&mut self, _possessed_pawn: UObjectRef<Pawn>) {
if let Ok(state_tree) = self.state_tree_ai().and_then(|s| s.checked()) {
state_tree.start_logic();
}
}
}
}
Starting the StateTree on possession, instead of automatically, makes sure the tree finds its pawn: started earlier, it logs that it could not find its context actor.
The enemy uses it, in its class defaults:
#![allow(unused)]
fn main() {
me.set_ai_controller_class(SubclassOf::<EnemyAIController>::base().upcast_to());
me.set_auto_possess_ai(EAutoPossessAI::PlacedInWorldOrSpawned);
}
The controller’s Blueprint child sets the State Tree of the StateTreeAI component.
Use case: a StateTree task
The goal: a task that makes the AI face an actor while its state is active.
#![allow(unused)]
fn main() {
use bindings::ai::{AIController, AIControllerExt};
use bindings::state_tree::{FStateTreeTransitionResult, StateTreeTaskBlueprintBase, StateTreeTaskBlueprintBaseExt};
#[uclass(parent = StateTreeTaskBlueprintBase)]
pub struct StateTreeFaceActorTask {
#[uproperty(EditAnywhere, category = "Context")]
controller: UObjectRef<AIController>,
#[uproperty(EditAnywhere, category = "Input")]
actor_to_face_towards: UObjectRef<Actor>,
}
#[uclass_impl]
impl StateTreeFaceActorTask {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
self.as_ref().checked()?.set_should_state_change_on_reselect(false);
Ok(())
}
#[ufunction(Override)]
fn receive_latent_enter_state(&mut self, _transition: UStructRef<FStateTreeTransitionResult>) {
if let Ok(controller) = self.controller().checked() {
controller.k2_set_focus(self.actor_to_face_towards());
}
}
#[ufunction(Override)]
fn receive_exit_state(&mut self, _transition: UStructRef<FStateTreeTransitionResult>) {
if let Ok(controller) = self.controller().checked() {
controller.k2_clear_focus();
}
}
}
}
- A task’s parent is
StateTreeTaskBlueprintBase; it overridesreceive_latent_enter_state,receive_exit_stateandreceive_tick. - The property categories are the StateTree’s binding categories: Context binds to the tree’s context (the actor, the controller), Input and Parameter to values, Output to values other tasks read.
- A task that finishes on its own calls
finish_task(Some(true))(succeeded) orfinish_task(Some(false))on itself; one that runs until its state is left does not.
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_latent_enter_state(&mut self, _transition: UStructRef<FStateTreeTransitionResult>) {
let found = self.select_target();
if let Ok(me) = self.as_ref().checked() {
me.finish_task(Some(found));
}
}
}
Use case: a StateTree condition
#![allow(unused)]
fn main() {
use bindings::state_tree::StateTreeConditionBlueprintBase;
#[uclass(parent = StateTreeConditionBlueprintBase)]
pub struct StateTreeIsGroundedCondition {
#[uproperty(EditAnywhere, category = "Context")]
character: UObjectRef<Character>,
#[uproperty(EditAnywhere, category = "Condition", default = false)]
b_must_be_in_air: bool,
}
#[uclass_impl]
impl StateTreeIsGroundedCondition {
#[ufunction(Override)]
fn receive_test_condition(&self) -> bool {
let Ok(movement) = self.character().checked().and_then(|c| c.get_character_movement().checked()) else {
return false;
};
movement.is_moving_on_ground() != self.b_must_be_in_air()
}
}
}
In the editor
- Open the StateTree asset (
ST_Enemy). - Select a state. In the Details panel, add a task under Tasks and pick State Tree Face Actor Task from the class list.
- Bind the task’s Controller to the tree’s AI controller context, and its Actor To Face Towards to an output of another task.
- Add the condition the same way, under the state’s Enter Conditions or a transition’s Conditions, and pick State Tree Is Grounded Condition.
- Compile and Save.
Rust tasks and conditions are listed under their class names, like Blueprint ones.
Screenshot: a StateTree state with the Rust task added and its Controller bound to the context. (images/recipes/state_tree_task.png)
Use case: AI perception
The goal: an enemy that reacts to what it sees and hears.
#![allow(unused)]
fn main() {
use bindings::ai::{AIPerceptionComponent, AIPerceptionComponentExt, FAIStimulus, FAIStimulusExt};
#[uclass(parent = AIController)]
pub struct GuardController {
#[component(name = "AIPerception")]
ai_perception: AIPerceptionComponent,
}
#[uclass_impl]
impl GuardController {
#[ufunction(Override)]
fn receive_possess(&mut self, _possessed_pawn: UObjectRef<Pawn>) {
if let Err(e) = self.bind_perception() {
log::warn!("[MyGame] AI perception: {e}");
}
}
}
impl GuardController {
fn bind_perception(&self) -> RustealResult<()> {
let me: UObjectRef<AIController> = self.as_ref();
self.ai_perception()?
.checked()?
.on_target_perception_updated()
.add(move |actor, stimulus| {
if let Ok(guard) = GuardController::from_obj(me) {
guard.on_perceived(actor, stimulus);
}
})?
.detach();
Ok(())
}
fn on_perceived(&self, actor: UObjectRef<Actor>, stimulus: OwnedStruct<FAIStimulus>) {
let location = stimulus.as_ref().get_stimulus_location().to_dvec3();
let heard_a_shot = FName(stimulus.as_ref().get_tag()) == FName::new("Shot");
}
}
}
The senses (sight, hearing, their ranges and angles) are set on the AIPerception component in the controller’s Blueprint child: Senses Config, +, AI Sight config. Noises come from make_noise (see Projectiles and weapons), which also needs a Pawn Noise Emitter component on the player.
Use case: moving to a point
#![allow(unused)]
fn main() {
let result = ai_controller.move_to_location(
&FVector::from_dvec3(goal),
Some(100.0),
Some(true),
Some(true),
Some(true),
Some(false),
None,
Some(true),
);
if result == EPathFollowingRequestResult::AlreadyAtGoal {
}
}
To know when the move ends, bind the controller’s receive_move_completed delegate:
#![allow(unused)]
fn main() {
controller
.receive_move_completed()
.add(move |_request_id, _result| {
if let Ok(mut unit) = Unit::from_obj(me) {
unit.on_arrived();
}
})?
.detach();
}
A level needs a Nav Mesh Bounds Volume for the AI to find paths.
Use case: running an EQS query from Rust
The goal: a unit asks EQS for the best point around a goal and moves there.
#![allow(unused)]
fn main() {
use bindings::ai::{EEnvQueryRunMode, EnvQuery, EnvQueryInstanceBlueprintWrapper, EnvQueryInstanceBlueprintWrapperExt, EnvQueryManager};
let me: UObjectRef<Unit> = self.as_ref().cast().unwrap_or_default();
let query_instance = EnvQueryManager::run_eqs_query(
me.upcast_to(),
self.move_query(),
me.upcast_to(),
EEnvQueryRunMode::RandomBest25Pct,
SubclassOf::<EnvQueryInstanceBlueprintWrapper>::base(),
);
self.set_env_query_instance(query_instance);
query_instance
.checked()?
.on_query_finished_event()
.add(move |finished_query, _status| {
if let Ok(mut unit) = Unit::from_obj(me) {
unit.on_eqs_finished(finished_query);
}
})?
.detach();
}
#![allow(unused)]
fn main() {
fn on_eqs_finished(&mut self, query: UObjectRef<EnvQueryInstanceBlueprintWrapper>) {
let Ok(query) = query.checked() else {
return;
};
let (found, locations) = query.get_query_results_as_locations();
if let Some(best) = locations.first().filter(|_| found) {
self.move_to(best.to_dvec3());
}
}
}
Keep the query instance in a #[uproperty] until it finishes, or the garbage collector may collect it.
Use case: an EQS context
The goal: EQS queries that score points around a location Rust knows, such as where danger was last seen.
#![allow(unused)]
fn main() {
use bindings::ai::EnvQueryContext_BlueprintBase;
use bindings::core_ue::Object;
#[uclass(parent = EnvQueryContext_BlueprintBase)]
pub struct EnvQueryContextDanger {}
#[uclass_impl]
impl EnvQueryContextDanger {
#[ufunction(Override)]
fn provide_single_location(
&self,
_querier_object: UObjectRef<Object>,
querier_actor: UObjectRef<Actor>,
resulting_location: UStructRef<FVector>,
) {
if let Ok(enemy) = Enemy::from_obj(querier_actor) {
let location = enemy.last_danger_location();
resulting_location.set_x(location.x);
resulting_location.set_y(location.y);
resulting_location.set_z(location.z);
}
}
}
}
provide_single_actor(&self, querier_object, querier_actor, resulting_actor: OutRef<UObjectRef<Actor>>) provides an actor instead. In the EQS editor, the context shows in every Context dropdown under its class name.
Point and click
The goal: click where the character should go, and it walks there along the navigation mesh; hold the button and it follows the cursor.
The top-down template’s player controller does both: a short click pathfinds to the point, a held click steers toward the cursor.
#![allow(unused)]
fn main() {
use bindings::ai::{AIBlueprintHelperLibrary, PathFollowingComponent};
use bindings::core_ue::EMouseCursor;
use bindings::engine::{
ControllerExt, ETraceTypeQuery, FHitResultExt, GameplayStatics, PawnExt, PlayerController,
PlayerControllerExt
};
use bindings::enhanced_input::{
ETriggerEvent, EnhancedInputLocalPlayerSubsystemExt, InputAction, InputMappingContext
};
#[uclass(parent = PlayerController)]
pub struct ClickController {
#[component(name = "Path Following Component")]
path_following_component: PathFollowingComponent,
#[uproperty(EditAnywhere, category = "Input", default = 0.3)]
short_press_threshold: f32,
#[uproperty(EditAnywhere, category = "Input")]
default_mapping_context: UObjectRef<InputMappingContext>,
#[uproperty(EditAnywhere, category = "Input")]
set_destination_click_action: UObjectRef<InputAction>,
cached_destination: DVec3,
follow_time: f32,
}
#[uclass_impl]
impl ClickController {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
me.set_show_mouse_cursor(true);
me.set_default_mouse_cursor(EMouseCursor::Default);
Ok(())
}
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if let Err(e) = self.setup_input() {
log::warn!("[MyGame] input setup failed: {e}");
}
}
#[ufunction]
fn on_input_started(&mut self) {
if let Ok(me) = self.as_ref().checked() {
me.stop_movement();
}
}
#[ufunction]
fn on_set_destination_triggered(&mut self) {
let Ok(me) = self.as_ref().checked() else {
return;
};
let delta = GameplayStatics::get_world_delta_seconds(me.as_ref().upcast_to()) as f32;
self.set_follow_time(self.follow_time() + delta);
let (hit, out_hit) = me.get_hit_result_under_cursor_by_channel(ETraceTypeQuery::TraceTypeQuery1, true);
if hit {
self.set_cached_destination(out_hit.as_ref().get_location().to_dvec3());
}
if let Ok(pawn) = me.k2_get_pawn().checked() {
let direction = (self.cached_destination() - pawn.k2_get_actor_location().to_dvec3()).normalize_or_zero();
pawn.add_movement_input(&FVector::from_dvec3(direction), Some(1.0), Some(false));
}
}
#[ufunction]
fn on_set_destination_released(&mut self) {
if self.follow_time() <= self.short_press_threshold() {
let destination = FVector::from_dvec3(self.cached_destination());
AIBlueprintHelperLibrary::simple_move_to_location(self.as_ref().upcast_to(), &destination);
}
self.set_follow_time(0.0);
}
}
impl ClickController {
fn setup_input(&self) -> RustealResult<()> {
let me: UObjectRef<PlayerController> = self.as_ref();
if !me.checked()?.is_local_player_controller() {
return Ok(());
}
enhanced_input_subsystem(me)?
.checked()?
.add_mapping_context(self.default_mapping_context(), 0, &OwnedStruct::new());
let click = self.set_destination_click_action();
bind_action(&me, click, ETriggerEvent::Started, "OnInputStarted")?;
bind_action(&me, click, ETriggerEvent::Triggered, "OnSetDestinationTriggered")?;
bind_action(&me, click, ETriggerEvent::Completed, "OnSetDestinationReleased")?;
bind_action(&me, click, ETriggerEvent::Canceled, "OnSetDestinationReleased")?;
Ok(())
}
}
}
simple_move_to_locationis the Blueprint node Simple Move to Location: it pathfinds for the controller’s pawn, which needs thePathFollowingComponentthe controller declares.- The project needs the
aifeature forbindings::ai(see Binding more of the engine), and the level a Nav Mesh Bounds Volume. - Touch works the same with
get_hit_result_under_finger_by_channel(ETouchIndex::Touch1, ...)and a second input action.
The template also spawns a Niagara effect at the clicked point: see Effects and sound.
Effects and sound
Two ways to play effects, and the templates use both:
- From the Blueprint child, through a
BlueprintImplementableEventthe Rust class calls (BP_OnHit,BP_OnDeath): the artist picks and tunes the effect in the graph, and Rust says when. This is the default in the templates. See Functions. - From Rust, with the engine’s functions, when the effect is part of the rule: a cursor marker where the player clicked, a sound at a computed location.
Use case: a Niagara effect at a point
The goal: a marker effect where the player clicked.
The project needs the niagara feature (see Binding more of the engine).
#![allow(unused)]
fn main() {
use bindings::niagara::{ENCPoolMethod, NiagaraFunctionLibrary, NiagaraSystem};
#[uproperty(EditAnywhere, name = "FXCursor", category = "Input")]
fx_cursor: UObjectRef<NiagaraSystem>,
}
#![allow(unused)]
fn main() {
NiagaraFunctionLibrary::spawn_system_at_location(
self.as_ref().upcast_to(),
self.fx_cursor(),
&FVector::from_dvec3(location),
&FRotator::from_rotator(Rotator::ZERO),
&FVector::from_dvec3(DVec3::ONE),
Some(true),
Some(true),
Some(ENCPoolMethod::None),
Some(true),
);
}
The arguments after the scale are auto destroy, auto activate, the pooling method and whether to pre-cull. spawn_system_attached attaches the effect to a component instead, to follow it.
Use case: a sound at a location
#![allow(unused)]
fn main() {
use bindings::engine::{GameplayStatics, SoundBase};
#[uproperty(EditAnywhere)]
impact_sound: UObjectRef<SoundBase>,
}
#![allow(unused)]
fn main() {
GameplayStatics::play_sound_at_location(
self.as_ref().upcast_to(),
self.impact_sound(),
&FVector::from_dvec3(location),
&FRotator::from_rotator(Rotator::ZERO),
None,
None,
None,
None,
None,
None,
None,
);
}
The Nones keep the defaults for volume, pitch, start time, attenuation, concurrency, owner and initial parameters. play_sound_2d plays a sound without a location (UI, music), and spawn_sound_at_location returns the audio component, to stop or fade it later.
Use case: a light on the character
The goal: a flashlight attached to the first-person camera.
#![allow(unused)]
fn main() {
use bindings::engine::{ELightUnits, LightComponentExt, LocalLightComponentExt, SpotLightComponent, SpotLightComponentExt};
#[uclass(parent = FirstPersonCharacter)]
pub struct HorrorCharacter {
#[component(attach = "first_person_camera_component", name = "SpotLight")]
spot_light: SpotLightComponent,
}
#[uclass_impl]
impl HorrorCharacter {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let light = self.spot_light()?.checked()?;
light.set_intensity(1500.0);
light.set_intensity_units(ELightUnits::Lumens);
light.set_attenuation_radius(2000.0);
Ok(())
}
}
}
Turning it on and off is light.set_visibility(on, None), bound to an input action.
Use case: a MetaSound parameter
The goal: a tuning fork whose MetaSound plays a pattern chosen by a seed set on each placed fork.
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_tick(&mut self, _delta_seconds: f32) {
if let Ok(audio) = self.audio().and_then(|a| a.checked()) {
audio.set_int_parameter(FName::new("Seed").handle(), self.random_seed());
}
}
}
The audio component’s Sound is the MetaSound source; any of its inputs is set by name with set_int_parameter, set_float_parameter or set_bool_parameter.
Use case: an effect a moment after a trigger
The goal: a confetti burst one second after a trigger first fires, and never again.
#![allow(unused)]
fn main() {
#[ufunction]
fn trigger_confetti(&mut self) {
if self.fired() {
return;
}
self.set_fired(true);
KismetSystemLibrary::k2_set_timer(self.as_ref().upcast_to(), "Burst", 1.0, false, None, None, None);
}
#[ufunction]
fn burst(&mut self) {
if let Ok(niagara) = self.niagara().and_then(|n| n.checked()) {
niagara.activate(Some(true));
}
}
}
The Niagara component is not auto-activated; activate(Some(true)) restarts it from the beginning.
Use case: playing a level sequence
The goal: walking into an area plays a cinematic placed in the level.
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_actor_begin_overlap(&mut self, _other_actor: UObjectRef<Actor>) {
if let Ok(sequence) = self.seq_ref().checked()
&& let Ok(player) = sequence.get_sequence_player().checked()
{
player.play();
}
}
}
seq_ref is a UObjectRef<LevelSequenceActor> set on the placed trigger. The project binds the sequencer modules with the level-sequence and movie features (see Binding more of the engine).
Vehicles
The vehicle template is the engine’s Vehicle template with its gameplay in Rust: a sports car and an offroad car on Chaos Vehicles, two cameras, a speed and gear HUD, and a time trial variant with laps.
rusteal new Racer --template vehicle
rusteal new Racer --template vehicle --variant time-trial
The project needs the chaos-vehicles feature, which the template turns on (see Binding more of the engine):
features = ["core", "engine", "input", "slate", "umg", "enhanced-input", "chaos-vehicles"]
[codegen.modules]
ChaosVehicles = { module = "chaos_vehicles", feature = "chaos-vehicles", plugin = "ChaosVehiclesPlugin" }
Use case: a car the player drives
The goal: a pawn on Chaos Vehicles, driven with Enhanced Input.
A car’s parent is WheeledVehiclePawn, which brings the skeletal mesh and the Chaos movement component. The input functions pass the axis values to the movement component:
#![allow(unused)]
fn main() {
use bindings::chaos_vehicles::{
ChaosVehicleMovementComponentExt, ChaosWheeledVehicleMovementComponent, WheeledVehiclePawn,
WheeledVehiclePawnExt
};
use bindings::enhanced_input::{ETriggerEvent, FInputActionValue, InputAction};
use rusteal_runtime::runtime::Checked;
#[uclass(parent = WheeledVehiclePawn)]
pub struct Car {
#[uproperty(EditAnywhere, category = "Input")]
steering_action: UObjectRef<InputAction>,
#[uproperty(EditAnywhere, category = "Input")]
throttle_action: UObjectRef<InputAction>,
#[uproperty(EditAnywhere, category = "Input")]
brake_action: UObjectRef<InputAction>,
#[uproperty(EditAnywhere, category = "Input")]
handbrake_action: UObjectRef<InputAction>,
}
#[uclass_impl]
impl Car {
#[ufunction(Override)]
fn receive_restarted(&mut self) {
if let Err(e) = self.bind_input() {
log::warn!("[Racer] input binding failed: {e}");
}
}
#[ufunction]
fn steering(&mut self, value: UStructRef<FInputActionValue>) {
if let Ok(movement) = self.movement() {
movement.set_steering_input(value.axis1d() as f32);
}
}
#[ufunction]
fn throttle(&mut self, value: UStructRef<FInputActionValue>) {
if let Ok(movement) = self.movement() {
movement.set_throttle_input(value.axis1d() as f32);
movement.set_brake_input(0.0);
}
}
#[ufunction]
fn brake(&mut self, value: UStructRef<FInputActionValue>) {
if let Ok(movement) = self.movement() {
movement.set_brake_input(value.axis1d() as f32);
movement.set_throttle_input(0.0);
}
}
#[ufunction]
fn start_handbrake(&mut self) {
if let Ok(movement) = self.movement() {
movement.set_handbrake_input(true);
}
}
#[ufunction]
fn stop_handbrake(&mut self) {
if let Ok(movement) = self.movement() {
movement.set_handbrake_input(false);
}
}
}
impl Car {
pub fn movement(&self) -> RustealResult<Checked<ChaosWheeledVehicleMovementComponent>> {
self.as_ref()
.checked()?
.get_vehicle_movement_component()
.cast::<ChaosWheeledVehicleMovementComponent>()?
.checked()
}
fn bind_input(&self) -> RustealResult<()> {
let me = self.as_ref();
if !me.checked()?.is_locally_controlled() {
return Ok(());
}
bind_action(&me, self.steering_action(), ETriggerEvent::Triggered, "Steering")?;
bind_action(&me, self.steering_action(), ETriggerEvent::Completed, "Steering")?;
bind_action(&me, self.throttle_action(), ETriggerEvent::Triggered, "Throttle")?;
bind_action(&me, self.throttle_action(), ETriggerEvent::Completed, "Throttle")?;
bind_action(&me, self.brake_action(), ETriggerEvent::Triggered, "Brake")?;
bind_action(&me, self.brake_action(), ETriggerEvent::Completed, "Brake")?;
bind_action(&me, self.handbrake_action(), ETriggerEvent::Started, "StartHandbrake")?;
bind_action(&me, self.handbrake_action(), ETriggerEvent::Completed, "StopHandbrake")?;
Ok(())
}
}
}
get_vehicle_movement_component()returns the base Chaos movement component; casting it toChaosWheeledVehicleMovementComponentreaches the wheeled vehicle’s settings.- Binding
Completedto the same function asTriggeredsends the final value of the axis (zero) when the key is released, so the car stops steering and accelerating. - The car’s mesh simulates physics and uses the Vehicle collision profile, set in class defaults:
mesh.set_simulate_physics(true),mesh.set_collision_profile_name(FName::new("Vehicle").handle(), Some(true)).
The Blueprint child (BP_SportsCar_Pawn in the template) assigns the skeletal mesh, its physics asset, the Animation Blueprint that turns the wheels, and the input actions.
Use case: engine, gearbox and steering
The goal: tune how the car accelerates, shifts and turns, in code, as the C++ template’s constructor does.
The movement component’s setups are structs: EngineSetup, TransmissionSetup, SteeringSetup, DifferentialSetup. Read one, change its fields, and write it back, in class defaults:
#![allow(unused)]
fn main() {
use bindings::chaos_vehicles::{
ChaosVehicleMovementComponentExt, ChaosWheeledVehicleMovementComponentExt, ESteeringType,
FVehicleEngineConfigExt, FVehicleSteeringConfigExt, FVehicleTransmissionConfigExt
};
#[uclass(parent = Car)]
pub struct SportsCar {}
#[uclass_impl]
impl SportsCar {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let movement = Car::from_obj(self.as_ref())?.movement()?;
movement.set_chassis_height(144.0);
movement.set_drag_coefficient(0.31);
let engine = movement.get_engine_setup();
engine.as_ref().set_max_torque(750.0);
engine.as_ref().set_max_rpm(7000.0);
engine.as_ref().set_engine_idle_rpm(900.0);
movement.set_engine_setup(&engine);
let transmission = movement.get_transmission_setup();
let gears = transmission.as_ref();
gears.set_use_automatic_gears(true);
gears.set_final_ratio(2.81);
gears.set_change_up_rpm(6000.0);
gears.set_change_down_rpm(2000.0);
gears.forward_gear_ratios().set_all(&[4.25, 2.52, 1.66, 1.22, 1.0])?;
gears.reverse_gear_ratios().set_all(&[4.04])?;
movement.set_transmission_setup(&transmission);
let steering = movement.get_steering_setup();
steering.as_ref().set_steering_type(ESteeringType::Ackermann);
steering.as_ref().set_angle_ratio(0.7);
movement.set_steering_setup(&steering);
Ok(())
}
}
}
- The getter returns a copy of the struct; the setter writes the whole copy back. A field the code does not set keeps the engine’s default.
- An array inside a struct (
forward_gear_ratios) is aUeArrayview into the copy:set_allreplaces it,pushadds to it. - The template keeps these steps in a small
setup.rsmodule, with one function per setup, so each car’s class defaults read as a list of values.
Use case: the wheels
The goal: front wheels that steer and rear wheels that drive, each car with its own tires.
A wheel is a class whose parent is ChaosVehicleWheel, configured in class defaults. The movement component creates one wheel object per entry of its WheelSetups, from the class and the bone the entry names:
#![allow(unused)]
fn main() {
use bindings::chaos_vehicles::{
ChaosVehicleWheel, ChaosVehicleWheelExt, ChaosWheeledVehicleMovementComponentExt, EAxleType,
FChaosWheelSetup, FChaosWheelSetupExt
};
#[uclass(parent = ChaosVehicleWheel)]
pub struct FrontWheel {}
#[uclass_impl]
impl FrontWheel {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
let wheel = self.as_ref().checked()?;
wheel.set_axle_type(EAxleType::Front);
wheel.set_affected_by_steering(true);
wheel.set_max_steer_angle(40.0);
wheel.set_wheel_radius(39.0);
wheel.set_wheel_width(35.0);
Ok(())
}
}
fn set_wheels(movement: &Checked<ChaosWheeledVehicleMovementComponent>) -> RustealResult<()> {
let front = SubclassOf::<FrontWheel>::base().upcast_to::<ChaosVehicleWheel>();
let rear = SubclassOf::<RearWheel>::base().upcast_to::<ChaosVehicleWheel>();
let setups = movement.wheel_setups();
setups.clear()?;
for (class, bone) in [
(front, "Phys_Wheel_FL"),
(front, "Phys_Wheel_FR"),
(rear, "Phys_Wheel_BL"),
(rear, "Phys_Wheel_BR"),
] {
let setup = OwnedStruct::<FChaosWheelSetup>::new();
setup.as_ref().set_wheel_class(class);
setup.as_ref().set_bone_name(FName::new(bone).handle());
setups.push(&setup)?;
}
Ok(())
}
}
The bone names are the wheel bones of the car’s skeletal mesh. set_wheels runs in the car’s class defaults, next to the engine setup.
Use case: two cameras
The goal: a chase camera behind the car that the player can turn with the mouse and that recenters on its own, and a hood camera, toggled with a key.
#![allow(unused)]
fn main() {
#[uclass(parent = WheeledVehiclePawn)]
pub struct Car {
#[component(attach = "mesh", name = "Front Spring Arm")]
front_spring_arm: SpringArmComponent,
#[component(attach = "front_spring_arm", name = "Front Camera")]
front_camera: CameraComponent,
#[component(attach = "mesh", name = "Back Spring Arm")]
back_spring_arm: SpringArmComponent,
#[component(attach = "back_spring_arm", name = "Back Camera")]
back_camera: CameraComponent,
front_camera_active: bool,
}
}
The front camera starts inactive (set_auto_activate(false) in class defaults). Toggling activates one and deactivates the other:
#![allow(unused)]
fn main() {
#[ufunction(BlueprintCallable)]
pub(crate) fn do_toggle_camera(&mut self) {
let front_active = !self.front_camera_active();
self.set_front_camera_active(front_active);
if let Ok(front) = self.front_camera().and_then(|camera| camera.checked()) {
front.set_active(front_active, None);
}
if let Ok(back) = self.back_camera().and_then(|camera| camera.checked()) {
back.set_active(!front_active, None);
}
}
}
Looking around turns the back spring arm (k2_add_local_rotation), and every frame it turns back toward the front:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_tick(&mut self, delta_seconds: f32) {
let Ok(arm) = self.back_spring_arm().and_then(|arm| arm.checked()) else {
return;
};
let yaw = arm.get_relative_rotation().to_rotator().yaw;
let yaw = model::f_interp_to(yaw, 0.0, f64::from(delta_seconds), 1.0);
arm.k2_set_relative_rotation(&FRotator::from_rotator(Rotator::new(0.0, yaw, 0.0)), false, false);
}
}
f_interp_to is the engine’s FMath::FInterpTo written in plain Rust in the template’s model.rs, where it is tested.
Use case: putting a flipped car back on its wheels
The goal: a car upside down for two checks in a row resets itself; a key resets it at any time.
A looping timer checks the car every few seconds:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
KismetSystemLibrary::k2_set_timer(self.as_ref().upcast_to(), "FlippedCheck", 3.0, true, None, None, None);
}
#[ufunction]
fn flipped_check(&mut self) {
let Ok(mesh) = self.as_ref().checked().and_then(|me| me.get_mesh().checked()) else {
return;
};
let up_dot = DVec3::Z.dot(mesh.get_up_vector().to_dvec3()) as f32;
let check = model::flip_check(self.previous_flip_check(), up_dot, -0.2);
if check.reset {
self.do_reset_vehicle();
}
self.set_previous_flip_check(check.flipped);
}
}
The reset lifts the car, keeps its yaw, teleports it and clears its velocity:
#![allow(unused)]
fn main() {
fn reset(&self) -> RustealResult<()> {
let me = self.as_ref().checked()?;
let location = me.k2_get_actor_location().to_dvec3() + DVec3::Z * 50.0;
let yaw = me.k2_get_actor_rotation().to_rotator().yaw;
let transform = KismetMathLibrary::make_transform(
&FVector::from_dvec3(location),
&FRotator::from_rotator(Rotator::new(0.0, yaw, 0.0)),
&FVector::from_dvec3(DVec3::ONE),
);
me.k2_set_actor_transform(&transform, false, true);
let mesh = me.get_mesh().checked()?;
let zero = FVector::from_dvec3(DVec3::ZERO);
mesh.set_physics_angular_velocity_in_degrees(&zero, None, None);
mesh.set_physics_linear_velocity(&zero, None, None);
Ok(())
}
}
The last argument of k2_set_actor_transform is the teleport flag: the physics body moves with the actor instead of being swept there.
Use case: speed and gear on screen
The goal: a HUD showing the speed, in km/h or mph, and the current gear.
The widget class declares what it can be told; its Widget Blueprint draws it:
#![allow(unused)]
fn main() {
#[uclass(parent = UserWidget)]
pub struct VehicleAdvUI {
#[uproperty(EditAnywhere, BlueprintReadOnly, name = "bIsMPH", category = "Vehicle", default = false)]
b_is_mph: bool,
}
#[uclass_impl]
impl VehicleAdvUI {
#[ufunction(BlueprintImplementableEvent)]
fn on_speed_update(&self, new_speed: f32) {}
#[ufunction(BlueprintImplementableEvent)]
fn on_gear_update(&self, new_gear: i32) {}
}
impl VehicleAdvUI {
pub fn update_speed(&self, forward_speed: f32) {
self.on_speed_update(model::display_speed(forward_speed, self.b_is_mph()));
}
pub fn update_gear(&self, gear: i32) {
self.on_gear_update(gear);
}
}
}
The player controller creates it and feeds it every frame:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_tick(&mut self, _delta_seconds: f32) {
let (Ok(car), Ok(ui)) = (Car::from_obj(self.vehicle_pawn()), VehicleAdvUI::from_obj(self.vehicle_ui())) else {
return;
};
if let Ok(movement) = car.movement() {
ui.update_speed(movement.get_forward_speed());
ui.update_gear(movement.get_current_gear());
}
}
}
get_forward_speed is in centimeters per second; display_speed multiplies it by 0.036 for km/h or 0.022 for mph, as the C++ template does.
Screenshot: the vehicle template in play, with the gear and the speed in the corner of the screen. (images/recipes/vehicle_hud.png)
Use case: a time trial
The goal: a countdown, gates the car must pass in order, laps, and the best lap time.
The time trial variant uses four classes:
| Class | Parent | What it does |
|---|---|---|
TimeTrialTrackGate | Actor | A box on the track; it knows the next gate, and whether it is the finish line. |
TimeTrialGameMode | GameModeBase | Finds the finish line by its tag when the race begins. |
TimeTrialPlayerController | PlayerController | Holds the gate the player must pass next and counts the laps. |
TimeTrialUI, TimeTrialStartUI | UserWidget | The countdown, the laps and the best time. |
A gate checks that the car passing it is going to the right gate:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_actor_begin_overlap(&mut self, other_actor: UObjectRef<Actor>) {
let Ok(other) = other_actor.checked() else {
return;
};
let Ok(mut controller) = TimeTrialPlayerController::from_obj(other.get_instigator_controller()) else {
return;
};
let me: UObjectRef<TimeTrialTrackGate> = self.as_ref().cast().unwrap_or_default();
if controller.target_gate() != me {
return;
}
controller.set_target_gate(self.next_marker());
if self.b_is_finish_line() {
controller.increment_lap_count();
}
}
}
The countdown is a widget animation in the Blueprint; when it ends, the Blueprint calls Finish Countdown, and two delegates carry the start to the controller:
#![allow(unused)]
fn main() {
#[uclass(parent = UserWidget)]
pub struct TimeTrialStartUI {}
#[uclass_impl]
impl TimeTrialStartUI {
#[udelegate]
pub fn on_countdown_finished(&self) {}
#[ufunction(BlueprintImplementableEvent, name = "BP_StartCountdown")]
fn bp_start_countdown(&self) {}
#[ufunction(BlueprintCallable)]
fn finish_countdown(&mut self) {
self.on_countdown_finished();
}
}
}
The lap UI creates the countdown when it is constructed, listens to it, and passes the start on with its own delegate, which the player controller listens to:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn construct(&mut self) {
if let Err(e) = self.start_countdown() {
log::error!("[TimeTrial] could not start the countdown: {e}");
}
}
fn start_countdown(&mut self) -> RustealResult<()> {
let owning_player = self.as_ref().checked()?.get_owning_player();
let start_ui = create_widget_of_class(&owning_player, self.start_ui_class())?;
start_ui.upcast_to::<UserWidget>().checked()?.add_to_viewport(Some(0));
let countdown = TimeTrialStartUI::from_obj(start_ui)?;
countdown.on_countdown_finished_add_ufunction(&self.as_ref(), "StartRace")?;
countdown.start_countdown();
Ok(())
}
}
#![allow(unused)]
fn main() {
TimeTrialUI::from_obj(widget)?.on_race_start_add_ufunction(&controller, "StartRace")?;
}
Until the race starts, the controller keeps the car’s input off (pawn.disable_input(controller) when it possesses it, enable_input in StartRace). The lap times are plain Rust in time_trial/model.rs, with tests:
#![allow(unused)]
fn main() {
impl LapTimes {
pub fn start_lap(&mut self, lap: i32, lap_start_time: f32) {
let lap_time = lap_start_time - self.last_lap_time;
self.best_lap_time = if lap <= 1 {
-1.0
} else if self.best_lap_time < 0.0 || lap_time < self.best_lap_time {
lap_time
} else {
self.best_lap_time
};
self.current_lap = lap;
self.lap_start_time = lap_start_time;
self.last_lap_time = lap_start_time;
}
}
}
In the editor
The gates are placed in the level, each one’s Next Marker set to the next gate in the Details panel, and the finish line has Is Finish Line checked and the tag the game mode’s Finish Tag names.
Screenshot: a track gate selected in the level, with Next Marker and Is Finish Line in the Details panel. (images/recipes/track_gate.png)
Virtual reality
The vr template is the engine’s Virtual Reality template with its gameplay in Rust: an OpenXR pawn that teleports along the navigation mesh and snap turns, hands that grab objects and take poses from the controllers, a pistol, a 3D menu, passthrough and a spectator camera.
rusteal new MyVR --template vr
Open the project and press Play > VR Preview with an OpenXR runtime and a headset connected. Without a headset, Play in the viewport runs the same level with a non-tracked pawn.
The project binds the XR modules, the navigation system the teleports land on and Niagara, which the template turns on (see Binding more of the engine):
features = ["core", "engine", "input", "slate", "umg", "enhanced-input", "xr", "openxr", "ai", "niagara"]
[codegen.modules]
HeadMountedDisplay = { module = "head_mounted_display", feature = "xr" }
XRBase = { module = "xr_base", feature = "xr", plugin = "XRBase" }
OpenXRHMD = { module = "openxr", feature = "openxr", plugin = "OpenXR" }
head_mounted_display has the motion controller component; xr_base the headset’s function library (HeadMountedDisplayFunctionLibrary), the loading screen and the device visualizations; openxr the passthrough blend modes.
Use case: a pawn with a headset and two controllers
The goal: the camera follows the headset and four motion controllers follow the hands, two at the grip and two at the aim pose.
#![allow(unused)]
fn main() {
use bindings::engine::{CameraComponent, Pawn, SceneComponent};
use bindings::head_mounted_display::{MotionControllerComponent, MotionControllerComponentExt};
#[uclass(parent = Pawn)]
pub struct XRPawn {
#[component(root, name = "VROrigin")]
vr_origin: SceneComponent,
#[component(attach = "vr_origin")]
camera: CameraComponent,
#[component(attach = "vr_origin")]
motion_controller_left_grip: MotionControllerComponent,
#[component(attach = "vr_origin")]
motion_controller_right_aim: MotionControllerComponent,
}
#[uclass_impl]
impl XRPawn {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
self.motion_controller_left_grip()?
.checked()?
.set_tracking_motion_source(FName::new("LeftGrip").handle());
self.motion_controller_right_aim()?
.checked()?
.set_tracking_motion_source(FName::new("RightAim").handle());
Ok(())
}
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
if HeadMountedDisplayFunctionLibrary::is_head_mounted_display_enabled() {
HeadMountedDisplayFunctionLibrary::set_tracking_origin(EHMDTrackingOrigin::LocalFloor);
}
}
}
}
The camera of a pawn whose root sits on the floor (LocalFloor) is moved by the headset; the motion controllers by the controllers. get_motion_source() reads which pose a controller follows (LeftGrip, RightAim…), which tells the code which hand it is.
Use case: teleporting along an arc
The goal: the right thumbstick aims an arc; where it lands on the navigation mesh, the player can teleport, with their head over the spot.
The arc is a projectile path traced against the world, and its end is projected onto the navigation mesh:
#![allow(unused)]
fn main() {
let (_, hit, path, _) = GameplayStatics::blueprint_predict_projectile_path_by_object_type(
me.upcast_to(),
&FVector::from_dvec3(start),
&FVector::from_dvec3(forward * 650.0),
true,
3.6,
&[EObjectTypeQuery::ObjectTypeQuery1],
false,
&[],
EDrawDebugTrace::None,
0.0,
Some(15.0),
Some(2.0),
Some(0.0),
);
let (valid, nav_point) = NavigationSystemV1::k2_project_point_to_navigation(
me.upcast_to(),
&FVector::from_dvec3(hit.as_ref().get_location().to_dvec3()),
UObjectRef::null(),
SubclassOf::null(),
&self.teleport_project_point_to_navigation_query_extent(),
);
}
The path’s points go to the arc’s Niagara system as an array parameter:
#![allow(unused)]
fn main() {
NiagaraDataInterfaceArrayFunctionLibrary::set_niagara_array_vector(
self.teleport_trace_niagara_system()?,
FName::new("User.PointArray").handle(),
&positions,
);
}
The teleport keeps the player’s head over the target: the pawn moves by the target minus the camera’s offset from the pawn, rotated by the pawn’s yaw. The model function, tested without the engine:
#![allow(unused)]
fn main() {
pub fn teleport_destination(projected: DVec3, camera_relative: DVec3, actor_yaw: f64) -> DVec3 {
projected - rotate_yaw(DVec3::new(camera_relative.x, camera_relative.y, 0.0), actor_yaw)
}
}
ObjectTypeQuery1 is WorldStatic: the arc lands on the level, not on objects. The template’s level has a Nav Mesh Bounds Volume around the floor and a Nav Modifier Volume where teleports are not allowed.
Use case: snap turning around the head
The goal: the left thumbstick turns the player by 45 degrees without moving their head.
#![allow(unused)]
fn main() {
let delta = model::snap_turn_delta(f64::from(self.snap_turn_degrees()), right_turn);
let camera_world = camera.k2_get_component_location().to_dvec3();
let camera_relative = camera.get_relative_location().to_dvec3();
let new_yaw = me.k2_get_actor_rotation().to_rotator().yaw + delta;
me.k2_add_actor_world_rotation(&FRotator::from_rotator(Rotator::new(0.0, delta, 0.0)), false, true);
me.k2_set_actor_location(
&FVector::from_dvec3(model::snap_turn_location(camera_world, camera_relative, new_yaw)),
false,
true,
);
}
Rotating the pawn alone would swing the head around the pawn’s origin; moving the pawn by the difference keeps the head where it was.
Use case: grabbing objects
The goal: any object with a grab component can be picked up with the grip; some snap into the hand, others keep the hold they were taken with.
The grab component is a SceneComponent attached to the object’s physics body. Its grab type is a #[uenum], and it tells the object when it is grabbed and dropped with #[udelegate]s:
#![allow(unused)]
fn main() {
#[uenum]
pub enum EGrabType {
None = 0,
Snap = 1,
Custom = 3,
Free = 4,
}
#[uclass(parent = SceneComponent)]
pub struct GrabComponent {
#[uproperty(EditAnywhere, BlueprintReadWrite, category = "Grab", default = EGrabType::Free)]
grab_type: EGrabType,
#[uproperty(BlueprintReadOnly, category = "Grab")]
motion_controller_ref: UObjectRef<MotionControllerComponent>,
#[uproperty(BlueprintReadOnly, category = "Grab")]
b_is_held: bool,
}
#[uclass_impl]
impl GrabComponent {
#[udelegate]
pub fn on_grabbed(&self) {}
#[udelegate]
pub fn on_dropped(&self) {}
#[ufunction(BlueprintCallable)]
pub fn try_grab(&mut self, motion_controller: UObjectRef<MotionControllerComponent>) -> bool {
match self.grab_type() {
EGrabType::Free | EGrabType::Snap => {
self.set_primitive_comp_physics(false);
self.attach_parent_to_motion_controller(motion_controller);
self.set_b_is_held(true);
}
EGrabType::Custom => self.set_b_is_held(true),
EGrabType::None => {}
}
if self.b_is_held() {
self.set_motion_controller_ref(motion_controller);
self.on_grabbed();
}
self.b_is_held()
}
}
}
The pawn finds what a hand can take with a sphere trace for physics bodies at the grip, and picks the nearest grab component of the actor it hits:
#![allow(unused)]
fn main() {
let (hit, result) = KismetSystemLibrary::sphere_trace_single_for_objects(
me.upcast_to(), &point, &point, self.grab_radius_from_grip_position(),
&[EObjectTypeQuery::ObjectTypeQuery4], false, &[], EDrawDebugTrace::None, true, &white, &green, Some(1.0),
);
}
ObjectTypeQuery4 is PhysicsBody: grabbable objects use the PhysicsActor collision preset, which the grab component sets on its parent at begin play.
Use case: an object that reacts to being held
The goal: a pistol that takes the trigger input of the hand holding it, and hides that hand while it is held.
The pistol listens to its grab component’s delegates, enables input on itself and adds the hand’s weapon mapping context:
#![allow(unused)]
fn main() {
let grabbed = grab.on_grabbed_add(move || {
if let Ok(mut pistol) = Pistol::from_obj(me) {
let _ = pistol.grabbed();
}
});
}
#![allow(unused)]
fn main() {
fn grabbed(&mut self) -> RustealResult<()> {
let me = self.as_ref();
let controller = GameplayStatics::get_player_controller(me.upcast_to(), 0);
me.checked()?.enable_input(controller);
bind_action(&me, self.shoot_left_action(), ETriggerEvent::Started, "ShootLeft")?;
bind_action(&me, self.shoot_right_action(), ETriggerEvent::Started, "ShootRight")?;
let subsystem = enhanced_input_subsystem(controller)?.checked()?;
subsystem.add_mapping_context(self.weapon_context()?, 1, &OwnedStruct::new());
Ok(())
}
}
The pawn hides the hand through the template’s Blueprint Interface, BPI_PawnAnim, which the pawn implements (implements = ["/Game/XRFramework/Blueprints/BPI_PawnAnim.BPI_PawnAnim_C"]) and the pistol calls on whoever holds it, without knowing its class:
#![allow(unused)]
fn main() {
let mut call = DynamicCall::new(&owner, "HideUnhideHand")?;
call.set("bHide", true)?;
call.set("bRightHand", self.held_by_right()?)?;
call.call()?;
}
Firing spawns the projectile at the muzzle and plays a haptic effect on the hand holding the pistol:
#![allow(unused)]
fn main() {
controller.play_haptic_effect(self.pistol_fire_haptic_effect(), self.grab()?.get_held_by_hand(), Some(1.0), Some(false));
}
Use case: a 3D menu
The goal: a menu the player opens with the menu button, floating in front of the controller, used with the other hand as a laser pointer or with the thumbstick as a cursor.
The menu is an actor with a Widget component showing a Widget Blueprint, and the pawn’s two Widget Interaction components (pointer index 0 and 1) are what press its buttons:
#![allow(unused)]
fn main() {
fn press(interaction: UObjectRef<WidgetInteractionComponent>, pressed: bool) {
let Ok(interaction) = interaction.checked() else {
return;
};
let key = FKey::named("LeftMouseButton");
if pressed {
interaction.press_pointer_key(&key);
} else {
interaction.release_pointer_key(&key);
}
}
}
The other hand’s interaction component reports whether it points at the menu (is_over_hit_test_visible_widget) and where (get_last_hit_result): the menu draws the laser there; otherwise the menu hand’s thumbstick moves a cursor over the widget, its value read with EnhancedInputLibrary::get_bound_action_value. The menu’s buttons are bound with BindWidget, as in UI.
The menu opens and closes by scaling its widget over 0.2 seconds, a Blueprint timeline that is a few lines of state played in the actor’s tick (model::Timeline).
Use case: hand poses from the controllers
The goal: the hands grasp, curl the index finger, point and raise the thumb as the player does on the controller.
The pawn binds the hand input actions and sets the targets on its hands’ Anim Blueprint, whose logic is in a Rust AnimInstance; see Animation.
Use case: passthrough
The goal: a menu button switches between the virtual room and the real one, on headsets that show the camera feed.
#![allow(unused)]
fn main() {
if OpenXRBlueprintFunctionLibrary::get_environment_blend_mode() != EOpenXREnvironmentBlendMode::AlphaBlend {
OpenXRBlueprintFunctionLibrary::set_environment_blend_mode(EOpenXREnvironmentBlendMode::AlphaBlend);
self.hide_passthrough_actors(true);
} else {
OpenXRBlueprintFunctionLibrary::set_environment_blend_mode(EOpenXREnvironmentBlendMode::Opaque);
self.hide_passthrough_actors(false);
}
}
Actors tagged Passthrough are hidden in passthrough mode (and lose their collision, except those also tagged Floor). get_supported_environment_blend_modes() lists what the runtime supports; without AlphaBlend, passthrough needs the headset vendor’s OpenXR plugin.
Use case: a spectator camera
The goal: a second camera, on the monitor, that people around the player control with the keyboard and a gamepad.
The spectator is an actor with a Scene Capture Component 2D rendering to a render target the headset’s spectator screen shows:
#![allow(unused)]
fn main() {
if HeadMountedDisplayFunctionLibrary::is_head_mounted_display_enabled() {
HeadMountedDisplayFunctionLibrary::set_spectator_screen_mode(ESpectatorScreenMode::Texture);
HeadMountedDisplayFunctionLibrary::set_spectator_screen_texture(self.spectator_texture());
} else {
controller.set_view_target_with_blend(me.upcast_to(), Some(0.0), None, Some(0.0), Some(false));
}
}
Without a headset it becomes the view target, which makes it testable in the viewport. Its fade writes the capture’s post process Color Gain:
#![allow(unused)]
fn main() {
let settings = capture.get_post_process_settings();
settings.as_ref().set_override_color_gain(true);
settings.as_ref().set_color_gain(&FVector4::from_dvec4(DVec4::new(gain, gain, gain, 1.0)));
capture.set_post_process_settings(&settings);
}
Screenshot: the vr template’s level in the viewport, with the spectator camera, the cubes and the pistols. (images/recipes/vr_level.png)
Testing without a headset
On a machine without a headset, Play In Editor runs the level with the pawn untracked. The template’s code can be checked there by calling what the input would call: StartTeleportTrace, TeleportTrace, TryTeleport, SnapTurn, ToggleMenu on the pawn, Fire on a pistol, EnableVRSpectator on the spectator. The hand and menu input actions only fire with a headset’s controllers.
Augmented reality
The ar template is the engine’s Handheld AR template with its gameplay in Rust: a pawn that starts an AR session, shows the planes the device detects, places an object on the plane the player taps, and moves, rotates and scales it with one and two finger gestures, while the scene’s lights follow the room’s light. Its menus switch between three styles kept in a data table.
rusteal new MyAR --template ar
The template targets ARCore (Android) and ARKit (iOS) devices. Rusteal does not build for Android or iOS yet, so the project runs in the editor, where there is no AR session: the menus, the gestures and the placing can be tried there by calling the pawn’s functions (see Testing without a device).
The project binds the engine’s AugmentedReality module, which the template turns on (see Binding more of the engine):
features = ["core", "engine", "input", "slate", "umg", "enhanced-input", "ar"]
[codegen.modules]
AugmentedReality = { module = "augmented_reality", feature = "ar" }
augmented_reality has the session functions (ARBlueprintLibrary), the tracked geometries (ARPlaneGeometry), the trace results and the light estimates.
Use case: starting the session
The goal: a button starts the AR session with the project’s session configuration, and the menu moves on once the session runs and has found planes.
#![allow(unused)]
fn main() {
#[ufunction]
fn start_session(&mut self) {
ARBlueprintLibrary::start_ar_session(self.session_config());
}
}
session_config is a UObjectRef<ARSessionConfig> property that the menu’s Blueprint sets to D_ARSessionConfig. The menu’s tick then waits for the session and for two seconds of scanning with at least one geometry:
#![allow(unused)]
fn main() {
if ARBlueprintLibrary::get_ar_session_status().as_ref().get_status() == EARSessionStatus::Running {
start.to_scanning_state();
self.set_session_has_started(true);
}
if f64::from(self.scan_timer()) > model::SCAN_SECONDS && !ARBlueprintLibrary::get_all_geometries().is_empty() {
start.to_select_plane_state();
}
}
Use case: reading the fingers
The goal: know, every frame, where the first two fingers are and when each one touches or leaves the screen.
The template polls the touches instead of binding input actions:
#![allow(unused)]
fn main() {
fn touch(&self, index: ETouchIndex) -> Touch {
let controller = GameplayStatics::get_player_controller(self.as_ref().upcast_to(), 0);
match controller.checked() {
Ok(controller) => {
let (x, y, pressed) = controller.get_input_touch_state(index);
Touch { pressed, location: DVec2::new(f64::from(x), f64::from(y)) }
}
Err(_) => Touch::default(),
}
}
}
The pawn keeps last frame’s Touch for each finger: a finger is pressed when it is down now and was not before, released the other way round. The pinch the device recognises is an analog key, read with get_input_analog_key_state(&FKey::named("Gesture_Pinch")).
Use case: telling a drag from a twist and a pinch
The goal: one finger held for a moment drags the object; two fingers twist it or pinch it, whichever the player started.
The recognition is plain Rust in model.rs, tested with cargo test. A gesture is recognised after 0.2 seconds; with two fingers, a mostly horizontal movement is a twist and a vertical one a pinch:
#![allow(unused)]
fn main() {
self.recognised = true;
self.current = if fingers_direction.y.abs() < 0.5 {
EGestureType::Rotation
} else {
EGestureType::Scale
};
}
Once recognised, the gesture drives the placed object until no finger has touched the screen for 0.1 seconds. Snapping, turned on in the options menu, rounds the location to 5 cm, the rotation to 20 degrees and the scale to 0.1.
Use case: placing an object on a plane
The goal: the first tap on a detected plane selects it; the second spawns the object where the finger touched it.
The tap becomes a ray from the camera through the finger:
#![allow(unused)]
fn main() {
let (_, origin, direction) = GameplayStatics::deproject_screen_to_world(
controller,
&FVector2D::from_dvec2(touch_location),
);
let start = origin.to_dvec3();
let end = start + direction.to_dvec3() * 1000.0;
}
traced with KismetSystemLibrary::line_trace_single_for_objects. When the actor hit is the selected plane, the pawn spawns the placeable there and tells the menus through its delegate:
#![allow(unused)]
fn main() {
let placeable = world.spawn_actor_of_class(self.placeable_class(), &transform)?;
Placeable::from_obj(placeable)?.set_bp_plane(self.selected_plane());
self.set_placeable_object(placeable);
self.ar_object_spawned();
}
ar_object_spawned is a #[udelegate(name = "ARObjectSpawned")] the main menu binds to show the bottom menu.
Use case: showing the planes the device finds
The goal: while the player looks for a surface, the plane at the center of the screen is drawn, a different color for each plane.
Every second the pawn traces the tracked geometries through the center of the screen and spawns a plane actor with the geometry’s transform and size:
#![allow(unused)]
fn main() {
let results = ARBlueprintLibrary::line_trace_tracked_objects(
&FVector2D::from_dvec2(center),
Some(false),
Some(false),
Some(true),
Some(true),
);
let geometry = ARTraceResultLibrary::get_tracked_geometry(first);
let plane_geometry = geometry.cast::<ARPlaneGeometry>()?.checked()?;
let plane = world.spawn_actor_of_class(self.plane_class(), &plane_geometry.get_local_to_world_transform())?;
}
The color is the geometry’s index among ARBlueprintLibrary::get_all_geometries(), modulo the ten colors of the pawn’s ColorArray.
Use case: lighting from the room
The goal: the virtual object is lit as brightly, and in the same tint, as the room the camera sees.
#![allow(unused)]
fn main() {
let estimate = ARBlueprintLibrary::get_current_light_estimate().cast::<ARBasicLightEstimate>();
let Ok(estimate) = estimate.and_then(|e| e.checked()) else {
return;
};
let intensity = (estimate.get_ambient_intensity_lumens() / 1000.0) as f32;
component.set_intensity(intensity);
component.set_light_color(&estimate.get_ambient_color(), Some(true));
}
The pawn does it every tick for the level’s directional light, and sets the sky light’s intensity too. Without a session there is no estimate, and the lights keep their values.
Use case: menu styles in a data table
The goal: the player switches the menus between a light, a dark and a game style; each style’s colors, font and icons are a row of a data table.
The row is a Rust struct, used as the data table’s row structure:
#![allow(unused)]
fn main() {
#[ustruct]
pub struct StructStyle {
#[uproperty(EditAnywhere)]
style: EUIStyle,
#[uproperty(EditAnywhere)]
text_color: OwnedStruct<FLinearColor>,
#[uproperty(EditAnywhere)]
font_size: i32,
#[uproperty(EditAnywhere)]
icons: UeArray<OwnedStruct<StructIcon>>,
}
}
Each menu reads the row of the style it is asked for:
#![allow(unused)]
fn main() {
for row_name in DataTableFunctionLibrary::get_data_table_row_names(table) {
let Some(row) = find_data_table_row::<StructStyle>(table, FName(row_name)) else {
continue;
};
if row.style() == current_style {
return row;
}
}
}
All the menus derive from a Rust StylizedUI widget with a SwitchStyle delegate; each binds its own function to it, and the main menu broadcasts the new style to them all. A data table made with a Blueprint struct moves to the Rust struct by filling it again from its JSON export with Fill Data Table from JSON String, the Rust struct as its row structure.
Screenshot: the ar template’s start menu in the editor viewport, with the Begin Scan button. (images/recipes/ar_menu.png)
Testing without a device
In the editor, Play In Editor runs the level with no AR session, and the start menu waits. A plane actor placed in the level stands in for a detected one: HandleTouchHit(plane, location) on the pawn selects it, a second call places the object, and then MoveOnPlane, SetPlaceableRotation and SetPlaceableScale on the placeable, ToggleSnap, ToggleHUD, SwitchStyle and ResetPlaceable on the main menu do what the gestures and buttons would.
Game flow
Use case: the game mode, pawn and controller in Rust
The goal: the level runs your Rust game mode, which spawns your character and controller.
#![allow(unused)]
fn main() {
use bindings::engine::GameModeBase;
#[uclass(parent = GameModeBase)]
pub struct MyGameMode {}
#[uclass_impl]
impl MyGameMode {}
}
The game mode’s Blueprint child names the classes: Default Pawn Class, Player Controller Class, HUD Class. The level, or the project, uses that Blueprint. See Blueprint children, “Game mode, pawn and controller”.
A game mode with rules overrides its events (receive_begin_play, choose_player_start) and holds the match’s state, as the templates do with scores and counters. Any actor reaches it:
#![allow(unused)]
fn main() {
let game_mode = GameplayStatics::get_game_mode(self.as_ref().upcast_to());
if let Ok(mut game_mode) = MyGameMode::from_obj(game_mode) {
}
}
Use case: choosing where each player starts
The goal: player one starts at the start tagged Player0, player two at Player1, and any start if none is tagged.
#![allow(unused)]
fn main() {
use bindings::engine::{Controller, KismetMathLibrary, PlayerStart};
#[uclass(parent = GameModeBase)]
pub struct MyGameMode {
next_player_start: i32,
}
#[uclass_impl]
impl MyGameMode {
#[ufunction(Override)]
fn choose_player_start(&mut self, _player: UObjectRef<Controller>) -> UObjectRef<Actor> {
let world = self.as_ref().upcast_to();
let class = SubclassOf::<PlayerStart>::base().upcast_to::<Actor>();
let tag = FName::new(&format!("Player{}", self.next_player_start()));
let mut starts = GameplayStatics::get_all_actors_of_class_with_tag(world, class, tag.handle());
self.set_next_player_start(self.next_player_start() + 1);
if starts.is_empty() {
starts = GameplayStatics::get_all_actors_of_class(world, class);
}
if starts.is_empty() {
return UObjectRef::null();
}
let index = KismetMathLibrary::random_integer_in_range(0, starts.len() as i32 - 1);
starts[index as usize]
}
}
}
Tag a Player Start in the level by adding the tag to Tags in its Details panel, under Actor.
Use case: local multiplayer
The goal: two to four players on one screen, split.
#![allow(unused)]
fn main() {
#[uclass(parent = GameModeBase)]
pub struct CoopGameMode {
#[uproperty(EditDefaultsOnly, default = 2)]
number_of_local_players: i32,
}
#[uclass_impl]
impl CoopGameMode {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
for _ in 2..=self.number_of_local_players() {
GameplayStatics::create_player(self.as_ref().upcast_to(), Some(-1), Some(true));
}
}
}
}
create_player adds a local player with its own controller and pawn; the engine splits the screen. Each player controller adds its own mapping contexts and widgets (add_to_player_screen puts a widget on that player’s part of the screen). GameplayStatics::get_num_local_player_controllers and get_player_pawn(world, index) reach each one.
Use case: a respawn
See Health, damage and death, “Respawning the player”.
Use case: one actor switching others
The goal: stepping on a trigger switches the lights wired to it and unlocks a door, which the next step locks again.
The trigger declares an event and broadcasts it on every overlap:
#![allow(unused)]
fn main() {
#[uclass_impl]
impl TriggerSwitch {
#[udelegate(name = "Trigger")]
pub fn trigger(&self) {}
#[ufunction(Override)]
fn receive_actor_begin_overlap(&mut self, _other_actor: UObjectRef<Actor>) {
self.trigger();
self.set_is_on(!self.is_on());
}
}
}
What it drives holds a reference to it, set on each placed actor in the Details panel, and binds one of its functions when play begins:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_begin_play(&mut self) {
let me = self.as_ref();
if let Ok(trigger) = TriggerSwitch::from_obj(self.trigger()) {
let _ = trigger.trigger_add_ufunction(&me, "Unlock");
}
}
#[ufunction]
fn unlock(&mut self) {
self.set_unlocked(!self.unlocked());
if self.unlocked() {
self.open_door();
} else {
self.close_door();
}
}
}
The interactables feature has the trigger (BP_DoorTrigger), lights (BP_Light), a door (BP_DoorFrame_Unlockable) and a confetti burst (BP_ConfettiTrigger) wired this way.
Use case: a door that slides open
The goal: a door that opens in 0.3 seconds as a player comes near and closes when the last one leaves, eased like the Blueprint timeline it replaces.
The timeline’s curve is a few keys with their tangents, evaluated in Rust, and its playhead moves in the actor’s tick:
#![allow(unused)]
fn main() {
const DOOR_CONTROL: [Key; 2] = [
curve::key(0.0, 0.0, 0.0, 7.314_739),
curve::key(0.3, 1.0, 0.0, 0.0),
];
#[ufunction(Override)]
fn receive_tick(&mut self, delta_seconds: f32) {
let mut timeline = self.timeline();
if !timeline.is_playing() {
return;
}
let position = timeline.advance(f64::from(delta_seconds), DOOR_CONTROL_LENGTH);
self.set_timeline(timeline);
self.move_doors(curve::eval(&DOOR_CONTROL, position));
}
}
timeline.play() runs it forward from where it is, timeline.reverse() back, so a player who turns around mid-way closes the door from there. The door’s frame and leaves are built in the construction script (user_construction_script) from its Door Size, Split Door and Door Thickness, with add_component_by_class.
Use case: changing level
#![allow(unused)]
fn main() {
GameplayStatics::open_level(self.as_ref().upcast_to(), FName::new("Lvl_Arena").handle(), Some(true), None);
}
Everything in the old level is destroyed, the game mode included; what must survive a level change lives in a game instance subsystem (see Subsystems) or a save.
Saving the game
The goal: keep the player’s progress between sessions.
A save game is a Rust class whose parent is SaveGame, written to and read from a slot with the engine’s GameplayStatics:
#![allow(unused)]
fn main() {
use bindings::engine::{GameplayStatics, SaveGame};
#[uclass(parent = SaveGame)]
pub struct Progress {
#[uproperty(SaveGame)]
unlocked_level: i32,
#[uproperty(SaveGame)]
best_times: UeArray<f32>,
#[uproperty(SaveGame)]
player_name: String,
}
#[uclass_impl]
impl Progress {}
const SLOT: &str = "progress";
pub fn save_progress(level: i32) -> RustealResult<()> {
let save = GameplayStatics::create_save_game_object(SubclassOf::<Progress>::base().upcast());
let mut progress = Progress::from_obj(save)?;
progress.set_unlocked_level(level);
if !GameplayStatics::save_game_to_slot(save, SLOT, 0) {
return Err(RustealError::invalid_operation("the save failed"));
}
Ok(())
}
pub fn load_progress() -> Option<Progress> {
if !GameplayStatics::does_save_game_exist(SLOT, 0) {
return None;
}
Progress::from_obj(GameplayStatics::load_game_from_slot(SLOT, 0)).ok()
}
}
create_save_game_objectmakes an empty save of the class;save_game_to_slotwrites all its properties to the slot;load_game_from_slotreads it back as a new object.- The second argument of each is the user index,
0for a single player. - Saves go to
Saved/SaveGames/<slot>.savin the project during development, and to the platform’s save location in a packaged game.
Use case: checkpoints
The goal: walking past a spawn point saves the player’s progress; the next time the level starts, the player starts there.
The checkpoints feature keeps the save in a world subsystem, which loads it when the level begins and moves the player:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_world_begin_play(&mut self) {
self.load_game_data();
self.respawn_player();
}
}
load_game_data loads the slot, or creates and saves an empty CheckpointSaveData when there is none; respawn_player moves the pawn and its controller’s rotation to the saved spawn point when the save is for this level. Spawn points tell the subsystem when the player walks into them:
#![allow(unused)]
fn main() {
#[ufunction(Override)]
fn receive_actor_begin_overlap(&mut self, other_actor: UObjectRef<Actor>) {
let me = self.as_ref();
let player = GameplayStatics::get_player_pawn(me.upcast_to(), 0);
if other_actor != player.upcast_to() {
return;
}
if let Ok(mut checkpoints) = CheckpointSubsystem::get(me.upcast_to()) {
checkpoints.update_spawn_point(me.upcast_to());
}
}
}
The save keeps the level’s name and the spawn point’s location and rotation, so it holds when the spawn point moves between sessions. Respawn Player and Clear Checkpoint are callable from Blueprints, for a fall into the void or a new game.
The SaveGame specifier
save_game_to_slot writes a SaveGame object whole, flagged or not. The SaveGame specifier matters when the object is serialized some other way: an archive that is a save game archive (the usual way to save actors’ state, for example in a save system that walks the level’s actors) writes only the properties that carry it. Marking the fields keeps them right in both cases.
#![allow(unused)]
fn main() {
#[uclass(parent = Actor)]
pub struct Chest {
#[uproperty(EditAnywhere, SaveGame)]
b_opened: bool,
}
}
Hot reload
A hot reload keeps the save game class’s properties, as for any class. A save written with an older shape of the class loads the properties that still exist under the same name, as in C++.
An inventory
The goal: items defined as assets, an inventory component any actor can have, a delegate that tells the UI when it changes, and limits set in Project Settings.
This is what the inventory plugin template builds:
rusteal plugin new Inventory --template inventory
The same code works in a game’s crate. The pieces:
Items as data assets
#![allow(unused)]
fn main() {
use bindings::engine::PrimaryDataAsset;
#[uclass(parent = PrimaryDataAsset)]
pub struct InventoryItem {
#[uproperty(EditAnywhere, BlueprintReadOnly, category = "Item")]
display_name: String,
#[uproperty(EditAnywhere, BlueprintReadOnly, category = "Item", default = 1)]
max_stack: i32,
#[uproperty(EditAnywhere, BlueprintReadOnly, category = "Item", default = 0.0)]
weight: f32,
}
}
Each item is an asset of this class: see Data assets for creating them in the editor.
The component
#![allow(unused)]
fn main() {
use bindings::engine::ActorComponent;
#[uclass(parent = ActorComponent)]
pub struct InventoryComponent {
#[uproperty(VisibleAnywhere, BlueprintReadOnly, category = "Inventory")]
items: UeArray<UObjectRef<InventoryItem>>,
#[uproperty(VisibleAnywhere, BlueprintReadOnly, category = "Inventory")]
counts: UeArray<i32>,
}
#[uclass_impl]
impl InventoryComponent {
#[udelegate]
pub fn on_inventory_changed(&self, item: UObjectRef<InventoryItem>, count: i32) {}
#[ufunction]
pub fn add_item(&mut self, item: UObjectRef<InventoryItem>, count: i32) -> i32 {
let added = self.try_add(item, count).unwrap_or(0);
if added > 0 {
self.on_inventory_changed(item, self.count_of(item));
}
added
}
#[ufunction(BlueprintPure)]
pub fn count_of(&self, item: UObjectRef<InventoryItem>) -> i32 {
self.stacks()
.map(|stacks| stacks.iter().filter(|(i, _)| *i == item).map(|(_, c)| c).sum())
.unwrap_or(0)
}
}
impl InventoryComponent {
pub fn stacks(&self) -> RustealResult<Vec<(UObjectRef<InventoryItem>, i32)>> {
let items = self.items().to_vec()?;
let counts = self.counts().to_vec()?;
Ok(items.into_iter().zip(counts).collect())
}
fn try_add(&mut self, item: UObjectRef<InventoryItem>, count: i32) -> RustealResult<i32> {
let definition = InventoryItem::from_obj(item)?;
let settings = InventorySettings::get_default()?;
let max_stack = definition.max_stack().max(1);
let (items, counts) = (self.items(), self.counts());
let mut left = count.max(0);
for index in 0..items.len()? {
if left == 0 {
break;
}
let held = counts.get(index)?;
if items.get(index)? == item && held < max_stack {
let put = left.min(max_stack - held);
counts.set(index, &(held + put))?;
left -= put;
}
}
while left > 0 && (items.len()? as i32) < settings.max_slots() {
let put = left.min(max_stack);
items.push(&item)?;
counts.push(&put)?;
left -= put;
}
Ok(count.max(0) - left)
}
}
}
- Two arrays side by side hold the stacks: properties cannot be maps yet.
on_inventory_changedis a delegate a UMG widget binds to redraw (see Delegates).add_itemandcount_ofare callable from Blueprints.
Giving any actor an inventory
A world subsystem finds an actor’s inventory, adding the component when asked:
#![allow(unused)]
fn main() {
use bindings::engine::{ActorComponent, ActorExt, SubsystemBlueprintLibrary, WorldSubsystem};
use bindings::rusteal::RustealWorldSubsystem;
#[uclass(parent = RustealWorldSubsystem)]
pub struct InventorySubsystem {}
#[uclass_impl]
impl InventorySubsystem {
#[ufunction]
pub fn inventory_of(&mut self, actor: UObjectRef<Actor>, create: bool) -> UObjectRef<InventoryComponent> {
find_or_add(actor, create).unwrap_or_default()
}
}
fn find_or_add(actor: UObjectRef<Actor>, create: bool) -> RustealResult<UObjectRef<InventoryComponent>> {
let actor = actor.checked()?;
let class = SubclassOf::<InventoryComponent>::base().upcast_to::<ActorComponent>();
let mut component = actor.get_component_by_class(class);
if !component.is_valid() && create {
let identity = FTransform::from_transform(Transform::IDENTITY);
component = actor.add_component_by_class(class, false, &identity, false);
}
component.cast::<InventoryComponent>()
}
}
See Subsystems for finding the subsystem itself.
Limits in Project Settings
#![allow(unused)]
fn main() {
use bindings::developer_settings::DeveloperSettings;
#[uclass(parent = DeveloperSettings, config = "Game")]
pub struct InventorySettings {
#[uproperty(Config, EditAnywhere, category = "Inventory", default = 20)]
max_slots: i32,
}
}
See Project settings.
Trying it from the console
The template registers give, take and list console commands (see Console commands and variables): press Play, open the console and type the command with an item’s path.
Subsystems
The goal: a system with one instance per game, per level or per player, created and destroyed by the engine, that any code can find: a wave director, a quest log, a score keeper, a music manager.
The engine creates one subsystem object for every class deriving from a subsystem base. A Rust subsystem derives from one of Rusteal’s bases, which turn the engine’s C++ virtuals into events:
| Parent | One per | Events |
|---|---|---|
RustealGameInstanceSubsystem | game session (survives level changes) | receive_initialize, receive_deinitialize, receive_should_create_subsystem |
RustealWorldSubsystem | world: each level, in the game and in Play In Editor | the same, plus receive_post_initialize, receive_world_begin_play, and receive_tick when it wants to tick |
RustealLocalPlayerSubsystem | local player | the same as the game’s, plus receive_player_controller_changed |
RustealEngineSubsystem | process | the same as the game’s |
They come from bindings::rusteal.
Use case: a wave director
The goal: a system that starts enemy waves when the level begins and counts down between them.
#![allow(unused)]
fn main() {
use bindings::rusteal::{RustealWorldSubsystem, RustealWorldSubsystemExt};
#[uclass(parent = RustealWorldSubsystem)]
pub struct WaveDirector {
#[uproperty]
wave: i32,
countdown: f32,
}
#[uclass_impl]
impl WaveDirector {
#[class_defaults]
fn class_defaults(&mut self) -> RustealResult<()> {
self.as_ref().checked()?.set_wants_tick(true);
Ok(())
}
#[ufunction(Override)]
fn receive_world_begin_play(&mut self) {
self.set_wave(1);
self.set_countdown(10.0);
}
#[ufunction(Override)]
fn receive_tick(&mut self, delta_seconds: f32) {
let countdown = self.countdown() - delta_seconds;
if countdown <= 0.0 {
self.set_wave(self.wave() + 1);
self.set_countdown(30.0);
} else {
self.set_countdown(countdown);
}
}
#[ufunction(BlueprintPure)]
pub fn current_wave(&self) -> i32 {
self.wave()
}
}
}
Finding a subsystem
From Rust, through the engine’s SubsystemBlueprintLibrary, with any object in the world as context:
#![allow(unused)]
fn main() {
use bindings::engine::{SubsystemBlueprintLibrary, WorldSubsystem};
impl WaveDirector {
pub fn get(context: UObjectRef<Object>) -> RustealResult<Self> {
let class = SubclassOf::<WaveDirector>::base().upcast_to::<WorldSubsystem>();
Self::from_obj(SubsystemBlueprintLibrary::get_world_subsystem(context, class))
}
}
let mut director = WaveDirector::get(self.as_ref().upcast_to())?;
}
get_game_instance_subsystem, get_local_player_subsystem and get_engine_subsystem find the other kinds.
Choosing the kind
- State of the current level, gone when the level changes: world.
- State that lives across levels (the player’s progress, settings loaded once, a connection): game instance.
- State per player in split screen: local player.
A world subsystem exists in the editor’s own world too only when it asks for it (bCreateInEditorWorlds); by default it exists in the game and in Play In Editor.
Project settings and config
The goal: values designers tune in Project Settings, saved in the project’s ini files, and read by the game.
#![allow(unused)]
fn main() {
use bindings::developer_settings::DeveloperSettings;
#[uclass(parent = DeveloperSettings, config = "Game")]
pub struct CombatSettings {
#[uproperty(Config, EditAnywhere, category = "Combat", default = 3)]
max_combo: i32,
#[uproperty(Config, EditAnywhere, category = "Combat", default = 0.4)]
combo_window: f32,
}
#[uclass_impl]
impl CombatSettings {}
}
- A class whose parent is
DeveloperSettingsand that names a config category shows in Project Settings. config = "Game"saves itsConfigproperties in the Game category’s files:Config/DefaultGame.inifor the project’s defaults.- The values live on the class default object, which
get_default()returns:
#![allow(unused)]
fn main() {
let settings = CombatSettings::get_default()?;
let window = settings.combo_window();
}
Binding the module
DeveloperSettings is its own engine module. A game binds it by adding it to [codegen.modules] in rusteal.toml, then running rusteal build --all:
DeveloperSettings = { module = "developer_settings", feature = "engine" }
Plugin templates already bind it.
In the editor
Open Edit > Project Settings. The class has a page of its own, named after the class, listed under Game by default. Changing a value there writes it to Config/DefaultGame.ini:
[/Script/Rusteal.CombatSettings]
MaxCombo=4
ComboWindow=0.35
The section is the class’s path, and the keys the properties’ Unreal names. A plugin’s settings use the plugin’s package ([/Script/Inventory.InventorySettings]).
Screenshot: the Project Settings page of a Rust DeveloperSettings class. (images/systems/project_settings_page.png)
Config on other classes
Any class can read properties from ini files: name a category in #[uclass(config = "...")] and mark the properties Config. The values are read into the class default object when the class is created, as in C++. Game, Engine, Input and GameUserSettings are the engine’s categories; any other name makes a category of its own (config = "Combat" reads Config/DefaultCombat.ini).
Data assets
The goal: a kind of asset of your own, items, recipes, enemy types, dialogue lines, that designers create and edit in the Content Browser.
#![allow(unused)]
fn main() {
use bindings::engine::{PrimaryDataAsset, StaticMesh, Texture2D};
#[uclass(parent = PrimaryDataAsset)]
pub struct EnemyType {
#[uproperty(EditAnywhere, BlueprintReadOnly, category = "Enemy")]
display_name: String,
#[uproperty(EditAnywhere, BlueprintReadOnly, category = "Enemy", default = 100.0)]
max_health: f32,
#[uproperty(EditAnywhere, BlueprintReadOnly, category = "Enemy")]
mesh: UObjectRef<StaticMesh>,
#[uproperty(EditAnywhere, BlueprintReadOnly, category = "Enemy")]
icon: UObjectRef<Texture2D>,
}
#[uclass_impl]
impl EnemyType {}
}
A class whose parent is PrimaryDataAsset is a data asset class: each asset of it is a set of values, edited in the editor and loaded by the game.
In the editor
- In the Content Browser, right-click and choose Miscellaneous > Data Asset.
- In the Pick Class For Data Asset Instance window, choose EnemyType and click Select.
- Name the asset (
DA_Grunt), open it, fill in its values, and Save.
Screenshot: the Pick Class For Data Asset Instance window with the Rust class selected. (images/systems/data_asset_class.png)
Using them
A property holds the asset a spawner or a level actor uses:
#![allow(unused)]
fn main() {
#[uproperty(EditAnywhere)]
enemy_type: UObjectRef<EnemyType>,
}
#![allow(unused)]
fn main() {
let enemy_type = EnemyType::from_obj(self.enemy_type())?;
let health = enemy_type.max_health();
}
Or the game loads one by path:
#![allow(unused)]
fn main() {
let grunt: UObjectRef<EnemyType> = load_object("/Game/Enemies/DA_Grunt.DA_Grunt")?;
}
An asset loaded only by path must be in a directory the cooker cooks: see Packaging a game.
Creating them in bulk
Editor Python creates and fills them, using the classes’ paths and the properties’ Unreal names:
import unreal
enemy_class = unreal.load_class(None, "/Script/Rusteal.EnemyType")
factory = unreal.DataAssetFactory()
factory.set_editor_property("data_asset_class", enemy_class)
tools = unreal.AssetToolsHelpers.get_asset_tools()
grunt = tools.create_asset("DA_Grunt", "/Game/Enemies", enemy_class, factory)
grunt.set_editor_property("MaxHealth", 80.0)
Console commands and variables
The console is a game’s quickest debugging tool: commands to give items, skip waves, spawn enemies; variables to turn on god mode or tune a value while playing. Rust adds both. (For Rusteal’s own commands, Rusteal.Reload and Rusteal.AutoReload, see Console commands.)
Use case: a cheat command
The goal: mygame.give_gold 500 in the console gives the player gold.
#![allow(unused)]
fn main() {
use bindings::core_ue::Object;
use bindings::engine::{GameplayStatics, World};
use bindings::prelude::*;
use rusteal_runtime::runtime::{CommandContext, RustealResult, UObjectRef, console};
fn startup() {
let result = console::register_command(
"mygame.give_gold",
"mygame.give_gold <amount>",
|args, context| {
let amount: i32 = args.first().and_then(|a| a.parse().ok()).unwrap_or(100);
if let Err(e) = give_gold(context, amount) {
log::error!("[MyGame] give_gold: {e}");
}
},
);
if let Err(e) = result {
log::error!("[MyGame] no console command: {e}");
}
}
rusteal_runtime::on_load!(startup);
fn give_gold(context: &CommandContext, amount: i32) -> RustealResult<()> {
let world: UObjectRef<World> = context.world()?;
let pawn = GameplayStatics::get_player_pawn(world.upcast_to::<Object>(), 0);
log::info!("[MyGame] giving {amount} gold to {}", pawn.get_name()?);
Ok(())
}
}
console::register_command(name, help, closure)adds the command. The closure receives the words after the command (["500"]) and aCommandContext, whoseworld()gives the world the command runs in, typed and checked: an error when the command runs with no world.on_load!(function)runs a function every time the library loads, hot reloads included. A library’s commands and variables are removed when it unloads and registered again when it loads, so they always run the current code.
Use case: a console variable
The goal: mygame.god 1 makes the player invulnerable.
#![allow(unused)]
fn main() {
use rusteal_runtime::runtime::ConsoleVariable;
pub const GOD_MODE: ConsoleVariable<bool> = ConsoleVariable::new("mygame.god");
fn startup() {
let _ = GOD_MODE.register("The player takes no damage", false);
}
rusteal_runtime::on_load!(startup);
}
#![allow(unused)]
fn main() {
if GOD_MODE.get().unwrap_or(false) {
return;
}
}
A ConsoleVariable<T> holds a bool, i32, f32 or String. It also reads and writes the engine’s own variables by name:
#![allow(unused)]
fn main() {
let _ = ConsoleVariable::<i32>::new("t.MaxFPS").set(60);
}
Use case: a command on the player controller
The goal: GiveGold 100 as a command of the player controller, as a C++ UFUNCTION(Exec) is.
#![allow(unused)]
fn main() {
#[uclass_impl]
impl MyPlayerController {
#[ufunction(Exec)]
fn give_gold(&mut self, amount: i32) {
}
}
}
A #[ufunction(Exec)] is a console command on the classes the console reaches: the player controller, its pawn, its HUD, the cheat manager, the game mode and the game state. The command is the function’s Unreal name, and its parameters come from the words after it.
In the editor
Press Play, open the console (` in the viewport, or the Cmd box of the Output Log), and type the command. Registered commands and variables show in the console’s autocomplete with their help text.
Work off the game thread
The goal: heavy work, pathfinding for hundreds of agents, procedural generation, parsing a big file, done on other cores, with the result applied to the game when it is ready.
Unreal’s objects belong to the game thread: engine calls are made from there. Everything else is plain Rust, and runs anywhere: threads, rayon, tokio. task::spawn connects the two:
#![allow(unused)]
fn main() {
use rusteal_runtime::runtime::task;
let seed = self.seed();
task::spawn(
move || generate_dungeon(seed),
move |dungeon| {
if let Ok(mut level) = LevelBuilder::from_obj(builder) {
level.place_rooms(&dungeon);
}
},
);
}
- The first closure runs on a thread of its own. It must not call the engine.
- The second runs on the game thread, on a frame after the first finishes, with its result. There, the engine can be called again.
Capture UObjectRefs, not Rust class values, in the second closure, and turn them back into the class with from_obj: the object may have been destroyed while the work ran.
Using every core
rayon spreads work over all cores inside the first closure:
#![allow(unused)]
fn main() {
use rayon::prelude::*;
task::spawn(
move || agents.par_iter().map(|agent| plan_path(agent, &grid)).collect::<Vec<_>>(),
move |paths| apply_paths(paths),
);
}
Add rayon to the game crate’s Cargo.toml like any dependency.
Other tools
| Function | What it does |
|---|---|
task::run_on_game_thread(closure) | Runs a closure on the game thread, on the next frame, from any thread. |
task::on_tick(closure) | Runs a closure every frame, the editor’s frames too, until the returned handle’s remove(). |
task::is_game_thread() | Whether the code runs on the game thread. |
#![allow(unused)]
fn main() {
let handle = task::on_tick(|delta_seconds| {
});
handle.remove();
}
Hot reload and shutdown
A library waits for its spawned work before it unloads (a hot reload, the editor closing), so no thread runs code that is gone. A result still waiting for the game thread at that moment is dropped. Keep spawned work short, or split it, so a reload does not wait for long.
A panic in the work is caught and logged, and the second closure does not run.
Plugins
A Rusteal plugin is an Unreal plugin whose code is a Rust library of its own, loaded next to the game’s. A gameplay system written once (an inventory, dialogue, quests, AI behaviors) goes into any Rusteal project as a plugin; an editor tool brings Rust into the editor: work on many assets at once, parsers and libraries from crates.io, all of it off the game thread when it is heavy.
| Kind | Runs in | What for |
|---|---|---|
runtime | games and the editor | gameplay systems, anything a packaged game needs |
editor | the editor only | tools: menus, asset validation, editor scripting |
Everything in Classes, The engine API and Systems applies to a plugin’s code: classes, properties, functions, components, delegates and interfaces are written the same way.
Where a plugin’s classes live
A plugin’s classes are in its module’s package, as a C++ plugin’s are: /Script/Inventory.InventoryComponent, where the game’s are in /Script/Rusteal. That is their path in assets, ini sections and editor Python:
unreal.load_class(None, "/Script/Inventory.InventoryItem")
What a plugin cannot do yet
- One library per plugin. A plugin’s Rust library is either runtime or editor. A system with both a runtime part and editor tools is two plugins:
InventoryandInventoryEditor. - The game cannot use a plugin’s Rust types directly. The game’s crate and the plugin’s are separate libraries, so the game cannot
usethe plugin’s classes. It reaches them as Blueprints do: through their Unreal functions with a dynamic call, through an interface, or through the engine (a subsystem found by class path). - Editor UI beyond menus. Slate UI, details panel customizations, custom asset editors, scripted asset actions, importers and Blueprint nodes are not covered yet.
A new plugin
The goal: a Rust plugin, built and loaded in a project where you can try it.
A plugin lives in a Rusteal project, which builds it with the engine and gives it a level to be tried in: the game it is for, or a project made for it.
rusteal new PluginLab
cd PluginLab
rusteal plugin new Inventory
rusteal plugin new LevelTools --template editor
rusteal plugin new writes the plugin into the project’s Plugins/, lists it in the .uproject and runs the build pipeline. The plugin’s name is also its C++ module’s: letters and digits, not the project’s own name.
| Flag | What it does |
|---|---|
--template <name> | The plugin template, blank by default. |
--project <path> | The project to add it to, when not run from inside it. |
--no-build | Writes the plugin and stops. |
Plugin templates
| Template | What it is |
|---|---|
blank | A runtime plugin with a HelloComponent in Rust. |
editor | An editor plugin: an editor subsystem, a Tools menu entry that counts the project’s assets on a background thread, a console command doing the same, and an asset validator. |
inventory | A runtime gameplay plugin: items as data assets, an inventory component that stacks them and broadcasts its changes, a world subsystem, project settings for its limits, and console commands that give, take and list the player’s items. See An inventory. |
mannequin | The mannequin feature, a mannequin that copies the player’s movement. See Adding features. |
Layout
Plugins/Inventory/
├── Inventory.uplugin # the plugin, depending on Rusteal
├── rusteal.toml # the plugin's library: crate, kind, engine modules
├── Source/Inventory/ # its C++ module, which hands the library to Rusteal
│ └── Generated/ # generated C++ wrappers
├── Rust/
│ ├── Cargo.toml # the plugin's own workspace
│ ├── inventory/ # its code
│ └── bindings/ # its generated bindings
├── Binaries/<Platform>/ # librusteal_Inventory.so, rusteal_Inventory.dll
└── Content/ # the plugin's assets
The plugin carries everything it needs: the directory can be copied into another Rusteal project at the same version, or kept in a repository of its own. Its Rust/Cargo.toml pins the Rusteal crates as the project’s does, and rusteal upgrade moves both.
The plugin’s rusteal.toml
[plugin]
crate = "inventory"
kind = "runtime"
[codegen]
features = ["core", "engine", "input", "slate", "umg"]
kind is runtime or editor. The [codegen] section works as the game’s (see Binding more of the engine). A module far bigger than a plugin needs can be limited to some classes, and the ones they derive from:
UnrealEd = { module = "unreal_ed", feature = "editor", classes = ["EditorActorSubsystem", "EditorAssetSubsystem"] }
Building and reloading
rusteal build
rusteal build --plugin Inventory
rusteal watch --plugin Inventory
rusteal build builds every library of the project, the game’s and each plugin’s. --plugin <Name> limits the codegen, Cargo and deploy steps to one plugin, which is what working on it needs. In the editor, Rusteal.Reload Inventory reloads only that plugin’s library, and each build reloads it on its own (see Hot reload).
In the editor
The plugin shows in Edit > Plugins, under the project’s plugins, and its content folder in the Content Browser once Settings > Show Plugin Content is on.
Screenshot: the Plugins window listing the Rust plugin. (images/plugins/plugins_window.png)
Editor tools
An editor plugin’s library runs in the editor only, and binds the editor’s modules, which the editor template lists. It reaches the engine’s editor-only functions as well, such as set_actor_label.
Use case: a menu entry
The goal: Tools > LevelTools > Count assets runs Rust code.
A menu entry is a class whose parent is ToolMenuEntryScript, with execute overridden:
#![allow(unused)]
fn main() {
use bindings::tool_menus::{FToolMenuContext, ToolMenuEntryScript};
#[uclass(parent = ToolMenuEntryScript)]
pub struct CountAssetsEntry {}
#[uclass_impl]
impl CountAssetsEntry {
#[ufunction(Override)]
fn execute(&mut self, _context: UStructRef<FToolMenuContext>) {
count_assets("/Game");
}
}
}
An editor subsystem creates it when the editor starts and removes it when it closes:
#![allow(unused)]
fn main() {
use bindings::manual::world_ext::new_object;
use bindings::rusteal_editor::RustealEditorSubsystem;
use bindings::tool_menus::{ToolMenuEntryScript, ToolMenuEntryScriptExt};
#[uclass(parent = RustealEditorSubsystem)]
pub struct LevelToolsSubsystem {
#[uproperty]
menu_entry: UObjectRef<ToolMenuEntryScript>,
}
#[uclass_impl]
impl LevelToolsSubsystem {
#[ufunction(Override)]
fn receive_initialize(&mut self) {
if let Err(e) = self.add_menu_entry() {
log::warn!("[LevelTools] no menu entry: {e}");
}
}
#[ufunction(Override)]
fn receive_deinitialize(&mut self) {
if let Ok(entry) = self.menu_entry().checked() {
entry.unregister_menu_entry();
}
}
}
impl LevelToolsSubsystem {
fn add_menu_entry(&mut self) -> RustealResult<()> {
let entry = new_object::<CountAssetsEntry>(&self.as_ref())?.upcast_to::<ToolMenuEntryScript>();
let script = entry.checked()?;
script.init_entry(
FName::new("LevelTools").handle(),
FName::new("LevelEditor.MainMenu.Tools").handle(),
FName::new("LevelTools").handle(),
FName::new("LevelToolsCountAssets").handle(),
Some("Count assets"),
Some("Count the project's assets by type, off the game thread"),
);
script.register_menu_entry();
self.set_menu_entry(entry);
Ok(())
}
}
}
init_entry takes the owner, the menu (LevelEditor.MainMenu.Tools is the Tools menu), the section, the entry’s name, its label and its tooltip.
Use case: work on many assets, off the game thread
The goal: list the project’s assets and count them by name prefix without freezing the editor.
#![allow(unused)]
fn main() {
use bindings::unreal_ed::{EditorAssetSubsystem, EditorAssetSubsystemExt};
use rusteal_runtime::runtime::task;
pub fn count_assets(path: &str) {
let assets = match editor_subsystem::<EditorAssetSubsystem>().and_then(|s| s.checked()) {
Ok(subsystem) => subsystem.list_assets(path, Some(true), Some(false)),
Err(e) => {
log::error!("[LevelTools] no asset subsystem: {e}");
return;
}
};
let path = path.to_string();
task::spawn(
move || {
let mut by_prefix = std::collections::BTreeMap::<String, usize>::new();
for asset in &assets {
let name = asset.rsplit('/').next().unwrap_or(asset);
let prefix = name.split_once('_').map_or("(no prefix)", |(p, _)| p);
*by_prefix.entry(prefix.to_string()).or_default() += 1;
}
(assets.len(), by_prefix)
},
move |(total, by_prefix)| {
log::info!("[LevelTools] {total} assets under {path}");
for (prefix, count) in by_prefix {
log::info!("[LevelTools] {prefix}: {count}");
}
},
);
}
}
The engine call that lists the assets runs on the game thread; the counting, plain Rust, runs on a thread of its own (see Work off the game thread).
The editor’s own subsystems (EditorActorSubsystem, EditorAssetSubsystem, LevelEditorSubsystem) come from EditorSubsystemBlueprintLibrary::get_editor_subsystem, which the template wraps:
#![allow(unused)]
fn main() {
use bindings::editor_subsystem::EditorSubsystem;
use bindings::unreal_ed::EditorSubsystemBlueprintLibrary;
use rusteal_runtime::runtime::{Inherits, RustealResult, SubclassOf, UObjectRef, UeClass};
pub fn editor_subsystem<T: UeClass + Inherits<EditorSubsystem>>() -> RustealResult<UObjectRef<T>> {
let class = SubclassOf::<T>::base().upcast_to::<EditorSubsystem>();
EditorSubsystemBlueprintLibrary::get_editor_subsystem(class).cast::<T>()
}
}
SubclassOf::<T>::base() is T’s class, and upcast_to passes it where a parent’s class is expected; the Inherits bound lets the compiler check that T is an editor subsystem.
Use case: validating assets on save
The goal: saving an asset whose name has no type prefix (SM_, T_, BP_) fails, with a message.
#![allow(unused)]
fn main() {
use bindings::core_ue::Object;
use bindings::data_validation::{EditorValidatorBase, EditorValidatorBaseExt};
const INVALID: u8 = 0;
const VALID: u8 = 1;
#[uclass(parent = EditorValidatorBase)]
pub struct NameValidator {}
#[uclass_impl]
impl NameValidator {
#[ufunction(Override, name = "K2_CanValidateAsset")]
fn can_validate_asset(&self, asset: UObjectRef<Object>) -> bool {
asset.is_valid()
}
#[ufunction(Override, name = "K2_ValidateLoadedAsset")]
fn validate_loaded_asset(&mut self, asset: UObjectRef<Object>) -> u8 {
let Ok(name) = asset.get_name() else {
return VALID;
};
let Ok(me) = self.as_ref().checked() else {
return VALID;
};
if name.contains('_') {
me.asset_passes(asset);
VALID
} else {
me.asset_fails(asset, &format!("'{name}' has no type prefix (SM_, T_, BP_...)"));
INVALID
}
}
}
}
A class whose parent is EditorValidatorBase runs when assets are saved, and on Validate Assets in the Content Browser’s right-click menu. It returns an EDataValidationResult as its u8 value. Validation results show in the Message Log.
Use case: one undo step for many changes
KismetSystemLibrary::begin_transaction, transact_object and end_transaction group changes into one step of Edit > Undo: begin the transaction, call transact_object on each object before changing it, and end the transaction.
Packaging a plugin
The goal: a plugin ready to be dropped into another Rusteal project, or shared.
rusteal plugin package Inventory --output ~/Packages
It builds the plugin’s library with the release profile and copies the plugin into ~/Packages/Inventory:
- its sources: the C++ module, the Rust crate, the generated code;
- its content and configuration;
- its binaries for the platform it was built on: the Rust library and the editor module;
- none of the build output.
Using it in another project
-
Copy the packaged directory into the other project’s
Plugins/. The project must be at the same Rusteal version. -
List the plugin in the project’s
.uproject, underPlugins. -
Build everything there:
rusteal build --all
A binary for another platform comes from packaging the plugin on that platform.
In a packaged game
A game packaged with rusteal package stages the libraries of its runtime plugins next to its own. Editor plugins are left out, as the engine leaves out editor modules.
Packaging a game
The goal: a build of the game that runs without the editor, to try it, share it or ship it.
rusteal package
rusteal package --shipping --output ~/Builds/MyGame
rusteal package builds every Rust library with the release profile, then cooks and packages the game for the platform it runs on, with the engine’s own BuildCookRun. The packaged game stages each Rust library it loads: the game’s and its runtime plugins’.
| Flag | What it does |
|---|---|
| (none) | A Development build, into <project>/Packaged. |
--shipping | A Shipping build: no console, no logging, fully optimized. |
--output <dir> | Where the packaged game goes. |
--optimizations off | Leaves out the platform optimizations, which the game carries by default. |
Before packaging
- Assets only Rust loads by path. The cooker packages the assets that something references. An asset that only
load_objectloads, by a path in Rust code, is not found: list its directory in Edit > Project Settings, Packaging, Additional Asset Directories to Cook. Assets assigned to properties in Blueprints or levels are found on their own. - The default maps. Project Settings > Maps & Modes names the map the game opens (Game Default Map); a packaged game does not start from the level open in the editor.
- Hot reload is for the editor. A packaged game loads its libraries once, at start.
Screenshot: Project Settings, Packaging, with a directory in Additional Asset Directories to Cook. (images/shipping/additional_directories.png)
Running it
The packaged game is in the output directory, under the platform’s name: run the executable there (MyGame.sh on Linux, MyGame.exe on Windows).
When something is missing
A packaged game that starts but misses Rust behavior usually lacks a library or an asset:
- the log (
Saved/Logs/next to the executable, in a Development build) shows whether the Rusteal libraries loaded, and their errors; - an asset that is in the editor but not in the package is the case above: add its directory to cook.
Attributes
Every attribute Rusteal’s macros accept. Anything not listed here is a compile error, with a message naming what is accepted.
#[uclass(...)]
On a struct: the struct is an Unreal class.
| Argument | Required | What it is |
|---|---|---|
parent = Type | yes | The parent class: an engine class from the bindings, or a Rust class. |
implements = [Trait, "path", ...] | no | Unreal interfaces the class implements: other interfaces by class path, and #[uinterface] traits (after 0.7.0). See Interfaces. |
config = "Category" | no | The config category its Config properties are read from and saved to. See Project settings and config. |
#[uproperty(...)]
On a field of a #[uclass] or a #[ustruct]: the field is an Unreal property.
| Argument | What it is |
|---|---|
EditAnywhere | Editable on the class defaults and on each instance. |
EditDefaultsOnly | Editable on the class defaults only. |
VisibleAnywhere | Shown, read-only. |
BlueprintReadWrite | Get and Set nodes in Blueprints. |
BlueprintReadOnly | A Get node in Blueprints. |
SaveGame | Written by save game archives. |
Config | Read from and saved to the class’s config category. |
BindWidget | On a UserWidget class: the widget of the same name in the Widget Blueprint’s layout, which must have one. |
BindWidgetOptional | The same, when the layout may leave it out. |
BindWidgetAnim | On a UserWidget class: the Widget Blueprint’s animation of the same name (a UObjectRef<WidgetAnimation>); it makes the property Transient. |
BindWidgetAnimOptional | The same, when the Widget Blueprint may not have it. |
Transient | Not saved with the object or its class defaults. |
default = value | The default value, for scalars, engine enums and #[uenum]s (not in a #[ustruct]). |
name = "Name" | The Unreal name. |
category = "Category" | The category in the Details panel; `“A |
See Properties.
#[component(...)]
On a field of a #[uclass]: the field is a default subobject, created with every instance.
| Argument | What it is |
|---|---|
root | The actor’s root component. |
attach = "field" | The component it attaches to: a field of the class, of a Rust parent, or an inherited engine component by field-style name (root_component, mesh). |
socket = "Socket" | The socket on the parent component. |
name = "Name" | The component’s name, when it is not the field’s. |
See Components.
A field with no attribute
Rust’s alone: any Rust type, a getter returning a clone, a set_ setter and a _mut accessor. Reset by hot reload, invisible to the garbage collector.
#[uclass_impl]
On an impl block of a #[uclass]: its marked methods are the class’s Unreal functions, events, delegates and defaults. A class can have several.
On the implementation of a #[uinterface] trait for a #[uclass] (impl Interactable for Door): the class implements the interface, and every method is its function of the same name, with no attribute.
#[uinterface]
Not released yet: it comes in a release after 0.7.0.
On a trait, with no arguments: the trait is an Unreal interface, Blueprint-implementable, whose functions are Blueprint events. Its functions take &self or &mut self, then #[ufunction] parameter types, return a #[ufunction] return type, and have no body; the trait has no generics and no supertraits. See Interfaces.
#[ufunction(...)]
On a method in a #[uclass_impl] block.
| Argument | What it is |
|---|---|
(none), BlueprintCallable | A function with execution pins, callable by name. |
BlueprintPure | A function without execution pins. |
BlueprintImplementableEvent | An event the Blueprint child implements; empty body. |
Override | An event of the parent class implemented in Rust. |
Exec | Also a console command (with the default kind or BlueprintPure). |
name = "Name" | The Unreal name. |
Override, BlueprintPure and BlueprintImplementableEvent exclude each other. See Functions and Engine events.
#[udelegate] and #[udelegate(name = "Name")]
On a method with an empty body in a #[uclass_impl] block: a multicast delegate, assignable from Blueprints. Calling it broadcasts; <name>_add(closure) and <name>_add_ufunction(&target, "Function") bind Rust listeners. See Delegates.
#[class_defaults]
On a method in a #[uclass_impl] block, returning RustealResult<()> or nothing: runs once on the class default object. See Class defaults.
#[ustruct]
On a struct whose fields are all #[uproperty]: an Unreal struct, with a <Name>Ext trait of getters and setters. See Structs and data tables.
#[uenum]
On an enum whose variants carry no data: an Unreal enum, usable in Blueprints, with values up to 254. See Enums.
Macros
| Macro | What it does |
|---|---|
rusteal_runtime::entry!() | Once per library, in lib.rs: the entry point the plugin calls when it loads the library. |
rusteal_runtime::on_load!(function) | Runs a function every time the library loads, hot reloads included. |
log::info!, log::warn!, log::error! | Write to the Output Log, through the log crate (see Logging). |
Types
Unreal types in Rust
| C++ | Rust | Notes |
|---|---|---|
bool, uint8, int32, int64, float, double | bool, u8, i32, i64, f32, f64 | |
FString | String (as a property or a return value), &str (as an argument) | |
FName | FName (properties, Rust code), FNameHandle (engine arguments and returns) | FName::new("x").handle(), FName::from(handle) |
FText | &str (as an argument), String (as a return value) | |
UObject*, TObjectPtr<T> | UObjectRef<T> | a handle; see Object references |
TSubclassOf<T> | SubclassOf<T> | a class |
TScriptInterface<IFoo> | ScriptInterface<dyn Foo> | an object implementing a #[uinterface]; object.interface::<dyn Foo>() (after 0.7.0) |
TSoftObjectPtr<T> | SoftObjectRef<T> | an asset by path |
TWeakObjectPtr<T> | TWeakObjectPtr<T> | |
A struct (FVector, FHitResult…) | OwnedStruct<T> (owned), UStructRef<T> (a reference into Unreal memory) | fields through the <Struct>Ext trait |
An enum (ECollisionChannel) | the Rust enum of the same name, or its u8 in some events | |
| Your enum | a #[uenum] | |
TArray<T> | UeArray<T> (a property), Vec<T> or &[T] (engine arguments and returns) | |
TMap<K, V> | UeMap<K, V> | engine properties only |
TSet<T> | UeSet<T> | engine properties only |
| An output parameter | part of the returned tuple; in an event, UStructRef<T> or OutRef<T> |
Math
| Rusteal and glam | Unreal | Conversion |
|---|---|---|
glam::DVec3 | FVector | FVector::from_dvec3(v), fvector.to_dvec3() |
glam::DVec2 | FVector2D | FVector2D::from_dvec2(v), .to_dvec2() |
glam::DQuat | FQuat | FQuat::from_dquat(q), .to_dquat() |
Rotator | FRotator | FRotator::from_rotator(r), .to_rotator() |
Transform | FTransform | FTransform::from_transform(t), .to_transform() |
LinearColor | FLinearColor | FLinearColor::from_linear_color(c) |
Rotator, Transform, LinearColor and Color come from rusteal_runtime::runtime; glam’s types from glam, re-exported as rusteal_runtime::glam.
Containers
| Method | UeArray<T> | UeMap<K, V> | UeSet<T> |
|---|---|---|---|
| Size | len(), is_empty() | len(), is_empty() | len(), is_empty() |
| Read | get(i), iter(), to_vec() | find(&k), iter(), to_hash_map() | contains(&v), iter(), to_hash_set() |
| Write | set(i, &v), push(&v), remove(i), clear(), set_all(&[..]) | add(&k, &v), remove(&k), clear() | add(&v), remove(&v), clear() |
Every method returns a RustealResult: the object holding the container may be gone.
Errors
RustealResult<T> is Result<T, RustealError>. See Errors.
CLI reference
Commands that take [project] find the project by walking up from the current directory when it is left out.
rusteal new
rusteal new <Name> [--dir <path>] [--template <name>] [--variant <name>] [--feature <name>]... [--no-build] [--optimizations <on|off>]
Creates a project from a template, installs the plugins, adds each --feature as a plugin, writes the Rust workspace and runs the whole build. See Your first project, Starting from a template and Adding features.
rusteal setup
rusteal setup <project>
Installs the Rusteal plugins into an existing project and writes a starter rusteal.toml. See Adding Rusteal to an existing project.
rusteal build
rusteal build [project] [--all | --from <n> | --step <n>] [--release] [--plugin <Name>]
Without flags, steps 4 and 5: build the Rust libraries and deploy them. See Building.
rusteal watch
rusteal watch [project] [--plugin <Name>] [--optimizations <on|off>]
Writes the platform optimizations, then runs rusteal build every time a Rust source changes, until Ctrl+C. See rusteal watch.
rusteal generate
rusteal generate [project]
Regenerates the bindings crate and the C++ wrappers from the exported reflection.
rusteal upgrade
rusteal upgrade [project]
Moves the project to the CLI’s Rusteal version. See Versions and upgrades.
rusteal package
rusteal package [project] [--shipping] [--output <dir>] [--optimizations <on|off>]
Builds the release libraries and packages the game, with the platform optimizations unless --optimizations off. See Packaging a game.
rusteal plugin new
rusteal plugin new <Name> [--template <name>] [--project <path>] [--no-build]
Creates a Rust plugin in the project. See A new plugin.
rusteal plugin package
rusteal plugin package <Name> [--project <path>] [--output <dir>]
Copies a plugin out with its release library. See Packaging a plugin.
Console commands
| Command | What it does |
|---|---|
Rusteal.Reload | Reloads every Rust library of the project. |
Rusteal.Reload <Name> | Reloads one library: the project’s name for the game’s, the plugin’s name for a plugin’s. |
Rusteal.AutoReload 0 | Stops reloading on deploy. |
Rusteal.AutoReload 1 | Reloads on deploy (the default). |
See Console commands.
Known limits
What Rusteal does not do yet, and what to do meanwhile.
Platforms
| Not yet | Meanwhile |
|---|---|
| Android and iOS builds (being actively worked on) | Run and test the game in the editor on Linux or Windows, touch controls included; the ar template runs there without an AR session. |
| Console builds (being actively worked on) | Linux and Windows builds. |
Classes
| Not yet | Meanwhile |
|---|---|
TMap and TSet properties | Two UeArrays side by side, or a Rust-only HashMap. |
String, arrays and FName as #[ufunction] parameters | A property the caller sets before the call. |
Network replication (Replicated, RPCs) | Single player and local multiplayer. |
| Implementing a C++-only interface | Interfaces whose functions Blueprints can implement. |
Hot reload
| Not yet | Meanwhile |
|---|---|
Reloading a changed #[ustruct] | Restart the editor. |
Replacing a changed class or interface outside the editor (-game) | Restart the game. |
| Keeping Rust-only fields across a reload | Keep what must survive in #[uproperty] fields. |
Delegates
| Not yet | Meanwhile |
|---|---|
Binding C++-only engine delegates (UPathFollowingComponent::OnRequestFinished) | A Blueprint-exposed delegate raised for the same event (AIController::ReceiveMoveCompleted). |
Plugins
| Not yet | Meanwhile |
|---|---|
| A runtime and an editor library in one plugin | Two plugins, Name and NameEditor. |
| The game using a plugin’s Rust types | Dynamic calls, interfaces, subsystems found by class. |
| Slate UI, details customizations, asset editors, importers, Blueprint nodes | Editor Utility Widgets in Blueprint calling Rust functions. |
Bindings
| Not yet | Meanwhile |
|---|---|
| Functions the engine reflects but does not export | They are in the blocklist of rusteal.toml; use the Blueprint-callable alternative or a dynamic call. |
| Modules the project does not list | Add them to rusteal.toml and run rusteal build --all. |
Platforms
| Platform | Status |
|---|---|
| Linux (x64) | Supported |
| Windows (x64) | Supported |
| macOS | Not tested yet |
Troubleshooting
Problems a game developer runs into, what causes them, and the fix. The Output Log is the first place to look: Rusteal logs under LogRusteal.
Building
rusteal build says the bindings have not been generated yet.
The project was never built on this machine, or Rust/bindings/ was deleted. Run rusteal build --all.
The Unreal Build Tool says another instance is running. It runs one build at a time per engine: the editor compiling, or a build of another project. Wait for it to finish and build again.
Link errors in a generated wrapper after adding a module to rusteal.toml.
A function the engine reflects but does not export. Add it to [codegen.blocklist] functions as "Class.Function" and run rusteal build --all.
The CLI refuses the project because of its version. The project and the CLI are at different Rusteal versions. See Versions and upgrades.
On Windows, Rust fails to link or the engine refuses the compiler. Check that Rust uses the MSVC host and that the Visual Studio components match the ones in Prerequisites.
In the editor
On Linux, the editor closes with a segfault in the NVIDIA driver, after a window closes or PIE stops.
The crash report shows vkWaitForFences under FVulkanSwapChain::AcquireImageIndex, and the log has AcquireNextImage() failed due to the outdated swapchain just before. Run rusteal watch once, so it writes the platform optimizations, and restart the editor. If it happens in the Project Browser, before a project is open, add the line to the engine’s own config, as that page shows.
A Rust class does not show in the Place Actors panel or the class picker.
The library did not load, or the class is not compiled in: check the Output Log for the library’s load messages and errors, and that the class’s module is declared in lib.rs (mod my_class;).
A change does not show after rusteal build.
Check the Output Log for the reload. If Rusteal.AutoReload is 0, run Rusteal.Reload. A changed #[ustruct] needs a restart.
A value set in class_defaults does not apply to a Blueprint.
The Blueprint child overrides it. In its Class Defaults, the value shows a reset arrow; click it to go back to the Rust default.
The Rust code of an event does not run.
A Blueprint child that implements the same event in its graph replaces it. Remove the event node from the Blueprint, or give the Blueprint a BlueprintImplementableEvent of its own.
A Blueprint node is in error after a reload. The Rust function or property it used was removed or renamed. Fix the node or put the function back.
Playing
Input does nothing.
The Output Log says which part is missing: no mapping contexts added (the player controller’s Blueprint child has none, or the game mode does not use that controller), or a function name in bind_action that does not exist. See Character movement and input.
spawn_actor logs that the actor has no root component.
The spawned class has no scene component as root, so the transform is ignored. Give it a #[component(root)], or spawn a class that has one.
An engine call fails with ObjectDestroyed.
The object was destroyed, or the reference was never set (a property the Blueprint child did not fill). Check the Blueprint’s Class Defaults, and handle the error where destruction is expected.
A panic message in the Output Log. Rust code panicked; the editor caught it and went on. The message and the location are in the log: fix the code and build again.
A StateTree logs that it could not find its context actor.
It started before its controller possessed the pawn. Start it in the controller’s receive_possess. See AI.
Packaging
The packaged game misses an asset. Only Rust code loads it, by path. Add its directory to Additional Asset Directories to Cook. See Packaging a game.