How to make a game in Unity3D – Part 3

Today, we will be finishing up on the game we have started making in part 1 and part 2. So, to recap on what we need to do to finish off the game is to add the music, leaderboard, start screen, game over screen, and the leaderboard display screen. I hope you guys are ready, this will go by rather quickly and give you the remaining information you need to finish this game and make other games.

Download the project zip file here

become a professional vr game developer with unity

Pre-Order The Complete Virtual Reality Game Development with Unity and learn to create immersive games and experiences by building 10 VR games. The course assumes no prior Unity or VR experience – We’ll teach you C#, Unity and 3D programming from the ground-up

Check it out on Zenva Academy and get Early Access!

 

To get started, let’s fix an issue that I failed to address and notice with part 2. The issue lies with the box collider we set up. The Is Trigger parameter should be left unchecked and I modified the parameters for the size of the box to better fit the jet. For center x should be 0, y should be 9, and z should be 0. The size should be changed as well, x is 70, y is 10, and z is 70.
JetFix

We should also do a slight fix to the code as well. We will remove the line that deals with attaching the rigid body component to each and every missile. However, this causes issues (internal issues to the Unity Engine).

using UnityEngine;
using System.Collections;

public class MissileController : MonoBehaviour
{
    public GameObject hazard;

    private float spawnWait = 1f;

    void Start()
    {
        StartCoroutine(SpawnWaves());
    }

    IEnumerator SpawnWaves()
    {
        while (true)
        {
            var xMinMax = Random.Range(-15f, 20f);
            Vector3 spawnPosition = new Vector3(xMinMax, 10f, 25f);
            Quaternion spawnRotation = Quaternion.Euler(new Vector3(90f,0f,0f));
            Instantiate(hazard, spawnPosition, spawnRotation);
            yield return new WaitForSeconds(spawnWait);
            spawnWait -= 0.01f;
            if (spawnWait < 0.05f) spawnWait = 0.05f;
        }
    }
}

This should fix the issue with the missiles and jet not always registering as being hit. In other words, the hit detection was extremely buggy.

Backend Code

Next up, we should create the back end for the leaderboard. To get started, we should first decide what format we will use for it. I have chosen to go with XML because it is the easiest method to use.  We will start off by building the back end code, mainly because the front end is the easiest part to do and should be saved for last.

So, to get started we need to make the Leaderboard class. Let’s look at the entire script and break it down from there.

using UnityEngine;
using System.Collections;
using System.Xml;
using UnityEngine.UI;

public class Leaderboard : MonoBehaviour
{
    public Text text;
    public GameObject content;

    void Start()
    {
        string Path = Application.dataPath + "/Data/Leaderboard.xml";

        XmlDocument doc = new XmlDocument();

        if (!System.IO.File.Exists(Path))
        {
            Debug.Log("File not found");
        }

        else
        {

            doc.Load(Path);

            XmlNodeList elemList = doc.GetElementsByTagName("SurvivalTime");
            for (int i = 0; i < elemList.Count; i++)
            {
                    text = Instantiate<Text>(text);
                    text.transform.SetParent(content.transform, false);
                    text.text = elemList[i].InnerXml;
            }
        }
    }

    public void writeToXML()
    {
        text = FindObjectOfType<Text>();
        string Path = Application.persistentDataPath + "/Leaderboard.xml";

        XmlDocument doc = new XmlDocument();

        if (!System.IO.File.Exists(Path))
        {
            XmlDeclaration declaration = doc.CreateXmlDeclaration("1.0", "UTF-8", "yes");
            XmlComment comment = doc.CreateComment("This is a generated XML File");
            XmlElement Leaderboard = doc.CreateElement("Leaderboard");
            XmlElement survivalTime = doc.CreateElement("SurvivalTime");

            survivalTime.InnerText = text.text;

            doc.AppendChild(declaration);
            doc.AppendChild(comment);
            doc.AppendChild(Leaderboard);

            Leaderboard.AppendChild(survivalTime);

            //Save document
            doc.Save(Path);
        }

        else //if file already exists
        {
            doc.Load(Path);

            // Get root element
            XmlElement root = doc.DocumentElement;

            XmlElement survivalTime = doc.CreateElement("SurvivalTime");

            //Values to the nodes
            survivalTime.InnerText = text.text;


            //Document Construction
            doc.AppendChild(root);

            // Append root element to word element
            root.AppendChild(survivalTime);

            //Append written values to word as child element

            //Save the document
            doc.Save(Path);
        }
    }
}
Reading and Writing XML Data

Since we want the data from the timer that we will build, we need to make sure we get the text element from our timer. Our timer will be a 3D text object, in code that is represented by a TextMesh object. We have two methods here, Load XML and Write to XML.

