You might already have seen our animation tools in the Ronitech tools trailer I made a while ago, but that was only a very short fragment, so here is a more complete demo of the animation capabilities of the Ronitech animation editor:
Before I continue, I should note that although I designed most of this system, the real credit should go to Thijs (at the time a coding intern at Ronimo, now a gameplay programmer): he implemented it all, figured out the nitty-gritty details, and built the editor's animation UI.
With so many variables that can be animated, we need a good way on the coding side to hide the actual animation code from everything else. In fact, ideally I would like to be able to just mark a variable as being animatable, and that's it. Relatively simple object oriented design allowed us to do just that.
To make a value animatable, we write a class around it called AnimatedFloat, which handles the animation and contains the data needed for that. This class is updated every frame, and whenever the value is used, we just ask for it in that class. Using our AnimatedFloat in a gameplay object looks something like this:
class WorldObject { AnimatedFloat scale; AnimatedFloat rotation; TexturedRectangle* visualObject; void update(float time) { position.updateAnimation(time); rotation.updateAnimation(time); visualObject->setScale(scale.getCurrentValue()); visualObject->setRotation(rotation.getCurrentValue()); } }; |
This works simple enough, and AnimatedFloat itself is quite straightforward to implement like this. There are a couple of things in this bit of code that we could try to improve, though. One is those calls to getCurrentValue(). This function returns the value that the scale has at the current moment in time. If we use the scale variable a lot, it might be cumbersome to call getCurrentValue() all the time. Luckily, there is a nice (and rather obscure) solution for this in C++: we can skip calling that function by providing a conversion operator in the AnimatedFloat class. Like this:
class AnimatedFloat { float currentValue; void updateAnimation(float time) { //perform actual animation here and update currentValue } operator float() const { return currentValue; } }; |
The operator float() there is a conversion operator and tells C++ what to do if we use an AnimatedFloat in a spot where a float was expected. It removes the need for calling the function getCurrentValue().
This is a simple and nice solution. However, in practice we ended up not using it in our code, because our AnimatedFloat also has a function getBaseValue() (for animations that perform a periodical animation around a base value), and we felt it was not clear enough which value the conversion operator would return.
A bigger problem in our current design is that it is easy to forget to update one of the AnimatedFloats in a class. If we forget to update it, everything still works, but the object won't animate anymore. With the large number of AnimatedFloats some of our classes have, this is an easy oversight, and forgettig it might not become apparent until an artist actually tries to animate something and it doesn't work. I would prefer it if we could make it so that it is not possible for the programmer to forget updating a single variable.
I imagine there is a solution for this using macros, but I in general I think macros are not very readable, so we avoid them whenever possible.
The solution we came up with, is that values are updated by another class: the ObjectAnimator. The AnimatedFloat itself can only be created by providing an ObjectAnimator. Now the programmer only needs to update the ObjectAnimator and cannot forget about individual variables anymore. It is still possible to forget to update the ObjectAnimator, but then nothing at all will be animatable, so this is much less likely to be overlooked. Our design now looks like this:
class ObjectAnimator { std::vector |
The clue is that AnimatedFloat's constructor requires an ObjectAnimator. Without an ObjectAnimator, it cannot be created at all, so it is impossible to forget about it in WorldObject's initialiser list.
This is a nice example of a coding principle that is extremely important to me: code needs to be designed in such a way that common mistakes are simply not possible. Of course, this principle cannot be achieved in everything, but at least it is a good goal to strive for. In this case, by changing our class design, we have avoided a future bug that was very likely to happen quite often.
A big flaw in this design is that it currently only animates floats. In reality, a lot of our values are other things, like Colour, Radian and Vector2. The solution to this is to make AnimatedFloat templatised and to rename it to AnimatedValue. This is quite straightforward if you are experienced with templatised programming, but for any programmers less familiar with this weird part of C++, I'll provide a small example of roughly how that looks. Just think of a template as a type that will be filled in depending on what it really is. So T here can be a float, or a double, or a Colour, or whatever you want. In this specific piece of code, substitute the T with something that one can do math on, and it will still be good code.
template ‹typename T› class AnimatedValue { T currentValue; T getCurrentValue() const { return currentValue; } }; class WorldObject: ObjectAnimator { AnimatedValue‹float› scale; AnimatedValue‹Radian› rotation; }; |
There are some details to this that make the real code a bit more complex, but the core idea remains the same. By using templates, we now have a single AnimatedValue class that can animate any kind of value that we can do math on.
There are several topics that I have completely ignored so far. The first is actual animation. However, I don't want to make this post too long, so I won't go into detail on that. Animations are calculated in the updateAnimation() function of AnimatedValue.
Another topic I have ignored is how to generate an interface for editing these values, how to save them, and how to even have complete undo/redo for editing animation details. That is an interesting topic by itself, so I think I might get to that in a future blogpost at some point.
Finally, there is the topic of performance. Updating all these animation objects uses actual performance, especially if our artists animate tons of values. I had to optimise our animation code quite a bit to still get a good framerate on consoles. In the end, this is a trade-off I seem to encounter all the time: in most cases, the more flexible a system is, the more performance it uses. Luckily, computers and consoles are getting faster and faster, so as this is only a small problem for us now, I have no doubt a couple of years from now the performance of systems like this will have become completely irrelevant in all but the biggest triple-A games.
The fun part of our animation system, is that it is extremely flexible, but at the same time rather simple to build and use. In that sense it is rather similar to our upgrade system for gameplay, which I explained in a blogpost last year. Our artists have made tons of animations with our animation system for Awesomenauts, and it contributed greatly to the lively feel that the game has!
Nice blogpost, though I wish you wouldn't call perfectly reasonable language features 'obscure' and 'weird.' Sure you can do weird things with templates, but this example is absolutely textbook and nobody writing C++ should be afraid of it. I will admit that conversion operators can be somewhat tricky, but still... ;)
ReplyDeleteI meant it in a different way than you think: I called conversion operators 'obscure', because they are little used and little known. I met quite a few C++ programmers who didn't know about them (including, until quite recently, myself) and I never encountered the conversion operator in any of the libraries I have used (I'll have to admit that most of the libraries I have used are in C, though, since that is what most console SDKs are written in).
DeleteAs for whether templates are 'weird': if you come from another coding language and never used them before, they are certainly a very weird and outlandish thing that is difficult to grasp and use. Once you get used to them, though, they are a very useful and neat tool.
As someone who is learning C++ right now, this was a really interesting blog post. Thanks Joost!
ReplyDeleteHi Joost,
ReplyDeleteLooking at the usage of AnimatedFloat and the fact that it's only got one ObjectAnimator owner, could you change it so that the animated floats are nodes in a linked list? This would mean no need for a std::vector, memory allocation or iteration overhead. Something like this:
class ObjectAnimator
{
AnimatedFloat *first;
ObjectAnimator() : first(NULL) {}
void add(AnimatedFloat* value)
{
value->next = first;
first = value;
}
void updateAnimations(float time)
{
for(AnimatedFloat *f = first; f; f = f->next)
{
f->updateAnimation(time);
}
}
};
class AnimatedFloat
{
AnimatedFloat *next;
//The constructor requires a pointer to an ObjectAnimator
AnimatedFloat(ObjectAnimator* owner)
{
//The AnimatedFloat registers itself for updating
owner->add(this);
}
};
Hope this helps,
Rich
I guess that would indeed work, but I don't think there is much point to it. The iterating itself is a very minor part of the performance cost of this system.
DeleteIt was less the iteration and more the dynamic allocation that I was trying to avoid. Especially for information which is actually invariant per class, rather than per instance. I'd consider trying to express this higher per class level, but that may not be as easy to verify as the having the constructor require an animator.
DeleteAnyways, just my 2p,
Rich
Ah, yes, I understand. Lots of developers seem to carry a lot for avoiding dynamic allocations, so that makes sense. We totally don't worry about that, though: we do tons of dynamic allocation, not really a problem for small things like this. Our memory manager is extremely fast for very small allocations anyway, and also doesn't have any fragmentation for them. :)
Delete