Game Development Community

Player animation code... description and 2 questions

by Joel Baxter · in Torque Game Engine · 05/24/2002 (4:14 pm) · 7 replies

It's gotten to the point where I'm going to be required to do unspeakable things to our player model and its animations, so I've been poking into the animation code, specifically for the Player class. Here's what it looks like to me so far... corrections welcome, and you'll also see that there are a specific couple of things that I'm puzzled about.


First of all, some lists about what the code looks for in the player DTS.

Sequences currently used (first eight are required):

"root" - idle

"run" - run forward

"back" - run backward

"side" - run left

"fall" - fall through the air

"jump" - jump while running

"standjump" - jump while standing

"land" - land after a fall

"look" - arm vertical motion, for weapon aiming; goes from -80 degrees to +80 degrees

"head" - head vertical motion, from -80 degrees to +80 degrees

"headside" - head horizontal motion, from datablock -maxLookAngle to +maxLookAngle (-100 degrees to 100 degrees)

"light_recoil", "medium_recoil", "heavy_recoil" - thread created for each of these sequences, but actually only the last one is used; it's activated if a mounted ShapeImage enters a state that is marked for recoil (of any kind)

"Damage" - sequence showing the transition from undamaged to dead

"Death0", "celwave", "celsalute" - triggered from the example scripts using setActionThread


Nodes that the engine code can use:

"Bip01 Pelvis" - x/y movement of this node moves the player shadow

"Bip01 Spine", "Bip01 Spine1", "Bip01 Spine2", "Bip01 Neck", "Bip01 Head" - all Bip01 nodes, including the pelvis, are involved in something I'll describe later

"eye" - first-person POV point; required

"cam" - third-person POV point; if not present, defaults to be the same as "eye"

"mount0" through "mount31" - weapon mount point (mount0) and others

"AIRepairNode" - exposed to scripting but unused in the example scripts


Details currently used:

"Collision-1" through "Collision-8" - set of hulls used for colliding polyhedrons with the shape

"LOS-1" through "LOS-8" - set of hulls used for colliding rays with the shape; if not present, defaults to the corresponding "Collision-#" hull


---

initialization:

The first eight sequences listed above are put into the first eight slots in the actionList and given special treatment in various situations. There may be 68 total sequences; obviously the current engine code doesn't use nearly that many, but any available sequence can be initiated from the scripting, like "Death0", "celwave", and "celsalute" are.

For each sequence that is not a death animation, the code extracts the "ground transform" to determine what direction the animation is moving in and at what speed. Among the some of the first eight sequences, the direction information is used to select a sequence appropriate for the direction of player movement, and the direction and speed information is used along with the current player direction and speed to calculate what speed to play the animation at. The info isn't used for any sequences outside the first eight, even though it is calculated for them.

If there is no ground transform for a sequence, it will end up with a recorded direction and speed of zero, except for the "run" and "back" sequences which have appropriate default directions and a default speed of 1.

A death animation is any sequence that has a name starting with "death" (case-insensitive); these will always be forced to have a direction and speed of 0 for this initial ground transform calculation, but the motion of a model through the death animation is still taken into account as described later.

A player object has a thread for running the "look" sequence (arm motion), two threads for the head sequences, a thread for recoil, a thread for the damage sequence, and the "action" thread used for everything else. Sequences played on the look, recoil, and head threads should be blend sequences... probably ditto for the damage thread as well.

Up to 32 more threads can be run on the player (or any ShapeBase object) using playThread (and related functions) from the scripting. Perhaps these could be blend sequences, or high-priority sequences to override the current action.

---

on processTick (server and client):

ShapeBase code first advances any threads that were started using playThread.

In the Player code, the server then does advanceTime for the recoil thread, and for the action thread as well if it is marked for animateOnServer (which will only be the case when the action thread is running the "land" sequence or a sequence invoked from the scripting by setActionThread).

I'm not entirely sure what the point of animateOnServer is. It doesn't actually transform any nodes of the shape, although it does set the appropriate nodes as "dirty" so that they will be transformed if the server chooses to animate them. I don't see where the server would do this, though. There's some indication that in the past the server used to animate the nodes to figure out where the player POV was before doing a scoping query, but it looks like currently the server just uses the untransformed eye node position as the starting point for scoping queries... soooo, I dunno what to think about this.

That's puzzle #1. Onward:

The Player code (either on client or server) then checks to see if the current state is RecoverState (after a fall), and if so, what needs to be done. The "land" sequence is played on the action thread during the first part of recovery, then played backwards slowly; this backward phase lasts until the player velocity drops below a threshold, at which point the player goes to MoveState.

If the player comes in contact with a ground surface, any in-progress jump sequence is cancelled.

Initiating a jump starts the appropriate jump sequence on the action thread.

The sequence on the "look" thread is then set to point the arms at the correct vertical angle, according to the pitch of the player's lookangle.

The positions of the sequence on the two head threads are set according to the pitch and yaw of the player's lookangle.

If the player is executing a death animation, find out how much a tick's worth of that animation is going to move the player. This will be added to whatever movement is calculated for the player for the upcoming tick.

If the player hits something hard, put them into RecoverState as described above.

Then for the rest of the animation-related code on the tick, we're back to server-only stuff.

The server decides if it's time to possibly update the sequence used by the action thread. First of all, if no sequence has been selected yet, an update is obviously required. During the normal course of things, though, it will check for an update if:

- The sequence has either ended, or the thread is not marked waitForEnd
AND
- The thread is not marked holdAtEnd
AND
- The sequence has been animated for at least 4 ticks

If a thread is marked holdAtEnd, it will also always be marked waitForEnd.

FYI the "land" sequence is used with waitForEnd, as are both jump sequences and any sequence invoked with setActionThread. The movement sequences are not. And none of the current code uses holdAtEnd.

So, if an update is called for, and the player is in MoveState and not dead, then a movement sequence (idling, falling, or running in some direction) is selected. For the running sequences, the selection is based on the directions initially calculated for those sequences and the current player movement direction. Note that the sideways-left animation is just played in reverse if going sideways-right.

On an update check the engine code will also call out into the scripting to execute animationDone in the namespace of the player datablock, if the just-completed sequence was not one of the first eight.

If the action thread sequence is now one of the first eight in the actionList, and it has some speed that was extracted from its ground transform, then the speed of animation playback is adjusted according to the current speed of player movement.

Finally, there's some code that plays with the Bip01 nodes, which is puzzle #2. If the action thread is marked firstPerson -- which by the way exactly tracks whether it is marked animateOnServer -- then all the spine nodes will be set to mode 0. Otherwise they will be set to mode MaskNodeAllButBlend. If I had to guess, I'd say that MaskNodeAllButBlend prevents those nodes from being animated, but again, there's the question of when nodes would be animated on the server, and why we are doing this to the spine nodes.

---

on interpolateTick (client only):

Not much animation action going on in interpolateTick, we just update the positions of the sequences on the "look" (arms) thread and head threads.

---

on advanceTime (client only):

First, the code determines if the current action thread sequence has been animated through designated trigger points which indicate that footprint decals should be laid down, and footprint sounds played. Then it goes through the rigmarole of possibly selecting a new movement sequence, just like the server did on the tick, although the client doesn't do the animationDone callout.

Time is then rolled forward for the sequences on the action and recoil threads.

The mysterious dance of the spine nodes is performed as well, this time with the additional behavior that mode must be zero if the player object is not the one being controlled by this client, or if it is but the POV is third-person. If my guess is right about MaskNodeAllButBlend, then that sorta makes sense, because you don't see your own body when in first-person POV so there's no need to animate it (altho, what happens then if renderFirstPerson is true in the datablock and the RenderMyPlayer pref is true?).

Finally, if the player object is being controlled by this client, then the nodes are animated now, so that the player POV will be in the correct spot when it comes time to render everything.

#1
05/29/2002 (2:54 pm)
More code-reading, testing, and guessing:

The spine node code is probably used to "lock your spine" while in first-person POV, so that your viewpoint stays rock-steady as you move around. Even though, for example, the default player model leans forward a little when it starts moving, you won't see this affect your viewpoint if you are in first-person. This isn't a hierarchical effect that cascades down to all nodes in the model; if the player datablock has renderFirstPerson true, and $Pref::Player::RenderMyPlayer, then you can still see your feet and arms move around in first-person POV... just your spine doesn't move.

The firstPerson boolean that affects the action animation thread is used to override this behavior; it allows an animation to move your spine nodes around, and therefore your POV (since I assume the eye node references the head node). This is why it is true by default for animations triggered from the script. If you trigger an animation that sends your character into a Dance Dance Revolution, it's neat to have the viewpoint bob around appopriately. And for the landing/recovery animation, you want the viewpoint to be shaken.

OK, next, let's assume that animateOnServer exactly tracks firstPerson because the server needs to know exactly where the player POV is. Not exactly exactly -- I noticed that the server won't mess with transitions between sequences even if animateOnServer is true -- but at least it wants to know if animation has grossly displaced the POV.

As I mentioned above, though, all the Player code does on the server for animateOnServer is advance the action thread time. This marks the "subshapes" (collections of nodes, "details") that are affected by that thread's animation but which doesn't actually animate the shape to move the nodes around.

How about ShapeBase then, does it ever animate the shape on the server? Yus, it does animate collision details. That doesn't seem to have any relationship to the above deduction about the way that animateOnServer is being used, but it's a moot point for the TGE SDK player setup since that doesn't use collision details for the player model (just a bounding box). In a game where you were using collision details with players, you'd probably always want the action thread to be using animateOnServer.

