Final Thoughts.

This project has actually been really fun for me, even though there were some major ups and downs. At the beginning of the module I felt really motivated to create the characters, as I was excited to make a whole character from scratch, all the way from low poly to high poly, to rigging, animating and importing into Unity. It’s something I’ve never done before, but I’ve done little bits and pieces separately so I felt like I had enough knowledge to do succeed. That turned out really well, though I have lost motivation while animating, partially because it was quite repetitive, but mainly because of personal matters that were going on in my life. However, I still think it ended up looking okay, considering it was my first time.

I think the most enjoyable part of that was the high-poly sculpting, as I didn’t have to worry about the polycount or anything like that, I just added as much detail as I wanted to, while sticking to Connor’s concept. I’m not really sure how happy Connor is with the visuals of the game, I feel like it might be quite far from his original concepts and visualisations, but it’s quite hard to translate 2D visuals into 3D. I feel like I may have disappointed him in that regard.

There were some frustrating parts in character creation, mainly with Rigging and Animating, but I’m glad I pushed through it. I’m also really proud of George’s input in this module, as he didn’t have much experience with 3D art before, but he’s done really well with very little help.

3 weeks before the deadline, there was almost 0 progress with the programming for our game, and I got really worried about it, as there were a lot of mechanics in Connor’s Game Design Document and we didn’t even have a working movement script. So we decided to switch roles around and have me to do the programming and have Cosmin do particle effects etc. I have to say, at that time I was feeling worried (as I wasn’t sure if we’ll get it done on time), scared of programming, and somewhat angry that after 14 weeks we had nothing usable on the programming side of development. However, I saw that as an opportunity to learn programming, as I always felt like it’s something I should know, at least on the basic level.

Going into it, I knew almost nothing. Last year I got overwhelmed by programming and learned almost nothing and I felt like I’ll never understand it. But taking it step by step, starting from the easy stuff, slowly progressing into the more advanced mechanics, I started getting a grasp of how it works. Without James’ help I managed to do the basic things like movement, jump etc. but then I needed James’ help with AI for the enemy guards and User Interface, as I didn’t really have time to learn all of it.

I’m pretty proud of myself for staying motivated throughout the last 3 weeks, I worked really hard as I wanted to prove to myself that I’m able to do something that I thought I’d never be able to do, and I didn’t want our team to fall short because of one team member. And also, I now have a completely new skill that I can (and I will) use in future projects, both for University and not.

Overall, I think this module went really well and I’m really happy I had Connor as a leader as he was always on top of things and gave specific information about what he needed, always replied quickly and whenever I asked for a layout or a quick design of something, he delivered it quickly. George has also done really well with the 3D stuff, and as a huge fan of 3D art, I personally hope he continues developing his skills as it was really nice to see him do so well and learn so many things in just one semester.

Game Builds – Finally!

With all the things in place and how I want them, it’s time to finally make a build of the game. The process is actually really simple, I just had to go to File>Build Settings:

Unity_2017-05-25_00-27-37.png

Then, I clicked “Add Open Scenes” which added the scene to the Build, then for the platform I picked “PC, Mac & Linux Standalone” and then I picked Windows (I also did a Mac version afterwards as that’s what we’ll be using in the exhibition next week) and then clicked Build and Run. After selecting a directory, Unity creates a build file and then you can play the game from an executable file.

Unity_2017-05-25_00-28-53.png

I’ve done this a few times with slight tweaks in the past week, but now I’m happy with the version I submitted, and I’m pretty proud of what I did in this semester. Below are download links to the builds and the Mirror Men Shared Blog:

PC – https://www.sendspace.com/file/0148gf

Mac – https://www.sendspace.com/file/7ing23

Shared Blog – https://mmdevjournal.wordpress.com/

The Unity Project Folder can be found on the memory stick with my submission.

Guards Collision Fix

I have noticed a bug where the guards would collide with each other, regardless of whether their colliders are on or off and they did that even when dead. It didn’t make much sense to me or James, so it actually took me quite a long time to figure out the issue. We started off by making sure all colliders were off, on top of the usual ‘GetComponent<Box Collider>()’ that we already used.

I then played around with Layers in Unity as I thought maybe that could solve the issue. I did that by changing the enemies’ Layer to Enemy, and then went to Edit>Project Settings>Physics where you can find the Layer Collision Matrix which determines which Layers can react with each other. That also didn’t solve the issue, which was odd because when I created a new scene with some cubes, it did work correctly.

That’s when I thought it’s probably an issue with the NavMesh feature, and it turned out I was right. The Obstacle Avoidance feature of the Nav Mesh Agent was causing the issue, and I solved it by changing all the values in that tab to 0 (which weirdly turned some of the values to ‘1e-05’), like so:

Unity_2017-05-25_00-24-06.png

Now, the guards no longer collide with each other:

2017-05-25_00-25-41.gif

Success, Fail and Guide Screens

Next step was to create Pop-ups for when you start the game (a guide showing you the map and what you need to do), the success screen (when you do all the objectives and return to the starting location) and a fail screen (when the player dies). I used Connor’s assets, which I imported into Unity as Sprites. For the starting screen, I parented the sprite to the camera and positioned it in the right place to fit the Game Screen nicely. I also added a script to it. The Script is really simple, all it does is it tells Unity to make it active on Start, then when the Left Mouse Button is pressed, set it to inactive:

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

public class GuidePopup : MonoBehaviour 
{
    private GameObject guide;

    void Start () 
    {
        guide = GameObject.Find (DemoLevelGuide01);
        guide.SetActive (true);
    }

    void Update () 
    {
        if (Input.GetMouseButtonDown (0)) 
        {
            guide.SetActive (false);
        }
    }
}

The Death one was slightly more complicated, but not overly complicated. I’ve used the ‘PlayerStats’ script, as it contains the Health information so it made setting it up easier. First, I made a reference to the correct Pop-up object and set it to inactive by default. Then, I created a new method called Death(), which gets triggered in the if statement in the Update() method with an Invoke (delay in execution) of 5 seconds. The Death() method sets the Pop-up to active, and then if the Left Mouse Button is pressed, the SceneReload() method is executed, which reloads the scene, so the player can start again. Here’s the whole script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class PlayerStats : MonoBehaviour 
{

    private object[] stats;
    private GameObject dScreen;

    void Start () 
    {
        //Values used to calculate player activity
        stats = new object[] 
        {
            100,                                                 //0:Health
            6,                                                   //1:Ammo
            50,                                                  //2:Dimension Swap Charge
        }; 
        dScreen = GameObject.Find (UI PopUps01);
        dScreen.SetActive (false);
    }

    public void SetHealth(int newHealth) 
    {
        stats[0] = newHealth;
    }

    public void SetAmmo(int newAmmo) 
    {
        stats[1] = newAmmo;
    }

    public void SetDimCharge(int newCharge) 
    {
        stats[2] = newCharge;
    }

    public object GetStat(int index) 
    {
        return stats [index];
    } 

    void CheckValues() 
    {
        //Keeps stat values within acceptable ranges
        if ((int)stats[0] < 0) 
        {
            stats[0] = 0;
        } 
        else if ((int)stats[0] > 100) 
        {
            stats[0] = 100;
        }

        if ((int)stats[1] < 0) 
        {
            stats[1] = 0;
        } 
        else if ((int)stats[1] > 6) 
        {
            stats[1] = 6;
        }

        if ((int)stats[2] < 0) 
        {
            stats[2] = 0;
        } 
        else if ((int)stats[2] > 50) 
        {
            stats[2] = 50;
        }
            
    }

    void Death()
    {
        dScreen.SetActive(true);
        if (Input.GetMouseButtonDown (0)) 
        {
            SceneReload ();
        }
    }

    void SceneReload()
    {
        SceneManager.LoadScene (connor_buildings);
    }

    void Update () 
    {
        CheckValues ();

        if ((int)stats [0] < 0.1) 
        {
            GetComponent<Animator>().Play (Death);
            GetComponent<PlayerMove> ().enabled = false;
            GetComponent<RotationCheck> ().enabled = false;
            GetComponent<player_computer> ().enabled = false;
            GetComponent<player_button> ().enabled = false;
            GetComponent<Player_sound> ().enabled = false;
            GetComponent<player_takedown> ().enabled = false;
            GetComponent<PlayerActions> ().enabled = false;
            GetComponent<PlayerControls> ().enabled = false;
            GetComponent<player_stairs> ().enabled = false;
            Invoke (Death, 5f);
        }
    }
}

