Day 3: Grid Logic System
Today, we explored using the UI Toolkit once again to create parameterized elements that can be reused in the UI Toolkit builder. More specifically, we implemented a reusable GridElement
component. Additionally, we explored how to do 9-slicing on sprites within the UI Toolkit Builder to automatically resize our container borders to wrap the size of the grid.
Table of contents
Today’s Tasks
-
Create Reusable GridSlotElement Template with CellSize parameter -
Create GridElement Template with Rows, Columns, and CellSize parameter -
Explore 9-slice Inventory Panel for background
Create Reusable GridSlotElement
The first goal was to attempt to create a reusable GridSlotElement
that would be used to represent a single cell of an inventory on the screen. The main idea is that this should be a single element that can be easily modified to change the look and feel of a slot throughout the program with ease.
To do this, we created a subclass of the VisualElement
. One requirement to use a VisualElement
in the UI Builder is that it MUST provide a zero-argument constructor. To style it, we will provide a .grid-slot
class that can be swapped out in any program to change the styling:
public class GridSlotElement : VisualElement
{
public GridSlotElement()
{
AddToClassList("grid-slot");
}
public int CellSize { get; set; }
}
To be able to access a VisualElement
in the UI Builder, you must also provide a UxmlFactory
that knows how to create it. From the documentation, the canonical way of doing this is to create an inner class that shadows the parent class name using the new
keyword:
public class GridSlotElement : VisualElement
{
// ...
public new class UxmlFactory : UxmlFactory<GridSlotElement> { }
}
With just this, you will have a GridSlotElement
available in the UI Builder’s Custom Controls Library.
However, because we would also like to provide the ability to specify the CellSize
in the UI Builder, we must also provide a UxmlTraits
specifically for the GridSlotElement
. Again, this is canonically done by shadowing the parent class and overriding the Init
method:
public class GridSlotElement : VisualElement
{
public GridSlotElement()
{
AddToClassList("grid-slot");
}
public int CellSize { get; set; }
// Must use UxmlFactory<GridSlotElement, UxmlTraits> to get the traits in the UI Builder
public new class UxmlFactory : UxmlFactory<GridSlotElement, UxmlTraits> { }
// Shadows the name for convenience
public new class UxmlTraits : VisualElement.UxmlTraits
{
// There are a variety of Uxml Attributes you can create for CellSize we want an int
private UxmlIntAttributeDescription _cellSize = new () { name = "cell-size", defaultValue = 32 };
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
// TODO: What should we do to intialize this element?
}
}
}
One thing I found quite annoying was the requirement that the name
attribute on this line:
private UxmlIntAttributeDescription _cellSize = new () { name = "cell-size", defaultValue = 32 };
must use kebab case and match with a public property that uses Pascal or camelCasing in the parent class:
public int CellSize { get; set; }
I wasn’t able to find where this was documented and took quite a bit of time to discover.
To be able to set the CellSize
we added an Init
method to the class that serves as a “constructor” for the object and sets the width
and height
of the cell. Lastly, we updated the UxmlTraits.Init
to forward the information from the UI Builder to the element:
public class GridSlotElement : VisualElement
{
public GridSlotElement(int cellSize) : this() => Init(cellSize);
public GridSlotElement() => AddToClassList("grid-slot");
public int CellSize { get; set; }
private void Init(int cellSize) {
CellSize = cellSize;
style.width = cellSize;
style.height = cellSize;
}
public new class UxmlFactory : UxmlFactory<GridSlotElement, UxmlTraits> { }
public new class UxmlTraits : VisualElement.UxmlTraits
{
private UxmlIntAttributeDescription _cellSize = new () { name = "cell-size", defaultValue = 32 };
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var slot = ve as GridSlotElement;
slot.Init(_cellSize.GetValueFromBag(bag, cc));
}
}
}
With this added, we can now add GridElement
s to UI Document using the UI Builder and adjust the size using the CellSize
property:
Create Reusable GridElement
With a basic understanding of creating custom elements in the UI Builder, we continued to implement a GridRowElement
which provides a Columns
and CellSize
attribute and is composed with multiple GridSlot
s within.
And finally, a GridElement
which provides a Rows
, Columns
, and CellSize
and is composed of multiple GridRowElement
s:
The final version of these files from today’s session are available on github:
Implement and Auto Sizing 9-Slice Inventory Panel
Elitestomper, an amazing member of the crew, designed a fantastic UI component that we wanted to use as a border for the inventory. Using the old Unity UI Canvas system, it is very easy to take an image and us a technique called 9-Slice to make dialogs / windows resize in a nice way. To do this in UI Toolkit is slightly different but not difficult.
We created a new UXML Document to use as a wrapper for our inventory grid and added a VisualElement
which would be a container for the grid. Our goal was to make that element use the sprite and dynamically resize to the size of a GridElement
. Here is the final result:
To accomplish this we created an inventory-border
style which is applied to the parent container. It specifies the sprite we would like to use as a background, as well as how to slice it. Additionally, we specify a padding to push the grid within the bounds.
.inventory-border {
background-color: rgba(0, 0, 0, 0);
background-image: url('project://database/Assets/Sprites/InventoryMainBox.png');
-unity-background-scale-mode: stretch-to-fill;
-unity-slice-left: 30;
-unity-slice-top: 30;
-unity-slice-right: 30;
-unity-slice-bottom: 30;
padding-left: 30px;
padding-right: 30px;
padding-top: 30px;
padding-bottom: 30px;
}
Not too bad! Now that we have a reusable InventoryGrid component, we are ready to attempt to implement a drag and drop inventory system. We only have 2 days left to finish! Can we do it? Only time will tell!
Join the Discussion
Before commenting, you will need to authorize giscus. Alternatively, you can add a comment directly on the GitHub Discussion Board.