[X,Y,Z]

Scripter | Game designer | Artist

Genre: Tower defence

Engine: Unity

Language: C#

Development time: Hobby project during 4 months

Team:  One man project

Scripter, Designer, Artist

  • Formulate game vision
  • Create and maintain design document
  • Script towers, enemies and player interactions
  • Create art style
  • Create stylized assets

[X,Y,Z] is an abstract matrix-based tower defence game. It gives a twist to the tower defence formula by adding a third dimension. This creates a game that feels both familiar and fresh, that will surprise and delight both old and new players of the genre.

Tower defence games are usually played on 2D surfaces. This game expands upon that by adding a third axis. The players must use their spatial ability to build towers and defend their base in three dimensions. The player needs to develop a different mind- and toolset from what they normally use when playing tower defence games. This may prove a fun and engaging challenge, even for veterans of tower defence games!

This game was a one man project where I did all of the design, graphics, scripting and programming. I created this game to be a part of my portfolio for applying at Futuregames.

Game Developer

This game was the first unique game of my own I created in Unity after going through tutorials and trying things out by myself. I learned a lot about game programming and game design. I learned the about raycasts, collisions trigger boxes and much much more.

l

Documentation

As a part of my Futuregames application I created my first game design document and learned a lot about communicating design through text, formulating unique selling points, explaining game mechanics and establishing a game vision.

Camera

Camera controls

The player holds down the left mouse button and drags the mouse to rotate the camera so that it looks like the play area was on top of a turntable.

By only allowing the camera to rotate horizontally around the play area, the game establishes up and down for the player to relate to, which otherwise might be confusing when dealing with an abstract game.

This creates intuitive, easy to learn controlls that helps the player to get an overview of the play area.

Camera physicality

To give the camera more life and make it a bit more fun I decided to make it feel more physical by implementing momentum in the rotation. When the player releases the mouse button it continues to rotate but slows down and then halts completely.

I implemented this by calculating the current rotation speed and direction when the player is rotating the camera and store those values in private variables.

When the player releases the mouse button I calculated the new rotation speed and the angle the camera should rotate from those variables in every update until the speed reaches zero.

This resulted in a satisfying weight and momentum to the camera.

Show code implementation of camera movement in C#
//public configurable variables
public float DragSpeed = 0.45f;
public float Deceleration = 1000;

//private variables
private float _mousePrevPosition;
private float _currSpeed = 0;
private float _dragDir = 0;

//Center is set Start.
private Vector3 _center;

//Called Every Update
private void RotateCamera()
{
    //If mouse buton is down, the player is draging and rotating the view.
    if (Input.GetMouseButton(0))
    {

        //Calculate the rotation.
        var rotateAngle = (Input.mousePosition.x - _mousePrevPosition) * DragSpeed;

        //Caclulate the rotation speed and direction, clockwise or counterclockwise.
        //v = ds/dt
        _currSpeed = Mathf.Abs(rotateAngle / Time.unscaledDeltaTime);
        _dragDir = Mathf.Sign(rotateAngle);

        //Rotate the camera.
        gameObject.transform.RotateAround(_center, Vector3.up, rotateAngle);
    }
    //if we have speed left we should rotate.
    else if (_currSpeed > 0)
    {
        //calculate the rotation speed acording to the formula:
        // v = v0 + a * t
        // s = ((v + v0) /2) * t
        var oldSpeed = _currSpeed;
        _currSpeed = Mathf.Max(_currSpeed - Deceleration * Time.unscaledDeltaTime, 0);
        var rotateAngle = _dragDir *((_currSpeed + oldSpeed)/2) * Time.unscaledDeltaTime;

        //Rotate the camera.
        gameObject.transform.RotateAround(_center, Vector3.up, rotateAngle);
    }

    //Save mouse position to next update
    _mousePrevPosition = Input.mousePosition.x;
}

Pathfinding

A core feature of a tower defence game is for the enemies to go from the start to the finish by the shortest path and thereby forcing the player to build a labyrinth to increase the walking distance. It was therefore important that I implemented an efficient algorithm for the pathfinding.

I decided to use the well known A* algorithm for the pathfinding. The enemies move in Manhattan blocks in the play area matrix and moves to the center of a box before moving on to the next box. I recalculate the pathfinding every time the play area changes. The algorithm also detects if any path from start to finish is possible.  The player is blocked from building a tower on the last remaining path.

This pathfinding implementation proved to be efficient and reliable.

 

Show implementation of the pathfinding in C#
using UnityEngine;
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;

/*
 * Class for finding the shortest path in a 3D matrix.
 * Works by using the A*-algorithm.
 */
public static class Pathfinding {

    private class PathfindingCoordinates : IntVector3 {
        public PathfindingCoordinates parrent;
        public int HCost = int.MaxValue; //Distance to Goal (estimate)
        public int GCost = int.MaxValue; //Distance from Starting node

