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!)