Hello! Welcome to the first lesson in our module on building a robust save/load system.
In the last module, you did a fantastic job bringing the game world to life. By implementing the "Smart Object" pattern, you've enabled characters to interact with objects in the environment, fulfilling their needs and creating a dynamic simulation. However, all this progress—every object placed, every need fulfilled—is lost the moment you close the game.
Today, we'll lay the foundation for making your game world persistent. Our goal is to design a unified Rust struct to represent the entire game state. This single, all-encompassing data structure will act as a snapshot of your game world at any given moment, containing everything from the position of a chair to a character's deepest feelings. This design is the essential blueprint we'll need before we can write the code to actually save and load the game.
We will focus on:
- Adopting a data-oriented design philosophy, which is highly effective for game development in Rust.
- Identifying the key pieces of data that make up our game's state.
- Structuring this data into a hierarchy of nested Rust structs.
- Composing these smaller structs into a single, unified
GameStatestruct.
1. Thinking in Data, Not Just Objects
As someone with experience in Unity and C#, you're likely accustomed to an Object-Oriented Programming (OOP) approach, where data and the methods that operate on it are bundled together into objects. Rust, while supporting some OOP patterns, often encourages a different way of thinking: data-oriented design.
This approach prioritizes the layout and transformation of data. For game development, this is incredibly powerful. It often leads to more performant and easier-to-reason-about code, especially when managing complex state.
To get a deeper insight into this philosophy, I'd like you to read a few sections from a keynote speech by the lead programmer of the game Starbound. It's a fantastic resource that explains why this approach is a natural fit for Rust and for game development in general.
My RustConf 2018 Closing Keynote
Please read 'My RustConf 2018 Closing Keynote' by Kyren. This article provides the perfect context for why we are designing our game state in a particular way and how to avoid 'fighting the borrow checker' by focusing on the data itself.
Read the following sections: Basic thesis of the talk: Understand the core argument about Rust rewarding data-oriented design. How do you EVEN make a game in Rust?: This frames the problem we're solving. How games were made in the past: Pay attention to the simple GameState struct example for an NES-era game. The UR-Architecture: Focus on the more complex GameState example for a Mario 64-like game. This is a direct inspiration for our initial design.
2. Identifying the Components of Our Game State
As the article you just read illustrates, a good first step is to simply list all the data that defines your game world. Let's think about what we've built so far:
- World Data: We have a pre-built world containing lots. Players can place objects (like beds and chairs) onto these lots. We need to store a list of all placed objects and their positions/rotations.
- Character Data: We have characters with names, ages, personalities (HEXACO), traits, and a set of needs (hunger, energy, etc.) that decay over time. Each character has a current mood.
- Relationship Data: Characters have relationships with each other, tracked with friendship and romance scores. We also planned for a 'memory' log.
- Game Clock: We have a clock that manages time progression (pause, normal, fast-forward).
Our GameState struct must be a container for all of this information.
3. Assembling the GameState Struct
Let's start drafting our GameState struct. We'll use the fundamental building blocks of Rust's data modeling: struct and enum.
A struct is a "product type," meaning an instance of it contains all of its fields (field A and field B and field C). An enum is a "sum type," meaning an instance can be only one of its variants (variant A or variant B or variant C). Using these tools effectively allows us to make invalid states unrepresentable in our code.
To refresh your memory on the syntax and power of these tools, the two short videos below are excellent resources.
This first video, 'Structs in Rust' from Let's Get Rusty, is a great refresher on the syntax for defining structs and implementing methods on them, which we'll use to add functionality like save and load later.
Watch from 00:38 to 02:54 to review how to define and instantiate structs. Then, watch from 11:40 to 16:55 to understand impl blocks and how methods (like save_game) are associated with a struct.
Rust Data Modelling Without Classes
This second video, 'Rust Data Modelling Without Classes' by No Boilerplate, explains the philosophy behind using structs and enums to create robust data models, similar to the principles in the article you read.
Watch from 05:27 to 07:27. This section applies database normalization principles to struct design, which is highly relevant to how we'll break down our complex game state into clean, interconnected structs.
The "Struct of Structs" Approach
Drawing on these principles, we won't create one gigantic GameState struct with hundreds of fields. Instead, we'll compose it from the other data structures we've designed in previous modules. This is much cleaner and more maintainable.
Let's imagine our file structure. We might have character.rs, world.rs, relationship.rs, etc. Our main game_state.rs file will bring them all together.
Here is a first draft of what our GameState could look like. For now, this is just a blueprint; you don't need to implement all the sub-structs from scratch.
// In a new file, perhaps `src/game_state.rs`
// We'll need to import the other structs we've defined elsewhere.
// For example:
// use crate::character::{Character, CharacterId};
// use crate::world::{World};
// use crate::relationships::{RelationshipManager};
// use crate::clock::{GameClock};
use std::collections::HashMap;
// Placeholder structs to make the example compile.
// In your actual project, these would be the detailed structs from other modules.
pub struct World { /* ... list of lots, placed objects, etc. */ }
pub struct Character { /* ... name, age, needs, personality, etc. */ }
pub type CharacterId = u64; // A unique identifier for each character
pub struct RelationshipManager { /* ... map of relationships between CharacterIds */ }
pub struct GameClock { /* ... current time, speed, etc. */ }
/// The top-level struct that represents the entire state of the game.
/// This is what we will serialize to a file.
pub struct GameState {
pub world: World,
// Using a HashMap allows us to efficiently look up characters by their unique ID.
pub characters: HashMap<CharacterId, Character>,
pub relationships: RelationshipManager,
pub game_clock: GameClock,
// A counter to ensure new characters always get a unique ID.
next_character_id: CharacterId,
}
impl GameState {
/// Creates a new, empty game state.
pub fn new() -> Self {
GameState {
world: World { /* ... */ },
characters: HashMap::new(),
relationships: RelationshipManager { /* ... */ },
game_clock: GameClock { /* ... */ },
next_character_id: 0,
}
}
// In the next lessons, we'll add more methods here, like:
// pub fn save_to_file(&self, path: &str) -> Result<(), ...>
// pub fn load_from_file(path: &str) -> Result<Self, ...>
// pub fn add_character(&mut self, character_data: ...) -> CharacterId
}
This structure gives us a single, unified "root" object (GameState) that holds the entire world. This is conceptually similar to how a graph database can represent many interconnected pieces of data within one model.

