Skip to main content Link Search Menu Expand Document (external link)

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
  1. Today’s Tasks
  2. Create Reusable GridSlotElement
  3. Create Reusable GridElement
  4. Implement and Auto Sizing 9-Slice Inventory Panel

Today’s Tasks

  1. Create Reusable GridSlotElement Template with CellSize parameter
  2. Create GridElement Template with Rows, Columns, and CellSize parameter
  3. 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 GridElements 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 GridSlots within.

And finally, a GridElement which provides a Rows, Columns, and CellSize and is composed of multiple GridRowElements:

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.