Unity3D Workshop – 04 – Putting U & I Together

Welcome to the fourth in installment of the Unity3D workshops, in these workshops we will take a closer look at game development by taking a game concept and making a simple version of it in the Unity3D engine (version 4.1.1 at the time of writing).  For this workshop, we will be investigating game ‘UI’, an element that all games have in common.  However, there is a style of game that is run primarily using simple UI and that is a interactive story.

Hatoful Boyfriend, Fire Emblem, Harvest Moon

All of these games use ‘Talking Heads’ to communicate to the player plot or other details.  In additional to simple conversations we will also setup a basic menu and scrolling text intro screen.

The asset package can be downloaded: here.

Getting started we open up a new Unity3D project and drop in our asset package, right off the bat you will notice a couple of new things in the file structure.

Step 01: Covering our assets

This comes with a few scripts in the ‘Editor’ folder, this is some extra code use to process the ‘XML’ files found in the aptly named ‘XML’ folder.  We will go over how this works in the associated appendix but for now it is not something to dive into due to it being very code heavy and would bog down this workshop.

The first thing we want to do for our UI is create a custom “GUI Skin” this can be done by using the ‘Create’ pull down and selecting “GUI Skin” and name it ‘Primary’ (case sensitive).  This object is what Unity uses to control what the UI looks like.  As with the Input object we have looked at before this one also has a lot of predefined elements in it.

Step 02: The skinny on GUI Skin

There are a lot of bells and whistles in here, that can be adjusted to make the UI look exactly how you would like.  There is even a ‘Custom Styles’ category that allows you to make a new style that fits any special need.  In this demo, all that there is to do is set the default font to ‘lucon’ and it’s size to ’18’ (a size of ‘0’ is the default size for the font).  When dealing with fonts there are lots of options, ‘lucon’ is a default windows system font.

When dealing with games, fonts come in a few different flavors, there are the commonly seen ‘True Type‘ fonts which is what you are reading now and used by text editors.  There are also ‘Font Strips’ which are graphical images of every character used in a font, created almost the exact same way sprite sheets were made in workshop #2.  There is not enough time to dive deep into ‘Typography‘ in the scope of this workshop but a lot of effort goes into something that does not get much praise.  A common theme with UI work.

The next step is creating an empty game object to be named ‘UI_Manager’ and placing the script of the same name onto it.  Then place the skin you made into the field for it inside the inspector for the new object.  If you have trouble selecting the skin from the inspector picker you can also drag it from the assets folder.

Step 03: Not empty for long.

Now it’s time to take a look at the script here for the ‘UI_Manager’

using UnityEngine;
using System.Collections;

public abstract class UI_Child : MonoBehaviour
{
    public abstract void OnGUIChild();
    public virtual bool ParseXML() { return false; }
}

public class UI_Manager : MonoBehaviour 
{
    // Local Members    
    public GUISkin m_pGUISKin;
    public UI_Child m_pMainMenu;
    public UI_Child m_pIntro;
    public UI_Child m_pConversation;

    // What state our game is in
    enum UIState
    {
        enMainMenu,
        enIntro,
        enConversation,
        enBattle
    }
    UIState m_enUIState = UIState.enMainMenu;

    // Update is called once per frame
    void OnGUI()
    {
        // Set our skin
        GUI.skin = m_pGUISKin;

        // We want to draw the main menu
        if(m_enUIState == UIState.enMainMenu && m_pMainMenu != null)
            m_pMainMenu.OnGUIChild();
        else if(m_enUIState == UIState.enIntro && m_pIntro != null)
            m_pIntro.OnGUIChild();
        else if(m_enUIState == UIState.enConversation && m_pConversation != null)
            m_pConversation.OnGUIChild();
    }

    // Callback when we start a new game
    void UI_NewGame()
    {
        m_enUIState = UIState.enIntro;
    }
    // Call back when intro has ended
    void UI_IntroEnd()
    {
        m_enUIState = UIState.enConversation;
    }
    // Call back when the conversation has ended
    void UI_ConversationEnd()
    {
        m_enUIState = UIState.enMainMenu;
    }
}

At the top of the file there is a abstract class being created, this is the same as what was being done for the enemies in the third workshop, however here it is being used to create a way to reference another script on the object.  Additionally it gives it two base functions that each implementation of ‘UI_Child’ will use.

public abstract class UI_Child : MonoBehaviour
{
    public abstract void OnGUIChild();
    public virtual bool ParseXML() { return false; }
}

This script is very similar to what you have already worked with before.  The UI manager has a simple ‘state machine’ for controlling what is displayed just like the player did in the second workshop.

    // What state our game is in
    enum UIState
    {
        enMainMenu,
        enIntro,
        enConversation,
        enBattle
    }
    UIState m_enUIState = UIState.enMainMenu;

One big difference on this behavior instead of any of the previous ones you have seen is the ‘OnGUI’ function.  Here it is being used instead of the ‘Update’ function, this is for code timing, ‘OnGUI’ is always called after all of the ‘Update’ functions have finished.  This allows the UI to have the most recent data.

    // Update is called once per frame
    void OnGUI()
    {
        // Set our skin
        GUI.skin = m_pGUISKin;

        // We want to draw the main menu
        if(m_enUIState == UIState.enMainMenu && m_pMainMenu != null)
            m_pMainMenu.OnGUIChild();
        else if(m_enUIState == UIState.enIntro && m_pIntro != null)
            m_pIntro.OnGUIChild();
        else if(m_enUIState == UIState.enConversation && m_pConversation != null)
            m_pConversation.OnGUIChild();
    }

You should notice the ‘GUI.skin’ variable where the skin chosen from the editor is being set as the active skin.  As with ‘Input’ or ‘Time’, ‘GUI’ is another shared global system variable changes made here will effect any following GUI interactions.  Then the function will draw more GUI elements based on what state the UI_Manager is in.

Add the ‘MainMenu’ scrip to the ‘UI_Manager’ object and then from inside the object inspector drag a reference of the ‘MainMenu’ script onto the ‘UI_Manager’ script.  Finally use the ‘LoadXML’ button to select the ‘MainMenu.xml’ file.

Step 04: Script nesting and XML loading

Note: The ‘Load XML’ button is not a standard inspector element found in the Unity editor.  It is a custom one created by the ‘UI_Child_Inspector’ script, the creation of this will be covered in the associated appendix.

Let’s take a look at the script for ‘UI_MainMenu’ the ‘OnGUIChild’ to be exact, there is some important stuff going on in the ‘ParseXML’ functions but that is something that will be covered later.

    // Do our part of the GUI only if the manager wants it
    public override void OnGUIChild()
    {
        // Draw our background
        GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), m_pBackground);

        // Create a horizontal group
        GUILayout.BeginHorizontal();
        {
            // Push in from the left
            GUILayout.Label(" ", GUILayout.MaxWidth(Screen.width));
            // Create a vertical group
            GUILayout.BeginVertical();
            {
                // Push down from the top
                GUILayout.Label(" ", GUILayout.MaxHeight(Screen.height));
                // Create all of our buttons
                XMLObject pChild;
                string strTemp;
                for(int nIter = 0; nIter < m_pXMLObject.m_aChildren.Count; ++nIter)
                {
                    // Create the button
                    pChild = (XMLObject)m_pXMLObject.m_aChildren[nIter];
                    if(pChild.m_dAttributes.TryGetValue("FriendlyName", out strTemp) && strTemp.Length > 0)
                    {
                        // If it was pressed, broadcast that to the rest of the object
                        if(GUILayout.Button(strTemp, GUILayout.MaxWidth(200)) && 
                           pChild.m_dAttributes.TryGetValue("Action", out strTemp))
                        {
                            gameObject.BroadcastMessage(strTemp);
                        }
                        GUILayout.Space(20);
                    }
                }
                // Push up from the bottom
                GUILayout.Label(" ", GUILayout.MaxHeight(Screen.height));
            }
            GUILayout.EndVertical();
            // Push in from the right
            GUILayout.Label(" ", GUILayout.MaxWidth(Screen.width));
        }
        GUILayout.EndHorizontal();
    }

A lot of new ‘GUI’ and ‘GUILayout’ call here there are few types of calls we will focus on the ‘Formatting’ and ‘Content’ types.  Formatting functions are the ‘Begin’ and ‘End’ pairs such as ‘BeginHorizontal’ or ‘Space’ these allow you to organize the UI by adding structure.  The content functions including ‘Label’ or ‘Button’ are generally what the user sees and interacts with.

The content creating functions often use additional layout options like ‘GUILayout.MaxWidth(Screen.width)’ these come in three basic flavors ‘Min’, ‘Max’, and ‘Fixed’ just like the ‘Min’ and ‘Max’ functions for numbers used previously these additional options add more formatting to the UI layout.

            // Push in from the left
            GUILayout.Label(" ", GUILayout.MaxWidth(Screen.width));

This blank label has a max with as wide as the screen so it will try to take up as much space as possible, however since it’s in the same horizontal group as the button’s vertical group and it’s partner blank label the space is evened out.  Run the project and you will see what is happening.

Step 05: Grouped groups

By using groups and white space we can position the button exactly where we want it.  The reason this is important is that using hard fixed numbers limits the type of display the project can work on.  Try grabbing the corner of the preview window and change the size arbitrarily, the button will stay in the center the screen in any situation even the untenable situation of the screen being too small for the button.  Speaking of that button, how about a close look.

                        // If it was pressed, broadcast that to the rest of the object
                        if(GUILayout.Button(strTemp, GUILayout.MaxWidth(200)) && 
                           pChild.m_dAttributes.TryGetValue("Action", out strTemp))
                        {
                            gameObject.BroadcastMessage(strTemp);
                        }

The UI for buttons and other controls not only draw but they also report any input associated with them.  Here the function that draws the button will return true if it was pressed during that update.  Since the press state of a button is something that you definitely want to know about a button combing the actions is very handy.  If the button was pressed our old friend the object broadcaster does it job and it just so happens that the ‘UI_Manager’ was listening for this event.

However since the other states of the UI_Manager have yet to be set pressing the button clears the screen, that’s not any fun so lets add the next scripts.  After the next state after main menu is the ‘Intro’ so repeat the process for adding a script and selecting an XML file for ‘UI_Intro’

    // Do our part of the GUI only if the manager wants it
    public override void OnGUIChild()
    {
        // Draw our background
        GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), m_pBackground);

        // Early out if the user presses the 'Skip' button
        Vector2 vSize = GUI.skin.button.CalcSize(new GUIContent("Skip"));
        Rect vPos = new Rect(10, 10, vSize.x, vSize.y);

        // Create area for the skip button
        GUILayout.BeginArea(vPos);
        {
            if(GUILayout.Button("Skip"))
            {
                gameObject.BroadcastMessage("UI_IntroEnd");
                GUILayout.EndArea();
                return;
            }
        }
        GUILayout.EndArea();

        // Keep track of our scrolling
        m_fScrollPosition += Time.deltaTime * m_fScrollSpeed;
        vPos.Set(0, Screen.height - m_fScrollPosition, Screen.width, m_pXMLObject.m_aChildren.Count * 100.0f);
        // Create an area for the text scroll
        GUILayout.BeginArea(vPos);
        {
            // Loop through all our lines of text
            XMLObject pChild;
            // Start a horizontal group
            GUILayout.BeginHorizontal();
            {
                GUILayout.Label(" ", GUILayout.MaxWidth(Screen.width));
                // Start a verticle group
                GUILayout.BeginVertical();
                {
                    // Walk all the children objects
                    for(int nIter = 0; nIter < m_pXMLObject.m_aChildren.Count; ++nIter)
                    {
                        // We use the space to find out where we are to adjust the alpha
                        GUILayout.Space(20);
                        m_textColor.a = GetTextAlpha((vPos.y + GUILayoutUtility.GetLastRect().y) / Screen.height);
                        GUI.color = m_textColor;

                        // Grab the child
                        pChild = (XMLObject)m_pXMLObject.m_aChildren[nIter];
                        GUILayout.Label(pChild.m_strValue, GUILayout.Width(Screen.width * 0.8f));
                    }
                }
                GUILayout.EndVertical();
                GUILayout.Label(" ", GUILayout.MaxWidth(Screen.width));
            }
            GUILayout.EndHorizontal();

            // If our final line of text was off the top of the screen, the intro is over
            if(GUILayoutUtility.GetLastRect().y < 0.0f)
            {
                gameObject.BroadcastMessage("UI_IntroEnd");
            }
        }
        GUILayout.EndArea();
    }

We will take a look at the ‘OnGUIChild()’ function again, much of this is the same as you saw in the ‘UI_MainMenu’ but a few new elements have show up.

        Vector2 vSize = GUI.skin.button.CalcSize(new GUIContent("Skip"));
        Rect vPos = new Rect(10, 10, vSize.x, vSize.y);

The ‘CalcSize’ function is a great little buddy to have around it will tell you how much space you need to create the element you pass in.  This let’s us know exactly how much space to set the area to so the ‘Skip’ button fits perfectly.

// Keep track of our scrolling
m_fScrollPosition += Time.deltaTime * m_fScrollSpeed;
vPos.Set(0, Screen.height - m_fScrollPosition, Screen.width, m_pXMLObject.m_aChildren.Count * 100.0f);
// Create an area for the text scroll
GUILayout.BeginArea(vPos);

This snippet here shows the ‘BeginArea’ slowly being moved up in screen space.  The scrolling text of the intro contains a large number of elements that go into this area, moving this one area will move the entire group and is a lot smarter way of handling the goal of scrolling the text up the screen.

m_textColor.a = GetTextAlpha((vPos.y + GUILayoutUtility.GetLastRect().y) / Screen.height);
GUI.color = m_textColor;

Here we have ‘GUILayoutUtility.GetLastRect()’ which is the chronological opposite of ‘CalcSize’ where instead of seeing the future size it instead returns the size of the last GUI element created.  This information is being use to adjust the ‘alpha’ of the text color, this alpha is exactly the same as the alpha on textures used in the previous workshops.  Take a peak at the ‘GetTextAlpha’ function that returns that alpha as a percent if you are interested in the math used.

Step 06: The plodding plot

After adding this script and linking it to the ‘UI_Manager’ clicking new game will tell the into to this workshop.  Without the third script this intro leaves us with many questions and uncertainties, so it’s time to get some answers by adding the ‘UI_TalkingHeads’ script and linking it up to the ‘UI_Manager’ and loading the similarly named XML one more time.

Much of this script is now common place to you, but there is a bit of new here to take a closer look at.  Namely the ‘GUIStyle’ that is custom to this script.

GUIStyle  m_pGUIStyle = new GUIStyle();
m_pGUIStyle.stretchWidth = true;
m_pGUIStyle.stretchHeight = true;
// Set the custom image into the GUIStyle
m_pGUIStyle.normal.background = m_pLeftHead;
GUILayout.Box("", m_pGUIStyle, GUILayout.Height(240), GUILayout.Width(240));

For the talking heads to make sure the images fill the entire space dedicated to them instead of using a ‘GUILayout.Label’ which can be created with an image a ‘GUILayout.Box’ is used instead with the background set to the image desired.  Having a custom style here allows for any image set as the background to be stretched or shrunk to fill the space exactly.

Step 07: I’m still not an artist.

Using the ‘Next’ button the user can walk through this exciting conversation and learn about the evil that is dooming the land.  By now you are probably wondering where all this content is coming from, now is a good time to talk about the XML files that we have been using.

‘XML’ or Extensible Markup Language is a document format that is readable by machines but can also be read by humans.  Up until now all of our data in our projects have been either hard coded into the scripts or manually set from the editor.  Using external XML files a developer gains even more flexibility.  Values and content can be changed without using the editor or recompiling the code, the data can be edited by external tools and shared in a more readable format.

Here is what one of the XML files looks like:

<TalkingHeads Background="MenuBG">
    <DialogLine LeftSpeaker="True" SpeakerName="Librarian" LeftHead="Librarian" RightHead="">Every this is <b>EXPLODING!</b> I sure hope the Duckess is okay! DUCKESS! WHERE ARE YOU MAGISTY?!?</DialogLine>
    <DialogLine LeftSpeaker="false" SpeakerName="The Duckess" LeftHead="Librarian" RightHead="Duckess">QUACK! QUACK! QUACK! quackity quackity QUACK!</DialogLine>
    <DialogLine LeftSpeaker="false" SpeakerName="Evil Incarnate" LeftHead="Librarian" RightHead="SpacePigeon">You'll never stop the darkness you meddiling librarian! We'll take the Duckess and use it for our foul rituals!<b>BWA HA HA HA HA!</b></DialogLine>
    <DialogLine LeftSpeaker="True" SpeakerName="Librarian" LeftHead="Librarian" RightHead="SpacePigeon">Not so fast! I'll stop you at all costs!</DialogLine>
</TalkingHeads>

As a flat text file you can see how the ‘DialogLine’ elements are children of the ‘TalkenHead’ node.  Each of the ‘DialogLine’ nodes have ‘Atrributes’ such as ‘SpeakerName’ and data inside of the node between the <DialogLine …> and </DialogLine>.  You could if you so choose edit this file by hand.

The other option would be to use a tool to edit it, such as the one following appendix creates, but you can use it now if you go back to the Unity editor and under ‘Window’ select ‘XML Editor’ which will let you load a file and work with it.

Step 07: Custom editor window

Having this data is great, but how it’s used in the project can be touched on lightly here and expanded on in detail in a code heavy appendix.  Take a look at the ‘XMLObject’ script

public class XMLObject
{
    public string m_strName = "NewNode";
    public string m_strValue = "";
    public Dictionary<string,string> m_dAttributes = new Dictionary<string,string>();
    public ArrayList m_aChildren = new ArrayList();

    ...
}

Just the very top part of the file, you can see that we are creating a class that contains the data that makes a XML node.  The name of the node, it’s value, an array of it’s children, and a new data type a ‘Dictionary’ of it’s attributes.  Also known as an ‘Associative Array‘ is a collection of elements have a pair of data that creates the associate.  Think of the classic word association technique used in physiology, that is what goes on in an Associative Array.

To access values in the ‘Dictionary’ the following function is used:

TryGetValue("FriendlyName", out strTemp)

This returns ‘true’ if the key is found and the value is copied into the ‘out’ parameter ‘strTemp’ using ‘out’ parameters allows have multiple return values from a function in essence.