Thursday, November 10, 2016

xna - How to chain actions/animations together and delay their execution?


I'm trying to build a simple game with a number of screens - 'TitleScreen', 'LoadingScreen', 'PlayScreen', 'HighScoreScreen' - each of which has it's own draw & update logic methods, sprites, other useful fields, etc. This works well for me as a game dev beginner, and it runs.


However, on my 'PlayScreen' I want to run some animations before the player gets control - dropping in some artwork, playing some sound effects, generally prettifying things a little. However, I'm not sure what the best way to chain animations / sound effects / other timed general events is.


I could make an intermediary screen, 'PrePlayScreen', which simply has all of this hardcoded like so:



Update(){
Animation anim1 = new Animation(.....);
Animation anim2 = new Animation(.....);
anim1.Run();
if(anim1.State == AnimationState.Complete)
anim2.Run();
if(anim2.State == AnimationState.Complete)
// Load 'PlayScreen' screen
}


But this doesn't seem so great - surely their must be a better way? I then thought, 'Hey - an AnimationManager! That'd be awesome!'. But then that creeping OOP panic set in as I thought about it some more.


If I create the Animation in my Screen, then add it to the AnimationManager (which may or may not be a GameComponent hooked up to Update/Draw), how can I get 'back' to it? To signal commands like start / end / repeat? I'd still need to keep a reference to the object in my Screen so that I could still communicate with it once it's buried in the bosom of a List in my AnimationManager. This seems bad.


I've also tried using events - call 'Update' on all the animations in the PlayScreen update loop, but crucially all of the animations have a bool flag ('Active') which determines whether they should begin. The first animation has this set to 'true', all others 'false'. On completion the first animation raises an event, which sets animation 2's bool flag to true (and so it then runs). Once animation 2 is complete another 'anim complete' event is raised, and the screen state changes.


Considering the game I'm making is basically as simple as it gets I know I'm overthinking this... it's just the paradigm shift from web -> game development is making me break out in a serious case of the stupids.



Answer



The Basic Framework


Here's my solution to this common problem. First let's choose a name for an action that spans several frames, such as an animation. I'll call it a Process which is the terminlogy I found in this book, but don't mistake it with the processes on your OS, it's a different concept. In this context, a process is anything that needs to be updated, possibly several times, before it completes.


I'll start by defining a base class for our process. Something as simple as this is enough (I kept the name as Process for consistency with the discussion above, but since .NET already has a class named like that, you might want to give it another name):


public abstract class Process
{

public abstract void Update(float elapsed);
public bool Finished { get; set; }
}

Now let's create a class that serves as a process queue. You add processes to this queue, and they get executed in turn. This is where my implementation differs from the one in the book above. In the book every process is updated every frame, but in this case I'll make them sequential instead, so that the second process only starts when the first one ends. Once again it's a simple class:


public class ProcessQueue
{
public void Add(Process process)
{
_processQueue.Add(process);

}

public void Update(float elapsed)
{
if (_processQueue.Count > 0)
{
_processQueue[0].Update(elapsed);
if(_processQueue[0].Finished)
{
_processQueue.RemoveAt(0);

}
}
}

private readonly List _processQueue = new List();
}

And that's it, although you're free to add more methods, for example to clear the queue or to know how many processes are currently queued. Now just make your Animation class inherit from Process and chain them like:


// On startup
var processQueue = new ProcessQueue();


// Chain a few animations
processQueue.Add(new Animation(...));
processQueue.Add(new Animation(...));
processQueue.Add(new Animation(...));

// On Update
processQueue.Update(elapsed);

Note that you can create and chain all the processes you want before the game starts, because they only really start being executed when you call update for the first time on the queue.



When implementing the Animation class inheriting from Process make sure to set the Finished property to true when the animation ends, so that it will get removed from the queue.


I think that probably already answers your question, but if you're interested in the subject, keep reading.




Bonus - Some Useful Processes!


I might as well drop some useful types of processes for pretty much any type of game! Hopefully these will inspire you to make your own processes too.


First we have the SleepProcess which as the name implies, does nothing for a certain duration of time:


public class SleepProcess : Process
{
public SleepProcess(float duration)
{

_duration = duration;
}

public override void Update(float elapsed)
{
_timer += elapsed;
if(_timer >= _duration)
Finished = true;
}


private float _timer;
private readonly float _duration;
}

And sometimes you don't need a process that spans several frames - a single frame is enough. The following GenericProcess makes that easy:


public class GenericProcess : Process
{
public GenericProcess(Action action)
{
_action = action;

}

public override void Update(float elapsed)
{
_action();
Finished = true;
}

private readonly Action _action;
}


Mixing Everything Together - An Example


Here's how you can use these processes. Let's say for instance that you would like to play animation A, wait 2 seconds, play animation B, and finally write a message to the debugger. By using the processes above you can implement this entire sequence as four lines of code:


processQueue.Add(new AnimationProcess(...));
processQueue.Add(new SleepProcess(2));
processQueue.Add(new AnimationProcess(...));
processQueue.Add(new GenericProcess(() => Diagnostics.Debug("Message")));

And the only thing you need to do is remember to call Update on the process queue. Can you see how powerful this concept can be when properly used? Pretty much everything in your game can be considered as a process!


What About Looping?



What if I wanted to take the example above and make it loop infinitely? Well, this too, is extremely simple to implement with this framework, using a sort of "delayed recursive" call. Just put the processes you want to loop inside a function, and then add a GenericProcess at the end that adds that calls that function once more:


void LoopProcesses()
{
processQueue.Add(new AnimationProcess(...));
processQueue.Add(new SleepProcess(2));
processQueue.Add(new AnimationProcess(...));
processQueue.Add(new GenericProcess(() => Diagnostics.Debug("Message")));

// Call LoopProcesses again after all of the above are finished
processQueue.Add(new GenericProcess(LoopProcesses));

}

// Call once to start looping
LoopProcesses();

No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...