Generalizing multiple icon displays using UI Grid Layout Groups (with scrollbars!)

The recipes in this chapter up to this point have been hand-crafted for each situation. While this is fine, more general and automated approaches to inventory UIs can sometimes save time and effort but still achieve visual and usability results of equal quality. In the next recipe, we will begin to explore a more engineered approach to inventory UIs by exploiting the automated sizing and layouts offered by Unity 5's Grid Layout Group component.

Generalizing multiple icon displays using UI Grid Layout Groups (with scrollbars!)

Getting ready

This recipe assumes that you are starting with the project Simple2Dgame_SpaceGirl setup from the first recipe in this chapter. The font you need can be found in the 1362_02_02 folder.

How to do it...

To display grey and yellow star icons for multiple object pickups using UI grid layout groups, follow these steps:

  1. Start with a new copy of the mini-game Simple2Dgame_SpaceGirl.
  2. In the Hierarchy panel, create a UI Panel Panel–background (Create | UI | Panel).
  3. Let's now position Panel–background at the top of the Game panel, stretching the horizontal width of the canvas. Edit the UI Image's Rect Transform component, and while holding down SHIFT and ALT (to set pivot and position), choose the top-stretch box.
  4. The panel will still be taking up the whole game window. So, now in the Inspector panel, change the Height (in the Rect Transform component) of Panel–background to 100, as shown in the following screenshot:
    How to do it...
  5. Add a UI Text object (Create | UI | Text), rename it Text-inventory, and change its text to Inventory.
  6. In the Hierarchy panel, child this UI Text object to panel Panel–background.
  7. In the Inspector panel, also set the font of Text-inventory to Xolonium-Bold (the Fonts folder). Center the text horizontally, top align the text vertically, set its Height to 50, and set the Font Size to 23.
  8. Edit the Rect Transform of Text-inventory, and while holding down SHIFT and ALT (to set pivot and position), choose the top-stretch box. The text should now be positioned at the middle top of the UI Panel Panel–background and its width should stretch to match that of the whole panel.
  9. Select the Canvas in the Hierarchy panel and add a new UI Panel object (Create | UI | Image). Rename it Panel-slot-grid.
  10. Position Panel-slot-grid at the top of the Game panel, stretching the horizontal width of the canvas. Edit the UI Image's Rect Transform component, and while holding down SHIFT and ALT (to set pivot and position), choose the top-stretch box.
  11. In the Inspector panel, change the Height (in the Rect Transform component) of Panel-slot-grid to 80 and set its Top to 20 (so it is below UI Text GameObject Text-inventory).
  12. With the panel Panel-slot-grid selected in the Hierarchy panel, add a grid layout group component (Add Component | Layout | Grid Layout Group). Set Cell Size to 70 x 70 and Spacing to 5 x 5. Also, set the Child Alignment to Middle Center (so our icons will have even spacing at the far left and right), as shown in the following screenshot:
    How to do it...
  13. With the panel Panel-slot-grid selected in the Hierarchy panel, add a mask (script) component (Add Component | UI | Mask). Uncheck the option Show Mask Graphic. Having this mask component means that any overflow of our grid will NOT be seen by the user—only content within the image area of the panel Panel-slot-grid will ever be visible.
  14. Add to your Canvas a UI Image object (Create | UI | Image). Rename it Image-slot.
  15. In the Hierarchy panel, child UI Image object Image-slot to panel Panel–slot-grid.
  16. Set the Source Image of Image-slot to the Unity provided Knob (circle) image, as shown in the following screenshot:
    How to do it...
  17. Since Image-slot is the only UI object inside Panel-slot-grid, it will be displayed (sized 70 x 70) in center in that panel, as shown in the following screenshot:
    How to do it...
  18. Each image slot will have a yellow star child image and a grey star child image. Let's create those now.
  19. Add to your Canvas a UI Image object (Create | UI | Image). Rename it Image-star-yellow.
  20. In the Hierarchy panel, child UI Image object Image-star-yellow to image Image–slot.
  21. Set the Source Image of Image-star-yellow to the icon_star_100 image (in folder Sprites).
  22. Now we will set our yellow star icon image to fully fill its parent Image-slot by stretching horizontally and vertically. Edit the UI Image's Rect Transform component, and while holding down SHIFT and ALT (to set pivot and position), choose the bottom right option to fully stretch horizontally and vertically. The UI Image Image-star-yellow should now be visible in the middle of the Image-slot circular Knob image, as shown in the following screenshot:
    How to do it...
  23. Duplicate Image-star-yellow in the Hierarchy panel, naming the copy Image-star-grey. This new GameObject should also be a child of Image-slot.
  24. Change the Source Image of Image-star-grey to the icon_star_grey_100 image (in folder Sprites). At any time, our inventory slot can now display nothing, a yellow star icon, or a grey star icon, depending on whether Image-star-yellow and Image-star-grey are enabled or not: we'll control this through the inventory display code later in this recipe.
  25. In the Hierarchy panel, ensure that Image-slot is selected, and add the C# Script PickupUI with the following code:
    using UnityEngine;
    using System.Collections;
    
    public class PickupUI : MonoBehaviour {
      public GameObject starYellow;
      public GameObject starGrey;
    
      void Awake(){
        DisplayEmpty();
      }
    
      public void DisplayYellow(){
        starYellow.SetActive(true);
        starGrey.SetActive(false);
      }
    
      public void DisplayGrey(){
        starYellow.SetActive(false);
        starGrey.SetActive(true);
      }
      
      public void DisplayEmpty(){
        starYellow.SetActive(false);
        starGrey.SetActive(false);
      }
    }
  26. With the GameObject Image-slot selected in the Hierarchy panel, drag each of its two children Image-star-yellow and Image-star-grey into their corresponding Inspector panel Pickup UI slots Star Yellow and Star Grey, as shown in the following screenshot:
    How to do it...
  27. In the Hierarchy panel, make nine duplicates of Image-slot in the Hierarchy panel; they should automatically be named Image-slot 1 .. 9. See the following screenshot to ensure the Hierarchy of your Canvas is correct—the parenting of Image-slot as a child of Image-slot-grid, and the parenting of Image-star-yellow and Image-star-grey as children of each Image-slot is very important.
    How to do it...
  28. In the Hierarchy panel, ensure that player-SpaceGirl is selected, and add the C# script Player with the following code:
    using UnityEngine;
    using System.Collections;
    using UnityEngine.UI;
    
    public class Player : MonoBehaviour {
      private PlayerInventoryModel playerInventoryModel;
    
      void Start(){
        playerInventoryModel = GetComponent<PlayerInventoryModel>();
      }
      
      void OnTriggerEnter2D(Collider2D hit){
        if(hit.CompareTag("Star")){
          playerInventoryModel.AddStar();
          Destroy(hit.gameObject);
        }
      }
    }
  29. In the Hierarchy panel, ensure that player-SpaceGirl is selected, and add the C# script PlayerInventoryModel with the following code:
    using UnityEngine;
    using System.Collections;
    
    public class PlayerInventoryModel : MonoBehaviour {
      private int starTotal = 0;
      private PlayerInventoryDisplay playerInventoryDisplay;
      
      void Start(){
        playerInventoryDisplay = GetComponent<PlayerInventoryDisplay>();
        playerInventoryDisplay.OnChangeStarTotal(starTotal);
      }
      
      public void AddStar(){
        starTotal++;
        playerInventoryDisplay.OnChangeStarTotal(starTotal);
      }
    }
  30. In the Hierarchy panel, ensure that player-SpaceGirl is selected, and add the C# script PlayerInventoryDisplay with the following code:
    using UnityEngine;
    using System.Collections;
    using UnityEngine.UI;
    
    public class PlayerInventoryDisplay : MonoBehaviour
    {
      const int NUM_INVENTORY_SLOTS = 10;
      public PickupUI[] slots = new PickupUI[NUM_INVENTORY_SLOTS];
    
      public void OnChangeStarTotal(int starTotal){
        for(int i = 0; i < NUM_INVENTORY_SLOTS; i++){
          PickupUI slot = slots[i];
          if(i < starTotal)
            slot.DisplayYellow();
          else
            slot.DisplayGrey();
        }
      }
    }
  31. With GameObject player-SpaceGirl selected in the Hierarchy panel, drag the ten Image-slot GameObjects into their corresponding locations in the Player Inventory Display (Script) component array Slots, in the Inspector panel, as shown in the following screenshot:
    How to do it...
  32. Save the scene and play the game. As you pick up stars, you should see more of the grey stars change to yellow in the inventory display.

