How-To: Display the Skill Tree

Goal: put a skill tree on screen and let the player interact with it, using nothing but this plugin. You build one host canvas widget: it spawns a node widget per node, positions each from the layout the Visual graph baked into the asset, and hands the whole set to the connection layer that paints the lines.

Prerequisites
A UCrimsonSkillTreeManager on your player and a configured tree (Quick Start), plus a node widget class - either the shipped UCrimsonSkillTreeWidget_Node or your own (How-To: Use Your Own Node Widget Class).

1. Point the asset at a node widget class

Open the skill tree asset and set Graph Config -> Default Node Widget Class. Any Widget Blueprint implementing `ICrimsonSkillTreeNodeWidget` is valid - there is no required base class. Subclass UCrimsonSkillTreeWidget_Node for a ready-made implementation (implement On Update Node Appearance / On Node Hovered), or start from your own widget base and implement the interface directly.

Where you set itApplies to
Graph Config -> Default Node Widget ClassEvery node in the tree
Graph Config -> Node Type To Widget Class MapEvery node carrying a given Node Widget Type Tag
A Visual node's Preview Widget ClassThat one node only (baked as its Widget Class Override)
Set the anchor inset as a class default
If the node's visible shape is smaller than its slot, set ConnectionAnchorInset on the class defaults, not per instance - the Visual Graph reads it off the Class Default Object so editor-side lines attach where the runtime ones will.

2. Build the host canvas

Create a Widget Blueprint (any UUserWidget parent). In the Designer, add the two children the host needs. Both names are yours to choose - nothing binds by name here:

WidgetNamePurpose
Canvas PanelNodeCanvasPanelHolds one child widget per skill node. Nodes are placed by absolute position, so a Canvas Panel is required - not a box or grid.
UCrimsonSkillTreeWidget_ConnectionLayerConnectionLayerPaints every connection in the tree. Drag it from the palette's Crimson Skill Tree category and place it behind the canvas panel so lines draw under the nodes.

Then populate it once, after the manager has built its runtime tree instances.

Event Construct -> Get Skill Tree Manager -> Get Skill Tree (Skill Tree Type Tag = your tree's tag) -> Is Valid (Branch, True) -> promote the result to a variable Tree. Get Skill Tree Manager is a pure node under Skill Tree | Utilities and resolves the local player's manager for you.

Then Get Tree -> Node Presentations -> For Each Loop. Inside the loop: Break the presentation struct, feed Node Guid into Get Tree -> Find Node By GUID, and Create Widget (Class = Widget Class Override, or Tree -> Graph Config -> Default Node Widget Class when the override is empty). Finish each node with Add Child to Canvas on NodeCanvasPanel -> Set Auto Size (true) -> Set Position (Game Position multiplied by Graph Config -> Node Position Scalar), then send the interface message Initialize Skill Node to the new widget with the node from Find Node By GUID.

Drive the loop from Node Presentations, not All Nodes
Node Presentations is the Visual graph's output and already carries the position and per-node widget override for every node the designer laid out. Looping All Nodes instead means looking the presentation back up by GUID for each one.
Verify
Play, open the widget, and every node from the Visual graph appears on the canvas in the same relative layout the graph showed. Clicking a node runs whatever your node widget does with Request Skill Node Action.

3. Place the decorations

The panels, headers and dividers authored in the Visual graph arrive on the asset as Tree -> Decorations. Each one resolves its widget class in one order: its own WidgetClassOverride when the designer set one, otherwise Graph Config -> Decoration Widget Classes for its Kind. So the loop is: resolve the class, spawn it, hand it the decoration, and place it yourself at the decoration's GamePosition and GameSize. A decoration that resolves neither is skipped and never rendered.

Get Tree -> Decorations -> For Each Loop. Inside the loop: Break the decoration struct, then resolve the class - Is Valid Class on Widget Class Override, and when it is not valid fall back to Get Tree -> Graph Config -> Decoration Widget Classes -> Find with Kind (a Select node on the override's validity reads cleanly here). Skip the entry when neither is valid; otherwise Create Widget with the resolved class. Finish with Add Child to Canvas on NodeCanvasPanel -> Set Position (Game Position) -> Set Size (Game Size) -> Set Z Order (-2), then send the interface message Initialize Skill Tree Decoration to the new widget with the same decoration struct.

Z-Order is what puts scenery behind the tree
Node slots keep the default Z-Order of 0, so any negative value puts a decoration under them. The connection layer in step 2 sits behind the whole canvas panel, so lines already draw over decorations placed this way; to put panels behind the lines as well, add a second Canvas Panel behind the connection layer and parent the decorations to that one instead.
Scenery takes no clicks
Set the decoration widget's visibility to Hit Test Invisible, in its Designer or in your InitializeSkillTreeDecoration override, so a panel never swallows the presses of the nodes standing on it or the drag the player pans with. UCrimsonSkillTreeWidget_Decoration fills a bound BackgroundBorder and TitleText from the decoration and leaves visibility to you.
What the tint means to your widget
TintColor is a multiplier on the widget's own authored color, and it defaults to white - so a widget that ignores it entirely still looks right. Honor it the way the shipped base does, by multiplying rather than replacing: UCrimsonSkillTreeWidget_Decoration writes it into a bound BackgroundBorder's brush color, which Slate multiplies over the brush, and a material-driven plate multiplies it onto the parameter's authored value. A decoration can then dim or recolor, never brighten past what the widget carries.
Verify
Play and the panels, headers and dividers appear behind the nodes, in the same places and at the same sizes the Visual graph showed - the graph previews the same widgets, so the two should match. A decoration that does not appear resolved no class: it has no Widget Class Override and its Kind has no entry in Decoration Widget Classes.

4. Paint the connection lines

One UCrimsonSkillTreeWidget_ConnectionLayer paints every line in the tree, so panning costs nothing, zooming only re-evaluates per-edge level of detail, and a purchase re-resolves styles without spawning or destroying a widget. Call it three times for three different reasons.

CallWhen
RebuildConnections(Tree, NodeWidgetMap, Policy, ZoomScale, bIsDesignTime)Once after populating, and again whenever topology or layout changes. Never for pan, zoom, or a state change.
UpdateConnectionStates()On OnSkillTreeStateUpdated - re-resolves every line's style and plays the purchase fill or drain.
UpdateZoomLOD(ZoomScale)When your canvas render scale changes. Panning needs no call at all.
The connection layer is driven from C++
RebuildConnections, UpdateConnectionStates and UpdateZoomLOD are C++ entry points - they take a widget map and a policy object that are not Blueprint-exposed, so a Blueprint-only canvas can show and drive nodes but cannot paint the lines between them. Give the canvas a C++ parent class (as in step 2) and Blueprint everything else on top of it. Get Hovered Connection and On Connection Hover Changed are Blueprint-facing, so hover handling stays in Blueprint either way.
cpp
// At the end of PopulateTree(), once the node widgets exist:
ConnectionLayer->RebuildConnections(
Tree,
NodeWidgetMap,
Tree->GraphConfig.LineDrawingPolicy,
CurrentZoomScale, // your canvas's uniform render scale; 1.0 to start
/*bInIsDesignTime=*/false);
// Stay in sync with gameplay. The manager broadcasts on every node state change,
// on the server and on clients alike (OnRep drives it client-side).
if (UCrimsonSkillTreeManager* Manager = UCrimsonSkillTreeBlueprintLibrary::GetSkillTreeManager(this))
{
Manager->OnSkillTreeStateUpdated.AddDynamic(this, &UMySkillTreeCanvas::HandleTreeStateUpdated);
}
// UFUNCTION() void UMySkillTreeCanvas::HandleTreeStateUpdated() { ConnectionLayer->UpdateConnectionStates(); }
Verify
Lines appear between parent and child nodes, following the shapes authored in the Visual graph. Buy a node in a Listen Server + Client session and the incoming line changes state on both.

5. Navigate with a gamepad or keyboard

Nodes do not sit on a grid, so Slate's built-in navigation cannot answer "which node is up from here". UCrimsonSkillTreeNavigationLibrary builds that answer once per tree: feed it one entry per navigable node with its canvas-space center, then ask the returned map at navigation time. Build the map once - it is O(n^2) in the node count, not per-press work.

Three nodes, all under Crimson Skill Tree | Navigation: you build the entry array, build the map once, then ask it on each direction press.

  1. Make an array of Crimson Skill Node Nav Entry - for each node widget, set Node to its data node and Center to the widget's canvas-space center (+Y down).
  2. Call Build Navigation Map (under Crimson Skill Tree | Navigation) with that array and a Crimson Skill Tree Navigation Params struct; promote the result to a variable.
  3. On a direction press, call Get Navigation Neighbor with the map, the focused node and the direction, then Set Focus on that node's widget.
Node widgets must be focusable
Slate refuses to move focus onto a widget that cannot take it, so an unfocusable node is a dead end - leave it out of the entry array rather than letting navigation stall on it. A CommonUI button node is focusable already; a plain User Widget node needs Is Focusable ticked. Bridges and junctions are skipped by the builder on purpose.

Tune the feel with FCrimsonSkillTreeNavigationParams - the cone half-angle, the sideways weight, and the discount applied to a candidate that shares an edge with the focused node.

6. Pan and zoom

Pan and zoom are your canvas's own transform, not something the plugin owns: scale NodeCanvasPanel's render transform to zoom, and offset it to pan. Any key works for either - mouse wheel, mouse XY, keyboard or gamepad stick - because the input side is entirely yours. The only plugin call the view needs is `UpdateZoomLOD` with the new scale after a zoom; panning needs none.

Leave the left stick to navigation
Pan with the right stick and zoom with the triggers, so the D-pad, left stick and arrow keys stay free for the node-to-node focus movement in step 5. Mouse XY is the natural pan axis for mouse and keyboard, gated behind a held middle mouse button.

7. Show it

Push or add your canvas widget to your UI the way you show any other menu. While it is up, stop the character moving underneath: set Ignore Move Input and Ignore Look Input on the Player Controller, and exclude your gameplay Input Mapping Contexts so ability inputs do not fire behind the menu.

Verify
The tree renders with its lines, the pointer or gamepad reaches every node, and buying a node updates the node widget and its incoming line. Run Play As Client and confirm the same on a client - node state arrives by replication and OnSkillTreeStateUpdated drives the refresh.
A prebuilt screen exists *(CrimsonCore - requires all Crimson plugins)*
CrimsonCore ships UCrimsonCoreSkillTreeGraph and UCrimsonCoreSkillTreeScreen - this whole page already built, with the navigation map, pan/zoom, gamepad prompts and a selected-node detail view-model wired up; you subclass them in Blueprint and set a few class defaults. It is free to download but compiles only with all Crimson plugins installed, so it is an alternative to this page, never a step in it. Everything above works with CrimsonSkillTree alone.

See also

  • How-To: Use Your Own Node Widget Class - the node contract, on any base class
  • How-To: Line States & Animations - per-state styles, purchase fill, hover
  • How-To: Drive Node Actions - what the manager does with a request