Create and Animate the Quest Filter
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:
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 toIgnored
, which means that it will not contribute to the parent size.AlignmentOrigin
,AlignmentOffset
andAlignmentTarget
are properties that adjust how the element aligns in relation to its parent. For example, becauseAlignmentOriginX
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
andShadowBlur
can be used to add a shadow effect and are responsible for the nice glow that the bar has.
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.
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 toQuestStatus.Active
by default.animTargetX
andanimTargetWidth
set the target x position and width of theDiv/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 theDiv/Accent
element to a new filter.QuestFilter_OnClick
is anAction
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
.
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.
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.