The interesting thing about game programming is that our flow isn’t usually just line after line of code executed in order. Instead we have to start something off, let it run across multiple frames, keeping track of the state, checking for when we need to transition to the next state, and so on. The logic is spread out over time rather than all sequential like “normal” programming.
One good way of dealing with this is behavior trees. They are very powerful and there is a place for them (and my current work in progress is heavily dependent on them). But they are just a single tool in the whole toolbox.
I mentioned state before for a reason. The trick to all of this is the need for executing a specific piece of code based on the current “state”. You may have heard of State Machine which is another valuable tool in your game programming arsenal. Do some reading about those.
So you can think of state as a step in a process. We can divide your example into three distinct steps. Run The Game, Wait For Audio Completion, Quit To Menu. (we could add a couple more in there, but this will get the point across).
So lets turn your code into a simple state machine. A good way to track the current step we’re on is to use an enum, which is basically a fancy wrapper around a list of numbers that each represent a different value. Our enum will have 3 values that correspond to our three steps: RunGame, WaitForAudio, QuitToMenu.
The basic idea behind the state machine is to execute the appropriate code for the current state, then check things to see if it’s time to switch to the next state or step, then start executing the code appropriate for that step, and so on. A good way to execute code based on a certain value is to use a “switch” statement. Something else for you to read about
So here’s some code that implements these ideas - it may or may not compile or run as is, but it gives you the basic idea and architecture and will hopefully get you pointed in a right direction.
class Test
{
enum State { RunGame, WaitForAudio, QuitToMenu }
public float RunTime = 120;
public float AudioTime = 5; // set this to the audio length, or set it in Start by looking up the audio length
State state;
float timer;
void SetState(State newState, float newTimer)
{
state = newState;
timer = newTimer;
}
void Start()
{
// set the game state to RunGame
SetState(State.RunGame, RunTime);
}
void Update()
{
// execute a different method depending on the current game state
// our initial state is RunGame (set in Start) so we'll be running RunGame initially
switch (state)
{
case State.RunGame:
RunGame();
break;
case State.WaitForAudio:
WaitForAudio();
break;
case State.QuitToMenu:
QuitToMenu();
break;
}
}
void RunGame()
{
// perform our actions while we're in the RunGame state
timer -= Time.deltaTime;
int timeRemaining = (int)timeFloat;
Timer.text = timeRemaining.ToString();
// check conditions for moving to the next state
// in this case, when the timer counts down to 0 then we want to
// start the audio and then set the state to WaitForAudio - our switch
// statement above will then start executing the WaitForAudio method
// next time Update runs
if (timer <= 0)
{
Timer.GetComponent<AudioSource>().Play();
SetState(State.WaitForAudio, AudioTime);
}
}
void WaitForAudio()
{
// perform our actions while we're in the WaitForAudio state
timer -= Time.deltaTime;
// check conditions for moving to the next state
// once the timer runs out we'll switch to QuitToMenu state
// and our switch statement will start executing that next
// time it runs
if (timer <= 0)
{
SetState(State.QuitToMenu, 0);
}
}
void QuitToMenu()
{
Application.LoadLevel("Quit menu");
}
}
Look at the switch statement in Update. This is one of the things I really like about state machines - those 3 lines there are almost like looking at “normal” code. A state executes until it’s done, then the next one does, then the next. It’s a really good way of making distributed code like this feel a little more like normal sequential code.
Now, here’s the cool thing. Coroutines are nothing more than beautiful syntactic sugar around that builds an internal state machine for you. The compiler interprets your yield statements as state transitions and builds internal structures automatically to track the current state and transition to the next state and so on.
So for your audio playing and waiting I would recommend just using a Coroutine, which would look something like the following code. You can kind of see the state transitions, and it looks even more like normal sequential code because the compiler is hiding all of the state transition internals from you. It’s beautiful and brings a tear to your eye.
IEnumerator PlayAudioAndQuit()
{
Timer.GetComponent<AudioSource>().Play();
yield return new WaitForSeconds(AudioTime);
Application.LoadLevel("Quit menu");
}
Your current code that starts the audio and loads the level would just be changed to do this…
StartCoroutine(PlayAudioAndQuit());
Coroutines aren’t always the answer either, but they’re perfect for this sort of “start something and wait for it to finish” thing.
Behavior Trees let you do all this sort of thing too, but with a lot more unnecessary overhead for simple things like this. They’re almost like having an entirely new scripting language on top of the C#. Useful and powerful, but not always the correct or most easily understood tool.