Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 13

Rust Structs for HEXACO & Trait Systems

Recommended resources

Hello! Welcome back to our journey in building a life simulation game.

In our last lesson, we designed the character.schema.json file, creating a definitive blueprint for our character data. This schema acts as a contract, ensuring that every character in our game has a consistent and valid structure.

Today, we will translate that abstract blueprint into concrete, usable code. The goal of this lesson is to implement the core components of our character's identity within the Rust engine. Specifically, our learning outcome is to implement the HEXACO personality model (6 float dimensions) and a string-based trait system in Rust structs. As we build out our main character data structure, we will also naturally cover the implementation of age stages, another key part of the character model.

By the end of this lesson, our Rust backend will have a native understanding of a character's personality, traits, and life stage, ready to be manipulated by the game's logic.

1. Representing Data with Rust structs

In Rust, the primary way to create a custom data type that groups related fields together is by using a struct. Since you have a background in computer science, you can think of a struct as being conceptually similar to a C struct or a C++/C# class that primarily holds data. It allows us to bundle different pieces of information into a single, meaningful unit.

To get a practical feel for how structs work in a game development context, let's watch a short segment of the following video.

Game Development in Rust 3: Structs

This video from the GetIntoGameDev channel provides a clear, game-oriented introduction to Rust structs.

Please watch from 03:43 to 05:33. This section covers how to declare a struct and how to create an instance of it, which are the two fundamental operations we'll be using today.

As you saw, the syntax is straightforward: we define the struct with a name and then list its fields and their types within curly braces.


2. Implementing the HEXACO Personality Model

Let's start by modeling the most complex part of our character's identity: their personality. In the previous lesson, we decided to use the HEXACO model, which defines personality across six distinct dimensions.

Dimensions of HEXACO Personality Test
A visual recap of the six dimensions of the HEXACO personality model: Honesty-Humility, Emotionality, eXtraversion, Agreeableness, Conscientiousness, and Openness to Experience.

Our JSON schema defined a nested personality object with six numeric fields. We will create a Personality struct in Rust that mirrors this structure exactly. Each dimension will be a 32-bit floating-point number (f32).

To understand the meaning behind each of these dimensions, you can refer back to the resource from our last module.

What is the HEXACO model?

This article from TraitForward provides a detailed breakdown of each HEXACO dimension and its associated facets. It's a useful reference for understanding what each of our struct fields represents.

You can quickly skim the sections describing the 6 dimensions: 'Honesty-Humility', 'Emotionality', 'eXtraversion', 'Agreeableness', 'Conscientiousness', and 'Openness'. This will help contextualize the fields in our struct.

Here is the Rust implementation. In a new Rust file within your project (e.g., src/character.rs), you can add the following code:

// We will need these `derive` macros later for serialization,
// so it's good practice to add them now.
// #[derive(Debug)] allows us to print the struct for debugging.
// #[derive(Serialize, Deserialize)] comes from the `serde` crate we used
// in Module 1 and allows us to convert this struct to and from JSON.



#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Personality {
    pub honesty: f32,          // Honesty-Humility
    pub emotionality: f32,     // Emotionality
    pub extraversion: f32,     // eXtraversion
    pub agreeableness: f32,    // Agreeableness
    pub conscientiousness: f32,// Conscientiousness
    pub openness: f32,         // Openness to Experience
}

We've made the fields pub (public) so that other parts of our code can access them. With this Personality struct, we now have a type-safe container for our character's core psychological makeup.


3. Implementing Age Stages and Traits

Next, let's define the other key attributes of a character. According to our schema, a character has an ageStage and a list of traits.

Age Stages as an enum

The ageStage can only be one of four specific values: "Child", "Teen", "Adult", or "Elder". In Rust, the ideal tool for representing a value that can only be one of a few definite variants is an enum.

Rust structs, enums and traits

This article by Prabhu Eshwarla provides a concise explanation of Rust enums.

Read the section titled 'Enum'. Focus on how an enum restricts a field to a predefined set of values, which is exactly what we want for our character's age stage.

Using an enum is more robust than using a simple String because it makes invalid states impossible to represent at the compile level. The compiler will ensure that a character's age stage is always one of the valid options.

Rust Data Modelling Without Classes

For a deeper insight into why enums are so powerful in Rust, this video from No Boilerplate is excellent.

Watch from the beginning to 01:34. The video explains the concept of 'making invalid states unrepresentable' using enums, a core tenet of Rust's design philosophy that will be valuable throughout this course.

Let's define our AgeStage enum in src/character.rs:




#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum AgeStage {
    Child,
    Teen,
    Adult,
    Elder,
}

Traits as a Vec<String>

Our schema defines traits as an array of strings. The standard, growable list type in Rust is the vector, written as Vec<T>, where T is the type of the elements. Therefore, a list of character traits can be represented as Vec<String>.


4. The Complete Character Struct

Now we can assemble these pieces into a main Character struct. This struct will represent a single character in our game world, containing all the data we've modeled so far.

Add this final struct to your src/character.rs file:

// It's good practice to keep related structs in the same file
// or module. We'll place everything in `src/character.rs` for now.




#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Character {
    pub character_id: String, // We'll use a simple String for now.
    pub name: String,
    pub age_stage: AgeStage,
    pub personality: Personality,
    pub traits: Vec<String>,
}

And here is how you could create an instance of a Character in your main.rs or another function, just as you saw in the first video:

// In another file, e.g., main.rs, make sure to import the types:
// use your_project_name::character::{Character, AgeStage, Personality};

fn create_character_example() {
    let alex = Character {
        character_id: "123e4567-e89b-12d3-a456-426614174000".to_string(),
        name: "Alex Chen".to_string(),
        age_stage: AgeStage::Adult,
        personality: Personality {
            honesty: 0.8,
            emotionality: 0.6,
            extraversion: 0.4,
            agreeableness: 0.7,
            conscientiousness: 0.9,
            openness: 0.85,
        },
        traits: vec![
            "Bookworm".to_string(),
            "Loves the Outdoors".to_string(),
        ],
    };

    // You can now access the data using dot notation:
    println!("Created character: {}", alex.name);
    println!("Their personality conscientiousness is: {}", alex.personality.conscientiousness);
}

Note: The .to_string() method is used to convert string literals (type &str) into the owned String type that our struct expects.


Conclusion

Excellent work! You have now successfully translated the abstract design from our JSON schema into concrete, type-safe Rust data structures. This is a critical step in building the foundation of our game's simulation engine.

Key Takeaways:

  • structs are Rust's way of creating custom types to bundle related data, which we used to define Personality and Character.
  • enums allow us to define a type that can only be one of a few specific variants, making them perfect for fixed states like our AgeStage. This makes our code safer and more expressive.
  • Vec<T> is Rust's standard vector type, ideal for representing growable lists like our character's traits.
  • By combining these, we've created a Character struct that directly mirrors our JSON blueprint, preparing our backend to understand and manage character data.

In our next lesson, we will switch our focus to the frontend. Now that our Rust backend has a clear definition of what a character is, we will build the character creator UI in Unity, complete with sliders and input fields, to allow a player to bring their own characters to life.