Skip to main content

Create and Animate the Quest Filter

caution

This document needs an UPGRADE

The player will be able to filter the quests based on those that are active or completed. The filter will be triggered by a click event that is hooked up to two text elements. There will be a simple bar that animates as it transitions from one element to the other. Here's what it'll look like when you are done:

no animation

Before jumping in you will need new UI and Style assets to work out of. Create them via a subdirectory under UserInterface and name them both QuestList.

Update AppRoot

It is possible to have templates call other templates. This is very helpful for creating modular code that is easier to maintain. Lets have you take care of that part of the setup before jumping into building out QuestList - that way you can see your progress in real time.

Open AppRoot.ui and add the following code to the end of the Template:

template AppRoot : AppRoot render {

Div(style = [@header-row]) {
Div(style = [@gradient-line]);
Title("Quests");
Div(style = [@gradient-line]);
}

Div(style = [@master-detail-container]) {
QuestList();
}
}

Add the following style to AppRoot.style:

style master-detail-container {
LayoutType = Horizontal;
SpaceBetweenHorizontal = 50px;
PreferredSize = 1s;
}

Everything here is straight forward. As you can see, the only new concept is that you supply the name of the template to reference it.

Add the Elements

The first thing you'll do is setup the base elements that the player will interact with. Open QuestList.ui and add the following code:

using QuestLog;
using System;
using System.Collections.Generic;

template QuestList {
render {
Group(style = [@quest-list-header]) {
Text_H2("Active", attr:uppercase);
Text_H2("Completed", attr:uppercase);
}

Div(style = [@accent]);
}
}

Group is another way to organize elements, like a Div. Text_H2 is the typography element that you created earlier. The uppercase attribute ensures that the text is displayed as such, thereby removing the dependency on the code to be correct.

Next, add the following styles to QuestList.style:

const color_LightWhite = #6D6C72;

style quest-list-header {
LayoutType = Horizontal;
SpaceBetweenHorizontal = 32px;
PreferredWidth = 496px;
PaddingLeft = 32px;
BorderBottom = 1px;
BorderColor = @color_LightWhite;
PaddingBottom = 8px;
MarginBottom = 12px;
}

style accent {

LayoutBehavior = Ignored;
PreferredSize = 90px 4px;
BackgroundColor = white;
AlignmentOriginX = -0.5w;
AlignmentOffsetX = 70px;
AlignmentOffsetY = 35px;
AlignmentTarget = Parent;
CornerRadius = 50%;
ShadowColor = white;
ShadowBlur = 15;
}

accent is introducing a few new style properties:

  • LayoutBehavior specifies whether the element should participate in the parent's layout phase. In this case, accent is set to Ignored, which means that it will not contribute to the parent size.
  • AlignmentOrigin, AlignmentOffset and AlignmentTarget are properties that adjust how the element aligns in relation to its parent. For example, because AlignmentOriginX is set to -0.5w, the element will adjust the X starting position to be negative half the width of the parent.
  • CornerRadius changes the element to have rounded corners by the specified amount.
  • ShadowColor and ShadowBlur can be used to add a shadow effect and are responsible for the nice glow that the bar has.
tip

Try changing the values of the elements above to see how they impact the visual look.

Push play to see how your new elements are drawn onto the UI. There's no interactivity yet though! You'll do that next.

no animation

Register Player Interactions

Two things need to happen when the player clicks on of the filters:

  • The bottom highlight needs to move to the new active state
  • The list of quests needs to filter based on their status

There also needs to be a check to make sure that nothing happens when the player clicks on the filter that is already enabled.

In this section you will focus just on animating the bottom highlight. Add the following code to the top of QuestList.UI:

template QuestList {
state QuestStatus status = QuestStatus.Active;

state float animTargetX = 0;
state float animTargetWidth = 0;

bool shouldStartAnimation = false;

state Action<UIElement, QuestStatus> QuestFilter_OnClick = (el, newStatus) => {
if(status == newStatus) {
return;
}

status = newStatus;

animTargetWidth = el.GetLayoutSize().width;
animTargetX = el.GetLayoutLocalPosition().x + animTargetWidth * 0.5f;

shouldStartAnimation = true;

};
//original code after this point
render {
...
}
}

The code above is defining and setting four state variables. State variables are a special kind of variable that persist across frames like regular C# variables. However, unlike regular variables, the value assignment runs just once when the template is first instantiated. Here is a deeper look at each variable:

  • status tracks whichever filter is currently active and is also set to QuestStatus.Active by default.
  • animTargetX and animTargetWidth set the target x position and width of the Div/Accent element that you created earlier. These values will be set again and used with the animation runs.
  • shouldStartAnimation represents whether code should run to move the Div/Accent element to a new filter.
  • QuestFilter_OnClick is an Action that will run each time the player clicks on a filter. The first thing it'll do is confirm that the player clicked on a new filter and will abort if they did not. The status is updated and animation values are set if the check passes.

It is now time to hook up the click event. Update the two Text_H2 elements so that they include a mouse:click event:

Group(style = [@quest-list-header]) {
Text_H2("Active", attr:uppercase,
mouse:click = QuestFilter_OnClick($this, QuestStatus.Active));

Text_H2("Completed", attr:uppercase,
mouse:click = QuestFilter_OnClick($this, QuestStatus.Completed));
});

Any element can register a callback. You just need to provide a prefix (i.e mouse), the event type (i.e. click), and assign it code to run. The Active and Completed filters are both registered to handle mouse:click events and pointed towards QuestFilter_OnClick.

info

There are many different types of events that you can hook into that cover mouse, keyboard, drag & drop, and touch events. You can read more here.

If you were to click on one of the buttons nothing would happen. That's because the actual animation logic is missing. Update the Div/Accent element at the bottom of your code with the following on:update event:

Div(style = [@accent], before:update => {
if (shouldStartAnimation) {
$this.style.SetAlignmentOffsetX(animTargetX);
$this.style.SetPreferredWidth(animTargetWidth);
}
});

Elements also have several lifecycle hooks that you can use. In this case, the on:update hook is used to check whether the element needs to be animated, and if so, sets a new AlignmentOffsetX and PreferredWidth values based on the animation state variables defined and set earlier.

Run or reload the project in Unity and try clicking on one of the filters.

no animation

Unfortunately, it's not smoothly animating just yet. This is because there's one style property that is missing from Accent. Open QuestList.style and update the accent style to include the following property:

style accent {
/* Other styles above */
transition CubicEaseIn 200ms = AlignmentOffsetX, PreferredWidth;
}

Transitions are easy to implement animations that observe style properties. When a property value is changed, the system will smoothly transition from the old value to the new one. Transitions are built on top of the animation system, which uses a generic interpolator that you can extend. In the property above, the standard CubicEaseIn interpolator is used. The duration is set to 200ms and the properties that the transition will monitor for are AlignmentOffsetX and PreferredWidth.

Run or reload the project again and you should now see it smoothly animating.

no animation