Raskol

Raskol is a top-down twin-stick shooter, set in an alternative Russia, divided after WW1. The player has direct control of the foot soldier Matvei, and indirect control of a tank commander, Oksana. The player uses a map to give orders to Oksana, telling her where to go with the tank and where to fire. Using the strengths and weaknesses of both characters, the player traverses the desolate lands of Russia and fights for their survival.

Platform:
Engine:
Language:
Development Time:
Team Size:

PC
Unity 5
C#
7 Weeks
3 Designers & 5 Artists

RESPONSIBILITIES

During the game project, I was a Scripter & Designer. My main scripting responsibilities were:

I also worked on other Additional tasks, such as the Explosion system, Dialog system, UI effects, UI- and Particle implementation. Post-Mortem of the project can be found at the bottom of the page.

Map System
Icon Managing

The map is a central part of the game. It is both used for navigation and for guiding the tank. The map itself is a drawing of the area, but the different icons cannot be handled so easily.

I didn’t want every object handling its own icon. This to separate the object and the map as much as possible  To do this, I gave every icon a unique ID and made an icon manager that stores all the ID’s of all the active icons. In order to update the icon, the object sends the icon ID and information to the icon manager. Then the icon manager updates the icon with the matching ID. 

Icon POSITIONING

The icons on the map are only useful if they accurately show the objects’s position.

To do this I placed two Points on opposite edges of the playable area. I did the same with the map, but the two Points are two opposite Edges instead. The objects’s position relative to the two Points is sent to the icon manager. It then moves the icon to that position, but in relation to the Edges instead.

< CODE: Icon Manager >< CODE: Update Icon >< CODE: ID Manager >

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public enum IconAnimations
{
    None,
    AtStart,
    Continues
}

public struct IconData
{
    public Image image;
    public Transform transform;
    public bool rotate;
    public int layer;
    public Vector2 size;
    public IconAnimations animation;
}

[RequireComponent(typeof(MapVisibility))]
[RequireComponent(typeof(IconAnimator))]
public class IconManager : MonoBehaviour
{
    [SerializeField] private Transform bounds;
    [SerializeField] private GameObject[] layers;


    //Vectors
    private Vector2 minBound;
    private Vector2 maxBound;

    //References
    private MapVisibility mapVisibility;
    private IconAnimator iconAnimator;

    //Collections
    internal Dictionary<string, Image> activeIcons = new Dictionary<string, Image>();


    private void Awake()
    {
        foreach (GameObject layer in layers)
        {
            layer.GetComponent<RectTransform>().offsetMax = transform.GetComponent<RectTransform>().offsetMax;
            layer.GetComponent<RectTransform>().offsetMin = transform.GetComponent<RectTransform>().offsetMin;
        }

        Vector3 tempMinBound = bounds.GetChild(0).position;
        minBound = new Vector2(tempMinBound.x, tempMinBound.z);
        Vector3 tempMaxBound = bounds.GetChild(1).position;
        maxBound = new Vector2(tempMaxBound.x, tempMaxBound.z);

        mapVisibility = transform.GetComponent<MapVisibility>();
        iconAnimator = transform.GetComponent<IconAnimator>();
    }

    public void UpdatePosOnMap(string id, IconData data)
    {
        if (activeIcons.ContainsKey(id))
        {
            //If ID already is active, update corresponding icon
            SetPosOnMap(activeIcons[id], data);
        }
        else
        {
            //If ID is not active, add it
            AddIcon(id, data);
        }
    }

    private void AddIcon(string id, IconData data)
    {
        Image icon = Instantiate(data.image, data.transform.position, data.transform.rotation, transform);

        //Set-up icon size, layer, default position and enabled state
        icon.GetComponent<RectTransform>().sizeDelta = data.size;
        icon.transform.SetParent(layers[data.layer - 1].transform);
        icon.rectTransform.localPosition = Vector2.zero;
        icon.enabled = mapVisibility.mapEnabled;

        activeIcons.Add(id, icon);

        //If icon has animation, add its ID to apropriate List
        switch (data.animation)
        {
            case IconAnimations.AtStart:
                iconAnimator.AddStartAnimKey(id);
                break;
            
            case IconAnimations.Continues:
                iconAnimator.AddContinuesAnimKey(id);
                break;
        }

        SetPosOnMap(icon, data);
    }

    private void SetPosOnMap(Image icon, IconData data)
    {
        //Get the position in the world, relativ to the map
        Vector2 relativPos;
        relativPos.x = (data.transform.position.x - minBound.x) / (maxBound.x - minBound.x);
        relativPos.y = (data.transform.position.z - minBound.y) / (maxBound.y - minBound.y);

        //Get icon min anchors
        Vector2 min;
        min.x = icon.rectTransform.anchorMin.x;
        min.y = icon.rectTransform.anchorMin.y;
        //Get icon max anchors
        Vector2 max;
        max.x = icon.rectTransform.anchorMax.x;
        max.y = icon.rectTransform.anchorMax.y;

        //Move icon anchors
        icon.rectTransform.anchorMin = new Vector2(relativPos.x - ((max.x - min.x) / 2), relativPos.y - ((max.y - min.y) / 2));
        icon.rectTransform.anchorMax = new Vector2(relativPos.x + ((max.x - min.x) / 2), relativPos.y + ((max.y - min.y) / 2));

        //Get the rotation that the icon should have
        Vector3 t = data.rotate ? data.transform.forward : Vector3.zero;
        //Rotate icon
        icon.rectTransform.rotation = Quaternion.Euler(0, 0, Mathf.Rad2Deg * Mathf.Atan2(t.z, t.x));
    }

