Documentation Unreal Engine Player Combat Tutorial
Universal Player Combat ships a guide for the AI coding agent you already use, such as Claude Code, Codex or Cursor: hand it over with the editor open, and it builds a player from your mesh and animations.
The guide is Resources/AGENTS.md in the plugin folder; its full text is at the end of this page. It tells your agent how to reach your open Unreal Editor, what to ask you, and every step of each build, with checks along the way. It builds assets and Blueprints in your own folder, with no C++. It edits a few things outside that folder (listed under What it changes outside your folder) and leaves two jobs to you:
Adding the UPC Hit Stop node, followed by Inertialization, to an Animation Blueprint you already had (without it, hits land with no frozen pose).
Reach animations for drawing and putting away an item on a custom skeleton; until you have them, the item has no Stowable fragment and disappears when put away.
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/universal-player-combat/agents.
Type: "Read Resources/AGENTS.md in the UniversalPlayerCombat plugin folder before you start".
A plugin installed from Fab sits under Engine/Plugins/Marketplace/ in your engine folder.
3
Let it point your project at the guide
On first use the agent offers to point your project at the guide, so later sessions find it. With your OK it adds one line naming the guide to CLAUDE.md (Claude Code) and to AGENTS.md (Codex and Cursor) in your project folder, and copies the upc-agent skill into .claude/skills, so /upc-agent starts a later Claude Code session on the guide.
4
Ask for a character
Tell it what to build and where your assets are. It asks for what your asset names do not settle, builds step by step, and ends with a report of what it made and assumed.
The agent uses the Python bridge unless you ask for Epic's Unreal MCP (the editor's own server for AI agents, in 5.8) or your session already has it connected. Either way it asks your OK before it turns on a plugin or edits your 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
Epic's Python Editor Script Plugin, which the engine marks Beta, and its remote execution, turned on in your project: two edits the agent makes with your OK, 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, which the engine marks Experimental, turned on with your OK, 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 its own tools to it, as the UPCAgentTools toolset, for the steps Epic's tools do not cover.
Other Unreal MCP servers are not supported, even when your agent has one connected.
The animations: a folder or a list. It sorts them by name into light attacks in combo order, a heavy attack, a dodge, hit reactions, idle and locomotion, and asks when the order of the light combo is unclear.
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 player holds one. Without one, the player fights with its hands.
The folder and name: /Game/Player/ unless you say otherwise.
It creates everything in that folder and duplicates a shipped asset into it before changing it.
The open level's GameMode Override, set to the character's game mode (the report names the level).
With your OK, on first use: one line naming the guide in your project's CLAUDE.md and AGENTS.md, and for Claude Code a copy of the upc-agent skill in .claude/skills.
With your OK, to reach the editor: the .uproject plugin entries and config lines for the Python bridge, or for Epic's Unreal MCP those entries plus the MCP client entry (.mcp.json for Claude Code), each followed by an editor restart.
The guide's reference files, one per area, which the guide reads as it builds.
Resources/AgentBridge/run_in_editor.py
The Python bridge: sends the agent's Python to the open editor and prints what the editor printed.
Resources/AgentSkills/upc-agent/SKILL.md
The /upc-agent skill for Claude Code.
Content/Python/upc_agent_tools
The plugin's agent tools: Python helpers for montages, notify windows, blade sockets, a locomotion Animation Blueprint and action graphs, offered on Epic's MCP as the UPCAgentTools toolset.
The rest of this page is the guide as the plugin ships it.
For the customer (an agent reading this file skips this block): an AI agent may not find this file on its own. Fab installs the plugin under the engine's Engine/Plugins/Marketplace/ folder. Tell the agent "Read Resources/AGENTS.md in the UniversalPlayerCombat plugin folder before you start". The same text is at https://rigbak.com/docs/universal-player-combat/agents. On first use, the agent offers to point the project at this file, so later sessions find it on their own.
Universal Player Combat (UPC) is an Unreal Engine 5.6 to 5.8 plugin for the player's side of combat: combos built as action graphs, dodges, items that bring their own moves, weapon hit detection, lock-on and a combat camera. You build a player from a character Blueprint, a few data assets and the customer's animations; 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.
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 UniversalPlayerCombat.uplugin: Plugins/UniversalPlayerCombat 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 Universal Player 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 /upc-agent command. Copy the folder <plugin folder>/Resources/AgentSkills/upc-agent into the project's .claude/skills/ folder. Typing /upc-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 a version such as 5.7, the engine folder is the InstallLocation of the UE_5.7 entry in the launcher's LauncherInstalled.dat (JSON text despite its extension, so search it as text when a file reader refuses it): C:/ProgramData/Epic/UnrealEngineLauncher/LauncherInstalled.dat on Windows, usually naming C:/Program Files/Epic Games/UE_5.7, and ~/Library/Application Support/Epic/UnrealEngineLauncher/LauncherInstalled.dat on a Mac. A source-built engine, and every engine on Linux, which has no launcher, is an identifier in EngineAssociation that UnrealEngine/Install.ini lists under [Installations] with its folder, in ~/.config/Epic/ on Linux and ~/Library/Application Support/Epic/ on a Mac; on Windows the registry key HKEY_CURRENT_USER\Software\Epic Games\Unreal Engine\Builds lists it. When none of these names the folder, ask the customer.
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. On a Mac the engine's Python is Engine/Binaries/ThirdParty/Python3/Mac/bin/python3, on Linux Engine/Binaries/ThirdParty/Python3/Linux/bin/python3. 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. The bridge runs on Epic's Python Editor Script Plugin, which the engine marks Beta and UPC itself does not need, so make these two edits only with the customer's OK, 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, which the engine marks Experimental and UPC itself does not need, so turn them on only with the customer's OK. Its tools are list_toolsets, describe_toolset and call_tool. To set it up, with that 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, UPCAgentTools (list_toolsets names it upc_agent_tools._toolset.UPCAgentTools; 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, add blade sockets to a weapon mesh, and wire the moves of an action graph. In Python they are the module upc_agent_tools, which ships with the plugin; on the Epic route they are the tools of the same names in UPCAgentTools, which take their arguments as "Driving the editor without Python" says.
Ask only what you cannot work out from asset names. State your assumptions in your final report so the customer can correct them.
A player they already have. A request to change the customer's player ("give my player a dodge") works on that player. 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()players = [str(a.package_name) for a in reg.get_assets_by_path("/Game", recursive=True) if (a.get_tag_value("NativeParentClass") or "").endswith(".UPCCharacterBase'")]print(players)
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 player has: DefaultItem on its EquipmentManagerComponent, StartingItems on its InventoryComponent, and those items' action graphs (upc.describe_action_graph). Add to it: a new move goes into the graph of the item it belongs to, a new weapon into the inventory's StartingItems, and nothing the player already does is rebuilt.
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 light attacks (in combo order), heavy attack, dodge, hit reactions, idle and locomotion. Ask when the order of the light combo is unclear.
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 player holds one. Without one, the player fights with its hands.
The folder and name. Default: /Game/Player/, with <Name> a short PascalCase name such as Knight.
Create everything in the customer's folder. Do not edit or save anything under /UniversalPlayerCombat/; 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 /UniversalPlayerCombat/), ask the customer which of their levels to use, or make one in their folder.
Use the exact property names in this file. set_editor_property accepts the C++ name (HeldMesh, bIsMainWeapon) as well as the Python name. Several editor labels differ from the property: the Visuals fragment's Mesh is HeldMesh, a transition's Fires On is its trigger, When No Window Is Authored is ChainWithoutWindow, Main Weapon is bIsMainWeapon.
In Python, a C++ type drops its U, A, F or E prefix: UUPCItemDefinition is unreal.UPCItemDefinition, FUPCTraceSocket is unreal.UPCTraceSocket. Enum values are upper snake case: EUPCTraceShape::CapsuleTwoPoint is unreal.UPCTraceShape.CAPSULE_TWO_POINT. The engine's animation functions are unreal.AnimationLibrary.
An input tag on an item mapping and on a graph transition must match exactly. A parent tag does not count.
Make montages only from animations on the character's own skeleton, the one its mesh names. An animation of another skeleton plays with that skeleton's proportions, or not at all, and nothing warns. When one does not match, ask the customer for the mesh's own animation or a retargeted copy.
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 upc_agent_tools as upcEAL = unreal.EditorAssetLibraryTOOLS = unreal.AssetToolsHelpers.get_asset_tools()DEMO = "/UniversalPlayerCombat/Demo"def load(path): obj = EAL.load_asset(path) if obj is None: raise RuntimeError(f"No asset at {path}") return objdef 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 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 unreal.AnimationLibrary.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="LogUPC"): 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 UPC'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 (asset_type is the parent class)
defaults(bp) and setting class defaults
BlueprintTools.get_default_object, then ObjectTools.set_properties
Instanced objects (item fragments, payloads)
ObjectTools.set_properties with the class path, as below
UPCAgentTools tools of the same names, given every argument ([] for no tags). A class is its path: add_notify_state_to_montage takes /Script/UPCCore.UPCMeleeTraceNotifyState and a track name (Combat), add_node takes /Script/UPCActions.UPCHoldActionPayload. set_montage_sections takes three lists in place of tuples: names, start seconds and next sections.
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/Player/AM_Knight_Light1.AM_Knight_Light1. The package path alone is refused.
A class is /Script/<Module>.<Name>, the C++ name without its first letter. The classes this guide uses are in these modules: UPCCore (damage configs, trace sockets, the melee trace, state tag and anim effect windows, the gameplay event notify), UPCMotion (the approach window), UPCActions (action graphs, the action window, transitions, input mappings, ability sets), UPCEquipment (item definitions and their fragments, hold places, the inventory, the equipment manager, the equip commit notify), UPCTargeting (targetable, combat styles, targeting configs), UPCFoundation (telegraph marker windows, the movement component) and UPCGame (the character base). For example /Script/UPCActions.UPCActionWindowNotifyState and /Script/UPCEquipment.UPCItemDefinition.
An instanced object (an item fragment, a payload) is made by giving its class path as a plain string: {"Fragments": ["/Script/UPCEquipment.UPCItemFragment_Visuals", "/Script/UPCEquipment.UPCItemFragment_Equippable"]}. Properties written beside the class path are dropped. Read the owner back with ObjectTools.get_properties, which gives each new object's path (.../Item_Knight_Sword.Item_Knight_Sword:UPCItemFragment_Visuals_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": "LogUPC[A-Za-z]*: (Warning|Error)"}.
F = "/Game/Player" # the customer's folderN = "Knight" # the player's namemesh = load("/Game/.../SKM_Knight")abp = load("/Game/.../ABP_Knight") # the customer's Animation Blueprint, or the one "No Animation Blueprint" makesprobe = load("/Game/.../Attack_01") # any animation on this skeleton, for bone checks
Duplicate the demo player. It carries everything a player needs beyond the base class: camera, movement and look input, sprint, Motion Warping, physical reactions, telegraph markers and a starting ability set with lock-on, and its TeamComponent from the base class is set to the player's team (Team ID 0). Give it a copy of that ability set without the demo's parry recoil and stun:
bp = duplicate(f"{DEMO}/Characters/Player/BP_UPC_Character", F, f"BP_{N}")abilities = duplicate(f"{DEMO}/Abilities/AbilitySet_UPC_Default", F, f"AbilitySet_{N}")rows = [r for r in abilities.get_editor_property("GrantedGameplayAbilities") if r.get_editor_property("Ability").get_name() not in ("GA_UPC_ParryRecoil_C", "GA_UPC_PlayerStun_C")]abilities.set_editor_property("GrantedGameplayAbilities", rows)save(abilities)d = defaults(bp)d.set_editor_property("StartingAbilitySet", abilities)body = d.get_editor_property("Mesh")body.set_editor_property("SkeletalMeshAsset", mesh)body.set_editor_property("AnimClass", abp.generated_class())print(body.get_editor_property("ComponentTags")) # must hold UPC.Montage and UPC.Attachment.Bodyplaces = d.get_editor_property("EquipmentManagerComponent").get_editor_property("BodyPlaces")for p in places: print(p.get_editor_property("Place"), p.get_editor_property("Socket"))save(bp)
GA_UPC_ParryRecoil and GA_UPC_PlayerStun play the demo mannequin's recoil and stun montages, which would make the customer's character perform the demo's motion. The copy keeps lock-on, sprint, interact, drop and the flinch on a poise break (GA_UPC_HitReact, which plays no animation). Without them the copy is never stunned and does not stagger back from a parried blow. To bring a recoil back on the customer's own animation, make a child Blueprint of GA_UPC_ParryRecoil, set its StunMontage, and add it to the copy. A stun is a child of GA_UPC_Stun added the same way; the duplicate's UPC.Vital.Stance row already sends UPC.Event.GuardBreak as its DepletedGameplayEvent, which starts it when the stance runs out. The demo's own player stun also lists UPC.Style.Soulslike in its ActivationRequiredTags, so only the Soulslike style stuns the demo player; leave that list empty for a stun under every style.
UPC.Montage on the mesh is where montages play; UPC.Attachment.Body is where items attach. Without the attachment tag, items do not show. The duplicate keeps both.
BodyPlaces maps each place to a socket: UPC.Place.Main to hand_r, UPC.Place.Off to hand_l, and three back places to the mannequin's stow_back_r, stow_back_l and back_center_heavy sockets. It also carries the demo's worn gear places: UPC.Place.Head on head, UPC.Place.Ring.Left on ring_01_l, UPC.Place.Ring.Right on ring_01_r, UPC.Place.Shoulders on back_center_heavy, and UPC.Place.Chest, .Hands, .Feet and .Neck with no socket. Change a row's Socket if the customer's skeleton names those bones otherwise, and delete the worn rows a game does not use. The back places matter only for items that hang on the body, the worn places only for worn gear (AgentGuide/ItemsEquipment.md, Worn gear).
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 character floats, sinks or faces sideways, adjust the Mesh component's RelativeLocation and RelativeRotation.
Check: capture the Blueprint viewport; the mesh stands inside the capsule, facing along the blue arrow.
This step sets the GameMode Override of the level open in the editor to the new game mode, so Play spawns the customer's character. Make the change, then name the level in your final report: it is one setting of that level, which the customer can set back in its World Settings.
Make the game mode a child Blueprint of the demo's BP_UPC_GameMode rather than a duplicate: the duplicated player's death and the respawn key bring the player back only through a BP_UPC_GameMode. A project with its own game mode makes it a child of BP_UPC_GameMode too. On Epic's MCP, BlueprintTools.create makes the child, with asset_type/UniversalPlayerCombat/Demo/Game/BP_UPC_GameMode.BP_UPC_GameMode_C.
The game mode keeps the demo's player controller, BP_UPC_PlayerController, which adds the movement keys (IMC_UPC_Default: move, look, sprint, lock-on, dash) and the belt of item slots. To use it in every level, the customer sets it in Project Settings, Maps and Modes.
Check: press Play. The character spawns, moves with the movement keys, and the camera follows. It has no attacks yet.
A move is a node in an action graph. A node plays a montage; a transition names the input that leads from one node to the next. The graph reaches the character through an item: the item that drives combat hands its graph over while it is drawn. A character with nothing in its hands uses its Default Item, so a bare-handed combo lives on an "unarmed" item.
skeleton = mesh.get_editor_property("Skeleton")other = [key for key, seq in seqs.items() if seq.get_editor_property("Skeleton") != skeleton]if other: raise RuntimeError(f"{other} are animations of another skeleton than {mesh.get_name()}'s")am = {key: upc.create_montage_from_animation(seq, F, f"AM_{N}_{key}") for key, seq in seqs.items()}
The tool turns root motion on, so an attack that steps forward moves the player, and saves the montage. It plays on the DefaultSlot slot.
For an attack animation that stops on its strike pose, with no return to idle, set the montage's Blend Out to the fade length (the demo combos use 0.5 s) and Blend Out Trigger Time to 0. The montage then holds its last frame and fades to idle, and looks the same on every player's machine in multiplayer.
Check: no error. An animation of another skeleton stops the step: ask the customer for the mesh's own animation or a retargeted copy, as the Rules say.
The demo characters flinch only on the hit that empties their poise, so a config with no poise row never flinches them. 20 and 60 match the demo's punch and the sword's heavy swing; AgentGuide/DamageDefense.md, Hit reactions, covers poise.
UPC Melee Trace Window (UUPCMeleeTraceNotifyState) over the frames that connect. Nothing takes damage without it. Without frame data from the customer, start it at 30% of the montage (40% for the heavy attack) and make it 25% long.
UPC Action Window (UUPCActionWindowNotifyState) where the next press may start the next attack. It has no settings. Put it on every attack that leads into another, from about 55% to 90%. A press made earlier waits for the window.
for key in ("Light1", "Light2", "Light3", "Heavy"): m, L = am[key], length(am[key]) start = 0.40 if key == "Heavy" else 0.30 w = upc.add_notify_state_to_montage(m, unreal.UPCMeleeTraceNotifyState, L * start, L * 0.25) w.set_editor_property("SocketIDs", []) # [] sweeps every socket the item registered w.set_editor_property("DamageConfig", dmg_heavy if key == "Heavy" else dmg_light) if key in ("Light1", "Light2"): upc.add_notify_state_to_montage(m, unreal.UPCActionWindowNotifyState, L * 0.55, L * 0.35) save(m)
Tell the customer the times you picked; only a person can judge whether the window sits on the right frames.
Check:unreal.AnimationLibrary.get_animation_notify_events(am["Light1"]) lists both windows.
Every graph starts with a node named ROOT, the character standing idle. Add one node per move and wire the transitions. A move that ends with nothing leading on returns to ROOT by itself.
Compile after the last edit and before saving: an edited graph keeps running its previous compile until then. Node names must be unique; ROOT is taken.
The move finished with nothing chaining. Pass "" as the input tag.
ON_FAILED
Action Failed
The move could not start. Pass "" as the input tag.
Timing (timing, unreal.UPCTransitionTiming): WAIT_FOR_WINDOW, the default, waits for the running move's action window; INTERRUPT cuts the running move at once, for a dodge or a parry.
Check: the describe output lists ROOT, the four moves with their montages, and each transition with its input tag.
IMC_UPC_Unarmed already maps the left mouse button to IA_UPC_LightAttack, the right mouse button to IA_UPC_HeavyAttack, Q to IA_UPC_Guard and Space to IA_UPC_Dash, with gamepad keys for each, so the copy needs no new key. For a key the demo lacks, key = unreal.Key(), key.import_text("F"), imc.map_key(action, key), then save(imc).
An Item Definition (UUPCItemDefinition) is a list of fragments. The Equippable fragment (UUPCItemFragment_Equippable) makes it drawable and carries its graph, its keys and its hit volumes.
item = new_data_asset(unreal.UPCItemDefinition, F, f"Item_{N}_Unarmed")eq = unreal.new_object(unreal.UPCItemFragment_Equippable, outer=item)eq.set_editor_property("ActionGraph", graph)eq.set_editor_property("InputMappingContext", imc)def mapping(action_name, tag_name): m = unreal.UPCInputMapping() m.set_editor_property("InputAction", load(f"{DEMO}/Inputs/{action_name}")) m.set_editor_property("InputTag", tag(tag_name)) return meq.set_editor_property("InputMappings", [ mapping("IA_UPC_LightAttack", "UPC.Input.LightAttack"), mapping("IA_UPC_HeavyAttack", "UPC.Input.HeavyAttack"), mapping("IA_UPC_Dash", "UPC.Input.Dash"),])def hand_socket(socket_id, bone): s = unreal.UPCTraceSocket() s.set_editor_property("ID", socket_id) s.set_editor_property("Shape", unreal.UPCTraceShape.SPHERE) s.set_editor_property("SocketOrBoneName", bone) s.set_editor_property("Radius", 12.0) return s # SourceComponentTag left empty: the socket follows the body mesheq.set_editor_property("TraceSockets", [hand_socket("HandR", "hand_r"), hand_socket("HandL", "hand_l")])eq.set_editor_property("AttackStopDistance", 25.0)item.set_editor_property("Fragments", [eq])save(item)
AttackStopDistance is how close in front of a target this item's attacks stop, in centimeters between the two bodies. The duplicated player's combat style stops attacks 50 cm short, which suits a blade; at that distance a punch stops out of reach, so fists take about 25. One move with a shorter reach than the rest, such as a shield bash beside a sword, sets its own StopDistance on its Play Montage node (AgentGuide/TargetingStylesSteering.md).
A new Equippable fragment already holds the main hand in HoldPlaces (UPC.Place.Main).
Check: reload the item and print each fragment's class, ActionGraph, the three mappings, the two sockets and AttackStopDistance.
d = defaults(bp)d.get_editor_property("EquipmentManagerComponent").set_editor_property("DefaultItem", item)d.get_editor_property("MeleeTraceComponent").set_editor_property("DefaultDamageConfig", dmg_light)save(bp)
DefaultItem is what the character holds when nothing else is drawn.
Success looks like:process_action_input reports the press as entered, the overlay above the character names Light1, and the first attack plays. Ask the customer to press light attack three times: the attacks chain in order and the character returns to idle after the third. Against a demo enemy, its punch stops at the second light hit, as its poise breaks. log_warnings() returns nothing about this character.
A dodge is a graph node like any attack, reached on its own input, whose montage holds i-frames: the tag UPC.State.Intangible, during which enemy swings pass through.
The dodge uses UPC.Input.Dash, which the unarmed item already maps to IA_UPC_Dash.
INTERRUPT lets the dodge cut a swing. A running move still keeps its first 0.1 seconds (Cancel Window Start on the node) before an interrupt fires.
A UPC melee trace skips a target holding UPC.State.Intangible (the trace's PassThroughTags default), so no other setting is needed.
Multiplayer detail: for a dodge that travels far and fast, also put UPC.State.Dashing in the same State Tag Window and set GraceTag = UPC.State.Dashing on the character's CharacterMovement (a UUPCCharacterMovementComponent), so the server accepts the dash's travel instead of pulling the player back.
To give the dodge a stamina cost or a cooldown, see AgentGuide/GatesCostsHooks.md.
Check: in Play, pressing the dash key mid-swing cuts to the roll, and UPC.Debug.Actions 1 names the Dodge node.
A blade is traced by a two-point capsule between two sockets on its mesh, by default TraceStart (above the guard) and TraceEnd (the tip). Check, and add them when missing:
sword_mesh = load("/Game/.../SM_Sword")for name in ("TraceStart", "TraceEnd"): print(name, sword_mesh.find_socket(name) is not None)print(upc.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.
Repeat Recipe 2, Steps 1 to 4, with the weapon's animations: montages AM_<Name>_Sword_*, damage configs, windows, and a graph AG_<Name>_Sword kept in the variable sword_graph. Add the dodge node from Recipe 3 to this graph too: a drawn weapon's graph replaces the unarmed one, so moves the player keeps with a weapon in hand must be in the weapon's graph.
IMC_UPC_Sword maps keys to IA_UPC_LightAttack, IA_UPC_HeavyAttack and IA_UPC_Guard. The dash key needs no mapping here: the controller's IMC_UPC_Default maps Space to IA_UPC_Dash whatever the player holds, and the item's IA_UPC_Dash row in Step 4 routes it to the sword's graph.
An item with a Visuals HeldMesh and no ItemActorClass shows that mesh in the hand on a component tagged UPC.ItemMesh, which is why the blade socket's SourceComponentTag is UPC.ItemMesh. An empty tag would trace from the body mesh instead.
bIsMainWeapon (editor label Main Weapon) is on by default: the sword's graph takes over while it is drawn.
The item has no Stowable fragment, so it disappears when put away rather than hanging on the body. Stowing needs reach animations and back sockets on the customer's skeleton; see the Equipment page.
inv = defaults(bp).get_editor_property("InventoryComponent")items = list(inv.get_editor_property("StartingItems"))if sword not in items: items.append(sword)inv.set_editor_property("StartingItems", items)save(bp)
DefaultItem, what the character holds when nothing else is drawn, is the demo's unarmed item on the duplicated character until Recipe 2, Step 7 sets the customer's own. When Recipe 2 was not run, clear it, so the demo's moves do not stay on the character:
The belt on BP_UPC_PlayerController takes the item into its first free slot and draws it, since nothing else is held. The slot keys in IMC_UPC_Default (1 to 4) draw and put away each slot.
Check: in Play, the sword sits in the right hand and the light attack plays the sword combo.
If the sword is held at the wrong angle, judge a correction from a capture and set it as the Offset of the Equippable fragment's UPC.Place.Main row in HoldPlaces. Python cannot set a Hold Places row's fields one by one, so write the whole row as text:
offset = unreal.Transform(unreal.Vector(0, 0, 0), unreal.Rotator(roll=0, pitch=0, yaw=90), unreal.Vector(1, 1, 1))row = unreal.UPCHoldPlaceSpec()row.import_text(f'(Place=(TagName="UPC.Place.Main"),Offset={offset.export_text()})')eq.set_editor_property("HoldPlaces", [row]) # eq: the sword's Equippable fragment from Step 4save(sword)
The duplicated character already locks on: its starting ability set (AbilitySet_<Name>, the copy of AbilitySet_UPC_Default from Recipe 1) grants the lock-on ability on UPC.Input.LockOn, and the demo controller maps the middle mouse button (gamepad: top face button) to IA_UPC_LockOn. Tell the customer the key.
What can be locked. Any actor with a UPC Targetable component (UUPCTargetableComponent) that is not on the player's team. Every character built on the UPC character base has one. For another kind of enemy, add the component to its Blueprint; AimAtSocket names where the lock aims.
How locked movement feels. The combat style decides. The duplicate uses the Over Shoulder style. For the Soulslike style's strafing around a locked target:
d = defaults(bp)d.get_editor_property("CombatComponent").set_editor_property("Style", load(f"{DEMO}/Combat/Styles/CombatStyle_UPC_Soulslike"))save(bp)
Range. The style's targeting config sets the scan radius (ScanRadius, 2000 cm) and whether a lock breaks with distance (LockBreakDistance, 0 turns it off). To change them, duplicate TargetingConfig_UPC_Soulslike and point the style's Targeting fragment at the copy, which means duplicating the style as well.
Check: place an enemy (the demo's BP_UPC_Enemy_Weak works) and play. Pressing the lock key holds the camera and facing on the enemy; the ability system carries UPC.State.LockedOn. Pressing again releases.
The player 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 UPC 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, and already carries the UPC Hit Stop and Inertialization nodes after its slot, so a landed hit freezes the pose. To match the character's own speeds, set WalkSpeed and RunSpeed on its class defaults (defaults(abp)) and save it.
An Animation Blueprint the customer already has works as it is. Hit stop (the frozen pose on a landed hit) also needs a UPC Hit Stop node after the slot with an Inertialization node after it; without it the log says so once and hits land without the freeze. Adding the node to the customer's own Animation Blueprint is a step for the customer.
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 a plain write refuses, components on a Blueprint, montage sections, new gameplay tags, checking an asset
AgentGuide/Python.md
Other kinds of moves (a held guard, a wait, an ability), a move's settings, cancel windows, state tags, transitions back to idle, tag gates and conditions on a transition, sub graphs, editing or removing moves
AgentGuide/ActionGraphs.md
Stamina, attack costs, cooldowns and charges, resource and tag gates, sprint drain, slowing during a move
AgentGuide/GatesCostsHooks.md
New keys and input tags, chords, abilities on keys, sprint, pausing input
AgentGuide/InputAbilities.md
Item fragments, the off hand, shields, two-handed weapons, presets, stowing on the body, swing trails, worn gear (helmets, capes, rings)
AgentGuide/ItemsEquipment.md
Items that are not gear (keys, materials, potions), containers, inventory configs and their rules made from scratch, a keychain that collects keys by itself, weight limits, combine recipes, saving the inventory, inventory screens, using an item, storage boxes, bags on a bag bar, the belt, pickups, drops, weapon racks, loot chests, interactable objects
AgentGuide/InventoryInteraction.md
Hit effects, blocks and parries, health and stamina rows, hit reactions
AgentGuide/DamageDefense.md
Attacks closing on the target, combat styles, targeting configs, targetable actors
AgentGuide/TargetingStylesSteering.md
Camera settings, camera shots, attack warning markers
AgentGuide/CameraTelegraphs.md
Finishers, counters and other two-character moves (Paired Actions)
Containers rows (import_text), CarryRules, CombineSets, SortOrder, bEquipAtOnce, bPutAwayAtOnce, bCombineAtOnce; set on the character's InventoryComponent.Config
Combine Set
UUPCCombineSet
new_data_asset
Recipes rows (two items, their counts, Results, Seconds), Combines; listed on the config's CombineSets
Damage Config
UUPCDamageConfig
new_data_asset
Damage, AdditionalVitalDamage (a UPC.Vital.Poise row), DamageTags, HitEffects
Input Mapping Context
UInputMappingContext (duplicate a demo one)
duplicate, map_key
Keys to Input Actions
Montage
UAnimMontage
upc.create_montage_from_animation
Notify windows below
Character components you touch (all on the duplicated character): Mesh (SkeletalMeshAsset, AnimClass, ComponentTags), EquipmentManagerComponent (DefaultItem, BodyPlaces), InventoryComponent (StartingItems, Config), MeleeTraceComponent (DefaultDamageConfig), VitalsComponent (Vitals rows: VitalTag, MaxValue, RegenRatePerSecond), CombatComponent (Style), TargetingComponent (TargetingConfig), CharacterMovement (GraceTag).
Every demo item to try on: walk onto a pad to equip its item, again to put it away; an inventory style pad switches the inventory to that style and its starter kit
Demo/Maps/LVL_UPC_Wardrobe (its hall LVL_UPC_Wardrobe_Hall also streams into LVL_UPC_Demo, reached by the Wardrobe plate in the start room), its pads Demo/Showcase/Equipment/BP_UPC_EquipPad and Demo/Showcase/Inventory/BP_UPC_InventoryStylePad
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 demo's animation layers, reach montages and the montages of GA_UPC_Stun and GA_UPC_ParryRecoil belong to the demo mannequin's skeleton, so do not give them to another skeleton's character or items.
Where the next press may chain. Without one, the node's When No Window Is Authored (ChainWithoutWindow) decides: by default the next move starts when this one ends
none
UPC State Tag Window
UUPCStateTagNotifyState
Tags for a stretch: UPC.State.Intangible (i-frames), UPC.State.HyperArmor (less poise damage)
Tags
UPC Approach Window
UUPCApproachWindowNotifyState
Gliding to the target during a wind-up
UPC Telegraph Window
UUPCTelegraphMarkerNotifyState
A warning marker over an enemy's swing
UPC Anim Effect Window
UUPCAnimEffectWindowNotifyState
Swing trails (UPC.AnimEffect.Weapon.Trail) and glows
Effect
UPC Anim Effect
UUPCAnimEffectNotify
Footsteps, landings and other one-frame sounds the character's Anim Effect Bank plays
Effect
UPC Equipment Commit
UUPCEquipmentCommitNotify
The swap frame of a draw or stow reach
UPC Gameplay Event
UUPCGameplayEventNotify
Sending a gameplay event at a frame
EventTag
Attack steering (an attack turning to the target and closing the gap) uses the engine's Motion Warping notify with the UPC Skew Warp modifier and Warp Target Name UPCTarget. The duplicated character has the Motion Warping component it needs.
The item mapping's InputTag differs from the transition's, the item's mapping context lacks the Input Action, or the graph was not compiled after the last change (upc.describe_action_graph says so on its first line).
Press does nothing and the log names the ability system
The character has no Ability System Component; the duplicate has one.
Combo never chains
The press landed before the action window and the buffer is set to Ignore, the transition's trigger is not ON_STARTED, or the press came after the move ended, later than its late input grace (the graph's LateInputGrace, or the move's own override); a move that leaves through an AUTO transition to another move grants none.
Item not visible in the hand
No mesh tagged UPC.Attachment, or the body place's socket is missing on the skeleton. The log names both.
Hits pass through
No UPC Melee Trace Window on the montage, the window names a socket ID the item does not register, the blade socket's SourceComponentTag is wrong, the target is on the player's team, or an unarmed item has no short AttackStopDistance, so punches stop out of reach. The log names an unregistered socket ID.
Weapon held crooked
Set Offset on the item's HoldPlaces row, as in Recipe 4, Step 5.
T-pose, or montages do not play
The Animation Blueprint is for another skeleton, or has no DefaultSlot Slot node.
An attack plays stretched, twisted or not at all
Its montage was made from an animation of another skeleton (Rules).
Lock-on finds nothing
The target has no UPC Targetable component, or is on the player's team; or the targeting config is unset.
Dodge does not avoid hits
The dodge montage has no State Tag Window with UPC.State.Intangible, or it does not cover the frames of the incoming hit.
Draw or stow plays nothing on a custom skeleton
Reach montages from the demo belong to the mannequin. Leave the item without a Stowable fragment, or give it reach montages on the customer's skeleton.
Debug in Play: UPC.Debug.Actions 1 writes each character's graph state above its head; UPC.Logs Verbose raises every UPC log category and UPC.Logs puts them back.
When the customer wants to understand something rather than build it, find the page in https://rigbak.com/docs/universal-player-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 2, builds a combo from montage moves and input transitions. This file covers the rest of the graph: other kinds of moves, a move's settings, transitions back to idle, tag gates and conditions on a transition, sub graphs, and editing or removing what exists.
Tasks: a move held while a button is down (a guard) and released back to idle; a move that fires an ability or waits; a heavy attack that cannot be canceled early; tags on the character while a move runs; a transition only while a tag is held or only in one stick direction; the next attack starting at once; moves that exist only while an item is drawn; renaming, editing or removing a move or a transition.
upc.get_node_settings(graph, name) returns the move's settings as an unreal.UPCActionNode; edit the copy, nested fields included, and hand it to upc.set_node_settings(graph, name, settings):
s = upc.get_node_settings(graph, "Heavy")s.cancellation.cancel_window_start = 0.3 # nothing may cut the heavy for 0.3 supc.set_node_settings(graph, "Heavy", s)print(upc.compile_action_graph(graph))save(graph)
Every edit waits for upc.compile_action_graph before it reaches the game; compile after the last edit and before saving. upc.describe_action_graph(graph) then lists each move with its payload, the values changed from their defaults, its state tags, costs, gates and hooks, and each transition with its tags and conditions.
On Epic's MCP, set_node_settings cannot make a new cost, gate or hook, since the MCP has no owner to make it in. Add them on the move's card instead, the object that owns the move's payload: get_node_settings returns the payload's path, and the card's path is that path without its last part (.../AG_Knight.AG_Knight:<graph>.<card>). Call ObjectTools.set_properties on the card with {"ActionNodeData": {"Costs": ["/Script/UPCActions.UPCActionCost_Vital"]}} (or Gates, Hooks), read the card back with ObjectTools.get_properties (properties: ["ActionNodeData"]) for each new object's path, set the object's own properties there, then compile_action_graph. Everything else goes through set_node_settings.
The move's name. Write another name to rename it; transitions follow.
Execution (unreal.UPCNodeExecution)
Payload: what the move does (below). InputBufferOverride (unreal.UPCInputBufferOverride: USE_GRAPH_DEFAULT, IGNORE, ACCEPT_FIRST, ACCEPT_LAST): what an early press does. bOverrideLateInputGrace with LateInputGrace (seconds, 0 off): this move's own late input grace in place of the graph's. bRetriggerFromRootOnFailedStart: a move that cannot start goes back to idle and tries the same press once more.
Cancellation (unreal.UPCNodeCancellation)
CancelWindowStart (seconds, default 0.1): the earliest an Interrupt transition may cut the move. bCanBeCancelled, CancellableByInputs, IgnoredCancelInputs: whether, and by which input tags, a press may cancel the move outright.
State (unreal.UPCNodeState)
AppliedTags: tags the character holds while the move runs, such as UPC.State.Blocking for a guard or UPC.State.HyperArmor.
Costs, Gates, Hooks
Stamina costs, cooldowns and resource checks, and effects while the move runs: AgentGuide/GatesCostsHooks.md.
upc.add_montage_node makes a Play Montage move. upc.add_node(graph, name, payload_class) makes any other kind; read its payload from get_node_settings(...).get_editor_property("Execution").get_editor_property("Payload"), set its fields, and write the settings back. On Epic's MCP, add_node takes the payload's class path, such as /Script/UPCActions.UPCHoldActionPayload.
Class (editor label)
Does
Fields
UUPCMontageActionPayload (Play Montage)
Plays a montage
Montage, PlayRate, SectionName (start from a section; see AgentGuide/Python.md for adding sections), ChainWithoutWindow (label When No Window Is Authored; AT_COMPLETION starts the next move when this one ends, FROM_ENTRY lets the next press start it at once), BackMontage, LeftMontage, RightMontage (played in place of Montage when the stick pushes that way from the body's facing; empty plays Montage), bTurnToInputAtStart (turns the body to the stick as the move starts while rotation follows movement, then plays Montage), StopDistance (cm between the bodies this attack stops at; -1 uses a playing window's, else the weapon's, else the style's; AgentGuide/TargetingStylesSteering.md), MakeRoomSeconds (-1 follows the style, 0 skips the step back for this move, above 0 turns it on)
UUPCHoldActionPayload (Hold)
Runs until a transition leaves it, so its state tags stay on as long as the button is held. Plays no animation
bOpenActionWindow (on: any input leaves at once)
UUPCDelayActionPayload (Delay)
Waits, then ends so its Action Completed transition fires
The guard plays the customer's guard animation and holds its pose while the guard button is down; releasing it returns to idle (a transition to ROOT ends the running move). The move holds UPC.State.Blocking, and the first frames of the raise hold UPC.State.Parrying, which is what a guard rule set reads (AgentGuide/DamageDefense.md).
am_guard = upc.create_montage_from_animation(load("/Game/.../Guard"), F, f"AM_{N}_Guard", enable_root_motion=False)L = length(am_guard)upc.set_montage_sections(am_guard, [("Raise", 0.0, "Hold"), ("Hold", L * 0.5, "Hold")]) # the second half loops while heldw = upc.add_notify_state_to_montage(am_guard, unreal.UPCStateTagNotifyState, 0.0, min(0.25, L * 0.5))w.set_editor_property("Tags", tags("UPC.State.Parrying"))save(am_guard)upc.add_montage_node(graph, "Guard", am_guard, 1.0)s = upc.get_node_settings(graph, "Guard")s.state.applied_tags = tags("UPC.State.Blocking")s.cancellation.cancel_window_start = 0.0 # a quick tap lets go at onceupc.set_node_settings(graph, "Guard", s)upc.add_transition(graph, "ROOT", "Guard", "UPC.Input.Guard")upc.add_transition(graph, "Guard", "ROOT", "UPC.Input.Guard", unreal.UPCTransitionTrigger.ON_COMPLETED, unreal.UPCTransitionTiming.INTERRUPT)print(upc.compile_action_graph(graph))save(graph)
The item's mappings need a row for IA_UPC_Guard on UPC.Input.Guard (AGENTS.md, Recipe 2, Step 6). Both demo mapping contexts map it already.
Pick the loop point from the animation: the frame where the guard is fully up. Tell the customer the times you picked.
The looping guard opens no action window, so every way out of it is an INTERRUPT transition: the release above, and an attack out of the guard. A release that waits for the window never lands.
A guard whose pose comes from the item's animation layer reading UPC.State.Blocking, as the demo's do, needs no montage: make the move with upc.add_node(graph, "Guard", unreal.UPCHoldActionPayload) instead, with the same tags and transitions.
Check:upc.describe_action_graph(graph) lists Guard plays AM_..._Guard ...; state tags UPC.State.Blocking, UPC.Input.Guard, Input Pressed ... -> Guard from ROOT and UPC.Input.Guard, Input Released, Interrupt ... -> ROOT from Guard. unreal.AnimationLibrary.get_animation_notify_events(am_guard) lists the State Tag Window, and am_guard.get_num_sections() is 2.
upc.add_transition(graph, from_node, to_node, input_tag, fires_on, timing, priority); to_node may be ROOT.
fires_on (unreal.UPCTransitionTrigger)
Editor label
Fires when
ON_STARTED
Input Pressed
The input is pressed (combos)
ON_COMPLETED
Input Released
The input is released (letting go of a hold)
ON_CANCELLED
Input Released Early
The input is released before its Input Action's hold trigger finishes
AUTO
Action Completed
The move ended on its own; input tag ""
ON_FAILED
Action Failed
The move could not start (a cost, a gate); input tag ""
timing is WAIT_FOR_WINDOW (the press waits for the running move's action window) or INTERRUPT (it cuts the running move once CancelWindowStart has passed: dodges and parries). Higher priority wins between transitions on the same input; a transition with conditions wins over an equal one without.
Dodge from every move: one add_transition(graph, move, "Dodge", "UPC.Input.Dash", ON_STARTED, INTERRUPT, 10) per move, as AGENTS.md, Recipe 3, does, or one add_alias(graph, "Dodge", "UPC.Input.Dash", ON_STARTED, INTERRUPT, 10), which also reaches moves added later and the moves a sub graph imports. required_tags and blocked_tags on an alias route one press to different moves by the character's tags.
A dodge per direction: one Play Montage move whose Montage is the forward dodge and whose BackMontage, LeftMontage and RightMontage hold the others; the stick's push against the body's facing picks one as the move starts, and in multiplayer the player's own machine makes the pick and the server plays the same. With bTurnToInputAtStart on, a character whose rotation follows its movement turns to the push and plays Montage, so one roll goes every way; locked on or facing the camera, it plays the direction montages instead. The demo routes Dash by style this way: AG_UPC_Sword sends it to Hop under UPC.Style.OverShoulder, to Dodge Locked under UPC.State.LockedOn, and to Dodge (the turning roll) otherwise.
upc.get_transition_settings(graph, from_node, to_node, input_tag, fires_on=None) returns an unreal.UPCActionTransition with InputTag, Trigger, Timing, Priority, RequiredTags, BlockedTags (tags on the character; blocked beats required) and Conditions. Edit it and write it back with upc.set_transition_settings(graph, from_node, to_node, input_tag, settings, fires_on=None), naming the transition as it is before the edit.
shield_graph's transitions leaving its ROOT leave ROOT here too, each requiring the entry tags; its other moves come along. Recompile the parent after editing the source graph. entry_blocked_tags and priority_bias (added to each entry transition's priority) are optional.
To start the folded-in moves from more moves than one, such as a shield guard pressed mid-combo, attach those moves to the same import; the moves are imported once and each attached move gets the entry transitions with the same tags and priority:
for move in ("Light1", "Light2", "Light3"): upc.attach_sub_graph(sword_graph, move, shield_graph)print(upc.compile_action_graph(sword_graph))
An entry that should cut a running attack needs INTERRUPT timing on the source graph's ROOT transition, and a priority above the parent's own transition on the same input (priority_bias).
On the graph asset: DefaultInputBufferMode (unreal.UPCInputBufferMode: IGNORE, ACCEPT_FIRST, ACCEPT_LAST, the default) decides what a press made before the action window does; LateInputGrace (seconds, default 0.25, 0 off): how long after a move runs to its end a press or release still follows that move's own transitions, so a combo carries on when the next press comes a moment late; a canceled move, or one that leaves through an AUTO transition to another move, grants none; HeldInputRestartWindow (seconds).
On the character's UPC Action Graph component (the C++ base creates it): GlobalBlockTags (default UPC.State.ActionsBlocked) drops every press while the character holds one; ActionSpeed scales how fast every move plays (1 is authored speed).
The graph was not compiled after the edit; describe_action_graph says so on its first line.
The guard never ends
No ON_COMPLETED transition from the guard to ROOT on the guard input, or that transition is WAIT_FOR_WINDOW, which waits for a window the looping guard never opens; make it INTERRUPT.
A set_node_settings call raises about an empty entry
A Costs, Gates or Hooks list holds None; make each object with unreal.new_object.
A directional move never plays
Its transition has the Direction condition, but the Input Action's movement is read from the character's movement input; with no stick pushed the direction is NEUTRAL.
A shield's moves play without the shield
The sub graph has no entry_required_tags; gate it on a tag the shield's item holds while drawn (its Equippable EquipmentTags).
A shield's guard starts only from idle
Only ROOT is attached to the shield's sub graph; attach_sub_graph each combo move it should start from.
Tasks: a character made of several meshes; walking differently while a weapon is drawn; hit stop in the customer's own Animation Blueprint; footstep sounds, per surface too; a weapon's own swing effects; finding out why an effect plays nothing; i-frames or hyper armor on frames; showing stamina on screen; button prompts; project-wide settings; finding out why a combo does not chain.
Every window is added with upc.add_notify_state_to_montage(montage, cls, start_seconds, duration_seconds); a one-frame notify with unreal.AnimationLibrary.add_animation_notify_event(montage, "Combat", seconds, cls). Save the montage after setting properties.
Component tags on the character's skeletal meshes say what each is for: UPC.Montage (follows the action's montage), UPC.AnimLayer (takes the item's animation layer), UPC.Attachment.<Slot> (items attach to it; the duplicated player's body has UPC.Attachment.Body). Matching is by prefix. Add the tags to an extra mesh's ComponentTags.
Walking differently with a weapon: the Equippable fragment's AnimLayerClass, a child of the demo's ABP_UPC_ItemAnimLayersBase for the customer's skeleton with its own walk and jog sets. Its Upper Body Default sequence also holds the arms through a dodge while UPC.State.Dashing is on, so a weapon playing the bare hands' dodge montages keeps its grip. Building the layer's AnimGraph is a step for the customer.
Tag-driven bools: an Animation Blueprint whose parent is UUPCAnimInstanceBase has GameplayTagPropertyMap on its class defaults, setting a bool while a tag is held.
Hit stop in the customer's own Animation Blueprint: a UPC Hit Stop node after the DefaultSlot slot and an Inertialization node after it. That is a step for the customer; the plugin's locomotion template already has both (AGENTS.md, "No Animation Blueprint"), and without them the log warns once and hits land without the freeze.
An animation asks the character for an effect with a UPC Anim Effect notify (one frame, UUPCAnimEffectNotify) or a UPC Anim Effect Window (UUPCAnimEffectWindowNotifyState), both carrying Effect (EffectTags, SocketName). Everything on the character that answers is asked, each once: the character's own Blueprint when it implements UPC Anim Effect Handler (IUPCAnimEffectHandler), then each component that does. The UPC Anim Effects component (UUPCAnimEffectBankComponent, AnimEffectComponent on the character base) plays the matching row of its Anim Effect Bank (UUPCAnimEffectBank: Rows with EffectTag, Sounds, Particles, bAttachToSocket; SurfaceSet), the deepest row whose tag the request sits under. The equipment manager runs each drawn item's Swing Trail and passes the request on to the drawn weapon's Blueprint.
Footsteps: duplicate the demo's AnimEffectBank_UPC_Default and set it as the component's Bank. The console command UPC.AnimEffects.PlantFootsteps <content path> save writes UPC.AnimEffect.Footstep.Left and .Right notifies where each foot lands, for every animation in a folder.
Footsteps per surface: give the bank a SurfaceSet (UUPCImpactSurfaceSet: MaterialSuffixes maps a physical material asset to a name such as Stone, SurfaceSuffixes a surface type) and a row for UPC.AnimEffect.Footstep.Stone, which then answers every footstep on that material unless a deeper row such as Footstep.Left exists. A row's tag must exist: upc.add_gameplay_tag(tag, comment). The component looks at the ground under the socket for an effect a bank it uses has a surface row for. upc.validate_asset(bank) warns about a surface no row ends in and notes a row that plays nothing, which silences its tag (how a layered bank mutes one).
A weapon Blueprint's own swing effects (a blade glow, a whoosh of its own, sparks): the Equippable fragment's ItemActorClass, a child of UPC Equippable Item, already implements UPC Anim Effect Handler, answering false. Overriding its functions Handle Anim Effect, Handle Anim Effect Begin and Handle Anim Effect End (under Interfaces in the Blueprint editor), checking the request's Effect Tags, returning true when it acted and false for the rest, is a Blueprint graph step for the customer; the demo BP_UPC_Sword lights its blade on UPC.AnimEffect.Weapon.Glow windows that way. The request's Mesh and Socket Name are the character's, so a blade effect goes on the weapon's own mesh. Windows can overlap: a heavy attack chained into another can open the next window before the last one closes, when the next montage blends in, so the graph counts open windows and switches the effect off when the count returns to zero, as the demo sword does. The character's own handlers do not pass requests on to the weapon; the equipment manager already does.
A ribbon behind the blade needs no graph: the Swing Trail fragment (AgentGuide/ItemsEquipment.md) and a window carrying UPC.AnimEffect.Weapon.Trail over each swing. upc.validate_asset(item) warns when no montage the item's Action Graph plays carries such a window.
Item sounds are item events, not anim effects: an item's draw and put-away sounds are its Stowable fragment's DrawSound and PutAwaySound, and its drop and landing sounds come from its World Item fragment.
In the animation editor, playing or scrubbing a clip plays the effects its notifies ask for from a preview bank: UUPCEditorSettings (Project Settings, Plugins, Universal Player Combat Editor, Animation Preview) PreviewBank, or PreviewBanksBySkeleton for one skeleton. While the clip, an animation or a montage, carries a UPC Anim Effect, the viewport's bottom left corner names the bank; a bank the project lacks plays nothing and shows no name.
Nothing plays:UPC.Debug.AnimEffects 1 in Play writes each character's last requests above it with what answered (the bank, its row and surface, the Swing Trail, a Blueprint by class name), in red when nothing did. Then check, in order: the notify sits on the mesh that plays the montage; the character has a handler (the component has a Bank); a row's tag sits above the requested tag; a surface row's tag exists; the effect plays on a client or listen server, never a dedicated server; the notify's TriggerWeightThreshold and NotifyTriggerChance.
The demo HUD WBP_UPC_HUD shows vitals (WBP_UPC_CoreVitals), the crosshair, cooldowns and the belt. A style's Tags fragment holds UPC.UI.Show.Stamina, UPC.UI.Show.Belt and the like, which the HUD reads to show each part. Changing a widget's layout is a step for the customer.
Button prompts:UUPCInputDisplaySettings (Project Settings, Plugins, Universal Player Combat Input Display) lists GlyphSets (UUPCInputGlyphSet: DeviceFamily, HardwareDeviceIdentifiers naming the pads a sheet serves, Glyphs rows Key, Icon, ShortLabel, LongLabel), top to bottom, the first sheet serving the device and covering the key winning: the PlayStation sheet (DualSense, DualSenseEdge, DualShock4), the generic gamepad sheet, then keyboard.
unreal.get_default_object(unreal.UPCCombatSettings): DefaultHitDetectionAuthority, MaxHitStopSeconds. The demo hit effects are switched off with Load Demo Hit Effects under Project Settings, Plugins, Universal Player Combat Demo. Editor Python cannot save a project setting to the config file before UE 5.8, so a value set this way lasts until the editor closes: ask the customer to set it in Project Settings, Plugins.
Each character's last anim effect requests and what answered them
UPC.Logs Verbose
Raises every UPC log category; UPC.Logs puts them back
Without Play: upc.describe_action_graph(graph) and upc.compile_action_graph(graph) for a graph, upc.validate_asset(asset) for anything else. After Play, log_warnings() returns UPC's warnings, each naming the asset and the fix.
Tasks: a closer over-the-shoulder camera; the camera pulling back while sprinting; a different camera with a weapon drawn; a cinematic camera shot; warning markers over enemy swings; a marker over a stunned enemy.
The duplicated player carries a UPC Camera Rig component (UPCCameraRig, added by the Blueprint; AgentGuide/Python.md, "Components on a Blueprint", shows how to read its template). DefaultSettings (unreal.UPCCameraRigSettings) is used when neither the drawn item nor the combat style names a camera:
Field
Does
ArmLength
Distance behind the character, cm
SocketOffset
Offset from the arm's end, cm; Y moves the camera over a shoulder
Other rig fields: OutputMode, bCreateRigIfMissing, ShoulderSwapSeconds.
Per item: a UUPCItemFragment_CameraRig (Camera Rig) on the item, Settings, while it drives combat. Per style: a UUPCStyleFragment_CameraRig on the combat style, Settings and States (unreal.UPCCameraStateRow: Label, RequiredTags, BlockedTags, Layer), a layer that blends in while the character holds the tags.
Pull back while sprinting: a style state row with RequiredTagsUPC.State.Sprinting and a Layer overriding ArmLength. Read a demo style's row first (CombatStyle_UPC_OverShoulder has a Camera Rig fragment) and copy its shape with export_text.
Check: in Play, capture the viewport before and after; a camera change only a person can judge, so describe it to the customer.
A Camera Shot (UUPCCameraShot, made with the UPC Camera Shot factory or new_data_asset) holds Keys (unreal.UPCCameraShotKey: TimeSeconds, Location, Rotation, FieldOfView, Interpolation), the first at 0 and the rest in increasing time. A paired action plays one through an Authored Camera Shot hook (AgentGuide/PairedActions.md). upc.validate_asset(shot) checks the key order.
The attacker's montage carries a UPC Telegraph Window (UUPCTelegraphMarkerNotifyState) over the wind-up: TelegraphType, bFindAttackAutomatically (finds the trace window it describes), DamageConfig, Tags, OriginSpace, OriginItemTag, Broadcast (its Shape). Add it with upc.add_notify_state_to_montage.
The player sees markers through a UPC Telegraph Viewer (the duplicated player has one) with a StyleSet: a Telegraph Style Set (UUPCTelegraphStyleSet: Styles rows unreal.UPCTelegraphStyleRequiredTags, BlockedTags, Priority, PresenterClass, FallbackSocket, Duration; ViewRules). Duplicate the demo's TelegraphStyle_UPC_Default and change rows; a new marker look is a presenter Blueprint (duplicate a BP_UPC_TelegraphPresenter_*).
A marker over a character rather than a swing: a UPC Telegraph Subject component (UUPCTelegraphSubjectComponent) and its Show Marker call, a Blueprint step.
AGENTS.md, Recipe 2, Step 2, makes damage configs. This file covers what a hit does beyond its damage, and how the player answers a hit: blocks, parries, i-frames, and the health and stamina pools.
Tasks: hits that drain stamina or stance; a knock-back; a hit stop; swings that slow when they strike armor; blocking frontal hits while guarding; a timed guard that parries; a shield that blocks; more health; a flinch when hit, or only when a few hits break poise.
Sends an event, which a hit reaction ability answers
EventTag (UPC.Event.Reaction)
UUPCRefillVitalHitEffect (UPC Refill Vital)
Fills a pool to its maximum, such as broken poise
VitalTag (UPC.Vital.Poise)
UUPCImpactSlowHitEffect (UPC Impact Slow)
Slows the swing when it strikes the level or armor
RelativeRate, Duration, RequireAnyTargetTags
UUPCPhysicalReactionHitEffect (UPC Play Physical Reaction)
Moves the struck body's bones under physics
ReactionName, Force
Every hit effect also has AffectedSides, RequireAnyResultTags, SuppressOnResultTags (run only, or never, on a Blocked or Parried hit), RequireAnyTargetTags, SuppressOnTargetTags, bRequireDamageApplied. Make each with unreal.new_object(cls, outer=damage_config).
The character's MeleeTraceComponent (UUPCCombatTraceComponent): DefaultDamageConfig, TeamFilter (unreal.UPCTeamFilter, Enemies Only by default), PassThroughTags (UPC.State.Intangible), MaxTargetsPerSwing, HitDetectionAuthority.
The guard rules are a Defense Rule Set (UUPCDefenseRuleSet) that an item lends while drawn through its Guard fragment (UUPCItemFragment_Guard, DefenseSet). The responses run in order and the first that answers decides; the parry goes first.
The tags come from the guard move: UPC.State.Blocking while the guard node runs and UPC.State.Parrying over the first frames of its montage (AgentGuide/ActionGraphs.md, "A held guard with a parry window"). Without them the rules never answer and hits land whole.
Put the guard move in the graph of the weapon held with the shield: the shield itself drives no moves.
A guard costs stamina when CostVitalTag names a vital (CostPerDamage per point of damage, FlatCost per hit); empty costs nothing.
ArcHalfAngleDegrees is half the arc in front that blocks; a hit from behind lands whole.
Check: reload the rule set and print each response's class, RequiredTags, ResultTag and DamageMultiplier. In Play, with the guard held, a frontal hit takes a fifth of its damage and the attacker's hit carries UPC.DamageResult.Blocked; inside the parry window nothing lands and it carries UPC.DamageResult.Parried.
ArcHalfAngleDegrees (60), DamageMultiplier (0.2), cost (CostPerDamage 1 when CostVitalTag is set)
UUPCDefenseRule_Scale (UPC Scale Damage)
Scales the hit and lets the next response run
DamageMultiplier
UUPCDefenseRule_Invulnerable (UPC Invulnerable)
Takes nothing
All have RequiredTags, BlockedTags (shown as Required and Blocked Defender Tags), ResultTag, RequiredAttackTags, BlockedAttackTags (an unblockable attack's damage tag), CostVitalTag, FlatCost, CostPerDamage, bSpendRemainderWhenUnaffordable, bContinueAfterClaim. A rule set's OutcomeEffects (hit effects) run once the hit is answered: the demo's sword set sends the attacker UPC.Event.ParryRecoil on a parry. The character's own UPC Defense component (DefenseComponent) holds DefenseSet, Responses and OutcomeEffects that do not come from an item.
VitalsComponent on the character: Vitals rows (unreal.UPCVitalDefinition: VitalTag, MaxValue, RegenRatePerSecond, RegenDelaySeconds, DepletedLooseTag, DepletedResultTag, DepletedGameplayEvent). The duplicated player has health; stamina is in AgentGuide/GatesCostsHooks.md. An emptied UPC.Vital.Stance row with DepletedGameplayEventUPC.Event.GuardBreak is how the demo breaks a guard.
A flinch is an ability that answers UPC.Event.Reaction, which a UPC Send Gameplay Event hit effect on the attacker's damage config (or a defense set's outcome effects) sends. The demo's GA_UPC_HitReact is one: it cuts the running move and holds UPC.State.ActionsBlocked for its HoldSeconds, plays no animation, and serves any character, so add it to the character's ability set.
To flinch only on the hit that breaks poise, as the demo does: a UPC.Vital.Poise row on the character's vitals with DepletedResultTagUPC.DamageResult.PoiseBroken, a poise row in each damage config's AdditionalVitalDamage, UPC.DamageResult.PoiseBroken in the event's RequireAnyResultTags with bRequireDamageApplied off, and a UUPCRefillVitalHitEffect (VitalTagUPC.Vital.Poise, the same result gate) beside it in the same outcome effects. The demo characters share these in DefenseRuleSet_UPC_Character. UPC.State.HyperArmor on the character (a State Tag Window over a swing) halves its poise damage through a UUPCDefenseRule_Scale in that set (RequiredTagsUPC.State.HyperArmor, DamageMultiplier 1, VitalDamageMultipliers row UPC.Vital.Poise at 0.5, bContinueAfterClaim on), so light hits do not stop the swing and a heavy one does.
Read AgentGuide/ActionGraphs.md first: everything here is set on a move through upc.get_node_settings and upc.set_node_settings, or on Epic's MCP on the move's card, as that file's "How a move is edited" says.
Tasks: give the player stamina; make attacks cost stamina; give a move a cooldown or charges; allow a move only at a resource level or while a tag is held; make sprinting drain stamina; slow the player or let them walk out of a move; read a cooldown for the HUD.
d = defaults(bp)vitals = d.get_editor_property("VitalsComponent")rows = list(vitals.get_editor_property("Vitals"))names = [str(unreal.GameplayTagLibrary.get_tag_name(r.get_editor_property("VitalTag"))) for r in rows]if "UPC.Vital.Stamina" in names: # the duplicated demo player has one row = rows[names.index("UPC.Vital.Stamina")]else: row = unreal.UPCVitalDefinition() row.set_editor_property("VitalTag", tag("UPC.Vital.Stamina")) rows.append(row)row.set_editor_property("MaxValue", 100.0)row.set_editor_property("RegenRatePerSecond", 25.0)row.set_editor_property("RegenDelaySeconds", 1.0)vitals.set_editor_property("Vitals", rows)save(bp)def add_cost(graph, node, amount): s = upc.get_node_settings(graph, node) cost = unreal.new_object(unreal.UPCActionCost_Vital) cost.set_editor_property("VitalTag", tag("UPC.Vital.Stamina")) cost.set_editor_property("Amount", float(amount)) s.set_editor_property("Costs", list(s.get_editor_property("Costs")) + [cost]) upc.set_node_settings(graph, node, s)for node in ("Light1", "Light2", "Light3"): add_cost(graph, node, 10)add_cost(graph, "Dodge", 25)s = upc.get_node_settings(graph, "Dodge")gate = unreal.new_object(unreal.UPCActionGate_Cooldown)gate.set_editor_property("CooldownTag", tag("UPC.Cooldown.Ability.Dodge"))gate.set_editor_property("Seconds", 1.0)gate.set_editor_property("Charges", 1)s.set_editor_property("Gates", list(s.get_editor_property("Gates")) + [gate])upc.set_node_settings(graph, "Dodge", s)print(upc.compile_action_graph(graph))save(graph)
Sprint drain is set on the character's UPC Sprint component, which the Blueprint adds (AgentGuide/Python.md, "Components on a Blueprint", shows how to read its template): StaminaVital = UPC.Vital.Stamina, StaminaDrainPerSecond = 15, MinStaminaToStart = 10. Compile and save the Blueprint.
A cost is checked when the press arrives and spent when the move starts. A press the player cannot pay for does not start the move; it takes the move's Action Failed transition when one is wired.
Do the same in every graph that has these moves: a drawn weapon's graph replaces the unarmed one.
The cooldown tag names the timer. Gates sharing a tag share the timer, so they must agree on Seconds and Charges (the compile warns when they differ). For a second cooldown, add its own tag first: upc.add_gameplay_tag("UPC.Cooldown.Ability.Heavy", "Heavy attack cooldown").
Check:upc.describe_action_graph(graph) lists costs Vital (VitalTag=..., Amount=10) on each light attack and gates Cooldown (... Seconds=1 ...) on the dodge. In Play, UPC.Debug.Vitals 1 writes the stamina above the character; it drops by the cost after each press and climbs back after a second.
UUPCActionHook_CancelOnMovement (Cancel On Movement)
Hooks
ReadMode (INPUT_INTENT, SPEED), MovementThreshold (0 to 1)
UUPCActionHook_DrainVital (Drain Vital)
Hooks
VitalTag, DrainRatePerSecond, EndBehavior (NONE, FINISH, CANCEL the move when the vital empties)
A gate decides whether the move may start; a cost is paid when it starts; a hook runs while it runs. A gate never spends anything.
Falling through to another move. When the press should do something else while a move is shut, put a condition on the transition rather than relying on the gate: UUPCTransitionCondition_CooldownReady (CooldownTag) or UUPCTransitionCondition_VitalThreshold (VitalTag, Comparison, Threshold, bUseFraction) on the transition into the gated move, and a second transition on the same input without it. See AgentGuide/ActionGraphs.md, "Transition settings".
HUD.unreal.UPCActionGate_Cooldown.get_cooldown_state(owner, cooldown_tag) reports charges ready, charges it can bank and when the next comes back. The demo HUD's WBP_UPC_AbilityCooldown reads it.
The character has no row for the cost's VitalTag in VitalsComponent's Vitals, or the cost sits on a move in a graph the player does not use while that item is drawn.
A press never starts the move
A cost the player cannot pay, a gate that is shut, or a cooldown still running. UPC.Debug.Actions 1 shows the graph state; log_warnings() names a cost whose vital is missing.
The compile warns about a cooldown
Two gates share a CooldownTag with different Seconds or Charges. Make them agree or give one its own tag.
Sprint never drains
StaminaVital is empty on the UPC Sprint component, or the character has no row for it.
Tasks: put a move on a new key; a combined press of two buttons; an input that works with any weapon; an ability on a key that can cut a swing (a potion, a heal); sprint speed and stamina; pausing combat input.
A key maps to an Input Action in a mapping context; a mapping row turns the Input Action into an input tag; the action graph's transitions listen for the tag. All three must agree.
Item mappings (while an item is drawn): the Equippable fragment's InputMappingContext, InputMappingPriority and InputMappings rows (unreal.UPCInputMapping: InputAction, InputTag, RequiredStateTags, DisplayName), as AGENTS.md, Recipe 2, Steps 5 and 6, builds.
Character mappings (always): the character's InputMappingComponent (UUPCInputMappingComponent): InputMappings, InputChords, ChordWindowSeconds, bAutoBind.
Keys:IMC_UPC_Default on the controller maps movement, look, slots 1 to 4, dash, lock-on, interact, drop and sprint. Item contexts map attack and guard keys.
Check: reload the item and print its mappings; upc.describe_action_graph(graph) lists UPC.Input.Kick leaving ROOT. In Play, graph_comp.process_action_input(tag("UPC.Input.Kick"), unreal.UPCTransitionTrigger.ON_STARTED) enters the move (AGENTS.md, Recipe 2, Step 8).
unreal.UPCInputChord rows (MemberTags, two or more input tags; OutputTag, the tag sent when all are pressed inside ChordWindowSeconds; Priority) in an item's InputChords or the character's. A transition on the output tag plays the move. ChordWindowSeconds 0 turns chords off.
The character's starting ability set (StartingAbilitySet, a copy of AbilitySet_UPC_Default in AGENTS.md, Recipe 1) grants abilities with their keys: GrantedGameplayAbilities rows (unreal.UPCAbilitySet_GameplayAbility: Ability, AbilityLevel, InputTag, InputAction, bInterruptsActionGraph (cuts the running move, for a dodge or a potion), bForwardInputWhileActive (a second press goes into the running ability, for a hold)). The row's fields refuse set_editor_property, so write the row as text:
An item grants its own sets while drawn through AbilitySetsToGrant. A press goes to the action graph first, then to a granted ability whose row carries the same input tag.
The sprint is the UPC Sprint Ability (UUPCSprintAbility, LatchTapSeconds) on UPC.Input.Sprint (in the copied ability set) and the UPC Sprint component (Sprint on the duplicated player, added by the Blueprint): WalkSpeed, SprintSpeed, SpeedBlendSeconds, MinInputStrength, MaxSprintAngle, BlockedTags, StopGraceSeconds, StaminaVital, StaminaDrainPerSecond, MinStaminaToStart. Stamina for it: AgentGuide/GatesCostsHooks.md.
Slower while a tag is held: a UPC Speed Modifier component (UPCSpeedModifier on the duplicated player): SpeedScaleByTag, a map of tag to multiplier.
GlobalBlockTags on the character's ActionGraphComponent (default UPC.State.ActionsBlocked) drops every press while the character holds one; holding the tag (a dialogue, a cutscene) is a Blueprint step or a gameplay effect.
Tasks: start with an item; an item, a container and an inventory config made from scratch; a keychain that takes every key by itself, cannot be dropped and weighs nothing; a weight limit or a cap per item kind; a belt that takes any item, or a belt laid out by item kind; saving the inventory in the game's save; a survival grid, a party RPG inventory, an open world list, or a character carrying only what its hands hold; an inventory screen, the demo's as it is, a copy changed for the game, or one of the project's own; a potion drunk from a screen or the belt; a stash; a rule, a combine or an item fragment of the project's own in Blueprint; two items combined into a third by a recipe; rounds loaded into a magazine by dropping them on it; a bag bar whose pouches hold items; an item on the floor to pick up; dropping the held weapon; a weapon rack; a loot chest every nearby player sees; only interacting with what the player looks at; a lever or a door that plays an animation.
Starting items:StartingItems on the character's InventoryComponent (AGENTS.md, Recipe 4, Step 5).
The belt is the UPC Quick Bar component on the player controller (BP_UPC_PlayerController; duplicate it to change it, and set the copy as the game mode's PlayerControllerClass). Fields: Slots (unreal.UPCQuickBarSlot rows: Filter, an item kind the item's SlotTags must hold, its children included), bAutoPopulateFromInventory (off by default; on, a new item also takes the first free slot that admits it, as on the demo controller; what a combine makes is no new item), bAutoDrawOnInit (with Auto Populate on, an item landing on a slot also comes to hand when its places are free; it does nothing while Auto Populate is off), bRestoreWhenPlaceFrees, bWeaponKeepsItsPlace, DefaultMaxCopiesPerDefinition (1 on the demo controller, so its belt holds one of each item; the demo sword's Belt Limit fragment of 2 lets it hold two swords), DriverReplacementOnDrop, BlockedTags (tags that switch the belt off while the character carries one; UPC.State.BeltBlocked by default). The controller's IMC_UPC_Default maps keys 1 to 4 and the D-pad's up, right, down and left to IA_UPC_SelectSlot_1 to _4.
Ask the customer which belt layout the game wants. Any item on any slot, as a survival game's hotbar: leave every slot's Filter empty, and a worn item drawn from the belt is put on. Slots by item kind, as the demo's souls-like diamond: slot 1 (top) UPC.Slot.Weapon, slot 2 (right) UPC.Slot.Consumable.Potion, slot 3 (bottom) UPC.Slot.OffHand, slot 4 (left) UPC.Slot.Consumable, with a Belt Limit fragment (MaxBeltCount 1) on the potion so its stacks take the right slot alone and the left slot holds other consumables; worn gear and a second weapon then stay in the inventory. Ask too whether any combat style has no belt: put UPC.State.BeltBlocked in that style's Gameplay Tags fragment (unreal.UPCStyleFragment_Tags, field Tags), as the demo Freeflow style does, and while it is active a belt press does nothing, nothing new goes on the belt and its slots keep their items. The demo HUD shows its belt only while the character carries UPC.UI.Show.Belt, which a style with a belt adds the same way.
Item kinds are one UPC.Slot hierarchy in each item's SlotTags, read by the belt's filters, the inventory's rules and its sort order: UPC.Slot.Weapon (.Sword, .TwoHanded), UPC.Slot.OffHand (.Shield), UPC.Slot.Consumable (.Potion), UPC.Slot.Apparel, UPC.Slot.Ingredient, UPC.Slot.Ammo (.Arrow), UPC.Slot.KeyItem, UPC.Slot.Upgrade. Add the project's own kinds under these, such as a spear kind under UPC.Slot.Weapon, so a filter on the parent takes them. An item with no SlotTags fits unfiltered slots only. A Belt Limit fragment caps copies on the belt.
An inventory with no config is one list with no limits. For slots, a grid, a bank, caps per item kind or a weight limit, set the inventory component's Config to a UPC Inventory Config (unreal.UPCInventoryConfig): defaults(bp).get_editor_property("InventoryComponent").set_editor_property("Config", config) on the character Blueprint, then save(bp). Three ready styles sit in /UniversalPlayerCombat/Demo/Items/Inventory, each a recipe below; duplicate one into the project's folder and change the copy. The demo player starts on InventoryConfig_UPC_SurvivalGrid; the style pads in the wardrobe hall switch it.
Containers rows (unreal.UPCContainerSpec): a Container tag and a UPC Container Definition (unreal.UPCContainerDefinition: DisplayName, Icon, Layoutunreal.UPCContainerLayout.LIST, SLOTS or GRID, SlotCount, GridSize (unreal.IntPoint), bAllowRotation, CapacityTag, Rules, bCarried, bTakesNewItems, AccessTag, bOpensBags). Adds try the rows in order, each followed by the bags open in it, and take the first container whose rules let the item in and that has room; a bTakesNewItems container off is filled by moves only. A List has no size and takes anything its rules allow, so give a container meant for one kind of item an Item Filter rule.
CarryRules, judged over every carried container together. The rules: Weight Limit (Limit, CapacityTag, bRefuseOverLimit, Thresholds of Fraction and Tag), Category Limit (Limits rows of Category, MaxUnits, CapacityTag, matched against the item's SlotTags) Item Filter (AllowedTags, BlockedTags) and Belt Only (no settings: lets in only what the belt holds, for a style with no inventory; the belt needs bAutoPopulateFromInventory on. It turns down any bag, an item with a Container fragment, since what a bag holds would be carried off the belt. In the Carry Rules it limits everything carried; on a container it limits that container alone, as the demo Simple config's Belt container beside a Worn container whose Item Filter takes apparel).
BeltSpace and EquippedSpace (unreal.UPCHeldItemSpace: KEEPS_ITS_PLACE, FREES_ITS_PLACE): whether an item on the belt, or drawn and off the belt, keeps its place in its container. Under FREES_ITS_PLACE, RequestMove of the whole held stack to a place in a carried container has the belt and the hand let go of it and puts it there, and CanPlace answers for that move. A screen that drops an item out of the hand onto a place sends RequestMove first and the belt's clear or RequestUnequipItem after it: a timed put-away running when the move arrives refuses the move. SortOrder: the Slot Tags RequestSort lays items out by. bEquipAtOnce, bPutAwayAtOnce and bCombineAtOnce (Timing): on, an equip, a put-away or a combine asked for from the inventory lands at once with no animation and no wait, which a screen that pauses the game needs; off, the request plays the item's reach, or waits DrawTiming or StowTiming (unreal.UPCItemActionTiming: Seconds, StartSound, CommitSound, bCancelOnMove) for an item with none, or the recipe's time for a combine. The server reads these three, so no request takes a flag for them. bSwapWhenFull is explained with pickups, in the paragraph after the code below. bShowCarriedItemsOnBody (off by default): on, every stowable item carried outside a bag hangs on the body while out of hand; off, only the belt's items do and an item only in the inventory shows nowhere on the character. BeltSlots (one tag per slot, an empty tag taking any item): the belt this config gives, standing in for the belt's own Slots while the config is used, as the party RPG's four slots taking any item; left empty, the belt keeps its own.
An item with a Capacity Bonus fragment raises a capacity tag while owned; a container's or a limit rule's CapacityTag names it.
Another config at runtime, such as a game that lets the player pick an inventory style: call the inventory's SetConfig on the server, never set Config itself outside the defaults. A container both configs list, such as a stash, keeps its items; every other item outside a bag is placed again in the new layout as an add would, each staying on the belt and in hand; one that fits nowhere stays in a carried container, past its end when that is full, with a warning. To start the new config empty, call RemoveAllItems first, which empties the stash too; to keep the stash, remove only the items IsItemCarriedAndInReach answers true for, as the demo wardrobe's BP_UPC_InventoryStylePad does before it adds the style's starter kit.
Row fields are edited on the asset only, so Python fills a row with import_text:
spec = unreal.UPCContainerSpec()spec.import_text('(Container=(TagName="UPC.Inventory.Container.Backpack"),Definition="/Game/Inventory/Container_Backpack.Container_Backpack")')config = load("/Game/Inventory/InventoryConfig_Knight") # the project's copy of a demo configconfig.set_editor_property("Containers", list(config.get_editor_property("Containers")) + [spec])save(config)
Adds and pickups take only what fits and a pickup leaves the rest on the ground. The world item's GetUnitsThatFit (with the picker) answers what a pickup would take, the inventory's what an add would. A pickup that takes nothing stays on the prompt with its verb saying why (No Room, Too Heavy) and a press does nothing; with the config's bSwapWhenFull on it reads Swap instead, and a press puts down the item in the way and takes the new one (GetSwapItem names what goes): an item in hand or on a belt slot that takes the new item, one of the same kind first, so a second sword replaces the sword put away rather than the shield. The demo's inventory screens draw all of this (Inventory screens, below); what follows is what any screen reads and sends. A UI reads GetItemsInContainer, GetItemsInSortOrder (a list kept in the config's SortOrder without moving anything), GetItemPlacement, GetContainerSize and CanPlace (its units out says how many a merge takes), a grid UI GetItemCells and GetPositionOfCell, a weight bar GetWeight with an empty scope, and redraws on OnLayoutChanged. The UI changes nothing itself: it asks the server through the inventory's RequestMove (move, split, merge, swap, combine, or into the bag dropped on), RequestSort and RequestUseItem, the belt's RequestAssignSlot, and the equipment manager's RequestEquipItem, RequestUnequipItem and RequestCancelSequence, and draws the change when it arrives. Clearing or refilling a belt slot whose item is in hand takes that item out of the hand at once, with no animation, once CanAssignSlot allows it. RequestEquipItem and CanEquipItem take the item and a Place, a paper doll's place (leave it empty to follow the item's Hold Places order), and RequestUnequipItem takes the item only. The config's bEquipAtOnce and bPutAwayAtOnce decide whether the requests play the draw and put-away animation or land at once. Each request has a check the UI asks first, the one the server makes: CanPlace for a move, the belt's CanAssignSlot for a drag onto or off the belt, the equipment manager's CanEquipItem and CanUnequipItem for Equip and Unequip buttons, and the inventory's IsItemCarriedAndInReach for a Use button; gray a button out while its check answers false. IsContainerInReach tells a bank or stash screen whether the player can reach it now. A timed draw announces OnEquipStepStarted and OnEquipSequenceEnded on the equipment manager, and GetPlayingStep answers how far it has got.
Check: in Play with the survival grid config on the player, GetContainerSize for UPC.Inventory.Container.Case reads 6 by 4, a sword added lands at the case's first free place (GetItemPlacement), and a hip pouch added lands in Key Items and opens six cells (GetContainerOpenedBy, then GetContainerSize of that container reads 6 by 1).
A small case where items take room by size and turn to fit, key items in a list beside it, a hip pouch that adds six cells under the case, and a stash for the rest. Duplicate InventoryConfig_UPC_SurvivalGrid:
Containers, in this order: Case (UPC.Inventory.Container.Case: Grid, GridSize 6 by 4, bAllowRotation on, an Item Filter rule with BlockedTagsUPC.Slot.KeyItem and UPC.Slot.Upgrade, CapacityTagUPC.Inventory.Capacity.CaseRows), Key Items (UPC.Inventory.Container.KeyItems: List, an Item Filter rule with AllowedTagsUPC.Slot.KeyItem and UPC.Slot.Upgrade, bOpensBags on, so keys and pouches take no room in the case and a pouch opens there) and Stash (UPC.Inventory.Container.Stash: Slots, SlotCount 30, bCarried off, bTakesNewItems off, AccessTagUPC.State.AtStash; the same container asset the three demo style configs list, so a stash survives a switch of config).
Each item larger than one cell gets a Grid Footprint fragment (unreal.UPCItemFragment_GridFootprint, Size columns by rows: the demo sword 1 by 3, the shield 2 by 2). The hip pouch (/UniversalPlayerCombat/Demo/Items/Bags/Item_UPC_HipPouch) is a bag whose Container fragment opens Container_UPC_HipPouch, a Grid 6 by 1 with the case's Item Filter; the case is listed first, so adds fill it before the pouch's cells, and what lies in those cells drops, travels and comes back with the pouch. The demo survival grid screen draws each open pouch's cells under the case (WBP_UPC_ExtraCells). For space that stays with the character, such as a case upgrade, give an item a Capacity Bonus row for UPC.Inventory.Capacity.CaseRows instead: it adds a row, and the player cannot drop it while that row holds an item.
CarryRules: the demo's Blueprint rule BP_UPC_InventoryRule_UniqueKeyItems (one of each key item). CombineSets: the demo's CombineSet_UPC_Demo (Combines, below). bPutAwayAtOnce on, so a screen's put-away leaves the hand at once while Equip keeps its draw animation. bShowCarriedItemsOnBody on, so every weapon in the case hangs on the body.
The stash is opened at a demo storage box (Storage boxes, below). A whole stack on the belt never goes into storage the character does not carry until the belt lets go of it; the demo screens' Store clears the belt slot first.
Check: in Play, a sword's CanPlace onto the case's cell (0, 3) answers REFUSED with UPC.Inventory.Refused.OutOfBounds upright and PLACE with bRotated on.
A backpack that never refuses for space, weight tiers that slow, pouches that open, worn gear for a paper doll and a stash. Duplicate InventoryConfig_UPC_PartyRPG:
Containers: Backpack (UPC.Inventory.Container.Backpack: List, bOpensBags on, so a pouch opens once picked up) and Stash (UPC.Inventory.Container.Stash: Slots, SlotCount 30, bCarried off, bTakesNewItems off, AccessTagUPC.State.AtStash).
CarryRules: a Weight Limit with Limit 60, bRefuseOverLimit off and ThresholdsUPC.State.Encumbered at 0.7 and UPC.State.HeavilyEncumbered at 1. Every item gets a Weight fragment. The slow is the character's UPC Speed Modifier (SpeedScaleByTag: the demo character's 0.85, and 0 for UPC.State.HeavilyEncumbered and UPC.State.Overburdened, which hold it still); the demo character's UPC Sprint names UPC.State.Encumbered and UPC.State.Overburdened in its BlockedTags, so an encumbered character cannot sprint.
CombineSets: CombineSet_UPC_Demo. bEquipAtOnce, bPutAwayAtOnce and bCombineAtOnce on, so the screen equips, puts away and combines at once. BeltSpace and EquippedSpaceFREES_ITS_PLACE, so an item on the belt or in hand, worn gear included, leaves the backpack while held and its weight still counts. BeltSlots: four empty tags, a row taking any item.
The Stash is the same container as the survival grid's, opened at the same storage box (Storage boxes, below). The party RPG screen's paper doll is WBP_UPC_PaperDoll (Inventory screens, below); a doll of the project's own reads the equipment manager's GetInstanceInPlace for each place it shows, skips the unarmed fallback (IsDefaultItem), and redraws on OnDrawnStateChanged. Loot chests are BP_UPC_LootChest (Loot chests, below).
Check: in Play, seven of the demo shields (weight 6 each) put UPC.State.Encumbered on the character, eleven add UPC.State.HeavilyEncumbered, and the character's speed modifier's GetCurrentSpeedScale reads 0.85, then 0.
One list with no size, weight that slows past the limit without refusing, categories by item kind and no belt, as in Skyrim. Duplicate InventoryConfig_UPC_OpenWorldList:
Containers: Satchel (UPC.Inventory.Container.Satchel: List) and the demo's Stash (Container_UPC_Stash, as the survival grid lists it), which the list screen shows in a panel of its own on the right of its frame while the character stands at a storage box.
CarryRules: a Weight Limit with Limit 60, bRefuseOverLimit off and a threshold UPC.State.Overburdened at 1, and the demo's Blueprint rule BP_UPC_InventoryRule_Valuables (UPC.State.CarryingValuables while an item worth 100 gold or more is carried).
SortOrder: UPC.Slot.Weapon, UPC.Slot.OffHand, UPC.Slot.Apparel, UPC.Slot.Consumable, UPC.Slot.Ingredient; items of no listed kind sort last. The demo's list screen shows the satchel in that order (GetItemsInSortOrder), with a category column filtering by the same kinds.
bHasBelt off: the player has no belt while this config is used. The belt has no slots, so the 1 to 4 keys do nothing and nothing goes on it, and the HUD and the list screen show no belt, no On Belt category and no Add to belt prompt. Leave it on for a list with the belt as favorites: the screen then sends RequestAssignSlot, and its On Belt category lists the belt's items.
CombineSets: CombineSet_UPC_Demo. bEquipAtOnce, bPutAwayAtOnce and bCombineAtOnce on, since the list screen pauses a game with no other players and a paused world runs no timers.
Check: in Play, RequestSort of UPC.Inventory.Container.Satchel lays a key, a herb, a potion, a helmet and a sword out as sword, helmet, potion, herb, key, and the player's belt GetNumSlots reads 0.
The character carries only what its two hands hold: a pickup comes straight to hand, a second one-handed item to the other hand, and a third swaps for the item in hand. InventoryConfig_UPC_SingleItem does it for one hand; for two, make the config from scratch (Containers, above):
Containers: one container, Slots with SlotCount 2, bCarried on, no rules. BeltSpace and EquippedSpace stay KEEPS_ITS_PLACE (the defaults), so two held items fill it.
bSwapWhenFull on, so a third pickup reads Swap and puts down the item in hand that makes room, the weapon driving combat first.
BeltSlots: two empty tags, a belt of two slots taking any item. On the controller's belt, bAutoPopulateFromInventory and bAutoDrawOnInit on: a picked-up item lands on the first free slot and comes to hand while its Hold Places are free. For two of one item, the belt's DefaultMaxCopiesPerDefinition must allow two (0 is no limit; the demo controller's is 1), or the item needs a Belt Limit fragment of 2.
No HUD shows the belt: leave UPC.UI.Show.Belt out of the style's Gameplay Tags fragment. Leave the belt's keys out of the game's mapping context so no key puts an item away.
An item the hands cannot hold beside what they hold, a two-handed weapon beside a sword, stays in its slot, unheld, until something draws it.
Check: in Play, picking up two swords puts one in each hand (GetItemsInHand holds two), and a third pickup's prompt reads Swap; taking it leaves the first sword on the ground and the new one in its hand.
Every key the player picks up goes onto a keychain by itself and never into the pack, a key cannot be dropped, and a key weighs nothing, with a pack that holds 30 weight beside it. This is also the pattern for any items that live apart from the rest: ammunition, quest items, crafting materials. Three facts carry it:
A container listed first is tried first by each add, and an Item Filter rule on it lets in only the UPC.Slot.KeyItem kind, so keys land there and nothing else does. The pack, listed second, takes the rest.
An item with no World Item fragment cannot be dropped or lie on the ground: the drop ability refuses it and the demo screens show no Drop for it.
An item with no Weight fragment weighs nothing, so the weight limit does not count a key, even in a pack already full.
F = "/Game/Inventory" # the customer's folderupc.add_gameplay_tag("UPC.Inventory.Container.Keychain", "The player's keychain")def make_item(name, kind, stack=1, weight=None): item = new_data_asset(unreal.UPCItemDefinition, F, f"Item_{name}") item.set_editor_property("DisplayName", name) item.set_editor_property("SlotTags", tags(kind)) item.set_editor_property("MaxStackSize", stack) if weight is not None: w = unreal.new_object(unreal.UPCItemFragment_Weight, outer=item) w.set_editor_property("Weight", weight) item.set_editor_property("Fragments", [w]) save(item) return itemrusty = make_item("RustyKey", "UPC.Slot.KeyItem") # no Weight, no World Item fragmentgold = make_item("GoldKey", "UPC.Slot.KeyItem")flint = make_item("Flint", "UPC.Slot.Ingredient", stack=10, weight=1.0)keychain = new_data_asset(unreal.UPCContainerDefinition, F, "Container_Keychain") # a List by defaultonly_keys = unreal.new_object(unreal.UPCInventoryRule_ItemFilter, outer=keychain)only_keys.set_editor_property("AllowedTags", tags("UPC.Slot.KeyItem"))keychain.set_editor_property("Rules", [only_keys])save(keychain)pack = new_data_asset(unreal.UPCContainerDefinition, F, "Container_Pack")save(pack)config = new_data_asset(unreal.UPCInventoryConfig, F, "InventoryConfig_Hero")rows = []for container_tag, definition in (("UPC.Inventory.Container.Keychain", keychain), ("UPC.Inventory.Container.Backpack", pack)): row = unreal.UPCContainerSpec() row.import_text(f'(Container=(TagName="{container_tag}"),Definition="{definition.get_path_name()}")') rows.append(row)config.set_editor_property("Containers", rows) # the keychain firstlimit = unreal.new_object(unreal.UPCInventoryRule_WeightLimit, outer=config)limit.set_editor_property("Limit", 30.0)config.set_editor_property("CarryRules", [limit])save(config)bp = load("/Game/Player/BP_Hero") # the customer's playerdefaults(bp).get_editor_property("InventoryComponent").set_editor_property("Config", config)save(bp)
UPC.Slot.KeyItem exists already; the keychain's container tag is new, and UPC.Inventory.Container.Backpack is a ready one. Every further key is one more make_item(..., "UPC.Slot.KeyItem") with no weight. A key with no World Item fragment has no look on the ground either, so it reaches the player from a chest (Loot chests, below), a reward or a starting item: inv.add_item_definition(key, 1) on the server. A key that has to lie in the level carries the fragment and can be dropped again, unless the project's drop ability refuses it in its Can Drop (a Blueprint step).
Check: in Play (AGENTS.md, Recipe 2, Step 8, starts it and reaches the pawn), inv is the pawn's unreal.UPCInventoryComponent. With 10 Flint in the pack, inv.add_item_definition(rusty, 1) returns a stack, unreal.GameplayTagLibrary.get_tag_name(inv.get_item_placement(stack).container.tag) reads UPC.Inventory.Container.Keychain, inv.get_weight(unreal.UPCContainerId()) is the same before and after, and rusty.find_fragment_by_class(unreal.UPCItemFragment_WorldItem) is None. The flint's placement reads UPC.Inventory.Container.Backpack.
Variant: the keychain is an item the character carries. A bag opens a container of its own (Bags, below), so the keychain can be an item the player is given, and the pack, which opens bags, turns keys away so adds reach the open keychain. The keychain is a bag with no World Item fragment, so it cannot be dropped either:
holds = keychain # Container_Keychain above: the Item Filter on keyschain = make_item("Keychain", "UPC.Slot.Upgrade")bag = unreal.new_object(unreal.UPCItemFragment_Container, outer=chain)bag.set_editor_property("Definition", holds) # ContainerTag keeps its default, UPC.Inventory.Container.Bagchain.set_editor_property("Fragments", [bag])save(chain)pack = new_data_asset(unreal.UPCContainerDefinition, F, "Container_PackWithBags")pack.set_editor_property("bOpensBags", True)no_keys = unreal.new_object(unreal.UPCInventoryRule_ItemFilter, outer=pack)no_keys.set_editor_property("BlockedTags", tags("UPC.Slot.KeyItem"))pack.set_editor_property("Rules", [no_keys])save(pack)# The config lists the pack alone; the character starts with the keychain.defaults(bp).get_editor_property("InventoryComponent").set_editor_property("StartingItems", [chain])
The keychain recipe shows the calls; the rest of the pieces take the same ones.
A container shaped as slots or a grid.layout = unreal.UPCContainerLayout.SLOTS with SlotCount, or GRID with GridSize as unreal.IntPoint(6, 4) and bAllowRotation; either grows with a CapacityTag. An item bigger than one grid cell carries a Grid Footprint fragment (Size, unreal.IntPoint(1, 3)). A container off the character, such as a bank, sets bCarried off (its items leave the weight limit, the belt and Get Item Count), and bTakesNewItems off keeps adds out of it; AccessTag locks it to a character holding that tag.
A cap per item kind. A Category Limit rule, on a container's Rules or the config's CarryRules, with Limits rows written as text: row = unreal.UPCCategoryLimit(); row.import_text('(Category=(TagName="UPC.Slot.Ammo.Arrow"),MaxUnits=20)'), then rule.set_editor_property("Limits", [row]).
A weight limit that slows instead of refusing.bRefuseOverLimit off and Thresholds rows, each '(Fraction=0.7,Tag=(TagName="UPC.State.Encumbered"))' through unreal.UPCWeightThreshold().import_text(...); the tag then sits on the character, and the character's UPC Speed Modifier or a transition's BlockedTags gives it an effect.
An item that grows a capacity. A Capacity Bonus fragment whose Bonuses row names the capacity tag and an Amount, written as text (unreal.UPCCapacityBonus().import_text('(CapacityTag=(TagName="UPC.Inventory.Capacity.CaseRows"),Amount=1)')); a container's, a Weight Limit's or a Category Limit's CapacityTag names the same tag. The player cannot drop, store or put back such an item while items sit in the slots or rows it gives (CanItemLeave answers UPC.Inventory.Refused.CapacityInUse); for extra space whose items go with it, make the item a bag.
The config's other settings.SortOrder (a list of tags: [tag("UPC.Slot.Weapon"), tag("UPC.Slot.Consumable")]), BeltSpace and EquippedSpace (unreal.UPCHeldItemSpace.FREES_ITS_PLACE), bSwapWhenFull, and the three at-once switches, all plain set_editor_property writes.
Which screen it opens. A config of the project's own needs a row in the screens component's InventoryScreenClasses, or it opens the open world list (Inventory screens, below).
Each extension is a Blueprint class whose parent is a UPC class, created with unreal.BlueprintFactory (parent_class) and listed on the config; its graph is a Blueprint step. The demo's are in /UniversalPlayerCombat/Demo/Items/Inventory/Blueprints, ready to duplicate:
A rule (parent unreal.UPCInventoryRule): override GetUnitsAccepted to turn items away (BP_UPC_InventoryRule_UniqueKeyItems reads the inventory's GetItemCount) or GetStatusTags to tag the character (BP_UPC_InventoryRule_Valuables). The context's Departing lists what leaves the rule's scope in the same change, a row per stack (a swap's other item, what a combine takes from both of its stacks), and ArrivingWith what comes in beside the item; count the first as gone and the second as already in, as the unique key rule counts Departing. List it on a container's Rules or the config's CarryRules. A rule keeps no state of its own.
A combine (parent unreal.UPCInventoryCombine): for what a Combine Set's recipe row cannot say; the demo's Load Holder and Coat Weapon (Combines, below).
An item fragment (parent unreal.UPCItemFragment): data an item carries, such as BP_UPC_ItemFragment_GoldValue (an integer Value). Add an instance to an item's Fragments like any fragment; a graph reads it with the item instance's FindFragmentByClass and a cast. The C++ sort reads Slot Tags only, so a Blueprint fragment orders nothing by itself.
The demo ships one inventory screen per ready style, Widget Blueprints in /UniversalPlayerCombat/Demo/UI/Widgets/Inventory:
Screen
Opens for
Shows
WBP_UPC_InventoryScreen_OpenWorldList
InventoryConfig_UPC_OpenWorldList, any config with no row of its own, and an inventory with no config
A category column, the satchel as one list in SortOrder, an item card with its prompts, a weight bar; a chest's items, or the Stash at a storage box, in a mirrored panel of their own on the right
WBP_UPC_InventoryScreen_SurvivalGrid
InventoryConfig_UPC_SurvivalGrid
The case as a grid where items turn to fit, Key Items beside it
WBP_UPC_InventoryScreen_PartyRPG
InventoryConfig_UPC_PartyRPG
The backpack, a panel per open pouch, the paper doll, a weight bar marking both slowdowns
BPC_UPC_InventoryScreens, a component on BP_UPC_PlayerController, opens the screen for the character's inventory config on I, or a tap of View on a pad (holding View respawns), and I, View, B or Escape closes it. While it is open the character stands: move, look and combat input stop, and a hit still lands. Every screen also shows the belt while the character holds UPC.UI.Show.Belt and the storage panel at a storage box; the grid and party screens show the chest panel at a loot chest. On the grid and party screens a mouse click selects an item and shows its details and actions (Consume and Combine sharing the first row, Equip, Unequip, Split, Drop, Add to belt, each grayed when it does not apply; Take alone for an item in a chest), a drag moves it, and right click runs its main action (Equip, Unequip or Consume; Take or Store at a chest or a storage box); a pad picks up and puts down with A, opens the actions with X, turns a carried item with Y and moves between panels with LB and RB. The list screen opens on All with the first item highlighted and its card showing: the mouse over an item, Up and Down, or the D-pad highlight it and its card follows; a click, a right click, E or pad A runs its main action (Equip, Unequip or Consume; Take or Store at a chest or a storage box), C or the right trigger combines, R or pad X drops it (takes everything at a chest), F or pad Y adds it to the belt, 1 to 4 put it on that belt slot, LB and RB step through the categories, and Left or B goes back to the categories. On every screen Combine holds the item until the next item picked, in any category or panel, is combined with it; Cancel, Back and closing the screen let it go.
Ask the customer whether the game uses a demo screen as it is, a copy changed for the game, or a screen of its own.
As it is. Keep BP_UPC_PlayerController, or the project's duplicate of it, as the game mode's PlayerControllerClass, or add BPC_UPC_InventoryScreens to the project's own player controller (Components panel, Add; a Blueprint step), which brings the screens, the I and Back keys and the chest's Open option with it; a player ability set duplicated from AbilitySet_UPC_Default already grants the use and drop abilities (Using and dropping from a screen, below). The component's InventoryScreenClasses maps each inventory config to its screen, and FallbackInventoryScreen serves the rest, so a config duplicated from a demo one needs a row of its own, or it opens the open world list.
A copy. Duplicate the screen into the project's folder, point the screens component's row at the copy, and change the copy in the Widget Blueprint editor (a Blueprint step). Data the customer changes in the editor's Class Defaults or Details panel: the list panel's CategoryRows (WBP_UPC_ListPanel, which both sides of the list screen share; each a Label, the item kinds it shows in Kinds, OtherKinds for a row taking what no other row names, or BeltOnly for a row of the belt's items), the paper doll's Places (WBP_UPC_PaperDoll: a place tag, a label, a row and a column each, following BP_UPC_Character's places, held once in the doll's class defaults, so a body with other BodyPlaces needs them edited there) and the message line's RefusalWords (WBP_UPC_InventoryMessage: the words shown for each UPC.Inventory.Refused and UPC.Equipment.Cancelled reason; a reason with no row shows the last part of its tag). A copy keeps using the blocks in Common beside the screens, so keep the demo in the project while it does.
A screen of the project's own. Start from a copy all the same: the blocks in Common hold every action once. WBP_UPC_ItemTile draws one item or one empty place; WBP_UPC_ContainerList, WBP_UPC_ContainerGrid and WBP_UPC_ContainerSlots draw one container each; BP_UPC_ItemMove is the carried item, for a mouse drag and a click or pad pick-up alike; BFL_UPC_InventoryScreens holds each item action as a check and a send, and the carry flow every screen calls (Pick Up, Put Down, Put Back, Send To Belt); BPI_UPC_InventoryScreen is what the screens component and the tiles call on a screen, Show Inventory among them; the panels are WBP_UPC_BeltPanel, WBP_UPC_StoragePanel, WBP_UPC_ChestPanel, WBP_UPC_BagPanel and WBP_UPC_PaperDoll. Keep the screens' contract: every action asks the check its request runs on the server (Containers, above), sends the request only when the check said yes, and redraws from the components' events once the change arrives, about a round trip later, with nothing predicted. A button that moves an item with no place picked, such as Take or Store, asks the inventory where it lands: FindTransferDestination between the player's inventory and a chest, FindMoveDestination into one container, FindAddDestination where an add would put it; each answers the place one request moves the most units into, with the server's own answer there and, on a refusal, its reason (Full, or Held when the belt or the hand holds the item and more of it would land once they let go), and the button sends RequestTransfer or RequestMove with that place. Pick Up plays the item's grab sound and Put Down and Put Back its place sound, from the equipment manager's GetInventoryMoveSound (the item's own InventoryGrabSound and InventoryPlaceSound, else its Stowable fragment's DrawSound and PutAwaySound, and none for a move into or out of the hand that the item's look sounds itself). UPCInventoryStatics.GetItemFootprint answers an item's grid cells for a drag picture, the belt's GetSlotOfItem the slot an item sits in, and the inventory's IsHeldBy whether the belt or a hand holds it.
The look. The screens, the HUD and the interaction prompts all draw from one theme asset, /UniversalPlayerCombat/Demo/UI/Theme/UITheme_UPC_Default: its colors, fonts, surfaces and paddings restyle every one of them, and a copied screen keeps reading it. In a copy, blocks move freely, and the weight bar, belt panel, hint bar and item details can be deleted. A new widget takes a theme role from a row in its Widget Blueprint's ThemeRoles map (the widget's name and an E_UPC_UIRole); a renamed widget needs its key renamed too, or that part keeps its own look and the log shows a line (LogBlueprintUserMessages) each time the designer refreshes that widget. A color change is an edit to a copy of the theme in the project's folder, since a plugin update replaces the shipped one:
Every shipped widget reads its theme through the Get UI Theme function of BFL_UPC_UITheme, beside the theme asset, which returns UITheme_UPC_Default; pointing it at the copy is a Blueprint step for the customer.
A row for a duplicated config or a copied screen goes on the screens component's class defaults: duplicate BPC_UPC_InventoryScreens into the project and put the copy on the project's controller in place of the demo's (a Blueprint step), then:
Opening from the world. An interaction option whose OptionId is OpenInventory opens the screen as the interaction starts, with the interactable's UPC World Inventory beside it when the actor has one. Give it a payload that ends as it starts, as the demo's Delay of 0 s, since only a payload reports the start; a longer one keeps the interaction running behind the screen, which hides the prompt and refuses the next interact press after a close (Interaction, below, writes the option as text):
Check: in Play on LVL_UPC_Demo, F at the hallway's loot chest takes its potions, ring and necklace; I then opens the open world list on its categories, All lists the ring and the necklace above the potions, Consumables lists the potions only, and I closes it again.
A stash holds each player's own items: the Stash is a container of the player's inventory reached with an Access Tag (the survival grid and party RPG recipes, above). The demo's BP_UPC_StorageBox (/UniversalPlayerCombat/Demo/Interactables/StorageBox) gives a character standing in its zone the Access Tag of that character's own storage, the first container its inventory does not carry, through GE_UPC_StorageAccess, applied on the server, and its Open option opens the screen with the storage panel beside the bag. One box serves every style. From the storage panel the player can equip an item or put it on the belt directly: RequestEquipItem and RequestAssignSlot move it into the carried containers first, and their checks (CanEquipItem, CanAssignSlot) answer for it; the inventory's CanTakeToHold names why one is refused, such as Too Heavy or Full. A chest's items work the same way. To open one container only, place one and set its tag:
Use drinks a potion or eats a herb. The screen's Use sends the inventory's RequestUseItem, and a belt slot holding an item that is not equippable does the same when pressed; on the server both send the gameplay event UPC.Event.UseItem with the item as the event's Optional Object to the character's ability system. The demo's GA_UPC_UseItem (Server Only, Ability Trigger UPC.Event.UseItem, granted by AbilitySet_UPC_Default with no input) spends one unit and applies the item's BP_UPC_ItemFragment_UseEffect (a vital tag and an amount); the screen offers Use only for an item carrying that fragment, and the demo potion, herb and mixed herb do. An ability of the project's own on the same trigger must be Server Only (NetExecutionPolicySERVER_ONLY): the event is sent on the server, which starts no Local Predicted ability for a remote player.
Drop sends UPC.Event.DropItem with the item to the player's own character, where GA_UPC_DropFromScreen (a UPC Drop Ability with ItemSelection From Event and DropCount 0, granted by AbilitySet_UPC_Default with no input) drops the whole stack. The screen offers Drop only for an item with a World Item fragment, while the inventory's CanItemLeave says yes.
Check: in Play, with the character's health below its maximum, Use on a potion spends one (GetItemStackCount) and raises UPC.Vital.Health (the character's VitalsComponent, GetVitalValue); a potion on the belt's right slot does the same when its key is pressed.
What combining one item with another makes, such as two herbs mixed into a mixed herb, rounds loaded into a magazine one at a time or a key opening a locked box, lives in a UPC Combine Set (unreal.UPCCombineSet, a data asset: Add, UPC, Items, Combine Set) on the config's CombineSets, so several configs share one set. A drop of one item onto another, or a screen's Combine, asks each set's recipes and then its combines, in order; the first that matches the two items decides, ahead of a merge or a swap. The demo's CombineSet_UPC_Demo (/UniversalPlayerCombat/Demo/Items/Inventory) holds Mix Herbs and Dip Arrows as recipes and Load Holder and Coat Weapon as Blueprint combines.
Recipes (unreal.UPCCombineRecipe, plain rows, no Blueprint): item_a and count_a, item_b and count_b, results (each a unreal.UPCCombineResult with item and count; a second result gives a byproduct back, such as an empty bottle), order_matters (off: either item dropped onto the other combines), required_tags (the character's ability system must hold them all, else the drop is refused UPC.Inventory.Refused.Locked; a workbench zone grants one as the demo storage box grants its Access Tag) and seconds (each combine's time, moving stopping it; 0 is at once). A drop makes as many whole combines as the dragged units and both stacks allow; it is refused UPC.Inventory.Refused.NotEnough while a stack holds too few for one, and with the add's reason when the results do not fit. The results of each combine are judged with the units it takes already gone, so at a weight limit a result weighing no more than what it uses passes. A row that would use up a bag still holding anything it does not also use is refused UPC.Inventory.Refused.BagNotEmpty; the bag is emptied first. The config's CombineResultPlace (unreal.UPCCombineResultPlace: ONTO_THE_TARGET, the default, ONTO_THE_SOURCE, AS_ADDED) says where the first result goes: the place of the item combined onto, or of the item picked first, once the combine uses that item's stack up, the place is not inside the other item (a bag the combine uses up) and the result fits there, else where an add would; the other results go where an add would. Nothing a combine makes goes on the belt by bAutoPopulateFromInventory, but a belt slot an ingredient emptied takes the first result when CanAssignSlot lets it in.
herb = load("/Game/Items/Item_Herb")mixed = load("/Game/Items/Item_MixedHerb")combine_set = new_data_asset(unreal.UPCCombineSet, "/Game/Inventory", "CombineSet_Knight")made = unreal.UPCCombineResult()made.set_editor_property("item", mixed)recipe = unreal.UPCCombineRecipe()recipe.set_editor_property("item_a", herb)recipe.set_editor_property("item_b", herb)recipe.set_editor_property("results", [made])recipe.set_editor_property("seconds", 1.0)combine_set.set_editor_property("recipes", list(combine_set.get_editor_property("recipes")) + [recipe])save(combine_set)config = load("/Game/Inventory/InventoryConfig_Knight") # the project's copy of a demo configconfig.set_editor_property("combine_sets", list(config.get_editor_property("combine_sets")) + [combine_set])save(config)
Combines (unreal.UPCInventoryCombine, instanced on the set's combines) for logic a row cannot say. The combine is the project's own class, a Blueprint child overriding its three functions (a Blueprint step) or a C++ subclass in the project's module; the demo's BP_UPC_Combine_LoadHolder and BP_UPC_Combine_CoatWeapon in /UniversalPlayerCombat/Demo/Items/Inventory/Blueprints are ready to copy. Load Holder fills any item carrying the demo Holder fragment (BP_UPC_ItemFragment_Holder: Holds, the item it takes; Capacity; StatTag, the stat counting what one unit holds), so a quiver is an item with that fragment holding arrows, with nothing else to set, and the demo screens show its load over its capacity, 12/20.
Matches(Item, Target): whether it handles Item dropped onto Target.
GetUnits(Query, OutReason): how many units to make, from 0 to Query.Count (Query, a UPCInventoryCombineQuery: Inventory, Item, Target, Count). Asked when the drop is judged, also by a screen previewing it, and again before each unit with the units left; it changes nothing. 0 turns the drop down; set OutReason to a tag under UPC.Inventory.Refused, such as UPC.Inventory.Refused.Full.
ApplyUnit(Context) (Context, a UPCInventoryCombineContext with the same four fields): makes one unit on the server, such as one more stack of a loaded stat tag on Target (AddStatTagStack) and one unit of Item consumed (the inventory's ConsumeItemInstance).
UnitTiming (unreal.UPCItemActionTiming): half a second per unit by default, stopped by moving; 0 makes every unit at once.
For a magazine: rounds stack, the magazine has MaxStackSize 1 and counts its rounds in a stat tag; GetUnits is the least of Query.Count, the inventory's GetItemStackCount(Item) and the room left, else 0 with Refused.Full. For a locked box: GetUnits is 1 when the key and the box share a lock tag in their SlotTags and the box lacks an unlocked stat tag, else 0 with UPC.Inventory.Refused.NotAdmitted; ApplyUnit adds the unlocked stat tag and consumes the key; UnitTiming 1.5 s.
The UI changes nothing for a combine beyond what it does for a move: CanPlace answers COMBINE (unreal.UPCPlaceResult) with the units, or REFUSED with the recipe's or combine's reason, and RequestMove starts it. A Combine button names the target by item instead: RequestCombine(Item, Target) combines Item's whole stack with Target, an item in hand with no place included, and unreal.UPCInventoryStatics.can_combine(inventory, item, target) is its check: None when the two do not combine now, else (results, reason) with the recipe's results (UPCCombineResult rows) and an empty reason (Python.md says why the yes or no is not returned; a Blueprint node has pins for the results and the reason, UPC.Inventory.Refused.NoCombine when nothing combines the two). The inventory's OnCombined reports each landing on the server and the owning player's machine, with the two items, the recipe (its Set and RecipeIndex in that set's recipes) or the Combine, and the units that landed together in its Units. The config's bCombineAtOnce lands every unit at once with no wait, as a screen that pauses the game needs, and OnCombined then fires once for all of them. While it runs, OnEquipStepStarted fires once per unit (the step's SourceItem is the target, UsedItem the dropped item, Timing.Seconds the unit's length) and OnEquipSequenceEnded reports the end, while GetPlayingStep answers how far the current unit has got and how many units have landed, for a widget opened partway; moving, RequestCancelSequence with either item, or either item leaving stops it, and the units made stay. One equipment change runs at a time, so a belt press during a load is turned down.
Check: in Play, with a stack of 10 rounds and an empty magazine of 6, the inventory's CanPlace for 10 rounds onto the magazine's place answers COMBINE with 6; RequestMove with the same arguments loads one round per half second, and after 3 seconds the magazine counts 6 and the stack 4.
A bag bar whose pouches hold items, as in an MMO: a bag in a bag bar slot opens a container of its own, and in a backpack that opens no bags it is closed, keeping what it holds out of reach. The demo pouch is /UniversalPlayerCombat/Demo/Items/Bags/Item_UPC_Pouch; the demo party RPG config opens it in its backpack instead (Container_UPC_Backpack has bOpensBags on).
The bag bar is a config row whose definition has bOpensBags on and bTakesNewItems off, listed after the backpack, so adds fill the backpack and then the bags in bag bar order (row tag UPC.Inventory.Container.BagBar).
A bag is an item with MaxStackSize 1 and a Container fragment (unreal.UPCItemFragment_Container): Definition, the UPC Container Definition of what it holds (its own size, rules, bCarried, bTakesNewItems, AccessTag, bOpensBags off), and ContainerTag (UPC.Inventory.Container.Bag by default; never a tag the config's own rows use).
A bag's container is named by a UPC Container Id (unreal.UPCContainerId: Tag and ProvidingItem, the bag); a tag alone names one of the config's rows. GetContainers lists each open bag's id right after the bag bar, and GetContainerOpenedBy answers it for one bag, so a bag bar slot opens its bag's window with it.
A UI moves a bag onto the bag bar with RequestMove; its container opens and OnCapacityChanged fires with its id. Dropping a stack on an open bag puts it inside: CanPlace answers INTO_BAG (unreal.UPCPlaceResult), or REFUSED with the bag's reason, such as UPC.Inventory.Refused.Full. A bag on the belt or in hand stays open, also where the belt or the hand frees its place. A bag keeps what it holds wherever it goes: moved off the bag bar, into storage or a chest, or dropped, it closes with its items inside, and they come back in their places when it opens again or is picked up (a dropped bag's GetUnitsThatFit judges the bag and its items together). A bag holding items never goes inside another bag (REFUSED with UPC.Inventory.Refused.BagNotEmpty), and an item the belt or the hand holds stays with the character when its bag leaves. Game code removing or consuming a bag holding items spills them into the first free places, kept past the end of the backpack when none is free; TakeItemOut takes a bag out with its items instead, as a drop does.
Check: in Play with a backpack row and a bag bar row on the player's config and a pouch added, RequestMove of the pouch to UPC.Inventory.Container.BagBar place 0 makes GetContainerSize of its id read 8 by 1; a potion stack's CanPlace onto that bag bar place answers INTO_BAG, and after RequestMove the potions sit in the pouch's container (GetItemsInContainer).
UPC is no save system: the game's own Save Game keeps the inventory. Give the Save Game Blueprint a variable of type UPC Inventory Snapshot (unreal.UPCInventorySnapshot), then, on the server (a Blueprint step):
Save: the inventory's ExportSnapshot into that variable, then Save Game to Slot, while the player's controller still possesses the character.
Load: Load Game from Slot, then the inventory's ImportSnapshot with the variable, from the player controller's On Possess after its parent call, or later. The character's Event Possessed is too early: the belt is not bound yet, so the belt slots stay empty and a warning says so. The import replaces everything the inventory held, starting items included, also on the first possession at map start, which comes before Begin Play, and puts back each stack's place, stat data, belt slot and what was in hand or worn, in the same places; anything the belt or the hand held from outside the inventory is put away.
Keep the redirector of an item asset moved or renamed after a save, or add a CoreRedirect for it, or the import leaves that item out with a warning. An item whose saved place a changed config no longer has takes the first free place, a bag's items come back inside it, and a stack saved larger than its item's Max Stack Size comes back as several stacks.
A UPC World Item (AUPCWorldItem, or the demo's BP_UPC_WorldItem) placed in the level is picked up with the interact key: ItemDefinition, StackCount, PhysicsMode. It shows the item's Visuals mesh (its DropMesh when set) only for an item that also has a World Item fragment, so the item needs both. To put a fresh item in the world from Blueprint, as the demo's pickup spots do when they restore a taken pickup, Spawn Actor BP_UPC_WorldItem on the server and set Item Definition, Stack Count and Physics Mode on the node; one world item class serves every item.
The duplicated player's ability set grants the interact ability on UPC.Input.Interact (E). A picked-up item goes into the inventory, and onto the first belt slot that takes it while the belt's bAutoPopulateFromInventory is on, as the demo controller's is.
Give the item a World Item fragment (UUPCItemFragment_WorldItem: DropMontage, WorldItemActorClass, DropAnchor, DropOffset, bProjectToGround, DropRotation, bSimulatePhysics, and ImpactSound with ImpactSilentSpeed and ImpactFullSpeed for the sound it makes each time it lands or bounces, louder the harder it hits) and the item's Visuals HeldMesh (or DropMesh) shows on the ground. The duplicated player's ability set grants the UPC Drop Ability (UUPCDropAbility: ItemSelection, DropCount, bSkipUndroppable) on UPC.Input.Drop, which IMC_UPC_Default maps to G. By default it drops what a hand holds, the weapon first, read from the equipment manager whether the item is on the belt or not; worn gear is never in hand, so G never drops a ring or a helmet. A screen's Drop button reaches a second drop ability, GA_UPC_DropFromScreen, through the event UPC.Event.DropItem (Using and dropping from a screen, above).
Check: in Play, G drops the held item as a world item and the inventory loses it; E near it picks it back up.
A UPC Item Holder component (UUPCItemHolderComponent: Places, unreal.UPCItemHolderPlace rows PlaceId, PlaceComponentTag, Filter; ClaimRadius) on a replicated actor offers Take and Put Back for each place. Duplicate the demo's BP_UPC_WeaponRack.
A UPC World Inventory component (UUPCWorldInventoryComponent) on a replicated actor holds items every nearby player sees: Config and StartingItems as on a character's inventory, AccessRadius (cm from the character to the actor's collision, at least the interactor's Detection Radius plus the interact ability's Server Range Tolerance; 0 reaches from any distance) and AccessTag (a tag the character's own ability system must hold, a key). Duplicate the demo's BP_UPC_LootChest (/UniversalPlayerCombat/Demo/Interactables/LootChest). Its UPC Interactable has two options: Open (OpenInventory, on UPC.Input.Interact, E) opens the player's inventory screen with the chest's items beside the player's (Opening from the world, above), and Take All (on UPC.Input.Interact.Secondary, F) calls the interacting character's inventory RequestTakeAll with the chest's World Inventory, which takes everything that fits and leaves the rest.
To give a chest contents of the project's own, duplicate the demo chest, write its World Inventory component and place the copy in the level. The component is a template inside the Blueprint (Python.md, Components on a Blueprint); InventoryConfig_UPC_LootChest is the demo's thirty-slot config, or make a config as the keychain recipe does and list its container on it:
chest = duplicate(f"{DEMO}/Interactables/LootChest/BP_UPC_LootChest", F, "BP_ProjectChest")sds = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem)for handle in sds.k2_gather_subobject_data_for_blueprint(chest): obj = unreal.SubobjectDataBlueprintFunctionLibrary.get_object_for_blueprint(unreal.SubobjectDataBlueprintFunctionLibrary.get_data(handle), chest) if isinstance(obj, unreal.UPCWorldInventoryComponent): obj.set_editor_property("Config", load(f"{DEMO}/Items/Inventory/InventoryConfig_UPC_LootChest")) obj.set_editor_property("StartingItems", [potion, potion, rusty]) # one unit per entry; repeats join into one stack obj.set_editor_property("AccessRadius", 250.0) # cm; an AccessTag locks it to a key holderunreal.BlueprintEditorLibrary.compile_blueprint(chest)save(chest)
Place the copy in the level the way the storage box example does (Storage boxes, above).
The demo's chest panel, WBP_UPC_ChestPanel, reads the World Inventory like any inventory (GetItemsInContainer, OnLayoutChanged) and moves items through the player's own inventory: CanTransfer and RequestTransfer (an item in either inventory, a destination in the other) and RequestTakeAll for its Take All button. Put the loot on an actor of its own, never on a character, and leave the chest config's container Access Tags empty; the World Inventory's own AccessTag locks the chest.
Check: in Play with two players, both read the chest's items with GetItemsInContainer, and F at the chest (Take All) moves them into the interacting player's inventory.
An actor becomes interactable with a UPC Interactable component (UUPCInteractableComponent): Options (unreal.UPCInteractionOption rows: OptionId, ActivationInputTag (UPC.Input.Interact), Prompt (Verb, Name, Icon), PayloadSpec, Approach (SpotComponentTag, AcceptanceRadius), Restrictions (bLockMovement, bLockFacing, bLockCamera, bEndRunningAction), bCancelByAbility, Priority). With no payload, choosing the option fires the component's On Option Interacted event, which the actor's own Blueprint answers (opening the door: a Blueprint step; the demo's BP_UPC_Door and BP_UPC_Chest show it).
PayloadSpec makes the character do something first: Play Montage (FUPCMontageInteractionPayloadSpec: Montage, PlayRate, SectionName), Delay (FUPCDelayInteractionPayloadSpec), Trigger Ability / Event (FUPCEventInteractionPayloadSpec), Run Ability (FUPCRunAbilityInteractionPayloadSpec) or Payload Asset (FUPCAssetInteractionPayloadSpec). Run Ability triggers the character's own ability (Server Initiated or Server Only, granted once) with the interactable as the event's Target; the ability reads which option started it with Get Interaction Option Id on the event's Target Data (UUPCInteractionStatics::GetInteractionOptionId in C++). Python cannot set its fields one by one; write the option as text, the payload as its struct path followed by its fields:
Read opt.export_text() back to confirm the payload took. An Action Payload asset for the Payload Asset kind is made with unreal.UPCActionPayloadFactory: set its PayloadClass (such as unreal.UPCMontageActionPayload), then TOOLS.create_asset(name, folder, unreal.UPCActionPayload, factory).
A drop on an item merges or swaps the two instead of combining them
No recipe or combine in the Combine Sets on the inventory's config takes the pair, the set is not on the config's CombineSets, or the character's inventory uses another config.
A key lands in the pack, not on the keychain
The keychain is not listed before the pack in the config's Containers, or its Item Filter's AllowedTags does not name the kind in the key's SlotTags (UPC.Slot.KeyItem). Rules are on the container definition's Rules, not on the config row.
Everything lands on the keychain
A List takes anything its rules allow; the keychain has no Item Filter rule, or the rule was never written to the container definition's Rules list.
A key can be dropped, or counts toward the weight limit
The key carries a World Item fragment (droppable) or a Weight fragment (weighs); leave both off the item definition.
A recipe's drop does nothing at the weight limit
The result weighs more than the units the combine takes, so the load would pass the limit (UPC.Inventory.Refused.TooHeavy).
A recipe using a bag does nothing
The bag holds an item the recipe does not use (UPC.Inventory.Refused.BagNotEmpty); take it out first.
A rule, fragment or row set from Python has no effect
The list was edited and not written back with set_editor_property, or a row struct (a container row, a threshold, a limit, a bonus) was filled field by field instead of with import_text (Python.md).
A combine does nothing
The character has no UPC Equipment Manager (a warning names it), or another equipment change is running.
An item lands in no slot
The belt's bAutoPopulateFromInventory is off, every slot's Filter refuses its SlotTags, the belt is full, or the character carries one of the belt's BlockedTags.
A belt press does nothing under one combat style
That style's Gameplay Tags fragment puts on a tag the belt lists in BlockedTags, such as UPC.State.BeltBlocked on the demo Freeflow style.
A ring or a helmet lands on the belt and draws with a number key
The belt's slots have no Filter; give each slot an item kind, as the demo's diamond does.
I opens no inventory screen
The game mode's player controller carries no BPC_UPC_InventoryScreens, or its default mapping context lacks IA_UPC_Inventory.
I opens the open world list for another style
The screens component's InventoryScreenClasses has no row for the config the character's inventory uses, as for a duplicated config; add one.
A screen offers no Use, or Use does nothing
The item carries no BP_UPC_ItemFragment_UseEffect, the character's ability set lacks GA_UPC_UseItem, or the project's own use ability is not Server Only.
A potion on the belt does nothing when pressed
No Server Only ability answers UPC.Event.UseItem on the character.
A screen's Drop does nothing
The character's ability set lacks GA_UPC_DropFromScreen, or the item has no World Item fragment.
The storage panel never shows at a box
The box's AccessTag differs from the container's Access Tag in the character's config, or the character's config has no container the character does not carry.
A stack on the belt cannot go into the stash
A whole stack on the belt never goes into storage the character does not carry; take it off the belt with RequestAssignSlot first, as the demo screens' Store does.
The pickup shows no mesh
The item has no World Item fragment, or no Visuals mesh.
G does nothing
The held item has no World Item fragment, or the character's ability set lacks the drop ability.
The prompt never shows
The interactor's Detection or DetectionRadius misses the actor, or RequireSight is blocked.
A placed actor ignores Blueprint changes to its options
The placed actor keeps its own copy of Options; reset it to the Blueprint's on the placed actor.
A chest shows its items on the server only
The chest's actor does not replicate; turn on Replicates in its Class Defaults (a warning names the class).
Take All or a transfer moves nothing
The character stands farther than the chest's AccessRadius from its collision, lacks its AccessTag, nothing fits in its inventory, or the request went to a World Inventory rather than the player's own inventory.
AGENTS.md, Recipes 2 and 4, make an unarmed item and a sword. This file covers the rest of an item: every fragment, which hand it takes, an off-hand shield, items shared through a preset, hanging items on the body when put away, and worn gear.
Tasks: an item that is not gear (a key, a material, a potion) and what it weighs, whether it can be dropped and where it is kept; a shield in the left hand beside the sword; a two-handed weapon; a supporting item that adds moves only while drawn; all swords sharing one set of keys; a sword on the back when sheathed; a draw animation; a Swing Trail behind each swing; only one heavy weapon on the belt; a helmet, a cape, armor or rings the character wears.
An Item Definition (UUPCItemDefinition): Fragments, Preset (UUPCItemPreset, whose Fragments the item inherits unless it has its own of that kind), SlotTags (which belt slots take it), MaxStackSize, DisplayName, Description, Icon. Make each fragment with unreal.new_object(cls, outer=item) and write the whole Fragments list back.
Fragment (picker label)
Does
Fields
UUPCItemFragment_Equippable (Equippable)
Makes it drawable
ActionGraph, bIsMainWeapon (label Main Weapon), HoldPlaces, PlacesRequired, EquipmentTags, InputMappingContext, InputMappingPriority, InputMappings, InputChords, TraceSockets, AttackStopDistance, AbilitySetsToGrant, AnimLayerClass, ItemActorClass, InstanceType, DrawTiming and StowTiming behind bOverrideDrawTiming and bOverrideStowTiming
How heavy one unit is, the cells it covers in a grid container, and the capacity owning it adds (AgentGuide/InventoryInteraction.md, Containers)
Weight; Size; Bonuses
UUPCItemFragment_Container (Container)
Makes the item a bag that opens a container of its own on a bag bar (AgentGuide/InventoryInteraction.md, Bags)
Definition, ContainerTag
UUPCItemFragment_BeltLimit (Belt Limit)
Caps its copies on the belt
MaxBeltCount
An item that is not gear (a key, a material, a potion) needs no Equippable fragment. Its kind is SlotTags (UPC.Slot.KeyItem, UPC.Slot.Ingredient, UPC.Slot.Consumable; the inventory's rules and the belt read it), MaxStackSize sets how many share a stack, and each fragment adds one behavior: Weight makes it count toward a weight limit, World Item lets it lie on the ground and be dropped, Grid Footprint gives it cells in a grid, Container makes it a bag, Capacity Bonus grows a capacity. An item has only the behaviors whose fragment it carries: with no Weight fragment it weighs nothing, with no World Item fragment it cannot be dropped. AgentGuide/InventoryInteraction.md, Recipe: a keychain, builds items of this kind with the container and config that keep them.
Main Weapon.bIsMainWeapon on (the default): its graph drives combat while drawn. Off with an ActionGraph: it drives only while no main weapon is drawn (a torch). Off with no graph: a supporting item (a shield), whose moves live in the graph of the weapon it is held with, or in a sub graph gated on its EquipmentTags (AgentGuide/ActionGraphs.md, Sub graphs).
HoldPlaces rows (unreal.UPCHoldPlaceSpec: Place, Offset, SocketOverride, PlaceTags) list where an item can be held, in order; PlacesRequired is how many it takes. A new Equippable holds UPC.Place.Main. The character's EquipmentManagerComponent.BodyPlaces maps each place to a socket (UPC.Place.Main to hand_r, UPC.Place.Off to hand_l). Python cannot set a row's fields one by one, so write the row as text:
A two-handed weapon lists both hands and takes both: two rows (UPC.Place.Main, UPC.Place.Off) and PlacesRequired 2.
A sword held in either hand lists both, main first, with PlacesRequired 1: a second copy takes the off hand. Turn on the off-hand row's bMoveToEarlierPlaceWhenFree (Move To Earlier Place When Free, written with import_text like the row's other fields) and, once the main-hand sword is put away or dropped, the other moves into the main hand and drives; leave it off for an item that stays in the hand it was drawn into, such as a dagger kept in the off hand. To put the second sword on a diamond belt's off-hand slot, give it the off-hand kind too (SlotTagsUPC.Slot.Weapon.Sword and UPC.Slot.OffHand).
The belt takes each starting item into its own slot and draws every one that fits beside what is held, so the sword (main hand) and the shield (off hand) are both drawn at the start. The slot keys 1 to 4 draw and put away each one.
To draw an item the inventory holds from code at once, putting nothing away, as a loadout handed out in a level does: the equipment manager's EquipItemIfFree (server only), which draws it only while it is equippable, not drawn, carried and in reach and every place it needs is free.
To give a character gear from Blueprint, on the server: the inventory's Add Item Definition, then Equip Item If Free on the equipment manager with the item it returns. A null answer means a place is taken, such as by another cape; Request Equip Item puts that one away first. Remove Item Instance takes the gear off as it leaves the inventory. For a game with no inventory, belt or weapons that only dresses a character, skip the inventory: Equip Item on the equipment manager (server) takes the item definition with no inventory item, puts away whatever holds the places it needs (places are gameplay tags, as many as the body's Body Places names) and returns the drawn instance, null when the body has no place for it; Unequip Item with that instance, or the one Get Instance In Place answers, takes it off. A player's equip request, RequestEquipItem, also takes an item in the Stash or a chest, moving it into the carried containers first.
The shield's block and parry are its Guard fragment (AgentGuide/DamageDefense.md); the guard move goes in the sword's graph (AgentGuide/ActionGraphs.md).
UPC.Equipment.Shielded sits on the character while the shield is drawn, so a transition or sub graph can require it.
If the shield sits crooked, judge a correction from a capture and write it as the row's Offset, as AGENTS.md, Recipe 4, Step 5, does for the sword.
Check: reload the item and print its fragments' classes, bIsMainWeapon, the hold place and EquipmentTags. In Play, the shield is in the left hand and the sword in the right.
An item with no Stowable fragment disappears when put away. To hang it on the body: a UUPCItemFragment_Stowable with StowPlaces rows (unreal.UPCStowPlaceSpec: Place such as UPC.Place.Back.Left, Offset, DrawMontage, StowMontage), written with import_text as above; a UPC Stow Visual component on the character Blueprint (the duplicated demo player has one); and the place's socket on the customer's skeleton, mapped in BodyPlaces. By default only an item on the belt hangs while out of hand, and an item only in the inventory shows nowhere on the character; the inventory config's bShowCarriedItemsOnBody hangs every carried item outside a bag as well, as the demo survival grid does, so no weapon can be hidden. A draw or stow montage carries a UPC Equipment Commit notify (UUPCEquipmentCommitNotify, BlendSeconds) on the frame the hand closes on the grip; add it with unreal.AnimationLibrary.add_animation_notify_event(montage, "Combat", seconds, unreal.UPCEquipmentCommitNotify). The demo's reach montages belong to the demo mannequin; the customer's character needs its own. The item's own sounds go on the same fragment, never on the reach montage, so items sharing one reach still sound like themselves: DrawSound plays as the item arrives in the hand and PutAwaySound as it arrives in its place, whether a reach, a pickup or a screen moved it, on every machine that shows the character; an item that appears with nothing showing it before, such as a starting item, plays nothing.
Worn gear is an Equippable item with bIsMainWeapon off and no ActionGraph, so it is drawn beside the weapon and leaves the moves alone. Its places are gameplay tags the project names, such as UPC.Place.Head and UPC.Place.Ring.Left, or only a top and a bottom. The character's EquipmentManagerComponent.BodyPlaces says where each place is on the body; a row needs a Socket only where a look sits. The duplicated demo player already has rows for the demo's worn places (AGENTS.md, Step 2), so the script below sets the socket on a row it finds and adds a row only for a place that is missing. Give worn gear SlotTags the belt slots' Filter does not pass, or a belt that takes new items puts it on a slot and puts it on at pickup. On a belt, its key puts it on and takes it off; it never counts as in hand, since only the equipment manager's HandPlaces (UPC.Place.Main and UPC.Place.Off by default) are hands, so the belt does not highlight its slot and the drop key never drops it. A body whose weapons are held in places of its own lists those places in HandPlaces.
Look
How
A static mesh on one bone (a helmet, a ring)
Visuals HeldMesh, the body place's socket or bone, and the hold place's Offset to turn it into place
A skinned mesh on the body (a cape, gloves, armor)
Worn Mesh Meshes, rigged to the character's skeleton; it follows the body's pose and comes off with the item
Nothing (a necklace that only grants a stat)
No look fragment, and a body place with no socket
A ring's bonus is a tag in its EquipmentTags, held on the character while the ring is worn, that something else reads: the duplicated demo player's UPC Speed Modifier turns UPC.State.Swift into five percent more walk speed, as the demo ring and necklace use it, and another character gets a row for the tag in its UPC Speed Modifier's SpeedScaleByTag. An attribute bonus is a gameplay effect in an ability set's GrantedGameplayEffects, on the Equippable's AbilitySetsToGrant, and that effect is authored in the editor, since Python cannot set a gameplay effect's components: a Gameplay Effect Blueprint with Duration Policy Infinite, plus a modifier for an attribute or, for a tag, the Grant Tags to Target Actor component with the tag in its Add Tags. Items fill their places in the order HoldPlaces lists them, so two rings take the left hand, then the right, and a third replaces the left one; CanDraw(item, evicted) names the one it would replace. A UI keeps its own list of the places it shows, asks GetInstanceInPlace for each and skips an instance whose IsDefaultItem is true, redraws on the equipment manager's OnDrawnStateChanged, which fires once a change has settled, the item's place included, and asks RequestEquipItem with the place an item was dropped on (CanEquipItem with that place first) and RequestUnequipItem. Hiding the body under armor, morph targets and cloth are set up in the meshes.
eqm = defaults(bp).get_editor_property("EquipmentManagerComponent")rows = list(eqm.get_editor_property("BodyPlaces"))for place, socket in (("UPC.Place.Head", "head"), ("UPC.Place.Shoulders", "None"), ("UPC.Place.Ring.Left", "ring_01_l"), ("UPC.Place.Ring.Right", "ring_01_r")): row = unreal.UPCBodyPlace() row.import_text(f'(Place=(TagName="{place}"),Socket="{socket}")') found = [i for i, r in enumerate(rows) if str(unreal.GameplayTagLibrary.get_tag_name(r.get_editor_property("Place"))) == place] if found: rows[found[0]] = row else: rows.append(row)eqm.set_editor_property("BodyPlaces", rows)save(bp)def worn(item, places): eq = unreal.new_object(unreal.UPCItemFragment_Equippable, outer=item) eq.set_editor_property("bIsMainWeapon", False) held = [] for place in places: row = unreal.UPCHoldPlaceSpec() row.import_text(f'(Place=(TagName="{place}"))') held.append(row) eq.set_editor_property("HoldPlaces", held) return eqhelmet = new_data_asset(unreal.UPCItemDefinition, F, f"Item_{N}_Helmet")vis = unreal.new_object(unreal.UPCItemFragment_Visuals, outer=helmet)vis.set_editor_property("HeldMesh", load("/Game/.../SM_Helmet"))helmet.set_editor_property("Fragments", [worn(helmet, ["UPC.Place.Head"]), vis])save(helmet)cape = new_data_asset(unreal.UPCItemDefinition, F, f"Item_{N}_Cape")look = unreal.new_object(unreal.UPCItemFragment_WornMesh, outer=cape)look.set_editor_property("Meshes", [load("/Game/.../SK_Cape")])cape.set_editor_property("Fragments", [worn(cape, ["UPC.Place.Shoulders"]), look])save(cape)ring = new_data_asset(unreal.UPCItemDefinition, F, f"Item_{N}_Ring")ring_eq = worn(ring, ["UPC.Place.Ring.Left", "UPC.Place.Ring.Right"])ring_eq.set_editor_property("EquipmentTags", tags("UPC.State.Swift"))ring_vis = unreal.new_object(unreal.UPCItemFragment_Visuals, outer=ring)ring_vis.set_editor_property("HeldMesh", load("/Game/.../SM_Ring"))ring.set_editor_property("Fragments", [ring_eq, ring_vis])save(ring)
Add each place tag to the project's tags first; the plugin declares the demo's places.
A ring that sits crooked gets an Offset on each of its two rows, written into the row text as for the shield.
Check: in Play, equip each item from the inventory: the helmet sits on the head, the cape moves with the body, the first ring goes on the left hand and the second on the right, and the ring's stat comes and goes with it.
A paired action is two characters playing synced animations together: an execution, a counter, a vault over an enemy. Tasks: a finisher from two animations; executing an enemy only while it is stunned; dealing the damage on the right frame; a cinematic camera during it; starting it from a combo.
A Paired Action (UUPCPairedActionAsset, new_data_asset) holds Participants (unreal.UPCPairedActionParticipant), one per role, and its placement and timing:
Participant field
Does
RoleId
The role's name, such as Attacker or Victim. Read-only in the editor; set it with a constructor keyword: unreal.UPCPairedActionParticipant(role_id="Attacker")
Montage, StartSection
What this role plays
Offset
Where it stands relative to the shared anchor
TravelMode
How it gets to its spot
bRequired
The action fails without a body in this role
RequiredActorTags, BlockedActorTags
Who may fill it
TagsDuringAction
Tags held while it plays (UPC.State.ActionsBlocked from the start)
Events
What happens on frames: UUPCPairedActionEvent_Damage (Deal Damage: DamageConfig, DamageOverride, LeaveHealthAtLeast, bKill, ExtraDamageTags, TimeSeconds, TargetRoleId), UUPCPairedActionEvent_Ragdoll (Ragdoll: Impact with Bone, Velocity in cm/s along ImpactFrame (X ahead, Z up) and Spread) and others
Hooks
Behavior riding the role: UUPCPairedActionHook_AutoCinematicView (FocusRole, ViewAngle, HeightOffset), UUPCPairedActionHook_CameraShot (Shot)
bSyncWithCurrent, SyncMarker
Start from an animation already playing, meeting at a UPC Paired Sync Marker notify (MarkerName)
pa = new_data_asset(unreal.UPCPairedActionAsset, F, f"Paired_{N}_Execute")atk = unreal.UPCPairedActionParticipant(role_id="Attacker")atk.set_editor_property("Montage", am_exec_attacker)atk.set_editor_property("bRequired", True)vic = unreal.UPCPairedActionParticipant(role_id="Victim")vic.set_editor_property("Montage", am_exec_victim)vic.set_editor_property("bRequired", True)cut = unreal.new_object(unreal.UPCPairedActionEvent_Damage, outer=pa)cut.set_editor_property("DamageConfig", dmg_heavy)cut.set_editor_property("LeaveHealthAtLeast", 1.0) # an earlier blow leaves the victim alive for the killing blowcut.set_editor_property("TimeSeconds", 0.6)cut.set_editor_property("TargetRoleId", "Victim")hit = unreal.new_object(unreal.UPCPairedActionEvent_Damage, outer=pa)hit.set_editor_property("DamageConfig", dmg_heavy)hit.set_editor_property("bKill", True) # takes all the health left, whatever defends the victimhit.set_editor_property("TimeSeconds", 1.2)hit.set_editor_property("TargetRoleId", "Victim")atk.set_editor_property("Events", [cut, hit])pa.set_editor_property("Participants", [atk, vic])pa.set_editor_property("AnchorRoleId", "Victim") # the victim stands still and the attacker lines up on itpa.call_method("DeriveOffsetsFromMontages")save(pa)print(upc.validate_asset(pa))
DeriveOffsetsFromMontages places each role from where its montage puts it, for animations authored together; set the anchor role first. upc.validate_asset(pa) lists what the pairing editor would warn about (a role with no montage, an anchor role that does not exist, a travel setting that loses the montage's own motion), each line naming the setting to change.
From a combo: a move whose payload is UUPCPlayPairedActionPayload (Play Paired Action: SourcePAIRED_ACTION with PairedAction, OwnerRoleId, TargetRoleId, PrimaryPicker; or PAIRED_ACTION_SETS with PairedActionSets), reached by a transition with a Paired Action Available condition (UUPCPairedActionCondition, Actions) or a Target State condition requiring a stun tag (AgentGuide/ActionGraphs.md). For a move the stick aims, such as a vault, make the PrimaryPicker a Proximity Scan (unreal.UPCPairedTargetPicker_Scan: Radius, MaxAngleDegrees, bArcFollowsMovementInput on), and give the node an ON_FAILED transition to the move that plays when nobody stands in that direction, such as a roll.
A set of options: a Paired Action Set (UUPCPairedActionSet: GatherRadius, GatherArcDegrees, Options rows unreal.UPCPairedActionOptionAction, OwnerRoleId, TargetRoleId, PrimaryPicker, RequiredTargetTags, TargetTeams, Priority), which one button searches. The demo sword's PairedActionSet_UPC_SwordExecute executes only the demo's Soulslike boss in its finisher stun: its one row requires UPC.Enemy.SoulslikeBoss, which BP_UPC_Boss_Soulslike carries in its CombatComponentCharacterTags, and UPC.State.Finishable, which that stun holds. For one action with its own montages per enemy type, put a type tag of the project's own (Enemy.Type.Heavy) in the enemy Blueprint's CombatComponentCharacterTags, add one row per type with a higher Priority, that tag in RequiredTargetTags and the type's own paired action as Action, and keep one catch-all row last with no RequiredTargetTags, which every other enemy gets.
From the level: a UPC Paired Action Spot or UPC Paired Action Interactable component on an actor.
Check:upc.validate_asset(pa) is empty. In Play, UPC.Debug.Paired.Draw 1 draws each role's spot.
Offsets not derived or set; run DeriveOffsetsFromMontages or set each Offset.
It plays on the wrong body
The option's RequiredTargetTags or TargetTeams are too loose.
The damage never lands
No Deal Damage event, or its TimeSeconds is past the action's end.
The victim survives the kill
A large DamageOverride meets a guard, a resistance or a health floor that holds it back. Tick bKill on the Deal Damage event: it takes whatever health is left, and its HealthVitalTag names the row to empty when the victim's health row is not UPC.Vital.Health.
The victim dies too early
An earlier Deal Damage event takes its last health, or has bKill ticked. Set LeaveHealthAtLeast (1 is enough) on every Deal Damage event before the killing blow, and tick bKill on the killing blow only.
A role is empty after a reload
RoleId was set with set_editor_property; build the participant with the role_id keyword.
A role skips its opening step
Its montage starts inside its Data Driven glide, which throws that root motion away, and upc.validate_asset(pa) names the loss and the value to set. Set ApproachDurationSeconds to StartDelaySeconds minus RoleStartSeconds, or StartDelaySeconds to the glide's end (and PlayFromSeconds to the frame shown at the join, for a montage already playing then).
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; fill a struct row a plain field write refuses; add an instanced object (a fragment, a payload, a cost); edit a component on a Blueprint; give a montage sections; add a gameplay tag; check an asset; read the log.
obj.set_editor_property("Name", value) takes the C++ name (bIsMainWeapon) or the Python name (is_main_weapon). It refuses a property the editor shows read-only, and it refuses 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 come back as copies. Edit the list, then write the whole list back:
A struct you make in Python is not a template, so its class-defaults-only fields refuse set_editor_property. Several of this plugin's row structs are like that, among them unreal.UPCHoldPlaceSpec (Hold Places rows), unreal.UPCStowPlaceSpec (Stow Places rows), unreal.UPCContainerSpec (an inventory config's Containers rows), unreal.UPCCapacityBonus (a Capacity Bonus fragment's Bonuses), unreal.UPCWeightThreshold (a Weight Limit's Thresholds), unreal.UPCCategoryLimit (a Category Limit's Limits) and the ability set rows unreal.UPCAbilitySet_GameplayAbility and unreal.UPCAbilitySet_GameplayEffect; an interaction option's PayloadSpec refuses a field write too. A Combine Set's rows (unreal.UPCCombineRecipe, unreal.UPCCombineResult) take field writes. Write any row that refuses as text:
Read a shipped row with row.export_text() to see the text form of each field.
A struct read from an object is a copy: edit it, then write it back to the object with set_editor_property. Inside that copy, nested fields edit in place: s.state.applied_tags = tags("UPC.State.Blocking").
A call that answers yes or no and also fills a value (unreal.UPCInventoryStatics.can_combine, the inventory's get_item_placement and can_item_leave) hands back the filled values and not the yes or no: one value comes back alone, several as a tuple, and None stands for no. answer = unreal.UPCInventoryStatics.can_combine(inv, item, target) is None when the two do not combine, else (results, reason). A call that returns a number and fills a value gives both: units, reason = inv.get_units_that_fit(item, 1, False).
Item fragments, action graph payloads, costs, gates, hooks, transition conditions, hit effects, defense rules and camera layers are objects that live inside the asset holding them.
Make one with unreal.new_object(cls, outer=<the asset that holds it>), set its properties, put it in the owner's list, then save the asset. The lists in this plugin: an item's Fragments, an inventory container's Rules, an inventory config's CarryRules and a Combine Set's Combines. Write the whole list back with set_editor_property: a list read from the asset is a copy, so adding to it without writing it back changes nothing and raises nothing.
An object made without outer= belongs to no asset: its properties refuse a write ("cannot be edited on instances").
Exception: for upc.set_node_settings and upc.set_transition_settings, make it with unreal.new_object(cls) and no outer; the call copies it into the graph.
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 (every component AUPCCharacterBase makes: EquipmentManagerComponent, InventoryComponent, ActionGraphComponent, MeleeTraceComponent, InputMappingComponent, AnimManagerComponent, VitalsComponent, DefenseComponent, TargetingComponent, TargetableComponent, TeamComponent, ApproachComponent, CombatComponent, InteractorComponent, AnimEffectComponent, AbilitySystemComponent, and the character's own CapsuleComponent, CharacterMovement and Mesh) is read from the class defaults: defaults(bp).get_editor_property("VitalsComponent"). Edit it there and save the Blueprint.
A component the Blueprint adds (sprint, camera rig, speed modifier, stow visuals on the demo player) lives in its construction script. Read its template:
sds = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem)handles = sds.k2_gather_subobject_data_for_blueprint(bp)for h in handles: data = unreal.SubobjectDataBlueprintFunctionLibrary.get_data(h) obj = unreal.SubobjectDataBlueprintFunctionLibrary.get_object_for_blueprint(data, bp) if isinstance(obj, unreal.UPCSprintComponent): sprint = obj
Edit sprint, then unreal.BlueprintEditorLibrary.compile_blueprint(bp) and save(bp).
It replaces every section and saves the montage. On Epic's MCP it takes three lists in place of the tuples: section names, start seconds, and next sections (empty, or one per section). Each section ends the montage, so a move whose payload names a section (SectionName on unreal.UPCMontageActionPayload) plays only that part. A third value names the section it plays on into instead, or its own name to loop it, which is how a held guard keeps its pose: [("Raise", 0.0, "Hold"), ("Hold", 0.4, "Hold")].
A tag must exist before a property can hold it (tag("UPC.Input.Kick") gives an empty tag otherwise). The plugin's input, state, vital, place and cooldown tags exist already. To add one:
upc.validate_asset(asset) runs the checks the editor's Validate Assets runs (item definitions, damage configs, combat styles, targeting configs, defense rule sets, paired actions, camera shots) and returns one line per problem.
For an action graph, upc.compile_action_graph(graph) is the check.
After Play, log_warnings() returns UPC's warnings; each names the asset and the fix.
AGENTS.md, Recipe 5, sets up lock-on. This file covers how attacks turn to and close on the target, combat styles, targeting configs and what can be targeted.
Tasks: attacks that snap to the target and close the gap; an attack that steps forward with nothing targeted; a lunge that lands on the target; souls-like strafing when locked on; lock range and target switching; making an actor targetable; snappier movement in combat.
The demo's unarmed combo puts a UPC Approach Or Move Window (UUPCApproachOrMoveWindowNotifyState) over each attack's wind-up: with a target it turns the player to it and glides into reach, and with none it steps forward. Put it before the attack's trace window:
for key in ("Light1", "Light2", "Light3", "Heavy"): m, L = am[key], length(am[key]) a = upc.add_notify_state_to_montage(m, unreal.UPCApproachOrMoveWindowNotifyState, L * 0.05, L * 0.25) a.set_editor_property("StopDistanceOverride", 25.0) # cm left between the bodies: about 25 for fists, 50 for a blade a.set_editor_property("MoveDistance", 60.0) # cm stepped forward when nothing is targeted save(m)
The target is the character's soft target or lock (the targeting config below); the duplicated player already picks one.
StopDistanceOverride -1 uses the item's AttackStopDistance, then the style's stop distance. A move's own StopDistance on its Play Montage node takes over from it (below). The gap must be shorter than the attack reaches past the player's body, or the swing stops short.
A plain UPC Approach Window (UUPCApproachWindowNotifyState) does the same without the step when nothing is targeted. Its Mode (unreal.UPCApproachMode: MOVE_AND_FACE, FACE_ONLY, MOVE_ONLY) picks turning, moving or both; bMoveByOffset with OffsetDirection and OffsetDistance moves a set distance instead (a step back: direction (-1, 0, 0)).
Check:unreal.AnimationLibrary.get_animation_notify_events(am["Light1"]) lists the window before the trace window. In Play, UPC.Debug.Steering.Draw 1 draws where each attack aims and stops; an attack started a few meters from an enemy glides to it and lands.
Root-motion attacks that travel by their own animation land on the target with the engine's Motion Warping window and the UPC Skew Warp modifier, as the demo sword does:
w = upc.add_notify_state_to_montage(m, unreal.AnimNotifyState_MotionWarping, L * 0.1, L * 0.3)mod = unreal.new_object(unreal.UPCRootMotionModifier_SkewWarp, outer=w)mod.set_editor_property("WarpTargetName", "UPCTarget")mod.set_editor_property("StopDistanceOverride", 50.0)w.set_editor_property("RootMotionModifier", mod)save(m)
A dash that carries the player into reach uses UUPCRootMotionModifier_GapCloser (UPC Gap Closer Warp) the same way. The duplicated player has the Motion Warping component.
The character's UPC Attack Approach component (UUPCAttackApproachComponent, ApproachComponent on the duplicated player) aims and steers attacks. Another character takes this one beside a UPC Targeting component (AgentGuide/Python.md, Components on a Blueprint), never its base UUPCApproachComponent, whose attacks neither aim nor keep their distance. The attack approach's settings: SteeringMode (unreal.UPCAttackSteering: OFF, TOWARD_TARGET, DISTANCE_ONLY), AttackFacing (unreal.UPCAttackFacing: NONE, AIM, MOVEMENT, LOCK_TARGET), KeepDistance (ON stops attacks the stop distance short of the target and of enemies in their path), MakeRoomSeconds (how long an attack that starts closer than the stop distance takes to step back to it; 0 turns it off; a move's own MakeRoomSeconds takes over), DefaultStopDistance, DefaultMaxApproachDistance, WarpTargetName (UPCTarget). The combat style sets most of these as it applies, so change them on the style.
Which stop distance an attack keeps, top wins, -1 passing to the next:
Level
Set on
The move
StopDistance on its Play Montage node (UUPCMontageActionPayload)
A window, advanced
StopDistanceOverride on the UPC Approach Window or UPC Skew Warp playing at that moment
The weapon
AttackStopDistance on the drawn weapon's Equippable fragment
The style
StopDistance on its Approach fragment (DefaultStopDistance on the component)
The move's MakeRoomSeconds works the same way against the style's: -1 follows the style, 0 turns the step back off for that move, above 0 turns it on. Set the move's own values for a short move beside a long weapon, such as a shield bash with a sword drawn. With UPC.Debug.Approach 1 each attack logs its stop distance and where it came from.
A Combat Style (UUPCCombatStyleAsset, Fragments) on the character's CombatComponent (Style) sets rotation, targeting, steering, approach and camera together. The demo ships CombatStyle_UPC_OverShoulder (the duplicate's), CombatStyle_UPC_Soulslike and CombatStyle_UPC_Freeflow under /UniversalPlayerCombat/Demo/Combat/Styles/; duplicate one to change it.
UUPCTargetingConfig: ScanRadius (cm), TargetTeams (Enemies Only by default), bAllowLock, LockBreakDistance (0 never breaks), LockBreakGraceSeconds, ScoreSwitchMargin, ScorePolicies and LockScorePolicies (UUPCTargetScorePolicy_Distance, _ViewAngle, _InputDirection, _AimDirection, _LineOfSight), SwitchInputAction (the stick that moves a lock), EngagedRadius, EngagedTag. Duplicate TargetingConfig_UPC_Soulslike (beside the styles) and point a duplicated style's Targeting fragment at it.
Targetable actors: a UPC Targetable component (UUPCTargetableComponent: AimAtSocket, AimAtComponentTag, AimOffset). Every character built on the UPC character base has one; add it to any other actor (AgentGuide/Python.md, Components on a Blueprint).