mit einem Klick
unreal-bridge
// Execute Python scripts inside a running Unreal Engine 5.3+ editor via TCP bridge. Use when the user asks to interact with UE, manipulate assets, query scenes, automate workflows, or run Python in Unreal.
// Execute Python scripts inside a running Unreal Engine 5.3+ editor via TCP bridge. Use when the user asks to interact with UE, manipulate assets, query scenes, automate workflows, or run Python in Unreal.
| name | unreal-bridge |
| description | Execute Python scripts inside a running Unreal Engine 5.3+ editor via TCP bridge. Use when the user asks to interact with UE, manipulate assets, query scenes, automate workflows, or run Python in Unreal. |
| allowed-tools | Bash Read Write Edit Glob Grep Monitor |
Execute Python directly inside a running UE 5.3+ editor. Auto-discovers via UDP multicast (239.255.42.99:9876); the TCP data port is OS-assigned per editor.
If bridge.py returns discovery: no UnrealBridge editors found, walk these in order — don't troubleshoot Python or firewalls first:
<UEProject>/Plugins/UnrealBridge/ (use sync_plugin.bat from this repo; user must edit its DST= once). Check: <UEProject>/Plugins/UnrealBridge/UnrealBridge.uplugin exists.<UEProject>/<Project>.uproject "Plugins" block for {"Name":"UnrealBridge", "Enabled":false} and flip if present.bridge.py ping returns "ready": true. false means MainFrame still loading; wait 10–60s.Last resort if multicast is blocked (corp VPN, virtual NIC): --endpoint=127.0.0.1:<port> from the editor log line LogUnrealBridge: Listening on 127.0.0.1:<port>. Python 3.7+ stdlib only.
After launching/relaunching the editor, poll readiness with Monitor
(streams progress events, recommended) or Bash with run_in_background: true (one completion event). Don't write a foreground for/sleep
countdown — the harness blocks long leading sleeps and you'll miss readiness.
Paste this verbatim as the command for either tool:
end=$(( $(date +%s) + 300 ))
i=0
until python .claude/skills/unreal-bridge/scripts/bridge.py --json --timeout 3 ping 2>/dev/null | grep -q '"ready": *true'; do
now=$(date +%s)
[ "$now" -ge "$end" ] && { echo "[wait] TIMEOUT after 300s"; exit 1; }
i=$((i + 1))
[ $((i % 3)) -eq 0 ] && echo "[wait] still booting ($((end - now))s left)"
sleep 10
done
echo "[wait] READY"
Then call one of:
Monitor(description="UE editor ready-poll", command=<block above>,
timeout_ms=360000, persistent=false)
Bash(description="Wait for UE editor ready", command=<block above>,
run_in_background=true, timeout=360000)
Locks (don't change without reading):
"ready": *true — TCP-up ≠ MainFrame-ready. success:true, ready:false
means exec calls will be rejected; ping success alone is not enough.i % 3 echo gate — caps notifications at ~10 over 5 min so Monitor doesn't
trip its event-flood auto-stop.end deadline must stay below the tool timeout_ms/timeout so you
get [wait] TIMEOUT rather than a silent harness kill.${CLAUDE_SKILL_DIR} is not reliably set
in Monitor/Bash subshells — don't substitute it here.bridge.py ping before real
work — bg success is past tense.--project=<name> to the ping inside the loop.rebuild_relaunch.py — it polls
internally and prints [rebuild] bridge is ready. on success.python "${CLAUDE_SKILL_DIR}/scripts/bridge.py" [options] <command> [args]
| Command | Purpose |
|---|---|
ping | Check UE connection (TCP-only, doesn't touch GameThread) |
exec "<code>" | Execute single inline statement |
exec --stdin <<'EOF' ... EOF | Multi-line script from stdin (default for >1 line; - is shorthand for --stdin) |
exec-file <path> | Execute a .py file (use when iterating, debugging, or keeping the script) |
preflight <path> | Lint a script for bridge-call errors WITHOUT sending to UE |
suggest [pattern] | Look up the bridge equivalent for a raw unreal.* fallback |
gamethread-ping | Probe GameThread liveness (bypasses exec queue; use when exec hangs) |
resume | Unstick a paused BP breakpoint |
list-editors | Print every editor that responded to a discovery probe |
wait-compile <material> / wait-pose-index <psd> | Client-side polling helpers |
Optional flags: --project=<name|path> (disambiguate when >1 editors run; or env UNREAL_BRIDGE_PROJECT), --endpoint=host:port, --token=<secret>, --timeout=<s>, --json, --no-preflight.
exec --stdin heredoc, NOT exec-file. A heredoc is the right mode for ~95% of one-shot work — no temp file to name, no cleanup, prompt stays self-contained, no risk of dangling scripts in $TEMP / .tmp / project root. Only reach for exec-file when you genuinely intend to re-run the same script multiple times (iterating on a fix, comparing runs). One-shots like "find X, list Y, build a report" → heredoc. If you find yourself writing with open("/tmp/foo.py", "w") followed by bridge.py exec-file /tmp/foo.py, stop and rewrite as a heredoc.--json for parseable output.0 success · 1 runtime/transport · 2 bad CLI args · 3 AST preflight rejected.exec hangs: from a separate terminal try gamethread-ping (high latency = GT mid-exec, queue will drain) or resume (BP breakpoint).Multi-line example:
python "${CLAUDE_SKILL_DIR}/scripts/bridge.py" exec --stdin <<'EOF'
from unreal_bridge import Asset
paths, _ = Asset.search_assets_in_all_content(query="Hero", max_results=5)
for p in paths:
print(p.export_text())
EOF
Windows path gotcha (Bash tool only): unquoted backslashes get eaten by bash word-splitting — python G:\Claude\UnrealBridge\.claude\skills\unreal-bridge\scripts\bridge.py exec ... arrives as G:ClaudeUnrealBridge.claudeskillsunreal-bridgescriptsbridge.py and fails with "No such file or directory". Use forward slashes (G:/Claude/...) or single-quote the path ('G:\Claude\...'). The body inside <<'EOF' ... EOF is literal and unaffected — only the arg before the heredoc matters. PowerShell tool is unaffected.
from unreal_bridge import Asset, Level, Blueprint, Editor, Anim, Material, PoseSearch, Chooser, ...
paths, _ = Asset.search_assets_in_all_content(query="Hero", max_results=20)
The wrapper has 21 classes (one per UnrealBridge*Library) with kwargs-only signatures — positional args raise TypeError immediately, no UE round-trip. This is the structural fix for positional-arg-order hallucinations. Regenerate after C++ header changes via python tools/gen_manifest.py.
Fallback: raw unreal.UnrealBridge*Library.foo(...) works (preflight catches errors), but prefer the wrapper.
Every exec* call runs an AST preflight in the bridge client BEFORE sending. Reads scripts/bridge_manifest.json and rejects locally (exit 3, no UE round-trip) when:
unreal.BridgeXxx.YYY enum member doesn't existType validation is NOT done by preflight. Wrong asset paths / wrong scope still surface at runtime — references prose still matters for those.
Treat preflight as a backstop, not a planning tool. If you write code expecting preflight to autocorrect your guesses, you'll produce noisy output and still hit runtime tracebacks for the half it can't catch (USTRUCT field names, Python attribute access, EditDefaultsOnly violations). See "Verify before you call" below.
Bypass with --no-preflight (rare). Preview with bridge.py preflight <path>.
| Symptom | Fix |
|---|---|
Bridge call returns [] for an asset you know exists | Wrong scope: PROJECT covers /Game only; plugin assets need ALL_ASSETS. Use Asset.search_assets_in_all_content(...). |
get_derived_classes hangs / huge results | Don't pass UObject / AActor — narrow to most specific base. |
| Multi-step BP edit feels chatty | Batch with exec --stdin heredoc or exec-file, not 3 inline exec calls. |
| Pawn movement script freezes the editor | time.sleep inside exec blocks GameThread — see bridge-gameplay-api.md "chase a target" pattern (use register_runtime_timer). |
print('中文' / '한글' / '日本語') shows ��� or 涓枃 mojibake | Almost always display-only — the wire is byte-perfect UTF-8. See "Non-ASCII output (CJK / Greek / emoji)" below. |
| Need "where is this GameplayTag used?" / Find References on a tag | unreal.UnrealBridgeGameplayTagLibrary.find_assets_referencing_tag(tag, include_children, ...). Mutations: add_gameplay_tag / rename_gameplay_tag (auto-redirect) / remove_gameplay_tag; pick the target ini via list_tag_source_inis(...). For PrimaryAssetId / other named-value structs use the generic UnrealBridgeAssetLibrary.find_assets_referencing_searchable_name(struct_type, value, ...). See bridge-gameplaytag-api.md. |
Preflight is a safety net, not a planning tool. Writing code from imagined API / field / attribute names and waiting for preflight to tell you the right one produces noisy output (4-8 [WARN] per script, often still ending in a runtime traceback) and wastes round-trips. Verify FIRST, then write the script. Four axes:
grep scripts/bridge_manifest.json or read the relevant references/bridge-*-api.md. Don't translate the C++ name into snake_case in your head and assume.BridgeGameplayEffectInfo keep a minimum set of fields (not exhaustive); UE 5.x evolves struct shape between versions. Look up the fields in bridge_manifest.json structs section, OR run a one-off probe print([a for a in dir(obj) if not a.startswith('_')]) BEFORE writing the consumer code.<obj>.<attr>. Always get_editor_property (and respect protected: / EditDefaultsOnly rejection). Use Level.list_actor_properties(actor_name=...) or Level.list_class_properties(class_path=...) to list first.unreal.UnrealBridgeXxx.fn.__doc__ first line is the signature. Cheaper than a guess + correction round-trip.Symptom you're in the bad loop: your last few exec attempts produced [WARN] lines from preflight followed by AttributeError tracebacks. Stop, run one heredoc with dir() / manifest grep, then write the real script.
unreal.*If a bridge call returns empty / errors, suspect your parameters (wrong scope / path / filter), not the wrapper. Falling back to unreal.AssetRegistryHelpers etc. bypasses preflight, audit logging, and often walks 100k–2M registry entries.
Preflight emits [WARN] on known raw-fallback patterns and shows the bridge equivalent inline. To look up before writing: bridge.py suggest [pattern]. Patterns currently mapped:
| Raw pattern | Bridge equivalent |
|---|---|
ar.get_assets_by_path(...) | Asset.list_assets_under_path(folder_path, include_subfolders) |
ar.get_referencers(...) | Asset.get_package_referencers(package_name, hard_only) |
ar.get_dependencies(...) | Asset.get_package_dependencies(package_name, hard_only) |
unreal.GameplayStatics.get_all_actors_of_class(...) | Level.find_actors_by_class(class_path, max_results) |
unreal.GameplayStatics.get_all_actors_with_tag(...) | Level.find_actors_by_tag(tag) |
If you genuinely believe no bridge function covers your case, ask the user before reaching for raw unreal.*.
Signatures are now mechanically enforced (preflight). References carry semantic traps, scope warnings, and workflow patterns preflight can't catch.
| Topic | File | When to read |
|---|---|---|
| Blueprint queries + authoring | references/bridge-blueprint-api.md | Class hierarchy, variables/functions/components, node search, write ops, auto-layout flow, lint loop |
| Asset queries | references/bridge-asset-api.md | Asset lookup, search, references/dependencies, SoftObjectPath stringification (top-of-file block) |
| UMG / Widget | references/bridge-umg-api.md | Widget Blueprint hierarchy/tree |
| Animation | references/bridge-anim-api.md | ABP state machines, slots, sequences, montages, blend spaces. Authoring an ABP? Read the "Authoring an Animation Blueprint (agent workflow)" section first. |
| DataTable | references/bridge-datatable-api.md | Schema, rows, fields, search, CSV |
| Material | references/bridge-material-api.md | Material instance parameters |
| Level / Actor | references/bridge-level-api.md | Level queries, spawn/destroy/move, property get/set, selection |
| Editor session | references/bridge-editor-api.md | Asset open/save, viewport camera, PIE start/stop, console/CVars, BP compile |
| GameplayAbility | references/bridge-gameplayability-api.md | GA Blueprint metadata |
| Generic UPROPERTY read/write (privileged) | references/bridge-property-api.md | When get_editor_property rejects with "protected" / set_editor_property rejects EditDefaultsOnly sub-fields / you need to read private: / bare UPROPERTY() / write nested struct values via export-text. list_u_properties / get_u_property_as_export_text(path, "Modifiers[0].Foo.Bar") / set_u_property_from_export_text (with fire_change_notify for editor refresh) / array_append_u_property (auto-handles FGameplayTagContainer ParentTags) / array_remove_u_property / array_clear_u_property / get_asset_cdo_path. Privileged — bypasses UE editor safety net. Use dedicated APIs (add_gameplay_tag, add_ge_modifier_scalable) when they exist. |
| GameplayTag references / sources / mutations | references/bridge-gameplaytag-api.md | Read: find_assets_referencing_tag (with child-tag expansion), list_all_registered_tags, get_tag_source_info. Write: add_gameplay_tag(tag, source_ini='', comment=''), rename_gameplay_tag(old, new, rename_children=True) (auto-inserts redirect, redirect lifetime hardened — survives subsequent mutations + cold restart), remove_gameplay_tag(tag). Source enum: list_tag_source_inis(filter_type=''). Redirect ops: list_gameplay_tag_redirects(source='', old_prefix='') + remove_gameplay_tag_redirect(old, new) for enumerate-then-sweep. Built on the AssetRegistry SearchableName index + IGameplayTagsEditorModule. bridge-asset-api.md "SearchableName Index" has the generic read version (PrimaryAssetId, custom named-value structs). |
| Motion Matching — PoseSearch | references/bridge-pose-search-api.md | Read before any PSS / PSD read or write — DatabaseAnimationAssets / Channels are private: and unreachable via get_editor_property; this lib is the only path. Includes wait-pose-index CLI. |
| Motion Matching — Chooser | references/bridge-chooser-api.md | Read before any CHT read or write — ResultsStructs / DisabledRows etc. are private:; this lib is the only path. Covers NestedChooser :Name paths, matched_row=-1 caveat, and the auto Compile+PostEditChange contract. |
| Reactive handlers | references/bridge-reactive.md | Register Python on UE events (GameplayEvent / AnimNotify / MovementMode / Attribute / ActorLifecycle / InputAction). |
| Navigation | references/bridge-navigation-api.md | NavMesh OBJ export |
| Perf snapshots | references/bridge-perf-api.md | Structured FPS / GT / RT / GPU / draw calls / mem / UObject histogram |
| Agent / Gameplay | references/bridge-gameplay-api.md | Mandatory before driving the player pawn — sticky inputs, camera steering, navmesh path planning. |
| UE Asset / Actor / Material | references/ue-python-*.md | Stdlib UE Python helpers (load, list, duplicate, spawn) |
Pawn control is non-obvious — read
bridge-gameplay-api.mdfirst. Driving the player (stickyIA_Move,apply_look_input, navigating to a moving target, holding an input over time) has hard constraints a fresh API read won't reveal:bridge.execruns on GameThread sotime.sleepinside oneexecfreezes the engine and stops the sticky ticker; continuous steering must run from a reactiveregister_runtime_timercallback, not a Pythonwhileloop;IA_Moveis camera-relative and the forward-axis convention varies per project. The "Pattern: chase a (possibly moving) target and stop on arrival" section has the working template.
Asset lookup by name defaults to
search_assets_in_all_content(name, max_results). When the user names an asset without a path, do not callunreal.AssetRegistryHelpers.get_assets_by_path('/Game', recursive=True)and filter — that walks 100k–2M+ entries and times out. The fullsearch_assetsform needsBridgeAssetSearchScope.ALL_ASSETS(notPROJECT) when the asset might live in a plugin mount (/PluginName/...).PROJECTcovers/Gameonly; using it for a plugin asset returns[]silently. Valid scope members:ALL_ASSETS,PROJECT,CUSTOM_PACKAGE_PATH— there is noGAME_FOLDER.
非必要不写蓝图 Node 和 Graph. Agent-authored BPs are visibly weaker than C++ on readability/maintainability/perf. Default response to "用蓝图实现 X":
UFUNCTION(BlueprintCallable) with thin BP wrapper, or another bridge surface (Material / Anim / UMG / DataTable / Level).Exceptions (skip the dance):
set_blueprint_variable_default etc.) without new nodesCLAUDE.md or current conversationHard line: any op spawning/connecting/removing nodes, adding events, or creating functions needs the confirmation dance.
After authoring/modifying a BP graph (and the user confirmed they want it):
1. plan — list events / functions / local vars in text first
2. build — add_*_node / connect_graph_pins / add_blueprint_variable …
3. auto_layout — MANDATORY after ANY graph mutation; use 'pin_aligned' (3-exec flow) or 'exec_flow' (bulk).
See bridge-blueprint-api.md for the exact open→sleep→layout sequence.
4. lint — lint_blueprint(...) and resolve every warning/error
5. collapse — collapse_nodes_to_function on any LongExecChain finding
6. straighten — straighten_exec_chain on main rails (after 'exec_flow' only)
7. comment — add_comment_box + set_comment_box_color per section (>10 nodes = mandatory)
8. compile — compile_blueprints clean
UnnamedCustomEvent / CustomEvent_0 to intent (OnHealthChanged)."1. Validate inputs", "2. Apply damage") and an appropriate preset color (Section, Validation, Danger, Network, UI, Debug, Setup).auto_layout_graph(... 'pin_aligned' ...) uses DFS-ordered leaves + downstream-driven Y alignment + live Slate geometry — matches hand-laid BP shape (.then rails horizontal, siblings stacked in one column). Requires the three-exec flow (open graph → sleep → layout) to read live widgets. Use 'exec_flow' (Sugiyama-lite, layer-center Y) for bulk tidy on large graphs where pixel-accuracy matters less, then follow with straighten_exec_chain on the main rail.pin_aligned treats h_spacing as a loose upper bound and internally uses DataHSpace = max(15, h/3) and ExecGap = max(30, h/2) — so h_spacing=100 renders tighter than the raw number suggests. Pass smaller values (40, 32) for max density.predict_node_size for each kind first so X offsets don't overlap.auto_layout_graph runs, Slate widgets don't refresh NodePosX/Y until they tick — so get_rendered_node_info returns pre-layout pin coords in the same exec. For crossing-detection / wire-length audits right after layout, read get_node_layout(bp, fn, guid).pos_* (authoritative from node model) and estimate pin Y as pos_y + 40 + 22 × dir_index. Only pay the open-graph + sleep cost when you specifically need Slate-accurate coords.auto_insert_reroutes is intentionally NOT in the loop — empirically it produces many-knot routing that reads worse than the original. Opt in per-graph if a specific case needs it.Bridge is UTF-8 byte-perfect end-to-end since 2026-05-04. Mojibake = display, not data. Confirm with hex:
print(got.encode('utf-8').hex(), '==', '测试'.encode('utf-8').hex())
If hex matches, it's a Windows cp936/cp1252 console issue — fix the display, not the bridge:
| Situation | Fix |
|---|---|
Piping bridge.py ... > out.txt writes cp936 | prefix PYTHONIOENCODING=utf-8 (bash) / $env:PYTHONIOENCODING='utf-8' + | Out-File -Encoding utf8 (PS) |
| Reading saved file back | open(p, encoding='utf-8') (or utf-8-sig for PS BOM) |
| Calling bridge.py from another Python process | set PYTHONIOENCODING=utf-8 in subprocess env |
Verified: identifiers + literals in script source, exception messages/tracebacks, DataTable FName keys, asset paths with Chinese folders, save→restart→read persistence. Korean/Japanese use the same UTF-8 path — verify with hex if anything looks off.
Never write temp files to C:\ root — use $env:TEMP\… or /tmp/….
unreal.ScopedEditorTransaction.exec calls.print() returns to bridge; unreal.log() only to UE Output Log.import json; print(json.dumps(...)) with --json.ping fails: editor not running, plugin not enabled, or wrong project (preconditions above).--timeout 120.