Unity and accessing common MonoBehaviour manager classes
Tuesday, March 22, 2011 at 6:31PM I've started a few Unity codebases over the last couple of years and one of the first things you want to do is work out how to access your MonoBehaviour 'Manager' classes attached to gameobjects from other scripts.
Now, the manager-type scripts that are attached to our Unity GameObjects are essentially Singletons. I usually call them 'MonoBehaviour Singletons' because they usually exist just once and need to be accessed from all over the place. I'm talking here about things like a class that manages all the playing of sound for a particular scene. You create a script called, for example, SoundManager and attach it to a GameObject in the Unity Editor and it is the single point of sound management throughout your scene. What I'm talking about here is how to access that script from wherever you need it.
So here is our little SoundManager class that we want to access. It just has a single method, PlayClickSound().
public class SoundManager : MonoBehaviour
{
public void PlayClickSound()
{
}
}
Direct Access
Now of course, the 'easiest' way to access that script is via a call to GameObject.FindObjectOfType or, if you have several classes of the same type, GameObject.FindWithTag where you have 'tagged' each gameobject differently.
(GameObject.FindObjectOfType(typeof(SoundManager)) as SoundManager).PlayClickSound();
// or
(GameObject.FindWithTag("Sounds").GetComponent(typeof(SoundManager)) as SoundManager).PlayClickSound();
But, calls to these methods are expensive and you absolutely do not want to be calling them often.
Keep a Reference
The first solution that is in the documentation is to call those expensive methods once in the Awake or Start method of each script that needs them, then store the reference for all further calls.
public class TestClass : MonoBehaviour
{
SoundManager mySoundManager;
bool needToPlaySound;
void Awake()
{
this.mySoundManager = GameObject.FindObjectOfType(typeof(SoundManager)) as SoundManager;
}
void Update()
{
if (this.needToPlaySound)
{
this.mySoundManager.PlayClickSound();
}
}
}
From a performance perspective, that's pretty much all you'd need to do (unless for example you are doing this from a 'bullet' script and creating many bullets a second) and I'm sure you all know that, but I'm looking at it from a maintainability and ease-of-use perspective. With even a small project, the code above is going to be copied many times.
Singleton Manager
In my opinion, the lesser the code the better. So, what's next? Create a static class that holds this code that we can access from anywhere. Since this class is controlling our access to the MonoBehaviour 'Singleton' manager classes, I've called it the SingletonManager
public class SingletonManager
{
public static SoundManager GetSoundManager()
{
return GameObject.FindObjectOfType(typeof(SoundManager)) as SoundManager;
}
}
Then in our classes we can access it like this:
public class TestClass : MonoBehaviour
{
SoundManager mySoundManager;
bool needToPlaySound;
void Awake()
{
this.mySoundManager = SingletonManager.GetSoundManager();
}
void Update()
{
if (this.needToPlaySound)
{
this.mySoundManager.PlayClickSound();
}
}
}
This is better in my option, but still not perfect. There is still a lot of code repeated in each of our accessing classes. The less code in our classes, the easier it is to see what is happening and the easier the maintain the code.
Singleton Singleton Manager
So, another option is to make our SingletonManager a classic singleton and have it store the references to all the MonoBehaviour singletons.
public class SingletonManager
{
private static SingletonManager instance;
public static SingletonManager Instance
{
get
{
if (instance == null)
{
instance = new SingletonManager();
}
return instance;
}
}
private SoundManager soundManager;
public SoundManager SoundManager
{
get
{
if (this.soundManager == null)
{
this.soundManager = GameObject.FindObjectOfType(typeof(SoundManager)) as SoundManager;
}
return this.soundManager;
}
}
}
Then we can access it like this:
public class TestClass : MonoBehaviour
{
bool needToPlaySound;
void Update()
{
if (this.needToPlaySound)
{
SingletonManager.Instance.SoundManager.PlayClickSound();
}
}
}
Note: I'm not touching on thread-safety here for simplicity but it shouldn't matter to most people. If you're doing anything with threads you'd better Google "threadsafe singleton"
This way we have concentrated the code accessing the MonoBehaviour singletons in one place. The Awake method can be used for more essential code needed there, and in many cases can be removed all together.
Interfaces - Use 'em
Now one final problem is that these MonoBehaviour singletons we are exposing with the SingletonManager are (obviously) extending the MonoBehaviour class, which is a huge class with many methods on it. This means that you have way too much access to that class and its attached GameObject that you need.
For example, you can type SingletonManager.Instance.SoundManager.transform.rotation = ... and suddenly you're able to change the rotation of the SoundManager's GameObject from anywhere in the code. From a good coding viewpoint this is not a good thing. The SoundManager should only let external classes have access to a very limited number of methods. That's where interfaces can save you.
So, instead of having the SingletonManager expose the SoundManager class directly, we create an interface ISoundManager and expose that.
public interface ISoundManager
{
void PlayClickSound();
}
Then our SoundManager will use the interface like this:
public class SoundManager : MonoBehaviour, ISoundManager
{
public void PlayClickSound()
{
}
}
And the SingletonManager will look like this:
public class SingletonManager
{
private static SingletonManager instance;
public static SingletonManager Instance
{
get
{
if (instance == null)
{
instance = new SingletonManager();
}
return instance;
}
}
private SoundManager soundManager;
public ISoundManager SoundManager
{
get
{
if (this.soundManager == null)
{
this.soundManager = GameObject.FindObjectOfType(typeof(SoundManager)) as SoundManager;
}
return this.soundManager;
}
}
}
Much better. Now, when something screwy is happening to our SoundManager, we know exactly what the external classes are capable of doing to it. Plus it essentially forces the coder to think about what relationship our MonoBehaviour singleton classes have with each other. If you need to do something more you have to add it to the interface, rather than hack away at whatever properties you want.
I highly recommend using interfaces in this way. You will thank yourself as your codebase grows.
Sam Cox |
3 Comments |
C#,
Interfaces,
Singletons,
Unity 