官术网_书友最值得收藏!

Singleton Components

As mentioned previously, Static Classes have difficulty interfacing with Unity-related functionality, and cannot directly make use of MonoBehaviour features, such as event callbacks, Coroutines, hierarchical design, and Prefabs. Also, since there's no object to select in the Inspector window, we lose the ability to inspect the a Static Class' data at runtime through the Inspector window, which can make debugging difficult. These are features that we may wish to make use of in our global classes.

A common solution to this problem is to implement a Component that acts like a Singleton--it provides static methods to grant global access, and only one instance of the MonoBehaviour is ever allowed to exist at any given time.

The following is the definition for a SingletonComponent class:

using UnityEngine;

public class SingletonComponent<T> : MonoBehaviour where T : SingletonComponent<T> {
private static T __Instance;

protected static SingletonComponent<T> _Instance {
get {
if(!__Instance) {
T [] managers = GameObject.FindObjectsOfType(typeof(T)) as T[];
if (managers != null) {
if (managers.Length == 1) {
__Instance = managers[0];
return __Instance;
} else if (managers.Length > 1) {
Debug.LogError("You have more than one " +
typeof(T).Name +
" in the Scene. You only need " +
"one - it's a singleton!");
for(int i = 0; i < managers.Length; ++i) {
T manager = managers[i];
Destroy(manager.gameObject);
}
}
}
GameObject go = new GameObject(typeof(T).Name, typeof(T));
__Instance = go.GetComponent<T>();
DontDestroyOnLoad(__Instance.gameObject);
}
return __Instance;
}
set {
__Instance = value as T;
}
}
}

This class works by creating a GameObject containing a Component of itself the first time it is accessed. Since we wish this to be a global and persistent object, we will need to call DontDestroyOnLoad() shortly after the GameObject is created. This is a special function that tells Unity that we want the object to persist between scenes for as long as the application is running. From that point onward, when a new Scene is loaded, the object will not be destroyed and will retain all of its data.

This class definition assumes two things. Firstly, because it is using generics to define its behavior, it must be derived from in order to create a concrete class. Secondly, a method must be defined to assign the _Instance property (which in turns sets the private __Instance  field) and cast it to/from the correct class type.

For example, the following is the minimum amount of code that is needed to successfully generate a new SingletonComponent derived class called EnemyManagerSingletonComponent:

public class EnemyManagerSingletonComponent : SingletonComponent< EnemyManagerSingletonComponent > {
public static EnemyManagerSingletonComponent Instance {
get { return ((EnemyManagerSingletonComponent)_Instance); }
set { _Instance = value; }
}

public void CreateEnemy(GameObject prefab) {
// same as StaticEnemyManager
}

public void KillAll() {
// same as StaticEnemyManager
}
}

This class can be used at runtime by having any other object access the Instance property at any time. If the Component does not already exist in our Scene, then the SingletonComponent base class will instantiate its own GameObject and attach an instance of the derived class to it as a Component. From that point forward, access through the Instance property will reference the Component that was created, and only one instance of this Component will exist at a time.

Note that this means we don't need to implement static methods in a Singleton Component class definition. For example, we could simply call EnemyManagerSingletonComponent.Instance.KillAll() to access the KillAll() method. 

Note that it is possible to place an instance of a SingletonComponent in a Hierarchy window since it derives from MonoBehaviour. Although, be warned that the DontDestroyOnLoad() method would never be called, which would prevent the Singleton Component's GameObject from persisting when the next Scene is loaded. We will perhaps need to call DontDestroyOnLoad() in the derived class' Awake() callback to make this work, unless, of course, we actually want destructible Singletons. Sometimes it makes sense to allow such Singletons to be destroyed between scenes so that it can start fresh each time; it all depends on our particular use cases.

In either case, shutdown of a Singleton Component can be a little convoluted because of how Unity tears down scenes. An object's OnDestroy() callback is called whenever it is destroyed during runtime. The same method is called during application shutdown, whereby every Component on each GameObject has its OnDestroy() callback called by Unity. The same activities take place when we end Play Mode in the Editor, thus returning to Edit Mode. However, destruction of objects occurs in a random order, and we cannot assume that the SingletonComponent object will be the last object destroyed.

Consequently, if any object attempts to do anything with the Singleton Component in the middle of their OnDestroy() callback, then they may be calling the SingletonComponent object's Instance property. However, if the Singleton Component has already been destroyed prior to this moment, then a new instance of the SingletonComponent will be created in the middle of application shutdown. This can corrupt our Scene files, as instances of our Singleton Components will be left behind in the Scene. If this happens, then Unity will throw the following error message:

"Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)"

The obvious workaround is to simply never call into a SingletonComponent object during any MonoBehaviour Component's OnDestroy() callback. However, there are some legitimate reasons we may wish to do so: most notable is that Singletons are often designed to make use of the Observer design pattern. This design pattern allows other objects to register/deregister with them for certain tasks, similar to how Unity latches onto callback methods, such as Start() and Update(), but in a more strict fashion.

With the Observer design pattern, objects will typically register with the system when they are created, will make use of it during runtime, and then either deregister from it during runtime when they are finished using it or deregister during their own shutdown for the sake of cleanup. We will see an example of this design pattern in the upcoming section, A global Messaging System, but if we imagine a MonoBehaviour making use of such a system, then the most convenient place to perform shutdown deregistration would be within an OnDestroy() callback. Consequently, such objects are likely to run into the aforementioned problem, where a new GameObject for the SingletonComponent is accidentally created during application shutdown.

To solve this problem, we will need to make three changes. Firstly, we need to add an additional flag to the SingletonComponent, which keeps track of its active state and disable it at the appropriate times. This includes the Singleton's own destruction, as well as application shutdown (OnApplicationQuit() is another useful Unity callback for MonoBehaviours, which is called during this time):

private bool _alive = true;
void OnDestroy() { _alive = false; }
void OnApplicationQuit() { _alive = false; }

Secondly, we should implement a way for external objects to verify the Singleton's current state:

public static bool IsAlive {
get {
if (__Instance == null)
return false;
return __Instance._alive;
}
}

Finally, any object that attempts to call into the Singleton during its own OnDestroy() method must first verify the state using the IsAlive property before calling instance, as follows:

public class SomeComponent : MonoBehaviour {
void OnDestroy() {
if (MySingletonComponent.IsAlive) {
MySingletonComponent.Instance.SomeMethod();
}
}
}

This will ensure that nobody attempts to access the Singleton instance during destruction. If we don't follow this rule, then we will run into problems where instances of our Singleton object will be left behind in the Scene after returning to Edit Mode.

The irony of the SingletonComponent approach is that we are using a Find() call to determine whether or not one of these SingletonComponent objects already exists in the Scene before we attempt to assign the __Instance reference variable. Fortunately, this will only happen when the Singleton Component is first accessed, which is usually not a problem if there aren’t too many GameObjects in the Scene, but it's possible that the initialization of the Singleton Component may not necessarily occur during Scene initialization and can, therefore, cost us a performance spike at a bad moment during gameplay when an instance is first acquired and Find() gets called. The workaround for this is to have some God Class confirm that the important Singletons are instantiated during Scene initialization by simply accessing the Instance property on each one.

Another downside to this approach is that if we later decide that we want more than one of these Singletons executing at once or we wish to separate out its behavior to be more modular, then there would be a lot of code that needs to change.

The final approach we will explore will attempt to solve many of the problems revealed by the previous solutions and provide a way to gain all of their benefits, by combining ease of implementation, ease of extension, and strict usage that also reduces the likelihood of human-error during configuration.

主站蜘蛛池模板: 融水| 靖江市| 苍梧县| 东至县| 玛纳斯县| 和平县| 呼伦贝尔市| 屏东市| 大冶市| 南木林县| 龙江县| 治县。| 南部县| 宣化县| 哈尔滨市| 武汉市| 内黄县| 哈尔滨市| 宜良县| 平乡县| 道真| 宕昌县| 杂多县| 恩施市| 分宜县| 浦城县| 静海县| 左云县| 名山县| 绥中县| 镇赉县| 神木县| 常宁市| 广汉市| 左权县| 衡阳市| 沙雅县| 民权县| 桃园县| 于都县| 海口市|