The video below shows both the starting screen and the death screen(note that the reloaded scene has really weird lighting, I looked into that issue and it is a problem with how it’s displayed inside Unity and will not occur in the build of the game):

Lastly, I have made the Mission Complete screen which I did by creating an Empty Child (named ‘MissionComplete’) with a Box Collider set to isTrigger and the trigger box was scaled correctly inside the starting room. I went back to the ‘player_computer’ script and set the new trigger box to be off by default and to only turn on after the second computer has been used.

Then I attached a script to the MissionComplete object, and made a reference to the correct Pop-up sprite and set it to inactive at the start. Then using ‘OnTriggerEnter’ with the Tag “Player”, Unity knows when the player has arrived to the starting location so it displays the pop-up, disables movement and sets the ‘canReload’ bool to true. While that bool is set to true, the player can press the Left Mouse Button to trigger the SceneReload() method, which is the same as in the script above. Here’s the whole script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MissionComplete : MonoBehaviour 
{
    private GameObject wScreen;
    private bool canReload;

    void Start () 
    {
        wScreen = GameObject.Find (UI PopUps02);
        wScreen.SetActive (false);
        canReload = false;
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.tag == Player)
        {
            GameObject.Find (JackTaylor).GetComponent<PlayerMove> ().enabled = false;
            wScreen.SetActive (true);
            canReload = true;
        }
    }

    void SceneReload()
    {
        SceneManager.LoadScene (connor_buildings);
    }

    void Update () 
    {
        if (Input.GetMouseButtonDown (0) && (canReload == true)) 
        {
            SceneReload ();
        }
    }
}

Here’s a gif of the Mission Complete screen pop-up and scene reloading after the computers have been used:

2017-05-25_00-15-58.gif

Mission Objectives

Next I had to set up the objectives for the level, which Connor decided will be using one computer, which unlocks the ability to use the second one, which then spawns more guards and the player has to go back to the building he started at. To do this, I edited the ‘player_computer’ script, by firstly setting the second computer to only activate after the first one has been used, and then enabling the guards after using the second one. It was pretty simple to set up, but it did take me a while to set up the guards and their waypoints.

Here’s the updated script:

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

public class player_computer : MonoBehaviour 
{

    public Animator anim;
    private bool canInteract;
    private bool isInteracting;
    public bool hasUsed;
    private GameObject g16;
    private GameObject g17;
    private GameObject g18;
    private GameObject g19;
    private GameObject g20;
    private GameObject g21;
    private GameObject g22;
    private GameObject g23;
    private GameObject g24;
    private GameObject g25;
    private GameObject g26;
    private GameObject g27;
    private GameObject g28;
    private GameObject g29;
    private GameObject g30;

    void Start () 
    {
        anim = GetComponent<Animator>();
        canInteract = false;
        isInteracting = false;
        hasUsed = false;
        g16 = GameObject.Find (Light_Guard16);
        g16.SetActive (false);
        g17 = GameObject.Find (Light_Guard17);
        g17.SetActive (false);
        g18 = GameObject.Find (Light_Guard18);
        g18.SetActive (false);
        g19 = GameObject.Find (Light_Guard19);
        g19.SetActive (false);
        g20 = GameObject.Find (Light_Guard20);
        g20.SetActive (false);
        g21 = GameObject.Find (Light_Guard21);
        g21.SetActive (false);
        g22 = GameObject.Find (Light_Guard22);
        g22.SetActive (false);
        g23 = GameObject.Find (Light_Guard23);
        g23.SetActive (false);
        g24 = GameObject.Find (Light_Guard24);
        g24.SetActive (false);
        g25 = GameObject.Find (Light_Guard25);
        g25.SetActive (false);
        g26 = GameObject.Find (Light_Guard26);
        g26.SetActive (false);
        g27 = GameObject.Find (Light_Guard27);
        g27.SetActive (false);
        g28 = GameObject.Find (Light_Guard28);
        g28.SetActive (false);
        g29 = GameObject.Find (Light_Guard29);
        g29.SetActive (false);
        g30 = GameObject.Find (Light_Guard30);
        g30.SetActive (false);
    }
        