How it works...

We have created a simple panel (Panel-background) and text at the top of the game canvas—showing a greyish background rectangle and text "Inventory". We created a small panel inside this area (Panel-slot-grid), with a grid layout group component, which automatically sizes and lays out the 10 Image-slot GameObjects we created with the knob (circle) source image. By adding a mask component to Panel-slot-grid, we ensure that no content will overflow outside of the rectangle of the source image for this panel.

Each of the 10 Image-slot GameObjects that are children of Panel-slot-grid contains a yellow star image and a grey star image. Also, each Image-slot GameObjects has a script component PickupUI. The PickupUI script offers three public methods, which will show just the yellow star image, just the grey star image, or neither (so, an empty knob circle image will be seen).

Our player's character GameObject player-SpaceGirl has a very simple basic Player script—this just detected collisions with objects tagged Star, and when this happens, it removes the star GameObject collided with and calls the AddStar() method to its playerInventoryModel scripted component. The PlayerInventoryModel C# script class maintains a running integer total of the number of stars added to the inventory. Each time the AddStar() method is called, it increments (adds 1) to this total, and then calls the OnChangeStarTotal(…) method of scripted component playerInventoryDisplay. Also, when the scene starts, an initial call is made to the OnChangeStarTotal(…) method so that the UI display for the inventory is set up to show that we are initially carrying no stars.

