Monthly Archives: April 2013

Unity3D Workshop – 05 – A platform for platformers.

(Note: This workshop is not 100% complete)

Welcome to the fifth 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).  This time in the workshop the game type that we will be examining will be one of the most common game types, the platformer.

Rayman Origins, Sonic the Hedgehog, Super Mario Bros.

The platformer has a lot of variety to them, but typically they have the player scrolling from left to right avoiding enemies and collecting powerups.  Much of the work that went into the infinite runner is applicable here also, if you have yet to run Workshop 02 it’s recommended that you go there first.

The major difference between the infinite runner and a typical side scroller is that the latter will have a greater care done to make specific levels that are unique and different.  Much like how the UI was crafted in Workshop 04, creating the levels in the editor while possible is often much more difficult and cumbersome than needed.  To facilitate this we will use a custom level editor, this editor is not default to Unity and details on how it works are available in the associate appendix. (!pending!)

The asset package for this project can be found here.  Create a new project, and bring in the assets package, tou may need to correct the material references after loading the assets.   Once setup, bring up the ‘XML Map Editor’ window found from the ‘Window’ pulldown menu.  Load the map and tile files provided and your window should look just like this.

Step 01: A map of nowhere.

As you can see this map is pretty unimpressive, so let’s resize it to something we can work with, 32×4.  Then enter a texture for the current tile, ‘Bricks’ is the one we want, and make sure to check ‘Collision’ so the player will be blocked by the brick blocks.  Take a minute here to draw out a map, left click will add a tile of the type selected holding ‘shift’ will clear the tile.  Here is an example of what you should see.

Step 02: Time to level with you

Make sure to save both the level and the tiles after making you level.  This will update the files in the ‘XML’ folder, and now that we have a level we can take it from XML to the game and see it in action.  Create a empty game object named ‘Level’ and place on it the ‘MapLoader’ script, and let then open that script up in MonoDevelop so we can take a look at what is going on.  Make sure to position the new ‘Level’ object at the origin (0,0,0) and the rotation (0, 180, 0).

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml;

public class MapLoader : MonoBehaviour
{
    // Local Variables
    public List<GameObject> m_aMapObjs = new List<GameObject>();
    public Dictionary<int, XMLObject> m_dMapTiles = new Dictionary<int, XMLObject>();

    void Start()
    {
        // TEMP - Update this!!
        LoadMap("Assets/XML/ExampleMap.xml", "Assets/XML/ExampleTiles.xml");
    }

    public void LoadMap(string strMapFile, string strTileSetFile)
    {
        // Read our XML document
        XMLObject pMapXML;
        XMLObject pTileSetXML;

        // Load our map
        XmlDocument pDoc = new XmlDocument();
        pDoc.Load(strMapFile);
        XmlNodeReader pNodeReader = new XmlNodeReader(pDoc);
        {
            pNodeReader.MoveToContent();
            pMapXML = XMLObject.ParseXML(pNodeReader);
        }
        pNodeReader.Close();

        // Load our Tileset
        pDoc.Load(strTileSetFile);
        pNodeReader = new XmlNodeReader(pDoc);
        {
            pNodeReader.MoveToContent();
            pTileSetXML = XMLObject.ParseXML(pNodeReader);
        }
        pNodeReader.Close();
        ReadTiles(pTileSetXML);

        // Walk through our map and build a tile for each node in the XML
        XMLObject pRowObj = null;
        XMLObject pColumnObj = null;
        XMLObject pTileObj = null;
        string strDictTemp = "";
        // We build bottom up
        for(int nRowIter = 0; nRowIter < pMapXML.m_aChildren.Count; ++nRowIter)
        {
            // Column by row
            pRowObj = pMapXML.m_aChildren[nRowIter];
            for(int nColumnIter = 0; nColumnIter < pRowObj.m_aChildren.Count; ++nColumnIter)
            {
                pColumnObj = pRowObj.m_aChildren[nColumnIter];
                // If there is no idea we don't need to create a block at this location
                if(pColumnObj.m_dAttributes.TryGetValue("TileID", out strDictTemp) && strDictTemp.Length > 0)
                {
                    // Try to find the tile being referenced
                    if(!m_dMapTiles.TryGetValue(int.Parse(strDictTemp), out pTileObj))
                        continue;

                    // Finally create the new map object and keep a reference to it
                    m_aMapObjs.Add(CreateMapObject(nRowIter, nColumnIter, pTileObj));
                }
            }
        }
    }