4. Designing for Serialization
Our ultimate goal is to save this GameState struct as a JSON file. The serde crate is the standard for this in Rust. To make our structs serializable, we simply need to add an attribute to them: #[derive(Serialize, Deserialize)].
We need to ensure that GameState and every struct contained within it (like World, Character, etc.) has this attribute.

Practical Exercise: From JSON to Struct
To solidify the connection between the JSON data we want and the Rust struct we need, let's use a helpful tool.
- Think about a simple part of your game state, like a single character. Sketch out what its data might look like in JSON format. For example:
{ "id": 1, "name": "Alex", "age_stage": "Adult", "needs": { "hunger": 85.5, "energy": 62.0, "social": 75.0, "fun": 40.0 }, "personality": { "honesty_humility": 0.6, "emotionality": 0.4, "extraversion": 0.8, "agreeableness": 0.5, "conscientiousness": 0.7, "openness": 0.9 } } - Go to the JSON to Rust Converter tool linked below. Paste your JSON into the input box.
- Observe the Rust structs it generates on the right. Notice how it automatically adds the
serdeattributes and creates nested structs (Needs,Personality).
Deserialize JSON with serde | Rust Struct Generator
This 'JSON to Rust Converter' is a web tool that automatically generates Rust structs from JSON data. It's a fantastic way to quickly prototype your data structures and see how serde works.
Spend about 5-10 minutes experimenting with this tool. Use the example JSON above or create your own. Pay attention to the options and the generated Rust code. Notice how nested JSON objects become nested Rust structs, which is exactly the pattern we're using for our GameState.
This exercise should make it clear how designing our GameState as a set of nested structs directly translates into a clean, hierarchical JSON save file.
Conclusion
Congratulations! You've successfully designed the data architecture that will serve as the backbone of your game's save/load functionality. This is a critical step that requires careful thought, and getting it right now will make future development much smoother.
Key Takeaways:
- Data-oriented design is a powerful paradigm for game development in Rust, encouraging you to focus on the structure and flow of data.
- A unified
GameStatestruct acts as a single source of truth, holding all the information that defines your game world at a specific moment. - We can build complex data models by composing smaller, logical structs (
Character,World, etc.) into a larger container struct. - By designing our structs with
serdein mind (#[derive(Serialize, Deserialize)]), we've prepared our data structure to be easily converted to and from a JSON file.
Preview of the Next Lesson:
With our GameState blueprint in hand, the next step is to bring it to life. In the next lesson, we will implement the save_game function. You will write the Rust code to take an instance of our GameState struct, serialize it into a JSON string, and write it to a file on the disk.