        public PathfindingCoordinates(int x, int y, int z) : base(x, y, z) {}

        public int FCost
        {
            get {
                if (HCost == int.MaxValue || GCost == int.MaxValue)
                {
                    return int.MaxValue;
                }
                return HCost + GCost;
            }
        }

        // Method for comparision
        public override bool Equals(object obj)
        {
            // Check for null values and compare run-time types.
            if (obj == null || GetType() != obj.GetType())
                return false;

            var p = (PathfindingCoordinates)obj;
            return this.x == p.x && this.y == p.y && this.z == p.z && this.HCost == p.HCost && this.GCost == p.GCost; 
        }

        // Method for comparision
        public override int GetHashCode()
        {
            return x ^ y ^ z ^ HCost ^ GCost;
        }

        public int Distance(PathfindingCoordinates e)
        {
            return Distance(e as IntVector3);
        }


        public int Distance(IntVector3 point)
        {
            return Math.Abs(this.x - point.x) + Math.Abs(this.y - point.y) + Math.Abs(this.z - point.z);
        }

        public IList<PathfindingCoordinates> Neighbours(IntVector3 max, PathfindingCoordinates[,,] matrix)
        {
            var list = new List<PathfindingCoordinates>();
            //1 x+1, y, z
            if (x + 1 < max.x)
            {
                list.Add(matrix[x + 1, y, z]);
            }
            //2 x-1, y, z
            if (x - 1 >= 0)
            {
                list.Add(matrix[x - 1, y, z]);
            }
            //3 x, y+1, z
            if (y + 1 < max.y)
            {
                list.Add(matrix[x, y + 1, z]);
            }
            //4 x, y-1, z
            if (y - 1 >= 0)
            {
                list.Add(matrix[x, y - 1, z]);
            }
            //5 x, y, z+1
            if (z + 1 < max.z)
            {
                list.Add(matrix[x, y, z + 1]);
            }
            //6 x, y, z-1
            if (z - 1 >= 0)
            {
                list.Add(matrix[x, y, z - 1]);
            }

            return list;
        }
    }

    public static GameObject PathFind(GameObject[,,] Gmatrix, LevelLayout level, GameObject pathPrefab, bool randomPath= false)
    {
        //Set up somplete matrix that represents the play area. 
        PathfindingCoordinates[,,] matrix= new PathfindingCoordinates[level.Size.x, level.Size.y, level.Size.z];
        for (var x = 0; x < level.Size.x; x++)
        {
            for (var y = 0; y < level.Size.y; y++)
            {
                for (var z = 0; z < level.Size.z; z++)
                {
                    var node = new PathfindingCoordinates(x, y, z);
                    node.HCost = node.Distance(level.Exit);
                    matrix[x, y, z] = node;
                }
            }
        }

        var startPos = matrix[level.Start.x, level.Start.y, level.Start.z];
        var endPos = matrix[level.Exit.x, level.Exit.y, level.Exit.z];

        startPos.HCost = 0;
        startPos.GCost = startPos.Distance(endPos);

        List<PathfindingCoordinates> open = new List<PathfindingCoordinates>();
        List<PathfindingCoordinates> closed = new List<PathfindingCoordinates>();

        open.Add(startPos);

        while(open.Count > 0)
        {
            if (randomPath)
            {
                Utils.ListRandomizer(open);
            }

            var current = open.OrderBy(x => x.FCost).FirstOrDefault();

            open.Remove(current);
            closed.Add(current);

            if (current.Equals(endPos))
            {
                break;
            }

            var neighbours = current.Neighbours(level.Size, matrix);
            foreach(var n in neighbours)
            {
                if (!Gmatrix[n.x, n.y, n.z].GetComponent<BoxController>().IsWalkable || closed.Contains(n))
                {
                    continue;
                }

                if (n.GCost > current.GCost + 1 || !open.Contains(n))
                {
                    n.GCost = current.GCost + 1;
                    n.parrent = current;
                    if (!open.Contains(n))
                    {
                        open.Add(n);
                    }     
                }
            }
        }

        if(endPos.parrent == null)
        {
            return null;
        }

        var currentPos = endPos;
        GameObject prevBox = null;
        while(currentPos != null && currentPos != startPos)
        {
            var p = Gmatrix[currentPos.x, currentPos.y, currentPos.z].GetComponent<BoxController>().SetPath(pathPrefab);
            if(prevBox != null)
            {
                p.GetComponent<PathController>().Parrent = prevBox;
            }
            prevBox = p;
            currentPos = currentPos.parrent;
        }

        return prevBox;
    }
}

 

Towers

