1
\$\begingroup\$

I'm working on a game that makes use of several singletons such as a game manager, audio manager, and a level loader. The game manager I created has references to the audio manager, level loader, and the player, so there's a high degree of coupling that I'm not sure is optimal:

private Player currentPlayer;
private CinemachineVirtualCamera CVC;
private AudioManagerScript AM;
private LevelLoader LL;

Is there a better way to write this to reduce coupling or is this kind of unavoidable?

\$\endgroup\$
2
  • 2
    \$\begingroup\$ You may get better/more actionable answers if you show some examples of how those references get used. Then folks can suggest alternative ways to achieve the same ends. \$\endgroup\$
    – DMGregory
    Commented Jan 17, 2023 at 13:54
  • 2
    \$\begingroup\$ Related question: Decoupling Components \$\endgroup\$
    – Philipp
    Commented Jan 17, 2023 at 16:07

1 Answer 1

3
\$\begingroup\$

Tight coupling between classes can often be avoided by using UnityEvents.

For example, let's say you want to play a sound when the player attacks. The tightly-coupled code would look something like this:

class Player : MonoBehaviour  {
    void Update() {
        if(Input.GetKeyDown("Attack")) {
             AudioManagerScript.Instance.PlayAttackSoundFor(gameObject);
        }
    }
}

This code has a tightly-coupled dependency between AudioManagerScript and Player. We don't want that. So what we can do instead, is this:

class Player : MonoBehaviour  {

    public UnityEvent<GameObject> onAttack;

    void Update() {
        if(Input.GetKeyDown("Attack")) {
             onAttack?.Invoke(gameObject);
        }
    }
}

Now the inspector for the player has an event "onAttack" which looks similar to this (reused a screenshot from another answer of mine to a different question).

Trigger Inspector

We can now use the inspector to bind this event to any number of methods from other gameObjects. We can use any method for this which receives a GameObject as their first parameter or which has no obligatory parameters.

The nice thing about this is that the Player doesn't need to know which (if any) other objects react to it attacking. And we can add as many objects as we want. For example, we could also delegate the spawining of a projectile to a separate class "ProjectileSpawner" that also subscribes to this event. And we can later refactor that class to use an object pool.

This also mean that the AudioManagerScript doesn't need to be a singleton anymore. It would be possible to have multiple instances of AudioManagerScript in the same scene and decide which one to use for what by binding the events of different objects to different managers.

And if the AudioManagerScript doen't really do more than calling the Play() method of the right AudioSource, then you might not even need the audio manager anymore, because you can just bind the Play method of the AudioSource to the event directly.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .