UDreamRingMenu
A ring menu whose hierarchy is code, not an asset: wedges around a hub, picked by direction.
Inherits from UDreamUIControl.
Declared in Public/Controls/DreamRingMenu.h.
A ring menu whose hierarchy is code, not an asset: wedges around a hub, picked by direction.
The radial menu every action game has and no UI framework ships, in the DreamGUI idiom. Items are
a UPROPERTY array, so .dui, the designer, Blueprint and a <-> binding all drive it with no glue;
the look comes from the project sheet like every other control's.
THE WEDGE IS A RECT
There is no new visual and no new shader here. UDreamRectBlock already draws a rounded rect with a border, a radial-fill mask, gradients and shadows -- and an annulus sector is exactly that rect with the corner radius at 100%, the body off (so the BORDER is the ring), and the mask set to the slice. UDreamProgressBar's Radial shape found the first half of this; the second half is that the mask's start and sweep are per-item rather than a percentage. So every wedge inherits gradients, inner and outer shadows and soft edges for free, and a project that wants a fancier ring styles the rect rather than waiting for a feature.
The cost is honest and worth stating: each wedge is a SQUARE the size of the whole ring, of which the shader keeps one slice. Every wedge therefore overlaps every other, and a rect hit test would hand the topmost one the entire circle -- which is why the hit shape is UDreamRingSectorRaycast and not the default.
WHY THE HIT TEST IS A CUSTOM RAYCAST AND NOT A TICK
The obvious build is a control that reads the pointer every frame and works out an angle. This one does not, and the difference is not style: with an exact sector as each wedge's hit shape the whole existing event pipeline just works -- enter, exit, press, click, the selectable's four transitions, event bubbling, the modal layer, the drag threshold. A ticking control would have had to reimplement each of those and would still have been wrong about which of two overlapping menus the pointer was in. The angle maths lives in one place, in the raycast, and the gamepad route (HighlightByDirection) calls the same two static functions rather than a second copy.
The gap between wedges is DRAWN ONLY. Were it in the hit shape too, dragging across one would exit a wedge and enter nothing, and the highlight would blink off between every pair of items.
HIGHLIGHT AND SELECTION ARE TWO THINGS
Highlight is where the pointer (or the stick) is; selection is what has been committed. A menu separates them -- hover, then click -- and a weapon wheel does not, which is one bool (bSelectOnHighlight) rather than two controls. Both are readable, both fire, and OnItemActivated fires on every commit even when the same item is chosen twice, because "invoke this command again" is a thing a menu has to be able to say.
Native.RingMenu Wheel {
HitArea = Slice
bSelectOnHighlight = true
Style.SweepAngle = 360
OnItemActivated -> HandleWheelPickProperties
| Name | Type | Category | In the details panel | From Blueprint | Description |
|---|---|---|---|---|---|
Style | FDreamRingMenuStyle | Ring Menu | yes | SetStyle | This instance's own look. The project sheet wins while StyleSource says so AND a sheet actually exists; with no sheet in the project this IS the look in effect -- which is why it stays editable instead of being gated on the enum. |
Items | TArray<FDreamRingMenuItem> | Ring Menu | yes | SetItems | The wheel. Order is clockwise from the style's StartAngle. |
SelectedIndex | int32 | Ring Menu | yes | SetSelectedIndex | The committed item. -1 is none, which is the resting state of a menu nobody has chosen from. |
HitArea | EDreamRingHitArea | Ring Menu | yes | GetHitArea / SetHitArea | See EDreamRingHitArea. |
SliceHitRadiusScale | float | Ring Menu | yes | GetSliceHitRadiusScale / SetSliceHitRadiusScale | How far a Slice reaches, as a multiple of the ring's outer radius. Ignored by Ring. |
DeadZoneRadius | float | Ring Menu | yes | GetDeadZoneRadius / SetDeadZoneRadius | The radius inside which the POINTER picks nothing, in ring units. Zero (the default) means the style's InnerRadius, which is the hub's edge and the answer nine times in ten; a Slice menu that wants a bigger "let go here to cancel" middle than its hub states one here. |
StickDeadZone | float | Ring Menu | yes | SetStickDeadZone | How far a stick must be pushed before HighlightByDirection picks anything, as the vector's MAGNITUDE -- an axis pair is already normalized, so this is 0 to 1 and has nothing to do with the ring's radii. What stops a resting stick's jitter from choosing an item every frame. |
LabelFacing | EDreamRingLabelFacing | Ring Menu | yes | GetLabelFacing / SetLabelFacing | See EDreamRingLabelFacing. |
bSelectOnHighlight | bool | Ring Menu | yes | SetSelectOnHighlight | Highlighting an item commits it, with no click -- the weapon wheel, where the pointer's direction IS the choice and releasing the key is the confirm. |
bAllowDeselect | bool | Ring Menu | yes | SetAllowDeselect | Clicking the item that is already selected clears the selection instead of re-choosing it. |
bShowLabels | bool | Ring Menu | yes | GetShowLabels / SetShowLabels | Off leaves the wedges to their icons. A wheel of glyphs is an ordinary thing to want. |
bHubFollowsHighlight | bool | Ring Menu | yes | SetHubFollowsHighlight | The hub shows the highlighted item's label (falling back to the selected one's, then to HubText). Off leaves HubText alone, which is how the hub becomes a static title. |
HubText | FText | Ring Menu | yes | SetHubText | What the hub says with nothing highlighted. Empty is an empty hub. |
WedgeTemplateClass | TSubclassOf<UDreamUserWidget> | Ring Menu | yes | SetWedgeTemplateClass | A wedge's CONTENT, authored elsewhere: one instance of this class is created inside every wedge widget, filling it, and the built-in label steps aside. The wedge's shape, its hit sector, its highlight and its click stay the control's, so a template only has to draw an item. |
RingNode | TObjectPtr<UDreamWidget> | Ring Menu | - | read only | The node everything hangs under, and what Open and Close scale and fade. |
BackdropNode | TObjectPtr<UDreamWidget> | Ring Menu | - | read only | The unbroken ring behind the wedges, filling the gaps between them. |
WedgeRootNode | TObjectPtr<UDreamWidget> | Ring Menu | - | read only | What the wedges are children of -- the template among them. |
WedgeTemplateNode | TObjectPtr<UDreamWidget> | Ring Menu | - | read only | The thing wedges are copied from -- authored once, inactive, never drawn itself. Public because it is half of the "hand it a template" story. |
HubNode | TObjectPtr<UDreamWidget> | Ring Menu | - | read only | The disc inside the ring. |
HubLabelNode | TObjectPtr<UDreamWidget> | Ring Menu | - | read only | |
WedgeNodes | TArray<TObjectPtr<UDreamWidget> > | Ring Menu | - | read only | The wedge widgets, parallel to Items. |
Functions
| Function | Kind | Description |
|---|---|---|
void ActivateHighlighted() | callable | Commit whatever is highlighted: what a "release the button to choose" input calls. |
void Close() | callable | Hide it, clearing the highlight on the way out. A player whose focus is on a wedge -- or went nowhere from the ring -- gets back what they had focused when it opened, before the fade starts. |
float GetDeadZoneRadius() | pure | Get Dead Zone Radius |
int32 GetHighlightedIndex() | pure | Get Highlighted Index |
EDreamRingHitArea GetHitArea() | pure | Get Hit Area |
int32 GetItemCount() | pure | Get Item Count |
float GetItemMidAngle(int32 InIndex) | pure | The middle of an item's slice, which is where its icon and label ride. |
float GetItemStartAngle(int32 InIndex) | pure | Where an item's slice begins, in degrees clockwise from twelve. |
float GetItemSweepAngle(int32 InIndex) | pure | How wide an item's slice is, in degrees -- its share of the style's sweep, by weight. |
EDreamRingLabelFacing GetLabelFacing() | pure | Get Label Facing |
int32 GetSelectedIndex() | pure | Get Selected Index |
bool GetShowLabels() | pure | Get Show Labels |
float GetSliceHitRadiusScale() | pure | Get Slice Hit Radius Scale |
UDreamWidget* GetWedgeWidget(int32 InIndex) | pure | The wedge standing for an item, or null when there is none. |
void HighlightByAngle(float InAngleDegrees) | callable | Highlight whichever item owns this angle, in degrees clockwise from twelve. |
void HighlightByDirection(FVector2D InDirection) | callable | Highlight whichever item owns this direction -- the gamepad route. |
int32 IndexAtAngle(float InAngleDegrees) | pure | Which item owns an angle, or -1 when the sweep does not cover it. |
bool IsOpen() | pure | Is Open |
void Open() | callable | Show the ring, scaling and fading it in when there is a world to tween in. Every player's focus is noted first. |
void RebuildItems() | callable | Throw the geometry at the wedges again, growing or shrinking the pool first if the item count moved. Called for you by ApplyStyle -- wedge geometry IS style -- and by SetItems. |
void SetAllowDeselect(bool bInAllowDeselect) | callable | Set Allow Deselect |
void SetDeadZoneRadius(float InRadius) | callable | Set Dead Zone Radius |
void SetHighlightedIndex(int32 InIndex) | callable | Move the highlight. -1, or a disabled item, clears it. |
void SetHitArea(EDreamRingHitArea InHitArea) | callable | The five setters below each write the field and then make the push that field is read by -- the whole of what a BlueprintSetter buys on this family. The hit-shape trio (HitArea, the slice's reach, the dead zone) is consulted by the raycast override at the moment a pointer arrives rather than baked into anything drawn, so those three need no restyle at all; the two that decide what a wedge SHOWS do. |
void SetHubFollowsHighlight(bool bInHubFollowsHighlight) | callable | Whether the hub shows the highlighted item's label. The hub is re-read at once. |
void SetHubText(FText InHubText) | callable | What the hub says when nothing else has a claim on it. The hub is re-read at once. |
void SetItems(TArray<FDreamRingMenuItem> InItems) | callable | Replace the wheel and rebuild it. |
void SetLabelFacing(EDreamRingLabelFacing InFacing) | callable | Re-orients every wedge's label, which is geometry the style push writes. |
void SetSelectedIndex(int32 InIndex) | callable | Moves the selection and fires both selection events. Out of range selects nothing. |
void SetSelectedIndexWithoutNotify(int32 InIndex) | callable | The same move, silently: for pushing an authored value in, which is not the user choosing. |
void SetSelectOnHighlight(bool bInSelectOnHighlight) | callable | Set Select on Highlight |
void SetShowLabels(bool bInShowLabels) | callable | Wakes or sleeps every wedge's label, which the style push decides. |
void SetSliceHitRadiusScale(float InScale) | callable | Set Slice Hit Radius Scale |
void SetStickDeadZone(float InStickDeadZone) | callable | How far the stick has to be pushed before it points at anything, 0..1. Read on every stick sample. |
void SetStyle(FDreamRingMenuStyle InStyle) | callable | Replace the look and push it. |
void SetWedgeTemplateClass(TSubclassOf<UDreamUserWidget> InWedgeTemplateClass) | callable | The authored wedge content. Instanced into each wedge at build, so a new class means a rebuild. |
void StepHighlight(int32 InDelta) | callable | Step the highlight round the ring, skipping disabled items. Wraps on a full wheel and stops at the ends of a partial one -- a half-ring has a first and a last item, and pretending otherwise is how a keyboard user ends up somewhere off screen. |
void ToggleOpen() | callable | Toggle Open |
Events
| Event | Signature | Description |
|---|---|---|
OnHighlightChanged | void DreamRingMenuIndexEvent__DelegateSignature(int32 Index) | Where the pointer is. Fires with both routes -- the wedges' hover and HighlightByAngle. |
OnSelectionChanged | void DreamRingMenuIndexEvent__DelegateSignature(int32 Index) | The committed item moved. |
OnValueChangedBP | void DreamRingMenuIndexEvent__DelegateSignature(int32 Index) | The <-> convention: two-way bindings synthesize their reverse route against this exact name, so a value control carries it alongside its spoken events. Fires with OnSelectionChanged. |
OnItemActivated | void DreamRingMenuActivatedEvent__DelegateSignature(int32 Index, FName Tag) | An item was CHOSEN -- clicked, or confirmed through ActivateHighlighted. Fires every time, including when the choice is the item already selected, because a menu entry is a command and "do it again" has to be sayable. The tag rides along so a handler never switches on an index. |
OnWedgeGenerated | void DreamRingMenuWedgeEvent__DelegateSignature(int32 Index, UDreamWidget* Wedge) | One per wedge, every time it is BOUND to an item. The hook for a consumer whose wedges are richer than an icon over a label but who would rather not author a whole class: everything under the wedge is reachable from here by display name, and anything added under WedgeTemplateNode before the first rebuild rides into every copy. |
OnOpened | void DreamRingMenuIndexEvent__DelegateSignature(int32 Index) | Open and Close, each carrying the SELECTION at that moment -- which is the whole of what a "hold to open, release to choose" input wants back from a close. |
OnClosed | void DreamRingMenuIndexEvent__DelegateSignature(int32 Index) |