    public void RemoveIcon(string id)
    {
        if (activeIcons.ContainsKey(id))
        {
            Image icon = activeIcons[id];
            activeIcons.Remove(id);
            DestroyObject(icon.gameObject);
        }
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UpdateIcon: MonoBehaviour
{
    [Header ("Map Icon Data")]
    [SerializeField] private Sprite sprite;
    [SerializeField] [Range(1, 5)] private int layer = 3;
    [SerializeField] private Vector2 size = new Vector2(20, 20);
    [SerializeField] private bool rotateOnMap = true;
    [SerializeField] private IconAnimations iconAnimation;

    [Header("Update Map Icon")]
    [SerializeField] private bool stopUpdatingPos;


    //Collections
    private IconData data;

    //References
    private IconManager manager;

    //Strings
    private string id;


    private void Awake()
    {
        //Make image out of sprite
        GameObject newObj = new GameObject();
        Image newImage = newObj.AddComponent<Image>();
        newImage.sprite = sprite;
        //What name it is going to have in the inspector
        newImage.gameObject.name = gameObject.name + " Icon";
        data.image = newImage;

        //Set up rest of icon data
        data.transform = transform;
        data.layer = layer;
        data.size = size;
        data.rotate = rotateOnMap;
        data.animation = iconAnimation;
    }

    private void Start()
    {
        //Get a unique ID
        id = Global.Instance.idManager.GenerateID();

        manager = FindObjectOfType<IconManager>();
    }

    private void Update()
    {
        if(manager == null)
        {
            manager = FindObjectOfType<IconManager >();
        }
        else if (!stopUpdatingPos)
        {
            manager.UpdatePosOnMap(id, data);
        }
    }

    public void RemoveMapIcon()
    {
        if (stopUpdatingPos == true)
            return;

        stopUpdatingPos = true;

        if (manager == null)
            manager = FindObjectOfType<IconManager>();

        manager.RemoveIcon(id);
    }

    public void AddMapIcon()
    {
        if (stopUpdatingPos == false)
            return;

        stopUpdatingPos = false;
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IDManager : MonoBehaviour
{
    private List<string> GeneratedIDs = new List<string>();
    private string characters = "0123456789abcdefghijklmnopqrstuvwxABCDEFGHIJKLMNOPQRSTUVWXYZ";


    public string GenerateID()
    {
        string newID;
        bool unique;

        while (true)
        {
            newID = "";
            unique = true;

            //There are 2,944,827,765 diffrent combinations, i think this will suffice
            for (int i = 0; i < 8; i++)
            {
                int a = Random.Range(0, characters.Length);
                newID = newID + characters[a];
            }

            foreach (string id in GeneratedIDs)
            {
                if (newID == id)
                {
                    unique = false;
                    break;
                }
            }

            if (unique)
                break;
            else
                continue;
        }

        GeneratedIDs.Add(newID);
        return (newID);
    }
}
Icon Animation

To give certain icons a bigger presence and importance on the map, you can give them one of two animations.

AtStart

Makes the icon much bigger on the map, the first time you see it. It will then quickly shrink to its original size. This allows for highlighting new things on the map that the player should immediately notice.

Continues

Makes the icons pulsate in size. Icons that are very important, like the level goal, could have this animation to signify their relevance.

 

< CODE: Icon Animator >

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(IconManager))]
public class IconAnimator : MonoBehaviour
{
    [Header("Continues Animation Settings")]
    [SerializeField] [Range(0.1f, 3f)] private float continuesAnimSpeed = 1f;
    [SerializeField] [Range(0.01f, 0.5f)] private float contiunesAnimRange = 0.1f;

    [Header("Start Animation Settings")]
    [SerializeField] [Range(0.1f, 20f)] private float startAnimSpeed = 1f;
    [SerializeField] [Range(1f, 10f)] private float startAnimMultiplier = 2f;


    //Collections
    private List<string> continuesAnimIDs = new List<string>();
    private List<string> startAnimIDs = new List<string>();

    //References
    private IconManager manager;
    private MapVisibility mapVisibility;


    private void Awake()
    {
        manager = transform.GetComponent<IconManager>();
        mapVisibility = transform.GetComponent<MapVisibility>();
    }

    private void Update()
    {
        //Makes it so the icons are only animated while the map is open
        if (!mapVisibility.mapEnabled)
            return;

        if (startAnimIDs.Count > 0)
        {
            StartAnimations();
        }

        if (continuesAnimIDs.Count > 0)
        {
            ContinuesAnimations();
        }
    }

    private void StartAnimations()
    {
        //Amount icons should shink this frame
        float shrinkAmount = Time.deltaTime * startAnimSpeed;

        //Go through all icons that should have the start animation
        for (int i = 0; i < startAnimIDs.Count; i++)
        {
            string currentID = startAnimIDs[i];

            //If the icon is still inside the active list
            if (manager.activeIcons.ContainsKey(currentID))
            {
                Image icon = manager.activeIcons[currentID];

                //Shrink it
                float currentSize = Mathf.Clamp(icon.rectTransform.localScale.x - shrinkAmount, 1, 100);
                icon.rectTransform.localScale = new Vector3(currentSize, currentSize, currentSize);

                //Remove it, if it's small enough
                if (icon.rectTransform.localScale.x == 1)
                {
                    startAnimIDs.Remove(currentID);
                }
            }
            else
            {
                //If it's not in the active list, remove it from the animation list
                startAnimIDs.Remove(currentID);
            }
        }
    }

    private void ContinuesAnimations()
    {
        //Get what size the icon should have
        float newSize = Mathf.SmoothStep(1 - contiunesAnimRange, 1 + contiunesAnimRange, Mathf.PingPong(Time.time * continuesAnimSpeed, 1f));

        //Go through all icons that should have the continues animation
        for (int i = 0; i < continuesAnimIDs.Count; i++)
        {
            string currentID = continuesAnimIDs[i];

            //If the icon is still inside the active list
            if (manager.activeIcons.ContainsKey(currentID))
            {
                //Resize it
                manager.activeIcons[currentID].rectTransform.localScale = new Vector3(newSize, newSize, newSize);
            }
            else
            {
                //If it's not in the active list, remove it from the animation list
                continuesAnimIDs.Remove(currentID);
            }
        }
    }

    public void AddStartAnimKey(string id)
    {
        startAnimIDs.Add(id);
        manager.activeIcons[id].rectTransform.localScale *= startAnimMultiplier;
    }

    public void AddContinuesAnimKey(string id)
    {
        continuesAnimIDs.Add(id);
    }
}
Tank Movement Feedback
Tank Terrain Hugging

Real life themes and scenarios heavily inspired this game, even the tanks and weapons are as historically accurate as possible. When making the tank, we wanted it to behave like an actual tank. A part of making it feel real was to make it “hug” the ground.

Usually in games, to make something hug the ground, you would cast a ray from the center of the object straight down. Then you would rotate the object accordingly with the direction (normal) of what you hit. This works for small objects, but as the tank is so large, the corners of it would clip through the ground in many situations.

To solve this, I cast four rays, one from each corner of the tank. I then get the Points where they hit and use them to create a hypothetical Plane. Then I rotate the tank accordingly with the Normal of the plane. This makes the tank smoothly hug the terrain without any of its corners clipping through the ground. 

< CODE: Tank Terrain Hugging >

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class TankTerrainHugging : MonoBehaviour
{
    [SerializeField] private LayerMask layers;
    [SerializeField] [Range(0, 5)] private float rayLength = 1;
    [SerializeField] private Transform[] rayPoints;

    //Vectors
    private Vector3 newUp;
    private Vector3 newForward;

    private Plane plane;

    private void FixedUpdate()
    {
        GetNewNormals();
        
        transform.LookAt(transform.parent.position + newForward, newUp);
    }

    private void GetNewNormals()
    {
        //Raycast at all points
        RaycastHit hitA = Raycast(rayPoints[0]);
        RaycastHit hitB = Raycast(rayPoints[1]);
        RaycastHit hitC = Raycast(rayPoints[2]);
        RaycastHit hitD = Raycast(rayPoints[3]);

        Vector3 tmpNormal = Vector3.zero;
        Vector3 tmpForward = Vector3.zero;

        //If all three raycasts hit something
        if (hitA.collider != null && hitB.collider != null && hitC.collider != null)  
        {
            //Makes a plane out of the three points
            plane.Set3Points(hitA.point, hitB.point, hitC.point);
            tmpNormal += plane.normal;

            //Get a forward normal
            // A = front right, B = back right
            tmpForward += Vector3.Normalize(hitA.point - hitB.point);
        }

        if (hitB.collider != null && hitC.collider != null && hitD.collider != null)
        {
            plane.Set3Points(hitB.point, hitC.point, hitD.point);
            tmpNormal += plane.normal;

            // D = forward left, C = back left
            tmpForward += Vector3.Normalize(hitD.point - hitC.point);
        }

        if (hitC.collider != null && hitD.collider != null && hitA.collider != null)
        {
            plane.Set3Points(hitC.point, hitD.point, hitA.point);
            tmpNormal += plane.normal;

            // D = forward left, C = back left
            tmpForward += Vector3.Normalize(hitD.point - hitC.point);
        }

        if (hitD.collider != null && hitA.collider != null && hitB.collider != null)
        {
            plane.Set3Points(hitD.point, hitA.point, hitB.point);
            tmpNormal += plane.normal;

            // A = front right, B = back right
            tmpForward += Vector3.Normalize(hitA.point - hitB.point);
        }

        //Get the average Up and Forward Normal
        newUp = tmpNormal.normalized;
        newForward = tmpForward.normalized;
    }

    private RaycastHit Raycast(Transform tran)
    {
        RaycastHit hit;
        Physics.Raycast(tran.position, Vector3.down, out hit, rayLength, layers);

        return(hit);
    }
}

< CODE: Wheel Movement Update >< CODE: Wheel Movement >

Wheel Movement

Another part of making the tank real was to make the wheels and tracks also hug the ground. To do this I used the more usual ground hugging technique. 

Each wheel casts a ray straight down and moves the bottom edge of the wheel as close to the hit point as possible. The tank tracks move automatically as they are rigged and each joint corresponds with a wheel, so when the wheel moves, so does that part of the track.

Having 16 wheels cast a ray once each frame could take up a bit of performance. So, to make it less performance heavy, I made a script that acts as an update for each wheel. The speed at which this script updates the wheels could be altered, allowing us to adjust the speed and save a bit of performance if necessary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class TankTerrainHugging : MonoBehaviour
{
    [SerializeField] private LayerMask layers;
    [SerializeField] [Range(0, 5)] private float rayLength = 1;
    [SerializeField] private Transform[] rayPoints;

    //Vectors
    private Vector3 newUp;
    private Vector3 newForward;

    private Plane plane;

    private void FixedUpdate()
    {
        GetNewNormals();
        
        transform.LookAt(transform.parent.position + newForward, newUp);
    }

    private void GetNewNormals()
    {
        //Raycast at all points
        RaycastHit hitA = Raycast(rayPoints[0]);
        RaycastHit hitB = Raycast(rayPoints[1]);
        RaycastHit hitC = Raycast(rayPoints[2]);
        RaycastHit hitD = Raycast(rayPoints[3]);

        Vector3 tmpNormal = Vector3.zero;
        Vector3 tmpForward = Vector3.zero;

        //If all three raycasts hit something
        if (hitA.collider != null && hitB.collider != null && hitC.collider != null)  
        {
            //Makes a plane out of the three points
            plane.Set3Points(hitA.point, hitB.point, hitC.point);
            tmpNormal += plane.normal;

            //Get a forward normal
            // A = front right, B = back right
            tmpForward += Vector3.Normalize(hitA.point - hitB.point);
        }

        if (hitB.collider != null && hitC.collider != null && hitD.collider != null)
        {
            plane.Set3Points(hitB.point, hitC.point, hitD.point);
            tmpNormal += plane.normal;

            // D = forward left, C = back left
            tmpForward += Vector3.Normalize(hitD.point - hitC.point);
        }

        if (hitC.collider != null && hitD.collider != null && hitA.collider != null)
        {
            plane.Set3Points(hitC.point, hitD.point, hitA.point);
            tmpNormal += plane.normal;

            // D = forward left, C = back left
            tmpForward += Vector3.Normalize(hitD.point - hitC.point);
        }

        if (hitD.collider != null && hitA.collider != null && hitB.collider != null)
        {
            plane.Set3Points(hitD.point, hitA.point, hitB.point);
            tmpNormal += plane.normal;

            // A = front right, B = back right
            tmpForward += Vector3.Normalize(hitA.point - hitB.point);
        }

        //Get the average Up and Forward Normal
        newUp = tmpNormal.normalized;
        newForward = tmpForward.normalized;
    }

    private RaycastHit Raycast(Transform tran)
    {
        RaycastHit hit;
        Physics.Raycast(tran.position, Vector3.down, out hit, rayLength, layers);

        return(hit);
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class WheelMovementUpdate : MonoBehaviour {

    [SerializeField] [Range(0.01f, 0.1f)] private float delay = 0.03f;

    public event Action UpdateWheels;

    private void Start()
    {
        StartCoroutine(CustomTime());
    }

    private IEnumerator CustomTime()
    {
        while (true)
        {
            UpdateWheels();
            yield return new WaitForSeconds(delay);
        }
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WheelMovement : MonoBehaviour {

    [SerializeField] private LayerMask hitMask;

    [Header("Raycast Origin Offset")]
    [SerializeField] private float xOffset;
    [SerializeField] private float yOffset;
    [Space]
    [SerializeField] [Range(0.01f, 0.5f)] private float wheelRadius = 0.17f;
    [SerializeField] [Range(0.1f, 5f)] private float raycastLength = 1f;

    //References
    private GameObject wheel;

    private void Awake()
    {
        wheel = transform.GetChild(0).gameObject;

        WheelMovementUpdate wheelUp = GetComponentInParent<WheelMovementUpdate>();
        wheelUp.UpdateWheels += UpdateWheel;
    }

    private void UpdateWheel()
    {
        RaycastHit hit;

        //Get Raycast origin
        Vector3 origin = transform.position;
        origin.x += xOffset;
        origin.y += yOffset;

        //If Raycast hit anything, move wheel bottom to hit point
        if (Physics.Raycast(origin, -transform.up, out hit, raycastLength, hitMask))
        {
            Vector3 wheelPos = wheel.transform.position;
            wheelPos.y = hit.point.y + wheelRadius;
            wheel.transform.position = wheelPos;
        }
        //If Raycast hit nothing, move wheel bottom to end of raycast
        else
        {
            Vector3 wheelPos = wheel.transform.position;
            wheelPos.y = yOffset + transform.position.y - raycastLength + wheelRadius;
            wheel.transform.position = wheelPos;
        }
    }
}
Health System
Health System

We wanted the health system to be very flexible so we could use it for many different scenarios. To do this I made many different options available in the health script.

Health Types

Every Health script requires a Health Type [Humanoid, Vehicle, LesserStructure, LargeStructure] and, in order to deal damage, a Damage Type  [Melee, Bullet, Fire, Crushing, Explosion]. This allowed us to easily make various interactions between different Health Types and Damage Types.

For example, if a building had the Health Type “Large Structure”, receiving damage from a gun with the Damage Type “Bullet” would do nothing. But receiving damage from the tank with the Damage Type “Explosion” would destroy it.

Life Segments

Splits up the health into different segments. It was used for both the different types of regeneration and for the tank’s lost functionality. The more damaged the tank, the less functionable it is.

Healing Type

Decides what type of regeneration the character should have. “Standard” means no regeneration, so the character could only be healed through external sources. “Regeneration” regenerates the characters health regularly and “Segmented Regeneration” only regenerates the current segment.

I also made an Editor Script that shows inside the inspector what the current health is, how many segments it has and what segment the life currently is in. This allowed for easier debugging and a clearer view of what the current health is.

< CODE: Health >< CODE: Health Editor >

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class Health : MonoBehaviour {

    [Header("Health Settings")]
    [SerializeField] private int maxHealth = 100;
    [SerializeField] [Range(1, 15)] private int lifeSegments = 1;
    [SerializeField] private HealthTypes healthType;

    [Header("Damage Settings")]
    [SerializeField] private bool speakAboutDamage = false;

    [Header("Healing Settings")]
    [SerializeField] private HealingTypes healingType;
    [HideInInspector] public int regenerationAmount;
    [HideInInspector] public float regenerationDelay;
    

    //Bools
    [HideInInspector] public bool dead = false;

    //Values
    [HideInInspector] public float currentHealth;
    private float currentRegenerationDelay = 0;
    private int currentLifeSegment;
    private float regenerationIncrease = 0;


    private void Awake()
    {
        currentHealth = maxHealth;
    }

    private void Update()
    {
        if (dead)
            return;

        GetCurrentSegment();
        Regenerate();

        //Decreases current Regeneration delay
        currentRegenerationDelay = Mathf.Clamp(currentRegenerationDelay - 1 * Time.fixedDeltaTime, 0, regenerationDelay);
    }


    //Example: If max life is 100 with 4 segments, then the value of the segments are 0-24, 25-49, 59-74, 75-100
    private void GetCurrentSegment()
    {
        if(lifeSegments == 1)
        {
            currentLifeSegment = 1;
            return;
        }

        for(int i = 1; i <= lifeSegments; i++)
        {
            if ((maxHealth / lifeSegments) * i == currentHealth)
            {
                if(currentHealth == maxHealth)
                {
                    currentLifeSegment = lifeSegments;
                    break;
                }
                else
                {
                    currentLifeSegment = i + 1;
                    break;
                }
            }
            else if((maxHealth / lifeSegments) * i > currentHealth)
            {
                currentLifeSegment = i;
                break;
            }
        }
    }

    private void Regenerate()
    {
        if (currentRegenerationDelay != 0 || healingType == HealingTypes.Standard)
            return;

        //Detects if we are at the end of a segment
        if (healingType == HealingTypes.SegmentedRegeneration)
            if (((maxHealth / lifeSegments) * currentLifeSegment) - 1 == currentHealth)
                return;
        //Increase the "timer" before we generate more health
        regenerationIncrease = regenerationIncrease + regenerationAmount * Time.fixedDeltaTime;

        if(regenerationIncrease >= 1)
        {
            heal(1);
            regenerationIncrease = -1;
        }
    }

    public void heal(int amount)
    {
        if (dead)
            return;

        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
    }

    public void takeDamage(int amount, DamageTypes damageType)
    {
        if (!CheckDamageType(damageType) || dead)
            return;

        currentHealth = Mathf.Clamp(currentHealth - amount, 0, maxHealth);

        if (currentHealth <= 0)
        {
            Death();
        }

        //If speak about damage is true, Brodcast a messeage to all other components on the same Actor that we have taken damage
        if (speakAboutDamage)
            BroadcastMessage("TookDamage", amount, SendMessageOptions.DontRequireReceiver);

        //Reset regeneration delay
        currentRegenerationDelay = regenerationDelay;
    }

    private bool CheckDamageType(DamageTypes damageType)
    {
        //Go throguh all HealthTypes / DamageTypes interactions
        switch (damageType)
        {
            case DamageTypes.Melee:
                if (healthType == HealthTypes.Humanoid)
                    return (true);
                break;

            case DamageTypes.Bullet:
                if (healthType == HealthTypes.Humanoid)
                    return (true);
                break;

            case DamageTypes.Fire:
                if (healthType == HealthTypes.Humanoid || healthType == HealthTypes.Vehicle)
                    return (true);
                break;

            case DamageTypes.Crushing:
                if (healthType == HealthTypes.Humanoid || healthType == HealthTypes.LesserStructure)
                    return (true);
                break;

            case DamageTypes.Explosion:
                return (true);
        }
        return (false);
    }

    private void Death()
    {
        dead = true;

        //Brodcast a messeage to all other components on the same Actor that we have died
        SendMessage("Dying", SendMessageOptions.DontRequireReceiver);
    }
}

//Enums
public enum HealthTypes
{
    Humanoid,
    Vehicle,
    LesserStructure,
    LargeStructure
}

public enum HealingTypes
{
    Standard,
    Regeneration,
    SegmentedRegeneration
}

public enum DamageTypes
{
    Melee,
    Bullet,
    Fire,
    Crushing,
    Explosion
}

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(Health))]
[CanEditMultipleObjects]
public class HealthEditor : Editor
{
    //Text
    private static readonly string[] _dontIncludeMe = new string[] { "m_Script" };

    //Colors
    private Color32 yellow = new Color32(244, 203, 17, 255);
    private Color32 red = new Color32(223, 0, 0, 255);
    private Color32 gray = new Color32(110, 110, 110, 255);

    SerializedProperty regenerationAmount;
    SerializedProperty regenerationDelay;

    private void OnEnable()
    {
        regenerationAmount = serializedObject.FindProperty("regenerationAmount");
        regenerationDelay = serializedObject.FindProperty("regenerationDelay");
    }

    public override void OnInspectorGUI()
    {
        GUILayout.Space(18);

        Rect rect = EditorGUILayout.BeginVertical();
        //Draw gray background
        EditorGUI.DrawRect(rect, Color.gray);

        //Get health and segment values
        float currentH = serializedObject.FindProperty("currentHealth").floatValue;
        float maxH = serializedObject.FindProperty("maxHealth").intValue;
        float lifePercentage = currentH / maxH;


        if (serializedObject.FindProperty("lifeSegments").intValue > 1)
        {
            //If there mare more than one segments, draw the segmented life bar
            DrawSegmentedLifebar(rect, currentH, maxH, lifePercentage);
        }
        else
        {
            DrawRegularLifebar(rect, currentH, maxH, lifePercentage);
        }

        if (Application.isPlaying)
        {
            //If game is running, write the current life below the health bar
            EditorGUI.LabelField(new Rect(rect.width / 2, rect.position.y + rect.height, rect.width, rect.height), currentH.ToString(), EditorStyles.boldLabel);
        }
        else
        {
            //If game is not running, write the max life below the health bar
            EditorGUI.LabelField(new Rect(rect.width / 2, rect.position.y + rect.height, rect.width, rect.height), maxH.ToString(), EditorStyles.boldLabel);
        }

        GUILayout.Space(18);
        EditorGUILayout.EndVertical();

        GUILayout.Space(9);

        serializedObject.Update();
        DrawPropertiesExcluding(serializedObject, _dontIncludeMe);

        GUILayout.Space(9);

        //Draw out options below "Healing Type"
        if (serializedObject.FindProperty("healingType").enumValueIndex != 0)
        {
            EditorGUILayout.PropertyField(regenerationAmount);
            EditorGUILayout.Slider(regenerationDelay, 0, 15);
        }
        else
        {
            // If the current Healing Type is Standard, draw them as grayed out
            GUI.enabled = false;
            EditorGUILayout.PropertyField(regenerationAmount);
            EditorGUILayout.Slider(regenerationDelay, 0, 15);
            GUI.enabled = true;
        }

        serializedObject.ApplyModifiedProperties();

        GUILayout.Space(25);
    }

    private void DrawRegularLifebar(Rect rect, float currentH, float maxH, float lifePercentage)
    {
        if (serializedObject.FindProperty("dead").boolValue)
            return;

        if (Application.isPlaying)
        {
            //Draw current health, change the color depending on current life to max life ratio
            EditorGUI.DrawRect(new Rect(rect.position, new Vector2(rect.width * lifePercentage, rect.height)), Color32.Lerp(red, yellow, lifePercentage));
        }
        else
        {
            //Draw max health
            EditorGUI.DrawRect(new Rect(rect.position, new Vector2(rect.width, rect.height)), yellow);
        }
    }

    private void DrawSegmentedLifebar(Rect rect, float currentH, float maxH, float lifePercentage)
    {
        int segments = serializedObject.FindProperty("lifeSegments").intValue;
        float leftMargin = 14 /*The margin between the left inspector edge and the lifebar*/;

        if (!serializedObject.FindProperty("dead").boolValue)
        {
            if (Application.isPlaying)
            {
                for (int i = 0; i < segments; i++)
                {
                    //If the current segment "contains" less life than the current life
                    if (currentH >= (i + 1) * (maxH / segments))
                    {
                        //Draw a full life segment
                        DrawRegulareSegment(rect, leftMargin, segments, i);
                    }
                    else
                    {
                        //Get how much life the current segment shall show and draw it

                        float posX = ((rect.width / segments) * i) + leftMargin;

                        float width = rect.width / segments;

                        width *= Mathf.Abs(((maxH / segments) * i - currentH) / (maxH / segments));

                        EditorGUI.DrawRect(new Rect(posX, rect.position.y, width, rect.height), Color32.Lerp(red, yellow, i / ((float)segments - 1)));

                        break;
                    }
                }
            }
            else
            {
                //If game is not playing, draw entire lifebar
                for (int i = 0; i < segments; i++)
                {
                    DrawRegulareSegment(rect, leftMargin, segments, i);
                }
            }
        }

        //Draw out the white lines between segments
        for (int i = 0; i < segments; i++)
        {
            if (i != 0)
            {
                float posX = ((rect.width / segments) * i) + leftMargin;

                float width = (rect.width / maxH) * 2;

                EditorGUI.DrawRect(new Rect(posX - (width / 2), rect.position.y, width, rect.height), Color.white);
            }
        }

    }

    private void DrawRegulareSegment(Rect rect, float leftMargin, int segments, int segment)
    {
        float posX = ((rect.width / segments) * segment) + leftMargin;

        float width = rect.width / segments;

        EditorGUI.DrawRect(new Rect(posX, rect.position.y, width, rect.height), Color32.Lerp(red, yellow, segment / ((float)segments - 1)));
    }
}
Additional
Explosion

I made the explosion script, in a way that is easy to see how much damage is dealt throughout the explosion radius. 

When the actual explosion happens, the script gets all Health scripts within the explosion radius. It then interpolates the damage accordingly to where you are within the explosion radius.

< CODE: Explosion >

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(DamageScript))]
public class Explosion : MonoBehaviour
{
    [Header("Explosion Settings")]
    [SerializeField] private float explosionRadius = 5f;
    [SerializeField] [Range(0f, 100f)] private float fullDamagePercentage = 50f;
    [SerializeField] [Range(0f, 100f)] private float halfDamagePercentage = 75f;

    [Header("References")]
    [SerializeField] private GameObject explosionEffect;

    //References
    private DamageScript dmg;


    private void Awake()
    {
        dmg = GetComponent<DamageScript>();
    }

    public void Explode()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position, explosionRadius);

        foreach (Collider col in hitColliders)
        {
            if(col.gameObject.GetComponent<Health>() && col.tag != "Ignore")
            {
                //Get the distance between the center of explosion and the closest point of the collider
                float dist = Vector3.Distance(col.ClosestPoint(transform.position), transform.position);

                if(dist <= explosionRadius * (fullDamagePercentage / 100))
                {
                    //If the distance is less or equal to the full damage radius, deal full damage
                    col.gameObject.GetComponent<Health>().takeDamage(dmg.damageAmount, dmg.damageType);
                }
                else if (dist <= explosionRadius * (halfDamagePercentage / 100) && halfDamagePercentage > fullDamagePercentage)
                {
                    //If the distance is more than the full damage radius and less than half damage radius
                    float distRelativToZero = dist - explosionRadius * (fullDamagePercentage / 100);
                    float explosionDistRelativFromZero = explosionRadius * ((halfDamagePercentage - fullDamagePercentage) / 100);

                    //Lerp the damage between full and half radius
                    float dmgMultiplier = Mathf.Lerp(1f, 0.5f, distRelativToZero / explosionDistRelativFromZero);

                    int damage = (int)(dmg.damageAmount * dmgMultiplier);

                    col.gameObject.GetComponent<Health>().takeDamage(damage, dmg.damageType);
                }
                else
                {
                    //If the distance is more than half damage radius
                    float distRelativToZero = dist - explosionRadius * (halfDamagePercentage / 100);
                    float explosionDistRelativFromZero = explosionRadius - explosionRadius * (halfDamagePercentage / 100);

                    //Lerp the damage between half and explosion radius
                    float dmgMultiplier = Mathf.Lerp(0.5f, 0f, distRelativToZero / explosionDistRelativFromZero);

                    int damage = (int)(dmg.damageAmount * dmgMultiplier);

                    col.gameObject.GetComponent<Health>().takeDamage(damage, dmg.damageType);
                }
            }
        }

        Instantiate(explosionEffect, transform.position, transform.rotation);
    }


    private void OnDrawGizmos()
    {
        //Draw explosion raius sphere
        DrawExplosionSphere(explosionRadius, 100, Color.green);

        //If half damage radius is more than full damage radius, draw half damage sphere
        if(halfDamagePercentage > fullDamagePercentage)
            DrawExplosionSphere(explosionRadius, halfDamagePercentage, Color.yellow);

        //Draw full damage sphere
        DrawExplosionSphere(explosionRadius, fullDamagePercentage, Color.red);
    }

    private void DrawExplosionSphere(float radius, float percentage, Color color)
    {
        Gizmos.color = color;

        Gizmos.DrawWireSphere(transform.position, radius * (percentage / 100));

        //Draw the two lines in the middle of the sphere
        Vector3 from = new Vector3 (transform.position.x - radius * (percentage / 100), transform.position.y, transform.position.z);
        Vector3 to = new Vector3(transform.position.x + radius * (percentage / 100), transform.position.y, transform.position.z);

        Gizmos.DrawLine(from, to);

        from = new Vector3(transform.position.x, transform.position.y, transform.position.z - radius * (percentage / 100));
        to = new Vector3(transform.position.x , transform.position.y, transform.position.z + radius * (percentage / 100));

        Gizmos.DrawLine(from, to);
    }
}
Dialog System

