Hello! Welcome back to our course on building a life simulation game.
In our last lesson, we put time itself under our control by building a GameClock. Our characters' needs now decay at a rate we can manage with pause, normal, and fast-forward speeds. However, this has created a new, inevitable problem: without any way to replenish their needs, every character is on a one-way trip to misery.
Today, we'll fix that. We will give our characters the ability to perform actions that restore their needs. This is the other half of the core simulation loop that will eventually drive all autonomous behavior.
Our learning outcome for this lesson is to create four need-fulfilling interactions (e.g., eat, sleep) that modify need values in the Rust backend. We will define what these interactions are, implement their effects on a character's state, and write tests to prove they work as expected.
1. The Core Simulation Loop: Needs and Actions
At its heart, a life simulation game is a cycle:
- A character's needs decay over time.
- Low needs create a motivation to act.
- The character performs an action to fulfill the need.
- The cycle repeats.
This simple loop is the foundation for creating believable, autonomous characters. To get a feel for this in practice, let's watch a short clip from a video about a simple evolution simulator.
I Made an Evolution Simulator (with silly little guys)
The video "I Made an Evolution Simulator" by Icoso shows this core loop in its most basic form. Notice how the creatures' internal needs (hunger, thirst) directly drive their external actions (searching for food/water).
Watch from 00:22 to 01:08. Pay attention to the direct cause-and-effect relationship: low need leads to a specific, need-fulfilling behavior.
Just like the "goblets" in the video, our characters need a set of actions they can perform to counteract their decaying needs. Our goal today is to build the backend logic for these actions.
2. Modeling Interactions in Rust
Before we write code, let's decide how to represent an "interaction." An interaction like "Eat a meal" can be complex, involving multiple steps: walk to the fridge, get ingredients, cook, sit down, and finally, eat.

For this lesson, we will model our interactions as atomic. When a character performs the Eat interaction, their hunger will be replenished instantly in the backend. Later in the course, we'll connect these backend commands to animations and object interactions in Unity, but the core state change happens here in Rust.
The simplest and most robust way to represent a fixed set of actions in Rust is with an enum. Let's define one for the four needs we've implemented: hunger, energy, social, and fun.
In your Rust project, add the following enum:
// You can add this near your other struct/enum definitions in lib.rs
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Interaction {
Eat,
Sleep,
Socialize,
PlayGame,
}
#[derive(Debug, Clone, Copy, PartialEq)]allows this enum to be easily printed for debugging, copied, and compared, which will be useful for our tests and future AI logic.
3. Implementing Need-Fulfilling Logic
Now that we have a way to represent what a character can do, we need to implement the effect of doing it. We will add a method to our Character struct that takes an Interaction and modifies the character's Needs accordingly.
Open the file containing your Character struct and add this impl block:
// In the file with your Character struct
impl Character {
/// Applies the effects of a given interaction to the character's needs.
pub fn perform_interaction(&mut self, interaction: Interaction) {
match interaction {
Interaction::Eat => {
// Eating restores a large amount of hunger.
self.needs.hunger = (self.needs.hunger + 0.7).min(1.0);
}
Interaction::Sleep => {
// Sleeping restores a large amount of energy.
self.needs.energy = (self.needs.energy + 0.8).min(1.0);
}
Interaction::Socialize => {
// Socializing restores social need and a little fun.
self.needs.social = (self.needs.social + 0.6).min(1.0);
self.needs.fun = (self.needs.fun + 0.2).min(1.0);
}
Interaction::PlayGame => {
// Playing a game restores a lot of fun, but slightly drains energy.
self.needs.fun = (self.needs.fun + 0.7).min(1.0);
self.needs.energy = (self.needs.energy - 0.1).max(0.0);
}
}
// After any interaction, it's a good idea to update the mood.
self.update_mood();
}
}
Let's break this down:
pub fn perform_interaction(&mut self, interaction: Interaction): This public method allows external code to tell a character to perform an action. It takes&mut selfbecause it needs to modify the character's internal state (their needs).match interaction { ... }: Amatchstatement is the perfect tool for handling different enum variants. It's clear, and the Rust compiler will ensure we've handled every possibleInteraction.self.needs.hunger = (self.needs.hunger + 0.7).min(1.0);: For each interaction, we add a certain amount to the relevant need. We then use.min(1.0)to clamp the value, ensuring it never goes above 1.0. Similarly, we use.max(0.0)to prevent a need from dropping below 0.0. This is crucial for keeping our data valid.- Notice that interactions can affect multiple needs.
Socializerestores both social and fun, whilePlayGamerestores fun but costs a little energy. This allows for more interesting gameplay trade-offs. self.update_mood(): After changing the needs, we immediately recalculate the character's mood to reflect their new state.
4. Testing the Interactions
We've written the logic, but how do we know it works? We don't need to boot up Unity and build a complex UI to find out. We canβand shouldβverify our backend logic with a unit test. This ensures our core simulation is solid before we add more layers on top.
Add the following test to your #[cfg(test)] mod tests block:
#[cfg(test)]
mod tests {
use super::*;
// ... your previous tests for clock, needs, etc. ...
#[test]
fn character_can_fulfill_needs_with_interactions() {
// 1. Setup
let mut character = Character::new("Testy".to_string());
// 2. Test Eating
character.needs.hunger = 0.2; // Set hunger to a low value
character.perform_interaction(Interaction::Eat);
assert!(character.needs.hunger > 0.8, "Eating should restore hunger.");
// 3. Test Sleeping
character.needs.energy = 0.1;
character.perform_interaction(Interaction::Sleep);
assert!(character.needs.energy > 0.8, "Sleeping should restore energy.");
// 4. Test Socializing
character.needs.social = 0.3;
character.needs.fun = 0.5; // Note the starting fun value
character.perform_interaction(Interaction::Socialize);
assert!(character.needs.social > 0.8, "Socializing should restore social need.");
assert!(character.needs.fun > 0.6, "Socializing should also restore some fun.");
// 5. Test Playing a Game
character.needs.fun = 0.1;
character.needs.energy = 0.5; // Note the starting energy value
character.perform_interaction(Interaction::PlayGame);
assert!(character.needs.fun > 0.7, "Playing a game should restore fun.");
assert!(character.needs.energy < 0.5, "Playing a game should drain some energy.");
}
}
This test is very explicit:
- It creates a character.
- For each interaction, it artificially sets the relevant need(s) to a low value.
- It calls our new
perform_interactionmethod. - It asserts that the need values have changed in the way we expect.
Run cargo test from your terminal. The test should pass, giving you confidence that your need-fulfillment logic is correct.
Conclusion
Excellent work! You have now implemented the crucial counterpart to need decay. Our characters are no longer doomed to a slow decline; they now have the means to care for themselves.
Key Takeaways:
- The core simulation loop consists of needs decaying and actions restoring them.
- Enums are a type-safe and clear way to represent a finite set of actions or
Interactions. - Interaction logic can be cleanly implemented in a method on the
Characterstruct using amatchstatement. - Always clamp need values (e.g., between 0.0 and 1.0) to prevent invalid data states.
- Unit tests are essential for verifying backend logic quickly and reliably, confirming that your interactions have the intended effects.
We have now created the what (the interactions) and the how (the perform_interaction method). In our next lesson, we will tackle the why. We'll build a simple autonomous AI in Rust that can look at a character's needs and decide which interaction to perform, taking our first major step towards creating intelligent, self-sufficient characters.