Steffan's Game Development Portfolio

See Projects Go to git repositories
Project AI
February 2017 - June 2017

The AI Project is a combination of 3 experimental projects of mine. The first being a system that communicates through the http protocol to a php backend. The php backend runs on a server and has a direct connection to a MySQL. The game can place requests for data and also send data to the server to be stored in the database. All the data sent between the server and the game is encrypted using the Rijndael advanced encryption standard. By using this encryption all the data that is sent between the client and the server are only readable by the client and the server, no third party can intercept and decrypt the packages.
The second part of the project is a terrain, I've put quite a few hours into getting a realistic terrain with lots of detail.
The third part is a Goal Driven Behaviour AI that uses a network of nodes with an A* algorithm to find its path to its goals.
External Links


Project AI

UserData
The UserData script handles communication with the php backend about the user's data, including the login of the user

UserData.cs
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.Runtime.Serialization.Formatters.Binary;
  4. using System.IO;
  5. using System;
  6. using UnityEngine;
  7. using UnityEngine.SceneManagement;
  8. using Utilities.Crypt;
  9. using System.Text;
  10. using System.Text.RegularExpressions;
  11. using Warroom.UI;
  12.  
  13. public class UserData : MonoBehaviour {
  14.  
  15.     private static UserData instance = null;
  16.     public static UserData Instance
  17.     {
  18.         get { return instance; }
  19.     }
  20.  
  21.     void Awake()
  22.     {
  23.         if (instance != null && instance != this)
  24.         {
  25.             Destroy(this.gameObject);
  26.             return;
  27.         }
  28.         else
  29.         {
  30.             instance = this;
  31.         }
  32.         DontDestroyOnLoad(this);
  33.         Time.timeScale = 1;
  34.         Load();
  35.     }
  36.  
  37.     //Vars
  38.     public UserFile userfile = null;
  39.  
  40.     void Start() {
  41.         if (userfile == null)
  42.         {
  43.             SceneManager.LoadScene("signin");
  44.         }
  45.         else {
  46.             StartCoroutine(ValidateAuthKey(userfile));
  47.         }
  48.     }
  49.     IEnumerator ValidateAuthKey(UserFile user) {
  50.         string validateurl = GameSettings.Instance.baseUrlWebBackend + "validateauthkey.php";
  51.         WWWForm form = new WWWForm();
  52.         form.AddField("deviceid", SystemInfo.deviceUniqueIdentifier);
  53.         form.AddField("userid", userfile.GetUserID());
  54.         form.AddField("data", Crypt.encrypt(userfile.GetAuthToken(), userfile.GetAuthToken()));
  55.         WWW download = new WWW(validateurl, form);
  56.         yield return download;
  57.         if (!string.IsNullOrEmpty(download.error)) {
  58.             showConnectionError(download.error);
  59.         } else {
  60.             checkValidation(download.text);        
  61.         }
  62.     }
  63.     IEnumerator RequestAuthkey(string user, string password)
  64.     {
  65.         string validateurl = GameSettings.Instance.baseUrlWebBackend + "requestauthkey.php";
  66.         WWWForm form = new WWWForm();
  67.         string loginData = user + "|" + Crypt.Md5Sum(password);
  68.         loginData = Crypt.encrypt(loginData, Crypt.Md5Sum(SystemInfo.deviceUniqueIdentifier));
  69.         form.AddField("deviceid", SystemInfo.deviceUniqueIdentifier);
  70.         form.AddField("logindata", loginData);
  71.         WWW download = new WWW(validateurl, form);
  72.         yield return download;
  73.         if (!string.IsNullOrEmpty(download.error))
  74.         {
  75.             showConnectionError(download.error);
  76.         }
  77.         else {
  78.             ProcessAuthkey(download.text, Crypt.Md5Sum(password));
  79.         }
  80.     }
  81.     void checkValidation(string data) {
  82.         if (data.Contains("1"))
  83.         {
  84.             gameObject.AddComponent<GraphicsSettings>();
  85.             Destroy(gameObject.GetComponent<GraphicsSettings>());
  86.             SceneManager.LoadScene("MainMenu");
  87.         }
  88.         else {
  89.             SceneManager.LoadScene("signin");
  90.         }
  91.     }
  92.     void ProcessAuthkey(string data, string pass) {
  93.         if (data.Contains("notactivated"))
  94.         {
  95.             ErrorDisplay.Instance.ShowNotActivatedError();
  96.         }
  97.         else if (data.Contains("poruwrong"))
  98.         {
  99.             ErrorDisplay.Instance.ShowIncorrectPasswordOrUser();
  100.         }
  101.         else
  102.         {
  103.             string data3 = Crypt.decrypt(data, pass);
  104.             string[] data2 = data3.Split(new string[] { "||" }, StringSplitOptions.None);
  105.             string username = Regex.Replace(data2[0], @"\s+", "");
  106.             string userid = Regex.Replace(data2[1], @"\s+", "");
  107.             string authkey = data2[2].Substring(0, 32);
  108.             userfile = new UserFile(username, userid, authkey);
  109.             SaveData();
  110.             gameObject.AddComponent<GraphicsSettings>();
  111.             Destroy(gameObject.GetComponent<GraphicsSettings>());
  112.             SceneManager.LoadScene("MainMenu");
  113.         }
  114.     }
  115.     public void showConnectionError(string error) {
  116.         ErrorDisplay.Instance.ShowConnectionError(error);
  117.     }
  118.  
  119.     public void SignIn(string user, string password) {
  120.         StartCoroutine(RequestAuthkey(user, password));
  121.     }
  122.  
  123.     public void SaveFacebookData(string username, string userid, string authtoken, string facebookuserid) {
  124.         userfile = new UserFile(username, userid, authtoken, facebookuserid);
  125.         SaveData();
  126.     }
  127.  
  128.     //Load and save functions
  129.     public void SaveData()
  130.     {
  131.         BinaryFormatter bf = new BinaryFormatter();
  132.         FileStream file = File.Open(Application.persistentDataPath + "/userdata.dat", FileMode.OpenOrCreate);
  133.         UserFile data = userfile;
  134.         bf.Serialize(file, data);
  135.         file.Close();
  136.     }
  137.  
  138.  
  139.     public void Load()
  140.     {
  141.         if (File.Exists(Application.persistentDataPath + "/userdata.dat"))
  142.         {
  143.             BinaryFormatter bf = new BinaryFormatter();
  144.             FileStream file = File.Open(Application.persistentDataPath + "/userdata.dat", FileMode.Open);
  145.             UserFile data = (UserFile)bf.Deserialize(file);
  146.             file.Close();
  147.  
  148.             this.userfile = data;
  149.         }
  150.     }
  151. }
  152. [Serializable]
  153. public class UserFile {
  154.     string username = "";
  155.     string userid = "";
  156.     string authtoken = "";
  157.     int accounttype = 0;
  158.     string facebookid = "";
  159.  
  160.     public UserFile(string username, string userid, string authtoken) {
  161.         this.username = username;
  162.         this.userid = userid;
  163.         this.authtoken = authtoken;
  164.     }
  165.     public UserFile(string username, string userid, string authtoken, string facebookid) {
  166.         this.username = username;
  167.         this.userid = userid;
  168.         this.authtoken = authtoken;
  169.         this.accounttype = 1;
  170.         this.facebookid = facebookid;
  171.     }
  172.     public string GetUsername() {
  173.         return username;
  174.     }
  175.     public string GetUserID() {
  176.         return userid;
  177.     }
  178.     public string GetAuthToken() {
  179.         return authtoken;
  180.     }
  181.     public int GetAccountType() {
  182.         return accounttype;
  183.     }
  184.     public string GetFacebookID() {
  185.         return facebookid;
  186.     }
  187. }
  188.  
