The Low-Tech AI of Elden Ring
The Low-Tech AI of Elden Ring
FromSoftware is legendary for creating NPC encounters that are both diverse and mercilessly punishing across the Soulsborne series. However, if you look under the hood, the actual decision-making logic is surprisingly primitive.
Because much of the logic is written in Havok Script (a specialized Lua implementation by Havok), it is relatively simple to peek behind the "fog wall" and analyze the implementation.
Disclaimer: This is not original research. I am interpreting and summarizing code that has already been extracted, decompiled, and reverse-engineered by the community.
The Concept of "Goals"
The cornerstone of FromSoftware's AI is the Goal. In their terminology, a Goal is a specific state an AI entity can occupy.
- Nature: Goals are essentially immutable tables of functions.
- Flexibility: They can be parameterized upon instantiation and can read data directly from the
Actor(the NPC).
While one might expect a complex Hierarchical Finite State Machine, FromSoftware utilizes a stack of states.
How the Goal Stack Works
Every frame, the Actor updates the Goal currently at the top of its stack. This Goal can then push "Sub-Goals" onto the stack, which will be executed in the subsequent frames.
The update function returns one of three states:
Continue: The stack remains as is.Success: The current Goal is popped off the stack.Failure: The current Goal is popped, and all subsequent Sub-Goals are cleared until the system returns to the Parent Goal (the one that originally pushed the sub-goal).
Visualizing the Stack Flow
Example Scenario:
Imagine a CoolBossBattle Goal. It pushes a sequence of attacks:
- Stack Level 0:
CoolBossBattle - Stack Level 1:
Attack (R2, Finisher) - Stack Level 2:
Attack (R2, Repeat) - Stack Level 3:
Attack (R2, Combo)Currently Updating
If the "Combo" attack lands, it returns Success and is popped. If the "Repeat" attack then fails, the stack unwinds entirely back to CoolBossBattle, allowing the boss to decide a new course of action.
The activate Function: The Brain of the AI
In the API, the root of the stack is the "Top Level Goal." The most critical logic resides in the activate callback. This function runs:
- The first time a Goal is updated.
- Whenever a Goal resumes execution after its Sub-Goals have been exhausted.
The activate function determines the next move using a combination of world context, Actor data, and randomness.
Weighted Random Selection
The AI typically uses a weighted random system to pick an "Action" (a function). The probability of an action can be expressed as:
Decision Matrix Example
Based on the distance to the player, the AI adjusts weights:
| Distance | Giga Death Ray | Leap Attack | Ground Slam | Light Combo | Heavy Combo |
|---|---|---|---|---|---|
| Far () | 15.0 | 65.0 | 0.0 | 10.0 | 10.0 |
| Mid () | 0.0 | 0.0 | 5.0 | 60.0 | 35.0 |
| Close () | 0.0 | 0.0 | 20.0 | 40.0 | 40.0 |
Pseudocode Implementation
Here is a simplified representation of how this looks in a Rust-like syntax:
fn action_light_attack_combo(goals: Goals, actor: Actor) {
// ApproachTarget is a common goal
goals.push_sub_goal(Goal::ApproachTarget, Target::Enemy);
goals.push_sub_goal(Goal::Attack, AnimId::R1, Combo::Initial);
goals.push_sub_goal(Goal::Attack, AnimId::R1, Combo::Repeat);
goals.push_sub_goal(Goal::Attack, AnimId::R1, Combo::Finisher);
}
fn activate(self, goals: Goals, actor: Actor) {
let dist = actor.target_distance(Target::Enemy);
// Determine weights based on distance
let mut weights = if dist > 6.0 {
[15.0, 65.0, 0.0, 10.0, 10.0]
} else if dist > 1.5 {
[0.0, 0.0, 5.0, 60.0, 35.0]
} else {
[0.0, 0.0, 20.0, 40.0, 40.0]
};
// Apply cooldowns to modify weights
if common::is_cooldown(goals, actor, AnimId::R1, 8.0) {
weights[3] = 0.0;
}
if common::is_cooldown(goals, actor, AnimId::R2, 10.0) {
weights[4] = 0.0;
}
let actions = [
action_giga_death_ray,
action_leap_attack,
action_ground_slam,
action_light_attack_combo,
action_heavy_attack_combo
];
// Roll the dice and execute
common::battle_activate(goals, actor, weights, actions);
}
Final Considerations
The AI's behavior is further tuned by:
- RNG Rolls: Simple random number generation from the Actor.
- HP Thresholds: Changing weights when health drops.
- Parameter Reading: Simple Goals that just push sub-goals based on their own settings.
By nesting these simple, weighted decisions within a stack, FromSoftware creates the illusion of complex, adaptive combat behavior using very straightforward tools.