Use static classes
Static classes are great for whenever you need broad access to some subsystem of your program, and they're widely used in games as a result.
/// <summary>Save/load is a good example because it
/// doesn't need any settings and it's
/// useful to call it from almost anywhere.</summary>
public static class GameSaver {
/// <summary>I save the game when I'm run.</summary>
public static void Save() {
// Save the game!
}
}
To use a static class, you just use the member directly - for example GameSaver.Save();
will work from "anywhere". Properties, fields, events etc can all be made static, but see the notes below first.
This is the easiest way of avoiding some kind of "god class" - that appears to be what you're describing (and yes, they are often a code disaster!) - that's a class which is excessively complicated and does everything. Break it up into a series of small, self contained modules.
Don't overuse static fields though!
Use a singleton for that.
Particularly in gaming, it's really common to have things that are only instanced once (like, say, the player or the audio system) which also needs to be easy to reset or has a large number of properties.
It's important to not have them all as static fields - that would be difficult to reset and hard to debug. That's where you'd use a static field and instance an ordinary class - this is called a singleton:
/// <summary>There's only ever one background music source!
/// It has instance properties though (i.e. an AudioSource)
/// so it works well as a singleton.</summary>
public class BackgroundMusic {
/// <summary>The static field - use the Play method from anywhere.</summary>
private static BackgroundMusic Current;
/// <summary>Plays the given clip.</summary>
public static void Play(AudioClip clip) {
if (Current == null) {
// It's not been setup yet - create it now:
Current = new BackgroundMusic();
}
// E.g. Current.Source.Play(clip);
}
public BackgroundMusic() {
// Instance a source now.
}
}
This just means BackgroundMusic.Play(..);
is available from anywhere. This kind of approach means that you don't need to setup anything in the inspector - just calling that method sets itself up on demand.
When MonoBehaviour is great
It's common to think that all code must be a MonoBehaviour and must be attached to a gameobject. That's not how Unity actually works; that just results in more work for whoever is using the editor when everything is a MonoBehaviour and must all be manually instanced and connected up.
To be clear, I'm not saying don't use MonoBehaviour at all. Rather, you should use an appropriate combination of the component model and static depending on what the code actually represents.
In general:
- If there is only one instance of something, use a singleton.
- But if there's only one and it has properties that are useful to edit in the inspector, use a MonoBehaviour and keep a reference to the single object as a static field too.
An example of that would be the player (in a single player game) with a range of default settings that you'd like to vary. You would setup the player as a prefab and have some kind of PlayerSettings.Current
static field which references the current instance:
/// <summary>Add this to a player prefab.</summary>
public class PlayerSettings : MonoBehaviour{
/// <summary>Still following the singleton pattern.</summary>
public static PlayerSettings Current;
/// <summary>Player speed. This can be edited in the inspector.</summary>
public float Speed;
public void Awake() {
// Update the static field:
Current = this;
}
}
This kind of approach gives a best of both worlds - you can use PlayerSettings.Current
from anywhere (after the player prefab has been instanced) without having to give up the inspector. It's also much easier to refactor than something like GameObject.Find("Player/Body").GetComponent<PlayerSettings>();
too, making it easier to maintain.
Multiple instances
If there are multiple instances of something, like NPC's, then generally you'd always use prefabs with MonoBehaviour's there. However, using static methods can be extremely useful with those:
public class NPC : MonoBehaviour{
/// <summary>Gets an NPC by their name.</summary>
public static NPC Locate(string name){
// E.g. get all GameObject instances with an NPC component.
// Return the first one which has a 'Name' value that matches.
}
/// <summary>The name of this NPC (editable in the inspector).
public string Name;
}
NPC.Locate("Dave");
becomes fairly self-explanatory as to what it's actually expected to do.