UserData.cs


Pathfinder
The Pathfinder scripts contains an A* algorithm that finds the quickest route in a network of nodes

PathFinder.cs
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using System.Linq;
  5. using System;
  6.  
  7. namespace Warroom.AI
  8. {
  9.     public class PathFinder : MonoBehaviour
  10.     {
  11.         public Graaf graaf;
  12.         public Node startNode;
  13.         public Node targetNode;
  14.         public List<Node> path = new List<Node>();
  15.  
  16.  
  17.         public List<QueueItem> nodes = new List<QueueItem>();
  18.         public List<QueueItem> queue = new List<QueueItem>();
  19.  
  20.         public List<Node> CalculatePath(Node target)
  21.         {
  22.             FindClosestNode();
  23.             this.targetNode = target;
  24.             this.path = FindPath();
  25.             return this.path;
  26.         }
  27.  
  28.         void FindClosestNode()
  29.         {
  30.             float distanceToClosest = 0;
  31.             int closest = 0;
  32.             for (int i = 0; i < graaf.nodes.Count; i++)
  33.             {
  34.                 float distance = Vector3.Distance(transform.position, graaf.nodes[i].transform.position);
  35.                 if (distance < distanceToClosest || distanceToClosest == 0)
  36.                 {
  37.                     distanceToClosest = distance;
  38.                     closest = i;
  39.                 }
  40.             }
  41.             this.startNode = graaf.nodes[closest];
  42.         }
  43.  
  44.         List<Node> FindPath()
  45.         {
  46.             // Clear the lists used to create a path
  47.             nodes.Clear();
  48.             queue.Clear();
  49.  
  50.             int start = 0;
  51.             int end = 0;
  52.  
  53.             //Find the position of the start-node and end-node in the node list of Graaf
  54.             for (int i = 0; i < graaf.nodes.Count; i++)
  55.             {
  56.                 nodes.Add(new QueueItem(i, i, 0, 0, false));
  57.                 if (startNode == graaf.nodes[i])
  58.                 {
  59.                     start = i;
  60.                 }
  61.                 if (targetNode == graaf.nodes[i])
  62.                 {
  63.                     end = i;
  64.                 }
  65.             }
  66.             //Add start node the Queue
  67.             queue.Add(nodes[start]);
  68.  
  69.             bool endNodeFound = false;
  70.  
  71.             //Iterate through queue till end-node is found
  72.             while (!endNodeFound)
  73.             {
  74.                 //Break loop when the end node is found
  75.                 if (queue[0].node == end)
  76.                 {
  77.                     endNodeFound = true;
  78.                     break;
  79.                 }
  80.  
  81.                 //Loop through the neighbours of the first item in the queue
  82.                 for (int i = 0; i < graaf.nodes[queue[0].node].neighbours.Count; i++)
  83.                 {
  84.                     int currentItem = graaf.nodes[queue[0].node].neighbours[i].node.posInList;
  85.  
  86.                     //Check wether current item is already in the queue
  87.                     bool alreadyInQueue = false;
  88.                     int posInQueue = 0;
  89.                     for (int j = 0; j < queue.Count; j++)
  90.                     {
  91.                         if (queue[j].node == currentItem)
  92.                         {
  93.                             alreadyInQueue = true;
  94.                             posInQueue = j;
  95.                             break;
  96.                         }
  97.                     }
  98.                     //Calculate weight and the heuretics of the neighbour
  99.                     float weight = graaf.nodes[queue[0].node].neighbours[i].GetWeight() + queue[0].weight;
  100.                     float heuru = Vector3.Distance(graaf.nodes[currentItem].transform.position, graaf.nodes[end].transform.position);
  101.                     if (alreadyInQueue)
  102.                     {
  103.                         if (nodes[currentItem].weight > weight || nodes[currentItem].node == nodes[currentItem].origin)
  104.                         {
  105.                             queue[posInQueue].origin = queue[0].node;
  106.                             queue[posInQueue].weight = weight;
  107.                             queue[posInQueue].heuru = heuru;
  108.                             nodes[currentItem].origin = queue[0].node;
  109.                             nodes[currentItem].weight = weight;
  110.                             nodes[currentItem].heuru = heuru;
  111.                         }
  112.                     }
  113.                     else
  114.                     {
  115.                         if (nodes[currentItem].weight > weight || nodes[currentItem].node == nodes[currentItem].origin && nodes[currentItem].node != start)
  116.                         {
  117.                             if (!nodes[currentItem].nodeChecked)
  118.                             {
  119.                                 queue.Add(new QueueItem(currentItem, queue[0].node, weight, heuru, false));
  120.                             }
  121.                             nodes[currentItem].origin = queue[0].node;
  122.                             nodes[currentItem].weight = weight;
  123.                             nodes[currentItem].heuru = heuru;
  124.                         }
  125.  
  126.                     }
  127.  
  128.                 }
  129.                 //Mark the node as checked and remove it from the queue
  130.                 nodes[queue[0].node].nodeChecked = true;
  131.                 queue.RemoveAt(0);
  132.  
  133.                 //Sort queue by priority values
  134.                 queue = SortByPriority(queue);
  135.             }
  136.  
  137.             bool pathComplete = false;
  138.  
  139.             //Create a prepath list with the integer positions of the nodes in the nodes-list
  140.             List<int> prePath = new List<int>();
  141.             prePath.Add(nodes[end].node);
  142.             prePath.Add(nodes[end].origin);
  143.             int currentItemBack = nodes[end].origin;
  144.  
  145.             while (!pathComplete)
  146.             {
  147.                 prePath.Add(nodes[currentItemBack].origin);
  148.                 if (nodes[currentItemBack].origin == start)
  149.                 {
  150.                     pathComplete = true;
  151.                     break;
  152.                 }
  153.                 currentItemBack = nodes[currentItemBack].origin;
  154.  
  155.             }
  156.  
  157.             //Translate prepath to actual Objects
  158.             List<Node> path = new List<Node>();
  159.             for (int i = (prePath.Count - 1); i >= 0; i--)
  160.             {
  161.                 path.Add(graaf.nodes[prePath[i]]);
  162.             }
  163.  
  164.             //Finally the return statement!
  165.             return path;
  166.         }
  167.  
  168.         List<QueueItem> SortByPriority(List<QueueItem> list)
  169.         {
  170.             List<QueueItem> newlist = list.OrderBy(x => x.GetPriority()).ToList();
  171.             return newlist;
  172.         }
  173.     }
  174.     [Serializable]
  175.     public class QueueItem
  176.     {
  177.         public int node = 0;
  178.         public int origin = 0;
  179.         public float weight = 0;
  180.         public float heuru = 0;
  181.         public bool nodeChecked = false;
  182.  
  183.         public QueueItem(int node, int origin, float weight, float heuru, bool nodeChecked)
  184.         {
  185.             this.node = node;
  186.             this.origin = origin;
  187.             this.weight = weight;
  188.             this.heuru = heuru;
  189.             this.nodeChecked = nodeChecked;
  190.         }
  191.  
  192.         public float GetPriority()
  193.         {
  194.             return weight + heuru;
  195.         }
  196.     }
  197. }
