How-To: Build a Radial Menu
Goal: put a wheel on screen - a weapon swap, a consumable picker, an emote ring - where the player points at an option and lets go to choose it.
Tick Show Plugin Content in the Content Browser's Settings menu or none of the
/CrimsonUI/ assets below are visible.1. Start from the shipped menu
CrimsonUI ships the whole wheel already assembled. Duplicate it rather than rebuilding it - the widget names below are BindWidget bindings and the Blueprint fails to compile if any is missing or misspelled.
W_CrimsonRadialMenu (/CrimsonUI/UI/Foundation/Radial/) is a UCrimsonRadialMenuWidget whose only content is a Canvas Panel holding one Wheel.Inside it,
W_CrimsonRadialWheel binds Canvas_Wheel (required) plus the optional hub readouts Image_Hub, Text_HubTitle and Text_HubSubtitle, and its class defaults already point SectorMaterial at MI_RadialSector and SliceWidgetClass at W_CrimsonRadialSlice.| Widget | Class | Binding | Job |
|---|---|---|---|
Wheel | UCrimsonRadialWheel | Required | The wheel itself. Sized by its slot - keep it square. |
Canvas_Wheel | UCanvasPanel | Required | Host for the sector art and slice content. |
Image_Hub | UImage | Optional | Center disc. Must FILL the canvas, not just the hub area. |
Text_HubTitle | UCommonTextBlock | Optional | Focused option's name. |
Text_HubSubtitle | UCommonTextBlock | Optional | Its description, or its disabled reason. |
0,0 - 1,1 with zero offsets and let the material draw the disc.2. Give it options
A slice is one FCrimsonRadialSlice. Fill in only what you want drawn - an empty CountText collapses the badge, an empty Icon collapses the icon - so a plain icon-and-label wheel needs no custom widget.
| Field | Type | Purpose |
|---|---|---|
Id | FGameplayTag | Identity returned on confirm. Prefer this over the index. |
DisplayName | FText | Label under the icon, and the hub title while focused. |
Description | FText | Hub subtitle while focused. |
Icon | TSoftObjectPtr<UTexture2D> | Slice icon. |
CountText | FText | Quantity badge - x12, 48/60. Empty hides it. |
bEnabled | bool | A disabled slice still draws but cannot be confirmed. |
DisabledReason | FText | Shown in the hub in place of the description. |
bDestructive | bool | Paints the focused state with the Destructive colors. |
TintOverride | FLinearColor | Per-slice rim and glow. Alpha 0 means no override. |
Cooldown | float | 0 ready, 1 fully cooling. Drives the sweep; 0 hides it. |
Payload | FInstancedStruct | Opaque caller data, handed back on confirm. |
Action | UCrimsonRadialAction | What choosing it does. See step 4. |
On the pushed menu call Set Options with an array of FCrimsonRadialSlice, or Set Options From Definition with a UCrimsonRadialMenuDefinition asset.
For a wheel whose contents never change, skip the call entirely: set DefaultDefinition in the menu Blueprint's Class Defaults and it fills itself in on activation, with no graph and no caller.
3. Hook up selection and confirm
The wheel drives itself. It reads the mouse and the gamepad stick on its own timer (the stick comes from StickAction, an Axis2D Input Action the player can rebind; without one it falls back to the raw left-stick keys) and picks the slice whose sector contains that direction - you do not bind anything for hover.
| Setting | Default | Effect |
|---|---|---|
bTrackMouse | true | Cursor direction from the wheel center selects. |
bAutoReadGamepadStick | true | Reads the stick each tick: from StickAction when set, else from StickXKey / StickYKey (Gamepad_LeftX / Gamepad_LeftY) directly. |
StickAction | None | Axis2D Input Action that steers the wheel. IMC_CrimsonUI_RadialMenu ships IA_CrimsonUI_RadialMenu_Direction on Gamepad_Right2D (the left thumb holds the D-pad chord that keeps the wheel open), player-mappable as "Radial Menu Stick", so switching sticks is a key rebind, not a widget edit. The same context binds the bindable wheel on gamepad: RB tap rebinds, RB or X held (0.4 s) clears, R3 carries, Y and X page. The widget binds these on Triggered, so a mapping needs an explicit trigger (Pressed, Tap) unless the action itself carries one: Clear keeps its Hold on the action, which is also what makes the bottom bar label it "(Hold)" and draw its progress. |
bConfirmOnMouseDown | true | Left click confirms the slice under the cursor. |
SelectionDeadZone | 0.34 | Central cancel zone. Keep equal to Style.InnerRadius. |
bClearFocusInDeadZone | true | Returning to the hub clears the selection. |
SelectionHysteresis | 0.3 | How far into a neighbor before focus switches. |
SelectionDeadZone matching Style.InnerRadius is what makes the hub disc be the cancel zone - sweep back to the middle and let go and nothing fires. Set them apart and the player has to learn an invisible rule instead of seeing one.For the hold-open pattern - hold a key, flick to an option, release to choose - set bConfirmOnRelease on the menu. Closing then becomes the choice, and Cancel suppresses it so Back still means Back.
Bind On Option Chosen on the menu. It carries the option index, its Id tag and its Payload, so a single handler can switch on the tag.
4. Optional: give the option its behavior
Instead of switching on a tag in one handler, a slice can carry its own behavior. Subclass UCrimsonRadialAction - in Blueprint or C++ - and set it on the slice's Action. The wheel runs it before the delegates fire, so a listener that closes the menu cannot swallow it.
| Override | When it runs | What to do in it |
|---|---|---|
Execute | On confirm | Do the thing. Makes no authority claims for you. |
QueryState | Before every render and refresh | Write live fields - Cooldown, CountText, bEnabled. |
GetDisplayContent | Before render, when ContentSource is Action | Supply icon, name and description. |
ContentSource to Action and its icon and text come from GetDisplayContent instead of being typed on the slice. Rename the underlying item or ability and every wheel offering it updates. Fields the action leaves empty fall back to the authored ones, so a partial provider tops up rather than blanking.bEnabled or raise Cooldown; it has no way to say "available again". The menu re-derives the options from a pristine copy on every refresh, which is what lets a slice recover. Do not cache state across calls inside your action.Create a Blueprint from UCrimsonRadialAction (Crimson UI -> Radial Menus -> Radial Action in the Content Browser's right-click menu), override Execute, and set it on the slice's Action field. It is an instanced object, so you author it inline on the slice or on a definition asset.
ContentSource to Action and the slice picks up the icon and name your override supplies.5. Keep it live while open
ActionStateRefreshInterval (default 0.1) re-asks every action for its state while the menu is up, so a cooldown that expires with the wheel open brings its own slice back and its sweep drains toward that moment. Set it to 0 to disable polling on a wheel whose options never change.
6. Multiplayer
The whole wheel is owning-client only. It is UI: nothing it does is authoritative, and nothing about it replicates. Execute runs on the client that chose the option, so any state change has to travel to the server itself - a Server_ RPC you validate, or a gameplay ability whose own net execution policy handles it.
Execute. A client-side wheel that writes authoritative state is a client that can write anything.See also
- Concept: Radial Selection & Slice State - how selection, state and the material fit together.
- How-To: Show a Context Menu - the radial presentation mode of the context-menu system.
- How-To: Push & Pop Screens - getting the menu on screen.
- CrimsonCore -> Radial Menu - the prebuilt gameplay-ability action and hold-to-open ability.