Simple Decision Tree

Introduction:

Decision Trees are simple tree structures that are used to simplify and modularise logic (there are also decision trees in machine-learning. We aren’t covering those here).

Use Case:

Say we have a shop keeper. This shop keeper needs to respond to a customer but the response will differ depending on some criteria.

The criteria are:

  • If the customer has no money, tell him “Go away, you’re broke!”.
  • If the customer has money but it is LESS than 5000, and the customer is a “sphere”, tell him “Now now, we don’t want any trouble. Just leave us alone.”
  • If the customer has money but it is LESS than 5000, and the customer is NOT a “sphere”, tell him “We have some used car parts around back.”
  • If the customer has money and it is MORE than 5000, and the customer is a “cube”, tell him “Your highness, take a look at this exquisite ruby.”
  • If the customer has money and it is MORE than 5000, and the customer is NOT a “cube”, tell him “We have some caviar in the back, but it’ll cost ya!”

Now try imagine what an if-else block would look like given the conditions mentioned above. It is definitely possible but would quickly become a bit of a mess to manage.

And then what happens when a designer asks you to change some of the conditions to handle a few more cases later on?

Well unit tests would definitely come in handy and are usually a good idea anyway but in the interest of “Keeping It Simple Stupid”, we really should come up with a more manageable and scalable solution…and if we can design our solution in such a way that designers can create the logic structures via dragging, dropping and a bit of configuration, then even better!

Enter the Decision Tree:

Logic Design:

Decision Tree

The first step is to map out the logic flow that your decisions need to follow.

In the tree above, there are two types of nodes:
Decision Nodes: the diamonds.
Action Nodes: the rectangles with rounded edges.

Decision Nodes:

  • Evaluate a condition.
    • If the condition evaluates to true, execution follows down to the connected “Yes” node.
    • If the condition evaluates to false, execution follows down to the connected “No” node.

Action Nodes:

  • These nodes perform an action.
  • They are the leaves and execution stops when it reaches one of them as being a leaf means that they’re the end of the line.

Implementation:

For this example, we’re going to keep it simple and have the Action/Leaf nodes return a string value.

The “Nodes” will extend MonoBehaviours so they can be attached to empty Game Objects in Unity and linked up visually.

We’ll need:

  • Decision Node:

    • To evaluate the condition and choose whether to go to the “Yes” or “No” node.
    • This Node class will also have a “Condition” component. The Condition will do the evaluation and the Decision Node will then continue execution down the “_trueNode” if the Condition evaluates to True, or “_falseNode” if the condition evaluates to false.
    • fork node - Decision Tree Design.png
  • Condition Component:

    • As mentioned above.
    • These can be interchangeable so the Decision Node is generic and the Condition Component may be changed or extended.
  • Action or Result Node:

    • To return the string value.
    • result node - Decision Tree Design (2)
  • Base Node:

    • Using polymorphism we can connect either another Decision Node or Result Node to the “_trueNode” or “_falseNode” fields of the Decision Node.
    • node - Decision Tree Design.png
  • Decision Tree Container:

    • Used to encapsulate the inner workings of the decision tree (even though, in this example there isn’t too much going on).
    • Decision Tree - Design.png

Configuration:

In the Unity Editor, the structure we end up with, given the conditions above looks something like this:

Screen Shot 2016-05-08 at 18.00.24 copy.png

And below are some of the Nodes, linked up and configured.

1 - sdecision tree root.png

2 - decision node.png

The Decision Condition component’s extend a base Decision Condition component and add some conditional logic. In this example, there is one Condition component that checks the “Customer’s” money amount and another that checks the Customer’s “shape” (not displayed in the below images).

3 - decision node.png

4 - false node.png

And finally a “Result” node that will return a value back up the call hierarchy to the Decision Tree.

So to create a decision tree:

  1. Create a GameObject with a DecisionTree component.
    1. Create a child with a DecisionNode and connect it to the “_rootNode” field of the DecisionTree.
      1. Create the a DecisionNode or ResultNode to evaluate if the parent DecisionNode’s Condition evaluates to True. Connect it to it’s parent’s “_trueNode”.
        1. Continue with DecisionNodes and ResultNodes until this branch of your Decision Tree is complete!
      2. Create the a DecisionNode or ResultNode to evaluate if the parent DecisionNode’s Condition evaluates to False. Connect it to it’s parent’s “_falseNode”.
        1. Continue with DecisionNodes and ResultNodes until this branch of your Decision Tree is complete!

Demo Project:

A demo project can be found here.
It contains a simple scene with a “ShopKeeper” and a “Customer”.

Screen Shot 2016-05-08 at 18.13.14.png

Change the values of the Customer, Press Play and click the “respond to customer” button to get a response from the shop owner.

Then change the values on the customer. Try combinations of:

  • No money.
  • Some Money.
  • Maximum Money.
  • Cube Shape.
  • Sphere Shape.
  • Any other Shape.

and see if you get all the correct responses as dictated by the Decision Tree Graph at the top of this post.

Also take a look at how the nodes are connected in Unity and try add your own condition. Perhaps make a new branch that gives a different result if the Customer’s shape is a “Cylinder” (take a look at the other condition classes in the project for reference).

Considerations:

This decision tree implementation is constructed using components extending MonoBehaviours, and taking advantage of the MonoBehaviours’ serialisable nature.

Other techniques involve using:

  • Data driven approach:
    • XML
  • Scripting Languages:
    • Python
    • Lua, etc.
  • Or hard coded:
    • C#
    • Native,  etc.

A data driven and/or scripted approach would require some form of editor to design the tree so for the sake of simplicity these haven’t been addressed in this post, though there’s a good chance they will be in a later post (on either decision trees or behaviour trees).

 

 

Finite State Machines

Purpose:

  • To simply organise and present the logic associated with a certain state of an object.

Description:

  • A state machine has a set number of states.
  • It can only be in any one state at a time.
  • States may transition to another state if certain conditions are met.

Example (enum and switch):

  • There are multiple ways to go about implementing state machines. The first way we will cover uses Enums and Switch statements.
  • Imagine you have a car. This car can:
    • Accelerate – after decellerating or breaking.
    • Decellerate – after Accelerating.
    • Break – after accelerating or decellerating.
  • An if-else block to adhere to these rules may look like this:
if(ButtonPressed("A"))
{
    if(!isBreaking && !isDecellerating)
    {
        // Accelerate
    }
}
else if(ButtonReleased("A"))
{
    if(isAccelerating)
    {
        // Decellerate
    }
}
else if(ButtonPressed("B"))
{
    if(isAccelerating || isDecellerating)
    {
        // Break
    }
}
  • Which is simple enough for now but as we add more features (maybe our car can rocket jump, has a boost option etc), this block will become more and more complicated with more and more flags.
  • This is where the Enum and Switch state machines come in handy:
switch(state)
{
case States.ACCELERATING:
    if(ButtonReleased("A"))
    {
        state = States.DECELLERATING;
    }
    else if(ButtonPressed("B"))
    {
        state = States.BREAKING;
    }
    break;

case States.DECELLERATING:
    if(ButtonPressed("A"))
    {
        state = States.ACCELERATING;
    }
    else if(ButtonPressed("B"))
    {
        state = States.BREAKING;
    }
    break;

case States.BREAKING
    if(ButtonPressed("A"))
    {
        state = States.ACCELERATING;
    }
    break;
}
  • Based on this code, it should now be a lot easier to see which states can be transitioned to from the other states.

This approach of using a switch statement is all fine and dandy for relatively simple cases, like controlling the behaviour of a character…but what happens when you need to handle something bigger, like say the state of the actual game?

Think about a modern strategy game, when you first open the game it logs you in using facebook, google play game services, game center or some other method. Next it takes you to your home world, where your base is. From there you can choose to battle someone, watch a replay of a previous battle or do a multitude of other things. In this case, having a switch statement in an update method that has a case for each state mentioned above could result in a massive amount of code sitting in one class.

This is where the State Pattern comes in.

Basically, each state is put into its own class so a state becomes an object and we can contain all the code specific to that state within its class:

interface IState
{
    void Enter(States fromState);
    void Exit();

    void HandleInput();
    void Update();
}

and then we can implement the actual states:

class LoggingInState : IState
{
    void Enter(States fromState)
    {
        // Ask user to login
    }

    void Exit()
    {
    }

    void HandleInput()
    {
    }

    void Update()
    {
        if(userDidLogin)
        {
            _stateManager.SetState(States.HOME);
        }
    }
}

class HomeState : IState
{
    void Enter(States fromState)
    {
        // Load the player's base
    }

    void Exit()
    {
        // Save the player's base
    }

    void HandleInput()
    {
        if(userClickedBattle)
        {
            _stateManager.SetState(States.BATTLE);
        }
        else if(userClickedBattle)
        {
            _stateManager.SetState(States.REPLAY);
        }
    }

    void Update()
    {
        // Update the player's base
    }
}

class BattleState : IState
{
    void Enter(States fromState)
    {
        // Load battle
    }

    void Exit()
    {
        // Save outcome
    }

    void HandleInput()
    {
        // Follow user's commands
    }

    void Update()
    {
        // Update the battle
        if(battleIsOver)
        {
            _stateManager.SetState(States.HOME);
        }
    }
}

class ReplayState : IState
{
    void Enter(States fromState)
    {
        // Load battle
    }

    void Exit()
    {
        // Don't save the outcome
    }

    void HandleInput()
    {
        // Read in saved commands so battle
        // is a replay of what originally happened.
    }

    void Update()
    {
        // Update the battle
        if(replayIsOver)
        {
            _stateManager.SetState(States.HOME);
        }
    }
}

with a state manager class to keep track of the current state and facilitate the state switching:

class StateManager
{
    IState _currentState;

    public StateManager()
    {
        _states[States.LOGGING_IN]  = new LoggingInState();
        _states[States.HOME]        = new HomeState();
        _states[States.BATTLE]      = new BattleState();
        _states[States.REPLAY]      = new ReplayState();
    }

    public void SetState(States newState)
    {
        _currentState.Exit();
        _currentState = _states[newState];
        _currentState.Enter();
    }

    public void HandleInput()
    {
        _currentState.HandleInput();
    }

    public void Update()
    {
        _currentState.Update();
    }
}

This pattern can also be used on characters if need be, a use may be where the character’s states differ greatly, say you had a car that could transform into an airplane. In this case, because the car state and the airplane state are wildly different, it might make sense to separate the CAR state and AIRPLANE state into separate classes.

But remember, always try keep it as simple as possible. If you’re not sure which to use, try the switch statement and then move onto the state objects if you find each switch case contains too much code to read easily.

Sources:

Vectors

Common Uses:

  • Represent a point in space or a force.
  • Represent a direction.

Useful Functions:

  • Subtraction – difference between vectors (distance, scale etc).
  • Magnitude – length of a vector.
  • Addition – combining two vectors to get a resulting vector.
  • Cross Product – returns a vector perpendicular to two vectors, useful for finding the “up vector”.
  • Dot Product – returns a scalar indicative of the angle between two vectors.
  • Normalisation – used for a vector to represent a direction.

Subtraction:

Note: Vectors are represented using lower case letters.
Screen Shot 2016-05-05 at 13.21.14 copy.png

 

 

 

 

 

 


a
= (4, 5, 6), b =  (1, 2, 3)
The difference between these vectors is: (ax – bx, ay – by, az – bz)
which equates to the new vector (3, 3, 3) which we will call vector c.

Magnitude:

The distance between a and b is the magnitude of the difference between them. In this case ab = c, so the distance is the magnitude of c.
The magnitude of a vector is expressed like this: |c|
|c| = √(cx2 + cy2 + cz2)
which = 5.19…
so the distance between the points a and b, is 5.19…

Addition:

If you had an airplane flying forward with a tail wind coming from the back right, the tailwind would have an effect on the plane’s motion. How would you calculate this?
Screen Shot 2016-05-05 at 13.59.59 copy 1

Simple, add the plane’s movement vector with that of the wind:
a + b = (ax + bx, ay + by, az + bz)
Screen Shot 2016-05-05 at 14.02.16 copy 2