PathFinder.cs


Character
The Character class contains the goal driven behaviour of the character

Character.cs
  1. using System.Collections;
  2. using System;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5.  
  6. namespace Warroom.AI
  7. {
  8.     [RequireComponent(typeof(PathFinder))]
  9.     [RequireComponent(typeof(Rigidbody))]
  10.     public class Character : MonoBehaviour
  11.     {
  12.         [Header("Movement & Rigidbody")]
  13.         public Vector3 centreOfMass = new Vector3(0, -1, 0);
  14.         public float acceleration = 10;
  15.         public float rotationSpeed = 50;
  16.  
  17.         [Header("Goal driven behaviour")]
  18.         public List<Need> needs = new List<Need>();
  19.         public int currentNeed = 0;
  20.         float needTime = 0;
  21.  
  22.  
  23.         PathFinder pathFinder = null;
  24.         public List<Node> path = new List<Node>();
  25.         public Node currentNode = null;
  26.         const float kmhToMsConvertValue = 0.1f;
  27.         Rigidbody rb;
  28.  
  29.         // Use this for initialization
  30.         void Start()
  31.         {
  32.             pathFinder = GetComponent<PathFinder>();
  33.             rb = GetComponent<Rigidbody>();
  34.             rb.ResetCenterOfMass();
  35.             rb.centerOfMass += centreOfMass;
  36.             currentNeed = CalculateDiscontentment();
  37.             needTime = needs[currentNeed].activityTime;
  38.         }
  39.  
  40.         // Update is called once per frame
  41.         void Update()
  42.         {
  43.             AddNeed();
  44.  
  45.             //Check if the character is at the node that belongs to the current needs action
  46.             if (currentNode == needs[currentNeed].node)
  47.             {
  48.                 rb.isKinematic = true;
  49.                 needTime -= Time.deltaTime;
  50.  
  51.                 //check if the time that a certain needs takes is over, if yes, calculate a new need and a path to that need
  52.                 if (needTime <= 0)
  53.                 {
  54.                     ApplyCosts();
  55.                     currentNeed = CalculateDiscontentment();
  56.                     needTime = needs[currentNeed].activityTime;
  57.                     path = pathFinder.CalculatePath(needs[currentNeed].node);
  58.                 }
  59.             }
  60.             else
  61.             {
  62.                 //Calculate path if needed
  63.                 if (path.Count < 1)
  64.                 {
  65.                     path = pathFinder.CalculatePath(needs[currentNeed].node);
  66.                 }
  67.                 rb.isKinematic = false;
  68.                 // Move character along the path to its current need
  69.                 Move();
  70.             }
  71.         }
  72.  
  73.         //Move along the given path
  74.         private void Move()
  75.         {
  76.             Node target = null;
  77.             if (currentNode == null)
  78.             {
  79.                 target = path[0];
  80.             }
  81.             else
  82.             {
  83.                 for (int i = 0; i < path.Count; i++)
  84.                 {
  85.                     if (path[i] == currentNode)
  86.                     {
  87.                         if (i + 1 < path.Count)
  88.                         {
  89.                             target = path[i + 1];
  90.                             break;
  91.                         }
  92.                         else
  93.                         {
  94.                             return;
  95.                         }
  96.                     }
  97.                 }
  98.             }
  99.             if (target != null)
  100.             {
  101.                 float speed = 0;
  102.                 if (currentNode != null)
  103.                 {
  104.                     NodeConnection nc = currentNode.GetConnectionToNode(target);
  105.                     if (nc != null)
  106.                     {
  107.                         speed = 5;
  108.                     }
  109.                 }
  110.                 else
  111.                 {
  112.                     speed = 5;
  113.                 }
  114.  
  115.                 Vector3 direction = (target.transform.position - transform.position).normalized;
  116.                 Quaternion look = Quaternion.LookRotation(direction);
  117.                 transform.rotation = Quaternion.Slerp(transform.rotation, look, Time.deltaTime * rotationSpeed);
  118.  
  119.                 transform.position += new Vector3(speed * Time.deltaTime * transform.forward.x, speed * Time.deltaTime * transform.forward.y, speed * Time.deltaTime * transform.forward.z);
  120.  
  121.             }
  122.  
  123.         }
  124.  
  125.         //Increase the values of each need
  126.         void AddNeed()
  127.         {
  128.             for (int i = 0; i < needs.Count; i++)
  129.             {
  130.                 needs[i].currentWorth += needs[i].increasePerSecond * Time.deltaTime;
  131.             }
  132.         }
  133.  
  134.         //Apply the costs  of an action to the characters needs
  135.         void ApplyCosts()
  136.         {
  137.             for (int i = 0; i < needs[currentNeed].costs.Count; i++)
  138.             {
  139.                 for (int j = 0; j < needs.Count; j++)
  140.                 {
  141.                     if (needs[j].needName == needs[currentNeed].costs[i].needName)
  142.                     {
  143.                         needs[j].currentWorth += needs[currentNeed].costs[i].cost;
  144.                         break;
  145.                     }
  146.                 }
  147.             }
  148.         }
  149.  
  150.         //Calculate which need is the best to fulfil
  151.         int CalculateDiscontentment()
  152.         {
  153.             List<NeedCost> discontentments = new List<NeedCost>();
  154.  
  155.             for (int i = 0; i < needs.Count; i++)
  156.             {
  157.                 float discontentment = 0;
  158.                 for (int x = 0; x < needs[i].costs.Count; x++)
  159.                 {
  160.                     float currentVal = 0;
  161.                     for (int y = 0; y < needs.Count; y++)
  162.                     {
  163.                         if (needs[y].needName == needs[i].costs[x].needName)
  164.                         {
  165.                             currentVal = needs[y].currentWorth;
  166.                             break;
  167.                         }
  168.                     }
  169.                     discontentment += (currentVal + needs[i].costs[x].cost) * (currentVal + needs[i].costs[x].cost);
  170.                 }
  171.                 discontentments.Add(new NeedCost(needs[i].needName, discontentment));
  172.             }
  173.             if (discontentments.Count > 0)
  174.             {
  175.                 int lowestDiscontentment = 0;
  176.                 for (int i = 0; i < discontentments.Count; i++)
  177.                 {
  178.                     if (discontentments[i].cost < discontentments[lowestDiscontentment].cost)
  179.                     {
  180.                         lowestDiscontentment = i;
  181.                     }
  182.                 }
  183.                 for (int i = 0; i < needs.Count; i++)
  184.                 {
  185.                     if (needs[i].needName == discontentments[lowestDiscontentment].needName)
  186.                     {
  187.                         return i;
  188.                     }
  189.                 }
  190.             }
  191.             return 0;
  192.         }
  193.  
  194.         private void OnTriggerEnter(Collider other)
  195.         {
  196.             Node n = other.gameObject.GetComponent<Node>();
  197.             if (n != null)
  198.             {
  199.                 if (pathFinder.path.Contains(n))
  200.                 {
  201.                     currentNode = n;
  202.                 }
  203.             }
  204.         }
  205.     }
  206.  
  207.     [Serializable]
  208.     public class Need
  209.     {
  210.         public string needName = "Toilet";
  211.         public float currentWorth = 5;
  212.         public float increasePerSecond = 0.05f;
  213.         public float activityTime = 5;
  214.         public Node node;
  215.  
  216.         public List<NeedCost> costs = new List<NeedCost>();
  217.     }
  218.     [Serializable]
  219.     public class NeedCost
  220.     {
  221.         public string needName;
  222.         public float cost;
  223.  
  224.         public NeedCost(string needName, float cost)
  225.         {
  226.             this.needName = needName;
  227.             this.cost = cost;
  228.         }
  229.     }
  230. }
Character.cs