14. Enemy AI and adaptive difficulty
In short: Every enemy (and every companion nobody plays) fights with an AI profile: a small data asset that says what the unit goes after. One utility AI serves every battle mode. A single number per mode, the intelligence (0 clumsy, 1 sharp), says how well it does it, and adaptive difficulty moves that number slowly from how the player is doing. Boss phases, team roles, rules and scorers make special enemies without code. If you already have Behavior Trees or StateTrees, nodes let them ask the utility AI what to do.
14.1 Basics
14.1.1 The idea: profile plus intelligence
- The profile (
URSEAIProfile, assets namedDA_AI_*) is what the unit wants: damage, kills, healing, statuses, safety, distance, protecting allies. - The intelligence is how well it pursues that. At low values it sometimes picks the second or third best move (plausible mistakes, never stupid ones), forgets weaknesses, ignores danger and rarely coordinates. At high values it uses weaknesses, focuses fire, guards its team, anticipates and (in the live modes) answers telegraphs with a well-timed dodge.
- The player never chooses a difficulty. The game starts at "Medium" (0.5) and adapts the enemies' intelligence only, never their stats or the rewards. Player progress (level, gear) always counts.
The same profile drives grid Tactics, Free Tactics, ATB, turn-based and Action battles. Each mode has an adapter that turns the shared scoring into its own commands.
14.1.2 Pick a ready style
- RPG Saga Studio → New enemy, step Abilities and AI: pick a style card. Each card has a one-line description.
- Want something else? Studio → New AI style (or Configurator → Tools → AI Profile Preview → New AI style...):
- pick a base style (the Boss tab lists the boss styles);
- name it, choose its team role and move the sliders;
- check the curves over intelligence;
- Simulate: your starting party fights test enemies that play the new style, then the base style;
- Create. The profile appears in the New Enemy wizard's list.
The ready styles:
| Style | Role | What it does |
|---|---|---|
| Berserker | Damage | Charges the nearest foe and never backs off; hits harder the more hurt it is |
| Sniper | Damage | Shoots from as far as it can and backs away from anyone who closes in |
| Summoner | Support | Calls its creatures first and stays safely behind them |
| Tank | Tank | Stands between enemies and its fragile allies, taunts, guards, blocks corridors |
| Controller | Controller | Sleeps, slows and weakens before dealing damage; casters and healers first |
| Coward | None | Weak hitter that runs away when hurt |
| Healer | Support | Keeps allies alive: raises and heals first, buffs, stays out of reach |
| Skirmisher | Damage | Darts in to strike from the side, then pulls back |
| Ambusher | Damage | Holds position until a foe is within reach, then strikes hard |
| Pack Hunter | Damage | Every member goes for the same prey until it falls |
Older profiles (Aggressive, Cautious, Guardian, Hunter, Support, Trickster) stay available. Boss styles are a profile plus phases: Juggernaut, Summoner Lord, Phase Shifter, Glass Cannon Caster, Enrage Timer and Duelist. They use only plugin content, so they work in an empty project. Replace the abilities and summons with yours.
14.1.3 Bosses and phases (no code)
A boss profile has Phases, entered in order at safe points (before any AI decision). In the AI Style wizard's Boss tab each step is one phase:
- When: HP below a percentage, an own turn count, a number of allies down, or custom conditions.
- Then: a banner and a line, statuses gained or removed, abilities gained or lost, units that join (count, how many turns), a new behavior (profile), a second wind (HP restored to a percentage), a cinematic.
Everything else of a phase (extra triggers, rules) stays editable in the profile. Create writes DA_AI_<Name> in your project. Creating again with the same name updates the profile in place, so enemies using it keep it. Plugin styles are never changed.
14.1.4 Set per-encounter limits
On an Encounter asset, Min Intelligence and Max Intelligence bound what adaptive difficulty may do for that fight. A boss with Min Intelligence 0.7 never drops below 0.7, however poorly the player does. To pin a fight at one value, set both to it.
14.2 How it works
Battle starts
-> URSEDifficultySubsystem::BeginBattle resolves the intelligence for the mode and encounter
-> each AI turn: the mode's adapter builds options (abilities x targets x positions)
-> the shared utility brain scores them: profile weights (curves over intelligence) + rules + scorers + role terms
-> "decision quality" noise: at low intelligence the 2nd or 3rd best option may win
-> the Decision Policy (optional Blueprint) may override the choice
-> the mode turns it into a command (Free Tactics, Tactics, ATB, turn-based or Action)
Battle ends
-> URSEDifficultySubsystem::RecordBattle measures it and nudges the intelligence for that mode
14.2.1 Utility scoring
Every option gets a score from the profile's weights. A weight is an FRSEAIScalar: a constant, a range over intelligence (Min at 0, Max at 1) or a curve. Weights include damage, kill bonus, flanking, status value, healing, danger avoidance, preferred distance, protecting allies, retreat HP, resource cost, and the intelligence knobs: Decision Quality, Weakness Knowledge, Danger Awareness, Focus Fire, VIP Focus, Pact Awareness, Charge Interrupt and Resource Management. Gates switch whole behaviors (flanking, protecting the support, combos) on only from a given intelligence. Global numbers (the noise temperature at low and high intelligence, focus-fire weight, item use penalty, summon value) are in Project Settings → AI.
14.2.2 Team roles
A profile's Role gives the unit team behavior in every mode:
| Role | Behavior |
|---|---|
| Tank | Holds the line: stands on the enemy's way to a fragile ally (support, controller, VIP, a healer, a badly hurt ally), prefers taunts and guards allies the enemy can reach |
| Damage | Goes for kills (a likely kill is worth more) and high-value targets: dangerous enemies, casters, healers |
| Controller | Disables and debuffs before raw damage, casters and healers first, areas that catch several enemies |
| Support | Heals, plus buffs on allies enemies can reach; the others guard it |
Only the weights of the unit's own role apply (the Team Role category of the profile).
14.2.3 Rules and scorers
- Rules (
FRSEAIRule, "gambits") are ordered if-then entries evaluated inside the pipeline. When every condition holds they Force an action, add a score Bonus or Forbid abilities. Each can apply only within an intelligence band. - Scorers (
URSEAIScorer) are extra considerations added to every ability option: weight times a raw value. Built in: Target Status, Target Missing HP, Target Threat, Target Is Caster Or Healer, Preferred Range, Ally Needs Protection, Area Value and Setup Combo. - Conditions (
URSEAICondition) are small tests used by rules and phases: Health, Resource, Attribute, Weak To Element, Has Status, Enemies In Range, Unit Count, Turn Number, Phase, Intelligence, Target Is Focus, Not, Any Of, and use conditions such as Has Tag, Equipped, Row, Position, Identity, Battle Mode and Story.
14.3 Key classes and assets
| Class / asset | Module | Role |
|---|---|---|
URSEAIProfile | RSECore | The AI profile data asset: weights, gates, role, rules, scorers, phases, last stand, decision policy |
FRSEAIScalar | RSECore | A value that may change with intelligence (constant, range or curve) |
FRSEAIRule | RSECore | One if-then rule |
FRSEAIPhase | RSECore | One boss phase |
URSEAICondition, URSEAIScorer | RSECore | Abstract, Blueprintable extension points |
URSEAIDecisionPolicy | RSECore | Abstract, Blueprintable: the last word on every decision |
URSEAIBattleBrain | RSECore | The AI state of one battle: phases, team blackboard, debugger records |
URSEFreeTacticsAIController, URSEActionAIController | RSECore | Mode controllers for Free Tactics and Action |
URSEAISettings | RSECore | Global AI numbers (Project Settings → AI), a rules-profile section |
URSEDifficultySubsystem | RSECore | Game-instance subsystem: owns the adaptive state, resolves intelligence, records battles |
URSEDifficultySettings | RSECore | Project Settings → Difficulty |
URSEAIStyleLibrary | RSECore | Blueprint/Python helpers behind the AI Style wizard |
URSEAIInterop | RSE | Shared logic of the Behavior Tree and StateTree nodes |
URSEActionInterop, URSEActionAgents | RSE | Action battle side of the interop, and finding an actor's action agent |
URSECombatRoleRegistry, URSECombatRoleSettings | RSECore | What your props mean in battle (cover, obstacles, ...) |
14.4 Settings
| Where | What |
|---|---|
| Project Settings → RPG Saga Engine → AI | Candidate limits (Max Move Candidates), noise (Noise Top Options, temperature at min and max intelligence), danger awareness floor, focus fire, VIP multiplier, ATB charge interrupt, item penalties, revive and summon value |
| … → Action AI | Real-time numbers: think rate, positioning, reactions, party orders (see Action battles) |
| … → Free Tactics → AI | Candidate positions, cover, height, spacing, overwatch and opportunity risk weights, single move, jumps |
| … → Difficulty | The adaptive controller (below) |
| … → AI Debugger | The in-game debugger |
| … → Combat Roles | Roles, suggestions, registries |
14.4.1 Adaptive difficulty settings
Tactics and ATB each have their own intelligence and their own history, so an ATB fight never moves the Tactics value (the turn-based modes use the ATB value).
| Setting | Default | Meaning |
|---|---|---|
| Enabled | on | Off: the encounter's own intelligence is used and nothing is recorded |
| Tactics / ATB → Start Intelligence | 0.5 / 0.5 | "Medium" at a new game |
| Tactics → Target / Worst Turns Per Enemy | 4 / 10 | Tempo that counts as "even" and as "struggling" |
| ATB → Target / Worst Turns Per Enemy | 3 / 8 | The same for ATB |
| Min / Max Intelligence | 0.15 / 1 | Global limits |
| Window Size | 5 | Battles averaged per mode |
| Min Battles To Adjust | 3 | Battles needed before the first change |
| Gain | 0.2 | Speed of adjustment |
| Dead Zone | 0.05 | Distance from "even" that changes nothing |
| Max Step Up / Down | 0.05 / 0.03 | Largest change per battle; falling is slower than rising |
| Tempo / Damage / Fallen Weight | 1 / 1 / 1 | Weights in the battle score |
| Target / Worst Damage Taken Share | 0.35 / 1 | Share of the party's max HP lost |
| Target / Worst Fallen Share | 0.1 / 0.75 | Share of units that fell |
| Retry Penalty | 0.1 | Score lost per retry of a won fight |
| Retry Weight Falloff | 0.5 | Weight of repeated defeats at one encounter (1, 0.5, 0.25...) |
| Record Escapes | off | Whether fleeing counts |
How it works: after each battle the subsystem scores it from 0 (the player struggled) through 0.5 (even) to 1 (the player dominated), from turns per enemy, damage taken and units fallen. A defeat scores 0. The controller is relative: it moves the intelligence only while the average of the last few battles sits above or below 0.5, so a player who plays an even game at 0.8 stays at 0.8. Repeated losses at one encounter count less and less, so losing on purpose does not fool it.
The intelligence a battle actually uses comes from, in order: the RSE.Difficulty.Override console variable (0 to 1, debug), else the adaptive value for the mode clamped to the global limits and then the encounter's Min and Max (the encounter's own Intelligence field is ignored while adaptive difficulty is on), else the encounter's Intelligence when adaptive is off. Rules profiles can override the Difficulty section per zone or boss.
14.5 Blueprint usage
14.5.1 Make a "royal guard" with a Decision Policy
A Decision Policy has the last word on a unit's decision, after the utility scored and picked an option.
- Create a Blueprint with the parent AI Decision Policy.
- Override Override Decision: you get the context, the options (best first: kind, ability, item, target, score) and the index the AI picked. Set Out Index and return true to take another option.
- Override Override Target: when the final option uses an ability, you get every unit that ability was scored on. Return true with another Out Target Id to aim it there.
- Set it on the profile (Decision Policy). Keep it deterministic: read the battle, never change it, no global random. Rules marked Force are not sent to the policy.
Use the RPG Saga | AI nodes to read the battle: Get Unit, Get HP Percent, Has Status, Get Resource, Get Attribute, Is Weak To, Get Distance, Are Enemies, Count Units, Get Team Focus Target, Get Unit Ids, Evaluate Scalar.
For a "royal guard" that never attacks the player's leader while the queen lives: in Override Target, if the candidate is the VIP and a Queen team value is set, return another candidate.
14.5.2 Add your own scorer or condition
- Parent AI Scorer, override Score (Context → float). Add it in the profile's Scorers with a Weight.
- Parent AI Condition, override Is Met (Context → bool). Use it in rules and phases.
14.5.3 Read or set intelligence
On the difficulty subsystem: Get Mode Intelligence, Set Mode Intelligence and Get Intelligence For Encounter.
14.5.4 Change a unit's behavior mid-fight
A battle script effect Change AI Profile (URSEBattleScriptEffect_ChangeAIProfile) swaps the profile from a scripted beat. Boss phases do the same through their Profile field.
14.6 C++ usage
A custom scorer:
UCLASS(meta = (DisplayName = "Prefer Burning Targets"))
class UMyBurningScorer : public URSEAIScorer
{
GENERATED_BODY()
protected:
virtual float Score_Implementation(const FRSEAIContext& Context) const override
{
FRSEBattleUnit Target;
if (!URSEAIBlueprintLibrary::GetUnit(Context, Context.TargetId, Target)) { return 0.f; }
return URSEAIBlueprintLibrary::HasStatus(Context, Context.TargetId, MyTags::Status_Burning) ? 1.f : 0.f;
}
};
Read the adaptive value from the game-instance subsystem:
if (URSEDifficultySubsystem* Difficulty = GetGameInstance()->GetSubsystem<URSEDifficultySubsystem>())
{
const float Tactics = Difficulty->GetModeIntelligence(RSEGameplayTags::Battle_Mode_Tactics);
UE_LOG(LogTemp, Log, TEXT("Tactics intelligence %.2f"), Tactics);
}
The state (FRSEDifficultyState) is SaveGame data: each mode's value and window, plus retries per encounter. It is part of the save (see Save and load). ResetToNewGame() puts everything back to Medium.
Extension points: subclasses of URSEAICondition, URSEAIScorer and URSEAIDecisionPolicy (C++ or Blueprint), the URSEFreeTacticsController for a fully custom Free Tactics decision-maker, and the interop below.
14.7 Advanced
14.7.1 Combat roles: what your props mean in battle
Battles run on your real map. A thick tree trunk should protect a unit behind it, a bush should not, a pebble should not matter and a pile of buckets should not end up in the middle of a fight. Combat roles tell the engine what each object means. You set them once per asset (a folder at a time if you like), and every placed copy follows. Nothing in your asset packs is modified: roles live in a registry asset in your project (/Game/Generated/DA_CombatRoles), listed in Project Settings → Combat Roles → Registries.
| Role | In battle |
|---|---|
| Full cover | Blocks line of sight and projectiles; occupies its tiles |
| Half cover | Attacks against a unit behind it hit less often; occupies its tiles |
| Obstacle | Cannot be crossed, protects nobody |
| Difficult terrain | Can be crossed, costs more movement |
| Climbable / high ground | Units stand on it and get the height advantage |
| Destructible cover | Half cover that can be broken (Free Tactics gives it hit points) |
| Explosive | Half cover that explodes when broken |
| Overhead | Looked through; the Free Tactics camera cuts it away |
| Neutral decoration | Does not matter; the grid looks through it |
| Avoid in combat areas | Small clutter that placement tools keep out of combat areas |
Quick start:
- In the Content Browser, right click an asset-pack folder → RPG Saga: Suggest Combat Roles (Unassigned Only). Every mesh and Blueprint gets a suggestion from its shape and its name.
- Right click the folder → RPG Saga: Combat Roles... to review the list in the Combat Role Configurator. Check rows, pick a Role, Apply to Checked. Manual choices always win over suggestions.
- Press Bake Grid again on Tactics zones (roles are read at bake time).
One placed object can differ from its asset: add the RSE Combat Role component to the actor, or an actor tag named like the role tag (for example CombatRole.Neutral). Place an RSE Combat Area over spots where you want battles and press Check to count free tiles and list clutter. In Blueprint, the node group RPG Saga | Combat Roles has Get Component Role, Get Actor Role, Get Asset Role, Get Cover Info, Is In Combat Area and Can Place At. Roles are data: add your own in Project Settings → Combat Roles → Roles with a new CombatRole. gameplay tag.
14.7.2 Behavior Tree and StateTree interop
Already have enemies, companions or NPCs run by a Behavior Tree or a StateTree? Keep them. Nodes for both let your trees ask the utility AI what to do, carry the answer out, keep a distance, start a battle and test the plugin's conditions. Movement, animation and combat code stay yours. Behavior Tree nodes are built into the RSE module; StateTree nodes are in the RSEStateTree module (enable RPG Saga Engine - StateTree Bridge in Edit → Plugins).
| What it does | Behavior Tree | StateTree |
|---|---|---|
| Asks the utility AI what the pawn should do | task RSE: Pick Best Action | task RSE: Pick Best Action |
| Carries an action out | task RSE: Execute Ability | task RSE: Execute Ability |
| Walks to a distance from a target | task RSE: Move To Preferred Range | task RSE: Move To Preferred Range |
| Starts a battle with the party | task RSE: Start Battle With Target | task RSE: Start Battle With Target |
| Runs an Interaction asset | (your own task) | task RSE: Run Interaction |
| Tests plugin conditions | decorator RSE: Condition | condition RSE: Condition, RSE: Flag Is |
| Is the pawn in a battle / its turn | decorator RSE: In Battle | condition RSE: In Battle |
| Does a unit have a status | decorator RSE: Has Status | condition RSE: Has Status |
| Tracks target, threat, distance | service RSE: Update Threat / Target | evaluator RSE: Update Threat / Target |
For action battles there are nodes of their own, with the same names in both systems: RSE Action: Attack, RSE Action: Use Ability, RSE Action: React To Threat, RSE Action: Keep Preferred Range, RSE Action: Target In Reach (decorator or condition) and RSE Action: Incoming Telegraph (decorator or condition).
Behavior Tree setup. Make a Blackboard with the keys you want (Target Actor as Object of base class Actor; Kind as Name; Ability as Object of base class RSE Ability Data; Item as Object of base class RSE Consumable Data; Target Location as Vector; Door Index as Int; Score, Threat and Distance as Float; In Battle as Bool). In your tree, pick the keys in each node's Output and Input groups. Every key is optional. BT_RSE_ExampleEnemy with BB_RSE_ExampleEnemy is in the plugin's example content. Its branches, from the top: fight on its turn (RSE: In Battle with Require Own Turn → Pick Best Action → Execute Ability), hunt the party (Move To Preferred Range → Start Battle With Target) and wait. If it is missing, run RSE.AIInterop.CreateExampleBT in the editor.
StateTree setup. Use the StateTree AI Component schema or the StateTree Component schema. Bind Pick Best Action's Output properties (Kind, Ability, Item, Target Actor, Target Location, Door Index, Score) to the inputs of Execute Ability. ST_RSE_ExampleEnemy is the example; RSE.StateTree.CreateExample creates it if missing.
Who decides. The battle stays the judge: whatever a tree does in a battle is a command like any other, validated and applied by the battle (the same in every mode and in the simulator). In Free Tactics the enemy side is played by the battle's own AI controller, and trees read and act around it. In Grid Tactics and ATB a tree can play a whole side: give it to an AI controller with no pawn and call Claim Battle Side (library RSE AI Interop, with the controller and the side's team tag). Release Battle Side hands it back. In action battles each unit is either the framework's or your pawn's own brain.
Your pawn in an action battle. Implement the RSE Action Agent interface (Class Settings → Interfaces) on your pawn or a component:
| Function | What it does |
|---|---|
| Can Accept Action Order | The pawn can take an order now |
| Order Move To (Location, Acceptance Radius) | Walk or run there; return true when you took it |
| Order Use Ability (Ability, Target Actor, Target Location) | Play the ability with your animation and code |
| Order Stop | Cancel the order |
| Is Busy With Order | An order is still being carried out |
| Get Action Battle Control | Framework (our action AI plays the unit, the default) or Own Brain (your tree plays it) |
| On Action Battle Joined / On Action Battle Left | Optional notifications |
To drive a boss with your own Behavior Tree: make the boss a Character with an AI controller, add the interface and return Own Brain, start the Action encounter passing your pawn in Enemy Actors, copy BT_RSE_ExampleBoss from the example content and run it. It answers the telegraph (RSE Action: Incoming Telegraph → React To Threat), strikes when in reach (Target In Reach → Attack) and closes in (Keep Preferred Range with Use Ability Reach). Add your own phase decorator above Strike and put Use Ability with the boss's special move in it.
The action nodes act only on the machine that hosts the battle; on a guest they read and do nothing. Decisions happen when a node runs, never every frame, and use the battle's seed, so the same state gives the same decision.
Without a tree. The library RSE AI Interop offers Is Actor In Battle, Is Actors Turn, Actor Has Status, Pick Best Action For (returns a decision without doing anything: good for hints) and Execute Decision.
14.7.3 Debugging the AI
RSE.AI.Debug 1(or the AI button of the battle HUD): every decision with its top options and score terms (for example Role: kill focus, Role: taunt, Scorer: <name>). The debugger also notes when a Decision Policy changed a decision.RSE.Difficulty.Override <0-1>fixes the intelligence of every battle (-1turns it off).RSE.Difficulty.Log 1logs every recorded battle (metrics, score, weight, change).- The battle simulator prints an
enemy AI:line with behavior statistics. See Battle fundamentals. - Configurator → Tools → AI Profile Preview shows the curves, gates, rules, scorers and phases of a profile.
14.7.4 Performance notes
Decisions are sampled and capped: Max Move Candidates in AI settings, AI Candidate Positions in Free Tactics, a think interval in Action AI. Weights resolve once per decision, so curves cost nothing inside the scoring loop. Lower the caps for weak hardware.
14.8 Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Enemies always play perfectly or terribly | Intelligence pinned by the encounter's Min and Max, or by RSE.Difficulty.Override | Widen the limits, set the override to -1 |
| Changing the encounter's Intelligence does nothing | Adaptive difficulty is on, and it ignores that field | Pin with Min and Max Intelligence, or turn Difficulty → Enabled off |
| The AI never gets harder | Fewer than Min Battles To Adjust battles, or the player sits inside the Dead Zone | Play more battles, or lower the dead zone; use RSE.Difficulty.Log 1 |
| A custom style does not appear in the New Enemy wizard | It was not created with Create | Run Create in the AI Style wizard |
| Pick Best Action always fails | The pawn is not a unit of a running battle, or it is not its turn | Check with Is Actor In Battle and Is Actors Turn; put RSE: In Battle with Require Own Turn above it |
| Execute Ability fails in battle | The battle refused the command (not active, not enough AP or MP, target out of range) | See the log category LogRSEAIInterop |
| Our AI and my tree both move the boss | The unit is not excluded from the framework AI | Return Own Brain from Get Action Battle Control before the battle starts |
| A Grid Tactics or ATB side does nothing | The controller has a pawn, or was not claimed | Use a controller with no pawn and Claim Battle Side with the side's team tag |
| A tree gives no cover | A combat role missing, or collision only on the canopy | Check the blocking volume in the Combat Role Configurator and set the role by hand |
| Roles did nothing on the grid | The grid was not baked again | Bake the Tactics zones again and check Bake Uses Combat Roles |
14.9 See also
- Battle fundamentals, Grid Tactics, ATB and turn-based, Free Tactics (Live) and Action battles.
- Characters and party for which profile a character uses.
- Product pages in
docs/product/:enemy-ai.md,ai-interop.md,combat-roles.md,ai-workflow.md,balance-tool.md. - Appendix A, Appendix B, Appendix C and Appendix D.