    void OnTriggerEnter(Collider other)
    {
        if (other.tag == Computer)
        {
            canInteract = true;
            GameObject.Find (TextInteract).GetComponent<MeshRenderer> ().enabled = true;
        }
    }

    void OnTriggerExit(Collider other)
    {
        if (other.tag == Computer)
        {
            canInteract = false;
            GameObject.Find (TextInteract).GetComponent<MeshRenderer> ().enabled = false;
        }
    }

    void Update () 
    {
        if (Input.GetKeyDown (e) && (canInteract == true)) 
        {
            transform.rotation = Quaternion.Euler(0f, 0f, 0f);
            anim.Play (Interact, 1, 0f);
            isInteracting = true;
            GameObject.Find (TextInteract).GetComponent<MeshRenderer> ().enabled = false;
            hasUsed = true;
            if (GameObject.Find (Computer2).GetComponent<BoxCollider> ().enabled == true) 
            {
                g16.SetActive (true);
                g17.SetActive (true);
                g18.SetActive (true);
                g19.SetActive (true);
                g20.SetActive (true);
                g21.SetActive (true);
                g22.SetActive (true);
                g23.SetActive (true);
                g24.SetActive (true);
                g25.SetActive (true);
                g26.SetActive (true);
                g27.SetActive (true);
                g28.SetActive (true);
                g29.SetActive (true);
                g30.SetActive (true);
            }
            GameObject.Find (Computer2).GetComponent<BoxCollider> ().enabled = true;
            GameObject.Find (Computer1).GetComponent<BoxCollider> ().enabled = false;
        }

        if (GetComponent<Animator> ().GetCurrentAnimatorStateInfo (0).IsName (Interact)) 
        {
            isInteracting = true;
            GetComponent<PlayerMove> ().enabled = false;
            GetComponent<RotationCheck> ().enabled = false;
            GetComponent<player_animation> ().enabled = false;
            canInteract = false;
        }

        if (!GetComponent<Animator> ().GetCurrentAnimatorStateInfo (0).IsName (Interact))
        {
            isInteracting = false;
            GetComponent<PlayerMove> ().enabled = true;
            GetComponent<RotationCheck> ().enabled = true;
            GetComponent<player_animation> ().enabled = true;
        }
    }
}

And here’s a video of it working (on the video I move the player by dragging the object in the Scene editor to make the video shorter, but it also works when played normally):

Setting Up Guards

Now with all the mechanics and level layout in place, I had to set up the Guards to stand and patrol in the right places. It was a relatively easy process, just took a lot of time and changing numbers as every guard needed to be specified in a few different places. First, I duplicated the Light_Guard objects and put them into place. Then renamed them in the Hierarchy to ‘Light_Guard1’, ‘Light_Guard2’ etc:

Unity_2017-05-24_22-09-29

Then I started duplicating and placing their waypoints and named them accordingly:

Unity_2017-05-24_22-12-05

After that, I had to add all the guards and their waypoints to the Waypoint_Management script, which was a chore but it didn’t take that long:

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

public class Waypoint_management : MonoBehaviour 
{
    public List<GameObject> g1;
    public List<GameObject> g2;
    public List<GameObject> g3;
    public List<GameObject> g4;
    public List<GameObject> g5;
    public List<GameObject> g6;
    public List<GameObject> g7;
    public List<GameObject> g8;
    public List<GameObject> g9;
    public List<GameObject> g10;
    public List<GameObject> g11;
    public List<GameObject> g12;
    public List<GameObject> g13;
    public List<GameObject> g14;
    public List<GameObject> g15;

    public List<GameObject> g16;
    public List<GameObject> g17;
    public List<GameObject> g18;
    public List<GameObject> g19;
    public List<GameObject> g20;
    public List<GameObject> g21;
    public List<GameObject> g22;
    public List<GameObject> g23;
    public List<GameObject> g24;
    public List<GameObject> g25;
    public List<GameObject> g26;
    public List<GameObject> g27;
    public List<GameObject> g28;
    public List<GameObject> g29;
    public List<GameObject> g30;

