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++.