A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4#if UNITY_EDITOR
5using UnityEditor.SceneManagement;
6#endif
7using UnityEngine;
8using UnityEngine.SceneManagement;
9using UnityObject = UnityEngine.Object;
10
11namespace Unity.VisualScripting
12{
13 /// <remarks>
14 /// Does not support objects hidden with hide flags.
15 /// </remarks>
16 public static class SceneSingleton<T> where T : MonoBehaviour, ISingleton
17 {
18 static SceneSingleton()
19 {
20 instances = new Dictionary<Scene, T>();
21
22 attribute = typeof(T).GetAttribute<SingletonAttribute>();
23
24 if (attribute == null)
25 {
26 throw new InvalidImplementationException($"Missing singleton attribute for '{typeof(T)}'.");
27 }
28 }
29
30 private static Dictionary<Scene, T> instances;
31
32 private static readonly SingletonAttribute attribute;
33 private static bool persistent => attribute.Persistent;
34 private static bool automatic => attribute.Automatic;
35 private static string name => attribute.Name;
36 private static HideFlags hideFlags => attribute.HideFlags;
37
38 private static void EnsureSceneValid(Scene scene)
39 {
40 if (!scene.IsValid())
41 {
42 throw new InvalidOperationException($"Scene '{scene.name}' is invalid and cannot be used in singleton operations.");
43 }
44 }
45
46 public static bool InstantiatedIn(Scene scene)
47 {
48 EnsureSceneValid(scene);
49
50 if (Application.isPlaying)
51 {
52 return instances.ContainsKey(scene);
53 }
54 else
55 {
56 return FindInstances(scene).Length == 1;
57 }
58 }
59
60 public static T InstanceIn(Scene scene)
61 {
62 EnsureSceneValid(scene);
63
64 if (Application.isPlaying)
65 {
66 if (instances.ContainsKey(scene))
67 {
68 return instances[scene];
69 }
70 else
71 {
72 return FindOrCreateInstance(scene);
73 }
74 }
75 else
76 {
77 return FindOrCreateInstance(scene);
78 }
79 }
80
81 private static T[] FindObjectsOfType()
82 {
83#if UNITY_2023_1_OR_NEWER
84 return UnityObject.FindObjectsByType<T>(FindObjectsSortMode.None);
85#else
86 return UnityObject.FindObjectsOfType<T>();
87#endif
88 }
89
90 private static T[] FindInstances(Scene scene)
91 {
92 EnsureSceneValid(scene);
93
94 // Fails here on hidden hide flags
95 return FindObjectsOfType().Where(o => o.gameObject.scene == scene).ToArray();
96 }
97
98 private static T FindOrCreateInstance(Scene scene)
99 {
100#if UNITY_EDITOR
101 var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
102
103 var targetScene = prefabStage == null ? scene : SceneManager.GetActiveScene();
104#else
105 var targetScene = scene;
106#endif
107
108 EnsureSceneValid(targetScene);
109
110 var instances = FindInstances(targetScene);
111
112 if (instances.Length == 1)
113 {
114 return instances[0];
115 }
116 else if (instances.Length == 0)
117 {
118 if (automatic)
119 {
120 // Because DontDestroyOnLoad moves objects to another scene internally,
121 // the scene singleton system does not support persistence (which wouldn't
122 // make sense logically either!)
123 if (persistent)
124 {
125 throw new UnityException("Scene singletons cannot be persistent.");
126 }
127
128 // Create the parent game object with the proper hide flags
129 var singleton = new GameObject(name ?? typeof(T).Name);
130 singleton.hideFlags = hideFlags;
131
132 // Immediately move it to the proper scene so that Register can determine dictionary key
133 SceneManager.MoveGameObjectToScene(singleton, targetScene);
134
135 // Instantiate the component, letting Awake register the instance if we're in play mode
136 var instance = singleton.AddComponent<T>();
137 instance.hideFlags = hideFlags;
138
139 return instance;
140 }
141 else
142 {
143 throw new UnityException($"Missing '{typeof(T)}' singleton in scene '{scene.name}'.");
144 }
145 }
146 else // if (instances.Length > 1)
147 {
148 throw new UnityException($"More than one '{typeof(T)}' singleton in scene '{scene.name}'.");
149 }
150 }
151
152 public static void Awake(T instance)
153 {
154 Ensure.That(nameof(instance)).IsNotNull(instance);
155
156 var scene = instance.gameObject.scene;
157
158 EnsureSceneValid(scene);
159
160 if (instances.ContainsKey(scene))
161 {
162 throw new UnityException($"More than one '{typeof(T)}' singleton in scene '{scene.name}'.");
163 }
164
165 instances.Add(scene, instance);
166 }
167
168 public static void OnDestroy(T instance)
169 {
170 Ensure.That(nameof(instance)).IsNotNull(instance);
171
172 var scene = instance.gameObject.scene;
173
174 // If the scene is invalid, it has probably been unloaded already
175 // (for example the DontDestroyOnLoad pseudo-scene), so we can't
176 // access it. However, we'll need to check in the dictionary by
177 // value to remove the entry anyway.
178 if (!scene.IsValid())
179 {
180 foreach (var kvp in instances)
181 {
182 if (kvp.Value == instance)
183 {
184 instances.Remove(kvp.Key);
185
186 break;
187 }
188 }
189
190 return;
191 }
192
193 if (instances.ContainsKey(scene))
194 {
195 var sceneInstance = instances[scene];
196
197 if (sceneInstance == instance)
198 {
199 instances.Remove(scene);
200 }
201 else
202 {
203 throw new UnityException($"Trying to destroy invalid instance of '{typeof(T)}' singleton in scene '{scene.name}'.");
204 }
205 }
206 else
207 {
208 throw new UnityException($"Trying to destroy invalid instance of '{typeof(T)}' singleton in scene '{scene.name}'.");
209 }
210 }
211 }
212}