
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.

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:
- Start with a new copy of the mini-game
Simple2Dgame_SpaceGirl
. - In the Hierarchy panel, create a UI Panel
Panel–background
(Create | UI | Panel). - 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. - 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: - Add a UI Text object (Create | UI | Text), rename it
Text-inventory
, and change its text to Inventory. - In the Hierarchy panel, child this UI Text object to panel
Panel–background
. - In the Inspector panel, also set the font of
Text-inventory
to Xolonium-Bold (theFonts
folder). Center the text horizontally, top align the text vertically, set its Height to50
, and set the Font Size to23
. - 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 PanelPanel–background
and its width should stretch to match that of the whole panel. - Select the Canvas in the Hierarchy panel and add a new UI Panel object (Create | UI | Image). Rename it
Panel-slot-grid
. - 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. - In the Inspector panel, change the Height (in the Rect Transform component) of
Panel-slot-grid
to80
and set its Top to20
(so it is below UI Text GameObjectText-inventory
). - 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 to70
x70
and Spacing to5
x5
. 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: - 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 panelPanel-slot-grid
will ever be visible. - Add to your Canvas a UI Image object (Create | UI | Image). Rename it
Image-slot
. - In the Hierarchy panel, child UI Image object
Image-slot
to panelPanel–slot-grid
. - Set the Source Image of
Image-slot
to the Unity provided Knob (circle) image, as shown in the following screenshot: - Since
Image-slot
is the only UI object insidePanel-slot-grid,
it will be displayed (sized 70 x 70) in center in that panel, as shown in the following screenshot: - Each image slot will have a yellow star child image and a grey star child image. Let's create those now.
- Add to your Canvas a UI Image object (Create | UI | Image). Rename it
Image-star-yellow
. - In the Hierarchy panel, child UI Image object
Image-star-yellow
to imageImage–slot
. - Set the Source Image of
Image-star-yellow
to theicon_star_100
image (in folderSprites
). - 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 ImageImage-star-yellow
should now be visible in the middle of theImage-slot
circular Knob image, as shown in the following screenshot: - Duplicate
Image-star-yellow
in the Hierarchy panel, naming the copyImage-star-grey
. This new GameObject should also be a child ofImage-slot
. - Change the Source Image of
Image-star-grey
to theicon_star_grey_100
image (in folderSprites
). At any time, our inventory slot can now display nothing, a yellow star icon, or a grey star icon, depending on whetherImage-star-yellow
andImage-star-grey
are enabled or not: we'll control this through the inventory display code later in this recipe. - In the Hierarchy panel, ensure that
Image-slot
is selected, and add the C# ScriptPickupUI
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); } }
- With the GameObject
Image-slot
selected in the Hierarchy panel, drag each of its two childrenImage-star-yellow
andImage-star-grey
into their corresponding Inspector panel Pickup UI slots Star Yellow and Star Grey, as shown in the following screenshot: - In the Hierarchy panel, make nine duplicates of
Image-slot
in the Hierarchy panel; they should automatically be namedImage-slot 1 .. 9
. See the following screenshot to ensure the Hierarchy of your Canvas is correct—the parenting ofImage-slot
as a child ofImage-slot-grid
, and the parenting ofImage-star-yellow
andImage-star-grey
as children of eachImage-slot
is very important. - In the Hierarchy panel, ensure that
player-SpaceGirl
is selected, and add the C# scriptPlayer
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); } } }
- In the Hierarchy panel, ensure that
player-SpaceGirl
is selected, and add the C# scriptPlayerInventoryModel
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); } }
- In the Hierarchy panel, ensure that
player-SpaceGirl
is selected, and add the C# scriptPlayerInventoryDisplay
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(); } } }
- With GameObject
player-SpaceGirl
selected in the Hierarchy panel, drag the tenImage-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: - 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:
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.

To implement a horizontal scrollbar for our inventory display, we need to do the following:
- Increase the height of
Panel-background
to 130 pixels. - 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. - In the Hierarchy panel, duplicate Image-slot 9 three more times so that there are now 13 inventory icons in
Panel-slot-grid
. - 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 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. - Size and position
Panel-scroll-container
so that it is just behind ourPanel-slot-grid
. So, you should now see a red rectangle behind the 10 inventory circle slots. - In the Hierarchy panel, drag
Panel-slot-grid
so that it is now childed toPanel-scroll-container
. - 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. - 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-tintedPanel-scroll-container
, as shown in the following screenshot: - Add a UI Scroll Rect component to
Panel-scroll-container
. - In the Inspector panel, drag
Scrolbar-horizontal
to the Horizontal Scrollbar property of the Scroll Rect component ofPanel-scroll-container
. - In the Inspector panel, drag
Panel-slot-grid
to the Content property of the Scroll Rect component ofPanel-scroll-container
, as shown in the following screenshot: - Now, ensure the mask component of
Panel-scroll-container
is set as active so that we don't see the overflow ofPanel-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
.
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:
- Create a new folder named
Prefabs
. In this folder, create a new empty prefab namedstarUI
. - From the Hierarchy panel, drag the GameObject
Image-slot
into your new empty prefab namedstarUI
. This prefab should now turn blue, showing it is populated. - In the Hierarchy panel, delete GameObject
Image-slot
and all its copiesImage-slot 1 – 9
. - Replace C# Script
PlayerInventoryDisplay
in GameObjectplayer-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(); } } }
- With GameObject
player-SpaceGirl
selected in the Hierarchy panel, drag the GameObjectPanel-slot-grid
into Player Inventory Display (Script) variable Slot grid, in the Inspector panel. - With GameObject
player-SpaceGirl
selected in the Hierarchy panel, drag from the Project panel prefabstarUI
into Player Inventory Display (Script) variable Star Slot Prefab, in the Inspector panel, as shown in the following screenshot:
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.
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# ScriptPlayerInventoryDisplay
in GameObjectplayer-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); }
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.
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.