UI Showcases
CrimsonCore is the aggregator layer - the only place allowed to mix CrimsonInput with a feature plugin's UI in one class. The widgets here are reference implementations that demonstrate the recommended wiring pattern; subclass them in Blueprint to use as-is, or copy them as a starting point for project-specific UIs.
UCrimsonCoreSkillTreeScreen
The ready-made skill tree screen. It is a UCrimsonActivatableWidget (CrimsonUI) that hosts a UCrimsonCoreSkillTreeGraph canvas, resolves the player's UCrimsonSkillTreeManager, drives a UCrimsonCoreSkillTreeViewModel for the detail panel, and owns all input. It lives here rather than in CrimsonSkillTree because it needs CrimsonUI, CrimsonInput and the MVVM stack at once - which only CrimsonCore is allowed to combine.
UEnhancedInputComponent. That gives one consistent path for analog axis values and press/hold events while the menu holds mouse capture. CommonUI remains the activation/screen framework but is out of the input path, and the screen deliberately does not take focus - CapturePermanently keeps focus on the viewport so the capture cannot collapse on a click.Configurable properties
| Property | Category | Purpose |
|---|---|---|
UIInputConfig | Input | Tagged input config. Its UIInputActions array must contain an entry per tag below. |
ZoomTag | Input | Axis1D action - wheel delta. |
PanMoveTag | Input | Axis2D action - pointer delta. Only applied while pan is active. |
PanToggleTag | Input | Digital action - held to enable panning. |
PrimaryTag | Input | Digital action - runs RequestPrimaryActionOnTarget. |
SecondaryTag | Input | Digital action - runs RequestSecondaryActionOnTarget. |
DefaultSkillTreeTypeTag | Skill Tree | Which configured tree to display on activation. |
ZoomSpeedFactor | Zoom | Per-notch zoom step (default 0.07). |
MinZoomScale / MaxZoomScale | Zoom | Clamp range (defaults 0.75 / 1.75). |
InitialCanvasScale | Zoom | Scale applied once the graph reports ready. 0 leaves the engine default. |
UCrimsonCoreSkillTreeGraph named `SkillTreeGraph`. It is a hard BindWidget, so the Blueprint will not compile without it.Step 1 - Author UI input actions
In your UCrimsonInputConfig asset, add five entries to the UI Input Actions array, each tagged with a UIInputTag. The screen resolves its UInputAction objects from these tags at activation, so rebinding is pure data - any key, mouse button, wheel axis or gamepad control works.
DA_InputConfig_PlayerUIUIInputActions[0].InputTag = UIInputTag.SkillTree.Zoom # Axis1D (mouse wheel)UIInputActions[0].InputAction = IA_SkillTree_ZoomUIInputActions[1].InputTag = UIInputTag.SkillTree.PanMove # Axis2D (mouse XY)UIInputActions[1].InputAction = IA_SkillTree_PanMoveUIInputActions[2].InputTag = UIInputTag.SkillTree.PanToggle # digital (RMB hold)UIInputActions[2].InputAction = IA_SkillTree_PanToggleUIInputActions[3].InputTag = UIInputTag.SkillTree.Primary # digital (LMB / gamepad accept)UIInputActions[3].InputAction = IA_SkillTree_PrimaryUIInputActions[4].InputTag = UIInputTag.SkillTree.Secondary # digital (RMB click / gamepad back)UIInputActions[4].InputAction = IA_SkillTree_Secondary
Step 2 - Create the widget Blueprint
Right-click in the Content Browser -> User Widget -> parent class UCrimsonCoreSkillTreeScreen. Place a UCrimsonCoreSkillTreeGraph in the widget tree and name it SkillTreeGraph. In Class Defaults, set the Input Config plus the five tags, and the default skill tree type tag. Push the screen onto your CommonUI Menu layer.
That is the whole integration. NativeOnActivated binds the Enhanced Input actions and NativeOnDeactivated unbinds them; the screen resolves the manager, populates the canvas and binds the view model on its own.
How pan works
Pan is a held gesture expressed as two bindings rather than a polling timer. PanToggle (Started / Completed) flips an active flag, and PanMove delivers an Axis2D pointer delta which is applied only while that flag is set. Because the delta arrives with the input event, there is no cursor polling and no tick rate to tune.
| Binding | Trigger | Handler | Effect |
|---|---|---|---|
PanToggleTag | Started | HandlePanPressed | Marks pan active. |
PanToggleTag | Completed | HandlePanReleased | Marks pan inactive. |
PanMoveTag | Triggered (Axis2D) | HandlePanMoveEI | Applies the delta via PanByViewportDelta, ignored unless pan is active. |
How zoom works
UCrimsonCoreSkillTreeGraph::ApplyZoom is a pure math primitive - a multiplier plus a screen-space anchor, with no speed or clamp policy. The screen owns the policy: it converts the wheel delta into a multiplier using ZoomSpeedFactor, clamps the resulting scale between MinZoomScale and MaxZoomScale, then passes the effective multiplier down.
const float WheelDelta = Value.Get<float>();const float CurrentScale = SkillTreeGraph->GetCanvasRenderScale();const float RawMultiplier = (WheelDelta > 0.f) ? (1.f + ZoomSpeedFactor): (1.f / (1.f + ZoomSpeedFactor));const float Desired = FMath::Clamp(CurrentScale * RawMultiplier, MinZoomScale, MaxZoomScale);const float Effective = Desired / CurrentScale;ZoomAtScreenPosition(Effective, MousePos);
InitialCanvasScale is applied separately through SetCanvasRenderScale when the graph broadcasts OnGraphReady.
Node widgets: any base class you like
The graph spawns node and decoration widgets as plain UUserWidget and talks to them only through CrimsonSkillTree's ICrimsonSkillTreeNodeWidget / ICrimsonSkillTreeDecorationWidget contracts, so a project may build nodes on any widget base. See CrimsonSkillTree -> How-To: Use Your Own Node Widget Class.
`UCrimsonCoreSkillTreeNodeButton` is the worked example shipped here: a node built on UCrimsonButtonBase instead of on any CrimsonSkillTree class, which is what gives it gamepad focus, navigation and per-input-method styling for free. Assign it like any other node widget class, via GraphConfig.DefaultNodeWidgetClass or NodeTypeToWidgetClassMap.
OnSkillNodeSelected / OnSkillNodeActivated and resolves no action itself. The graph forwards those to OnNodeWidgetSelected / OnNodeWidgetActivated, and the screen turns them into SetTargetNode + RequestPrimaryActionOnTarget. Action resolution stays in one place, so the mouse and gamepad paths cannot drift apart.Non-CommonUI projects
The action API is input-agnostic and public: ZoomAtScreenPosition, PanByViewportDelta, RecenterOnRoot, RecenterOnContent, SetTargetNode, RequestPrimaryActionOnTarget, RequestSecondaryActionOnTarget and RequestActionOnNode are all BlueprintCallable. If you would rather not use this screen, host UCrimsonCoreSkillTreeGraph in your own widget and drive that same API from wherever your input lives.
Plugins/CrimsonCore/Source/CrimsonCore/Public/UserInterface/SkillTree/ - Screens/CrimsonCoreSkillTreeScreen.h, Graph/CrimsonCoreSkillTreeGraph.h, Nodes/CrimsonCoreSkillTreeNodeButton.h, ViewModels/CrimsonCoreSkillTreeViewModel.h, with the matching .cpp files under Private/UserInterface/SkillTree/.UCrimsonCoreCastBarWidget
A HUD cast / channel bar. Binds the owning player's ASC OnAbilityProgress (CrimsonAbilitySystem), and interpolates the fill locally from the message's StartWorldTime - the ability broadcasts only a start and a stop, never a per-tick stream. Register it at the HUD.Slot.CastBar extension point through UCrimsonGameFeatureAction_AddWidget.
| Member | Purpose |
|---|---|
On Cast Bar Shown(Message) | A cast or channel began. Read Kind to pick a presentation; Duration <= 0 means an indeterminate channel. |
On Progress Visual(Progress01, Elapsed, Remaining) | Every frame while active. Progress01 is 0 for an indeterminate channel - use Elapsed. |
On Cast Bar Ended(bWasInterrupted) | The segment ended and the widget is still visible. Play a finish animation here. |
On Cast Bar Outro Aborted | A new cast preempted the finish animation. Stop it; On Cast Bar Shown fires immediately after. |
Finish Cast Bar Outro() | Call when the finish animation completes. Collapses the bar. |
Outro Timeout Seconds | Ceiling on how long the bar waits for that call. 0 hides immediately - raise it to use an outro at all. |
bBindInputActionToClick | Off by default, and leave it off for an action bar. CrimsonInput already binds ability input on the player's EnhancedInputComponent, so turning this on registers a second binding for the same key and one press activates twice. Off still uses the Input Action - just for the glyph. |
GetSlotInputAction() | The Input Action the ability is currently bound to, resolved through UCrimsonInputConfig from the spec's tags. Follows a rebind; null when nothing is bound. |
Outro Timeout Seconds is a safety ceiling, not the intended duration - set it just above your longest finish animation.Finish Cast Bar Outro is deliberately a no-op outside an outro, because an animation-finished delegate can still fire after a new cast took the bar over.UCrimsonCoreAbilityFailureWidget
Surfaces 'why did nothing happen' feedback - out of resource, still on cooldown, silenced, out of charges. Listens on TAG_ABILITY_SIMPLE_FAILURE_MESSAGE, filtered to the owning player, and calls On Failure Visual(Reason, FailureTags). Register it at HUD.Slot.Toasts.
Reason is the designer-authored string from the ability's FailureTagToUserFacingMessages map. An ability with no message configured for that failure tag sends an empty text - check before showing it. FailureTags lets you style cooldown differently from out-of-resource.UCrimsonCoreAbilityStateWidget
One action-bar slot: icon, cooldown sweep, cost and block states, charges. Derives from UCrimsonButtonBase (CrimsonUI), so CommonUI focus, hover, gamepad navigation and the input-action-widget hookup come for free rather than being reimplemented.
| Member | Purpose |
|---|---|
SlotInputTag | Hand-authored fallback for a bar not driven by the Fixed Slot UI. Matched with HasTagExact against the spec's dynamic source tags - the exact key UCrimsonAbilitySet writes at grant and AbilityInputTagPressed matches on, so the button and the keypress cannot point at different abilities. |
SetAbilitySpecHandle(Handle) | Deliberate override; always wins. For anything not driven by input: a tooltip, a skill tree node, an inspect panel. |
bClickToActivate | Clicking activates through AbilityInputTagPressed, so a click and a keypress take the identical path. Turn it off for a read-only display; interaction is disabled, which also stops the slot taking gamepad focus. |
PollRateHz | How often discrete state is re-evaluated (default 10). Polled rather than evented because GAS broadcasts nothing when a cost becomes affordable through regeneration. |
GetAbilityState() | Returns FCrimsonAbilityStateSnapshot: bIsGranted, bIsActive, bCanActivate, bCostMet, bOnCooldown, CooldownRemaining/Duration/Progress01, bUsesCharges, CurrentCharges, MaxCharges, FailureTags. |
OnAbilityStateChanged | Fires only when discrete state actually moved, plus once on construct. Rebuild visuals here without guarding against per-frame spam. |
OnCooldownProgress | Fires every frame while a cooldown runs. Progress01 rises 0 -> 1; drive a radial sweep with it. |
OnIconResolved | Fires when the icon becomes known, including after a loadout swap. Icon is null when the ability authored no display data - draw your empty-slot treatment. |
OnActivationRefused | The slot was clicked but the ability refused. Carries the failure tags for a shake or an error beep. |
| Binding path | When it applies |
|---|---|
SetAbilitySpecHandle | Explicit override. Always wins. |
| Fixed Slot UI context | The ability registered the slot itself. Delivered via ICrimsonUIExtensionWidget::SetExtensionPayload. The intended path for an action bar. |
SlotInputTag | Hand-authored bar not using the Fixed Slot UI. |
SetFixedSlotContext(Payload) | Blueprint escape hatch. Rarely needed - the interface delivers the payload automatically. |
bUseFixedSlotUI on the ability, set FixedSlotExtensionPointTag, and name your slot Blueprint in FixedSlotWidgetClass. The ability then registers a UCrimsonFixedAbilitySlotContext at that tag and the widget is spawned for you - so a granted ability brings its bar entry with it, and a removed one takes it away.Two setup steps that fail silently if missed: add
CrimsonFixedAbilitySlotContext to the extension point's DataClasses array (the payload is filtered out otherwise and nothing spawns), and make sure the ability's tag and the extension point's tag match exactly under ExactMatch.The context's
OnCooldownStarted, OnChargesChanged and OnIdentityChanged delegates drive immediate refreshes on top of the poll. OnIdentityChanged is the one that matters most: an augment can swap which ability backs the slot, and the icon follows.bOnCooldown and bCanActivate are both true. The state model already treats those as independent axes, so charges cost two extra fields rather than a subclass. Check bUsesCharges and show your pip row. Subclass in Blueprint if you want distinct visuals; the state model needs no change.GetPrimaryInstance), falling back to the CDO only when there is no instance. Ability-scoped values - an augment-modified cost or cooldown - live in the instance's PropertyCache, so reading the CDO would silently show the unaugmented numbers.UCommonButtonBase already carries an optional InputActionWidget and UCrimsonButtonBase already refreshes it when the input method changes - all that was missing was the action.The slot resolves that action dynamically, per poll: it takes the tags on the resolved ability spec and asks
UCrimsonInputConfig::FindAbilityInputActionForTag about each one, using the config on the pawn's UCrimsonPawnData - the same config the hero component actually binds. It deliberately does not read the ability's authored FixedSlotInputAction, because that is a fixed value in the asset and cannot follow a hotbar rebind. Rebind the ability and the glyph changes on the next poll.To use it: add a
CrimsonActionWidget to your slot Blueprint named exactly InputActionWidget (a BindWidget with OptionalWidget = true, so omitting it just means no glyph).The action is pushed with
SetEnhancedInputAction, which resolves a glyph and registers no binding - the key keeps activating the ability through CrimsonInput alone. Glyph caveat: an Enhanced Input action only resolves to a key while it is in a currently added Input Mapping Context.CrimsonCore/Public/UserInterface/Ability/CrimsonCoreAbilityStateWidget.h. Display data comes from CrimsonAbilitySystem -> API Reference -> Ability display data.