Skip to main content

Welcome to a Grasp lesson

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

Lesson 42

Understanding Relationship Types

Recommended resources

Hello! Welcome back.

In the last lesson, we built the dynamic core of our social engine. You implemented a system where social interactions update raw friendship and romance scores, with the outcomes cleverly modified by each character's unique personality traits. Now, our simulation generates numbers, but those numbers lack clear, actionable meaning.

Today, we'll fix that. Our learning outcome is to classify relationships into types (e.g., friend, acquaintance) based on score thresholds. We will translate the floating-point scores we're calculating into discrete, meaningful categories. This is a crucial step that will allow the game's AI, UI, and interaction systems to understand and react to the social landscape you're building.


1. From Numbers to Narratives: Designing Relationship Tiers

Before we write any code, let's think about the design. How do games represent the evolution of a relationship? Typically, they don't just show a number; they group numerical scores into qualitative tiers. This creates clear stages of progression that players can understand and strive for.

The core idea is to define thresholds. For instance, a friendship score above 30 might mean two characters are "Friends," while a score below -30 might mean they are "Disliked."

Creating Relationship & Approval Systems

To understand this concept in detail, let's read an excellent guide on creating relationship systems. This article breaks down how to design and use score ranges to define relationship stages.

Read the following sections from this Idrelle Games article: Start with 'What Is An Approval System?' to get an overview of the key design questions. Read 'Linear Approval Systems' and 'Bi-Directional Approval Systems' to see concrete examples of how numerical ranges are mapped to relationship labels (e.g., 'Neutral feelings', 'Friendly', 'Loving/close friends'). We are using a bi-directional system. Finally, read 'Why Work in Ranges?' to understand the benefits of this approach for game pacing and player experience. Focus on the logic of mapping scores to labels; the code examples are for a different engine, but the principles are universal.

As the article explains, these tiers provide structure. They give both the player and the game's systems a clear understanding of where a relationship stands. You can see this exact concept in many life simulation games.

Set Relationship Values UI (The Sims Cheat)
This UI from a mod for The Sims shows separate numerical inputs for 'Friendship' and 'Romance' scores. Note the text indicating a score of '10 or higher' is needed to establish the romance bar, a clear example of a threshold triggering a new relationship state.
Score Range Editor UI
This UI for an unrelated tool perfectly illustrates our goal: defining adjustable thresholds on a numerical scale to create qualitative categories like 'Fair', 'Good', and 'Excellent'.

For our game, let's define a simple bi-directional scale for friendship based on a score from -100 to +100:

  • Enemies: < -75
  • Disliked: -75 to -30
  • Acquaintances: -30 to +30
  • Friends: +30 to +75
  • Best Friends: > +75

This gives us five distinct, meaningful tiers that we can now implement.


2. Implementing Relationship Types in Rust

The ideal way to represent a fixed set of categories like our relationship tiers in Rust is with an enum. An enum ensures type safety, meaning a relationship can only be one of the types we define, preventing bugs from invalid values.

Rust Structs and Enums

If you need a refresher on enums in Rust, this article is a great resource. We will be using a simple enum and then writing a function to choose a variant based on our score.

Briefly review the section 'Now, Let's Talk Enums: Representing Choices'. Pay attention to how enums are defined and how they represent a type that can be one of several possible values.

Let's create our RelationshipType enum. Add this to your lib.rs file where you have your other social struct definitions.

use serde::{Serialize, Deserialize};
use std::collections::HashMap;

// Add this enum definition



#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum RelationshipType {
    BestFriends,
    Friends,
    Acquaintances,
    Disliked,
    Enemies,
    Strangers, // A default for when no relationship exists
}

Note: We've added #[derive(Debug, ...)] to allow for easy printing, comparison, copying, and serialization of the enum, which will be useful later.

Now, let's write the logic to classify a friendship score. We can add a method to our existing RelationshipScores struct that returns the correct RelationshipType.

In the impl block for RelationshipScores, add the following function:

// This code goes inside the `impl RelationshipScores { ... }` block
// that you should already have from previous lessons.

pub fn get_friendship_type(&self) -> RelationshipType {
    if self.friendship > 75.0 {
        RelationshipType::BestFriends
    } else if self.friendship > 30.0 {
        RelationshipType::Friends
    } else if self.friendship >= -30.0 { // Use >= to include -30 in this range
        RelationshipType::Acquaintances
    } else if self.friendship >= -75.0 { // Use >= to include -75
        RelationshipType::Disliked
    } else {
        RelationshipType::Enemies
    }
}

This function is a direct translation of the thresholds we designed. It takes the raw friendship score and maps it cleanly to one of our enum variants.