There are several different types of towers in the game. A basic tower shoots at enemies and deals damage but other kinds of towers may have other abilities or effects. One tower may damage several enemies in one strike dealing area of effect damage. Some towers may only attack in one specific direction making the placement and of them a puzzle within itself.

All the different towers share some basic functionality while also having some special abilities and attacks. Therefore I decided to use an abstract base class for the tower controllers. A controller for a specific type of tower would extend and inherit from that class.

This created a good framework to build towers upon and also speeds up the implementation of new towers since the base functionality is shared in the abstract class.

 

Black Tower

  • Fast low damage
  • Medium range
  • Low cost

Red Tower

  • Slow AOE damage
  • Medium range
  • Medium cost

Smasher Tower

  • Fast medium damage
  • Melee range
  • High cost

Exploding Tower

  • Slow high damage
  • Short range
  • High cost
Show implementation of the base class for tower controllers in C#
using UnityEngine;
using System.Collections.Generic;

public abstract class TowerController : MonoBehaviour
{
    public string Name;
    public Color ButtonColor;
    public Color ButtonTextColor;
    public int Cost;

    protected virtual void Awake () { }
    protected virtual void Start() { }
    protected virtual void Update () { }

    public virtual void EffectOnTriggerStay(Collider other) { }
    public virtual void EffectOnTriggerEnter(Collider other) { }
    public virtual void EffectOnTriggerExit(Collider other) { }

    public abstract IList GetMenuItems();

    public virtual void SetGameOver() {
        var fade = gameObject.GetComponent();
        if(fade == null)
        {
            fade = gameObject.GetComponentInChildren();
        }

        if(fade != null)
        {
            fade.ResetFade(true);
        }

        var mbs = transform.GetComponents();
        foreach (var mb in mbs)
        {
            mb.enabled = false;
        }
        mbs = transform.GetComponentsInChildren();
        foreach (var mb in mbs)
        {
            mb.enabled = false;
        }
    }
}
Show implementation of the Black Tower Controller in C#
using UnityEngine;
using System.Collections.Generic;

public class BlackTowerController : TowerController {

    public GameObject MissilePrefab;
    public float ReloadTime = 1f;
    public float ShiverTime = 1f;

    private float _lastFired;
    private float _enemyTime;

    protected override void Awake () {
        base.Awake();
        _lastFired = Time.time;
        gameObject.GetComponent<ScaleUpDown>().Reset(true);
        gameObject.GetComponent<ScaleUpDown>().ScaleUp();
    }

    protected override void Update()
    {
        base.Update();
        if(_enemyTime + ShiverTime < Time.time)
        {
            gameObject.GetComponent<Shiver>().StopShiver();
        }
    }

    public override void EffectOnTriggerStay(Collider other)
    {
        if (other.gameObject.CompareTag("Enemy") && (Time.time - _lastFired) > ReloadTime &&
              GameController.Instance != null && GameController.Instance.GamePlaying)
        {
            var missle = Instantiate(MissilePrefab, transform.position, Quaternion.identity) as GameObject;

            var mc = missle.GetComponent<MissleController>();
            mc.Target = other.gameObject;

            missle.transform.SetParent(PlayAreaGenerator.Instance.MissleContainer.transform); 

            _lastFired = Time.time;
            _enemyTime = Time.time;
            gameObject.GetComponent<Shiver>().StartShiver();
        }
    }

    public override IList<PopUpMenuItemModel> GetMenuItems()
    {
        return new List<PopUpMenuItemModel>();
    }
}
Show implementation of the Red Tower Controller in C#
using UnityEngine;
using System.Collections.Generic;

public class RedTowerController : TowerController {

    public GameObject TorpedoPrefab;
    public float ReloadTime = 5f;

    private float _lastFired;

    private Rotate _rotate;

    protected override void Awake()
    {
        base.Awake();
        _lastFired = Time.time - ReloadTime;
        gameObject.GetComponent<ScaleUpDown>().Reset(true);
        gameObject.GetComponent<ScaleUpDown>().ScaleUp();
        _rotate = gameObject.GetComponent<Rotate>();
        _rotate.Duration = ReloadTime;
    }

    public override void EffectOnTriggerStay(Collider other)
    {
        if (other.gameObject.CompareTag("Enemy") && !_rotate.RotateNow && GameController.Instance != null &&
              GameController.Instance.GamePlaying)
        {
            var missle = Instantiate(TorpedoPrefab, transform.position, Quaternion.identity) as GameObject;
            missle.GetComponent<TorpedoController>().Target = other.gameObject.transform.position;
            missle.transform.SetParent(PlayAreaGenerator.Instance.MissleContainer.transform);
            _lastFired = Time.time;
            _rotate.RotateNow = true;
        }
    }

    public override IList<PopUpMenuItemModel> GetMenuItems()
    {
        return new List<PopUpMenuItemModel>();
    }
}