AI Agent Guide: Build Unreal Engine Combat with Claude Code, Codex or Cursor
Documentation Unreal Engine AI Tutorial
An Unreal Engine 5 combat plugin made for AI coding agents: hand Claude Code, Codex or Cursor the guide the plugin ships, and it builds a souls-like enemy from your own mesh and animations in your open editor, with no C++.
The plugin ships a guide written for an AI coding agent; the agent itself is not included, so use the one you already have. Hand it the guide with the editor open, and it builds an enemy from your own mesh and animations, then a weapon, combos, reactions and a boss.
The guide is Resources/AGENTS.md in the plugin folder, and its full text is at the end of this page. It tells an agent such as Claude Code, Codex or Cursor how to reach your open Unreal Editor, what to ask you, and every step of each build, with checks along the way. Everything it makes is assets and Blueprints in your own folder, with no C++.
Set it up
2 minutes, then the build
1
Open your project
Open the project in the Unreal Editor and leave it open. The agent works in the running editor.
2
Hand your agent the guide
Do one of these in your agent's chat:
Drag AGENTS.md from the plugin's Resources folder into it.
Paste this page's link: https://rigbak.com/docs/soulslike-combat/agents.
Type: "Read Resources/AGENTS.md in the SoulslikeEnemyCombat plugin folder before you start".
A plugin installed from Fab sits in your engine folder, under Engine/Plugins/Marketplace/.
3
Say yes to the one-time setup
On first use the agent offers to point your project at the guide, so later sessions find it without being handed it. With your OK it adds one line naming the guide to CLAUDE.md and to AGENTS.md in your project folder: Claude Code reads the first, Codex and Cursor the second. For Claude Code it also copies the sec-agent skill into your project's .claude/skills folder, so typing /sec-agent starts a later session on the guide.
4
Ask for an enemy
Tell it what to build and where your assets are. It asks for anything it cannot work out from your asset names, builds step by step, and ends with a report of what it made and what it assumed.
There are two ways in. The agent uses the Python bridge unless you ask for Epic's Unreal MCP, or your session already has it connected. Either way it asks your OK before it turns on a plugin or edits your project's config files, and tells you when the editor needs a restart.
Route
Engine versions
What it needs
Python bridge
5.6, 5.7 and 5.8
The engine's Python plugin and its remote execution, turned on in your project: two edits the agent makes, then one editor restart. The bridge itself, Resources/AgentBridge/run_in_editor.py, runs with the engine's own Python, so nothing gets installed.
Epic's Unreal MCP
5.8
The engine plugins ModelContextProtocol and AllToolsets turned on, their server starting with the editor, and an entry for it in your agent's MCP settings; then a restart of the editor and of the agent session. The plugin adds a toolset of its own, SECAgentTools, for the steps Epic's tools do not cover.
Other Unreal MCP servers are not supported, even when your agent has one connected.
On either route the plugin's agent tools make montages, add notify windows to them, make a locomotion Animation Blueprint for a skeleton that has none, and add blade sockets to a weapon mesh.
The animations: a folder or a list. It sorts them by name into attacks, hit reactions, dodge, idle and locomotion, and asks when a name does not say which attack is light and which is heavy. It needs no death animation, since the enemy it builds goes limp when it dies; playing one instead is a Blueprint step on the enemy controller's On Death event, which it leaves to you.
An Animation Blueprint for that skeleton with a DefaultSlot Slot node. With none, it makes one from the plugin's locomotion template.
A weapon mesh, if the enemy holds one. An unarmed enemy hits with its hands.
The folder and name: /Game/Enemies/<Name>/ unless you say otherwise.
It creates everything in that folder and never edits the plugin's own content: to change a shipped asset, it duplicates it into your folder first. Outside that folder it adds blade sockets to your weapon mesh and places the enemy in the level you have open.
Soulslike Enemy Combat (SEC) is an Unreal Engine 5.6 to 5.8 plugin that gives a character souls-like enemy AI: it notices the player, keeps its distance, circles, picks attacks by range and timing, lands hits through swept weapon traces, and reacts to being hit. You build an enemy from data assets and a few Blueprints; no C++ is needed.
This file is written for an AI coding agent working in a customer's project while the Unreal Editor is open. Read all of it before you start. With the reference files in AgentGuide/ beside it, it is enough to build what it describes; do not search the plugin's source files.
Customer note: your agent may not find this file on its own. Fab installs the plugin under your engine's Engine/Plugins/Marketplace/ folder. Tell your agent "Read Resources/AGENTS.md in the SoulslikeEnemyCombat plugin folder before you start". The same text is at https://rigbak.com/docs/soulslike-combat/agents. On first use, the agent offers to point your project at this file, so later sessions find it on their own.
The first time this guide is used in a project, offer the customer this setup, which lets later sessions find the guide without being handed it. Make it only with their OK. It does not hold up the build: if they have not answered, build first and offer it again in your final report. Skip it when the project's CLAUDE.md or AGENTS.md already names this guide.
Find the plugin folder, the one holding SoulslikeEnemyCombat.uplugin: Plugins/SoulslikeEnemyCombat in the project folder, or, when Fab installed the plugin into the engine, a folder under Engine/Plugins/Marketplace/ in the engine folder ("The Python bridge" below, step 1, finds the engine folder).
Point the project at this guide. Add this line to CLAUDE.md and to AGENTS.md in the project folder, making a file that is missing, with the plugin folder written out (relative to the project folder when the plugin is inside it):
For Soulslike Enemy Combat, read <plugin folder>/Resources/AGENTS.md before building anything with it.
Claude Code reads CLAUDE.md; Codex and Cursor read AGENTS.md.
Claude Code only: the /sec-agent command. Copy the folder <plugin folder>/Resources/AgentSkills/sec-agent into the project's .claude/skills/ folder. Typing /sec-agent in a later session then starts it on this guide.
Everything below runs in the customer's open Unreal Editor. There are two ways in. Use the Python bridge unless the customer asks for Epic's Unreal MCP or this session already has it connected. Other Unreal MCP servers are not supported, even when one is connected.
The plugin ships AgentBridge/run_in_editor.py in its Resources folder, beside this file. If this file reached you on its own, without the AgentBridge folder beside it, find the plugin folder ("One-time setup in a project", step 1) and use its Resources/AgentBridge. Run with the engine's own Python, it sends Python to the open editor and prints what the editor printed. It needs a shell and nothing installed.
Read the project's .uproject. EngineAssociation is the engine version. For 5.6, the engine folder is the InstallLocation of the UE_5.6 entry in C:/ProgramData/Epic/UnrealEngineLauncher/LauncherInstalled.dat (JSON text despite its extension, so search it as text when a file reader refuses it), usually C:/Program Files/Epic Games/UE_5.6.
Check the connection:
"<engine folder>/Engine/Binaries/ThirdParty/Python3/Win64/python.exe" "<plugin>/Resources/AgentBridge/run_in_editor.py" --project "<path to the .uproject>" -c "import unreal; print(unreal.SystemLibrary.get_engine_version())"
It prints the engine version when it is connected. In PowerShell, put & and a space before the quoted python.exe path.
Exit code 2 means no editor answered. If the editor is closed, ask the customer to open the project. If it is open, the project needs a one-time setup. Ask the customer's OK, make these two edits, then ask them to restart the editor:
In the .uproject's Plugins list: { "Name": "PythonScriptPlugin", "Enabled": true }.
Write each step's Python to a file and run it: ... run_in_editor.py --project "<path to the .uproject>" step.py. Exit code 1 means the Python raised; the traceback follows.
Names a run defines stay defined in the editor until it restarts. Run the setup block in "Driving the editor with Python" once per editor session, and again after a restart or a NameError.
The editor does nothing else while a run is going. Never wait or sleep inside the Python; start Play in one run and inspect it in the next.
Epic's Unreal MCP is two engine plugins, ModelContextProtocol and AllToolsets. Its tools are list_toolsets, describe_toolset and call_tool. To set it up, ask the customer's OK, make these edits, then ask them to restart the editor and this agent session:
In the .uproject's Plugins list: { "Name": "ModelContextProtocol", "Enabled": true } and { "Name": "AllToolsets", "Enabled": true }.
In Config/DefaultEditorPerProjectUserSettings.ini, so the server starts with the editor:
The client entry, for Claude Code .mcp.json in the project root: { "mcpServers": { "unreal-mcp": { "type": "http", "url": "http://127.0.0.1:8000/mcp" } } }. Cursor reads the same shape from .cursor/mcp.json; Codex reads [mcp_servers.unreal-mcp] with url = "http://127.0.0.1:8000/mcp" from .codex/config.toml.
It runs no editor Python (its execute_tool_script only strings its own tools together in a sandbox), so use the tool table in "Driving the editor without Python". The plugin adds its own toolset, SECAgentTools (list_toolsets names it sec_agent_tools._toolset.SECAgentTools; pass that name as toolset_name), for the steps Epic's tools do not cover.
On both routes the plugin's agent tools make montages, notify windows and the locomotion Animation Blueprint, and add blade sockets to a weapon mesh. In Python they are the module sec_agent_tools, which ships with the plugin; on the Epic route they are the tools of the same names in SECAgentTools.
Ask only what you cannot work out from asset names. State your assumptions in your final report so the customer can correct them.
An enemy they already have. A request to change one of the customer's enemies ("turn my knight into a boss") works on that enemy. Find it before building anything; build a new one only when there is none, and ask which one when there are several:
reg = unreal.AssetRegistryHelpers.get_asset_registry()enemies = [str(a.package_name) for a in reg.get_assets_by_path("/Game", recursive=True) if (a.get_tag_value("NativeParentClass") or "").endswith(".EnemyCharacterBase'")]print(enemies)
On Epic's MCP, list the customer's assets with AssetTools.find_assets and read each Blueprint's parent with BlueprintTools.get_parent. Then read what the enemy has, its AIConfig and that config's sets, and build on them.
The mesh. The Skeletal Mesh asset path. Read its Skeleton from the mesh.
The animations. A folder or a list. Sort them by name into attacks, hit reactions, dodge, idle and locomotion. Ask when names do not say which attack is light and which is heavy. A death animation is not needed: the duplicated enemy goes limp when it dies. Playing one instead is a Blueprint step on the enemy controller's On Death event; tell the customer.
An Animation Blueprint for that skeleton, with a Slot node named DefaultSlot in its AnimGraph. If the customer has none, make one from the plugin's template ("No Animation Blueprint" below).
A weapon mesh, if the enemy holds one. Unarmed enemies hit with their hands.
The folder and name. Default: /Game/Enemies/<Name>/, with <Name> a short PascalCase name such as DarkKnight.
Create everything in the customer's folder. Do not edit or save anything under /SoulslikeEnemyCombat/; duplicate a plugin asset into the customer's folder when you need to change it.
Level steps work in the level open in the editor. If that level is the plugin's own (its path starts with /SoulslikeEnemyCombat/), ask the customer which of their levels to use, or make one in their folder.
Check each step as described before you move on. A wrong value fails silently in several places; the checks catch it.
Use the exact property names in this file. set_editor_property accepts the C++ name (SocketIDs, bUseAsWeapon) as well as the Python name.
In Python, a C++ type drops its U, A, F or E prefix: UEnemyAIConfig is unreal.EnemyAIConfig, FActionSpec is unreal.ActionSpec. Enum values are upper snake case: ESECTraceShape::CapsuleTwoPoint is unreal.SECTraceShape.CAPSULE_TWO_POINT. The engine's animation functions are unreal.AnimationLibrary.
An ability that SEC runs must reach End Ability on every path: completion, cancel and interrupt. The shipped GA_SEC_MontageAbility does; check any custom ability for it.
Give unreal.new_object the asset that will hold the object as its outer, never a class default object (unreal.get_default_object(...), defaults(...)), not even to look at a class's properties: a later object of that class with the same name takes it as its template, and the asset holding that object fails to save.
Run this once at the start of each Python session. Every later snippet uses it.
import unrealimport sec_agent_tools as secEAL = unreal.EditorAssetLibraryANIM = unreal.AnimationLibraryTOOLS = unreal.AssetToolsHelpers.get_asset_tools()def load(path): obj = EAL.load_asset(path) if obj is None: raise RuntimeError(f"No asset at {path}") return objdef bp_class(path): return EAL.load_blueprint_class(path)def new_data_asset(cls, folder, name): factory = unreal.DataAssetFactory() factory.set_editor_property("DataAssetClass", cls) return TOOLS.create_asset(name, folder, cls, factory)def duplicate(source_path, folder, name): return EAL.duplicate_asset(source_path, f"{folder}/{name}")def defaults(bp): return unreal.get_default_object(bp.generated_class())def bp_component(bp, name_start): sds = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem) lib = unreal.SubobjectDataBlueprintFunctionLibrary for handle in sds.k2_gather_subobject_data_for_blueprint(bp): obj = lib.get_associated_object(lib.get_data(handle)) if obj and obj.get_name().startswith(name_start): return obj return Nonedef tag(name): t = unreal.GameplayTag() t.import_text(name) return tdef tags(*names): return unreal.GameplayTagLibrary.make_gameplay_tag_container_from_array([tag(n) for n in names])def length(anim): return ANIM.get_sequence_length(anim)def save(*assets): for a in assets: if not EAL.save_loaded_asset(a, False): raise RuntimeError(f"{a.get_path_name()} did not save; log_warnings('EditorErrors') says why")def log_warnings(prefix="LogSEC"): name = unreal.Paths.get_base_filename(unreal.Paths.get_project_file_path()) path = unreal.Paths.project_log_dir() + name + ".log" with open(path, encoding="utf-8", errors="ignore") as f: return [l.strip() for l in f if prefix in l and ("Warning" in l or "Error" in l)]
Montage sections, a new gameplay tag, an asset's own checks as text: AgentGuide/Python.md.
Each call raises with a sentence saying what is wrong when it cannot do its job; read it and fix that before going on. log_warnings() is how you read SEC's setup warnings after a Play session. Each warning names the asset and the fix.
Epic's MCP lists three tools: list_toolsets, describe_toolset and call_tool. Use describe_toolset to read the exact argument names. In call_tool, toolset_name is the name list_toolsets prints, such as editor_toolset.toolsets.asset.AssetTools, and tool_name is the tool's own name, such as find_assets. Map each Python step to these tools:
Python step
Epic Unreal MCP tool
load, find assets
AssetTools.find_assets, AssetTools.load_asset
duplicate
AssetTools.duplicate
new_data_asset
DataAssetTools.create
Blueprint with a parent class
BlueprintTools.create
defaults(bp) and setting class defaults
BlueprintTools.get_default_object, then ObjectTools.set_properties
SECAgentTools tools of the same names. add_notify_state_to_montage takes the class path (for example /Script/SECCore.SECMeleeTraceNotifyState) and a track name (Combat).
The property names and values in every step are the same on both routes. How Epic's tools take values:
An asset is its object path: the package path, a dot, and the asset name again, as in /Game/Enemies/DarkKnight/AM_DarkKnight_Light.AM_DarkKnight_Light. The package path alone is refused.
A class is /Script/<Module>.<Name>, the C++ name without its first letter. SEC's classes are in three modules. SECMotion holds the approach window: /Script/SECMotion.SECApproachWindowNotifyState. SECCore holds damage configs, the melee trace, telegraph and state tag windows, telegraph shapes, the gameplay event and anim effect notifies, vitals and teams: /Script/SECCore.SECMeleeTraceNotifyState. SoulslikeEnemyCombat holds the rest (action and reaction sets, the AI config and
the awareness config, movement profiles, execution methods, payloads, scorers, gates, selectors, weapons, the encounter trigger): /Script/SoulslikeEnemyCombat.ActionSet, /Script/SoulslikeEnemyCombat.SECAwarenessConfig.
An instanced object (an execution method, a payload, a scorer, a selector) is made by giving its class path as a plain string: {"TargetSelector": "/Script/SoulslikeEnemyCombat.AwarenessFilteredTargetSelector"}, or in an array, {"Scorers": ["/Script/SoulslikeEnemyCombat.SECDistanceScorer"]}. Properties written beside the class path are dropped. Read the owner back with ObjectTools.get_properties, which gives the new object's path (
.../ActionSet_DarkKnight.ActionSet_DarkKnight:SECAbilityExecutionMethod_0), and set its own properties on that path.
An array changes one way per ObjectTools.set_properties call: elements added or removed while every other element stays exactly as get_properties returned it, or elements changed at the same length. Keep an existing instanced element as {"refPath": "<its path>"}. A call refused as ambiguous does both at once; split it in two.
Log lines come from LogsToolset.GetLogEntries, which requires pattern, a regular expression, and reads only its own category unless category is empty: {"category": "", "pattern": "LogSEC[A-Za-z]*: (Warning|Error)"}.
F = "/Game/Enemies/DarkKnight" # the customer's folderN = "DarkKnight" # the enemy's namemesh = load("/Game/.../SKM_DarkKnight")seq_light = load("/Game/.../Attack_01")seq_heavy = load("/Game/.../Attack_03")seq_hit = load("/Game/.../Hit_Front")abp = load("/Game/.../ABP_DarkKnight") # the customer's Animation Blueprint, or the one "No Animation Blueprint" makes
The tool turns root motion on for the sequence, so an attack that steps forward moves the enemy, and saves the montage. It plays on the DefaultSlot slot.
A Damage Config (USECDamageConfig) says what one hit is worth. Six ship under /SoulslikeEnemyCombat/Core/Damage/ (Damage_SEC_Slashing, _Piercing, _Bludgeoning, _Fire, _Ice, _Lightning). Make your own to set the amount:
SEC Approach Window (USECApproachWindowNotifyState) over the wind-up. It turns the enemy to its target and glides it into reach, so an attack chosen from a few metres away still lands. Its defaults (Move And Face, toward Target) are right. Set its StopDistanceOverride: the gap in cm it leaves between the two bodies, which has to be shorter than the weapon reaches past the enemy's own body. About 40 suits a sword, one-handed or two-handed, and 20 fists; only a spear or a polearm takes
more. Left at -1 it uses the approach component's 150, and a sword swings short of the target. Without the window the enemy swings from where it stands and misses.
SEC Melee Trace Window (USECMeleeTraceNotifyState) over the frames where the weapon connects. Nothing takes damage without it.
Without frame data from the customer, put the approach window from 10% to 30% of the montage and the trace window from 30% for 25% of its length; tell the customer the times you picked.
for am, dmg in ((am_light, dmg_light), (am_heavy, dmg_heavy)): L = length(am) a = sec.add_notify_state_to_montage(am, unreal.SECApproachWindowNotifyState, L * 0.10, L * 0.20) a.set_editor_property("StopDistanceOverride", 40.0) # cm between the two bodies; fit it to the weapon w = sec.add_notify_state_to_montage(am, unreal.SECMeleeTraceNotifyState, L * 0.30, L * 0.25) w.set_editor_property("SocketIDs", ["Blade"]) # the weapon's trace socket ID; [] sweeps every registered socket w.set_editor_property("DamageConfig", dmg) save(am)
For an unarmed enemy, use the socket IDs from "Unarmed enemies" in Recipe 2.
Check:ANIM.get_animation_notify_events(am_light) lists two notify states, SECApproachWindowNotifyState and SECMeleeTraceNotifyState.
Duplicate the demo enemy. It already carries what an enemy needs beyond the base class: a Motion Warping component, the damage handler that tells the enemy it was hit (which drives flinch reactions), its controller and a trace team filter.
StartingAbilities is emptied because the demo's stagger and stun abilities (GA_SEC_Stagger, GA_SEC_Stunned) play the demo mannequin's montages, which would make the customer's character perform the demo's stagger and stun. Without them the enemy is never stunned; a flinch on the customer's own animation comes from the Reaction Set in Step 7.
The duplicate keeps the demo's AI controller, BP_SEC_EnemyController, and its starting weapon. Recipe 2 replaces the weapon.
The duplicate keeps the demo mannequin's mesh placement, which fits a character built like the UE5 mannequin (origin at the feet, facing +Y). If the customer's character floats, sinks or faces sideways, adjust the Mesh component's RelativeLocation and RelativeRotation, and CapsuleHalfHeight and CapsuleRadius on CapsuleComponent.
Check: open the Blueprint's viewport, or capture it, and confirm the mesh stands inside the capsule facing along the blue arrow.
An Action Set (UActionSet) holds the enemy's attacks in Actions, an array of FActionSpec. For each action set:
ActionId: a short unique name. An empty ID is dropped silently.
ExecutionMethod: a Gameplay Ability method (USECAbilityExecutionMethod) whose AbilityClass is the shipped GA_SEC_MontageAbility, with a Montage Payload (USECMontagePayload) whose Montage is the attack montage.
A Distance Scorer (USECDistanceScorer) in CustomScoring.Scorers. A new action has no scorers and fires at any distance, so every melee attack needs one.
Setting AbilityClass on the method makes its Montage Payload by itself; montage_method only fills it.
What the numbers mean:
A Range scores 1 between OptimalMin and OptimalMax and falls to 0 at MinValue and MaxValue. Distances are in centimetres.
SelectionWeight under 1 and RiskPenalty over 1 make an action come up less often on its own.
The chain link makes Heavy score 50% higher for a short window after Light completes, so the enemy combos.
Each new action already holds SEC.State.IsMovementBlocked in AddTags, which stops the enemy walking while it swings. Leave it.
Check: reload with load(...) and print len(aset.get_editor_property("Actions")), each action's ActionId, and that its ExecutionMethod and payload Montage are set.
A Reaction Set (UReactionSet) holds Reactions (FReactionSpec) and the TriggerRules (FSECReactionTriggerRule) that pick one. A set with no trigger rules reacts to nothing.
SEC.Stimulus.DamageTaken reaches the enemy because the duplicated demo Blueprint reports each hit it takes. A Blueprint made from Enemy Character Base directly does not; its Handle Incoming Damage must call Resolve Enemy Damage, a Blueprint edit the customer makes by hand.
Check: print the reaction's ReactionId and the rule's OnStimulus.
StateTree (shown as Default State Tree) stays empty, which runs SEC's built-in brain: notice, investigate, close in, circle, attack, react, plus patrol and the walk home once the placed enemy has territory posts (AgentGuide/AwarenessTerritory.md). The demo's /SoulslikeEnemyCombat/Demo/AI/StateTrees/ST_SEC_Core runs patrol, investigation, the fight and the walk home as a State Tree; set it only when the customer wants to edit the brain in the State Tree editor.
The awareness config does the sensing, and the filtered selector turns what the enemy sensed into its target. A player the enemy detects joins the target pool on its own, so the player Blueprint needs no change.
Sight skips actors on the enemy's own team. Enemies default to Team Id 1; a player with no SEC Team component counts as neutral and is seen.
Check: print config.get_editor_property("TargetSelector"); it is not None.
world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)has_nav = any(isinstance(a, unreal.NavMeshBoundsVolume) for a in actors.get_all_level_actors())enemy = actors.spawn_actor_from_class(bp.generated_class(), unreal.Vector(800, 0, 100))unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).save_current_level()
The enemy walks on a navmesh. If has_nav is False, spawn a NavMeshBoundsVolume over the floor and scale it to cover the play area, or ask the customer to add one. Navigation builds on its own in the editor.
Success looks like: walking into the enemy's view turns its awareness line to Detected; it closes to about the movement profile's distance, circles, swings its light attack within range, sometimes follows with the heavy, flinches some of the time when hit, and a hit on the player lands. log_warnings() returns nothing about this enemy.
Ask the customer to play it too: only a person can judge whether the strike windows sit on the right frames. SEC.Debug.Melee.DrawTracing 1 draws each sweep.
A blade is traced by a two-point capsule between two sockets on its mesh, by default named TraceStart (above the guard) and TraceEnd (the tip). Check, and add them when missing:
for name in ("TraceStart", "TraceEnd"): print(name, sword_mesh.find_socket(name) is not None)print(sec.add_blade_sockets(sword_mesh))
This places TraceStart near the guard and TraceEnd near the tip, and saves the mesh. It changes the customer's mesh: tell them, and ask them to check the sockets sit on the blade when the grip is not at the mesh's origin.
The pawn's melee trace has no data field for its own sockets; a Blueprint registers them at Begin Play with Register Sphere Socket. To stay data-only, give the enemy a "fists" weapon with no mesh whose trace sockets sit on the body's hand bones:
Add the tag Body to the pawn Mesh's ComponentTags.
Duplicate BP_SEC_WeaponBase as BP_<Name>_Fists, clear its Static Mesh component's StaticMesh, and give it two TraceSockets rows: IDHandR and HandL, Shape Sphere, SocketOrBoneNamehand_r and hand_l, SourceComponentTagBody, Radius 12.
Equip it as in Step 4, and list HandR (or both) in each window's SocketIDs.
A socket's SourceComponentTag is looked up on the enemy and on everything attached to it, which is how the fists' sockets find the body mesh.
AttachSocket stays hand_r; set AttachOffset so the off hand meets the grip. The animations decide the pose.
Slower, heavier attacks: Damage 30 to 45, distance ranges 150 to 350, Cooldown.Duration 3 to 6, PlayRate 0.9 on the payloads.
StopDistanceOverride stays about 40 on each approach window. A heavier weapon does not stand farther off: at a 60 cm gap a two-handed swing of the same length falls short and never lands.
DesiredDistance 350 on the movement profile.
A weapon can carry its own moveset: set WeaponActionSet on the weapon Blueprint's defaults and it wins over the AI Config while equipped. WeaponReactionSet does the same for reactions.
Recipe 1, Step 7. For hits from the front and from behind, make one reaction each and gate them with an Incoming Angle Gate (USECGate_IncomingAngle) in the reaction's CustomScoring.Gates: HalfAngleDegrees 90 on the front reaction, and the same with bInvert on for the back one. The gate measures an arc to either side of straight ahead, so it cannot tell left from right; a side hit plays the front or back reaction.
An enemy can only answer a swing that announces itself. The attacker's montage needs a SEC Telegraph Window (USECTelegraphNotifyState) over its wind-up, with Broadcast.Shape set to a Sphere (USECTelegraphShape_Sphere, Radius 500 by default). A telegraph with no shape reaches nobody.
Then add a Parry (or Dodge) reaction playing its montage, and a trigger rule with OnStimulus = SEC.Stimulus.AttackTelegraphed linking to it, Chance 0.3 to 0.5 and MinInterval 2. For i-frames on a dodge, put a SEC State Tag Window (USECStateTagNotifyState) holding SEC.State.Intangible over the dodge's evasive frames.
Pawn. Duplicate BP_<Name> as BP_<Name>_Boss. Raise its health on the Vitals component:
vit = defaults(boss_bp).get_editor_property("Vitals")rows = vit.get_editor_property("Vitals")for r in rows: if str(unreal.GameplayTagLibrary.get_tag_name(r.get_editor_property("VitalTag"))) == "SEC.Vital.Health": r.set_editor_property("MaxValue", 600.0)vit.set_editor_property("Vitals", rows)save(boss_bp)
For a bigger boss, scale the Mesh and the capsule together.
Role. Duplicate the AI Config as EnemyConfig_<Name>_Boss and point the boss pawn's AIConfig at it. In its RoleRegistrationParams set AllowedRoles = tags("SEC.Role.Elite"), PreferredRole = tag("SEC.Role.Elite"), Priority = 200. Set bIgnoreTargetRedistribution = True. Elite slots are unlimited by default, so the boss does not wait for a turn.
Phases by health. Make ActionSet_<Name>_Boss. Give phase one actions a Health Scorer (USECHealthScorer) with Range 0.49, 0.5, 1.0, 1.0, and phase two actions one with Range 0.0, 0.0, 0.5, 0.51. Health is a 0 to 1 fraction, so phase one stops and phase two starts at half health. Phase two can reuse the same montages with PlayRate 1.25 and shorter cooldowns, plus a new finisher. Set it as the boss config's DefaultActionSet.
Arena. Spawn the demo's boss arena trigger, /SoulslikeEnemyCombat/Demo/Game/BP_SEC_BossArenaTrigger (on Epic's MCP its class is /SoulslikeEnemyCombat/Demo/Game/BP_SEC_BossArenaTrigger.BP_SEC_BossArenaTrigger_C), covering the arena. It is an SEC Encounter Trigger that ends the fight as Defeated once every enemy it lists is dead or gone. Set EncounterName (text), bTriggerOnce = True, bEndWhenPlayerLeaves = False, and Enemies = [placed boss actor]. Enemies can only be set
on the placed actor, not on a Blueprint. Size its box with trigger.get_editor_property("TriggerBox").set_box_extent(unreal.Vector(x, y, z)).
Fight from the start. For an arena where the boss knows about the player at once, set bAlwaysAware = True on a dedicated awareness config for the boss.
What needs a person: a health bar on screen needs a widget bound to the SEC Encounter Subsystem's On Encounter Started (the demo's WBP_SEC_HUD_Encounter shows how). Tell the customer it is a Blueprint step.
An enemy needs an Animation Blueprint on its skeleton that blends idle, walk and run by speed and has a DefaultSlot Slot node, which is where every SEC montage plays. If the customer has none, ask whether one came with the animation pack under another name. Otherwise make one from the plugin's locomotion template:
The new Animation Blueprint blends idle into walk by 200 cm/s and walk into run by 500 cm/s. To match the enemy's own speeds (MaxWalkSpeed on its CharacterMovement), set WalkSpeed and RunSpeed on its class defaults (defaults(abp)) and save it.
The recipes above build the common cases. For anything else, open the one file below that covers the task. Each lists its tasks first, then recipes with a check, the exact class and property names, and the common mistakes. They sit in AgentGuide/ beside this file, in the plugin's Resources folder; when this file reached you on its own, open them there.
Task
File
A property refuses a value, rows in arrays, components on a Blueprint, montage sections, new gameplay tags, checking an asset, project settings
AgentGuide/Python.md
A moveset from several animations, ranges, combos and pacing, gates, cooldowns, one clip with several attacks, movesets per weapon or role
AgentGuide/Actions.md
Flinches by direction, parry and dodge rules, what an enemy can notice
AgentGuide/Reactions.md
Keeping distance, circling, backing off when stared at, flanking angles
AgentGuide/Movement.md
Target selection, combat roles and limits, taking turns, teams, the brain, repeatable fights
AgentGuide/BrainRolesTargets.md
Sight and hearing, patrols, guard posts, leashes
AgentGuide/AwarenessTerritory.md
An attack run as a behavior tree, the State Tree tasks
AgentGuide/BehaviorTrees.md
Montage windows, root-motion lunges, step-backs, tag-driven animation, walking slower on a tag
AgentGuide/Animation.md
Weapons and loadouts, damage and hit effects, enemies that block and parry
AgentGuide/WeaponsDamageDefense.md
Health, stamina and stance, death, encounters and boss bars, settings, debugging
Pawn components you touch (all on the duplicated pawn): Mesh, CharacterMovement, MeleeTraceComponent (DefaultDamageConfig), EquipmentComponent (StartingLoadout), Vitals (Vitals rows: VitalTag, MaxValue, RegenRatePerSecond, RegenDelaySeconds; one SEC.Vital.Health row at 100 by default).
The demo content must stay in the project while anything references it. Duplicate a demo asset into the customer's folder when the customer plans to remove the demo.
The pawn's AI controller is not an SEC one, or AIConfig is empty.
Enemy never notices the player
AwarenessConfig empty, or the player is on the enemy's team (Team Id 1).
Enemy moves but never attacks
No action passes its gates: bEnabled off, Selection not Scored, or a stuck SEC.Action.BlockActions tag. SEC.Debug.Scoring 1 prints the refusal reason.
Enemy swings from across the room or repeats one move
The action has no Distance Scorer.
Action wins and nothing plays
ExecutionMethod empty, payload Montage empty, or ActionId empty.
Attacks once, then freezes
The ability never reaches End Ability. Use GA_SEC_MontageAbility, or set AbilityTimeout on the execution method.
Hits pass through
SocketIDs on the window do not match the weapon's TraceSocketsID, bUseAsWeapon is off, or the window sits on the wrong frames. The log names an unregistered socket ID.
Enemies hit each other
The pawn's melee trace TeamFilter is NoFilter, the default on a fresh Blueprint. Set it to unreal.SECTeamFilter.ENEMIES_ONLY; the demo duplicate already does.
Never parries
The attacker's montage has no SEC Telegraph Window, its Broadcast.Shape is empty, or the Reaction Set has no trigger rule.
Never flinches
The pawn does not report damage (see Recipe 1, Step 7), or the set has no rule on SEC.Stimulus.DamageTaken.
Does not move at all
No navmesh, or a leftover SEC.State.IsMovementBlocked tag.
T-pose or no animation
The Animation Blueprint is for another skeleton, or has no DefaultSlot Slot node, so montages play nowhere.
Debug commands in Play: SEC.Debug.All 1, SEC.Debug.Awareness 1, SEC.Debug.Scoring 1, SEC.Debug.Melee.DrawTracing 1, SEC.Debug.LogActionDecisions 1, SEC.Debug.LogReactions 1, SEC.Debug.Stimulus.Draw 1. SEC.Debug.WatchPawn <name> narrows the overlays to one enemy.
When the customer wants to understand something rather than build it, find the page in https://rigbak.com/docs/soulslike-combat/llms.txt, which lists every docs page with a one-line summary, fetch the page's Markdown version (the page URL plus .md), answer from it, and give them the page link. The table names the main pages.
AGENTS.md, Recipe 1, Step 6, builds a two-attack Action Set with the helpers montage_method, range_scorer and action. This file covers everything else an action can say. Define those three helpers first (copy them from Step 6).
Tasks: a moveset from several animations; attacks for close, middle and long range; a combo that always follows on; a back-step or a gap closer; attacks only from the front, only at low health or only with stamina; pacing between attacks; several enemies taking turns; one long animation holding several attacks; a different moveset per weapon or per combat role; using the customer's own ability.
Two quick slashes that combo, an overhead slam, a thrust from farther away, and a back-step when the player is too close.
names = {"Slash1": "Attack_Slash_01", "Slash2": "Attack_Slash_02", "Slam": "Attack_Overhead", "Thrust": "Attack_Thrust", "BackStep": "Step_Back"}am = {k: sec.create_montage_from_animation(load(f"/Game/.../{v}"), F, f"AM_{N}_{k}") for k, v in names.items()}for key in ("Slash1", "Slash2", "Slam", "Thrust"): # the back-step deals no damage and needs no window m, L = am[key], length(am[key]) a = sec.add_notify_state_to_montage(m, unreal.SECApproachWindowNotifyState, L * 0.10, L * 0.20) a.set_editor_property("StopDistanceOverride", 40.0) w = sec.add_notify_state_to_montage(m, unreal.SECMeleeTraceNotifyState, L * 0.30, L * 0.25) w.set_editor_property("SocketIDs", ["Blade"]) w.set_editor_property("DamageConfig", dmg_heavy if key == "Slam" else dmg_light) save(m)aset = new_data_asset(unreal.ActionSet, F, f"ActionSet_{N}")slash1 = action(aset, "Slash1", am["Slash1"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 80, 250, 400)], cooldown=2.0)link = unreal.ActionChainLink()link.set_editor_property("TargetActionId", "Slash2")link.set_editor_property("BonusMultiplier", 1.5)link.set_editor_property("Pacing", unreal.SECChainPacing.IMMEDIATE)slash1.set_editor_property("ChainLinks", [link])slash2 = action(aset, "Slash2", am["Slash2"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 80, 250, 400)])slash2.set_editor_property("Selection", unreal.SECActionSelection.CHAIN_ONLY)slam = action(aset, "Slam", am["Slam"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 100, 250, 350)], cooldown=6.0, weight=0.7, risk=1.5)thrust = action(aset, "Thrust", am["Thrust"], [range_scorer(aset, unreal.SECDistanceScorer, 250, 350, 550, 700)], cooldown=4.0)back = action(aset, "BackStep", am["BackStep"], [range_scorer(aset, unreal.SECDistanceScorer, 0, 0, 100, 150)], cooldown=5.0, weight=0.8)aset.set_editor_property("Actions", [slash1, slash2, slam, thrust, back])save(aset)
Each band is a Distance Scorer: the action scores 1 between OptimalMin and OptimalMax and 0 outside MinValue to MaxValue (cm). Give each attack its own band; the thrust's band starts where the slashes end.
CHAIN_ONLY makes Slash2 play only after Slash1; IMMEDIATE pacing lets it start as soon as Slash1 ends, skipping the recovery gap.
The back-step's band ends at 150 cm, so it only plays with the player close. It has no approach window, so it steps back from where the enemy stands.
Point the AI Config's DefaultActionSet at the new set and save the config.
Check: reload the set and print each action's ActionId, its execution method's payload Montage, the band of its first scorer, and Slash1's chain link. sec.validate_asset(aset) returns nothing. In Play with SEC.Debug.Scoring 1, the overlay lists every action's score; SEC.Debug.LogActionDecisions 1 logs each pick, and Slash2 follows Slash1.
Unique name in the set; chain links name it. An empty one is dropped.
bEnabled
Off keeps the action in the set but never picks it.
Selection (unreal.SECActionSelection)
SCORED (picked by score), CHAIN_ONLY (only as a chain follow-up), MANUAL_ONLY (only when Blueprint or a State Tree runs it by name)
ExecutionMethod
How it runs (below).
SelectionWeight, RiskPenalty
Multiply and divide the score: below 1 and above 1 make it rarer.
CustomScoring (unreal.SECCustomScoring)
Scorers (soft factors) and Gates (hard vetoes), below.
TagScoreMultipliers
Tag to multiplier: while the target holds the tag, the score is multiplied.
RequiresTags, BlockTags
Tags that must be, or must not be, on the target or in the world.
bRequireLineOfSight
Only with a clear line to the target.
AddTags
Tags the enemy holds while it runs. SEC.State.IsMovementBlocked is there from the start; leave it on attacks.
Cooldown (unreal.ActionCooldown)
Duration, InitialCooldown (wait before the first use), Randomization (0 to 0.5), InterruptRefund (0 to 1), SpawnCooldownChance, MaxConsecutiveUses (-1 unlimited)
bOverrideRecoveryTime, RecoveryTime
This action's own pause after it ends (else the AI Config's ActionRecoveryTime).
ChainLinks (unreal.ActionChainLink)
TargetActionId, BonusMultiplier (score bonus while the chain window is open), Pacing (SCORE_ONLY, IMMEDIATE)
Hook
A USECActionHook Blueprint run around the action (a Blueprint step; duplicate BP_SEC_ExampleHook).
On the AI Config: ActionRecoveryTime, InterruptRecoveryTime, ActionRecoveryTimeRandomization (seconds the enemy waits after an action before picking another).
AbilityClass (GA_SEC_MontageAbility plays the payload's montage), AbilityTimeout, Payload
USECAbilityByTagExecutionMethod (Gameplay Ability By Tag)
The granted ability carrying a tag
ActivationTag, AbilityTimeout, Payload
USECAbilityFireAndForgetExecutionMethod (Gameplay Ability (Fire And Forget))
Starts an ability and ends the action at once
AbilityClass, ActivationTag
USECBehaviorTreeExecutionMethod (Behavior Tree Sequence)
Behavior trees one after another
AgentGuide/BehaviorTrees.md
The payload is a USECMontagePayload (Montage Payload): Montage, StartSection, PlayRate. Make it with the method as its outer (montage_method does). An ability the customer wrote must reach End Ability on every path, or the action never ends; AbilityTimeout is the safety limit.
One clip, several attacks. Give the montage sections, then set each payload's StartSection:
On a weapon Blueprint's class defaults, WeaponActionSet (and WeaponReactionSet) win over the AI Config while it is equipped.
On the AI Config, RoleActionSets takes unreal.RoleActionSetConfig rows (Role, bManaged, ActionSet); roles not listed use DefaultActionSet. bManageActionSetsAutomatically (on) is the switch for both.
AGENTS.md, Recipe 1, Steps 2 and 4, makes montages and puts an approach window and a trace window on each attack. This file covers every window SEC reads, the skew warp for root-motion lunges, sections, tag-driven animation bools and the speed modifier.
Tasks: an attack that turns and closes the gap; a step back or to the side by a set distance; a lunge whose root motion tracks the target; i-frames on a dodge; an event at a frame; footsteps and trails; several attacks in one clip; a guard pose while a tag is held; walking slower while blocking.
Add each with sec.add_notify_state_to_montage(montage, cls, start_seconds, duration_seconds), set its properties on the returned window, and save the montage. sec.add_notify_state_to_montage takes notify states only; a single-frame notify is added with unreal.AnimationLibrary.add_animation_notify_event(montage, "Combat", seconds, cls).
Mode (unreal.SECApproachMode: MOVE_AND_FACE, FACE_ONLY, MOVE_ONLY), ApproachTargetName (Target), StopDistanceOverride (cm left between the bodies), MaxApproachDistanceOverride; bMoveByOffset with OffsetDirection and OffsetDistance moves a set distance in the enemy's own frame instead (a back-step: direction (-1, 0, 0), 200 cm)
SEC State Tag Window
USECStateTagNotifyState
Tags held while it runs: SEC.State.Intangible (i-frames), SEC.State.Blocking
SEC Telegraph Window
USECTelegraphNotifyState
Broadcast (unreal.SECTelegraphBroadcast: Shape = USECTelegraphShape_SphereRadius or USECTelegraphShape_ConeRadius, HalfAngleDegrees), bFindAttackAutomatically, DamageConfig (shown as Attack), Tags, Magnitude
SEC Anim Effect Window
USECAnimEffectWindowNotifyState
Effect (EffectTags, SocketName), answered by an anim effect handler on the pawn
An attack animated with root motion that should land on the target (rather than glide by an approach window) uses the engine's Motion Warping window with the SEC Skew Warp modifier:
w = sec.add_notify_state_to_montage(am_lunge, unreal.AnimNotifyState_MotionWarping, L * 0.1, L * 0.3)mod = unreal.new_object(unreal.SECRootMotionModifier_SkewWarp, outer=w)mod.set_editor_property("WarpTargetName", "Target")mod.set_editor_property("StopDistanceOverride", 40.0)w.set_editor_property("RootMotionModifier", mod)save(am_lunge)
The demo enemy carries the Motion Warping component it needs. SEC.Debug.Warp 1 draws where each warp lands.
sec.set_montage_sections(montage, [("Slash", 0.0), ("Stab", 1.1)]) gives a montage sections; a Montage Payload's StartSection then plays one (AgentGuide/Actions.md).
The locomotion template (sec.create_locomotion_anim_blueprint, AGENTS.md, "No Animation Blueprint") blends idle, walk and run and plays montages on DefaultSlot.
Tag-driven bools: an Animation Blueprint whose parent is USECAnimInstanceBase has GameplayTagPropertyMap on its class defaults: each row sets a bool variable while the enemy holds a tag (a guard pose while SEC.State.Blocking). Wiring that bool into the AnimGraph is a step for the customer.
Add a SEC Speed Modifier component (USECSpeedModifierComponent) to the enemy Blueprint (AgentGuide/Python.md, Components on a Blueprint) and fill SpeedScaleByTag, such as {tag("SEC.State.Blocking"): 0.5}.
Tasks: how far and how fast an enemy notices the player; a guard that notices slowly from the side; an enemy that knows about the player at once (a boss); waking nearby allies; patrolling between points; guarding one spot; giving up a chase past a distance and walking back.
An enemy notices targets through its Awareness Config (USECAwarenessConfig), named on the AI Config as AwarenessConfig. Without one the enemy never notices anyone. The demo's is /SoulslikeEnemyCombat/Demo/AI/Awareness_SEC_Default.
Field
Does
SightRadius, LoseSightRadius
Sight range in cm, and the range at which a seen target is lost
PeripheralVisionHalfAngle
Half the sight cone, in degrees (90 is a 180 degree cone)
Check: in Play, SEC.Debug.All 1 writes the awareness state (Suspicious, Detected) above the enemy as the player walks into view.
The target selector turns what the enemy noticed into its target: unreal.AwarenessFilteredTargetSelector (MinRequiredState, InnerSelector) on the AI Config, as AGENTS.md, Recipe 1, Step 8, sets. Sight skips actors on the enemy's own team (AgentGuide/BrainRolesTargets.md, Teams).
Every enemy on AEnemyCharacterBase carries a SEC Territory component, TerritoryComponent. It is set on the placed enemy in the level, since posts are level actors. Posts are SEC Territory Post actors (ASECTerritoryPost), each tuned on its Post component (USECTerritoryPostComponent).
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)posts = [actors.spawn_actor_from_class(unreal.SECTerritoryPost, unreal.Vector(x, y, 50)) for x, y in ((0, 0), (1200, 0), (1200, 900))]for p in posts: pc = p.get_editor_property("Post") pc.set_editor_property("WaitTime", 2.0) pc.set_editor_property("bFaceForwardOnArrival", True)terr = placed_enemy.get_editor_property("TerritoryComponent")terr.set_editor_property("Posts", posts)terr.set_editor_property("RestingBehavior", unreal.SECRestingBehavior.PATROL_POSTS)terr.set_editor_property("PostOrder", unreal.SECPostOrder.SEQUENTIAL_LOOP)terr.set_editor_property("bLeashEnabled", True)terr.set_editor_property("LeashAnchor", unreal.SECLeashAnchor.POSTS_CENTERED)terr.set_editor_property("LeashRadius", 1500.0)terr.set_editor_property("DisengageBehavior", unreal.SECDisengageBehavior.RETURN_TO_POST)unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).save_current_level()
placed_enemy is the enemy actor in the level (AGENTS.md, Recipe 1, Step 9, keeps it as enemy).
SEC Territory field
Does
Posts
The posts this enemy owns, in route order
RestingBehavior
PATROL_POSTS (walk the route) or HOLD_GROUND (stand at the first post)
bLeashEnabled, LeashAnchor (SELF, POSTS_CENTERED, POSTS_UNION, ACTOR with LeashAnchorActor), LeashRadius
How far it may be dragged from its ground
LeashBreakGraceTime
Seconds past the leash before it gives up
bOnlyFightInsideLeash
Ignore targets outside the leash
DisengageBehavior
RETURN_TO_POST or SETTLE_HERE once a fight ends
Post field
Does
Shape
POINT, RADIUS (wander inside WanderRadius), BOX (inside WanderBoxExtent)
WaitTime, WaitTimeVariance
Seconds it waits on arrival
bFaceForwardOnArrival
Turn to the post's facing
bPassThrough
Walk through without stopping
The built-in brain (the AI Config's StateTree, shown as Default State Tree, left empty) walks the posts, goes to look where it last sensed the player, and walks home after a fight. The demo brain /SoulslikeEnemyCombat/Demo/AI/StateTrees/ST_SEC_Core runs patrol, investigation and the walk home as a State Tree the customer can open and change; set it as the AI Config's StateTree only when they want that.
Check: in Play, the enemy walks its posts; dragged past LeashRadius for LeashBreakGraceTime it turns back to its posts.
Tasks: run one attack as a behavior tree with the plugin's tasks (close in, swing, back off); tune the shipped tree; know what each State Tree task does.
An action's ExecutionMethod can be a Behavior Tree Sequence (USECBehaviorTreeExecutionMethod): it runs its trees one after another on the controller, then the action ends.
Field
Does
AbilityToGrant
The gameplay ability the trees start (GA_SEC_MontageAbility plays the payload's montage)
BehaviorTreeSequence
The trees, in order
BehaviorTreeTimeout
Seconds before a tree is stopped
PayloadKeyName
Blackboard key the payload is written to, SEC_Payload
Payload
A USECMontagePayload (outer: the method), as for an ability method
The plugin's tasks, which the editor's behavior tree palette lists:
Task (class)
Does
Fields
Move Until Distance (UBTTask_MoveUntilDistance)
Walks toward the blackboard target until within reach
Editor Python cannot build a behavior tree's shape. The shipped BT_SEC_AbilityAction already is this shape (a Sequence of Move Until Distance, Activate Action Ability and Make Distance, with a Wait), so duplicate it and tune its tasks:
bt = duplicate("/SoulslikeEnemyCombat/Demo/AI/BehaviorTrees/BT_SEC_AbilityAction", F, f"BT_{N}_HeavyAttack")pkg = bt.get_outermost().get_name()nodes = [o for o in unreal.ObjectIterator(unreal.BTNode) if o.get_outermost().get_name() == pkg]for n in nodes: kind = n.get_class().get_name() # the plugin's task classes have no Python type of their own; match them by name if kind == "BTTask_MoveUntilDistance": n.set_editor_property("TargetDistance", 150.0) elif kind == "BTTask_MakeDistance": n.set_editor_property("TargetDistance", 450.0)save(bt)method = unreal.new_object(unreal.SECBehaviorTreeExecutionMethod, outer=aset)method.set_editor_property("AbilityToGrant", bp_class("/SoulslikeEnemyCombat/Core/Abilities/GA_SEC_MontageAbility"))method.set_editor_property("BehaviorTreeSequence", [bt])payload = unreal.new_object(unreal.SECMontagePayload, outer=method)payload.set_editor_property("Montage", am_heavy)method.set_editor_property("Payload", payload)
Use method as the action's ExecutionMethod in place of montage_method(...), then write the actions back and save the set. Keep a Distance Scorer on the action.
Check: reload the tree and print each task's TargetDistance; in Play, SEC.Debug.LogActionExecution 1 logs the tree starting and ending with the action.
A tree of another shape (a Selector, decorators, other tasks) is built by hand in the Behavior Tree editor: tell the customer which tasks in which order, from the table above.
The brain is a State Tree when the AI Config's StateTree names one; editor Python cannot build or edit a State Tree's states. Use the shipped trees (ST_SEC_Core: notice, patrol, investigate, fight, walk home; ST_SEC_RoleBehavior: per combat role) or have the customer duplicate one and edit it in the State Tree editor. The plugin's tasks and conditions there:
Task
Does
Build Decision Context
Gathers distance, angle, health and threat each tick
Poll Action, Do Action
Scores the actions and runs the chosen one
AI Movement
Runs the movement profile
Poll Combat Role, Poll Focus Actor, Poll Last Known Location
Read the role, the focus target, where the target was last seen
Sync Action Set, Sync Reaction Set, Sync Movement Profile, UpdateActionSet
Swap sets on a role change, or to a named Action Set
Territory Patrol, Territory Investigate, Return To Territory, Hold Out Of Combat, Monitor Combat Leash
Patrol, search, walk home, stand down, watch the leash
Condition
Passes when
Awareness State At Least
The target is noticed to a level
Target Eligible For Combat, Target Outside Leash, Pawn Outside Leash, Pawn On Its Ground, Territory Owns Ground, Resting Behavior Is
Territory and leash state
Action Executing, Action Recovering, Actions Blocked, Reaction Executing, ASC Has Tag
Tasks: which target an enemy picks when there are several players; enemies spreading across players; an enemy that only ever flanks; at most two attackers at once; enemies taking turns on one player; factions that fight each other; a fight that repeats exactly; choosing the brain.
All of it sits on the Enemy AI Config (UEnemyAIConfig), the pawn's AIConfig, plus a few Project Settings.
How it picks a target (below). Empty picks the first registered target it may fight.
TargetFilter
Which actors it will fight at all; empty uses the built-in rule (never itself, never an ally, never an excluded team). A custom filter is a Blueprint child of USECTargetFilter (a Blueprint step).
What it notices (AgentGuide/AwarenessTerritory.md, AgentGuide/Reactions.md).
StateTree (shown as Default State Tree)
Empty runs the built-in brain: notice, investigate, close in, circle, attack, react, plus patrol and the walk home once the placed enemy's territory has posts. ST_SEC_Core runs the same phases as a State Tree the customer can edit; ST_SEC_RoleBehavior drives each combat role differently. Both under /SoulslikeEnemyCombat/Demo/AI/StateTrees/.
DecisionContextParams
AggressionLevel (0 to 2) and line-of-sight trace settings.
Roles share out what each enemy does in a group fight: SEC.Role.Attacker, .Flanker, .Waiter, .Supporter, .Elite. RoleRegistrationParams on the config: AllowedRoles (empty allows any), PreferredRole, Priority (0 to 1000, higher picks first), FitnessEvaluators (UDistanceRoleEvaluatorIdealDistance, EffectiveRange; UCooldownRoleEvaluatorCurrentRolePenaltyMultiplier; each with RoleWeights, a map of role tag to weight).
p = config.get_editor_property("RoleRegistrationParams")p.set_editor_property("AllowedRoles", tags("SEC.Role.Flanker"))p.set_editor_property("PreferredRole", tag("SEC.Role.Flanker"))config.set_editor_property("RoleRegistrationParams", p)save(config)
How many at once is a Project Setting: RoleLimits on unreal.SoulslikeEnemyCombatSettings (unreal.RoleLimitConfig rows Role, Limit), with RoleReassignmentInterval and MinTimeInRole. A project setting changed from Python lasts until the editor closes (AgentGuide/Python.md, Project Settings), so ask the customer to set it in Project Settings, Plugins, Soulslike Enemy Combat, Combat Roles. Elite slots are unlimited.
Check: in Play, SEC.Debug.LogRoleAssignments 1 logs each assignment; the overlay from SEC.Debug.All 1 names each enemy's role.
Put a Combat Token Gate (USECTokenGate, TokenTagSEC.Token.Attack, Cost 1) in each attack's CustomScoring.Gates (AgentGuide/Actions.md). How many enemies may hold SEC.Token.Attack against one target is TokenBudgets in Project Settings (unreal.SECTokenBudgetConfig rows TokenTag, Budget, ReissueDelay), or per target with a SEC Token Budget Component (USECTokenBudgetComponent) on the player Blueprint: add it as AgentGuide/Python.md, Components on a Blueprint,
shows, set its TokenBudgets to {tag("SEC.Token.Attack"): 1}, compile and save.
Every actor's team is its SEC Team component (USECTeamComponent): TeamId (a number; enemies default to 1, the player is 0), bFreeForAll, ExcludedTeamIds, AllowedHostileTeamIds, bIgnorePlayer, bIgnoreNeutrals, FightOnly (one actor and nothing else). An actor with no team component is neutral and can be fought. Enemies on AEnemyCharacterBase carry one as TeamComponent. Two factions: give each enemy Blueprint its own TeamId. The melee trace's TeamFilter decides whether a
swing can hit allies. SEC.Debug.All 1 shows each actor's team.
Tasks: how far an enemy keeps from its target; circling and how often it switches direction; backing off when the player stares at it; flanking from an angle; different spacing per combat role; stopping farther or closer when an attack closes the gap.
Everything here is data. An enemy moves by its Movement Behavior Profile (UMovementBehaviorProfile), named on its AI Config as DefaultMovementProfile (AGENTS.md, Recipe 1, Step 8).
Keeps about four metres away, circles the player, and backs off while the player looks at it.
movement = new_data_asset(unreal.MovementBehaviorProfile, F, f"Movement_{N}_Duelist")movement.set_editor_property("DesiredDistance", 400.0) # cm between the enemy and its targetmovement.set_editor_property("DistanceTolerance", 0.15)movement.set_editor_property("CrowdTolerance", 0.2) # steps back once closer than 320 cmmovement.set_editor_property("bEnableStrafing", True)movement.set_editor_property("StrafeTimeLimit", 4.0)movement.set_editor_property("bAdjustDistanceByThreat", True)movement.set_editor_property("ThreatDistanceScale", 0.5) # up to 1.5 times the distance while stared atmovement.set_editor_property("bSwapStrafeOnHighThreat", True)save(movement)config = load(f"{F}/EnemyConfig_{N}")config.set_editor_property("DefaultMovementProfile", movement)config.set_editor_property("ActionRecoveryTime", 2.5) # seconds between attacks, spent backing off and circlingsave(config)aset = config.get_editor_property("DefaultActionSet")bands = [s for spec in aset.get_editor_property("Actions") for s in spec.get_editor_property("CustomScoring").get_editor_property("Scorers") if isinstance(s, unreal.SECDistanceScorer)]farthest = max(bands, key=lambda s: s.get_editor_property("Range").get_editor_property("MaxValue"))r = farthest.get_editor_property("Range")r.set_editor_property("MaxValue", max(r.get_editor_property("MaxValue"), 750.0))farthest.set_editor_property("Range", r)save(aset)
The profile only moves the enemy while no attack can start, so ActionRecoveryTime gives it that time after each attack; with none it attacks again as soon as an attack is ready and never backs off. CrowdTolerance 0.2 sends it back out past 320 cm after an attack; the default 0.45 lets it stand at 220 cm. While stared at, this enemy waits up to 400 x 1.15 x 1.5 = 690 cm out, past the 450 and 500 cm bands of the core recipe's attacks. An attack starts only where its Distance Scorer reaches (
below), so the last block stretches the band of the attack that reaches farthest to 750 cm, the farthest its approach window glides in. Without it the enemy circles at range and never attacks a player who keeps it on screen.
How long a stare takes to count is on the AI controller's Threat Detection component. The demo controller BP_SEC_EnemyController carries one; to change it, duplicate the controller into the customer's folder, set it on the pawn (AIControllerClass on the pawn's class defaults), and edit its class defaults:
ctrl = duplicate("/SoulslikeEnemyCombat/Demo/Characters/Enemy/BP_SEC_EnemyController", F, f"BP_{N}_Controller")threat = defaults(ctrl).get_editor_property("ThreatDetectionComponent")threat.set_editor_property("PlayerLookAngleThreshold", 20.0) # degrees off the enemy that count as looking at itthreat.set_editor_property("PlayerLookTimeThreshold", 1.0) # seconds of looking before it countssave(ctrl)defaults(bp).set_editor_property("AIControllerClass", ctrl.generated_class())save(bp)
Check: reload the config and print its DefaultMovementProfile and the stretched band's MaxValue. In Play, SEC.Debug.All 1 shows the enemy's movement state; it stops at about DesiredDistance, circles, opens the gap while the player keeps it centred on screen (SEC.Debug.Threat.Draw 1 draws the look cone), and still attacks from there.
Multiply the distance by 1 + threat x scale; threat climbs from 0 to 1 while the player looks at the enemy
bSwapStrafeOnHighThreat
false
Switch circling direction once the player has stared long enough
PositioningRules
empty
Extra direction preferences, below
Angle preference.UAnglePreferenceRule in PositioningRules pulls the enemy toward an angle off the target's facing: PreferredAngle (0 in front, 90 at the side, 180 behind; default 90), AngleTolerance (30), DirectionCommitment (45), bAvoidInstead (stay away from that angle), Weight (0 to 1). Make it with unreal.new_object(unreal.AnglePreferenceRule, outer=movement) and write the list back. A flanker uses PreferredAngle 120 to 150.
Per combat role.RoleMovementProfiles on the AI Config takes unreal.RoleMovementProfileConfig rows (Role a SEC.Role.* tag, MovementProfile); roles not listed use DefaultMovementProfile. The demo ships one profile per role under /SoulslikeEnemyCombat/Demo/AI/MovementProfiles/ (MovementProfile_SEC_Attacker, _Flanker, _Waiter, _Supporter, _Elite); duplicate one as a starting point.
Threat.UThreatDetectionComponent on the controller: PlayerLookAngleThreshold (15 degrees) and PlayerLookTimeThreshold (3 seconds). The controller's CombatControllerComponent turns it on (bEnableThreatDetection, on by default).
The profile decides where the enemy waits between attacks; the enemy picks its next attack from there. An action can start only while the gap is inside its Distance Scorer's band (below MaxValue). The attack then closes the last gap through the SEC Approach Window on its montage (AGENTS.md, Recipe 1, Step 4): its StopDistanceOverride is the gap it leaves, and the pawn's SEC Approach component sets the rest: DefaultStopDistance, DefaultMaxApproachDistance (farthest it glides,
750 cm), SpeedLimit.
So the farthest the enemy waits, DesiredDistance x (1 + DistanceTolerance), times (1 + ThreatDistanceScale) while stared at when bAdjustDistanceByThreat is on, has to sit inside at least one attack's band.
DesiredDistance below about 150, or the profile is not the config's DefaultMovementProfile while bManageMovementProfilesAutomatically is on (the default).
It jitters back and forth
DistanceTolerance too small for the distance; raise it toward 0.2.
It stays close after each attack
An attack is always ready, so it never has time to walk back. Set ActionRecoveryTime on the AI Config (the demo uses 2), or longer Cooldown.Duration on the attacks.
It circles but never attacks
It waits farther out than any attack's Distance Scorer reaches, often only while the player looks at it (bAdjustDistanceByThreat). Stretch the farthest attack's MaxValue past the farthest wait, as the recipe does, or lower DesiredDistance or ThreatDistanceScale.
It never backs off when stared at
bAdjustDistanceByThreat off, or the controller has no Threat Detection component (the demo controller has one).
It changes direction constantly
StrafeTimeLimit short with bAutoStrafeSwap on; raise StrafeSwapThreshold or StrafeTimeLimit.
A role's profile is ignored
The role tag on the row does not match the role the enemy holds; SEC.Debug.LogRoleAssignments 1 logs it.
Rules every other file relies on. Read this when a set_editor_property call is refused, when a value you wrote is gone after a reload, or before you add components, rows or instanced objects.
Tasks: set a property; edit a row in an array; add an instanced object (an execution method, a scorer, a selector); edit a component on a Blueprint; give a montage sections; add a gameplay tag; check an asset; save a Project Settings value; read the log.
obj.set_editor_property("Name", value) takes the C++ name (bAlwaysAware) or the Python name (always_aware). It refuses a property the editor shows read-only, and a class-defaults-only property on anything that is not a template (an asset, a class default object, or an object inside an asset).
Arrays and their elements come back as copies. Edit the element, put it back in the list, and write the whole list:
Execution methods, payloads, scorers, gates, target selectors, positioning rules, stimulus filters, hit effects and defense rules live inside the asset holding them.
Make one with unreal.new_object(cls, outer=<the asset that holds it>). A payload's outer is its execution method.
Never give a class default object as the outer (unreal.get_default_object(...)), not even to look at a class: the next object of that class with the same name takes it as its template, and the asset holding it fails to save.
A component the C++ base creates is read from the class defaults: on the pawn, defaults(bp).get_editor_property("MeleeTraceComponent") (also EquipmentComponent, Vitals, CharacterMovement, Mesh); on the controller, ThreatDetectionComponent, MovementEvaluatorComponent, ActionEvaluationComponent, AwarenessComponent, CombatControllerComponent, SECBrainComponent. Edit it there and save the Blueprint.
A component the Blueprint adds lives in its construction script; bp_component(bp, "<name start>") from AGENTS.md returns its template. Edit it, compile with unreal.BlueprintEditorLibrary.compile_blueprint(bp) and save.
It replaces every section and saves the montage. Each section ends the montage, so an action whose payload names a StartSection plays only that part. A third value names the section it plays on into instead, or its own name to loop it: ("Hold", 0.4, "Hold").
A tag must exist before a property can hold it. The plugin's role, token, stimulus, state and vital tags exist already. To add one, such as a role or a token pool of the project's own:
sec.add_gameplay_tag("SEC.Role.Guardian", "Stays beside the boss")
It writes Config/DefaultGameplayTags.ini and the tag can be used at once.
unreal.get_default_object(unreal.SoulslikeEnemyCombatSettings) edits the values under Project Settings, Plugins, Soulslike Enemy Combat (RoleLimits, TokenBudgets, DefaultTargetSelectorClass, DefaultTargetFilterClass, vital tags). Editor Python cannot write them to the project's config file before UE 5.8, so a change lasts until the editor closes: ask the customer to set the value in Project Settings, or on UE 5.8 save it with Epic's MCP ConfigSettingsToolset.
sec.validate_asset(asset) runs the checks the editor's Validate Assets runs (Action Sets, Reaction Sets, damage configs) and returns one line per problem.
After Play, log_warnings() returns SEC's warnings; each names the asset and the fix.
AGENTS.md, Recipe 1, Step 7, makes a flinch and Recipe 4 a parry. This file covers the rest of a Reaction Set.
Tasks: flinch only sometimes, or not twice in a row; different reactions for hits from the front and from behind; parry some swings and dodge others; parry only light attacks; not react to what the enemy cannot see; a counter after a parry; reactions only in one phase.
Something happens to the enemy (it takes damage, a swing is announced near it) and arrives as a stimulus tag. Each trigger rule (unreal.SECReactionTriggerRule) in TriggerRules that answers that tag, asked highest RulePriority first, rolls its Chance; the first that fires picks one of the reactions it links, weighted by WeightMultiplier. A set with no trigger rules reacts to nothing.
Stimulus
Arrives when
SEC.Stimulus.DamageTaken
The enemy takes a hit (the duplicated demo Blueprint reports it).
SEC.Stimulus.AttackTelegraphed
A swing near it is announced by a SEC Telegraph Window on the attacker's montage.
SEC.Stimulus.StanceBroken
The enemy's Blueprint reports it when the stance vital empties (AgentGuide/VitalsEncountersDebug.md); the plugin never sends it on its own.
Tags the stimulus must or must not carry: the attacker's damage tags and the telegraph's Tags arrive here, so a parry rule can skip unblockable swings.
Conditions
Gates that must all pass (below).
ReactionLinks (unreal.SECReactionTriggerLink)
ReactionId, WeightMultiplier.
ReactionCategory
Answer with any reaction of this category instead of named links.
ReactionId, ExecutionMethod (as an action's; montage_method from AGENTS.md), Priority, SelectionWeight, Cooldown (unreal.ActionCooldown), CustomScoring (Scorers, Gates), AddTags, RequiresTags, BlockTags (tags on the enemy itself), ReactionCategory, bCancelCurrentAction (cut the attack it was making).
SEC.State.ReactionActive in one reaction's AddTags and another's BlockTags stops them overlapping.
Two rules on SEC.Stimulus.AttackTelegraphed: the parry rule at RulePriority 10 with Chance 0.4, MinInterval 2 and a BlockContextTags of the tag the unblockable swings' telegraph carries; the dodge rule at priority 0 with Chance 0.5. A swing the parry rule skips or does not roll falls to the dodge. A Time To Impact Gate (MinSeconds 0.15) on each keeps the enemy from starting an answer it cannot finish. The attacker's montage needs a SEC Telegraph Window with a Broadcast.Shape (
AGENTS.md, Recipe 4).
For i-frames on the dodge, a SEC State Tag Window holding SEC.State.Intangible over its evasive frames. A parry that stops the hit is a defense rule: AgentGuide/WeaponsDamageDefense.md.
What the enemy can notice before an attack lands: StimulusFilters on the AI Config (USECStimulusFilter_FacingConeHalfAngleDegrees, USECStimulusFilter_AwarenessMinimumState, USECStimulusFilter_LineOfSightTraceChannel, EyeHeight; each has BypassOnContextTags). Without them it notices everything in range.
WeaponReactionSet on a weapon's class defaults wins while it is equipped; RoleReactionSets on the AI Config takes unreal.RoleReactionSetConfig rows per role.
Tasks: more health; a stance meter that breaks into a stun; stamina for the enemy; what happens on death; a fight that starts when the player walks in and shows a boss bar; turning off the demo hit effects; finding out why an enemy does nothing.
The pawn's SEC Vitals component (Vitals) holds Vitals rows (unreal.SECVitalDefinition): VitalTag, MaxValue, RegenRatePerSecond, RegenDelaySeconds, DepletedLooseTag (held while empty), DepletedResultTag (on the hit that empties it), DepletedGameplayEvent (a gameplay event sent to the enemy's own ability system once when it empties, starting a granted ability that lists the tag as a trigger; a Reaction Set never hears it). The duplicated demo enemy has health and stance rows.
A stance meter that breaks:
vit = defaults(bp).get_editor_property("Vitals")rows = list(vit.get_editor_property("Vitals"))names = [str(unreal.GameplayTagLibrary.get_tag_name(r.get_editor_property("VitalTag"))) for r in rows]if "SEC.Vital.Stance" in names: # the duplicated demo enemy has one stance = rows[names.index("SEC.Vital.Stance")]else: stance = unreal.SECVitalDefinition() stance.set_editor_property("VitalTag", tag("SEC.Vital.Stance")) rows.append(stance)stance.set_editor_property("MaxValue", 100.0)stance.set_editor_property("RegenRatePerSecond", 15.0)stance.set_editor_property("RegenDelaySeconds", 2.0)stance.set_editor_property("DepletedLooseTag", tag("SEC.State.StanceBroken"))vit.set_editor_property("Vitals", rows)save(bp)
Attacks drain it through AdditionalVitalDamage on their damage configs (AgentGuide/WeaponsDamageDefense.md). DepletedLooseTag holds SEC.State.StanceBroken on the enemy from the moment the pool empties until it starts refilling, 2 seconds here; list that tag in each action's BlockTags so a stunned enemy does not swing.
The stun animation is a reaction on SEC.Stimulus.StanceBroken (AgentGuide/Reactions.md) playing the customer's stun montage, and the plugin never sends that stimulus. Sending it is a Blueprint step for the customer: in the enemy Blueprint, bind the Vitals component's On Vital Depleted, and when its Vital Tag is SEC.Vital.Stance, call Report Stimulus To with Receiver set to the enemy and a stimulus whose Tags hold SEC.Stimulus.StanceBroken. The call reports on the server only and does nothing on a client.
Which vital counts as health and as stamina is HealthVitalTag and StaminaVitalTag in Project Settings.
With bAutoHandleDeathOnHealthDepleted on (the default) on the controller's CombatControllerComponent, an emptied health vital shuts the enemy down: brain, actions, reactions, traces, role. bDropWeaponOnDeath drops its weapon. Playing a death montage or a ragdoll is the controller's On Death event in Blueprint, a step for the customer; the demo Blueprints show it.
A placed SEC Encounter Trigger (ASECEncounterTrigger) names a place or opens a fight when the player walks into its TriggerBox: EncounterName, Enemies (on the placed actor only), Payload, bTriggerOnce, StartCondition (PLAYER_ENTERS, MANUAL), bEndWhenPlayerLeaves. The demo's /SoulslikeEnemyCombat/Demo/Game/BP_SEC_BossArenaTrigger also ends the fight as Defeated once its enemies die, so spawn that one (AGENTS.md, Recipe 5, step 4).
A boss bar is a widget bound to the SEC Encounter Subsystem's On Encounter Started event; the demo's WBP_SEC_HUD_Encounter is one. Adding it to the customer's HUD is a Blueprint step.
unreal.get_default_object(unreal.SoulslikeEnemyCombatSettings): RoleLimits, AdditionalRoles, RoleReassignmentInterval, MinTimeInRole, TokenBudgets, DefaultTargetSelectorClass, DefaultTargetFilterClass, HealthVitalTag, StaminaVitalTag. The demo hit effects are switched off with Load Demo Hit Effects under Project Settings, Plugins. Before UE 5.8 a value set from Python lasts until the editor closes (AgentGuide/Python.md), so ask the customer to set it in Project Settings,
Plugins, Soulslike Enemy Combat.
Raises every SEC log category; SEC.Logs puts them back
After Play, log_warnings() returns SEC's warnings, each naming the asset and the fix. sec.validate_asset(asset) checks an Action Set, Reaction Set or damage config without playing.
AGENTS.md, Recipe 2, makes a weapon and Recipe 1, Step 3, a damage config. This file covers the rest: loadouts, hit volumes, what a hit is worth beyond health, effects on landing, and an enemy that blocks or parries.
Tasks: drop the weapon on death; several weapons; heavy hits that drain stamina or stance and knock the player back; sparks on metal; swings that spare other enemies; an enemy that blocks frontal hits while guarding; a parry that turns a blow aside.
Every hit effect also has AffectedSides, SuppressOnResultTags, RequireAnyResultTags (run only on Blocked, Parried and the like), RequireAnyTargetTags, SuppressOnTargetTags, bRequireDamageApplied, bRunOnSceneryHits. Make each with the damage config as outer.
MeleeTraceComponent on the pawn: DefaultDamageConfig, TeamFilter (unreal.SECTeamFilter: NO_FILTER, ENEMIES_ONLY, FRIENDLIES_ONLY, NEUTRAL_ONLY), MaxTargetsPerSwing, HitResetInterval, PassThroughTags (SEC.State.Intangible lets i-frames work), SubStepDistance, bDrawDebug. The demo enemy's TeamFilter is ENEMIES_ONLY, so its swings spare other enemies.
A Defense Rule Set (USECDefenseRuleSet) lists Responses run in order and OutcomeEffects. The pawn's SEC Defense component (Defense) runs its DefenseSet and Responses; a weapon lends its WeaponDefenseSet while equipped.
Every response has RequiredTags and BlockedTags (shown as Required and Blocked Defender Tags: what the enemy must hold, such as SEC.State.Blocking), ResultTag (what the hit is called: the attacker's effects read it), RequiredAttackTags, BlockedAttackTags, and a cost (CostVitalTag, FlatCost, CostPerDamage).
The guard holds while the enemy carries SEC.State.Blocking: a SEC State Tag Window on a block reaction's montage, or AddTags on a block action or reaction. The demo's two-handed weapon uses DefenseRuleSet_SEC_TwoHanded and GA_SEC_TwoHanded_Block. For a parry, a second Guard response with a narrower arc, RequiredTags of the parry window's tag and its own ResultTag, placed first.
Check: reload the rule set and print each response's class, tags and arc. In Play, a frontal hit while the enemy blocks deals nothing.