That's really all I can find. The only other relevant tidbit I can come up with is this comment from ShapeBase::onCameraScopeQuery:
LH - always use eye as good enough, avoid camera animate
which may indicate that in the past, the shape was animated to get its POV position as the starting point for the scoping query, but currently the untransformed eye node position is used as "good enough". If this was the only thing that the Player code was using animateOnServer for, then it's a relic now.

Plausible? Anyone else around here ever looked at the animation code?
#2
05/29/2002 (3:16 pm)
Excellent work Joel, keep going like this and I'll understand the damn engine :))

Phil.
#3
05/29/2002 (3:27 pm)
That's assuming I ever understand it, of course. :-) Perhaps some things man was not meant to know.
#4
05/29/2002 (3:28 pm)
hmmm, I think I see the shadow of a game lurking around Joel's last reply ...
#5
05/31/2002 (1:15 pm)
Animate on server. It does looked like a call to animate() was removed from somewhere. Setting the action's animateOnServer flag will sort-of work, as animate() is called under some circumstances, but it's not currently setup the way it was originally. Normally you don't want to waste server time animating, but if an animation affects a node (such as gun mount point), then it needs to animate on the server. Can't remember if it was being called for the POV. I imagine animate was showing up on the server profiles, and instead of turning off any unnecessary threads (or clearing there' animateOnServer flag), someone just took out the call to animate().

Your right about the Bip01 & Spine manipulations. All the player motions for T2 (and T1) were full body motion capture. These looked fine from third person, but were totally unacceptable from first person. Slight movements in the head (and thus the eye) were impossible to remove from the motion capture, so basically the whole upper torso is "frozen" when in first person. This was only an issue because T1 & T2 show the body first person, and the feet and arms still needed to animate while keeping the eye steady. If you don't display the body first person (as Realm Wars and most other FPS don't), then it's a lot easier; you'd just fix the eye somewhere and be done with it.
#6
05/31/2002 (2:29 pm)
Quote:That doesn't seem to have any relationship to the above deduction about the way that animateOnServer is being used, but it's a moot point for the TGE SDK player setup since that doesn't use collision details for the player model (just a bounding box). In a game where you were using collision details with players, you'd probably always want the action thread to be using animateOnServer.

I am extremely interested in this topic, as the POV moves in RealmWars when the Wave and Salute animations play, but never anytime else. ???

How would one go about renabling this behavior for other animation sequences? Or just having it on all the time?

That way when you play the "whacked on the noggin with the skillet" animation or the "dramatic spin and fall death" animation, the POV will track what the player would actually see?

Anyway, I am working on figuring out how to do actual model collision detection on projectile hits. So I need the animateOnServer functionality for sure.
#7
06/12/2002 (4:51 pm)
Quote:I am extremely interested in this topic, as the POV moves in RealmWars when the Wave and Salute animations play, but never anytime else. ???

Yep, cf. above, "In the Player code, the server then does advanceTime for the recoil thread, and for the action thread as well if it is marked for animateOnServer (which will only be the case when the action thread is running the "land" sequence or a sequence invoked from the scripting by setActionThread)." The wave and salute anims are sequences invoked by the scripting.


Quote:How would one go about renabling this behavior for other animation sequences? Or just having it on all the time?

You need the animateOnServer and firstPerson fields to be true, so change the Player code appropriately.


Quote:Anyway, I am working on figuring out how to do actual model collision detection on projectile hits. So I need the animateOnServer functionality for sure.

Yeah, this is a concern for us as well. There's some challenges there, though. If you're using collision details to detect hits, then the client needs to have the same idea as the server about what the current positions for the various sequences are. This means that you have to take a close look at fudgery like "freezing nodes" on the client but not the server; if that's a matter of suppressing very tiny differences, it's probably not a big deal, but the larger the differences suppressed, the more likely it is that you'll be creating significant differences in the collision hull on the server compared to that used on the client.

Using animated collision details also means that if you're doing extrapolation -- which, if you're interested in accurate client collisions to the point of using collision details rather than a bounding box, you probably will/should be -- you have to be careful to extrapolate the animations as well as the object positions.

Also note that currently, animation transitions are only done on the client, not the server. This is another source of client/server disagreement on the state of the collision hull, and you have to decide if it's an important difference based on your gameplay and on the animations in question.

There's additional concerns if you're talking about the control object, because of the local prediction of the control object's behavior. (The collision detail animation issue is actually the main reason I started looking at the networking.) Animations directly controlled by your move input will be pretty well synced with the server. However, animations triggered on the server -- because of a detected collision, or some scripted event, or whatever -- will probably have run along for many frames on the server by the time the update containing the do-this-animation command reaches the client.