    // This is where we create a block of the map, we use the tile refernce to fill in the data
    GameObject CreateMapObject(int nRow, int nColumn, XMLObject pTileXML)
    {
        GameObject pTemp = GameObject.CreatePrimitive(PrimitiveType.Cube);
        pTemp.name = "Block_" + nRow + "_" + nColumn;
        pTemp.transform.position = new Vector3(nColumn, nRow, 0.0f);
        pTemp.transform.parent = gameObject.transform;

        // Grab the texture if there is one
        string strTemp;
        if(pTileXML.m_dAttributes.TryGetValue("Texture", out strTemp) && strTemp.Length > 0)
        {
            pTemp.renderer.material = (Material)Resources.Load("Materials/" + strTemp) as Material;
        }
        else
        {
            pTemp.renderer.enabled = false;
        }

        // Set the collision status
        if(pTileXML.m_dAttributes.TryGetValue("Collision", out strTemp) && strTemp.Length > 0)
        {
            pTemp.collider.isTrigger = !bool.Parse(strTemp);
        }

        // Give the block an extra script
        Basic_Block pBlock;
        if(pTileXML.m_dAttributes.TryGetValue("BlockType", out strTemp) && strTemp.Length > 0)
        {
            pBlock = (Basic_Block)pTemp.AddComponent(System.Type.GetType(strTemp + "_Block"));
        }
        else
        {
            pBlock = pTemp.AddComponent<Basic_Block>();    
        }

        // Set the collision callback
        if(pTileXML.m_dAttributes.TryGetValue("Trigger", out strTemp) && strTemp.Length > 0)
        {
            pBlock.m_strTriggerEvent = strTemp;
        }

        return pTemp;
    }

    // Build our tile list
    void ReadTiles(XMLObject pTileSetXML)
    {
        // Safety Check
        if(pTileSetXML == null)
            return;

        // Run every child
        string strTemp = "";
        for(int nChildIter = 0; nChildIter < pTileSetXML.m_aChildren.Count; ++nChildIter)
        {
            // Ignore any children that don't have an ID at all
            if(pTileSetXML.m_aChildren[nChildIter].m_dAttributes.TryGetValue("TileID", out strTemp))
            {
                // Put each tile into the map for faster reference
                m_dMapTiles.Add(int.Parse(strTemp), pTileSetXML.m_aChildren[nChildIter]);
            }
        }
    }
}

(Note: File loading here was roughed out, level selection to be added)

As with the XML files from Workshop 04 we are now reading over what was saved by the level editor and creating the level geometry based on that info.  We won’t worry about what exactly an ‘XMLObject’ is here, but we will go over how the map is built.  The first thing we do is build a ‘Dictionary’ of all our tiles inside of the ‘ReadTiles’ function, doing this saves a lot of work each time we need to associate a tile index from the map to the tile file.
The question is of course then, why do use an index from the map to the tiles instead of just simply storing all the tile data in the map.  The reason for this is that we will likely come back and need to change a tile on the map at a latter point, by referencing the tile by index any bulk changes are done in the tile definitions and work across the entire map.Inside of ‘LoadMap’ you can see map is build tile by tile row by column.  Each tile or ‘MapObject’ is created by the function ‘CreateMapObject’ and stored in a list of the level this list exists because when changing maps having a quick way to remove all the previous items would be a big help.  Let’s take a closer look at ‘CreateMapObject’.

    // This is where we create a block of the map, we use the tile refernce to fill in the data
    GameObject CreateMapObject(int nRow, int nColumn, XMLObject pTileXML)
    {
        GameObject pTemp = GameObject.CreatePrimitive(PrimitiveType.Cube);
        pTemp.name = "Block_" + nRow + "_" + nColumn;
        pTemp.transform.position = new Vector3(nColumn, nRow, 0.0f);
        pTemp.transform.parent = gameObject.transform;
        
        // Grab the texture if there is one
        string strTemp;
        if(pTileXML.m_dAttributes.TryGetValue("Texture", out strTemp) && strTemp.Length > 0)
        {
            pTemp.renderer.material = (Material)Resources.Load("Materials/" + strTemp) as Material;
        }
        else
        {
            pTemp.renderer.enabled = false;
        }
        
        // Set the collision status
        if(pTileXML.m_dAttributes.TryGetValue("Collision", out strTemp) && strTemp.Length > 0)
        {
            pTemp.collider.isTrigger = !bool.Parse(strTemp);
        }
        
        // Give the block an extra script
        Basic_Block pBlock;
        if(pTileXML.m_dAttributes.TryGetValue("BlockType", out strTemp) && strTemp.Length > 0)
        {
            pBlock = (Basic_Block)pTemp.AddComponent(System.Type.GetType(strTemp + "_Block"));
        }
        else
        {
            pBlock = pTemp.AddComponent<Basic_Block>();    
        }
        
        // Set the collision callback
        if(pTileXML.m_dAttributes.TryGetValue("Trigger", out strTemp) && strTemp.Length > 0)
        {
            pBlock.m_strTriggerEvent = strTemp;
        }
        
        return pTemp;
    }

This is the first time ‘transform.parent’ is being used, making each new game object a child of the level’s transform is a quality of life choice, doing this will collect the objects under the ‘Level’ object in the editors object hierarchy to keep the editor nice and clean while running.

After that basic setup the block is positioned based on it’s row/column which is also used to generate the name for the block.  Then the common parameters are set, notice that objects without a texture have their renderer disabled.  Also the ‘BlockType’ is some that will be important later.  Before getting to that, take a moment to run the project and if everything is setup right your map should load up.

Step 03: Level up!

Nice to see the level in the editor, yet without a player there really is not much going here.  Take a brief moment to setup the camera as before, so the blocks are nice and bright against a dark background to make them stand out well.

Step 04: Lights, Camera, …

The good old running man from Workshop 02 is back, create a player object and drop the ‘PlayerSpriteSheet’ material onto the object along with the ‘PlayerMovement’ script.  Now is also a good time to set the rotation (0, 180, 0) and a position where the player won’t start inside the a wall of the map, for the example used the position (1, 2, 0) was fine.

Step 05: A head on collision.

Before going to the script, there were a few modifications made here, first the ‘Box Collider’ was replaced with a sphere collider.  Doing this makes it easier for the player to land on ledges and control the character because the player is more a sphere than it is a box.  Secondly, inside the rigid body, freezing the ‘z’ axis and all of the object rotation there won’t be any problem with the player bouncing off the map that was created.  Now onto the player movement script:

using UnityEngine;
using System.Collections;

[RequireComponent (typeof (Rigidbody))]
public class PlayerMovement : MonoBehaviour
{
    // Local Variables
    public float m_fGroundSpeed = 5.0f;
    public float m_fRunSpeedBoost = 2.5f;
    public float m_fAcceleration = 10.0f;
    public float m_fJumpHeight = 5.0f;
    bool m_bTryJump = false;

    // Use this for initialization
    void Start ()
    {
        rigidbody.freezeRotation = true;

        renderer.material.SetTextureScale("_MainTex", new Vector2(0.25f, 1.0f));
    }

    // Update is called once per render frame
    void Update()
    {
        // Did we want to jump
        m_bTryJump |= Input.GetButtonDown("Fire1");
        if(m_bTryJump)
            rigidbody.WakeUp();
    }

    // FixedUpdate at a regulated rate
    void FixedUpdate()
    {
        // See if we're pushing enough to move
        float fInput = Input.GetAxis("Horizontal");
        bool bRunning = Input.GetButton("Fire2");
        if(fInput != 0.0f && Mathf.Abs(rigidbody.velocity.x) < (m_fGroundSpeed + (bRunning ? m_fRunSpeedBoost : 0.0f)))
        {
            rigidbody.AddForce(fInput * (m_fAcceleration + (bRunning ? m_fRunSpeedBoost : 0.0f)) * Time.deltaTime, 0.0f, 0.0f, ForceMode.VelocityChange);
        }

        // Flip our facing if we are moving left
        if(Mathf.Abs(rigidbody.velocity.x) > 0.1f)
        {
            renderer.material.SetTextureOffset("_MainTex", new Vector2(rigidbody.velocity.x > 0.0f ? 0.0f : -0.25f, 1.0f));
            renderer.material.SetTextureScale("_MainTex", new Vector2(rigidbody.velocity.x > 0.0f ? 0.25f : -0.25f, 1.0f));
        }

        // We had a jump press since the last physics update
        if(m_bTryJump && rigidbody.velocity.y < 0.1f)
        {
            RaycastHit pHit;
            Physics.Raycast(transform.position, -Vector3.up, out pHit);
            if(pHit.distance < 1.0f)
            {
                rigidbody.AddForce(0.0f, m_fJumpHeight, 0.0f, ForceMode.VelocityChange);
            }
        }
        m_bTryJump = false;
    }

    // React to taking damage
    public virtual void TakeDamage() { }

    //
    void OnCollisionStay(Collision pCollision)
    {
        if(pCollision.contacts[0].normal.y == 1.0f)
        {
            Vector3 vTransform = transform.position;
            vTransform.y = pCollision.transform.position.y + 1;
            transform.position = vTransform;
        }
    }
}

Much of this is the same as before from other Workshops, however new type of update has join the ranks of ‘Update’ and ‘OnGUI’ that being ‘FixedUpdate’.  The unique part to ‘FixedUpdate’ is that it is called every frame, instead it happens outside of the frame processing and is called at a locked rate right before the physics is processed.  Using this function is necessary to make sure that all physics reactions get that same processing frequency.  Below is an image showing how the variable frame rate might be compared to a fixed update.

The downside of the fixed update is that the Input checks that are true based on ‘Up’ or ‘Down’ maybe be consumed between fixed updates by the normal update.  To compensate for this, the ‘Jump’ input is being noted as a request in the ‘Update’ function then processed in the ‘FixedUpdate’.  Looking at the spot where that is done you should also see the ‘rigidbody.WakeUp();’ call, for performance reasons the rigidbodies are put in a low cost ‘sleep’ mode, if we don’t wake it up when jump is pressed, the input could be delayed for a significant time.

// Flip our facing if we are moving left
if(Mathf.Abs(rigidbody.velocity.x) > 0.1f)
{
     renderer.material.SetTextureOffset("_MainTex", new Vector2(rigidbody.velocity.x > 0.0f ? 0.0f : -0.25f, 1.0f));
     renderer.material.SetTextureScale("_MainTex", new Vector2(rigidbody.velocity.x > 0.0f ? 0.25f : -0.25f, 1.0f));
}

The above block of code is a bit of fancy math to flip the frame of the player object to face the way it is moving.  Now is a good time to go and play the project and explore the map you made, depending on how you created it you might need to tweak the jump heights or movement speeds.

After spending a little time with your level, the lack of ‘scrolling’ for your side scroller becomes very apparent.  To fix that you will create your first script for the camera, ‘FollowCamera’, this script will keep the player object within a certain bounds of the camera space.  Drop that script onto the ‘Main Camera’ and make sure to put the play in as the ‘Follow Target’.  A quick glance at the script to see what is going on behind the camera:

using UnityEngine;
using System.Collections;

public class FollowCamera : MonoBehaviour
{
    // Member Variables
    public GameObject m_pFollowTarget;
    public Vector2 m_vBounds = new Vector2(5.0f, 5.0f);

    // Update is called once per frame
    void Update ()
    {
        // Safety check
        if(m_pFollowTarget == null)
            return;

        // We want to keep the target within the bounds
        Vector3 vDetaPos =  transform.position - m_pFollowTarget.transform.position;
        // Check our x bounding area
        if(vDetaPos.x < -m_vBounds.x)
            vDetaPos.x += m_vBounds.x;
        else if(vDetaPos.x > m_vBounds.x)
            vDetaPos.x -= m_vBounds.x;    
        else
            vDetaPos.x = 0;    

        // Check our y bounding ara
        if(vDetaPos.y < -m_vBounds.y)
            vDetaPos.y += m_vBounds.y;
        else if(vDetaPos.y > m_vBounds.y)
            vDetaPos.y -= m_vBounds.y;    
        else
            vDetaPos.y = 0;    

        // Leave our z offset alone
        vDetaPos.z = 0;

        // Make the adjustment to the position
        transform.position -= vDetaPos;
    }
}

In this case, the camera script works just like any other script for a game object.  Now if you run your project the camera should follow along when the player gets near the edge of the visible space as defined by the camera bounds.

At this point the project might be a nice little maze if the level is setup just right, but there is a lot more that can be done to a side scroller.  Looping back to the editor we can add some special blocks.  We will add a new block that will tigger ‘EnterPit’ and set the bricks to be ‘Breakable’.

Step 06: Chip off the ol’ block

When adding the the blocks that are invisible the map editor just shows the number for the ‘Tile ID’ so you can easily see what blocks are there.  This is a good time to open up the

using UnityEngine;
using System.Collections;

public class Basic_Block : MonoBehaviour
{
    public string m_strTriggerEvent = "";

    void OnTriggerEnter(Collider pOther)
    {
        // Send the tigger event to the object we collided with if there is one
        if(m_strTriggerEvent.Length > 0)
            pOther.BroadcastMessage(m_strTriggerEvent);
    }
}

public class Breakable_Block : Basic_Block
{
    // Something collided with us
    void OnCollisionEnter(Collision pOther)
    {
        // From below
        if(pOther.contacts[0].normal.y > 0.9f)
            Destroy(gameObject);
    }
}

Two tiny little scripts here, but very flexible.  The ‘Basic_Block’ will broadcast the trigger to the player, in this case ‘EnterPit’ was chosen.  In the case of ‘Breakable_Block’s when there is a physics collision we check the ‘Normal‘ which tells direction the object that collided with the block came from.  In this case if it came from below then we destroy this block.

What happens when the player recieves the ‘EnterPit’ message?

public class PlayerEvents : MonoBehaviour
{
    // We fell in a pit!
    void EnterPit()
    {
        // We die!
        Destroy(gameObject);
    }
}

Nothing good!  Make sure to add the ‘PlayerEvents’ script to the player object and then try out the project again.

(Note: More pending!)

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.