How-To: Build a Player-Editable Wheel
Goal: ship an empty wheel and let the player decide what goes in it - a consumable here, an emote there - with the arrangement surviving a save and reload.
UCrimsonPrimaryGameLayout in play, from How-To: Get the Building Blocks.Everything on this page uses CrimsonUI alone. The tag provider shipped here needs no other Crimson plugin, so a project that installed only CrimsonUI can complete every step.
1. Create the layout definition
A UCrimsonRadialLayoutDefinition describes the shape of the wheel and nothing about its contents. Contents come from the player.
In the Content Browser: right-click -> Miscellaneous -> Data Asset, pick CrimsonRadialLayoutDefinition, and name it DA_MyWheel.
Add one entry to Pages. Set its Layout Id to a gameplay tag that is unique across every wheel in your project - this is the key the player's bindings are stored under. Then size Slots to how many positions the wheel has (8 is the usual maximum before sectors get too thin to hit).
2. Decide what each slot accepts
Every slot is a FCrimsonRadialSlotDefinition. All of its fields are optional; an untouched slot accepts anything and starts empty.
| Field | Purpose |
|---|---|
SlotId | Stable identity for the position. Survives reordering the array - prefer it over the index anywhere near a save. |
bLocked | The player cannot change this slot. Its content is whatever DefaultBinding says. A fixed entry is a locked slot, not a separate concept. |
DefaultBinding | What a fresh profile starts with, and what Reset To Defaults restores. |
AcceptedEntries | A FGameplayTagQuery matched against each entry's FilterTags. Empty accepts everything. |
EmptyLabel / EmptyIcon | The placeholder shown while the slot is empty - conventionally a plus sign. |
bLocked to true. After step 6 you will find the picker refuses to open on it and a carried slice refuses to land on it.3. Add a provider
A provider is where bindable things come from. UCrimsonRadialBindingProvider_Tag ships with CrimsonUI and offers a hand-authored list of gameplay tags, firing its OnTagChosen event (and the process-wide native OnAnyTagChosen) with the chosen one - a complete emote or ping wheel on its own.
On the layout asset, add one entry to Providers and choose Gameplay Tags from the class picker (it is Instanced, so it is authored inline on the asset).
Set Provider Id to a tag that identifies this provider - every binding it issues carries it, and that is how a saved slot finds its way back. Set Message Channel to the tag your game listens on. Then fill Entries, one per emote: its Tag, DisplayName, Icon, and any FilterTags you want slots to be able to filter on.
ProviderId is skipped with a warning and offers nothing, because a binding with no route cannot be resolved after a reload. Two providers claiming the same id is also a warning - the first wins, and which one that is would otherwise depend on array order.4. Give the player somewhere to store bindings
UCrimsonRadialBindingComponent holds every layout the player has edited. Add it to whichever actor your project persists - the PlayerController is the simplest, the PlayerState survives seamless travel. Find searches the controller, then its PlayerState, then its Pawn, so callers never care which you chose.
Open your PlayerController Blueprint, Add Component -> Crimson Slot Binding. There is nothing to configure on it.
showdebug or check the component list on your controller - the component is present on the server and on the owning client.5. Build the wheel widget
Create a Widget Blueprint whose parent class is CrimsonRadialMenuWidget_Bindable. It inherits everything from UCrimsonRadialMenuWidget, so the wheel binding is unchanged.
| Widget name | Type | Required |
|---|---|---|
Wheel | A CrimsonRadialWheel subclass | Yes - inherited BindWidget |
Then set these Class Defaults:
| Class Default | Set to |
|---|---|
| Layout Definition | The DA_MyWheel asset from step 1. |
| Picker Widget Class | The widget from step 6. Without one, slots can be cleared but not filled. |
| Picker Layer Tag | A UI.Layer.* tag the picker is pushed onto - UI.Layer.Modal is the usual choice. |
| Start In Edit Mode | On for a wheel opened from a loadout screen; off for a hold-to-open combat wheel. |
W_CrimsonRadialMenu_Bindable (/CrimsonUI/UI/Foundation/Radial/) - it is exactly this step, with Wheel bound to W_CrimsonRadialWheel and the picker class and layer already set. Duplicate it as your starting point and point Layout Definition at your own asset. Plugin content is hidden in the Content Browser until you tick Settings -> Show Plugin Content.6. Build the picker
Create a Widget Blueprint whose parent class is CrimsonRadialBindingPickerWidget. It gathers from every provider on the layout, drops anything the slot's filter rejects, and lists the rest.
| Widget name | Type | Required |
|---|---|---|
List_Entries | ListView with Entry Widget Class set to your row widget | Optional, but without it the picker shows nothing |
Text_Search | EditableTextBox | Optional - filters the list as the player types |
Text_Title | CommonTextBlock | Optional - names the slot being edited |
The row widget is an ordinary UserWidget implementing User Object List Entry. Its list item is a CrimsonRadialBindableEntryObject; read Entry.Display for the icon and name, and bIsClearEntry to style the pinned Clear Slot row differently.
UListView requires UObject items and a USTRUCT cannot be one. UCrimsonRadialBindableEntryObject exists for that reason alone.W_CrimsonRadialBindingPicker (/CrimsonUI/UI/Foundation/Radial/) - ContentPanel > Canvas_Root > VBox_Picker holding Text_Title, Text_Search and List_Entries, with the list on a Fill rule inside a fixed-size canvas slot so it has bounded height to virtualize against. Its row is W_CrimsonRadialBindingEntry, which derives from UCrimsonRadialBindingEntryWidget - that C++ base is what supplies the required IUserObjectListEntry implementation, so derive your own row from it rather than from UserWidget.7. Wire the edit inputs
Edit mode has three verbs. Confirm and the mouse secondary button work with no wiring; the other two are BlueprintCallable so you bind them however your project binds UI actions.
| Input | Call | Effect |
|---|---|---|
Click / A | (automatic) | Opens the picker for that slot |
| Right click | (automatic) | Clears the slot, or cancels a carry |
| A bound action | ToggleCarryAtFocus | Lifts a slice; confirm elsewhere to swap the two |
| A bound action | TriggerSecondaryAction on the wheel | The gamepad equivalent of right click |
| Shoulder buttons | NextPage / PreviousPage | Cycles pages when the layout has more than one |
| A bound action | ToggleEditMode | Enters and leaves edit mode |
Bind them the same way the shipped screens do - RegisterUIActionBinding against your own UI.Action.* tags, from the Blueprint subclass, so CrimsonUI stays free of any input-plugin knowledge.
CommonBoundActionBar silently drops any action whose Input Action has no key mapped in an added Input Mapping Context. If your clear or page buttons do not show up, that is the first thing to check.8. Persist the bindings
UCrimsonRadialBindingComponent deliberately has no save-system dependency of any kind. It exposes the whole store as plain data and takes no position on where it goes.
Get Save Data returns a CrimsonRadialBindingSaveData struct; Apply Save Data writes one back. Route them into whatever your project already saves - a SaveGame object, a profile blob, your own subsystem.
ApplySaveData is server-authoritative. Calling it on a client logs a warning and does nothing.
UCrimsonCoreRadialBindingComponent is this component already wired into the Crimson save pipeline (fragment CrimsonRadialBindings), and needs no code from you at all.COND_OwnerOnly.9. Accept drops from another screen (optional)
A player can also assign from outside the wheel - dragging an item off an inventory grid, the way The Witcher 3 does it.
Override Translate Drop To Binding on your wheel Blueprint. You are handed the drag operation and return the binding it stands for, plus true. The default already handles CrimsonRadialBindingDragDropOperation, so override it only for your own drag type.
On the drag source, Make Binding Drag Operation builds the shipped operation from a CrimsonRadialBindableEntry.
See also
- Concept: Slot Bindings - why a binding is not a slice, and what stale means
- How-To: Build a Radial Menu - the authored wheel this one builds on
- API Reference -> Slot bindings - every type and function
- CrimsonCore -> Radial Slot Bindings - providers for abilities, items and equipment