And the resulting vector is the plane’s actual motion with the wind applied:
Screen Shot 2016-05-05 at 14.04.00 copy 2.png

Cross Product:

Note: this only works for vectors with 3 dimensions.
a x b means the cross product of the vectors a and b.

There are two ways to calculate the cross product – which returns a vector at right angles to both of the input vectors.

cx = aybz – azby
cy = azbx – axbz
cz = axby – aybx

or

a x b = |a| |b| sin(Θ) n
where Θ is the angle between the two vectors and n is a unit vector at right angles to both a and b.

Note the “handedness” of your coordinate system as that will affect the direction of the output vector.

For more information click here.

Dot Product:

Works with vectors of any dimension.
a.b means the dot product of a and b.

a.b = |a| x |b| x cos(Θ)
where Θ is the angle between the two vectors.

Note: this is useful when using normalised vectors (directions) as you get a values of

  • 1 if the vectors are in exactly the same direction.
  • -1 if they point in completely opposite directions.
  • 0 if the vectors are perpendicular.

For more information click here.

Normalisation:

Vectors that have a magnitude of 1 are called unit vectors and have been normalised.

To normalise a vector we simply divide each component by the vector’s magnitude.

Unit vectors are great for storing direction as you can multiply a force vector by a unit vector to get the force in the unit vector’s direction, without changing the force’s magnitude.

Note: for (x, y, z, w) vectors, the w component is often used to indicate whether the vector is a direction or position. w = 1 is position, w = 0 is direction.

Sources:

SOLID Design Principles

SOLID is an acronym comprised of 5 basic principles intended to create clean, maintainable and simple software architecture.

Single Responsibility Principle:

  • “Every software module should have only one reason to change”, meaning each class should have one distinct purpose and only contain code for that exact purpose.
  • For example: An EnemyDefinition class may contain the data/state of an enemy, which will eventually be saved. The save code doesn’t belong in the EnemyDefinition. The EnemyDefinition may however contain Serialise and Deserialise methods as they pertain to the way the data is interpreted but the EnemyDefinition should not care how the data is saved, whether that be on disk, sent to a server etc.
  • In the example below, thanks to the single responsibility principle, we could change our system to upload and download data from a server without needing to touch a line of code in the EnemyDefinition. Since that is the job of the DefinitionSaveLoad, that is the only class we would need to change (or swap out using polymorphism).
using System;
using System.IO;
using System.Collections.Generic;

class MainClass
{
    public static void Main (string[] args)
    {
        EnemyDefinition enemy1 = new EnemyDefinition();
        enemy1.name ="Frank";
        enemy1.damage   = 5;
        enemy1.health   = 300;

        EnemyDefinition enemy2 = new EnemyDefinition();
        enemy2.name = "Bob";
        enemy2.damage   = 2;
        enemy2.health   = 150;

        Console.WriteLine("Initialised");
        Console.WriteLine(enemy1.ToString());
        Console.WriteLine(enemy2.ToString());

        DefinitionSaveLoad saveLoad = new DefinitionSaveLoad();
        saveLoad.AddSaveData(enemy1.Serialise());
        saveLoad.AddSaveData(enemy2.Serialise());
        saveLoad.Save();
        Console.WriteLine("\n Saved");

        enemy1.name = "";
        enemy1.damage   = 0;
        enemy1.health   = 0;

        enemy2.name = "";
        enemy2.damage   = 0;
        enemy2.health   = 0;

        Console.WriteLine("\nData Cleared");
        Console.WriteLine(enemy1.ToString());
        Console.WriteLine(enemy2.ToString());

        string[] loadData = saveLoad.Load();
        enemy1.Deserialise(loadData[0]);
        enemy2.Deserialise(loadData[1]);

        Console.WriteLine("\nData Loaded:");
        Console.WriteLine(enemy1.ToString());
        Console.WriteLine(enemy2.ToString());
    }
}

class EnemyDefinition
{
    public string name;
    public int damage;
    public int health;

    public override string ToString ()
    {
        return Serialise();
    }

    public string Serialise()
    {
        return name
            + "," + damage
            + "," + health;
    }

    public void Deserialise(string serialisedData)
    {
        string[] data   = serialisedData.Split(',');
        name        = data[0];
        damage      = int.Parse(data[1]);
        health      = int.Parse(data[2]);
    }
}

class DefinitionSaveLoad
{
    List<string> _saveData = new List<string>();
    string FilePath
    {
        get
        {
            return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/saveFile.txt";
        }
    }

    public void AddSaveData(string data)
    {
        _saveData.Add(data);
    }

    public void Save()
    {
        if(File.Exists(FilePath))
        {
            File.Delete(FilePath);
        }

        File.WriteAllLines(FilePath, _saveData);
    }

    public string[] Load()
    {
        return File.ReadAllLines(FilePath);
    }
}

Open Closed Principle:

  • “A software module/class is open for extension and closed for modification” which means that one should never need to modify a base class to cater to the needs of a derived class.
  • Using inheritance and method overrides is a way to overcome this.
  • The example below illustrates a problem. What happens when one wants to add a new EnemyType, say a SWIMMING type? They’d have to modify the EnemyDefinition to add new code so GetDifficultyRating can cater for the new SWIMMING type too:
using System;

class MainClass
{
    public static void Main (string[] args)
    {
        EnemyDefinition enemy1 = new EnemyDefinition();
        enemy1.name     = "Walker Frank";
        enemy1.damage   = 5;
        enemy1.health   = 300;
        enemy1.type     = EnemyType.WALKING;

        EnemyDefinition enemy2 = new EnemyDefinition();
        enemy2.name     = "Flyer Bob";
        enemy2.damage   = 2;
        enemy2.health   = 150;
        enemy2.type     = EnemyType.FLYING;

        Console.WriteLine(enemy1.name + "'s difficulty rating is: " + enemy1.GetDifficultyRating());
        Console.WriteLine(enemy2.name + "'s difficulty rating is: " + enemy2.GetDifficultyRating());
    }
}

class EnemyDefinition
{
    public string name;
    public int damage;
    public int health;
    public EnemyType type;