Loading the XML is a really simple method. The first thing that the method does is load the xml file. Next up, we declare what element we actually want the data from. The last thing it does is loop through all of the elements with the tag Survival Time and display the results.

    void Start()
    {
        string Path = Application.dataPath + "/Data/Leaderboard.xml";

        XmlDocument doc = new XmlDocument();

        if (!System.IO.File.Exists(Path))
        {
            Debug.Log("File not found");
        }

        else
        {

            doc.Load(Path);

            XmlNodeList elemList = doc.GetElementsByTagName("SurvivalTime");
            for (int i = 0; i < elemList.Count; i++)
            {
                    text = Instantiate<Text>(text);
                    text.transform.SetParent(content.transform, false);
                    text.text = elemList[i].InnerXml;
            }
        }
    }

 

 

Next up, we have the Write to XML code. It is definitely a lot more code than the loading code. Essentially, this code contains a lot of boiler plate that specifies exactly how we want the xml file to be generated as well as the location. Let’s get into it and break it down.

Here we specify that the text object needs to find the object of the type Text Mesh. This will allow us to much more easily call this code in another class if we need to. We create a string called path. We assign the path to be the application’s data path, and add that we want the file name to be Leaderboard and it is an xml file.
Let me explain exactly what that is. It contains the path that houses the game’s data folder. Which is 100% different from the application’s persistent data path. More on that later.
Finally, We define doc as a new Xml Document.

   public void writeToXML()
    {
        text = FindObjectOfType<Text>();
        string Path = Application.persistentDataPath + "/Leaderboard.xml";

        XmlDocument doc = new XmlDocument();

        if (!System.IO.File.Exists(Path))
        {
            XmlDeclaration declaration = doc.CreateXmlDeclaration("1.0", "UTF-8", "yes");
            XmlComment comment = doc.CreateComment("This is a generated XML File");
            XmlElement Leaderboard = doc.CreateElement("Leaderboard");
            XmlElement survivalTime = doc.CreateElement("SurvivalTime");

            survivalTime.InnerText = text.text;

If file and path does not exist, then we need to build the entire xml structure. Declaration creates the header of the xml file. Comment, while it isn’t needed to have a valid xml file, is added to say we didn’t build it by hand. Leaderboard is the parent element we need to use. Survival time is the child element that will house the timer’s data.

We will set the survival time’s inner text to be whatever the text mesh’s text is. We then append the child for the doc as declaration, comment, and leaderboard. Leaderboard will have its own child element of survival time.  Then, we can save the document to the path specified.

            doc.AppendChild(declaration);
            doc.AppendChild(comment);
            doc.AppendChild(Leaderboard);

            Leaderboard.AppendChild(survivalTime);

            //Save document
            doc.Save(Path);
        }

        else //if file already exists
        {
            doc.Load(Path);

            // Get root element
            XmlElement root = doc.DocumentElement;

            XmlElement survivalTime = doc.CreateElement("SurvivalTime");

            //Values to the nodes
            survivalTime.InnerText = text.text;


            //Document Construction
            doc.AppendChild(root);

            // Append root element to word element
            root.AppendChild(survivalTime);

            //Append written values to word as child element

            //Save the document
            doc.Save(Path);

If the file and path does exist, then load the document. We need to retrieve the root element which in our case is the document’s main element (in our case is leaderboard) and we also want to make sure that we can add more survival time elements.
We again set the survival time’s inner text to be the text mesh’s text. We then set that the root element of the document can be added to. Finally, we can append or add more to the survival time of the leaderboard element. The last step is to save the document to the path specified.

Not too difficult, just very tedious in the way I decided to go with creating and modifying the Leaderboard with xml. There are easier ways to handle this, but it is always a good idea to learn exactly what is going on behind the scenes with XML and/ or JSON.

Timing and timer

Let’s start the timer code. It is rather simple and straight forward. We will begin by creating a public Text Mesh, a private float, and two private integers. It should look like the code below

   public Text scoreText;

    private int minutes;
    private int seconds;

    private float Score;

The only method we will have is an Update method. The reason for this is because it is easier to consolidate into a single method that handles this one task.
The float value of score should be addition and assigned to delta time.
The int of minutes should be assigned to the Mathf class and floored to int the score divided by 60.
The int of seconds should be assigned to the Mathf class and floored to int the score subtracted by the minutes multiplied by 60.
Next we format the string value to have the minutes and second display.
Finally, the Text Mesh’s text should have the text “Survival Time: “ and directly underneath it the formatted string value we created.

    void Update()
    {
        Score += Time.deltaTime;
        minutes = Mathf.FloorToInt(Score / 60f);
        seconds = Mathf.FloorToInt(Score - minutes * 60f);
        string formatedTime = string.Format("        {0:0}:{1:00}", minutes, seconds);
        scoreText.text = "Survival Time: " + System.Environment.NewLine + formatedTime;
    }

Modification for Collision events and writing XML Data

The next to last bit of code that needs to be done for now is to modify the Destroy Missile class. We will change it from on trigger enter to on collision enter and create a new method called save Results. We will call the leaderboard class, name it leader, and game object add component leaderboard on it. Leader should then have the ability to call the write to xml method from that class.

    void OnCollisionEnter(Collision other)
    {
        Destroy(other.gameObject);
        saveResults();
        Destroy(gameObject);
        SceneManager.LoadScene(2);
    }

    void saveResults()
    {
        Leaderboard leader = gameObject.AddComponent<Leaderboard>();
        leader.writeToXML();
    }

Scene Transitions

The final portion of the code we need to discuss is the scene transition class. This is relatively simple to write and utilize. We call the Scene Management class from within Unity Engine. Then to call the scene we want to load, we utilize the Scene Manager class and call the Load Scene method.
I should note that this can take an integer value or a string as the parameter. I typically use the integer method and you have to make sure that you go to your build settings and assemble the scenes appropriately. If you mix up the integer values, the scenes will load but they will be in the incorrect order.

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneController : MonoBehaviour
{
    public void StartGame()
    {
        // New Game
        SceneManager.LoadScene(1);
    }

    public void Leaderboard()
    {
        // Game Over with Leaderboard 
        SceneManager.LoadScene(2);
    }

    public void EndGame()
    {
        // Exit Game
        Application.Quit();
    }
}

Game Start Scene Building

This scene will be utilizing our Jet and Plane prefabs, as well as other components. Make sure the scene is a 3D scene and add the jet and plane to the scene. The jet’s position should be (x = 0, y = 3.7, z = 24.75) and should be scaled correctly however, just in case it didn’t; The correct values for scaling is (x = 0.3, y = 0.3, z = 0.3). You can remove the player script, box collider, and ridged body from the Jet on this scene if you would like.
The plane should have the scale values set as (x = 100, y = 100, z = 100). The position of the plane should be (x = 0, y = -38.23, z = 545.27).
Add an Empty game object and call it Controller. Add a canvas, panel, and 2 buttons to the scene. The canvas should be the primary parent object, the panel should be a direct child of the canvas, and the two buttons are a direct child of the panel. Name the buttons start and exit respectively.
GameStartEndHierarchyPane

Attach the scene controller script to the controller game object we just created. Next, attach the game object to each button script from within the button on the hierarchy pane. Assign either start or exit to their respective buttons.

GameStartEndButtonInspector

Save the Scene and name it Game Start End, and we have completed the scene.

Game Over Scene Building

Add a Canvas, Panel, Scroll View, Empty Game Object, and 2 buttons to the scene. The Canvas is the parent Object, the 2 buttons, Empty Game Object and Panel are direct child objects to the Canvas, and the scroll View is a direct child object to the Panel.

GameOverSceneLook

Rename the Empty Game object to be Controller as we did in the last scene. Attach the scene management and Leaderboard scripts to it.

Create a text prefab and place it into the prefabs folder, we will need this for the next step.

Navigate to the Scroll view and look for an object inside called Content.
2016-08-25 00_15_11-Unity Personal (64bit) - GameOver.unity - Create_A_Game_Part_3 - PC, Mac & Linux

The next step is to make sure that we add a vertical layout group to the content object. This will allow us to structure the displayed text the way we want instead of how Unity would display by default. The only thing we want unchecked is the child force expand on width. The remainder of the properties should be as written here: Left = 0, Right = 0, Top = 0, Bottom = 0, Space = 30, and child alignment = middle center.
ControllerGameOver

With the Leaderboard script that we attached, you probably noticed that it requires two game objects; A Content and Text object. The Content object we will use is the content object from within the Scroll View, and the text is from the text object that we just created a prefab from.

The New Game and Exit Game buttons should have their respective scripts attached to them as we did in the scene before it.

Finalizing the Game Play Scene

Add a Canvas to the scene and create a child object of text. The canvas should be set to screen space camera just like the other scenes. The text should have the position of x = 45.13, y = 27, z = 0, width = 160, and height = 30.

We have now completed the game play scene.

As you can see, putting together all of the little aspects to create a game is very simple to do with Unity3D, the only thing we have not done is add music. So, let’s go ahead and do that shall we?

Right click on the hierarchy pane, highlight audio, then click on audio source.
GetAudioSource

Now that we have the audio source added to the scene, let’s take a look at some of the properties that are available for us to manipulate.
AudioSourceInspector
We have the source of the audio, option to specify the output device, mute, bypass effects, bypass listener effects, Bypass reverb zone, play on awake, loop, priority, volume, pitch, stereo pan, spatial blend, and reverb zone mix. We have a drop down for 3D sound manipulation we can do as well.

We want to make sure that Play on Awake and Loop are checked. Priority is set to 128, Volume is 0.4, Pitch is 1, Stereo Pan is 0, Spatial Blend is 0.5, and Reverb Zone Mix is 1.

Completed Audio Listener

Congratulations, you have a game that has music, the ability to save and load data, controllable character, and collision detection. I hope you enjoyed this series on building a game in Unity3D, I have plenty more to teach you in the future. Until then, may your code be bug free; Jesse signing out.