Dungeon Ho!: Unity – Dev Blog #12

Welcome back, Item Picker-Uppers!  Today we’re finally going to look at the PickUpItemsDialog and how to leverage all of the things we’ve done so far in order to get items from the map into the player’s inventory, and back again.  But first, we need to know how to trigger this sort of thing from a keypress.

Do you remember the DungeonMaster class?  That’s the one that handles player keypresses.  I’ve done a lot of things to this class since we’ve seen it last, but the thing we’re most interested in is the collectInput() method:

// Primary keyboard driver - Queries Input object and puts commands in queue.
    public void collectInput()
    { 
        UIScreen topmostScreen;

        if (uiManager == null)
            uiManager = dungeon.getGame().getUIManager();

        topmostScreen = uiManager.getTopmostUiComponent();

        // Hotkeys
        if (Input.GetKeyDown(KeyCode.P))
            addCommand(new PassCommand(dungeon.getPlayer()));

        // Hotkeys for particular screens ////////////////////////////////////////
        if (topmostScreen != null)
        {
            // We don't want the user to be able to operate stuff "under"
            // any currently-displaying UI components.  IE, they shouldn't
            // be able to move with the character screen up.
            topmostScreen.handleHotkey();

            if (Input.GetKeyDown(KeyCode.Escape))
                topmostScreen.hide();

            return;
        }

        // Screen activation /////////////////////////////////////////////////////
        if (Input.GetKeyDown(KeyCode.I))
            uiManager.showUI(UIManager.INVENTORY_SCREEN);

        if (Input.GetKeyDown(KeyCode.C))
            uiManager.showUI(UIManager.CHARACTER_SHEET);

        if (Input.GetKeyDown(KeyCode.G))
            uiManager.showUI(UIManager.PICK_UP_ITEMS);

        if (Input.GetKeyDown(KeyCode.Q))
            uiManager.showUI(UIManager.QUEST_LOG);

        // Directionals //////////////////////////////////////////////////////////
        if (Input.GetKeyDown(KeyCode.LeftArrow))
            addCommand(new MoveCommand(dungeon.getPlayer(), MoveCommand.WEST));

        if (Input.GetKeyDown(KeyCode.RightArrow))
            addCommand(new MoveCommand(dungeon.getPlayer(), MoveCommand.EAST));

        if (Input.GetKeyDown(KeyCode.UpArrow))
            addCommand(new MoveCommand(dungeon.getPlayer(), MoveCommand.NORTH));

        if (Input.GetKeyDown(KeyCode.DownArrow))
            addCommand(new MoveCommand(dungeon.getPlayer(), MoveCommand.SOUTH));

        // TODO: Read abstract game actions
        // TODO: Read mouse/clicks
    }

Fun stuff!  The first part of the method assigns a reference to the pre-existing UIManager so that we have something to work with, assuming we haven’t already done so, and the next bit determines the topmost UI Canvas so that we can feed it keypresses.  Basically, if there’s a screen already active, it should get all the keys and then exit the method.  If there isn’t a UI Canvas visible, then we can assume a keypress will be one of the player commands… such as picking up an item (…see what I did there?).

As you can see, we’ll trigger the PickUpItemsDialog with the ‘G’ key on the keyboard – and eventually the mouse, when we get that far.  But that’s a post for another time.

If you recall from last time, we can simply call the showUI() method of the UIManager to activate the relevant canvas.  In this case, it’s the UIManager.PICK_UP_ITEMS canvas.

Speaking of which, let’s take a look at it;

DHU 31

I’ll leave it as an exercise to you, the reader, to recreate this dialog in the Unity editor.  It’s basically just three buttons and a ScrollView.  Each element of the ScrollView is a Prefab denoting an entry in the list, which is currently just a Text label.  We’ll look at slot prefabs in more detail once we take a deep dive into the inventory screen.

public class PickUpItemsDialog : UIScreen
{
     public GameObject dungeon;
     public GameObject contents;
     public GameObject slotPrefab;
}

At its base, the PickUpItemsDialog is a UIScreen, the abstract class we looked at last time.  It has public slots for the dungeon, its own contents (the GameObject representing the ScrollView’s content object), and its object Prefab, which – as previously explained – is simply a Label.  The beauty of using a Prefab here is that we can design a new slot object, perhaps with icons or something, and swap it out with no extra effort on our end apart from maybe having to change a line or two of code to not reference the Label if we decided to delete it.  Simply drag and drop the relevant game objects into these slots in the Editor and away we go.

Next up is the overridden method, prepare():

public override void prepare()
{
     Vector2Int playerPos;

     base.prepare();

     playerPos = player.getPosition();

     populateList(dungeon.GetComponent<Dungeon>().getAllEntitiesOnTile(playerPos.x, playerPos.y));
}

We’ll need the player’s position, so we grab it after we call the base prepare() method (so that we have a player to grab it from, among other things).  Then we populate the list of items based on the objects we get from the dungeon object.  (Since the dungeon is a GameObject, we get a reference to its Dungeon component first, since that’s where all of the data is.)

private void populateList(List<Entity> entsOnTile)
{
     clearcontents();

     if (entsOnTile != null)
     {
          foreach (Entity entity in entsOnTile)
          {
               if (entity != player)
                    createEntitySlot(entity).transform.SetParent(contents.transform);
          }
     }
}

private void clearcontents()
{
     foreach (Transform text in contents.transform)
     {
          GameObject.Destroy(text.gameObject);
     }
}

populateList() populates the contents object with slot prefabs based on the items passed into the method.  First it removes all of the existing items from the list, then creates a new Slot prefab for each item and attaches it by parenting it to the contents object.  The slot prefab is created in the createEntitySlot() method, which spawns a new instance of the Slot prefab and sets its text to the name property of the passed-in item.

private GameObject createEntitySlot(Entity item)
{
     GameObject slot = Instantiate(slotPrefab);

     PickupSlot slotComponent = slot.GetComponent<PickupSlot>();

     slot.name = slot.name.Replace("(Clone)", "");

     slotComponent.pickupDialog = this;

     slotComponent.setItem(item);

     slot.GetComponentInChildren<Text>().text = item.name;

     return slot;
}

Each SlotComponent (…which we’ll look at next) contains a reference to the dialog, as well as the Item object it’s attached to.  We need both of these objects so that it can manipulate the dialog and tell the PickUpCommand which item it should run on when the user clicks the relevant button.

Speaking of the PickupSlot Prefab object, it has the following script attached;

public class PickupSlot : MonoBehaviour
{
     private Color unselectedColor = Color.black;
     private Color selectedColor = Color.green;

     private Entity item;

     private bool selected;

     public PickUpItemsDialog pickupDialog;

     public void setItem(Entity i)
     {
          item = i;
     }

     public Entity getItem()
     {
          return item;
     }

     public void toggleSelected()
     {
          selected = !selected;

          if (selected)
               gameObject.GetComponentInChildren<Text>().color = selectedColor;
          else
               gameObject.GetComponentInChildren<Text>().color = unselectedColor;
     }

     public bool isSelected()
     {
          return selected;
     }
}

The most interesting method is the toggleSelected() method, which changes the color of the Prefab based on whether or not it’s selected.  When the user clicks on an item in the Pickup dialog, we want to highlight it/de-highlight it so that it’s included (or not) in the items that are to be picked up.  Therefore, each slot has a boolean that determines if it’s been selected or not.

So, how does all of that work?  Well, it requires an Event Trigger.  We define one and then attach it to the PickupSlot prefab, like so;

public class PickUpItemEventTriggerHandler : EventTrigger
{
     public override void OnPointerClick(PointerEventData data)
     {
          PickupSlot slot = gameObject.GetComponent<PickupSlot>();

          slot.pickupDialog.onItemSelected(slot);
     }
}

The OnPointerClick() method is automagically called when the Prefab is clicked, passing in the Event data of the PointerEvent that was generated by said click.  The event was generated by the gameObject the script is attached to, so we simply grab the PickupSlot script component of that gameObject, then call its attached pickupDialog’s onItemSelected() method, passing in that slot component.  We do this because the dialog may have to do some bookkeeping relevant to itself when the slot is selected, and we don’t want to tangle that code up in here.  Keeps everything neat.

The onItemSelected() method looks like this;

public void onItemSelected(PickupSlot slot)
{
     slot.toggleSelected();
}

And finally, we use the Unity Editor to call either pickUpSelectedItems() or pickUpAllItems() on the PickUpItemsDialog when the relevant button has been clicked.  pickUpAllItems() simply loops through all of the slots attached to the ScrollView and generates a PickUpItemCommand for each one, so we won’t bother to look at it.  We’ll look at pickUpSelectedItems() instead.

public void pickUpSelectedItems()
{
     DungeonMaster dm = dungeon.GetComponent<Dungeon>().getDungeonMaster();

     PickupSlot slot;

     foreach (Transform child in contents.transform)
     {
          slot = child.gameObject.GetComponent<PickupSlot>();

          if (slot.isSelected())
               dm.addCommand(new PickUpItemCommand(player, slot.getItem()));
     }

     hide();
}

As you can see, the foreach loop checks all of the contents’ object’s children to see if their PickupSlot component is selected.  If it is, a new PickUpItemCommand – which we saw way back when we looked at Command objects – is generated with the player as the source and the Item entity linked to the slot as the target.  The DungeonMaster object then processes all of these commands.

Could we have simply added the selected items to a separate list and then looped through that list and picked up all the items?  Sure, we could have.  Whether or not that’s a better solution is left as an exercise to the reader.  Finally, though, we call hide() on the pickupDialog, which hides it from view until we’re ready to pick up more items.

There’s one final note, here; I’ve implemented a maximum length on the player’s command queue, so if there are more items in the tile than slots in the queue, the player will only be able to pick up as many as there are free slots.  So for example, if the queue’s maximum length is 10 and there’s 20 objects in the tile, the player will only be able to pick up 10 items with one click, no matter how many they select in the dialog.

Well, that certainly was a big one! (obligatory “That’s what she said” joke here.) Hope it was worth the wait!  We’ll be using a similar model for other UI dialogs, especially the Inventory screen.  Tune in next time when we do just that, which will give us the ability to drop items back into the dungeon.

– Steve

Categories: Development, Dungeon Ho!, Unity | Leave a comment

Post navigation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: