About This Chapter
C4 provides an extremely robust bone-based animation system, which comes with in-built support for a variety of ways to play-back and manipulate animations at run-time. C4's animation system implements and directly exposes the necessary functionality to play-back frame-based animations and combine frame-based animations in two very important ways, namely animation blending and merging.
In addition to the run-time functionality afforded to programmers, C4 also provides a complete content pipeline including several tools that can be used for importing and viewing model animations from resources that defined using the COLLADA graphical interchange format1.
This chapter will focus on programming aspects involved with the basics of playing back and manipulating animations at run-time. Topics targeted towards artists regarding the creation of 3D models, rigging, animating or even the exporting/importing process are out of this chapter's scope. Numerous resources already exist for these topics, including information available on the official C4 Wiki.
Animators
We'll start off by diving right into the heart of the animation system, animators. In C4 an Animator is a C++ object that can be attached to a Model at run-time. Animators are used to manipulate the vertices of a model by transforming the position and orientation of bones that make-up a model's skeleton. C4 comes with three types of built-in animators, or Animator subclasses:
FrameAnimator
A frame animator is used to play-back a skeletal animation frame-by-frame, as defined by an animation resource. Each individual frame animator is applied to one node, typically a bone, and affects that node and all nodes below it in a model's node hierarchy,
A single frame animator could be applied to the root bone of a model and provide output representing the playback of a single animation; similar to what would be seen in 3D modelling/animating software.
BlendAnimator
A blend animator is used to blend the output from precisely two animator sub-nodes, or sub-animators. A blend animator's output will be a mixture of the output from its two sub-animators. The weight of each sub-animator in the final blend is determined by each sub-animator's weight interpolator. Blend animators are typically used to cross-fade between multiple animations.
As an example, a blend animator could be used to create a smooth transition between a character's walking and running animations by gradually fading out the walking animation as the running animation fades in.
MergeAnimator
A merge animator is used to combine the output from multiple sub-animators. The combining is performed whereby one or more sub-animators output will override a portion of the output from one or more other sub-animators. When bones are shared between the first sub-animator and second sub-animator, the second sub-animator's output will take precedence and appear in the output of the merge animator. When a third sub-animator is present, its output will take precedence over both the output of the first and second sub-animators, and so forth. A merge animator can be used to independently animate different bones of a model.
A simple use case would be merging the output of a frame animator playing a full-body running animation, and a second frame animator playing a weapon aiming animation that only affects the shoulders and arms. The combined output would be a simultaneous running and aiming animation.
Using Animators
As is typically the case when using C4, when you want to affect change in a World the behaviour is controlled by a Controller. As such, animators are typically instantiated by a controller and then attached to their target node, which in order to animate will be an instance of the Model class. The model's controller is then responsible for determining which animations should be played, blended and merged at any given moment, as well as telling the model to animate each frame.
SimpleFrameAnimator
Typically you want your model to enter the world and immediately begin animating. As such the best place to set-up a model's animations is in its controller's overridden Preprocess() virtual method.
SimpleFrameAnimator.h
…
class SimpleFrameAnimatedCharacterController : public CharacterController
{
private:
FrameAnimator *frameAnimator;
…
public:
…
void Preprocess(void) override;
void Move(void) override;
Model *GetTargetNode(void) const
{
return static_cast(CharacterController::GetTargetNode());
}
…
};
…
SimpleFrameAnimator.cpp
…
void SimpleFrameAnimatedCharacterController::Preprocess(void)
{
CharacterController::Preprocess();
frameAnimator = new FrameAnimator(GetTargetNode());
GetTargetNode()->SetRootAnimator(frameAnimator);
// Setup the frame animator to play the animation defined by the
// "TechTrooper/Run" animation resource file. Then setup the frame
// interpolator to play the animation forward and loop.
frameAnimator->SetAnimation("TechTrooper/Run");
frameAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
}
void SimpleFrameAnimatedCharacterController::Move(void)
{
CharacterController::Move();
GetTargetNode()->Animate();
}
…
In the header file we have defined a new CharacterController subclass called SimpleFrameAnimatedCharacterController (a bit of a mouth full). We have also declared that each instance of this class has a pointer to a FrameAnimator and that we will override both CharacterController::Preprocess() and CharacterController::Move() with our own implementations. In addition, we also provide a new GetTargetNode() convenience method which hides the super-class' method of the same name and returns a pointer to a Model instead of a pointer to a Node.
In our overridden Preprocess() method we first call the super-class' implementation. We then allocate a new FrameAnimator and assign it to our instance variable, frameAnimator. Animators must know about the model they will be attached to. As such when we construct the frame animator we pass in a pointer to the controller's target node, using our GetTargetNode() convenience method. We attach the frame animator to our model by setting the frame animator as the target node's root animator. Then we tell the frame animator to play the animation called " TechTrooper/Run".
Each frame animator has a frame interpolator. This Interpolator is used to define how an animation will be played back. Playback rate, duration and starting time are all configurable. However, calling SetAnimation(const char *name) on a frame animator will cause the frame animator to choose sensible default values that ensure a whole animation will be played back at the correct rate. On top of this we typically need to define the interpolator's mode manually. An interpolator mode is made up of several flags that can be used to define if an interpolator is initially stopped, playing forward, playing backward, looping and/or oscillating. In this particular instance we have set-up our frame interpolator (and hence our animation) to play forward and loop.
In order to ensure our model animates on each loop of the engine that the controller is activated we override the controller's Move() method and call Animate() on our target node/model.
If you run the included SimpleFrameAnimator project you will see a Tech Trooper model, kindly provided by Dexsoft Games 2, looping its run animation. When you're done press tilde and type quit and then press enter to exit.
Figure 1: SimpleFrameAnimator, Tech Trooper
Educational License provided by www.dexsoft-games.com
(see Addendum for details)
SimpleBlendAnimator
This SimpleBlendAnimator code is very similar to the SimpleFrameAnimator sample. The important changes are included below:
SimpleBlendAnimator.h
…
class SimpleBlendAnimatedCharacterController : public CharacterController
{
private:
BlendAnimator *blendAnimator;
FrameAnimator *frameAnimators[2];
…
};
…
SimpleBlendAnimator.cpp
…
void SimpleBlendAnimatedCharacterController::Preprocess(void)
{
static const float interpolationTime = 5000.0F; // 5000 ms (i.e. 5 seconds)
CharacterController::Preprocess();
// Setup the first frame animator to play "TechTrooper/Idle", forward and
// looping.
frameAnimators[0] = new FrameAnimator(GetTargetNode());
frameAnimators[0]->SetAnimation("TechTrooper/Idle");
frameAnimators[0]->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Setup the second frame animator to play "TechTrooper/Run", forward and
// looping.
frameAnimators[1] = new FrameAnimator(GetTargetNode());
frameAnimators[1]->SetAnimation("TechTrooper/Run");
frameAnimators[1]->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Create a new blend animator and add the two frame animators as
// sub-animators.
blendAnimator = new BlendAnimator(GetTargetNode());
blendAnimator->AddNewSubnode(frameAnimators[0]);
blendAnimator->AddNewSubnode(frameAnimators[1]);
// Setup our frame animators weight interpolators for use with the blend
// animator, causing the final animation to interpolate back and forth between
// our idle and run animators.
frameAnimators[0]->GetWeightInterpolator()->SetRange(0.0F, 1.0F);
frameAnimators[0]->GetWeightInterpolator()->Set(0.0F, 1.0F / interpolationTime,
kInterpolatorForward | kInterpolatorLoop | kInterpolatorOscillate);
frameAnimators[1]->GetWeightInterpolator()->SetRange(0.0F, 1.0F);
frameAnimators[1]->GetWeightInterpolator()->Set(1.0F, 1.0F / interpolationTime,
kInterpolatorBackward | kInterpolatorLoop | kInterpolatorOscillate);
// We want the model to receive output from the blend animator. So we set
// the model's root animator to be the blend animator.
GetTargetNode()->SetRootAnimator(blendAnimator);
}
…
In this sample, in addition to making use of frame animators' frame interpolators, we also make use of their weight interpolators. Quite simply a weight interpolator determines the amount of output from a blend animator's sub-animator present in the blend animator's output. If one sub-animator has a greater weight than its sibling animator, then it will be more visible in the final blend. As a percentage, an animator's final weight (or its affect on the final blend) can be calculated as:
Figure 2: Blend weight percentage of sub-animator x
Unlike frame interpolators, weight interpolators are not specific to the FrameAnimator class. Instead weight interpolators are provided by the Animator class, which ensures the output from any Animator sub-class can be blended by a BlendAnimator.
In the SimpleBlendAnimator sample we have set-up the frame animators with weight interpolators that have a maximum value 1.0f. We have then set-up both weight interpolators to oscillate back and forth between 0.0f and 1.0f. One sub-animator has been given an initial weight of 0.0f and told to animate forward whilst the other has been given a value of 1.0f and told to animate backwards.
When our world first loads the first sub-animator (playing the idle animation) will take full effect, appearing as the only animation in the final output from the blend animator. Over the period of 5 seconds the idle animation will slowly fade out as the second sub-animator (playing the run animation) fades in, until the point where the run animation is the only animation appearing in the final output. At this point in time the weight interpolators will both be at different ends of their respective ranges (0.0f and 1.0f). The oscillating loop mode will cause both weight interpolators to flip direction, from kInterpolatorForward to kInterpolatorBackward (or vice versa), resulting in the run animation fading out and the idle animation fading back in again.
This process will repeat indefinitely; assuming the two weight animators don't become out of sync over a very long period of time.
SimpleMergeAnimator
The SimpleMergeAnimator code is also very similar to the SimpleFrameAnimator and SimpleBlendAnimator samples:
SimpleMergeAnimator.h
…
class SimpleMergeAnimatedCharacterController : public CharacterController
{
private:
MergeAnimator *mergeAnimator;
FrameAnimator *frameAnimators[2];
…
};
…
SimpleMergeAnimator.cpp
…
void SimpleMergeAnimatedCharacterController::Preprocess(void)
{
CharacterController::Preprocess();
// Setup the first frame animator to play "TechTrooper/Run", forward and
// looping.
frameAnimators[0] = new FrameAnimator(GetTargetNode());
frameAnimators[0]->SetAnimation("TechTrooper/Run");
frameAnimators[0]->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Setup the second frame animator to play "TechTrooper/GunShoot", forward
// and looping on the model's upper body.
frameAnimators[1] = new FrameAnimator(GetTargetNode(), GetTargetNode()->FindNode(
Text::StaticHash<'B', 'I', 'P', '_', 'S', 'p', 'i', 'n', 'e', '1'>::value));
frameAnimators[1]->SetAnimation("TechTrooper/GunShoot");
frameAnimators[1]->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Create a new merge animator and add the two frame animators as sub-animators.
mergeAnimator = new MergeAnimator(GetTargetNode());
mergeAnimator->AddNewSubnode(frameAnimators[0]);
mergeAnimator->AddNewSubnode(frameAnimators[1]);
// We want the character to receive output from the merge animator. So we set
// the model's root animator to be the merge animator.
GetTargetNode()->SetRootAnimator(mergeAnimator);
}
…
The key bit of code to note in this sample is, that when calling the constructor for our second frame animator, we have not accepted the default value of nullptr for the Node *node parameter. Instead we have passed in a pointer to a node, or more specifically a Bone , that we found in the target model's node hierarchy by calling FindNode(unsigned_int32 hash) and passing it a hash of the bone's name.
By specifying a node we are indicating that the frame animator should only produce output for the specified node's sub-tree. Because the frame animator only produces output for certain bones, when used as the second sub-animator in a merge animator, it will only overwrite the output of the first sub-animator for the sub-tree of the specified node. This behaviour can be observed by running the included SimpleMergeAnimator sample project. The final output is a running Tech Trooper whose upper body looks as though it's shooting a weapon.
Figure 3: SimpleMergeAnimator
Figure 4: SimpleMergeAnimator – Right clavicle instead of spine
If you were to comment out the second FrameAnimator constructor parameter, on the line where frameAnimators[1] is assigned a value, and run the sample again; instead of seeing the running animation with the upper body shooting, you would see the Tech Trooper's whole body running with no shooting animation.
Alternatively, you could replace the node hash passed the FrameAnimator constructor with the hash for a different node, such as " Bip_R_Clavicle", and observe a different portion of the body play the " TechTrooper/GunShoot" animation.
Animator Trees
By now you should have a pretty good idea of how to use frame, blend and merge animators. However, the true power of C4's animation system lies in its tree-based animator hierarchy, which allows you to easily combine the output of several numerous animator in order to produce truly complex animations with smooth transitions.
If you know precisely which combination of animations a model can play, blend and/or merge at any given time then it's extremely easy to come up with a basic tree structure that can be used to represents all animation possibilities that may occur at run-time.
Figure 5: A simple Animator Tree
SimpleAnimatorTree
The SimpleAnimatorTree sample is a mash-up of the SimpleBlendAnimator and SimpleMergeAnimator samples. This time, instead of adding two frame animators to a merge animator, we have added one blend animator and one frame animator. The blend animator itself has two frame animators as sub-animators, resulting in the same oscillating idle/run animation sequence seen in the SimpleBlendAnimator sample. The resulting output of this animator tree as a whole is the Tech Trooper slowly blending between the idle and run animations, with his upper body looking as though it's shooting a gun.
Figure 5 is implemented in the SimpleAnimatorTree sample project as follows:
SimpleAnimatorTree.h
…
class SimpleAnimatedCharacterController : public CharacterController
{
private:
MergeAnimator *mergeAnimator;
BlendAnimator *blendAnimator;
FrameAnimator *frameAnimators[2];
FrameAnimator *upperBodyAnimator;
…
};
…
SimpleAnimatorTree.cpp
…
void SimpleAnimatedCharacterController::Preprocess(void)
{
static const float interpolationTime = 5000.0F; // 5000 ms (i.e. 5 seconds)
CharacterController::Preprocess();
// Setup the first frame animator to play "TechTrooper/Idle", forward and
// looping.
frameAnimators[0] = new FrameAnimator(GetTargetNode());
frameAnimators[0]->SetAnimation("TechTrooper/Idle");
frameAnimators[0]->GetWeightInterpolator()->SetValue(0.0f);
frameAnimators[0]->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Setup the second frame animator to play "TechTrooper/Run", forward and
// looping.
frameAnimators[1] = new FrameAnimator(GetTargetNode());
frameAnimators[1]->SetAnimation("TechTrooper/Run");
frameAnimators[1]->GetWeightInterpolator()->SetValue(1.0f);
frameAnimators[1]->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Setup our blend animator to interpolate back and forth between our idle and
// run animators.
frameAnimators[0]->GetWeightInterpolator()->SetRange(0.0F, 1.0F);
frameAnimators[0]->GetWeightInterpolator()->Set(0.0F, 1.0F / interpolationTime,
kInterpolatorForward | kInterpolatorLoop | kInterpolatorOscillate);
frameAnimators[1]->GetWeightInterpolator()->SetRange(0.0F, 1.0F);
frameAnimators[1]->GetWeightInterpolator()->Set(1.0F, 1.0F / interpolationTime,
kInterpolatorBackward | kInterpolatorLoop | kInterpolatorOscillate);
// Create a new blend animator and add the two frame animators as sub-animators.
blendAnimator = new BlendAnimator(GetTargetNode());
blendAnimator->AddNewSubnode(frameAnimators[0]);
blendAnimator->AddNewSubnode(frameAnimators[1]);
// Setup the second frame animator to play "TechTrooper/GunShoot", forward
// and looping on the model's upper body.
upperBodyAnimator = new FrameAnimator(GetTargetNode(), GetTargetNode()->FindNode(
Text::StaticHash<'B', 'I', 'P', '_', 'S', 'p', 'i', 'n', 'e', '1'>::value));
upperBodyAnimator->SetAnimation("TechTrooper/GunShoot");
upperBodyAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Create a new merge animator and add the two frame animators as sub-animators.
mergeAnimator = new MergeAnimator(GetTargetNode());
mergeAnimator->AddNewSubnode(blendAnimator);
mergeAnimator->AddNewSubnode(upperBodyAnimator);
// We want the character to receive output from the merge animator. So we set
// the model's root animator to be the blend animator.
GetTargetNode()->SetRootAnimator(mergeAnimator);
}
…
Figure 6: Idling and shooting a gun
Due to chosen hierarchy the " TechTrooper/GunShoot" animation will always appear in the final output regardless of whether the Tech Trooper is idling or running. If you want the Tech Trooper to only shoot when he is idling, you could modify the SimpleAnimatorTree source code to produce an animator tree of the following structure:
Figure 7: Alternate Simple Animator Tree
Dynamically Changing Animations
Up until this point all animators have been set-up to play specific animations in our controller as soon as our controller enters the world, when its Preprocess() method is called. This is all well and good for simple examples. However, typically you want animations to be chosen dynamically, at run-time, based on AI decisions, scripts or in the simplest case, user input.
SimpleDynamicAnimation
It's fairly standard behaviour in many PC games that if a user presses the ' W' key on their keyboard, they expect their character to play a forward running animation. Alternately, if the user presses the ' S' key on their keyboard, then they expect their character to play a backward walking animation. When the user releases both the ' W' and ' S' keys, then it's expected that the character will smoothly return to their standing animation.
This behaviour is achieved in the SimpleDynamicAnimation sample as follows:
SimpleDynamicAnimation.h
…
class SimpleDynamicAnimatedCharacterController : public CharacterController
{
private:
// The following enum uniquely identifies the different animations
// that our controller could ask our frame animators to play.
enum
{
kAnimationStand = 0,
kAnimationRunForward,
kAnimationRunBackward
};
BlendAnimator *blendAnimator;
FrameAnimator *frameAnimators[2];
// We're going to be swapping back and forth between the two frame
// animators. So we need an extra pointer to keep track of which is
// the current "active" animator.
FrameAnimator *activeAnimator;
// We're also going to be changing between three different
// animations. As such we're going to need to keep track of the
// current "active" animation.
int32 activeAnimation;
// For the sake of simplicity, we're just going to use two booleans
// to indicate if a SimpleDynamicAnimatedCharacterController is
// attempting to run forward and/or run backward. If we had a lot more
// states then this approach might becoming unwieldy and using
// bit-flags, like the official Terathon SimpleChar sample, would make
// a lot more sense.
bool runningForward;
bool runningBackward;
…
void PlayAnimation(int animation);
public:
…
void SetRunningForward(bool runningForward)
{
this->runningForward = runningForward;
}
void SetRunningBackward(bool runningBackward)
{
this->runningBackward = runningBackward;
}
…
};
enum
{
// By default, the 'W' key is mapped to 'frwd' in Data/Engine/input.cfg.
kActionForward = 'frwd',
// Also, by default, the 'S' key is mapped to 'bkwd'.
kActionBackward = 'bkwd'
};
// This Action sub-class is used to define custom behaviour in response to user
// input.
class RunAction : public Action
{
private:
SimpleDynamicAnimatedCharacterController *controller;
public:
RunAction(ActionType actionType,
SimpleDynamicAnimatedCharacterController *controller);
~RunAction();
void Begin(void) override;
void End(void) override;
};
…
SimpleDynamicAnimation.cpp
…
void SimpleDynamicAnimatedCharacterController::Preprocess(void)
{
CharacterController::Preprocess();
// Setup the first frame animator to play "TechTrooper/Idle", forward and
// looping.
frameAnimators[0] = new FrameAnimator(GetTargetNode());
frameAnimators[0]->SetAnimation("TechTrooper/Idle");
frameAnimators[0]->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// The second frame animator's output won't be visible when the node/controller
// first enters the world. As such, we'll just create it but won't set it up to
// play an animation.
frameAnimators[1] = new FrameAnimator(GetTargetNode());
// Create a new blend animator and add the two frame animators as sub-animators.
blendAnimator = new BlendAnimator(GetTargetNode());
blendAnimator->AddNewSubnode(frameAnimators[0]);
blendAnimator->AddNewSubnode(frameAnimators[1]);
// We setup out first frame animator's weight interpolator to have a range of
// [0.0F, 1.0F]. However, this time around we set the starting value to 1.0F
// and set the mode as kInterpolatorStop. We've also set the rate to 0.0F,
// however this is redundant as we've told the interpolator stop interpolating.
frameAnimators[0]->GetWeightInterpolator()->SetRange(0.0F, 1.0F);
frameAnimators[0]->GetWeightInterpolator()->Set(1.0F, 0.0F, kInterpolatorStop);
// We setup the second frame animator's weight interpolator to have a range of
// [0.0F, 1.0F], a intial value of 0.0F and to be stopped. This means that
// unless we do something at run-time the second frame animator will never be
// present in the blend animator's output. Which is just as as well, as we
// haven't even set an animation for this frame animator to play back!
frameAnimators[1]->GetWeightInterpolator()->SetRange(0.0F, 1.0F);
frameAnimators[1]->GetWeightInterpolator()->Set(0.0F, 0.0F, kInterpolatorStop);
// We want to keep track of the active animator, i.e. the one playing the most
// recently started animation. At the moment that's the first frame animator,
// as it's the only one we've told to play an animation at this point.
activeAnimator = frameAnimators[0];
// We also want to keep track of the active animation. Currently that's
// "TechTrooper/Idle", which we're going refer to as kAnimationStand.
activeAnimation = kAnimationStand;
// We want the model to receive output from the blend animator. So we set
// the model's root animator to be the blend animator.
GetTargetNode()->SetRootAnimator(blendAnimator);
}
void SimpleDynamicAnimatedCharacterController::Move(void)
{
CharacterController::Move();
// This time around our controller has some truly dynamic behaviour. We need to
// look at the runningForward and runningBackward booleans in order to determine
// which animation should be played.
int animation;
// There are four possible states:
//
// 1. (runningForward && !runningBackward)
// 2. (!runningForward && runningBackward)
// 3. (runningForward && runningBackward)
// 4. (!runningForward && !runningBackward)
//
// These four states can be expressed as nested if statements.
if (runningForward)
{
// If the user has triggered actions to try run both forward and backward
// at once then it's a good idea to just stand still!
if (runningBackward)
animation = kAnimationStand;
else
animation = kAnimationRunForward;
}
else
{
// If the user isn't trying to run forward or backwards, then we'll want
// to play the stand animation.
if (runningBackward)
animation = kAnimationRunBackward;
else
animation = kAnimationStand;
}
// Now we don't want to go repeatedly playing the same animation over and
// over. So we'll only call PlayAnimation(int animation) if activeAnimation
// doesn't already equal the desired animation.
if (activeAnimation != animation)
PlayAnimation(animation);
GetTargetNode()->Animate();
}
void SimpleDynamicAnimatedCharacterController::PlayAnimation(int animation)
{
static const float fadeTime = 500.0F; // 500 ms
// We don't want to suddenly stop playing the current active animation. Instead
// we're going to cause the active animation (played by the active animator) to
// fade out, using its weight interpolator.
//
// We set the weight interpolators mode to backwards and its rate to
// 1.0F / fadeTime. This time around we don't set the weight interpolator to
// loop. Once our animation fades out, we want it to stay faded out!
activeAnimator->GetWeightInterpolator()->SetRate(1.0F / fadeTime);
activeAnimator->GetWeightInterpolator()->SetMode(kInterpolatorBackward);
// We want to play a new animation. So we need whichever frame animator that was
// not the active animator, to become the new active animator.
if (activeAnimator == frameAnimators[0])
activeAnimator = frameAnimators[1];
else
activeAnimator = frameAnimators[0];
// Make the new active animator play the appropriate animation resource.
switch (animation)
{
case kAnimationStand:
// Play "TechTrooper/Idle" forward and loop.
activeAnimator->SetAnimation("TechTrooper/Idle");
activeAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
break;
case kAnimationRunForward:
// Play "TechTrooper/Run" forward and loop.
activeAnimator->SetAnimation("TechTrooper/Run");
activeAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
break;
case kAnimationRunBackward:
// Play "TechTrooper/Walk" animation backward and loop.
activeAnimator->SetAnimation("TechTrooper/Walk");
activeAnimator->GetFrameInterpolator()->SetMode(kInterpolatorBackward |
kInterpolatorLoop);
break;
}
// We've already told the old active animator to fade out. But we also need to
// tell the new active animator to fade in using its weight interpolator.
activeAnimator->GetWeightInterpolator()->SetRate(1.0F / fadeTime);
activeAnimator->GetWeightInterpolator()->SetMode(kInterpolatorForward);
// We keep track of the last played animation using activeAnimation.
activeAnimation = animation;
}
// We explictly call the super-class' constructor as we need to pass actionType
// on to it. This is necessary so that the input manager can map this action to
// particular control (or key), as defined in Data/Engine/input.cfg.
RunAction::RunAction(ActionType actionType,
SimpleDynamicAnimatedCharacterController *controller) : Action(actionType)
{
// We store a pointer to a SimpleDynamicAnimatedCharacterController instance so
// that this action can call its instance methods.
this->controller = controller;
}
// The RunAction destructor doesn't do anything, but always defining your
// destructors is a good habit to get into.
RunAction::~RunAction()
{
}
// This method is called whenever the user triggers the RunAction, by pressing
// down the button associated with the RunAction.
void RunAction::Begin(void)
{
// At run-time two different instances of RunAction will exist. One will be
// mapped to the kActionForward action type, and the other the kActionBackward
// action type. Depending on the action type we want different behaviour for
// each instance.
//
// We want to start running forward or backward so we call the appropriate
// setter, matching the action type, and pass true.
switch (GetActionType())
{
case kActionForward:
controller->SetRunningForward(true);
break;
case kActionBackward:
controller->SetRunningBackward(true);
break;
}
}
// This method is called whenever the user stops triggering the RunAction, by
// releasing the associated button, after they had previously begun the action.
void RunAction::End(void)
{
// As with RunAction::Begin(), we also want different behaviour for different
// instances with different action types.
//
// We want to stop running forward or backward so we call the appropriate
// setter, matching the action type, and pass false.
switch (GetActionType())
{
case kActionForward:
controller->SetRunningForward(false);
break;
case kActionBackward:
controller->SetRunningBackward(false);
break;
}
}
…
If you run the SimpleDynamicAnimation sample project then you'll be able to observe the smooth blending between a running and standing animation. However, it probably won't take you long to notice a distinct hiccup in the animation whenever you try quickly swap between running forward and running backward. This hiccup is the conglomeration of two smaller issues.
The first issue is a human input issue. It is extremely difficult to swap between walking forward and walking backwards without standing for a very short period of time. In order to do so you would have to release the forward key (' W') and press the back key (' S'), or vice versa, in exactly the same engine run loop. Given that you would typically want your game to be completing at least 60 run loops a second, then this is extremely difficult to achieve.
The second issue is that we're only making use of two frame animators. As a result of the first issue, when we try transition from running forward to running backward we will pass through the standing state and therefore attempt to play three different animations in quick succession. However, we only have two animators at our disposal. Before the frame animator playing the forward running animation has a chance to finish fading out, it is reused to play the backward running animation. This is done because the other frame animator has already been used to play the standing (idle) animation.
If you don't think the hiccup between running forward and running backwards is particularly noticeable. Then a similar issue is very easily observed by repeatedly tapping and releasing the forward (or backward) key.
Now, there are definitely solutions to these issues that don't involve the use of more animators i.e. preventing the users from swapping animations too quickly, or delaying the standing animation to see if a user is just trying to transition between running forward and walking backward. These sorts of solutions may work quite well for some games. However, depending on what you're trying to achieve you may find these solutions have undesirable effects on responsiveness and/or game-play.
Figure 8: Three Blended Frame Animators
What is the solution then? Add an extra blend and frame animator?
What about two extra blend animators? Or three, or four? If we have four frame animators in total, should we have three blend animators and an unbalanced tree, or two blend animators and a balanced tree?
Figure 9: Balanced tree; four Blended Frame Animators
Perhaps we don't need to decide at all?
Dynamic Animator Trees
Much like C4's Node objects used in a world's node hierarchy; Animator objects can be dynamically added and removed from a model's animator tree at run-time.
Dynamic animator trees can be used to produce extremely complex animations by adding and removing animators as needed at runtime. For instance, an unequipped character may not require any animations to be merged. However, when the character picks up a pistol you may wish to merge an aiming animation in with the existing animations. This can easily be achieved by inserting a new merge animator into the animator tree as needed. When the character puts the pistol away then the merge animator can then be removed from the animator hierarchy.
Dynamic Animator Tree Example
Using a dynamic animator tree behaviour we can redesign the SimpleDynamicAnimation sample so that animation hiccups no longer take place. Initially we'll start out with one frame animator playing the idle animation.
Figure 10: Dynamic tree – Initial State
When the user presses a button to play the forward running animation we'll insert a new frame animator, and a new blend animator, into the hierarchy as follows. The new blend animator will be made the root animator for our model, and the frame animators will be its sub-animators.
Figure 11: Dynamic animator tree – Fade-in running animation
The new running frame animator will have its weight interpolator set to 0.0f and mode set to interpolate forward. The original idle frame animator will have its weight interpolator set to 1.0f and its mode set to interpolate backwards. As we've seen previously in the chapter this will have the effect of fading out the idle animation whilst the running animation fades in. When the idle frame animator has finished fading out, we will remove it and the blend animator from the hierarchy. Again leaving us with one frame animator.
Figure 12: Dynamic Animator Tree – Running Animation
Now, if the user quickly releases the forward button, in preparation of pressing the backward button, a new frame animator will be added to the tree once more to play the idle animation.
Figure 13 Dynamic Animator Tree: Forward to Backwards – Part 1
The user will then immediately press down the backward key and expect to see a backward walking (" TechTrooper/Walk" played in reverse) animation. It is at this point that issues would have started to appear in the SimpleDynamicAnimation sample.
This time around, instead of reusing the frame animator playing the run animation; we're going to add a new frame animator, and very importantly, a new blend animator into the animator tree.
When we insert the new blend animator, as before, we will insert it as the model's root animator. However, now the new root blend animator will have one frame animator (playing the backwards animation) and one blend animator as its sub-animators.
This is the first time we've seen a blend animator added as a sub-animator to another blend animator. However, there is nothing special about this setup. Just as before we will fade in the new animator (backwards walking frame animator) and fade out the old animator (the existing blend animator with two frame animators as its sub-animators).
Figure 14: Dynamic Animator Tree: Forward to Backwards – Part 2
Problem solved.
All three animations are playing at once. The frame animator playing " TechTrooper/Walk" (in reverse) is fading in, its sibling blend animator is fading out. The frame animator playing " TechTrooper/Idle" is still fading in, and the " TechTrooper/Run" frame animator is still fading out. It may seem a slightly strange that the " TechTrooper/Idle" frame animator is still technically fading in. However, its parent blend animator is fading out, so in root animator's final output the " TechTrooper/Idle" animation will appear as though it's fading out.
But what happens when the frame animator playing " TechTrooper/Run" finishes fading out?
You might think that you can simply remove the parent blend animator of frame animator playing " TechTrooper/Idle", and replace it with the frame animator itself. However, we've just pointed out that " TechTrooper/Idle" was fading in. In fact, if the frame animator playing " TechTrooper/Run" has finished fading out, then the frame animator playing " TechTrooper/Idle" will have just finished fading in. Not only would simply replacing the blend animator cause the idle animation to be far too prevalent in the final output, the idle animation would never fade out.
There is a trick to sorting this out. When we replace the blend animator with the frame animator (that is playing the idle animation), we need to setup the frame animator's weight interpolator to be exactly the same as the blend animator's weight interpolator. Setting the weight interpolator's value to match that of the blend animator will ensure the idle animation doesn't suddenly appear more prevalent in the final output. Also, by setting the frame interpolator's weight interpolator mode to match the blend animator's weight interpolator mode (which will be interpolate backwards), we ensure the idle animation will continue to fade out of the root animator's output.
Figure 15: Dynamic Animator Tree: Forward to Backwards – Part 3
When the frame animator playing the idle animation finishes fading out we will remove the blend animator and be left with just the one frame animator playing the backwards running animation.
Figure 16: Dynamic Animator Tree
Forward to Backwards – Part 4: Success
In order for this all to work we need some means of determining when an animator has finished fading out. You could loop through the whole animator tree every time a controller's Update() method is called and look for stopped weight interpolators. However, that's both inefficient and quite messy. Of course, C4 offers an efficient in-built solution to this problem.
Interpolator Completion Procedures
C4's Interpolator class has built-in support for assigning callbacks to interpolators known as completion procedures. A completion procedure will automatically be called by an interpolator when it stops interpolating as a result of its value reaching either end of its range. Looping interpolators don't stop when they reach their range boundaries, so completion procedures are only useful for interpolators that don't loop.
| _ Note: _As well as completion procedures, interpolators also support "loop procedures". These operate much in the same way as completion procedures but are called each time an interpolator loops instead when an interpolator stops/completes. | | --- |
Completion procedure functionality is implemented in the Interpolator class through inheritance of the Completable super-class. As such an interpolator's completion procedure must match the following signature:
| typedef** void *Interpolator::CompletionProc(Interpolator \, void *); | | --- |
Some other class (typically a Controller sub-class) could declare a completion procedure with a matching signature. Such as...
| static** void *InterpolatorCompleted(Interpolator \interpolator,** void *\cookie); | | --- |
where cookie is a pointer to some sort of user defined data.
A completion procedure and cookie can then be set for an interpolator using Interpolator::SetCompletionProc(CompletionProc *proc, void *cookie).
SimpleDynamicAnimatorTree – Putting It All Together
By taking advantage of interpolator completion procedures we can implement a fully dynamic animator tree as follows.
SimpleDynamicAnimatorTree.h
…
class DynamicTreeAnimatedCharacterController : public CharacterController
{
private:
// Unique animation identifiers.
enum
{
kAnimationStand = 0,
kAnimationRunForward,
kAnimationRunBackward
};
// We need to keep track of the active animation.
int32 activeAnimation;
// Booleans to indicate if a DynamicTreeAnimatedCharacterController is
// attempting to run forward and/or run backward.
bool runningForward;
bool runningBackward;
// We store an array of old animators, so they can be deleted when it
// is safe.
Array oldAnimators;
static void WeightInterpolatorCompleted(Interpolator *interpolator,
void *cookie);
…
void PlayAnimation(int animation);
public:
…
};
…
SimpleDynamicAnimatorTree.cpp
…
void DynamicTreeAnimatedCharacterController::Preprocess(void)
{
static const float interpolationTime = 5000.0F; // 5000 ms (i.e. 5 seconds)
CharacterController::Preprocess();
// Initially we just want our animator tree to consist of a single frame
// animator playing the "TechTrooper/Idle" animation.
FrameAnimator *frameAnimator = new FrameAnimator(GetTargetNode());
frameAnimator->SetAnimation("TechTrooper/Idle");
frameAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
frameAnimator->GetWeightInterpolator()->SetRange(0.0F, 1.0F);
frameAnimator->GetWeightInterpolator()->Set(1.0F, 0.0F, kInterpolatorStop);
GetTargetNode()->SetRootAnimator(frameAnimator);
// Keep track of the active animation.
activeAnimation = kAnimationStand;
}
void DynamicTreeAnimatedCharacterController::Move(void)
{
CharacterController::Move();
// We maintain an array of old (unused) animators so that they can safely be
// deleted here in DynamicTreeAnimatedCharacterController::Move().
for (int32 i = 0; i < oldAnimators.GetElementCount(); i++)
delete oldAnimators[i];
oldAnimators.Purge();
// This time around our controller has some truly dynamic behaviour. We need to
// look at the runningForward and runningBackward booleans in order to determine
// which animation should be played.
int animation;
// Determine which animation should be played depending on runningForward and
// runningBackward.
if (runningForward)
{
if (runningBackward)
animation = kAnimationStand;
else
animation = kAnimationRunForward;
}
else
{
if (runningBackward)
animation = kAnimationRunBackward;
else
animation = kAnimationStand;
}
// Only play the animation if we're not already playing it.
if (activeAnimation != animation)
PlayAnimation(animation);
GetTargetNode()->Animate();
}
void DynamicTreeAnimatedCharacterController::PlayAnimation(int animation)
{
static const float fadeTime = 500.0F; // 500 ms
// Each time we play a new animation we're going to create a new frame animator
// to play the new animation.
FrameAnimator *frameAnimator = new FrameAnimator(GetTargetNode());
switch (animation)
{
case kAnimationStand:
// Play "TechTrooper/Idle" forward and loop.
frameAnimator->SetAnimation("TechTrooper/Idle");
frameAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
break;
case kAnimationRunForward:
// Play "TechTrooper/Run" forward and loop.
frameAnimator->SetAnimation("TechTrooper/Run");
frameAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
break;
case kAnimationRunBackward:
// Play "TechTrooper/Walk" animation backward and loop.
frameAnimator->SetAnimation("TechTrooper/Walk");
frameAnimator->GetFrameInterpolator()->SetMode(kInterpolatorBackward |
kInterpolatorLoop);
break;
}
// Fade in.
frameAnimator->GetWeightInterpolator()->Set(0.0F, 1.0F / fadeTime,
kInterpolatorForward);
// In order for this new frame animator to fade in we need to blend it with the
// existing animator tree. As such we'll create a new blend animator and add
// the existing root animator and our new frame animator as sub-animators.
BlendAnimator *blendAnimator = new BlendAnimator(GetTargetNode());
// We use AddSubnode(Node *node) for the existing (soon to be old) root animator
// as we don't want Preprocess() to be called again.
Animator *oldRootAnimator = GetTargetNode()->GetRootAnimator();
blendAnimator->AddNewSubnode(frameAnimator);
blendAnimator->AddSubnode(oldRootAnimator);
// We want all the existing animations to fade out, so we setup the old root
// animator's weight interpolator to fade out.
oldRootAnimator->GetWeightInterpolator()->Set(1.0F, 1.0F / fadeTime,
kInterpolatorBackward);
// When our weight interpolator finishes causing the animator to fade out we
// want our completion procedure to be called. We pass a pointer to the
// interpolator's animator as our user defined data (aka cookie).
oldRootAnimator->GetWeightInterpolator()->SetCompletionProc(
&DynamicTreeAnimatedCharacterController::WeightInterpolatorCompleted,
oldRootAnimator);
// Set our blend animator as the new root animator so that our target model
// receives output from it.
GetTargetNode()->SetRootAnimator(blendAnimator);
// Update the last played (active) animation.
activeAnimation = animation;
}
void DynamicTreeAnimatedCharacterController::WeightInterpolatorCompleted(
Interpolator *interpolator, void *cookie)
{
// An animator has just finished fading out so we want to remove it from our
// animator tree. However, we don't want to leave behind a blend animator with
// just one sub-animator. As such we need to remove the parent blend animator.
Animator *fadedOutAnimator = static_cast(cookie);
BlendAnimator *blendAnimator = static_cast(
fadedOutAnimator->GetSuperNode());
// We want to replace the unneeded blend animator with the child animator which
// has not faded out; the one that has technically "faded in".
Animator *fadedInAnimator = blendAnimator->GetFirstSubnode();
Model *model = blendAnimator->GetTargetModel();
if (blendAnimator == model->GetRootAnimator())
{
// If the blend animator is the root animator then we can simply replace it
// with the faded-in animator and we don't have to worry about any blending.
model->SetRootAnimator(fadedInAnimator);
// When you attach an animator to another parent animator it is
// automatically removed from the old parent. However, this does not happen
// automatically when you set an animator as the root animator. As such we
// do this manually.
blendAnimator->RemoveSubnode(fadedInAnimator);
}
else
{
// If the blend animator wasn't the root animator, then it must also be a
// "fading out" sub-animator of another blend animator.
//
// In which case we need to copy this blend animator's weight interpolator
// value over to the faded-in animator, and tell it to fade out in the
// same way the blend animator was.
fadedInAnimator->GetWeightInterpolator()->Set(
blendAnimator->GetWeightInterpolator()->GetValue(),
blendAnimator->GetWeightInterpolator()->GetRate(),
kInterpolatorBackward);
// We'll also want the completion procedure to be called when the "faded-in
// animator" finishes fading out.
fadedInAnimator->GetWeightInterpolator()->SetCompletionProc(
&DynamicTreeAnimatedCharacterController::WeightInterpolatorCompleted,
fadedInAnimator);
// Now all we need to do is remove the blend animator from its parent
// animator and add the faded-in animator in its place.
Animator *parentAnimator = blendAnimator->GetSuperNode();
parentAnimator->RemoveSubnode(blendAnimator);
parentAnimator->AddSubnode(fadedInAnimator);
// Due to the way sub-animators are updated in C4, even though we've just
// detached the blend animator from our animator tree, its Update() method
// will still be called this frame. As such it's possible the blend
// animator also finished fading out this frame. However, it's no longer a
// part of our animator tree, so we don't want its completion procedure to
// be called.
blendAnimator->GetWeightInterpolator()->SetCompletionProc(nullptr, nullptr);
}
// When this method is called C4 is in the middle of executing some code on
// the animators (and their iterators). As such we can't delete the blend
// animator (or its sub-animator, the faded-out animator) just yet. Instead we
// add them to the controller's oldAnimator array so they can be deleted safely
// next time the controller's Move() method is called.
DynamicTreeAnimatedCharacterController *controller =
static_cast(
model->GetController());
controller->oldAnimators.AddElement(blendAnimator);
}
…
This sample is a direct implementation of the dynamic animator tree explained earlier. The code is reasonably complex compared to the past examples. If you have trouble digesting the code then it's recommended you compare the code to the sequence of diagrams a few pages back.
If you run the sample you will be able to see smooth transitions between the animations for standing (idle), running forwards and running backwards. If you quickly swap between these animations you'll notice that the animation glitch, present in the SimpleAnimatorTree sample, is no longer present. You can achieve good quality results by using dynamic animator trees, especially when you intend to blend a variable number of animations together. Although the examples you've seen don't make use of any merge animators, you can insert merge animators into the dynamic tree as needed.
It's important to be aware that when you're using dynamic animator trees you're constantly allocating (and deleting) new animators. This incurs a performance hit that is not present when making use of static animator trees. In addition to this, the C4 engine may have to do a bit of work behind the scenes to regenerate animator output buffers as the animator tree changes. C4 does include checks to mitigate the creation of new buffers when it's not necessary, however this is a potential performance issue that you should be aware of when choosing dynamic animator trees over the simpler static animator trees seen earlier in the chapter.
Animation Cues
C4's animation system can be used for more than just altering the vertices of a model. Frame animators have support for a very useful piece of functionality known as animation cues. Animation cues allow you to hook-up game-specific functionality that should be executed when a particular frame of a playing animation is reached.
Animation cues can be added to a previously imported animation by using the model viewer that comes as a part of C4's world editor plugin. This can be done at any time the C4 engine is running by following these steps:
- Press the tilde key on your keyboard.
- Click the C4 logo on the menu bar in the bottom-left corner of the screen.
- Chose "Open model..." from the menu.
- Chose an animated model that you have imported to C4.
- Pick an animation from the panel on the left.
- Slide the frame slider to the frame you wish to attach an animation cue to.
- From the top menu bar chose "Cue" → "Insert Cue...".
- Finally, enter a four character identifier for the cue and press "Ok".
After you animation cues have been inserted or modified the changes to the animation will need to be saved. This can be done by choosing " Model" → " Save Animation" from the top menu bar. Dexsoft's Tech Trooper model, as distributed with this book, comes with these animation cues already in place for the " TechTrooper/Run" animation. However, the " TechTrooper/Walk" animation is provided with no animation cues in place, so you can try your hand at adding them in. Of course once you've inserted animation cues into an animation you will want to ensure that something happens when a frame containing an animation cue is played.
Figure 17: Insert Animation Cue
SimpleAnimationCues
As mentioned earlier in the chapter, frame animators are responsible for the play-back of animation resources. Along with the animation itself, animation resources also store any animation cues that you've inserted using the model viewer. As such, it is the responsibility of the FrameAnimator class to notify our game code when an animation cue is encountered during playback.
Whenever a frame animator encounters an animator cue during playback it posts an event, using the animation cue's four character identifier as the event type. Events posted by a frame animator can be handled by setting a FrameAnimatorObserver as a frame animator's observer.
In response to an animation cue event firing, the frame animator's observer then calls a user-defined member function allowing custom code to be executed for the animation cue encountered.
In this sample, we take advantage of animation cues already in place for Dexsoft's Tech Trooper model. In order to minimise the use of complex unrelated code the SimpleAnimationCues sample is based on the SimpleFrameAnimator sample.
SimpleAnimationCues.h
…
class SimpleAnimationCueCharacterController : public CharacterController
{
private:
FrameAnimator *frameAnimator;
FrameAnimatorObserver
frameAnimatorObserver;
void HandleAnimationEvent(FrameAnimator *frameAnimator, CueType cueType);
…
public:
…
};
…
SimpleAnimationCues.cpp
…
void SimpleAnimationCueCharacterController::HandleAnimationEvent(
FrameAnimator *frameAnimator, CueType cueType)
{
static const char *stepSounds[4] = { "sound/step/Wood1", "sound/step/Wood2",
"sound/step/Wood3", "sound/step/Wood4" };
// The "TechTrooper/Run" animation has two animation cues with the 'STEP' cue
// type.
if (cueType == 'STEP')
{
// Randomly chose one sound from the four above.
const char *randomSoundName = stepSounds[Math::Random(4)];
// Create a omnidirectional sound source with a range of 25.0F and volume of
// 0.15F
OmniSource *source = new OmniSource(randomSoundName, 25.0F);
source->SetSourceVolume(0.15F);
// Add the omnidirection sound to the model's supernode at the same
// position as the model itself. Given that the model's origin is its feet
// this is a good spot to place the stepping sound.
Model *model = GetTargetNode();
source->SetNodePosition(model->GetNodePosition());
model->GetSuperNode()->AddNewSubnode(source);
}
}
…
void SimpleAnimationCueCharacterController::Preprocess(void)
{
CharacterController::Preprocess();
frameAnimator = new FrameAnimator(GetTargetNode());
GetTargetNode()->SetRootAnimator(frameAnimator);
// Setup the frame animator to play the animation defined by the
// "TechTrooper/Run" animation resource file. Then setup the frame interpolator
// to play the animation forward and loop.
frameAnimator->SetAnimation("TechTrooper/Run");
frameAnimator->GetFrameInterpolator()->SetMode(kInterpolatorForward |
kInterpolatorLoop);
// Create a frame animator observer that has
// SimpleAnimationCueCharacterController::HandleAnimationEvent() as the callback
// method when an event occurs.
frameAnimatorObserver =
new FrameAnimatorObserver(this,
&SimpleAnimationCueCharacterController::HandleAnimationEvent);
frameAnimator->SetObserver(frameAnimatorObserver);
}
…
You may notice in the code above that we add an OmniSource node the world and make no attempt to keep track of it in order to delete it when the sound finishes playing. This is because Source nodes will automatically delete themselves when they have finished play-back of their sound, which will obviously only occurs if the source isn't set to loop playback.
Another thing that may have caught your eye is that we're passing a pointer to SimpleAnimationCueCharacterController::HandleAnimationEvent() , which is a member function (method) and not a static class method. This is reasonably rare, but perfectly legitimate C++. Pointers to member functions are not overly common due to a few restrictions and limitations with respect to how they're implemented in C++. Internally C4 implements a clever work-around or two to enable their use in this particular circumstance.
Allowing the use of member functions as callbacks is actually quite elegant. However, due to the intricacies of the way the Observer class is implemented in C4, it is not recommended that you attempt to use virtual methods as callbacks for observers.
If you compile and run the SimpleAnimationCue sample you'll see a character running on the spot, as in the SimpleFrameAnimator sample. However, this time around if you have speakers or head-phones turned on; you should hear footsteps in time with the animation.
The SimpleAnimationCue sample show-cases just one simple example of how animation cues can be utilised in a game. They can also be utilised to implement substantially more interesting game-play features. A few examples of how animation cues can be used to aid in the implementation of game-specific functionality can be observed in Terathon's own game3. The 31st uses animation cues to determine precisely when damage should be dealt during a melee attack animation. Additionally, The 31st also uses animation cues to indicate precisely which frame of an animation that hand-held projectiles should be released from a character's hand.
Summary
Having read this chapter you should now have a good understanding of how to you can utilise C4's built-in Animator sub-classes to play-back simple bone-based animations. You should know how you can play-back frame-based animations for models containing a skeleton. You should also be capable of producing a wide variety of new animations at run-time, by mixing together frame-based animations in a variety of different ways.
Additionally, we've covered the use of animation cues. In particular, how they can be used to implement audio play-back at reasonable times through-out an animation. We've also mentioned at a few other ways in which animation cues can be used to help implement game specific functionality.
The functionality described through-out this chapter is enough to implement many of the animations required for a wide variety of games. Clever use of blending and merging animations alone can be enough to produce a truly immersive experience for players of your game.
Conclusion
What Next?
Whilst you should now have a good understanding of the pre-canned functionality C4 provides you with, we've only skimmed the surface of what C4's animation system can truly achieve. In addition to the functionality covered in this chapter, C4's animation system also allows the use of a variety of much more technically advanced techniques which can be implemented as custom Animator sub-classes. Some custom Animator sub-class samples ("Animation Chapter Extras") accompany this chapter's resources and are provided as additional reading.
1 Khronos Group, COLLADA 3D Asset Exchange Schema, http://www.khronos.org/collada/
2 Dexsoft Games - http://www.dexsoft-games.com/. Note custom animation by Adrian Slusarek.
3 Terathon's Game, The 31st - http://www.the31stgame.com/
Addendum
- This chapter and code: Copyright Benjamin Dobell. License terms to use code for commercial and non-commercial purposes is in the asset downloads section for the book. Please visit www.macktek.com.Tech Trooper model pack provided by Dexsoft Games: http://www.dexsoft-games.com/License for personal educational use only. Tech Trooper may not be distributed or sold without additional purchase from Dexsoft Games. The animations included with the Tech Trooper model were customized for this chapter. Full details of license and restrictions available with chapter assets.
( This Page Is Intentionally Left Blank )