The C# script class PlayerInventoryDisplay has two properties: one is a constant integer defining the number of slots in our inventory, which for this game we set to 10, and the other variable is an array of references to PickupUI scripted components—each of these is a reference to the scripted component in each of the 10 Image-slot GameObjects in our Panel-slot-grid. When the OnChangeStarTotal(…) method is passed the number of stars we are carrying, it loops through each of the 10 slots. While the current slot is less than our star total, a yellow star is displayed, by the calling of the DisplayYellow() method of the current slot (PickupUI scripted component). Once the loop counter is equal to or larger than our star total, then all remaining slots are made to display a grey star via the calling of method DisplayGrey().

This recipe is an example of the low coupling of the MVC design pattern. We have designed our code to not rely or make too many assumptions about other parts of the game so that the chances of a change in some other part of our game breaking our inventory display code are much smaller. The display (view) is separated from the logical representation of what we are carrying (model), and changes to the model are made by public methods called from the player (controller).

Note

Note: It might seem that we could make our code simpler by assuming that slots are always displaying grey (no star) and just changing one slot to yellow each time a yellow star is picked up. But this would lead to problems if something happens in the game (for example, hitting a black hole or being shot by an alien) that makes us drop one or more stars. C# script class PlayerInventoryDisplay makes no assumptions about which slots may or may not have been displayed grey or yellow or empty previously—each time it is called, it ensures that an appropriate number of yellow stars are displayed, and all other slots are displayed with grey stars.

There's more...

Some details you don't want to miss:

Add a horizontal scrollbar to the inventory slot display

We can see 10 inventory slots now—but what if there are many more? One solution is to add a scroll bar so that the user can scroll left and right, viewing 10 at a time, as shown in the following screenshot. Let's add a horizontal scroll bar to our game. This can be achieved without any C# code changes, all through the Unity 5 UI system.

Add a horizontal scrollbar to the inventory slot display

To implement a horizontal scrollbar for our inventory display, we need to do the following:

  1. Increase the height of Panel-background to 130 pixels.
  2. In the Inspector panel, set the Child Alignment property of component Grid Layout Group (Script) of Panel-slot-grid to Upper Left. Then, move this panel to the right a little so that the 10 inventory icons are centered on screen.
  3. In the Hierarchy panel, duplicate Image-slot 9 three more times so that there are now 13 inventory icons in Panel-slot-grid.
  4. In the Scene panel, drag the right-hand edge of panel Panel-slot-grid to make it wide enough so that all 13 inventory icons fit horizontally—of course the last three will be off screen, as shown in the following screenshot:
    Add a horizontal scrollbar to the inventory slot display
  5. Add a UI Panel to the Canvas and name it Panel-scroll-container, and tint it red by setting the Color property of its Image (Script) component to red.
  6. Size and position Panel-scroll-container so that it is just behind our Panel-slot-grid. So, you should now see a red rectangle behind the 10 inventory circle slots.
  7. In the Hierarchy panel, drag Panel-slot-grid so that it is now childed to Panel-scroll-container.
  8. Add a UI Mask to Panel-scroll-container so now you should only be able to see the 10 inventory icons that fit within the rectangle of this red-tinted panel.

    Note

    Note: You may wish to temporarily set this mask component as inactive so that you can see and work on the unseen parts of Panel-slot-grid if required.

  9. Add a UI Scrollbar to the Canvas and name it Scrollbar-horizontal. Move it to be just below the 10 inventory icons, and resize it to be the same width as the red-tinted Panel-scroll-container, as shown in the following screenshot:
    Add a horizontal scrollbar to the inventory slot display
  10. Add a UI Scroll Rect component to Panel-scroll-container.
  11. In the Inspector panel, drag Scrolbar-horizontal to the Horizontal Scrollbar property of the Scroll Rect component of Panel-scroll-container.
  12. In the Inspector panel, drag Panel-slot-grid to the Content property of the Scroll Rect component of Panel-scroll-container, as shown in the following screenshot:
    Add a horizontal scrollbar to the inventory slot display
  13. Now, ensure the mask component of Panel-scroll-container is set as active so that we don't see the overflow of Panel-slot-grid and uncheck this mask components option to Show Mask Graphic (so that we don't see the red rectangle any more).

You should now have a working scrollable inventory system. Note that the last three new icons will just be empty circles, since the inventory display script does not have references to, or attempt to make, any changes to these extra three slots; so the script code would need to be changed to reflect every additional slot we add to Panel-slot-grid.

The automation of PlayerInventoryDisplay getting references to all the slots

There was a lot of dragging slots from the Hierarchy panel into the array for the scripted component PlayerInventoryDisplay. This takes a bit of work (and mistakes might be made when dragging items in the wrong order or the same item twice). Also, if we change the number of slots, then we may have to do this all over again or try to remember to drag more slots if we increase the number, and so on. A better way of doing things is to make the first task of the script class PlayerInventoryDisplay when the scene begins to create each of these Image-slot GameObjects as a child of Panel-slot-grid and populate the array at the same time.

To implement the automated population of our scripted array of PickupUI objects for this recipe, we need to do the following:

  1. Create a new folder named Prefabs. In this folder, create a new empty prefab named starUI.
  2. From the Hierarchy panel, drag the GameObject Image-slot into your new empty prefab named starUI. This prefab should now turn blue, showing it is populated.
  3. In the Hierarchy panel, delete GameObject Image-slot and all its copies Image-slot 1 – 9.
  4. Replace C# Script PlayerInventoryDisplay in GameObject player-SpaceGirl with the following code:
    using UnityEngine;
    using System.Collections;
    using UnityEngine.UI;
    
    public class PlayerInventoryDisplay : MonoBehaviour
    {
      const int NUM_INVENTORY_SLOTS = 10;
      private PickupUI[] slots = new PickupUI[NUM_INVENTORY_SLOTS];
      public GameObject slotGrid;
      public GameObject starSlotPrefab;
    
      void Awake(){
        for(int i=0; i < NUM_INVENTORY_SLOTS; i++){
          GameObject starSlotGO = (GameObject)
          Instantiate(starSlotPrefab);
          starSlotGO.transform.SetParent(slotGrid.transform);
          starSlotGO.transform.localScale = new Vector3(1,1,1);
          slots[i] = starSlotGO.GetComponent<PickupUI>();
        }
      }
    
      public void OnChangeStarTotal(int starTotal){
        for(int i = 0; i < NUM_INVENTORY_SLOTS; i++){
         PickupUI slot = slots[i];
          if(i < starTotal)
            slot.DisplayYellow();
          else
            slot.DisplayGrey();
        }
      }
    }
  5. With GameObject player-SpaceGirl selected in the Hierarchy panel, drag the GameObject Panel-slot-grid into Player Inventory Display (Script) variable Slot grid, in the Inspector panel.
  6. With GameObject player-SpaceGirl selected in the Hierarchy panel, drag from the Project panel prefab starUI into Player Inventory Display (Script) variable Star Slot Prefab, in the Inspector panel, as shown in the following screenshot:
    The automation of PlayerInventoryDisplay getting references to all the slots

The public array has been made private and no longer needs to be populated through manual drag-and-drop. When you run the game, it will play just the same as before, with the population of the array of images in our inventory grid panel now automated. The Awake() method creates new instances of the prefab (as many as defined by constant NUM_INVENTORY_SLOTS) and immediately childed them to Panel-slot-grid. Since we have a grid layout group component, their placement is automatically neat and tidy in our panel.

Tip

Note: The scale property of the transform component of GameObjects is reset when a GameObject changes its parent (to maintain relative child size to parent size). So, it is a good idea to always reset the local scale of GameObjects to (1,1,1) immediately after they have been childed to another GameObject. We do this in the for-loop to starSlotGO immediately following the SetParent(…) statement.

Note that we use the Awake() method for creating the instances of the prefab in PlayerInventoryDispay so that we know this will be executed before the Start() method in PlayerInventoryModel—since no Start() method is executed until all Awake() methods for all GameObjects in the scene have been completed.

Automatically changing the grid cell size based on the number of slots in inventory

Consider a situation where we wish to change the number of slots. Another alternative to using scrollbars is to change the cell size in the Grid Layout Group component. We can automate this through code so that the cell size is changed to ensure that NUM_INVENTORY_SLOTS will fit along the width of our panel at the top of the canvas.

To implement the automated resizing of the Grid Layout Group cell size for this recipe, we need to do the following:

  • Add the following method Start() to the C# Script PlayerInventoryDisplay in GameObject player-SpaceGirl with the following code:
    void Start(){
      float panelWidth = slotGrid.GetComponent<RectTransform>().rect.width;
      print ("slotGrid.GetComponent<RectTransform>().rect = " + slotGrid.GetComponent<RectTransform>().rect);
      
      GridLayoutGroup gridLayoutGroup = slotGrid.GetComponent<GridLayoutGroup>();
      float xCellSize = panelWidth / NUM_INVENTORY_SLOTS;
      xCellSize -= gridLayoutGroup.spacing.x;
      gridLayoutGroup.cellSize = new Vector2(xCellSize, xCellSize);
    }
    Automatically changing the grid cell size based on the number of slots in inventory

We write our code in the Start() method, rather than adding to code in the Awake() method, to ensure that the RectTransform of GameObject Panel-slot-grid has finished sizing (in this recipe, it stretches based on the width of the Game panel). While we can't know the sequence in which Hierarchy GameObjects are created when a scene begins, we can rely on the Unity behavior that every GameObject sends the Awake()message, and only after all corresponding Awake() methods have finished executing all objects, and then sends the Start() message. So, any code in the Start() method can safely assume that every GameObject has been initialized.

The above screenshot shows the value of NUM_INVENTORY_SLOTS having been changed to 15, and the cell size, having been corresponding, changed, so that all 15 now fit horizontally in our panel. Note that the spacing between cells is subtracted from the calculated available with divided by the number of slots (xCellSize -= gridLayoutGroup.spacing.x) since that spacing is needed between each item displayed as well.

Add some help methods to the Rect Transform script class

If we wish to further change, say, the RectTransform properties using code, we can add extension methods by creating a file containing special static methods and using the special "this" keyword. See the following code that adds SetWidth(…), SetHeight(…), and SetSize(…) methods to the RectTransform scripted component:

using UnityEngine;
using System;
using System.Collections;

public static class RectTransformExtensions
{
  public static void SetSize(this RectTransform trans, Vector2 newSize) {
    Vector2 oldSize = trans.rect.size;
    Vector2 deltaSize = newSize - oldSize;
    trans.offsetMin = trans.offsetMin - new Vector2(deltaSize.x * trans.pivot.x, deltaSize.y * trans.pivot.y);
    trans.offsetMax = trans.offsetMax + new Vector2(deltaSize.x * (1f - trans.pivot.x), deltaSize.y * (1f - trans.pivot.y));
  }

  public static void SetWidth(this RectTransform trans, float newSize) {
    SetSize(trans, new Vector2(newSize, trans.rect.size.y));
  }

  public static void SetHeight(this RectTransform trans, float newSize) {
    SetSize(trans, new Vector2(trans.rect.size.x, newSize));
  }
}

Unity C# allows us to add these extensions methods by declaring static void methods whose first argument is in the form this <ClassName> <var>. The method can then be called as a built-in method defined in the original class.

All we would need to do is create a new C# script class file RectTransformExtensions in the folder Scripts in the Project panel, containing the above code. In fact, you can find a whole set of useful extra RectTransform methods (on which the above is an extract) created by OrbcreationBV, and it is available online at http://www.orbcreation.com/orbcreation/page.orb?1099.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.88.157