The game has voice acting in Russian, so we needed a subtitle system. To keep it organized and easy to change, I made a XML document that keeps all that information. And as most dialog in the game is one liners at random points (barks), I structured it so that it is easy to randomize within a chosen category of dialog options.

< CODE: Dialog XML File >< CODE: Dialog XML Reader >

<track>Oksana/O_Intro01</track>
Oksana – “They said we should expect some resistance in the area outside of town. There are probably fortified positions.”

<track>Matvei/M_Intro01</track>
Matvei – “Yeah, I can see a few of them, I will clear this area, be ready to move up!”

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
<?xml version="1.0" encoding="utf-8"?>

<data>
  
  <!--Rundown of the XML document laout-->
  <header>
    <!--Who shall say the line-->
    <character name="Example">
      <!--Group of the line, making it easier to randomize between lines within the same group-->
      <entries id="example">
        <!--Specifik line/track within the group-->
        <entry num="0">
          <!--The subtitles-->
          <line> <![CDATA[Audio subtitles]]> </line>
          <!--The file path to the sound track-->
          <track>Audio track path</track>
        </entry>
      </entries>
    </character>
  </header>
  
  
  
  <character name="Matvei">
    
    
    <entries id="moveOrder">
      <entry num="1">
        <line> <![CDATA[Move over here!]]> </line>
        <track>Matvei/M_MvOrd01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[Move here!]]> </line>
        <track>Matvei/M_MvOrd02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[Move to these coordinates!]]> </line>
        <track>Matvei/M_MvOrd03</track>
      </entry>
    </entries>

    <entries id="fireOrder">
      <entry num="1">
        <line> <![CDATA[Fire on this target!]]> </line>
        <track>Matvei/M_FirOrd01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[Fire on  these coordinates!]]> </line>
        <track>Matvei/M_FirOrd02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[Fire here!]]> </line>
        <track>Matvei/M_FirOrd03</track>
      </entry>
    </entries>

    <entries id="takeDamage">
      <entry num="1">
        <line> <![CDATA[*pain grunt*]]> </line>
        <track>Matvei/M_TakeDmg01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[*pain grunt*(Variation)]]> </line>
        <track>Matvei/M_TakeDmg02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[*pain grunt*(Another Variation)]]> </line>
        <track>Matvei/M_TakeDmg03</track>
      </entry>
    </entries>

    <entries id="intro">
      <entry num="1">
        <line> <![CDATA[Yeah, I can see a few of them, I will clear this area, be ready to move up!]]> </line>
        <track>Matvei/M_Intro01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[Ok Oksana, it's clear, you can move up. I need your help to remove this obstacle]]> </line>
        <track>Matvei/M_Intro02</track>
      </entry>
    </entries>

    <entries id="tankDeath">
      <entry num="1">
        <line> <![CDATA[Oksana? You ok? Oksana!?]]> </line>
        <track>Matvei/M_TankDeath</track>
      </entry>
    </entries>

    <entries id="sequence">
      <entry num="1">
        <line> <![CDATA[Likely. I'll keep an eye out for minefields also]]> </line>
        <track>Matvei/M_Seq01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[You put too little faith in me, sister. I have them right here]]> </line>
        <track>Matvei/M_Seq02</track>
      </entry>
    </entries>

    <entries id="tankSequence">
      <entry num="1">
        <line> <![CDATA[So, is there a name for the tank yet?]]> </line>
        <track>Matvei/M_TankSeq01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[Really? What makes it so striking?]]> </line>
        <track>Matvei/M_TankSeq02</track>
      </entry>
    </entries>

    
  </character>
  
  
  
  <character name="Oksana">

    
    <entries id="acknowledgement">
      <entry num="1">
        <line> <![CDATA[Affirmative!]]> </line>
        <track>O_Ack01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[If you say so!]]> </line>
        <track>Oksana/O_Ack02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[Right away!]]> </line>
        <track>Oksana/O_Ack03</track>
      </entry>
      <entry num="4">
        <line> <![CDATA[I'm on it!]]> </line>
        <track>Oksana/O_Ack04</track>
      </entry>
    </entries>

    <entries id="deny">
      <entry num="1">
        <line> <![CDATA[I can't do that]]> </line>
        <track>Oksana/O_Deny01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[Are you kidding me, Matvei? I can't do that]]> </line>
        <track>Oksana/O_Deny02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[Not possible]]> </line>
        <track>Oksana/O_Deny03</track>
      </entry>
      <entry num="4">
        <line> <![CDATA[Sorry, Matvei, I can't]]> </line>
        <track>Oksana/O_Deny04</track>
      </entry>
    </entries>

    <entries id="takeDamage">
      <entry num="1">
        <line> <![CDATA[I'm pinned down!]]> </line>
        <track>Oksana/O_TookDmg01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[*swears*]]> </line>
        <track>Oksana/O_TookDmg02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[Weren't you supposed to remove these?]]> </line>
        <track>Oksana/O_TookDmg03</track>
      </entry>
    </entries>

    <entries id="repair">
      <entry num="1">
        <line> <![CDATA[And where are you going? I'm not moving anywhere in this state]]> </line>
        <track>Oksana/O_Rpr01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[If you want my help, repair the tank]]> </line>
        <track>Oksana/O_Rpr02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[I can barely operate this thing. Come back and do repairs!]]> </line>
        <track>Oksana/O_Rpr03</track>
      </entry>
    </entries>

    <entries id="farAway">
      <entry num="1">
        <line> <![CDATA[Are you just gonna leave me in the woods?]]> </line>
        <track>Oksana/O_FarAwy01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[I feel like an open target, where are you?]]> </line>
        <track>Oksana/O_FarAwy02</track>
      </entry>
      <entry num="3">
        <line> <![CDATA[If the bolsheviks surprise me, you're setting camp tonight!]]> </line>
        <track>Oksana/O_FarAwy03</track>
      </entry>
    </entries>

    <entries id="intro">
      <entry num="1">
        <line> <![CDATA[Matvei? Matvei? Do you read me? Can you see anything up ahead?]]> </line>
        <track>Oksana/O_Intro01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[...]]> </line>
        <track>Oksana/O_Intro03</track>
      </entry>
    </entries>

    <entries id="soldierDeath">
      <entry num="1">
        <line> <![CDATA[...]]> </line>
        <track>Oksana/O_SoldierDeath</track>
      </entry>
    </entries>

    <entries id="sequence">
      <entry num="1">
        <line> <![CDATA[They said we should expect some resistance in the area outside of town. There are probably fortified positions]]> </line>
        <track>Oksana/O_Seq01</track>
      </entry>
      <entry num="2">
        <line> <![CDATA[I hope you didn't forget your tools again]]> </line>
        <track>Oksana/O_Seq02</track>
      </entry>
    </entries>

    <entries id="tankSequence">
      <entry num="1">
        <line> <![CDATA[Yes. I'm calling it Striker]]> </line>
        <track>Oksana/O_TankSeq01</track>
      </entry>
    </entries>

    
  </character>
  
  
  
</data>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml;

public class DialogReader
{
    public void GetDialog(string name, string id, int num, ref AudioClip track, ref string subtitles)
    {
        //Get XML file
        TextAsset textAsset = (TextAsset)Resources.Load("Dialogs");
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.LoadXml(textAsset.text);

        //Get all nodes with the name character
        XmlNodeList characters = xmlDoc.GetElementsByTagName("character");

        foreach (XmlNode character in characters)
        {
            //Look for character node with right name
            if (character.Attributes["name"].Value == name)
            {
                foreach (XmlNode entries in character.ChildNodes)
                {
                    //Look for entries node with right id
                    if (entries.Attributes["id"].Value == id)
                    {
                        foreach (XmlNode entry in entries.ChildNodes)
                        {
                            //Look for entry node with right num
                            if (entry.Attributes["num"].Value == num.ToString())
                            {
                                //Get first child, which would be <line>
                                subtitles = entry.ChildNodes[0].InnerText;

                                //Get second child, which would be <track>
                                string path = "VO/" + entry.ChildNodes[1].InnerText;
                                track = Resources.Load(path) as AudioClip;

                                return;
                            }
                        }
                    }
                }
            }
        }
    }
}
Post-Mortem

This project helped me increase my experience in making of systems, as I had to create multiple big and interconnected ones. Each system required a lot of designing, scripting, and testing to make them work in a good way. I also learned more about making scripts modular, flexible, and accessible to others.