SOLID – Single Responsibility Principle

Most of the time our code looks like the swiss army knife in the image above. It can do everything at once, but when you try to use it you probably will injure yourself.

When it comes to programming we generally prefer the plain old kitchen knife which has one single job: To cut. Let’s find out why.

1. Single Responsibility Principle

Each system,class and function should be responsible for exactly one thing.
Unity does this too by the way. You have seperate components for certain functionalities i.e. Transform, Collider and Rigidbody.
I mean they could have just mixed all three of these together in a single component but do you think that would have been smart? … obviously not because sometimes you need i.e. transforms that don’t have a collider and rigidbody attached!

Let’s examine this further in a practical example:

Usually when I create Prototypes my Scripts start out as so called God-Objects, which means they do everything: i.e. a PlayerScript for an ActionRPG like Diablo would contain the logic for movement, inputhandling, and pathfinding (and so much more). However when I know, that this is more than just a weekend experiment I apply the single respobsibility principle and split the Player-GodObject into seperate classes:

God Object

God Object

Seperated Single Responsibility

Refactored Playercontroller

public class PlayerController : MonoBehaviour
{
  [SerializeField] private PlayerInput _playerInput;
  [SerializeField] private PlayerMotor _playerMotor;
  [SerializeField] private Pathfinding _pathfinding;
  
  private void Awake()
  {
    _playerInput = GetComponent<PlayerInput>();
    _playerMotor = GetComponent<PlayerMotor>();
    _pathfinding= GetComponent<Pathfinding>();
  }
  
  private void Start()
  {
    //Playerinput handles different inputsystems (i.e. Tablet, mouse, etc)
    _playerInput.onClick += PlayerInput_OnClick
  }
  
  private void PlayerInput_OnClick(Vector3 clickedWorldPos)
  {
    Path path _pathfinding.CalculatePath(clickedWorldPos);
    Vector3 moveDirection = (path.nextNode.position - transform.position).normalized;
    //Playermotor handles Move-Animation, acceleration etc.
    _playerMotor._moveDirection = moveDirection;
  }
  //...
}

In this example you can see, that the PlayerController is a Top-Level Script that controls how different aspects of the player work together.

For Example the PlayerController has no idea how the pathfinding actualy works. It just tells the Pathfinding-Script, that it should generate a path and then tells the PlayerMotor-Script to move along this path.
Again, the PlayerController has no idea and doesn’t care, that you have to play Move-Animations, play Soundeffects and slowly ramp up the velocity for the movement because the PlayerMotor is responsible for that.

The Upside of this approach is, that your code get’s a better structure and is easier to read and reuse. For example in this case we could reuse the Pathfinding-Component for enemies.

However one should not overdo it. I’ve seen “Best Practice” Examples where people suggest, that you should create 6 classes for the Player in Pong to seperate the functionality properly. Each class would in this case probably contain no more than 10 lines of code which I think definitely doesn’t help the readability and ultimately defeats the purpose of the separation.

It is sometimes quite hard to determine which functionalities to put into seperate Scripts. At first glance it might for example look like a good idea to seperate Movement, Walljumping and the Dash-Ability into seperate scripts. However quite often they somehow interact with- or depend on one another and this would lead to some significant problems down the road.

So at the end of the day use your common sense and think clearly which functionalities make sense to be seperated and which not. Also don’t overdo it. Scripts with a few hundred lines of code are absolutely fine but once you have >~500 lines you may want to think about seperating them.