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.

Prerequisites
How-To: Get the Building Blocks for the UI layout and layer tags, and How-To: Push & Pop Screens for getting a screen on screen at all.

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.

Reference implementation
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.
WidgetClassBindingJob
WheelUCrimsonRadialWheelRequiredThe wheel itself. Sized by its slot - keep it square.
Canvas_WheelUCanvasPanelRequiredHost for the sector art and slice content.
Image_HubUImageOptionalCenter disc. Must FILL the canvas, not just the hub area.
Text_HubTitleUCommonTextBlockOptionalFocused option's name.
Text_HubSubtitleUCommonTextBlockOptionalIts description, or its disabled reason.
Image_Hub covers the whole canvas
Its radius parameters are in UV units relative to the image's half-extent. Sizing it to look like the hub puts its geometry in a different space from the sectors and it will not line up. Anchor it 0,0 - 1,1 with zero offsets and let the material draw the disc.
Verify
Push the duplicated menu to a layer. You get a ring of empty sectors and a hub, centered.

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.

FieldTypePurpose
IdFGameplayTagIdentity returned on confirm. Prefer this over the index.
DisplayNameFTextLabel under the icon, and the hub title while focused.
DescriptionFTextHub subtitle while focused.
IconTSoftObjectPtr<UTexture2D>Slice icon.
CountTextFTextQuantity badge - x12, 48/60. Empty hides it.
bEnabledboolA disabled slice still draws but cannot be confirmed.
DisabledReasonFTextShown in the hub in place of the description.
bDestructiveboolPaints the focused state with the Destructive colors.
TintOverrideFLinearColorPer-slice rim and glow. Alpha 0 means no override.
Cooldownfloat0 ready, 1 fully cooling. Drives the sweep; 0 hides it.
PayloadFInstancedStructOpaque caller data, handed back on confirm.
ActionUCrimsonRadialActionWhat 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.

Verify
Slices appear evenly spaced, option 0 at 12 o'clock proceeding clockwise. The disabled one is hatched and desaturated.

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.

SettingDefaultEffect
bTrackMousetrueCursor direction from the wheel center selects.
bAutoReadGamepadSticktrueReads the stick each tick: from StickAction when set, else from StickXKey / StickYKey (Gamepad_LeftX / Gamepad_LeftY) directly.
StickActionNoneAxis2D 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.
bConfirmOnMouseDowntrueLeft click confirms the slice under the cursor.
SelectionDeadZone0.34Central cancel zone. Keep equal to Style.InnerRadius.
bClearFocusInDeadZonetrueReturning to the hub clears the selection.
SelectionHysteresis0.3How far into a neighbor before focus switches.
The hub is the cancel gesture
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.

Verify
Sweep out to a slice and release - the handler fires once. Sweep back to the hub and release - nothing fires.

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.

OverrideWhen it runsWhat to do in it
ExecuteOn confirmDo the thing. Makes no authority claims for you.
QueryStateBefore every render and refreshWrite live fields - Cooldown, CountText, bEnabled.
GetDisplayContentBefore render, when ContentSource is ActionSupply icon, name and description.
Let the thing describe itself
Set a slice's 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.
QueryState is one-way
It can clear 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.

Verify
Choose the option - the action runs. Set 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.

Why polling and not a delegate
An effect-removed or tag-count delegate fires at the start and end of a cooldown. The sweep needs a value that keeps moving between those two points, so one poll covers both the animation and the state flip.
Verify
Open the wheel on an option that is cooling down. Its sweep drains and the slice returns to normal without you closing and reopening.

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.

A radial action is not an authority boundary
Do not mutate gameplay state directly inside 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.