    public int GetDifficultyRating()
    {
        int difficultyRating = 0;

        if(type == EnemyType.WALKING)
        {
            difficultyRating = (damage + health) * 2;
        }
        else if(type == EnemyType.FLYING)
        {
            difficultyRating = (damage + health) * 10;
        }   

        return difficultyRating;
    }
}

public enum EnemyType
{
    FLYING,
    WALKING,
}
  • Also think about the repercussions of modifying a method that could be shared by many enemy types. If someone somehow “broke” the difficulty rating calculation, there’s potential for all enemys’ difficulty rating calculations breaking since they’re all sharing the exact same code. Each modification has the potential to introduce a bug across all the types of enemies.
  • A solution to this issue would be to have the EnemyDefinition as an extensible base class, and override the GetDifficultyRating method in each of the derived classes:
using System;

class MainClass
{
    public static void Main (string[] args)
    {
        WalkingEnemyDefinition enemy1 = new WalkingEnemyDefinition();
        enemy1.name     = "Walker Frank";
        enemy1.damage   = 5;
        enemy1.health   = 300;

        FlyingEnemyDefinition enemy2 = new FlyingEnemyDefinition();
        enemy2.name     = "Flyer Bob";
        enemy2.damage   = 2;
        enemy2.health   = 150;

        FlyingEnemyDefinition enemy3 = new FlyingEnemyDefinition();
        enemy3.name     = "Swimmer Jane";
        enemy3.damage   = 10;
        enemy3.health   = 200;

        Console.WriteLine(enemy1.name + "'s difficulty rating is: " + enemy1.GetDifficultyRating());
        Console.WriteLine(enemy2.name + "'s difficulty rating is: " + enemy2.GetDifficultyRating());
        Console.WriteLine(enemy3.name + "'s difficulty rating is: " + enemy3.GetDifficultyRating());
    }
}

class EnemyDefinition
{
    public string name;
    public int damage;
    public int health;

    public virtual int GetDifficultyRating()
    {
        return damage + health;
    }
}

class WalkingEnemyDefinition : EnemyDefinition
{
    public override int GetDifficultyRating()
    {
        return (damage + health) * 2;
    }
}

class FlyingEnemyDefinition : EnemyDefinition
{
    public override int GetDifficultyRating()
    {
        return (damage + health) * 10;
    }
}

class SwimmingEnemyDefinition : EnemyDefinition
{
    public override int GetDifficultyRating()
    {
        return (damage + health) * 5;
    }
}
  • And there you have it. We can now create as many different enemy types with different difficulty rating calculations without needing to touch a single line of code in the EnemyDefinition.

Liskov Substitution Principle:

  • Also known as (LSP) – not to be confused with the Lumpy Space Princess from the popular TV series “Adventure Time”.
  • “You should be able to use any derived class instead of a parent class and have it behave in the same manner without modification”, which means that child classes should extend the functionality of their parents, not change it completely.
  • In the following code, the issue is that there is StatueEnemyDefinition type. In this case Statue Enemy types don’t have a difficulty rating and one needs to remember not to call the GetDifficultyRating method on Statue Enemy types or there will be an exception thrown:
using System;
using System.Collections.Generic;

class MainClass
{
    public static void Main (string[] args)
    {
        List<EnemyDefinition> enemies = new List<EnemyDefinition>();

        EnemyDefinition enemy1 = new WalkingEnemyDefinition();
        enemy1.name     = "Walker Frank";
        enemy1.damage   = 5;
        enemy1.health   = 300;
        enemies.Add(enemy1);

        EnemyDefinition enemy2 = new FlyingEnemyDefinition();
        enemy2.name = "Flyer Bob";
        enemy2.damage   = 2;
        enemy2.health   = 150;
        enemies.Add(enemy2);

        EnemyDefinition enemy3 = new StatueEnemyDefinition();
        enemy3.name = "Statue Jane";
        enemy3.damage   = 0;
        enemy3.health   = 0;
        enemies.Add(enemy3);

        foreach(EnemyDefinition enemy in enemies)
        {
            Console.WriteLine(enemy.name + "'s difficulty rating is: " + enemy.GetDifficultyRating());
        }
    }
}

class EnemyDefinition
{
    public string name;
    public int damage;
    public int health;

    public virtual int GetDifficultyRating()
    {
        return damage + health;
    }
}

class WalkingEnemyDefinition : EnemyDefinition
{
    public override int GetDifficultyRating()
    {
        return (damage + health) * 2;
    }
}

class FlyingEnemyDefinition : EnemyDefinition
{
    public override int GetDifficultyRating()
    {
        return (damage + health) * 10;
    }
}

class StatueEnemyDefinition : EnemyDefinition
{
    public override int GetDifficultyRating()
    {
        throw new Exception("Statue Enemies don't have a difficulty!");
    }
}
  • This is dangerous and a really bad design as anyone using this code will always have to remember not to call the method on the Statue Enemy type.
  • In the code below, we’ve corrected this design flaw.
    • Firstly, there is no need for statue enemy types to have “health” or “damage” fields as they have no health and do no damage, so I’ve changed the design so the EnemyDefinition contains only the fields shared by ALL enemies.
    • The attributes specific to Attackable Enemies have been moved to an attackable enemy class.
    • With the latest set of modifications, each type is substitutable for its parent and behaves as expected when treated as such – all the classes with the GetDifficultyRating method actually require and implement it:
using System;
using System.Collections.Generic;

class MainClass
{
    public static void Main (string[] args)
    {
        List<EnemyDefinition> enemies = new List<EnemyDefinition>();
        List<AttackableEnemyDefinition> attackableEnemies = new List<AttackableEnemyDefinition>();

        AttackableEnemyDefinition enemy1 = new WalkingEnemyDefinition();
        enemy1.name = "Walker Frank";
        enemy1.damage   = 5;
        enemy1.health   = 300;
        enemies.Add(enemy1);
        attackableEnemies.Add(enemy1);

        AttackableEnemyDefinition enemy2 = new FlyingEnemyDefinition();
        enemy2.name = "Flyer Bob";
        enemy2.damage   = 2;
        enemy2.health   = 150;
        enemies.Add(enemy2);
        attackableEnemies.Add(enemy2);

        EnemyDefinition enemy3 = new EnemyDefinition();
        enemy3.name     = "Statue Jane";
        enemies.Add(enemy3);

        Console.WriteLine("All Enemies");
        foreach(EnemyDefinition enemy in enemies)
        {
            Console.WriteLine(enemy.name);
        }

        Console.WriteLine("\nAttackable Enemies");
        foreach(AttackableEnemyDefinition enemy in attackableEnemies)
        {
            Console.WriteLine(enemy.name + "'s difficulty rating is: " + enemy.GetDifficultyRating());
        }
    }
}

