A lot of that looks good.
Singleton -
this can be very useful, as long as you understand its short falls. Really a Singleton is a lot like a static class, only that it has object identity (it can be referenced as an object, useful for functions that need to take a reference to something). So just like a static class, it means you have a ‘global state’, which is considered a giant no-no to some people.
Thing is, in game design… we cut corners sometimes and forego into no-no land, because to not is a lot more leg work.
And the thing is, if your requirements are a single point of authority, and you must maintain that single point… then doing something else is just as prone to problems as well.
And for something like a GameManager, where there is really only ONE game ever… it’s a completely legitimate design approach.
So yeah, Singleton away with it. Heck, I use a Singleton for my SPSceneManager as well.
Dispatching events -
C# events are decent for this as well.
The Singleton for the GameManager makes it an easy way to reference it and register for said events.
Only downside is that you can’t really register for the event until Awake or Start, which means depending ordering, you might fail to register for the event before it’s actually raised. So make sure you plan for this… you can’t let the GameManager raise the event until you know for certain that the scene is loaded and ready to receive the event.
Ability to meet some conditions to signal to the GameManager to change game states
This one here I’m not sure about.
So I’m assuming this is for between ‘SETUP’ and ‘START’. A sort of ‘loading’ time.
And the thing is that loading could take a while, several frames, if it has to do things like asychronously load data from disk or the web or something.
This part of the design can be a bit tough.
So in my SPSceneManager you’ll notice I have some code that does this:
https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Scenes/SPSceneManager.cs
In LoadScene I do this:
public IProgressingYieldInstruction LoadScene(ISceneLoadOptions options)
{
if (options == null) throw new System.ArgumentNullException("options");
//here I'm just canceling out any previous loads... this is incase someone tries to load a new scene mid load
if (_loadingOp != null)
{
_loadingOp.Cancel();
if (!_loadingOp.NextSceneStarted) _currentSceneBehaviour = null;
_loadingOp = null;
}
//start the load process
_lastSceneBehaviour = _currentSceneBehaviour;
_currentSceneBehaviour = null;
_loadingOp = new WaitForSceneLoaded();
_loadingOp.Start(this, options, _lastSceneBehaviour);
return _loadingOp;
}
I create a process called ‘WaitForSceneLoaded’, and it’s a nested class in the SPSceneManager, and it does this:
private class WaitForSceneLoaded : RadicalYieldInstruction, IProgressingYieldInstruction
{
#region Fields
private SPSceneManager _manager;
private ISceneLoadOptions _options;
private ISceneBehaviour _lastScene;
private IProgressingYieldInstruction _loadOp;
private bool _started;
private RadicalCoroutine _routine;
#endregion
#region Properties
public SPSceneManager Manager { get { return _manager; } }
public ISceneLoadOptions LoadOptions { get { return _options; } }
public ISceneBehaviour LastScene { get {return _lastScene; } }
public bool NextSceneStarted { get { return _started; } }
#endregion
#region Methods
public void Start(SPSceneManager manager, ISceneLoadOptions options, ISceneBehaviour lastScene)
{
//set the state variables needed during loading
_manager = manager;
_options = options;
_lastScene = lastScene;
//run the coroutine that loads stuff
_routine = manager.StartRadicalCoroutine(this.DoLoad()); //GameLoopEntry.Hook.StartRadicalCoroutine(this.DoLoad(), RadicalCoroutineDisableMode.Default);
}
public void Cancel()
{
if (_routine != null)
{
_routine.Cancel();
_routine = null;
}
//this is because this can be used async, signals are for thread merging
this.SetSignal();
}
private System.Collections.IEnumerator DoLoad()
{
//allow the last scene to do any extra clean up it may need to do before loading the next scene
if(_lastScene != null)
{
//end last scene
var endInstruction = _lastScene.EndScene();
if (endInstruction != null) yield return endInstruction;
if(_manager._lastSceneBehaviour == _lastScene) _manager._lastSceneBehaviour = null;
}
//the event args for communicating back and for between this and handlers
var args = new SceneLoadingEventArgs(_manager, _options);
object[] instructions;
//signal about to load
_options.OnBeforeSceneLoaded(_manager, args);
_manager.OnBeforeSceneLoaded(args);
//here we get all the yield args from the handlers, so that we can wait until gameobjects in the scene are done
if (args.ShouldStall(out instructions)) yield return new WaitForAllComplete(GameLoopEntry.Hook, instructions);
//do load
var scene = _options.GetScene(_manager);
_loadOp = (scene != null) ? scene.LoadAsync() : RadicalYieldInstruction.Null;
yield return _loadOp;
//get scene behaviour
var sceneBehaviour = _options.LoadCustomSceneBehaviour(_manager);
if(sceneBehaviour == null)
{
var go = new GameObject("SceneBehaviour");
go.transform.parent = _manager.transform;
go.transform.localPosition = Vector3.zero;
sceneBehaviour = go.AddComponent<SceneBehaviour>();
}
SceneBehaviour.SceneLoadedInstance = sceneBehaviour;
_manager._currentSceneBehaviour = sceneBehaviour;
//signal loaded
_options.OnSceneLoaded(_manager, args);
_manager.OnSceneLoaded(args);
if (args.ShouldStall(out instructions)) yield return new WaitForAllComplete(GameLoopEntry.Hook, instructions);
else yield return null; //wait one last frame to actually begin the scene
//signal scene begun
_started = true;
var beginInstruction = sceneBehaviour.BeginScene();
_options.OnSceneStarted(_manager, args);
_manager.OnSceneStarted(args);
if (args.ShouldStall(out instructions))
{
var waitAll = new WaitForAllComplete(GameLoopEntry.Hook, instructions);
if (beginInstruction != null)
waitAll.Add(beginInstruction);
beginInstruction = waitAll;
}
if (beginInstruction != null) yield return beginInstruction;
_manager._loadingOp = null;
this.SetSignal();
}
#endregion
#region IProgressAsyncOperation Interface
public float Progress
{
get
{
if (this.IsComplete)
return 1f;
else if (_loadOp == null)
return 0f;
else
return _loadOp.Progress * 0.99f;
}
}
#endregion
}
So really, this is a Coroutine… it’s a coroutine inside a class so that I can store some state information as well.
But when I call Start on it in LoadScene, really it’s just starting this coroutine and working its way through it.
The coroutine is the ‘DoLoad’ method inside ‘WaitForSceneLoaded’. It calls through to the ‘SceneLoadOptions’ so that it can do work (remember this is the object that actually tells the manager what/how to load). As each event occurs, it passes out ‘SceneLoadingEventArgs’ (standard C# event design here), which the handlers of said event can call the method ‘RequestManagerToStall’ on:
https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/Scenes/SceneLoadingEventArgs.cs
If no one calls that message, the manager moves on to the next step in the loading process. Otherwise it yields until any yield instructions that the handlers stalled on are complete.
So basically if any handlers of one of these events needs to load with a WWW or something, it starts a coroutine in the event handler, and it calls ‘RequestManagerToStall’ passing in the coroutine as the instruction to stall on. The manager will then stall for as long as those objects wait.
void OnBeforeSceneLoaded(object sender, SceneLoadingEventArgs e)
{
e.RequestManagerToStall(this.StartCoroutine(this.DoLoad()));
}
void DoLoad()
{
//load stuff from the internet
}
It does this for every intermediate load event.
OnBeforeSceneLoaded
OnSceneLoaded
OnSceneStarted
Now, of course you may be looking at my code and be wondering how the hell of all that greek of mine is doing that.
And this is because a few reasons…
-
my code is heavily using my framework of tools, I have a lot of tools that do work for me, my way. If your not familiar with it, it might look weird.
-
It’s heavily abstracted. I’m attempting to deal with every which unknown… honestly, when used, really only one of those events is really ever used for any logic. I just have them all there just in case I need to hook in somewhere weird.