--- name: umg-widgets display_name: UMG Widget Blueprints description: Create and modify UMG Widget Blueprints (UI) β€” build the widget hierarchy, set properties, style fonts/brushes, author widget animations, bind events, capture previews, run PIE checks, and wire MVVM ViewModels. Use when the user asks to create a UI/HUD/menu, add or arrange widgets (Button, TextBlock, Image, panels), style or animate a widget, or set up data bindings in a Widget Blueprint (WBP). vibeue_classes: - WidgetService - BlueprintService unreal_classes: - EditorAssetLibrary keywords: - umg - widget - ui - hud - menu - canvas - button - textblock - panel - viewmodel - mvvm - binding - animation related_skills: - blueprints - blueprint-graphs --- > 🧠 **Brains complement:** IF an `unreal-engine-skills-manager` tool (external MCP) exists in this session, call it with `{action: "load", skill: "umg-and-slate"}` for UE domain knowledge on this topic β€” correct APIs, architecture, best practices β€” and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone β€” do NOT attempt the call. # UMG Widget Blueprints Skill This skill covers authoring **Widget Blueprints (WBP)** with `WidgetService`. Read the Critical Rules below first β€” they prevent the most common failures. Then pick a task from the Task Index and load the matching workflow doc and/or run the matching sample script. > Load the **`blueprints`** skill when you also need to add Blueprint variables, functions, or > components, and **`blueprint-graphs`** when wiring event-graph nodes for bound events. ## Critical Rules ### 🚨 Creating Widget Blueprints β€” only one pattern works `WidgetService` does **not** create the asset. Create it with the factory, then use `WidgetService` to populate it: ```python import unreal factory = unreal.WidgetBlueprintFactory() factory.set_editor_property("parent_class", unreal.UserWidget) asset_tools = unreal.AssetToolsHelpers.get_asset_tools() asset_tools.create_asset("MainMenu", "/Game/UI", unreal.WidgetBlueprint, factory) ``` Do **not** call `BlueprintEditorLibrary.get_blueprint_class()`, `unreal.create_widget_blueprint()`, or `WidgetService.create_widget()` β€” none exist. (Runnable: `scripts/create_widget.txt`.) ### 🚨 Hierarchy: set the root first, parents before children ```python unreal.WidgetService.add_component(path, "CanvasPanel", "RootCanvas", "", True) # empty parent -> becomes root unreal.WidgetService.add_component(path, "Button", "PlayButton", "RootCanvas", False) ``` That 5th argument is `is_variable`, **not** `set_as_root` β€” a widget becomes the root because its parent name is empty, not because of that flag. A 6th argument, `child_index`, inserts at a position instead of appending; `reorder_component` moves an existing widget within its parent. See `reference.md` β–Έ Child order. > ⚠️ **Widget names are unique per-blueprint, NOT per-parent.** UMG enforces one name across the > whole Widget Blueprint, so you can't add an `ItemLabel`/`ItemButton` under each of `Item1`, > `Item2`, `Item3`. Suffix per-parent (`Item1Label`, `Item2Label`, …) β€” reusing a name silently > lands the component on the wrong parent or no-ops. ### 🚨 Property values are ALWAYS strings ```python unreal.WidgetService.set_property(path, "PlayText", "Font.Size", "24") # "24", not 24 ``` ### 🚨 Color properties: FSlateColor vs FLinearColor β€” verify with a readback `ColorAndOpacity` on text/image widgets is **FSlateColor**, whose export text is `"(SpecifiedColor=(R=1.0,G=0.5,B=0.0,A=1.0))"` β€” a bare LinearColor string is auto-wrapped into that form by `set_property`. `ShadowColorAndOpacity` is a plain **FLinearColor**: `"(R=0.0,G=0.0,B=0.0,A=1.0)"`. Named color strings ("red", "orange") are NOT supported on either β€” pass numeric RGBA. **Always `get_property` after `set_property` and compare.** A struct set can return `True` while writing nothing (UE struct import ignores unrecognized member names). If the readback still shows the old value, your value string didn't match the struct's member layout β€” check `list_properties` for the property's real type and use its export-text form. ### 🚨 Use dedicated style APIs for full font/brush edits Use `set_font` / `set_brush` (with `WidgetFontInfo` / `WidgetBrushInfo`) when changing a complete font or brush β€” they keep related fields together. Use `set_property` only for single leaf values or slot aliases like `Position X`, `Size Y`, `Anchor Min X`. See `reference.md` for every field name; runnable examples in `scripts/apply_font.txt` and `scripts/apply_brush.txt`. `set_brush` requires a **`slot_name`**: `set_brush(widget_path, component_name, slot_name, brush_info)` (for an Image the slot is `"Brush"`). `set_font` takes an optional `property_name` that defaults to `"Font"`. > ⚠️ **`set_property("Font.Typeface", ...)` silently returns False** (size and color leaf-sets work, > but typeface/family do not go through the struct sub-property path). Always change typeface/family > via `set_font` with a `WidgetFontInfo`. Don't trust the `True/False` return of `set_property` on > nested struct sub-fields β€” read it back to confirm. > βœ… **`set_font` colour is now applied** (`ColorAndOpacity` is an `FSlateColor`; the old path > silently no-op'd a bare `(R=,G=,B=,A=)` tuple). Pass a LINEAR tuple, e.g. `(R=0.035,G=0.002,B=0.002,A=1)`; > `set_font` returns **False** and logs a Warning if a supplied colour did not land, so trust the return. > ⚠️ **`get_font` readback quirk (issue #470):** the struct-typed string fields `color`, > `shadow_color`, and `shadow_offset` come back as **two concatenated representations** glued > together, e.g. `shadow_offset == "(X=0.0,Y=0.0)(X=1.000000,Y=1.000000)"`. The *trailing* > parenthesized group is the real value. Scalar fields (`size`, `typeface`, `font_family`, > `letter_spacing`, `outline_size`) are clean. When verifying a font round-trip, parse the last > `(...)` group of these three fields (or compare scalars only) until #470 is fixed. ### 🚨 Override-gated properties (SizeBox width/height etc.) `set_editor_property("width_override", 34.0)` on a `USizeBox` stores the value but leaves the paired `bOverride_WidthOverride` edit-condition flag false β€” the override silently never applies (the value even reads back as 34.0). Call the dedicated setters (`set_width_override()`, `set_height_override()`, `set_min_desired_width()`, ...) which set flag + value together. Applies to any widget property gated by a `bOverride_*` flag; after a raw property set, read the flag back before trusting the layout. ### 🚨 Animations require real property paths `add_animation_track` / `add_keyframe` target actual widget properties or slot aliases (`RenderOpacity`, `ColorAndOpacity`, `Position X/Y`, `Size X/Y`). Always create the animation first, then the track, then keyframes. (Runnable: `scripts/create_animation.txt`.) ### 🚨 Preview vs PIE β€” different purposes - `capture_preview` renders an editor-side PNG without starting gameplay β€” use for appearance checks. It now renders with a single gamma pass (an sRGB PNG matching the designer, no more double-gamma) and a layout prepass (Overlay-centred content is centred, not bottom-aligned). - `start_pie` + `spawn_widget_in_pie` are for runtime state / live property reads β€” use only when you need a live instance. ### 🚨 Custom WBPs as components β€” discover first, pass the asset name Use `list_widget_blueprints("")` to find existing WBPs, then pass just the asset name (e.g. `"WBP_HealthBar"`, not the full package path) as `component_type`. The child WBP must already exist; circular references are rejected; the parent recompiles automatically. ### 🚨 Field-name gotchas (return objects) | WRONG | CORRECT | |-------|---------| | `p.name` | `p.property_name` | | `p.type` | `p.property_type` | | `p.property_value` | `p.current_value` | | `w.component_name` / `w.name` | `w.widget_name` | | `w.widget_type` | `w.widget_class` | `WidgetInfo` complete fields: `widget_name`, `widget_class`, `parent_widget`, `is_root_widget`, `is_variable`, `children`. **`children` is an `Array[str]` of widget NAMES, not nested objects.** Iterating it and reading `.widget_name`/`.is_variable` raises `AttributeError: 'str' object has no attribute ...`. To walk the tree, build a nameβ†’info map first: ```python infos = {w.widget_name: w for w in unreal.WidgetService.get_hierarchy(path)} for child_name in infos["RootCanvas"].children: child = infos[child_name] # look the child up by name print(child.widget_name, child.widget_class, child.is_variable) ``` `get_widget_snapshot(path)` returns full data (hierarchy + slot + all properties) for a valid path β€” if it comes back **empty, the path is wrong**; do not conclude the widget is empty. Full field tables for every return type are in `reference.md`. ### 🚨 Auditing widgets (best-practices review) β€” use these exact APIs For "do our widgets follow best practices?" style audits: - **Property bindings** (the #1 anti-pattern): in a snapshot's `properties`, any `*Delegate` entry (e.g. `VisibilityDelegate`, `TextDelegate`) with `current_value` of `(null).None` is **unbound** β€” a function name there means a polling property binding exists. - **Tick/graph checks** go through `BlueprintService` (a WBP is a Blueprint): `list_graphs(path)` β†’ `get_nodes_in_graph(path, "EventGraph")` β†’ find `Event Tick` β†’ `get_node_pins(path, graph, node_id)` and check each pin's `connected` flag. A Tick node with no connected pins is just the default stub, not live logic. - These methods do **NOT** exist β€” do not guess them: `BlueprintService.get_graphs`, `get_functions`, `get_connected_nodes`, `unreal.find_class`. Use `list_graphs`, `list_functions`, `get_nodes_in_graph`, `get_node_details` (includes pin connections), and `unreal.load_class(None, path)` instead. ### Slot editing via `set_property` `set_property` edits slot layout through these aliases (string values): - **Canvas children:** `Position X/Y`, `Size X/Y`, `Anchor Min X/Y`, `Anchor Max X/Y`, `Alignment X/Y` (pivot β€” set to `0.5` with anchors at `0.5` to truly center), `ZOrder` (or `Z Order`). - **Box/Overlay children** (VerticalBox/HorizontalBox/Overlay): `Horizontal Alignment` / `Vertical Alignment` (values `Fill`/`Left`/`Center`/`Right`/`Top`/`Bottom`), `Padding` (one value or `(Left=..,Top=..,Right=..,Bottom=..)`), `Padding Left/Top/Right/Bottom`, and on box slots `Size Rule` (`Fill`/`Automatic`) + `Size Value`. ```python unreal.WidgetService.set_property(path, "PlayButton", "ZOrder", "5") unreal.WidgetService.set_property(path, "HeaderRow", "Vertical Alignment", "Top") unreal.WidgetService.set_property(path, "HeaderRow", "Padding", "8") ``` > **Shadowed names prefer the WIDGET (fixed in issue #553).** A name that exists on both a widget > and its slot (e.g. `HorizontalAlignment` on a Border sitting in an Overlay) now writes the > widget's own property; a log warning notes the ambiguity. To target the slot explicitly, prefix > with `Slot.` β€” e.g. `set_property(path, "BackgroundBorder", "Slot.HorizontalAlignment", "Center")` > (value aliases like `Fill`/`Center` still work behind the prefix). Names that exist only as slot > aliases (the lists above) keep resolving to the slot with no prefix needed. ### Reparenting β€” `reparent_widget` Move an existing widget to a new parent panel (preserves the widget object/GUID): ```python unreal.WidgetService.reparent_widget(path, "BackgroundImage", "MainContainer") ``` Rejects moving a panel into itself/a descendant; the root cannot be reparented. ### Panel types | Type | Purpose | |------|---------| | `CanvasPanel` | Absolute positioning (X, Y coords) | | `VerticalBox` | Stack children vertically | | `HorizontalBox` | Stack children horizontally | | `Overlay` | Stack children on top of each other | --- ## Task Index Pick the task, load its workflow section, and/or run its sample script via `execute_python_code`. Sample scripts under `scripts/` are **runnable examples** β€” edit the variables at the top, then execute. | Task | Workflow | Sample script (run via `execute_python_code`) | |------|----------|-----------------------------------------------| | Create a Widget Blueprint | `workflows.md` β†’ Create Widget Blueprint | `scripts/create_widget.txt` | | Build a menu (root + panel + buttons) | `workflows.md` β†’ Build a Menu | `scripts/build_menu.txt` | | Position widgets on a CanvasPanel | `workflows.md` β†’ Canvas Positioning | β€” | | Inspect a widget (hierarchy + properties) | `workflows.md` β†’ Inspect a Widget | `scripts/inspect_hierarchy.txt` | | Apply full font styling | `workflows.md` β†’ Font Styling | `scripts/apply_font.txt` | | Apply full brush styling | `workflows.md` β†’ Brush Styling | `scripts/apply_brush.txt` | | Author a widget animation | `workflows.md` β†’ Widget Animation | `scripts/create_animation.txt` | | Bind a widget event to a function | `workflows.md` β†’ Bind Event | β€” | | Rename / remove a widget | `workflows.md` β†’ Edit the Hierarchy | β€” | | Edit slot layout (z-order, alignment, padding) / reparent | SKILL.md β†’ Slot editing | `scripts/edit_slots.txt` | | Capture a preview PNG | `workflows.md` β†’ Capture Preview | `scripts/capture_preview.txt` | | Inspect a widget live in PIE | `workflows.md` β†’ PIE Runtime Check | `scripts/pie_inspect.txt` | | Add MVVM ViewModel + bindings | `mvvm.md` | `scripts/mvvm_hud.txt` | ## Sub-docs Load these only when the task needs them β€” read the sibling file directly under `Plugins/VibeUE/Content/Skills/umg-widgets/` (the engine `AgentSkillToolset` `GetSkills` is the loader that exposes this skill; there is no `vibeue-skills-manager` tool): - **`workflows.md`** β€” step-by-step task workflows with copy-paste Python for every task above. - **`reference.md`** β€” field-name tables for every return type (`FWidgetInfo`, `FWidgetComponentSnapshot`, `FWidgetSlotInfo`, `FWidgetFontInfo`, `FWidgetBrushInfo`, `FWidgetAnimInfo`, `FWidgetPreviewResult`, `FPIEWidgetHandle`), the WidgetService action list, common properties, and event names. - **`mvvm.md`** β€” MVVM ViewModel support: rules, creation types, binding modes, field names, and full add/bind/list/remove workflows. ## Verification (do this before claiming success) 1. After hierarchy edits: re-read with `get_widget_snapshot(path)` and confirm the expected widgets/parents. 2. After property/style edits: read the value back (`get_property` / `get_font` / `get_brush`). 3. Open/compile the WBP and confirm no errors, then `unreal.EditorAssetLibrary.save_asset(path)`. Never report a widget as created/configured until it appears in a fresh `get_widget_snapshot` result. ## Additional gotchas - `capture_preview` renders correct gamma/colour now, but still confirm slot values with `get_component_snapshot` and layout in PIE. UMG colours are LINEAR β€” a dark sRGB red is about (0.035, 0.002, 0.002). - Slot values written through `ObjectIterator` are discarded on the next compile; set them with `set_property(path, widget, "Slot.", value)` or in C++ `NativeConstruct`. - A fresh WBP has no root: the first `add_component(path, type, name, parent_name="")` becomes it. `BindWidget` binds only when the tree widget is `is_variable=True`; reading a bound member from Python is blocked, so enumerate `ObjectIterator(unreal.TextBlock)` filtered by the instance path. - `bind_event` is only provably bound on the `GameInstance_*`-outered PIE instance (asset/preview instances always read unbound); simulate a click with `on_clicked.broadcast()` on that instance. Hand-built bound-event nodes compile but never fire β€” use `create_component_bound_event`. - A widget can be fully authored yet never compiled; compile a legacy widget and read the error first. UE 5.8 renamed `WidgetBlueprintLibrary` to `unreal.WidgetLibrary`, and `UserWidget` instances do not expose `get_widget_from_name`. - Input modes: a UI-only mode drops the first WASD press after it closes, so use `SetInputMode_GameAndUIEx` in Construct; the input mode survives `ServerTravel`; `DefaultInput.ini` holds a full serialised InputSettings block mid-file, so a key appended at the section top is silently overridden.