QA

[Q&A] Learn By Example – SOLID Principles in Unity (Part 2)

This is the Q&A section for my video Learn By Example – SOLID Principles in Unity (Part 2)

I do my best to respond to every comment, but some questions require very detailed answers. As a result, I’ve created this page so Jason Storey and I have a place to provide detailed replies to some of the more complicated questions we receive.

Navigation

📺 Watch the video here

Question 1

How about native Unity classes like Transform? We can’t support the dependency inversion principle in such situations and it looks like a kind of a mess. What do you think?


Антон Смирнов

Jason Storey’s Answer

I wouldn’t be so sure about that! Dependency inversion is just a way of saying someone else will deal with that problem. The thing about using transforms. is that it is just another dependency. there are multiple ways to detach a dependency on it. lets look at a few.

Say you have a simple movement script. you want to set a direction to move in

void MoveTowards(Vector3 direction){
      transform.position += direction * speed;
}

ok. so that works. but you are dependent on the transform. but what about this.

void MoveTowards(Vector3 direction){
      rigidbody.AddForce(direction*speed);
}

this is moving the object but in a different way. so what if i had this

interface MoveMethod {
    void MoveTowards(Vector3 direction);
}

so which implementation does the MoveMethod use? does it matter? can you even tell if it uses a transform or a rigidbody?

now lets revisit the idea of transform dependence. Say you had some gameobject with this code in it

class Duck{
    MoveMethod _moveMethod;
    void Update(){
        var direction = GetPlayerInput();
         if(_moveMethod != null)
          _moveMethod.MoveTowards(direction);
    }
}

Do you see any mention of transform? rigidbody? maybe I want to use a tweening library to move. in short. a transform is just another “thing”. it is no more special than any other system in your code. it doesn’t need special treatment. it can be decoupled in just the same way. in fact it can even go the other way.

Imagine you wanted to get the position of objects but for some you want the position to be the feet, for others you want it to be the center of mass.

public class GameCharacter {
     Vector3 Position {get;set;}
}

You can use this code no problem. set the position get the position. no problem. but behind the scenes you could change it’s implementation from:

public class GameCharacter : MonoBehaviour {
     Vector3 Position {
         get => transform.position;
         set => transform.position = value;
      }
}

to:

public class GameCharacter : MonoBehaviour {
     public Vector3 PositionOffset = new Vector3(0,1,0);
     Vector3 Position {
         get => transform.position+PositionOffset;
         set => transform.position = value-PositionOffset;
      }
}

and the consuming code is none the wiser…

Question 2


With the current implementation, it feels very error prone for new users to start using the SelectionManager. It requires multiple additional scripts attached to the GameObject and the only way to see if the SelectionManager will work is to hit Play and parse the console messages. Is there a better way to handle this?


Mineeero

Jason Storey’s Answer

You bring up a good point, In real world terms you would follow a principle I call “Batteries Included” that states that even without configuration your code should work in a default state with additional configuration being optional, not required.

it avoids those confusing examples like you describe where someone is trying to use the code but doesn’t know what they need.

There are multiple possible ways to solve this problem. Lets look at some of them.

Requires Component

One option is to mark a script as being the one that composes the parts it needs to function with the RequiresComponent attribute. Which will automatically add the child classes it needs. This is a great help when you have specific implementations in mind but removes your ability to compose parts differently.

Default Implementation

A method I personally prefer is where you provide an implementation that does the bare minimum functionality. Lets take a simple example of achievements.

If you were making a game that was cross-platform and wanted to use google play for notifications on android, steam achievements on pc and use a custom implementation on apple devices you would have say, a GameScores script that would call into a Scoreboard to update it. lets have a look.

class GameScores {
     Scoreboard scoreboard;
     ScoreUI scoreScreen;
     Player player;
     Score score;
     
     public void GameOver(){
       scoreboard.SendScore(Player,score);
       scoreScreen.Show(score);

     }

}

In the above example you are calling to some external scoreboard to do whatever it wants with the score information and save it on whatever platform suits best. but if the scoreboard doesn’t exist you don’t want that to break the ability to show a score on the screen.

Also, you don’t want to be in a situation where someone tries to use your GameScores script but doesn’t know they need a scoreboard and everything breaks.

This is a prime candidate for the NullObjectPattern. What we want to do is effectively say “If you don’t provide me with a scoreboard I will use [X] by default” the important thing is though, it doesn’t matter what that [X] is, as long as it is a valid “scoreboard”.

That means it still counts even if it… does nothing!

Say we had:

class NoScoreBoard : Scoreboard {
  void SendScore(Player player,Score score){}
}

Now we can write something like

class GameScores {
     Scoreboard scoreboard;
     ScoreUI scoreScreen;
     Player player;
     Score score;
     
     public void GameOver(){
       GetScoreBoard().SendScore(Player,score);
       scoreScreen.Show(score);

     }

     ScoreBoard GetScoreBoard(){
         if(scoreboard == null) scoreboard = new NoScoreboard();
        return scoreboard;
     }

}

So even if the person using the code does not provide any implementation of a scoreboard at all, the code will work perfectly just… not post scores!

With this kind of a design what looks like requirements start to become “plugins” you can always provide a base level of implementation and expose an ability for the user to assign any new behavior they want.