UI Toolbox logoUI Toolbox

UnlockTree

UnlockTree in UIToolbox shows cards that unlock in order: a tech tree, a skill tree, a talent tree, research or a quest chain. Each child UnlockNode is a card at a column and a row. It names the ids of the cards that it requires. The tree computes the state of each card and draws a line from each requirement to its card.

Video tutorial: Skill tree. All videos: Video tutorials.

<tb:UnlockTree name="tech" column-width="160" row-height="76">
  <tb:UnlockNode id="agriculture" title="Agriculture" cost="40 science" column="0" row="1" />
  <tb:UnlockNode id="pottery" title="Pottery" cost="60 science" requires="agriculture" column="1" row="0" />
  <tb:UnlockNode id="writing" title="Writing" cost="100 science" requires="pottery" column="2" row="0" />
  <tb:UnlockNode id="mathematics" title="Mathematics" requires="writing, wheel" column="3" row="1" />
</tb:UnlockTree>
var tree = root.Q<UnlockTree>("tech");
tree.UnlockRequested += node =>
{
    if (science < CostOf(node)) return;
    science -= CostOf(node);
    tree.Unlock(node.Id);
};
tree.Unlocked += node => Debug.Log(node.Title + " is done");

In the UI Builder, the tree shows a dashed cell at each column and row, labeled "Column 2, Row 1", with one free column and row past the last card. Set column and row on a card to the numbers of the cell where it goes. The cells do not show in Play mode or in a build.

States

State When Look
Locked A requirement is not unlocked. Dim. A click plays the error sound and does nothing.
Available All requirements are unlocked. Accent border. A click or Submit raises UnlockRequested.
Unlocked Unlock or SetUnlocked unlocked it. Green border and a check mark. The cost does not show.
Hidden A secret card that is locked. It does not show, and its lines do not show.
Blocked A card on another path is unlocked: one that it excludes, or one that excludes it. Faint, with a red border. A click plays the error sound. Its tooltip names the card that closed it.

Paths that exclude each other

A choice closes the other path. Give one of the two cards excludes:

<tb:UnlockNode id="guild" title="Join the guild" requires="blade" column="4" row="0" />
<tb:UnlockNode id="thieves" title="Join the thieves" requires="blade" excludes="guild" column="4" row="2" />
<tb:UnlockNode id="ending" title="Face the dragon" requires-any="guild, thieves" column="5" row="1" />

When the guild unlocks, the thieves card is Blocked, and BlockedBy(node) returns the guild card. A secret card on the closed path stays Hidden.

Unlock

A click, Enter or the gamepad submit on a card raises NodeClicked. When the card is Available and CanUnlock allows it, UnlockRequested comes next. The tree does not unlock the card itself: the game checks the cost and calls Unlock(id). A research can wait, so the game can unlock the card later.

With unlock-on-click="true", the click unlocks the card after UnlockRequested. Use CanUnlock to stop a click when the player cannot pay:

tree.CanUnlock = node => CostOf(node) <= points;
tree.Unlocked += node =>
{
    points -= CostOf(node);
    tree.Refresh();   // CanUnlock has new answers
};

An Available card that CanUnlock does not allow gets the class tb-unlock-node--denied: its cost is red, and a click plays the error sound. Call Refresh() when the answer of CanUnlock changes.

Save and load

saved = string.Join(",", tree.GetUnlockedIds());
tree.SetUnlocked(saved.Split(','));

SetUnlocked does not check the requirements and raises no Unlocked event. StateChanged comes for each card that changes. Ids that are not in the tree are ignored.

API

UnlockTree

Member Description
column-width, row-height The distance in pixels from one column or row to the next. Defaults 200 and 100.
padding The space in pixels around the cards. Default 16.
direction Horizontal (default): the lines go from the right side of a requirement to the left side of the card. Vertical: from the bottom to the top.
link-shape Elbow (default): lines at right angles, with the turn half way. Straight: one line. A card that is not after its requirement gets a straight line.
unlock-on-click A click on an available card unlocks it. Default false.
show-tooltips Sets the tooltip of each card: its description, and for a locked card the requirements that are missing. Default true.
AddNode(node), AddNode(id, title, column, row, params requires) Adds a card from code.
RemoveNode(id), GetNode(id), Nodes The cards.
GetState(id) The state of a card. Locked for an unknown id.
Unlock(id) Unlocks an Available card and raises Unlocked. False for a card that is not available. It does not ask CanUnlock.
Reset() Locks all cards.
GetUnlockedIds(), SetUnlocked(ids) Save and load.
MissingRequirements(node) The ids that the card still needs.
BlockedBy(node) The unlocked card that blocks the card, or null.
CanUnlock Optional. False stops the click on an available card.
Refresh() Places the cards, makes the lines and computes the states again.
Links The lines, each an UnlockLink with From, To, IsAnyOf and State.
ScrollView The scroll view around the cards.
NodeClicked, UnlockRequested, Unlocked, StateChanged The events. StateChanged gives the card and its state before.

UnlockNode

Attribute Description
id The id that other cards name in requires, and that the save holds.
title The bold text of the card.
description The text of the tooltip. Rich text tags work.
cost The muted text under the title, such as "60 science". It does not show on an unlocked card.
icon The picture at the left of the card.
column, row The place of the card, from 0. A fraction such as 2.5 places the card between two rows.
requires, requires-any The ids of the requirements, split by commas.
excludes The ids of the cards on another path, split by commas. When one of the two unlocks, the other is Blocked.
secret A locked secret card is Hidden. Its title shows as "?" in the tooltips of other cards.
progress The part of the research that is done, from 0 to 1, as a bar at the bottom of the card.

The card is a Button, so it gets the button sound. State and IsUnlocked give its state in code.

Keyboard and gamepad

The arrow keys and the gamepad move the focus to the nearest visible card in that direction. The tree scrolls to the focused card. Enter or the gamepad submit clicks the card. A drag on the background of the tree pans it.

Styling

Class Element
tb-unlock-tree The tree (grows; background, border).
tb-unlock-tree__scroll, __canvas, __links, __nodes The scroll view, the canvas that is as large as the cards, and the layers of the lines and the cards.
tb-unlock-tree__link A line, with --locked, --available, --unlocked, --hidden, --blocked and --any.
tb-unlock-tree__editor-grid, __editor-cell The layer and the cells of the column and row grid in the UI Builder.
tb-unlock-tree--panning The tree while a drag pans it.
tb-unlock-node A card (168px wide).
tb-unlock-node__icon, __body, __title, __cost, __check, __progress, __progress-fill The parts of a card.
tb-unlock-node--locked, --available, --unlocked, --hidden, --blocked, --denied The state of a card.
Variable On Description
--tb-unlock-link-color tb-unlock-tree__link The color of the line.
--tb-unlock-link-width tb-unlock-tree__link The width of the line in pixels.
--tb-unlock-link-dash tb-unlock-tree__link The length of a dash in pixels. 0 draws a solid line. Locked lines are dashed.

The card is a Button, so its rules need two classes to beat the button rule of the workspace:

.unity-button.tb-unlock-node { width: 140px; }
.tb-unlock-tree__link--available { --tb-unlock-link-color: #f0c040; }

The UnlockTreeDemo scene shows a tech tree with a research queue, a skill tree with points and a quest chain.