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

Day 4: Clickable Items

Today, we used Leonaro.AI to generate equipment graphics, added a UI component for items, and connected the inventory grid logic with the UI through a controller script.

Table of contents
  1. Today’s Tasks
  2. Find / Generate Item Graphics
  3. Implement Simple InventoryGridController
  4. Wire up Inventory Logic

Today’s Tasks

  1. Find / Generate Item Graphics (1x1), (2x1), (1x2) (2x2), (3x2)
  2. Implement base InventoryGridController with drag and drop listeners
  3. Wire up InventoryGridController to InventoryGrid logic

Find / Generate Item Graphics

Before adding the logic to the UI, we wanted to make sure we had items we could use to test it out. Because the Captain lacks the ability to create art, we decided to take advantage of Leonardo.AI to generate a few item graphics for us. We wanted a variety of item shapes so we created 5 different shapes:

Potion Belt Dagger Shield Armor

Implement Simple InventoryGridController

The next step was to implement an InventoryGridController that could listen for mouse events and adjust where the images on the screen are placed.

To do this, we added two listeners to the GridSlotElements. One listens for the pointer entering the slot and the other listens for click events. These events are propagated through to the GridElement which can be registered on to know the Position that was most recently interacted with:

public event System.Action<GridSlotElement> OnPointerEntered;
public event System.Action<GridSlotElement> OnClicked;

private void OnPointerEnter(PointerEnterEvent evt) => OnPointerEntered?.Invoke(this);
private void OnPointerDown(PointerDownEvent evt) => OnClicked?.Invoke(this);

With this information exposed, the InventoryGridController listens for clicks and determines if an item is within the slot and “selecting” it. When an item is selected, it knows how to reposition itself based on the row / col of the mouse:

public class GridItemElement<T> : VisualElement where T : class, IGridableItem
{
    private bool IsSelected = false;
    private readonly GridElement _parent;
    public GridItemElement(Core.Position position, T item, GridElement parent)
    {
        // Initialization code omitted for brevity
    }

    // Called when the item is to be placed on the cursor
    public void Select()
    {
        IsSelected = true;
        _parent.OnPointerEntered += HandleMouseMove;
    } 

    // Called when the item should no longer be on the cursor
    public void UnSelect()
    {
        IsSelected = false;
        _parent.OnPointerEntered -= HandleMouseMove;
    }

    // If the item is selected, update its position when
    // the cursor enters a new GridSlot
    private void HandleMouseMove(GridSlotElement slot)
    {
        if (!IsSelected) { return; }
        style.top = slot.Position.Row * _parent.CellSize;
        style.left = slot.Position.Col * _parent.CellSize;
    }
}

Full Source: GitHub

Wire up Inventory Logic

With this in place, we could move the items freely within the InventoryGrid but the items themselves were not being moved in the data model. To do this, we added a few simple checks to the InventoryGridController that utilized the methods we wrote in the logic:

public class InventoryGridController<T> : MonoBehaviour where T : class, IGridableItem
{
    // The underlying data model for the inventory
    [field: SerializeField]
    public InventoryGridData<T> InventoryData { get; private set; }

    // Tracks the currently selected item
    private GridItemElement<T> _selected = null;

    // Used to look up the UI elements using the underlying data
    private readonly Dictionary<T, GridItemElement<T>> _itemLookup = new();
    
    // The overall Grid UI element
    private GridElement _gridElement;

    private void Awake()
    {
        VisualElement root = GetComponent<UIDocument>().rootVisualElement;
        _gridElement = root.Q<GridElement>("GridElement");
        // Update the UI Element to the correct dimensions
        _gridElement.UpdateDimensions(InventoryData.InventoryGrid.GridSize);
        // Listens for clicks
        _gridElement.OnClicked += HandleClick;
    }

    public bool AddItem(Core.Position position, T item)
    {
        if (!InventoryData.InventoryGrid.TrySetItemAt(position, item)) { return false; }

        GridItemElement<T> element = new(position, item, _gridElement);
        _gridElement.Add(element);
        _itemLookup[item] = element;
        return true;
    }

    // Determines if the click is picking up, putting down, or swapping an item
    private void HandleClick(GridSlotElement slot)
    {
        if (_selected == null)
        {
            HandleSelect(slot);
        }
        else 
        {
            HandleUnSelect(slot);
        }
    }

    // Called when we click on a slot is clicked and no item is currently selected. 
    // Checks to see if there is anything in the slot
    // if so, attempts to remove it and add it to the mouse cursor
    private void HandleSelect(GridSlotElement slot)
    {
        Debug.Assert(_selected == null, "HandleSelect should only be invoked when nothing is selected.");
        if (InventoryData.InventoryGrid.TryRemoveItemAt(slot.Position, out T item))
        {
            _selected = _itemLookup[item];
            _selected.Select();
        }
    }

    // Called when we click on a slot while holding an item.
    // Checks to see if the item can be added at that slot. If so,
    // checks to see if the add was a swap with another item.
    private void HandleUnSelect(GridSlotElement slot)
    {
        Debug.Assert(_selected != null, "HandleUnSelect should only be invoked when something is selected.");
        if (InventoryData.InventoryGrid.TrySetItemAt(slot.Position, _selected.Item, out T removed))
        {
            _selected.UnSelect();
            _selected = null;
            if (removed != null)
            {
                _selected = _itemLookup[removed];
                _selected.Select();
            }
        }
    }

}

And with that, we have a very simple but working grid based inventory system:

With one day to spare, we hope to clean up some of the more finicky UI things and see how difficult it will be to combine the inventory system with the crafting system we wrote last week.

Join the Discussion

Before commenting, you will need to authorize giscus. Alternatively, you can add a comment directly on the GitHub Discussion Board.