    public List<List<GameObject>> master;

    void Awake () 
    {
        g1 = new List<GameObject> ();
        g1.Add (GameObject.Find (Guard1_Waypoint1));
        g1.Add (GameObject.Find (Guard1_Waypoint2));
        g2 = new List<GameObject> ();
        g2.Add (GameObject.Find (Guard2_Waypoint1));
        g2.Add (GameObject.Find (Guard2_Waypoint2));
        g3 = new List<GameObject> ();
        g3.Add (GameObject.Find (Guard3_Waypoint1));
        g3.Add (GameObject.Find (Guard3_Waypoint2));
        g4 = new List<GameObject> ();
        g4.Add (GameObject.Find (Guard4_Waypoint1));
        g4.Add (GameObject.Find (Guard4_Waypoint2));
        g5 = new List<GameObject> ();
        g5.Add (GameObject.Find (Guard5_Waypoint1));
        g5.Add (GameObject.Find (Guard5_Waypoint2));
        g6 = new List<GameObject> ();
        g6.Add (GameObject.Find (Guard6_Waypoint1));
        g6.Add (GameObject.Find (Guard6_Waypoint2));
        g7 = new List<GameObject> ();
        g7.Add (GameObject.Find (Guard7_Waypoint1));
        g7.Add (GameObject.Find (Guard7_Waypoint2));
        g8 = new List<GameObject> ();
        g8.Add (GameObject.Find (Guard8_Waypoint1));
        g8.Add (GameObject.Find (Guard8_Waypoint2));
        g9 = new List<GameObject> ();
        g9.Add (GameObject.Find (Guard9_Waypoint1));
        g9.Add (GameObject.Find (Guard9_Waypoint2));
        g10 = new List<GameObject> ();
        g10.Add (GameObject.Find (Guard10_Waypoint1));
        g10.Add (GameObject.Find (Guard10_Waypoint2));
        g11 = new List<GameObject> ();
        g11.Add (GameObject.Find (Guard11_Waypoint1));
        g11.Add (GameObject.Find (Guard11_Waypoint2));
        g12 = new List<GameObject> ();
        g12.Add (GameObject.Find (Guard12_Waypoint1));
        g12.Add (GameObject.Find (Guard12_Waypoint2));
        g13 = new List<GameObject> ();
        g13.Add (GameObject.Find (Guard13_Waypoint1));
        g13.Add (GameObject.Find (Guard13_Waypoint2));
        g14 = new List<GameObject> ();
        g14.Add (GameObject.Find (Guard14_Waypoint1));
        g14.Add (GameObject.Find (Guard14_Waypoint2));
        g15 = new List<GameObject> ();
        g15.Add (GameObject.Find (Guard15_Waypoint1));
        g15.Add (GameObject.Find (Guard15_Waypoint2));

        g16 = new List<GameObject> ();
        g16.Add (GameObject.Find (Guard16_Waypoint1));
        g16.Add (GameObject.Find (Guard16_Waypoint2));
        g17 = new List<GameObject> ();
        g17.Add (GameObject.Find (Guard17_Waypoint1));
        g17.Add (GameObject.Find (Guard17_Waypoint2));
        g18 = new List<GameObject> ();
        g18.Add (GameObject.Find (Guard18_Waypoint1));
        g18.Add (GameObject.Find (Guard18_Waypoint2));
        g19 = new List<GameObject> ();
        g19.Add (GameObject.Find (Guard19_Waypoint1));
        g19.Add (GameObject.Find (Guard19_Waypoint2));
        g20 = new List<GameObject> ();
        g20.Add (GameObject.Find (Guard20_Waypoint1));
        g20.Add (GameObject.Find (Guard20_Waypoint2));
        g21 = new List<GameObject> ();
        g21.Add (GameObject.Find (Guard21_Waypoint1));
        g21.Add (GameObject.Find (Guard21_Waypoint2));
        g22 = new List<GameObject> ();
        g22.Add (GameObject.Find (Guard22_Waypoint1));
        g22.Add (GameObject.Find (Guard22_Waypoint2));
        g23 = new List<GameObject> ();
        g23.Add (GameObject.Find (Guard23_Waypoint1));
        g23.Add (GameObject.Find (Guard23_Waypoint2));
        g24 = new List<GameObject> ();
        g24.Add (GameObject.Find (Guard24_Waypoint1));
        g24.Add (GameObject.Find (Guard24_Waypoint2));
        g25 = new List<GameObject> ();
        g25.Add (GameObject.Find (Guard25_Waypoint1));
        g25.Add (GameObject.Find (Guard25_Waypoint2));
        g26 = new List<GameObject> ();
        g26.Add (GameObject.Find (Guard26_Waypoint1));
        g26.Add (GameObject.Find (Guard26_Waypoint2));
        g27 = new List<GameObject> ();
        g27.Add (GameObject.Find (Guard27_Waypoint1));
        g27.Add (GameObject.Find (Guard27_Waypoint2));
        g28 = new List<GameObject> ();
        g28.Add (GameObject.Find (Guard28_Waypoint1));
        g28.Add (GameObject.Find (Guard28_Waypoint2));
        g29 = new List<GameObject> ();
        g29.Add (GameObject.Find (Guard29_Waypoint1));
        g29.Add (GameObject.Find (Guard29_Waypoint2));
        g30 = new List<GameObject> ();
        g30.Add (GameObject.Find (Guard30_Waypoint1));
        g30.Add (GameObject.Find (Guard30_Waypoint2));

        master = new List<List<GameObject>> ();
        master.Add (g1);
        master.Add (g2);
        master.Add (g3);
        master.Add (g4);
        master.Add (g5);
        master.Add (g6);
        master.Add (g7);
        master.Add (g8);
        master.Add (g9);
        master.Add (g10);
        master.Add (g11);
        master.Add (g12);
        master.Add (g13);
        master.Add (g14);
        master.Add (g15);
        master.Add (g16);
        master.Add (g17);
        master.Add (g18);
        master.Add (g19);
        master.Add (g20);
        master.Add (g21);
        master.Add (g22);
        master.Add (g23);
        master.Add (g24);
        master.Add (g25);
        master.Add (g26);
        master.Add (g27);

        master.Add (g28);
        master.Add (g29);
        master.Add (g30);

    }

    void Update () 
    {
        
    }
}

Then I had to change the Waypoint Index value for every guard in the Inspector to assign them to their waypoints:

Unity_2017-05-24_22-18-59

Then, for the guards that are meant to stand still, I made it so that once they see a player, their patrol points are enabled so if the player runs away, they start patrolling instead. The way I did that was I turned off the ‘FSM_States’ and ‘FSM_Behaviour’ scripts for those guards in the Inspector, and then in the ‘enemy_sight’ script I added a couple of lines of code in the Scan() method to turn them on when the player is seen. Here’s the updated Scan() method:

    void Scan()
    {
        Debug.DrawRay (transform.position + new Vector3 (0f, 1.6f, 0f), transform.forward * 10, Color.red);
        RaycastHit hit;

        if (Physics.Raycast (transform.position + new Vector3 (0f, 1.6f, 0f), transform.forward * 10, out hit, 10)) 
        {
            switch (hit.collider.gameObject.tag) 
            {
            case Player:
                seesPlayer = true;
                GetComponent<FSM_Behaviour> ().enabled = true;
                GetComponent<FSM_States> ().enabled = true;
                playerLastLocation = hit.collider.gameObject.transform.position;
                break;
            }
        }
        else
        {
            seesPlayer = false;
        }
    }

That left me with a working patrol system throughout the level:

2017-05-24_22-23-22

Camera Position

Next I just wanted to attach the camera to the player so that it always follows him, however simply parenting it to the player object in the Hierarchy didn’t work as the who character rotates by 180 degrees when going in the other direction, so the camera messes up, so I had to write a script for it. It was really simple though, so it wasn’t a problem at all, and it was a lot like the script for the Text Mesh. I simply got the position values of the player object, then using ‘new Vector3’ I added some to make sure it’s where I want it to be:

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

public class Camera_position : MonoBehaviour 
{
    void Update () 
    {
        transform.position = GameObject.Find (JackTaylor).transform.position + new Vector3 (11f, 1.3f, 0f);
    }
}

Here’s a gif of it working:

2017-05-24_21-44-13.gif