Friday, December 30, 2011

Watching It For Hours: Making Character Animations Less Predictable, More Interesting and More Controllable

For my graduate school capstone project I worked on a game called Remote Shepherd, which, more so than many other games, consisted largely of sitting back and watching virtual people. For the development team this meant that the player is spending a lot of time watching and considering our character animations. To try to make this more interesting, and to keep the animation and AI systems from becoming entangled, I developed an animation control system that would allow character animations to be more dynamic, thus more interesting. The system also allowed the use of many different sets of character animations by combining atomic animations, as well as allowed the AI system to control the character animations in as simple a way as possible. I called the system the State-subState-Set Animation Engine [3SAE].

As the name might imply, 3SAE is a three-layered system: on top is a finite state machine of sorts (the Animation State Machine), each node of which contains a Markov chain (the Animation Sequence Graph), and each node of that draws from the third layer, an animation dictionary (the Animation Map). Let’s start with the Animation State Machine.

Animation State Machine

The Animation State Machine is similar to a normal Finite State Machine, except that there are no predicates. Each node is a cycling animation, and each edge represents a transitional animation. For example, you might have an Animation State Machine that looks like this:
Each animation, both the nodes and edges, are actually animation sequence graphs containing sub-states. It’s not a terribly important point right now, but it might be helpful to keep in mind.

An animation state machine could be set up like this, where this->UID is the unique ID of the owning agent:

            this->animStateEngine = new AnimationStateEngine(this->UID);


            AnimationState* walk = new AnimationState(this->UID, "Walk");
            this->animStateEngine->addState(walk);

            AnimationState* run = new AnimationState(this->UID, "Run");
            this->animStateEngine->addState(run);

            AnimationState* sit = new AnimationState(this->UID, "Sitting");
            this->animStateEngine->addState(sit);

            walk->addEdge("Sitting", "StandToSitBench");

            sit->addEdge("Walk", "SitToStandBench");

Transitional animations are animations that are used to transition from one cyclical animation to another. They are meant to be played one time before a cycling animation starts and are used to prevent disruptive jumping when changing from one cycling animation to another, drastically different one.

Some might wonder why transition animations are needed if you’re using a system such as Havok, which has the ability to mathematically interpolate between animations smoothly. The simple answer is that it is not able to cleanly interpolate between complex poses, such as from sitting cross-legged to standing. It also lacks the little touches that give an animation style or realism, such as anticipation and secondary motion. Third, by explicitly defining the transition animation you can explicitly define different transition animations for each agent, making them more interesting. Maybe one guy always takes a deep breath before standing, and someone else always puts their hand on their knee.

As said before, there are no predicates in the animation state machine. Instead it is controlled by the agent’s AI system. The AI system sets the current state of the animation state machine, perhaps by code such as this:

pAgent->animStateEngine->setCurrentState("Sitting");

Each agent has their own animation state machine, so in the line of code above pAgent is the agent this particular AI system is controlling, who in turn has an animation state machine called animStateEngine. This is the only control the AI system gets over the animation system. It’s important that the AI system have as little to do with the animation system as possible from a coding philosophy view point and an encapsulation view point. Animation is an expression of AI, not AI itself, thus the AI should only tell the agent to express itself, not how. The AI system shouldn’t care if an animation is done, or if it’s cyclical; that’s the job of the animation system. By keeping the number of hooks a particular system has into another system to a minimum, that system can be more easily transplanted into another project. In this case, the only hook the AI system has into the animation system is a bit of code telling the animation engine which state it should be in, instead of dozens of lines of code keeping track of other things that should only be relevant to the animation system.

When the current state of the animation state machine is changed, it waits until the previous state finishes its cycle, stops it, plays the transition animation once, then starts cycling the animation for the current state.

Remember when I said each node wasn’t a specific animation, but rather an animation sequence graph? About that …

Animation Sequence Graph

An animation sequence graph is a Markov chain of possible animations (actually, not animations but animation sets, more on that later). An animation sequence graph for the “Running” node from the animation state machine might look like this:
A Markov chain is sort of a finite state machine with probabilities instead of predicates. Every time a cyclical animation completes a loop, a random roll determines which animation to play next based on which animation has just played. The system is memoryless, so the only thing that matters is what just played, nothing before that affects the system. In this case, if the animation that just played was “Run” there is a 95% chance that “Run” will play again, and a 5% chance that the “Stumble” animation will play. You can see by looking at the graph that “Stumble” will never play twice in a row. We don’t want our character looking too clumsy.

An animation sequence graph might be set up like this, where this->UID is the unique ID of the agent that owns this animation sequence graph and the AnimationSubState object corresponds to the nodes of the Markov chain:

            AnimationSubState* runSS = new AnimationSubState(this->UID, "Run");
            AnimationSubState* tripSS = new AnimationSubState(this->UID, "Trip");
            runSS ->addEdge(tripSS, 0.05);
            runSS ->addEdge(runSS, 0.95);
            tripSS->addEdge(runSS, 1.0);

Animation sequence graphs can give variety and personality to normally repetitive animation states, such as walking or idling, while using a handful of short, atomic animations instead of a bunch of long, highly-specific animations.

They can also be used almost as a simple animation scripting system. Consider a graph of a few nodes, each only having one edge to the next and the weight of that edge being 1.0. This would be executed as: play animation A once, then play animation B once, then play animation C once, etc.

Extremely complex animation cycles can be created with an animation sequence graph, and despite the essential randomness of the system, a measure of control can be maintained by being careful with your edges. For example, take this animation sequence graph for an “Idle” node (probabilities not shown):

You would have to be careful with probabilities to reduce the chance of unintended loops. Alternatively, you could have multiple nodes representing the same animation to be used in different places in the graph. Lastly, the probabilities can be used to establish personality. Maybe this character is terribly paranoid of bad breath for instance.

But remember, even these nodes don’t represent specific animations, rather, they represent sets of atomic animations as part of an …

Animation Map

The animation map is the simplest concept of the three layers. Each agent gets one and all it does is map animation names, e.g. “Walk”, to a specific animation. The easiest way to implement an animation map is to use something like a dictionary or other associative array if your language supports it. Setting up the animation map might look something like this:

animations["Walk"] = "Walk_injured03";
animations["Run"] = "Run_injured02";
animations["Trip"] = "Trip02";

If you set up your animation sequence graphs to give your animation engine (you know, the one that actually plays animations, not the one being described here) values from your animation map instead of hard values, then you might have the following code in your AnimationSubState class:

               pAgent->animMesh->playAnimation(pAgent->animations[this->anim]);

To break it down: pAgent->animMesh is your animating geometry (in Remote Shepherd’s case, a Havok Animation Mesh) assigned to your agent. playAnimation is simply the play function of your animation mesh. pAgent->animations is the agent’s animation map, and lastly, this->anim is the name of the animation assigned to this AnimationSubState.

The benefit of the animation map is that many characters can use the exact same animation state machine and/or animation sequence graphs but still look different thanks to separate animation maps.

Putting it all together

Here’s an example animation setup from Remote Shepherd using all three layers:

             // set up animation map 
            animations["Walk"] = "Walk_male03";
            animations["Run"] = "Run_scared01";
            animations["Trip"] = "Trip02";
            animations["StandToSitBench"] = "StandToSitBench01";
            animations["SitToStandBench"] = "SitToStandBench00";
            animations["Sitting"] = "Sitting03";

            // set up the animation state machine
            this->animStateEngine = new AnimationStateEngine(this->UID);

            // walk state
            AnimationState* walk = new AnimationState(this->UID, "Walk");
            this->animStateEngine->addState(walk);

            // run state, which will actually be an animation sequence graph
            AnimationState* run = new AnimationState(this->UID, "Run");
            this->animStateEngine->addState(run);
            // set up the run animation sequence graph
            AnimationSubState* fleeSS = new AnimationSubState(this->UID, "Run");
            AnimationSubState* tripSS = new AnimationSubState(this->UID, "Trip");
            // add the edges to animation sequence graph
            fleeSS->addEdge(tripSS, 10);
            fleeSS->addEdge(fleeSS, 90);
            tripSS->addEdge(fleeSS, 1.0);
            // add each substate to the run state
            run->addSubState(fleeSS);
            run->addSubState(tripSS);
            // set the starting state of this animation sequence graph
            run->setCurrentSubState(0);

            // sit state
            AnimationState* sit = new AnimationState(this->UID, "Sitting");
            this->animStateEngine->addState(sit);

            // add the edges to the animation state machine
            walk->addEdge("Sitting", "StandToSitBench");
            run->addEdge("Sitting", "StandToSitBench");
            sit->addEdge("Walk", "SitToStandBench");
            sit->addEdge("Run", "SitToStandBench");

Let’s Review

The point of creating this system was two-fold: to allow character animations to be more interesting and varied without putting a huge strain on our animation team, and to provide the AI system a means of controlling character animations without actually being too involved in it. The second goal is accomplished quite easily by setting up our finite state machine to be smart about transitions. Accomplishing the first goal is a little more involved. By using a layered system of finite state machines, Markov chains and dictionaries we can create complex, varied, and dynamic character animations by combining simple atomic animations. A nice side-effect of the use of a Markov chain is that personality can be created in an agent by tweaking the edge weights. For a development team with a small art division, and especially one with a project like Remote Shepherd in which much of the game is watching character animations, this system was vital, and helped a great deal in accomplishing our core design goal.

No comments:

Post a Comment