3. Combining Friendship and Romance

Our system tracks both friendship and romance. A complete relationship status needs to account for both dimensions. Two characters could be "Best Friends" but not romantically involved, or "Enemies" who share a secret crush.

Let's create another enum for the romantic status, using thresholds inspired by the Sims UI we saw earlier.




#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum RomanceStatus {
    NotInterested,
    Crushing,
    Dating,
    InLove,
}

And now, add the corresponding classification function to your RelationshipScores implementation:

// Add this function to the `impl RelationshipScores { ... }` block as well.

pub fn get_romance_status(&self) -> RomanceStatus {
    if self.romance > 60.0 {
        RomanceStatus::InLove
    } else if self.romance > 30.0 {
        RomanceStatus::Dating
    } else if self.romance > 10.0 {
        RomanceStatus::Crushing
    } else {
        RomanceStatus::NotInterested
    }
}

To represent the complete picture, we can bundle these two classifications into a single struct. This gives us a clear and comprehensive snapshot of any given relationship.

// A new struct to hold the classified relationship status



#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct RelationshipStatus {
    pub friendship: RelationshipType,
    pub romance: RomanceStatus,
}

Finally, let's add a convenience function to RelationshipScores that returns this complete status.

// Add this final function to the `impl RelationshipScores { ... }` block.

pub fn get_status(&self) -> RelationshipStatus {
    RelationshipStatus {
        friendship: self.get_friendship_type(),
        romance: self.get_romance_status(),
    }
}

With this in place, we can get a full, classified relationship status from any RelationshipScores instance with a single function call: some_scores.get_status().


4. Practical Application: A Test Case

Let's see this in action. We can update our GameState to include a test function that reports the classified relationship between two characters. This function simulates what you'll eventually need to do to display this information in the Unity UI.

Add this method to your GameState implementation:

// This goes inside your `impl GameState { ... }` block.
// Assumes CharacterId is defined and can be cloned, and used as a HashMap key.

pub fn get_classified_relationship(
    &self,
    char1_id: &CharacterId,
    char2_id: &CharacterId,
) -> RelationshipStatus {
    // Find char1's perspective on char2
    let character1 = self.characters.get(char1_id).unwrap();
    
    match character1.relationship_scores.get(char2_id) {
        Some(scores) => scores.get_status(),
        None => {
            // If no relationship entry exists, they are Strangers with no romance
            RelationshipStatus {
                friendship: RelationshipType::Strangers,
                romance: RomanceStatus::NotInterested,
            }
        }
    }
}

// You can add a main or test function to try this out:
/*
fn main() {
    // Setup some characters and a GameState...
    // let mut game_state = ...;
    // let char_a_id = ...;
    // let char_b_id = ...;

    // Manually set scores for testing
    let char_a = game_state.characters.get_mut(&char_a_id).unwrap();
    let scores_with_b = char_a.relationship_scores.entry(char_b_id.clone()).or_default();
    scores_with_b.friendship = 45.0; // Should be "Friends"
    scores_with_b.romance = 15.0;  // Should be "Crushing"

    // Get and print the classified status
    let status = game_state.get_classified_relationship(&char_a_id, &char_b_id);
    println!("Character A's relationship with Character B is: {:?}", status);
    // Expected output: RelationshipStatus { friendship: Friends, romance: Crushing }
}
*/

This simple function demonstrates the power of our new system. Instead of dealing with ambiguous floats like 45.0 and 15.0, our game logic can now ask, "Are these two Friends?" or "Is character A Crushing on character B?". This is a much more robust and readable way to build gameplay features.



Conclusion

Fantastic work today. You've successfully bridged the gap between raw simulation data and meaningful game mechanics. By classifying relationship scores, you've given your game's systems a vocabulary to understand social dynamics, which is fundamental to creating believable character AI and engaging social gameplay.

Key Takeaways:

  • Relationship Tiers: You designed a system of relationship tiers by defining score thresholds, a common and effective pattern in simulation games.
  • Type-Safe Classification: You implemented this system in Rust using enums, ensuring that relationship types are always valid and well-defined.
  • Composite Status: You created a composite RelationshipStatus struct that considers both friendship and romance, allowing for nuanced relationship states.
  • Actionable Data: You've transformed abstract numerical scores into concrete, queryable states (e.g., RelationshipType::Friends) that can be used to drive game logic.

In our next lesson, we will add another layer of depth: memory. We'll implement a system for storing a simplified log of significant interactions between characters. This "memory" system will make relationships feel more persistent and contextual, and the relationship types we defined today will be key in determining which events are considered "significant" enough to be remembered.