class EnemyDefinition
{
    public string name;
}

class AttackableEnemyDefinition : EnemyDefinition
{
    public int damage;
    public int health;

    public virtual int GetDifficultyRating()
    {
        return damage + health;
    }
}

class WalkingEnemyDefinition : AttackableEnemyDefinition
{
    public override int GetDifficultyRating()
    {
        return (damage + health) * 2;
    }
}

class FlyingEnemyDefinition : AttackableEnemyDefinition
{
    public override int GetDifficultyRating()
    {
        return (damage + health) * 10;
    }
}

Interface Segregation Principle:

  • “Clients should not be forced to implement interfaces they don’t use. Instead of one fat interface, many small interfaces are preferred based on groups of methods, each one serving one sub module.”
  • Which basically means to avoid being forced to implement code that won’t be used by the inheriting class.
  • In the example below, the Statue Enemy is forced to implement the Save and Load methods because they are part of the base class, even though – for the sake of this example – statue enemies don’t need to be saved or loaded:
using System;
using System.Collections.Generic;

class MainClass
{
    public static void Main (string[] args)
    {
        List<EnemyDefinition> enemies = new List<EnemyDefinition>();

        AttackableEnemyDefinition enemy1 = new AttackableEnemyDefinition();
        enemy1.name     = "Walker Frank";
        enemies.Add(enemy1);

        AttackableEnemyDefinition enemy2 = new AttackableEnemyDefinition();
        enemy2.name     = "Flyer Bob";
        enemies.Add(enemy2);

        EnemyDefinition enemy3 = new StatueEnemyDefinition();
        enemy3.name     = "Statue Jane";
        enemies.Add(enemy3);

        Console.WriteLine("All Enemies");
        foreach(EnemyDefinition enemy in enemies)
        {
            Console.WriteLine(enemy.name);
        }

        Console.WriteLine(" \nSaving Enemies");
        foreach(EnemyDefinition enemy in enemies)
        {
            Console.Write(enemy.name + ": ");
            enemy.Save();
        }
    }
}

abstract class EnemyDefinition
{
    public string name;
    public abstract void Save();
    public abstract void Load();
}

class AttackableEnemyDefinition : EnemyDefinition
{
    public override void Save()
    {
        Console.WriteLine("Saving attackable enemies.");
    }

    public override void Load()
    {
        Console.WriteLine("Loading attackable enemies.");
    }
}

class StatueEnemyDefinition : EnemyDefinition
{
    public override void Save()
    {
        Console.WriteLine("Not saving statues.");
    }

    public override void Load()
    {
        Console.WriteLine("Not loading statues.");
    }
}
  • In the modified code, the EnemyDefinition has had the Save and Load code removed as not all enemies need to be saved and loaded (the Statue enemy in this case).
  • Instead, the Save and Load has been placed in an interface that can be implemented by any entity that requires the ability to be saved and loaded. In this way, we avoid having unused code in our classes:
using System;
using System.Collections.Generic;

class MainClass
{
    public static void Main (string[] args)
    {
        List<EnemyDefinition> enemies = new List<EnemyDefinition>();
        List<ISaveLoad> saveLoadable = new List<ISaveLoad>();

        AttackableEnemyDefinition enemy1 = new AttackableEnemyDefinition();
        enemy1.name     = "Walker Frank";
        enemies.Add(enemy1);
        saveLoadable.Add(enemy1);

        AttackableEnemyDefinition enemy2 = new AttackableEnemyDefinition();
        enemy2.name     = "Flyer Bob";
        enemies.Add(enemy2);
        saveLoadable.Add(enemy2);

        EnemyDefinition enemy3 = new StatueEnemyDefinition();
        enemy3.name     = "Statue Jane";
        enemies.Add(enemy3);

        Console.WriteLine("All Enemies");
        foreach(EnemyDefinition enemy in enemies)
        {
            Console.WriteLine(enemy.name);
        }

        Console.WriteLine(" \nSaving");
        foreach(ISaveLoad saveLoadableEntry in saveLoadable)
        {
            saveLoadableEntry.Save();
        }
    }
}

abstract class EnemyDefinition
{
    public string name;
}

interface ISaveLoad
{
    void Save();
    void Load();
}

class AttackableEnemyDefinition : EnemyDefinition, ISaveLoad
{
    public void Save()
    {
        Console.WriteLine("Saving " + name);
    }

    public void Load()
    {
        Console.WriteLine("Loading " + name);
    }
}

class StatueEnemyDefinition : EnemyDefinition
{
}

Dependency Inversion Principle

  • “High-level modules/classes implement business rules or logic in a system/application. Low-level modules/classes deal with more detailed operations.” meaning that high-level classes shouldn’t be concerned with the nitty gritty details of writing files to a specific file system etc. This principle serves to help abstraction.
  • In the code below, what happens when we want to disable logging to the console? Or what if we want to display the text on screen instead? Or send it to a server for processing?
using System;

class MainClass
{
    public static void Main (string[] args)
    {
        EnemyDefinition enemy1 = new EnemyDefinition();
        enemy1.SetName("Frank");

        EnemyDefinition enemy2 = new EnemyDefinition();
        enemy2.SetName("Bob");
    }
}

class EnemyDefinition
{
    string _name;

    public void SetName(string name)
    {
        _name = name;
        Console.WriteLine("Setting name to: " + name);
    }
}
  • If we wanted to make any changes to the way logging worked, we’d have to go through potentially many classes, modifying the code to change the Console.WriteLine to whatever else.
  • This is where Dependency Inversion comes in handy. Using DI, we can wrap up the nitty gritty of the logging functionality, allowing the EnemyDefinition and its derived classes to continue doing their thing without needing to do the actual nitty gritty logging to the console themselves:
using System;

class MainClass
{
    public static void Main (string[] args)
    {
        Logger logger = new Logger();

        EnemyDefinition enemy1 = new EnemyDefinition(logger);
        enemy1.SetName("Frank");

        EnemyDefinition enemy2 = new EnemyDefinition(logger);
        enemy2.SetName("Bob");
    }
}

class EnemyDefinition
{
    Logger _logger;
    string _name;

    public EnemyDefinition(Logger logger)
    {
        _logger = logger;
    }

    public void SetName(string name)
    {
        _name = name;
        _logger.Log("Setting name to: " + name);
    }
}

class Logger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}
  • So now if we want to disable logging, we only need to make one change in one place in our code (the Log method of the Logger class).
  • We could also have different Logger implementations, possibly one for the console and another that logs to a file:
using System;
using System.IO;

class MainClass
{
    public static void Main (string[] args)
    {
        ILogger logger = new FileLogger();

        EnemyDefinition enemy1 = new EnemyDefinition(logger);
        enemy1.SetName("Frank");

        EnemyDefinition enemy2 = new EnemyDefinition(logger);
        enemy2.SetName("Bob");
    }
}

class EnemyDefinition
{
    ILogger _logger;
    string _name;

    public EnemyDefinition(ILogger logger)
    {
        _logger = logger;
    }

    public void SetName(string name)
    {
        _name = name;
        _logger.Log("Setting name to: " + name);
    }
}

interface ILogger
{
    void Log(string message);
}

class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

class FileLogger : ILogger
{
    string LogPath
    {
        get
        {
            return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/log.txt";
        }
    }

    public void Log(string message)
    {
        if(!File.Exists(LogPath))
        {
            File.WriteAllText(LogPath, message + "\n");
        }
        else
        {
            File.AppendAllText(LogPath, message + "\n");
        }
        Console.WriteLine(LogPath);
    }
}
  • As you can see, it is now pretty simple to swap out the ConsoleLogger for a FileLogger, or whatever other type of logger we decide to create, with the best part being that not a single line of code in the EnemyDefinition needs to change!

Sources:

Model-View-Controller

Purpose:

  • To simplify code architecture.

Description:

  • Logic is separated into 3 main groups of functionality:
    • Model: Data.
    • View: Interface/Rendering.
    • Controller: Business/Game Logic.
  • Models:

    • Hold Data/State.
    • Serialise and Deserialise (saving).
    • Convert between types.
    • Are accessed but never do any accessing (they are “dumb” data objects).
  • Views:

    • Can get data from models.
    • Should never mutate models.
    • Strictly implements the functionality of its class:
      • Acts as a black box.
      • Does not store the core data (as this is what the model should store).
      • Does not contain game/business logic.
  • Controllers:

    • Do not store core data.
    • Update and use the Model’s data.
    • Manages Unity’s scene workflow.

Example:

This is a relatively simple example where some racers are created and set to race against each other across a console window.

  • Racers:

    • Each racer has a RacerData object which is the model. This data gets manipulated by a RacerController. All the racer controllers are set to race against each other by the race simulator. The RaceSimulator is also a controller.
    • The RacerRenderer contains the logic to “draw” the racers as they are raced against one another by the race simulator. It reads the racer’s data but does not modify it in any way

Racer MVC

using System;
using System.Threading;
using System.Collections.Generic;

class MainClass
{
	public static void Main (string[] args)
	{
		RaceSimulator simulation= new RaceSimulator(Console.WindowWidth);
		RacerRenderer renderer 	= new RacerRenderer();

		// Add the player to the simulation
		Console.WriteLine("Enter your name and press Enter.");
		string playerName = Console.ReadLine();
		RacerFactory.CreateRacer(playerName, simulation, renderer);

		// Add other racers to simulation
		string[] racerNames = new string[]{"Jack", "Joe", "Jane", "Jenny", "Jason"};
		foreach(string racerName in racerNames)
		{
			RacerFactory.CreateRacer(racerName, simulation, renderer);
		}

		// Game Loop
		while(true)
		{
			simulation.Update();
			renderer.Render();

			// Fixed framerate for simplicity
			Thread.Sleep(300);
		}
	}
}

/// <summary>
/// MODEL
/// - Stores the racer data.
/// </summary>
public class RacerData
{
	public string name;
	public int x;
	public int y;
	public int speed;
	public int lap;
}

/// <summary>
/// CONTROLLER
/// - Game Logic which modifies the racer's data.
/// </summary>
public class RacerController
{
	public RacerController(RacerData data)
	{
		_data = data;
	}

	public void SetPosition(int y)
	{
		_data.y	= y;
	}

	public void Move()
	{
		_data.x += _data.speed;
	}

	public void CompleteLap()
	{
		_data.lap++;
		_data.x = 0;
	}

	public bool WithinBounds(int width)
	{
		return _data.x <= width;
	}

	private RacerData _data;
}

/// <summary>
/// CONTROLLER
/// - Makes the individual race controllers race against one another.
/// </summary>
public class RaceSimulator
{
	public RaceSimulator(int width)
	{
		_width 	= width;
		Racers 	= new List<RacerController>();
	}

	public void AddRacer(RacerController racer)
	{
		racer.SetPosition(Racers.Count);
		Racers.Add(racer);
	}

	public void Update()
	{
		foreach(RacerController racer in Racers)
		{
			racer.Move();

			if(!racer.WithinBounds(_width))
			{
				racer.CompleteLap();
			}
		}
	}

	public List<RacerController> Racers { get; private set; }
	int _width;
}

/// <summary>
/// VIEW
/// - Draws the racers
/// </summary>

public class RacerRenderer
{
	public RacerRenderer()
	{
		_racers	= new List<RacerData>();
	}

	public void AddRacer(RacerData racerData)
	{
		_racers.Add(racerData);
	}

	public void Render()
	{
		Console.Clear();

		foreach(RacerData racer in _racers)
		{
			string racerInfo = racer.name + ", Lap: " + racer.lap;

			if(racer.x < Console.WindowWidth)
			{
				Console.SetCursorPosition(racer.x, racer.y);
				Console.Write(racerInfo);
			}
		}
	}

	List<RacerData> _racers;
}

/// <summary>
/// Helper factory class to create and configure the racer objects.
/// </summary>
static class RacerFactory
{
	static Random _random = new Random();

	public static void CreateRacer(string name, RaceSimulator simulation, RacerRenderer renderer)
	{
		RacerData racerData = new RacerData();
		racerData.name	= name;
		racerData.x 	= 0;
		racerData.y		= 0;
		racerData.lap	= 0;
		racerData.speed	= _random.Next(1, 10);

		RacerController racerController	= new RacerController(racerData);

		simulation.AddRacer(racerController);
		renderer.AddRacer(racerData);
	}
}

Sources:

Build Size

Why?

Currently Apple and Google both limit Over-the-Air downloads to 100MB. This means that if you want your users to be able to download your app without a WiFi connection, it needs to adhere to these size limits.

Let’s Begin:

As with many aspects of life, there’s no point rushing into things blindly – although that does sort of help natural selection along.

With build sizes, the same is true. You’ll get much greater effect determining which part of your project is contributing the most to build size and going after that area, rather than just randomly gutting bits here and there, hoping for the best and wasting time.

For instance, you can compress audio all you want, but if you have a 4K texture that’s used on a leaf in the distance, you’d be much better off finding that leaf and changing the texture size to something more along the lines of 32 pixels or even less.

This post is about finding that tricksy little leaf …and a bit more too.

The Size Breakdown:

After doing a build, Unity will put a breakdown of the various asset sizes in the “Editor Log” which you can find by clicking the “Open Editor Log” option in the menu at the top right corner of Unity’s console window (or Google “How to find the Unity Editor Log”).

  1. Open the Editor Log.
  2. Find the Size Breakdown by search around till you find something that looks a bit like this:

Textures 154.5 mb 56.8%
Meshes 8.9 mb 3.3%
Animations 75.5 mb 27.7%
Sounds 18.6 mb 6.8%
Shaders 170.5 kb 0.1%
Other Assets 4.8 mb 1.8%
Levels 537.8 kb 0.2%
Scripts 984.2 kb 0.4%
Included DLLs 5.1 mb 1.9%
File headers 3.1 mb 1.1%
Complete size 272.2 mb 100.0%

Here we can see that for this project: Textures take up the lion’s share of my project’s size, followed by Animations, Sounds and then Meshes.

These are the 4 categories we will be focusing on first, and after, if the file size is still too big, we’ll go through the Resources folder to gut any unused assets before finishing off on the Player Settings.

The reason going through Resources is last, is this is the most likely to cause issues at runtime if you accidentally remove the wrong “unused” asset. But if you are 100% sure that certain assets in the Resources folder aren’t used, take a look at the Resources section of this post first.

Note:

  • Unity re-codes imported assets, so the file format in the Unity project isn’t as important as its compression settings in the Inspector. E.g. A multi-layer Photoshop texture will be flattened and compressed before building.
  • Unity strips out most unused assets during the build (except scripts, which are usually tiny anyway). It won’t however strip assets out of the Resources folder which is why we need to do that ourselves.

Tip:

  • I recommend doing a build after each optimisation phase and giving it a descriptive name so you can see just how much impact each phase has on your build size.

Textures:

Here our main aim is to find that 4K texture applied to a little leaf off in the distance.

Luckily Unity’s Editor Log can help us here too as it lists the textures in the project from largest to smallest:

Textures 154.5 mb 56.8%
5.3 mb 2.0% Assets/Resources/Textures/Ring/old/ring_diffuseColour.png
2.7 mb 1.0% Assets/Resources/Textures/Ring/ring01.1Surface_Color.tif
2.7 mb 1.0% Assets/Resources/Textures/Minions/Old textures/Bomb_Diffuse1.jpg
2.7 mb 1.0% Assets/Resources/Textures/Minions/Old textures /Bomb_Diffuse2.jpg
2.7 mb 1.0% Assets/Resources/Textures/Player/Shorts Skins/Yellow_Diffuse_003.jpg
etc…

Based on this we can decide which textures to tackle first for greatest effect.

The parameters we will be looking at changing are:

  • Size

    • Each time you go down a power of 2 in your images’s dimensions, you are decreasing the amount of pixels by 3 which drastically decreases the file size.
    • This is why it is a good idea to set the image size as small as possible within Unity without too much loss of quality.
    • This will likely involve a lot of trial and error but eventually you should start to recognise what sizes the various images in your project should be.
  • Texture Format

    • Start with the lowest quality setting and see how high you need to go to achieve something that looks acceptable:
      1. Compressed:

        • Most common for diffuse textures.
      2. 16 bit:

        • Lower quality True Colour.
      3. True Colour:

        • Highest Quality.
    • Bare in mind that your textures most likely need to look good across multiple devices with varying screen resolutions.
    • Take a look at the Unity Manual, specifically the Texture Format section for more detail.
  • Mip Mapping

    • Mip maps are smaller versions of your texture that are automatically created if this option is turned on.
    • They may almost double your texture size but are used to optimise graphics performance so it is a good idea to leave this option on for objects that will be moving around your scene, like textures used on characters and scenery (if you have a moving camera) etc.
      • Test performance on lower end devices if you do end up turning Mip Mapping off on any textures.
    • They can and should be turned off for UI.
    • For more information, take a look at the Mip Maps section of the Unity documentation.

Animations:

In the Animations Tab, the option we’re interested in is:

  • Anim. Compression:

    1. Optimal:

      • Only available for generic and humanoid rigs.
      • Reduces keyframes and increases performance.
    2. Keyframe Reduction and Compression:

      • Reduces the amount of keyframes and also compresses stored animation files resulting in smaller build size.
      • If animations are producing inaccuracies, lower the “Animation Compression Error” values.
    3. Keyframe Reduction:

      • Reduces the keyframes on import, resulting in both performance and build size optimisations.
    4. Off:

      • Not recommended. Even for high precision animations it is recommended to use Keyframe Reduction and just lower the Animation Compression Error values.

Sounds:

Most sound clips will be relatively small so unless you have tons of effects, there probably won’t be much effect targeting sounds other than the longer, music tracks.

There are quite a few options here for different cases so I’ll go through the important ones:

  • Force To Mono:

    • Most sounds have a separate waveform for the Left and Right speakers.
      • For mobile, unless your users are going to be using headphones, this is often barely audible so sounds can be set to Force To Mono which should reduce the file size by half.
  • Compression Format:

    • ADPCM:

      • For short sounds like footsteps and impacts. The file size will be larger than Vorbis/MP3 but that doesn’t usually make a massive impact as far as the file sizes go.
    • Vorbis/MP3:

      • Best for medium length sound effects and music.
  • Sample Rate Setting:

    • There is no point increasing a sample rate higher than what the source file has, so check the source rate before overriding it.
    • You can also see the effect your compression settings will have on the selected audio clip in the inspector which is quite handy.
    • Override Sample Rate:

      • 22,050 Hz: For short sounds like footsteps and impacts.
      • 44,100 Hz: For longer clips and music. Any sound with a lot of detail, like a song multiple instruments.

Another thing you can do to decrease the size of large audio files is to make the loops shorter. Open them up in your favourite audio program and start chopping. Think about how long you expect users to stay in any one menu or level and shorten the audio clip so that it loops a few times without feeling too repetitive. If you cut down the a 10 minute song to a 1 minute loop, you’ve most likely saved yourself about 5 MB worth of space.

Meshes:

In the Model Tab, the only option to concern ourselves with is:

  • Mesh Compression:

    • The higher the compression, the smaller the file size will be, though as with textures, there is a tradeoff in quality.
    • Increase the compression as high as possible without seeing too much of a quality loss.

Note:

  • A compressed mesh has less vertices, meaning less calculations are needed to be done at runtime so this would also optimise graphical performance.
  • The “Optimise Mesh” option is for graphical performance and is a good idea to leave on too. More information can be found here with more technical detail here.

Resources:

All assets in the Assets/Resources are included in the build. This is so that they can be loaded dynamically at runtime, using Resources.Load(…);

This is a really hand feature of Unity, though sometimes people (usually of the Artistic persuasion) forget to remove older, unused versions of assets.

For example, in the project I’m busy optimising, there’s this little guy:

Assets/Resources/Textures/Ring/old/ring_diffuseColour.png

To be safe, rather than deleting the seemingly unused assets, we can just create another folder in the project called something along the lines of “Unused“.

Remember: Unity will strip unused assets unless they are in the resources folder, so by moving these assets out of the Resources folder, they won’t get included in the build.

But how can I make sure the asset isn’t being loaded using “Resources.Load(…)”?
Well unfortunately, the only way to be 100% sure is to look at the code.

The steps are as follows:

  1. Copy all the assets from “Resources” into the “Unused” folder.
  2. Do a search in the code and look for all occurrences of “Resourced.Load”.
  3. Systematically copy each resource that is referenced back from the “Unused” folder into the “Resources” folder, making sure the folder structure remains the same.

E.g: After copying out all the assets, I’ve done a search and found the following line in code:

Resources.Load(“Main game/Boss”)

The asset was moved into the unused folder, it’s path being:

Assets/Unused/Main game/Boss.prefab

 

 

Which needs to be copied to:

Assets/Resources/Main game/Boss.prefab

Note: Most of process could be automated and you’ll probably want to do so for larger projects.

This is fine for smaller, simple projects but when it comes to larger or more complex ones there are a few things to watch out for:

  • Variables:
    • Resources.Load(_bannerLocation)

    • You’ll need to find out what value is being stored inside the variable “_bannerLocation”.
      • Where is it assigned?
      • Does the value ever change and if so, what does it change to because that asset will need to be included too?
  • Dynamically constructed paths:
    • for(int i = 1; i < 4; i++)
      {
      Resources.Load(“Main Game/Textures/Enemy_” + i);
      }

    • The code above creates the following 3 paths:
      • Main Game/Textures/Enemy_1
        Main Game/Textures/Enemy_2
        Main Game/Textures/Enemy_3

      • So all of those assets will need to be added back to the Resources folder.

For this reason, it’s a lot easier to just make sure your team knows to remove unused assets from the Resources folder before you get to this point…unfortunately this knowledge is far too often only obtained when it is too late.

If you accidentally remove an asset your game does actually still load from the resources folder, it will throw a NullReferenceException. Be on the lookout for these after removing the unused assets from your game.

Player Settings:

  • Api Compatibility Level:

    • .Net 2.0 Subset:

      • We don’t usually need all the functionality of the entire .Net library so we can generally get away with using this trimmed down version.
  • Stripping Level:

    1. Use micro mscorlib:

      • Smaller version of mscorlib.
      • Try using this.
      • If it doesn’t work, go up the options till you get to an option that does.
    2. Strip Assemblies:

      • Unused classes and methods are removed from the DLLs.
    3. Note: 
      • You can tell Unity not to strip certain classes by creating a stripping blacklist.
      • More information can be found here.
  • Script Call Optimisation:

    • Set to Fast but No Exceptions:
      • This stops Unity from generating extra error handling code.
      • Not recommended during development, as it makes debugging considerably harder. Though it is fine for release if you really need to squeeze every last drop out of your build size.
  • Optimize Mesh Data:

    • Removes additional information on the meshes that isn’t required (by their textures).

More information on the Build Player Settings.

Conclusion:

So, my project’s build sizes started at 131MB and at each stage decreased to:

  1. 95MB – removing unused assets in Resources.
  2. 70 MB – Textures.
  3. 63 MB – Animations.
  4. 52 MB – Sounds.
  5. 50 MB – Meshes.
  6. 49 MB – Player Settings.

This will be different with every project, but hopefully you get a rough idea how much a badly (or just not at all) optimised project can be brought down regarding build size.

As with most things in life, this is all a lot easier if you keep note of the app size and apply the various size optimisation techniques during development.

If you can’t get your app below 100MB for Google, you can take a look at splitting the application binary which makes your .apk considerably smaller by moving a large portion of your game’s assets into an .obb file which can be downloaded later.

 

 

Sources: