Script-side Player Animation Definitions
by Daniel Buckmaster · 08/21/2009 (2:03 am) · 11 comments
Last resource update: 23/08/2009 4:58pm (GMT+8)
At the moment, TGE has a hardcoded list of animations that the Player class can use. This means a simple change like adding a new movement animation becomes a case of recompiling the engine. It also means all Players have to use the same animations. This resource breaks those animation definitions into script for easier control over animations used by different Players.
[So much for the 5,000 word limit :P]
[I love how all my text formatting is kept, so I can space out my paragraphs!]
Developed and tested on TGE 1.5.2. Not network tested, but I see no reason for it not to work.
Length: Long (Zip file with code and script changes available here, thanks to EB!)
Difficulty: Advanced (significant alteration to Player class, working knowledge of C++ and ability to make own code changes assumed, not for beginners!)
Disclaimer
Before I start, I'll offer a few words about what this resource is not, and how you can expect to use it. First, it’s not a way to change your character’s animations in real-time, nor is it an animation manager. You can’t really do much more with it than Torque’s stock animation definition system. I’ve also posted no screenshots or videos, because when it works, it looks exactly the same as stock Torque. That was my goal – to seamlessly replace the back-end without changing any of the effects. Also, this modification only affects ‘action animations’ – animations typically used to represent movement. Arm animations are a separate can of worms that I’ll be taking a big juicy bite out of soon.
So why bother with this resource at all? What it does is make the system far more flexible, extendable, and most importantly, script-controlled. This won’t do much for your stock TGE installation – but if you plan on adding things like stances to the Player class, then this should help you add extra animations for each stance with far more ease than the default engine. At the end of the resource, I’ll provide an example of extending my system to add different animations for different movement speeds (you can test it quite simply with an AIPlayer).
The existing system
First, I’m going to lay out how Torque already handles animation definitions. Understanding this is a good idea, so you can see the reasons behind my changes more clearly. It’ll be helpful for you to browse through the source as I’m describing things.
The existing action animation system consists of three parts. The first, and the prime culprit, is the ActionAnimationDef struct defined in PlayerData. It stores two basic pieces of information about an animation: its name, and its ground transform direction (which direction of movement the animation represents). A static array of these is created at the top of player.cc, which together define all the animations the Player class code is aware of. (Yes, I specified the code. You can play script-side animations, but the code will treat them as second-class in favour of the animations defined in this table.) These are all the movement animations Players can perform – and the number is woefully small. You’ll recall many resources, such as the venerable ‘new Player positions’ resource which allows you to add crouching, prone and swimming stances, have you add entries to this table to make the Player code aware of a wider range of animations – such as crouched movement.
The second part of the action animation system is the ActionAnimation struct, also defined in PlayerData, conveniently right below ActionAnimationDef. Each Player datablock stores a bunch of these in an array called actionList. These ActionAnimation objects are basically ActionAnimationDefs with more data which is specific to the datablock in question.
This point is key to the whole system. The static ActionAnimationDef table is a constant across all PlayerData datablocks. So it has to be able to be reused with many different shape files and animations. It just contains the very basics for each animation – name and direction. The ActionAnimation struct, however, only has to cover an animation for one datablock – so we know which shape file and animation files are being used. So this struct includes specific data such as the sequence in the shape file, the length of the ground transform, etcetera.
The third and final part is the enumerated table of animation names also defined in PlayerData – enums such as RootAnim, SideLeftAnim, and FallAnim. If you’d implemented the ‘new Player positions’ resource or similar, you’d have had to add entries to this table to mirror the changes made to the ActionAnimationDef array I mentioned earlier. This table provides an easy way to access the animations in the static array. We know that RootAnim’s numerical value is 0 – just like, in the array in player.cc, the “root” definition is at position number 0. So in code, all we have to do is call for RootAnim, and we’re pointed to the right animation definition.
The tricky part is that we’re not pointed to the static array at all, but the array in the datablock, actionList. When the datablock is created, it copies all the data from the static array into its own array (in PlayerData::preload), and from there on in the static array data isn’t used again. Then when the player wants to animate with a specific action (say, SideLeftAnim), it uses the SideLeftAnim enumerated value as the index for the actionList array to get the sequence number.
Sounds complicated, right? I took ages to wrap my head around it. Trust me, reading the source helps.
So what I am doing here is replacing the static array with a new object that is dynamic – a new datablock object which you define in scripts. The functionality is nearly the same. Now, instead of copying data from the static array into the datablock array, the PlayerData object stores references to the information in the new datablock object. The end effect is the same, but lots of terminology has to be swapped around.
The code changes
That’s enough of my rambling. Here’s what you need to do to break your action animation definitions free of hardcoding. I’ve broken it into three easy steps: the new datablock object, the associated changes to the Player class, and script changes.
Part 1. ActionAnimationData
The first step is to add the new object to the engine. This is a datablock object inheriting directly from GameBaseData, though there’s no associated non-datablock object. I won’t go into too much detail on the object here – it’s a simple class and well commented, so please browse through and see how I’ve built it.
Add the following code to the top of player.h after the class declarations but before the definition of PlayerData:
Now all that code has been defined, it’s time to implement it. I’ve also tried to comment this code as well as I can, so you should be able to get a good feel for how the class works. Add this code to the top of player.cc after the double dashed line:
Now we’ve defined the new object, you can actually recompile and run the engine. You can define an object of this type with all the appropriate fields – but we’ll save that until the end. This is the end of Part 1. Congratulations! Now the hard work starts.
Part 2. Changing Payer to accommodate our changes
This is the largest and most tedious part. Now that we’ve added the new class, we have to make the Player and PlayerData classes use it instead of the old system.
First things first – let’s rip all the old stuff straight out! Find and remove the following code blocks from player.h:
Right, now that’s over with. The next big change is all the new and changed definitions in player.h. I’ll walk you through them.
First, we need to add a reference in PlayerData to an ActionAnimationData object. Put this code where you ripped out the ‘struct ActionAnimationDef’ code in the declaration of PlayerData:
You’ll notice that in removing that block of enums from player.h, we removed some useful constants that are referred to throughout the Player class. We’ll go ahead and replace these now. You can do this very easily with a simple find/replace command, or you can search through manually and replace each instance. What I did was:
Replace ‘PlayerData::NumTableActionAnims’ with ‘mDataBlock->animationData->numValidAnimations’ and ‘NumTableActionAnims’ with ‘animationData->numValidAnimations’ (make sure you do it in this order!) (should be 9 occurrences overall)
Replace ‘PlayerData::NumActionAnims’ with ‘mDataBlock->actionCount’ and ‘NumActionAnims’ with ‘ActionAnimationData::MaxAnimations’ (2 occurrences overall)
Replace ‘PlayerData::NullAnimation’ with ‘ActionAnimationData::NullAnimation’ (18 occurrences)
Replace ‘PlayerData::ActionAnimBits’ with ‘ActionAnimationData::AnimationBits’ (4 occurrences)
Don’t forget to find/replace in both player.h and player.cc!
One last thing – after the definition of ‘U32 lookAction’ in PlayerData, add:
Okay. Now we’ve changed all those definitions we can go ahead and implement those changes in player.cc. First, to the constructor of PlayerData, add the following wherever it makes sense to you:
The next big change is to PlayerData::preload. It’s here that we used to copy the static array data into our datablock array. Now we store a reference and do some other things. Replace all the code in PlayerData::preload between and including the following lines:
Those changes require we do things a little differently in getGroundInfo. It’s mostly minor changes – if you can be bothered to read through them, good on you, but I wouldn’t consider it necessary. Replace the existing PlayerData::getGroundInfo method with this one:
In PlayerData::initPersistFields, we need to make the script aware of the new reference in PlayerData. Add the following line somewhere in that method:
Now, due to the fact that we removed both the ‘name’ and ‘dir’ members of ActionAnimation, we need to replace bits of code that reference them with the new way to get that information. I’ll give you the first example and let you find all the other places where it’s necessary to make this change – it’s not hard! Probably another case of find/replace, or even compiling and just fixing the errors you’re given.
On line 939 of stock player.cc, in method Player::onNewDatablock, find this line:
Now we need to update the way Players try to find suitable animations. Before we would pick a single animation using the values defined in the enum table. Now there’s no enum table, and I’ve written up a new way to search for the animations we need.
The ‘type’ member of ActionAnimationDef basically replaces the enum table that we used before. I’ll use the first replacement as an example. This code is in Player::setState:
Introducing the Animation Search State! Let’s add it to player.h and I can tell you all about it. After this line in the definition of Player:
Let’s go ahead and add the new methods now (I put them after pickActionAnimation):
Now, one more example. In Player::updateMove, you should find this (after the find/replacing above):
Now, there’s lots of spots where the table enums are used for different things. I’m not going to list every one of them. Just go through finding every instance of each enum value (SideLeftAnim, RunForwardAnim, FallAnim, etc.), figuring out how it is being used, and replacing it with a call to compareActionType or findActionAnimation. (The easy way, as I did it, is to complete the rest of the code changes, then compile and see where you get errors for not having defined the enums.) You can also figure out whether it would be best to specify the animation by direction. One example of where I did that was in the jump animation clause, previously:
One more place I’m going to mention directly is the pickActionAnimation method, because it’s undergone significant changes with this new regime. I basically moved the direction-choosing logic out of it when writing findActionAnimation. So here’s the new method:
That’s it for code changes! Recompile, and watch the wheels fall off. Seriously, every time I do stuff like this, I spend more time fixing typos than actually writing the code in the first place. Now, I haven’t held your hand through this process, and I expected you to be able to do some stuff for yourself. So when something breaks, don’t post here about it first, try and work out what the error message is telling you and fix it. Try searching for the error message to find similar problems and see how they were fixed – it’s worked for me before. *Then* come and post it here and I’ll do what I can for you. I’ve gone through the resource following all my own instructions, and the build worked fine – so I have faith that you can do the same!
That said, please do speak up if you find any of my instructions misleading, confusing or unclear. I of course know exactly what I mean when I say things, but that doesn’t apply to everyone.
Sorry about that little tirade ;P.
But now you’re finished with Part 2! At this point, if you load a mission, well, you won’t. Because we haven’t added an animationData object to our PlayerData, so its preload fails and when you try to join a mission, you’re stuck in limbo at the ‘loading objects’ phase, because your Player never gets added to the mission. You should see a nice console error, though.
So let’s remedy that with
Part 3. Script changes
There really isn’t much to do here, but it’s very important. Open playr.cs and add this somewhere before the definition of PlayerBody:
Oh no! That would usually require source change, and there’s an excellent resource here telling you just how to do it. But with our handy dandy new scripted animation definitions, it’s as simple as changing this:
To this:
And now you’ll see that Kork is animating when moving both directions! Of course, you’ll need an actual animation for moving right, otherwise the direction is wrong. But you should see how easy it is to add new animations now.
Right now, if you have the right animations, you could add eight-point direction movement animations to Kork, to avoid the odd ‘running forward moving diagonally’ thing Kork has going on. Also, see if you can add a different animation for jumping when standing still (hint: it’s all about the direction), since Kork already has an animation for that.
Fantastic! You’ve finished!
Now I’m going to go on and show you how to extend what I’ve made for your own purposes. I’m going to add a ‘speed’ member to ActionAnimationDef, so that different animations can be used at different movement speeds (if your Player has low acceleration, or if you’ve got AIPlayers set to a low movement speed).
We’ll start with the definition of ActionAnimationDef in player.h:
I’m not going to walk you through all the changes to the ActionAnimationData code changes in player.c, but here’s a list of what you need to do:
1. Initialise speeds and ActionAnimationDef.speed in constructor
2. Add a field in initPersistFields
3. Copy from the speeds array into ActionAnimationDef.speed in preload
4. Pack and unpack
If you’ve gotten through the rest of this resource, I’ll assume you know well how to do all that ;).
Now, we want to be able to find animations with a given speed. This ‘speed’ value is going to be a float from 0 to infinity, representing the percentage of our top movement speed we’re travelling at. So when the value is 1, we’re at full speed; 0.5, we’re at half speed, etcetera. We’ll choose the animation with the closest speed value o our actual value.
This requires two modifications: a new member in ActionAnimationSearchState, so we can specify a speed when we’re searching for animations, and a change in the way pickActionAnimation finds a suitable animation – it now needs to take speed into account as well as direction.
Easy changes: in the definition of ActionAnimationSearchState in player.h, add this:
Now we can take a look at findActionAnimation.
At the moment, the method reads somewhat like this:
1. By default, return the datablock’s root action
2. Add all actions of the correct type to a ‘matches’ list
3. If we have no matches, then bug out
4. If a non-zero direction was specified, set the return action to the one with the best dot product with our desired direction (dot products less than 0.1 (i.e. greater than 85 degrees or so away from our target direction) not considered at all)
5. If we were told to clean up, do so, and return any action we’ve found
We need to change this to factor in speed as well. I’m going to restructure the code a little. Now instead of getting matches and choosing the best one, we’re going to get matches, then remove all the bad ones according to the dot product (‘bad’, in this case, is defined as smaller than the largest dot product we’ve got so far. This allows us to have multiple animations remaining on the matches list if they are all going in the same direction). *Then* we choose the best one according to speed.
This seems to be a good standard way to choose items in order of priority. We remove entries not satisfying the dot test because we really don’t want to be playing our sideways animation when moving forwards, just because there’s no forward animation defined. However, it’s acceptable to play a running animation in place of a missing walking animation.
So in short, when adding your own checks – check in order of priority, most to least important. In all checks except the last, remove items from the matches list. In the last check, choose the best match from the list.
Here’s how that works in practise:
Now, to actually use this. The best place to specify a speed I can think of is in pickActionAnimation. So let’s do that. Change this:
Now if you recompile you should again see no changes. That’s because our scripts haven’t been updated. So in the ActionAnimationData, change this:
Well, that concludes this resource. I sincerely hope you find it useful, and that my little example implementation sparks people to adapt this basic system for their own nefarious purposes. I know this has certainly been not the most user-friendly resource (being able to upload code might help, GG…), but if you’ve gotten through it and are now basking in the scripted sunlight, congratulations. I did it – I know how much hard work it is!
See you next time.
At the moment, TGE has a hardcoded list of animations that the Player class can use. This means a simple change like adding a new movement animation becomes a case of recompiling the engine. It also means all Players have to use the same animations. This resource breaks those animation definitions into script for easier control over animations used by different Players.
[So much for the 5,000 word limit :P]
[I love how all my text formatting is kept, so I can space out my paragraphs!]
Developed and tested on TGE 1.5.2. Not network tested, but I see no reason for it not to work.
Length: Long (Zip file with code and script changes available here, thanks to EB!)
Difficulty: Advanced (significant alteration to Player class, working knowledge of C++ and ability to make own code changes assumed, not for beginners!)
Disclaimer
Before I start, I'll offer a few words about what this resource is not, and how you can expect to use it. First, it’s not a way to change your character’s animations in real-time, nor is it an animation manager. You can’t really do much more with it than Torque’s stock animation definition system. I’ve also posted no screenshots or videos, because when it works, it looks exactly the same as stock Torque. That was my goal – to seamlessly replace the back-end without changing any of the effects. Also, this modification only affects ‘action animations’ – animations typically used to represent movement. Arm animations are a separate can of worms that I’ll be taking a big juicy bite out of soon.
So why bother with this resource at all? What it does is make the system far more flexible, extendable, and most importantly, script-controlled. This won’t do much for your stock TGE installation – but if you plan on adding things like stances to the Player class, then this should help you add extra animations for each stance with far more ease than the default engine. At the end of the resource, I’ll provide an example of extending my system to add different animations for different movement speeds (you can test it quite simply with an AIPlayer).
The existing system
First, I’m going to lay out how Torque already handles animation definitions. Understanding this is a good idea, so you can see the reasons behind my changes more clearly. It’ll be helpful for you to browse through the source as I’m describing things.
The existing action animation system consists of three parts. The first, and the prime culprit, is the ActionAnimationDef struct defined in PlayerData. It stores two basic pieces of information about an animation: its name, and its ground transform direction (which direction of movement the animation represents). A static array of these is created at the top of player.cc, which together define all the animations the Player class code is aware of. (Yes, I specified the code. You can play script-side animations, but the code will treat them as second-class in favour of the animations defined in this table.) These are all the movement animations Players can perform – and the number is woefully small. You’ll recall many resources, such as the venerable ‘new Player positions’ resource which allows you to add crouching, prone and swimming stances, have you add entries to this table to make the Player code aware of a wider range of animations – such as crouched movement.
The second part of the action animation system is the ActionAnimation struct, also defined in PlayerData, conveniently right below ActionAnimationDef. Each Player datablock stores a bunch of these in an array called actionList. These ActionAnimation objects are basically ActionAnimationDefs with more data which is specific to the datablock in question.
This point is key to the whole system. The static ActionAnimationDef table is a constant across all PlayerData datablocks. So it has to be able to be reused with many different shape files and animations. It just contains the very basics for each animation – name and direction. The ActionAnimation struct, however, only has to cover an animation for one datablock – so we know which shape file and animation files are being used. So this struct includes specific data such as the sequence in the shape file, the length of the ground transform, etcetera.
The third and final part is the enumerated table of animation names also defined in PlayerData – enums such as RootAnim, SideLeftAnim, and FallAnim. If you’d implemented the ‘new Player positions’ resource or similar, you’d have had to add entries to this table to mirror the changes made to the ActionAnimationDef array I mentioned earlier. This table provides an easy way to access the animations in the static array. We know that RootAnim’s numerical value is 0 – just like, in the array in player.cc, the “root” definition is at position number 0. So in code, all we have to do is call for RootAnim, and we’re pointed to the right animation definition.
The tricky part is that we’re not pointed to the static array at all, but the array in the datablock, actionList. When the datablock is created, it copies all the data from the static array into its own array (in PlayerData::preload), and from there on in the static array data isn’t used again. Then when the player wants to animate with a specific action (say, SideLeftAnim), it uses the SideLeftAnim enumerated value as the index for the actionList array to get the sequence number.
Sounds complicated, right? I took ages to wrap my head around it. Trust me, reading the source helps.
So what I am doing here is replacing the static array with a new object that is dynamic – a new datablock object which you define in scripts. The functionality is nearly the same. Now, instead of copying data from the static array into the datablock array, the PlayerData object stores references to the information in the new datablock object. The end effect is the same, but lots of terminology has to be swapped around.
The code changes
That’s enough of my rambling. Here’s what you need to do to break your action animation definitions free of hardcoding. I’ve broken it into three easy steps: the new datablock object, the associated changes to the Player class, and script changes.
Part 1. ActionAnimationData
The first step is to add the new object to the engine. This is a datablock object inheriting directly from GameBaseData, though there’s no associated non-datablock object. I won’t go into too much detail on the object here – it’s a simple class and well commented, so please browse through and see how I’ve built it.
Add the following code to the top of player.h after the class declarations but before the definition of PlayerData:
//----------------------------------------------------------------------------
struct ActionAnimationData: public GameBaseData {
//Define a parent class so we can call its methods back up the class tree
typedef GameBaseData Parent;
//Allow Player objects to access our pivate methods and fields
friend class Player;
/// Some miscellaneous constants
enum Constants {
AnimationBits = 7, ///< How many bits we use for animation index numbers
MaxAnimations = (1 << AnimationBits), ///< 128 animations quite enough?
NullAnimation = MaxAnimations - 1, ///< Last anim slot
MaxNameLength = 64, ///< Maximum length of an animation name
GroundDirBits = 16, ///< Number of bits of precision to write the components of our ground direction
TypeBits = 4, ///< Allowing 16 types
};
/// Tells us what type of animation we are dealing with
enum AnimationType {
Root,
Move,
Jump,
Fall,
Land,
Other,
};
/// This struct holds the information for a single action animation. You can
/// add your own members, like stances or movement speeds, to this struct. But
/// don't add anything that will be different for various player datablocks
/// using the same animation data - see PlayerData::ActionAnimation for more info.
struct ActionAnimDef {
const char* name; ///< The name of the animation as used by TSShapeConstructor
Point3F groundDir; ///< The direction our ground transform goes in
AnimationType type; ///< What type of animation we're in
};
U32 numValidAnimations;
/// An array of ActionAnimDef objects for different animations
ActionAnimDef actionAnimations[MaxAnimations]; //Note we use one of our constants to set the array size
/// Three arrays to store the data we get from scripts when we're created - we
/// then transfer the data over to the struct array.
const char* names[MaxAnimations];
Point3F directions[MaxAnimations];
AnimationType types[MaxAnimations];
//We don't really need any get/set methods - Players will directly reference the
//private data and information is all loaded in initPersistFields.
public:
DECLARE_CONOBJECT(ActionAnimationData);
ActionAnimationData();
bool onAdd();
static void initPersistFields();
bool preload(bool server, char errorBuffer[256]);
void packData(BitStream* stream);
void unpackData(BitStream* stream);
};
DECLARE_CONSOLETYPE(ActionAnimationData);Now all that code has been defined, it’s time to implement it. I’ve also tried to comment this code as well as I can, so you should be able to get a good feel for how the class works. Add this code to the top of player.cc after the double dashed line:
//This table allows us to use the enumerated values in script
static EnumTable::Enums enumAnimationTypes[] =
{
{ ActionAnimationData::Root, "Root" },
{ ActionAnimationData::Move, "Move" },
{ ActionAnimationData::Jump, "Jump" },
{ ActionAnimationData::Fall, "Fall" },
{ ActionAnimationData::Land, "Land" },
{ ActionAnimationData::Other, "Other" },
};
static EnumTable EnumAnimationType(6, &enumAnimationTypes[0]);
IMPLEMENT_CO_DATABLOCK_V1(ActionAnimationData);
ActionAnimationData::ActionAnimationData()
{
//Initialise all the values in our arrays
for(U32 i = 0; i < MaxAnimations; i++)
{
actionAnimations[i].groundDir.set(0.0f,0.0f,0.0f);
actionAnimations[i].name = "";
actionAnimations[i].type = Root;
names[i] = "";
directions[i].set(0.0f,0.0f,0.0f);
types[i] = Root;
}
numValidAnimations = 0;
}
bool ActionAnimationData::onAdd()
{
if(!Parent::onAdd())
return false;
//Not much to do here...?
return true;
}
void ActionAnimationData::initPersistFields()
{
//This is how we let scripters add fields in the datablock constructor
addField("name",TypeString,Offset(names,ActionAnimationData),MaxAnimations);
addField("direction",TypePoint3F,Offset(directions,ActionAnimationData),MaxAnimations);
addField("type",TypeEnum,Offset(types,ActionAnimationData),MaxAnimations,&EnumAnimationType);
}
bool ActionAnimationData::preload(bool server, char errorBuffer[256])
{
//Always call the parent function...
if(!Parent::preload(server, errorBuffer))
return false;
//The datablock constructor has filled up our 'names' and 'directions' arrays.
//Now we need to copy that data over into our struct array.
//Note: on the client, our objects have already been set by the data sent
//from the server so we don't need to do this.
//Note: the data is copied over in a way that ensures the actionAnimations
//array has no 'bubbles' (empty entries in between valid ones). We do this
//by only incrementing j when we’ve got a valid animation.
if(server)
{
U32 j = 0;
for(U32 i = 0; i < MaxAnimations; i++)
{
//If the name here is not ""
if(dStricmp(names[i],""))
{
//Copy it over
actionAnimations[j].name = names[i];
actionAnimations[j].groundDir = directions[i];
actionAnimations[j].groundDir.normalizeSafe();
actionAnimations[j].type = types[i];
j++;
}
}
numValidAnimations = j;
}
return true;
}
void ActionAnimationData::packData(BitStream* stream)
{
Parent::packData(stream);
//This method is only called on the server object. Here we write all the data we
//were created with into a stream which is sent to the client.
for(U32 i = 0; i < MaxAnimations; i++)
{
//Write a flag if there's a name in this entry
if(stream->writeFlag(dStricmp(actionAnimations[i].name,"")))
{
stream->writeString(actionAnimations[i].name,MaxNameLength);
stream->writeNormalVector(actionAnimations[i].groundDir,GroundDirBits);
stream->writeInt(actionAnimations[i].type,TypeBits);
}
}
}
void ActionAnimationData::unpackData(BitStream* stream)
{
Parent::unpackData(stream);
//Now we're on the client, and we pull the data out of the stream from the
//server and stick it straight into our array.
//Use the same special structure that ensures there are no 'bubbles' in the data.
U32 j = 0;
for(U32 i = 0; i < MaxAnimations; i++)
{
if(stream->readFlag())
{
actionAnimations[j].name = stream->readSTString();
stream->readNormalVector(&actionAnimations[j].groundDir,GroundDirBits);
actionAnimations[j].type = (AnimationType)stream->readInt(TypeBits);
j++;
}
}
numValidAnimations = j;
}
IMPLEMENT_CONSOLETYPE(ActionAnimationData)
IMPLEMENT_GETDATATYPE(ActionAnimationData)
IMPLEMENT_SETDATATYPE(ActionAnimationData)Now we’ve defined the new object, you can actually recompile and run the engine. You can define an object of this type with all the appropriate fields – but we’ll save that until the end. This is the end of Part 1. Congratulations! Now the hard work starts.
Part 2. Changing Payer to accommodate our changes
This is the largest and most tedious part. Now that we’ve added the new class, we have to make the Player and PlayerData classes use it instead of the old system.
First things first – let’s rip all the old stuff straight out! Find and remove the following code blocks from player.h:
struct ActionAnimationDef {
const char* name; ///< Sequence name
struct Vector {
F32 x,y,z;
} dir; ///< Default direction
}; enum {
// *** WARNING ***
// These enum values are used to index the ActionAnimationList
// array instantiated in player.cc
// The first five are selected in the move state based on velocity
RootAnim,
RunForwardAnim,
BackBackwardAnim,
SideLeftAnim,
// These are set explicitly based on player actions
FallAnim,
JumpAnim,
StandJumpAnim,
LandAnim,
//
NumMoveActionAnims = SideLeftAnim + 1,
NumTableActionAnims = LandAnim + 1,
NumExtraActionAnims = 512,
NumActionAnims = NumTableActionAnims + NumExtraActionAnims,
ActionAnimBits = 9,
NullAnimation = (1 << ActionAnimBits) - 1
};static ActionAnimationDef ActionAnimationList[NumTableActionAnims];And the following code from player.cc:
// Action Animations:
PlayerData::ActionAnimationDef PlayerData::ActionAnimationList[NumTableActionAnims] =
{
// *** WARNING ***
// This array is indexed useing the enum values defined in player.h
// Root is the default animation
{ "root" }, // RootAnim,
// These are selected in the move state based on velocity
{ "run", { 0.0f, 1.0f, 0.0f } }, // RunForwardAnim,
{ "back", { 0.0f, -1.0f, 0.0f } }, // BackBackwardAnim
{ "side", { -1.0f, 0.0f, 0.0f } }, // SideLeftAnim,
// These are set explicitly based on player actions
{ "fall" }, // FallAnim
{ "jump" }, // JumpAnim
{ "standjump" }, // StandJumpAnim
{ "land" }, // LandAnim
};Doesn’t it feel good to just outright destroy pieces of the engine?Right, now that’s over with. The next big change is all the new and changed definitions in player.h. I’ll walk you through them.
First, we need to add a reference in PlayerData to an ActionAnimationData object. Put this code where you ripped out the ‘struct ActionAnimationDef’ code in the declaration of PlayerData:
S32 animationDataID; ///< The ID of our ActionAnimationData datablock, same for server and client ActionAnimationData* animationData; ///< Pointer to the datablock itself, different from server to clientAnd now, like I mentioned before, we’ll be changing up the ActionAnimation struct a little. Replace the existing definition of ActionAnimation in PlayerData with this one:
struct ActionAnimation {
ActionAnimationData::ActionAnimDef* def;
S32 sequence; ///< Sequence index
F32 speed; ///< Speed in m/s
bool velocityScale; ///< Scale animation by velocity
bool death; ///< Are we dying?
};I’m sure you can see the differences – no name, no direction, but a pointer to an ActionAnimationDef object. Remember that a whole array of these objects is created for every ActionAnimationData object?You’ll notice that in removing that block of enums from player.h, we removed some useful constants that are referred to throughout the Player class. We’ll go ahead and replace these now. You can do this very easily with a simple find/replace command, or you can search through manually and replace each instance. What I did was:
Replace ‘PlayerData::NumTableActionAnims’ with ‘mDataBlock->animationData->numValidAnimations’ and ‘NumTableActionAnims’ with ‘animationData->numValidAnimations’ (make sure you do it in this order!) (should be 9 occurrences overall)
Replace ‘PlayerData::NumActionAnims’ with ‘mDataBlock->actionCount’ and ‘NumActionAnims’ with ‘ActionAnimationData::MaxAnimations’ (2 occurrences overall)
Replace ‘PlayerData::NullAnimation’ with ‘ActionAnimationData::NullAnimation’ (18 occurrences)
Replace ‘PlayerData::ActionAnimBits’ with ‘ActionAnimationData::AnimationBits’ (4 occurrences)
Don’t forget to find/replace in both player.h and player.cc!
One last thing – after the definition of ‘U32 lookAction’ in PlayerData, add:
U32 rootAction;We’ll use this to store a root animation for the datablock as a small convenience later on.
Okay. Now we’ve changed all those definitions we can go ahead and implement those changes in player.cc. First, to the constructor of PlayerData, add the following wherever it makes sense to you:
rootAction = 0; animationDataID = -1; animationData = NULL; actionCount = 0;
The next big change is to PlayerData::preload. It’s here that we used to copy the static array data into our datablock array. Now we store a reference and do some other things. Replace all the code in PlayerData::preload between and including the following lines:
// Go ahead a pre-load the player shape
…
…
lookAction = c;With this:if (!animationData && animationDataID != -1)
if (!Sim::findObject(animationDataID, animationData))
Con::errorf(ConsoleLogEntry::General, "PlayerData::preload - Invalid packet, bad datablockId(animationData): 0x%x", footPuffID);
if(!animationData)
{
//We don't have an animationManager, so report an error and fail the preload
dSprintf(errorBuffer, 256, "PlayerData::preload: Couldn't find animationData object");
return false;
}
// Go ahead a pre-load the player shape
TSShapeInstance* si = new TSShapeInstance(shape, false);
TSThread* thread = si->addThread();
bool rootFound = false;
// Extract ground transform velocity from animations
// Get the named ones first so they can be indexed directly.
ActionAnimation *dp = &actionList[0];
for (int i = 0; i < animationData->numValidAnimations; i++)
{
ActionAnimationData::ActionAnimDef *sp = &animationData->actionAnimations[i];
dp->def = sp;
dp->sequence = shape->findSequence(sp->name);
dp->velocityScale = true;
dp->death = false;
if (dp->sequence != -1)
getGroundInfo(si,thread,dp);
if(sp->type == ActionAnimationData::Root && !rootFound)
{
rootAction = dp - actionList;
rootFound = true;
}
dp++;
AssertWarn(dp->sequence != -1, avar("PlayerData::preload - Unable to find named animation sequence '%s'!", sp->name));
}
for (int b = 0; b < shape->sequences.size(); b++)
{
if (!isTableSequence(b))
{
dp->def = new ActionAnimationData::ActionAnimDef;
dp->def->type = ActionAnimationData::Other;
dp->sequence = b;
dp->def->name = shape->getName(shape->sequences[b].nameIndex);
dp->velocityScale = false;
getGroundInfo(si,thread,dp++);
}
}
actionCount = dp - actionList;
AssertFatal(actionCount <= ActionAnimationData::MaxAnimations, "Too many action animations!");
delete si;
// Resolve lookAction index
dp = &actionList[0];
const char *lookName = StringTable->insert("look");
for (int c = 0; c < actionCount; c++,dp++)
if (dp->def->name == lookName)
lookAction = c;If you read through, you’ll notice that we actually create a new ActionAnimationDef object for every animation in the list, whether or not it’s in the ActionAnimationData object. This is because we no longer have duplicate data – we actually need an ActionAnimationDef for basic information.Those changes require we do things a little differently in getGroundInfo. It’s mostly minor changes – if you can be bothered to read through them, good on you, but I wouldn’t consider it necessary. Replace the existing PlayerData::getGroundInfo method with this one:
void PlayerData::getGroundInfo(TSShapeInstance* si, TSThread* thread,ActionAnimation *dp)
{
dp->death = !dStrnicmp(dp->def->name, "death", 5);
if (dp->death)
{
// Death animations use roll frame-to-frame changes in ground transform into position
dp->speed = 0.0f;
dp->def->groundDir.set(0.0f, 0.0f, 0.0f);
}
else
{
VectorF save = dp->def->groundDir;
si->setSequence(thread,dp->sequence,0);
si->animate();
si->advanceTime(1);
si->animateGround();
Point3F dir;
si->getGroundTransform().getColumn(3,&dir);
if ((dp->speed = dir.len()) < 0.01f)
{
// No ground displacement... In this case we'll use the
// default table entry, if there is one.
if (save.len() > 0.01f)
{
dp->speed = 1.0f;
dp->velocityScale = false;
}
else
dp->speed = 0.0f;
}
}
}You’ll also need to replace PlayerData::isJumpAction with this:bool PlayerData::isJumpAction(U32 action)
{
if(action < animationData->numValidAnimations)
return (actionList[action].def->type == ActionAnimationData::Jump);
return false;
}Notice how we’re using the new ‘type’ member of ActionAnimationDef in the place of the enum table.In PlayerData::initPersistFields, we need to make the script aware of the new reference in PlayerData. Add the following line somewhere in that method:
addField("animationData",TypeActionAnimationDataPtr,Offset(animationData,PlayerData));We also need to send the data we get from scripts over the network so clients know. In the packData/unpackData methods, add the following lines respectively (one at the end of each method):if(stream->writeFlag(animationData))
stream->writeRangedU32((U32)animationData->getId(),DataBlockObjectIdFirst,DataBlockObjectIdLast);if(stream->readFlag())
animationDataID = stream->readRangedU32(DataBlockObjectIdFirst,DataBlockObjectIdLast);Now, due to the fact that we removed both the ‘name’ and ‘dir’ members of ActionAnimation, we need to replace bits of code that reference them with the new way to get that information. I’ll give you the first example and let you find all the other places where it’s necessary to make this change – it’s not hard! Probably another case of find/replace, or even compiling and just fixing the errors you’re given.
On line 939 of stock player.cc, in method Player::onNewDatablock, find this line:
setArmThread(prevData->actionList[prevAction].name);Replace it with this:
setArmThread(prevData->actionList[prevAction].def->name);Get the idea? Do the same by replacing ‘dir’ with ‘def->groundDir’. There should be about four replacements of ‘name’ and about two of ‘dir’. Note that they’re not always in the form ‘actionList[x].something’; I did a search for just ‘.name’ and ‘.dir’ and read each line before replacing.
Now we need to update the way Players try to find suitable animations. Before we would pick a single animation using the values defined in the enum table. Now there’s no enum table, and I’ve written up a new way to search for the animations we need.
The ‘type’ member of ActionAnimationDef basically replaces the enum table that we used before. I’ll use the first replacement as an example. This code is in Player::setState:
case RecoverState: {
mRecoverTicks = recoverTicks;
mReversePending = U32(F32(mRecoverTicks) / sLandReverseScale);
setActionThread(PlayerData::LandAnim, true, false, true, true);
break;
}We set our action thread to whatever is defined under the enumerated value Player::LandAnim. Now that those enums are gone, what we need to do is search for an animation of type ActionAnimationData::Land.Introducing the Animation Search State! Let’s add it to player.h and I can tell you all about it. After this line in the definition of Player:
static Range mHeadHRange;Add the following:
//This state configures a search for an action animation. If you add members to
//ActionAnimDef, then you should probably add them here as well so you can filter
//searches based on those properties of animations.
//Usage: simply fill in all the appropriate fields of mAnimSearchState and call
//findActionAnim with the appropriate type. The action ID is returned. Root action
//is returned if an appropriate match is not found.
struct ActionAnimationSearchState {
Point3F dir;
void cleanState();
} mAnimSearchState;
/// Returns true if the supplied action is of the supplied type
bool compareActionType(U32 action,ActionAnimationData::AnimationType type);
/// Finds the index in the datablock actionList of a suitable animation considering
/// the given type and data in mAnimSearchState
U32 findActionAnimation(ActionAnimationData::AnimationType type,bool clean = true);Also add the following method somewhere in player.cc:void Player::ActionAnimationSearchState::cleanState()
{
dir.set(0.0f,0.0f,0.0f);
}And add this to Player::Player():mAnimSearchState.cleanState();If you’ve read the comments, you should be able to figure out what I’m on about. The ‘search state’ holds information about an animation we’re looking for, and calling findActionAnimation with an appropriate type actually gives us an index to the datablock actionList. compareActionType is basically a utility method which replaces instances where the code checks to see if something is equal to one of the previous enum values – now, we check to see if the animation’s type is correct.
Let’s go ahead and add the new methods now (I put them after pickActionAnimation):
bool Player::compareActionType(U32 action, ActionAnimationData::AnimationType type)
{
if(action < mDataBlock->actionCount)
return mDataBlock->actionList[action].def->type == type;
return false;
}
U32 Player::findActionAnimation(ActionAnimationData::AnimationType type,bool clean)
{
//At the moment all we have is a direction to search with, so match it by best dot.
//This method could use some improvement in the picking process. It works fine
//for now, but if you try to add multiple checks you’ll need a more elegant process.
U32 action = mDataBlock->rootAction;
//First traverse our animation list and find ones that match the type
Vector<U32> matches;
for(U32 i = 0; i < mDataBlock->animationData->numValidAnimations; i++)
{
if(mDataBlock->actionList[i].def->type == type)
matches.push_back(i);
}
if(matches.size() == 0)
return action;
// Pick animation that is the best fit for our search direction.
if(!mAnimSearchState.dir.isZero())
{
F32 curMax = 0.1f;
for (U32 i = 0; i < matches.size(); i++)
{
PlayerData::ActionAnimation &anim = mDataBlock->actionList[matches[i]];
if (anim.sequence != -1 && anim.speed) {
F32 d = mDot(mAnimSearchState.dir, anim.def->groundDir);
if (d > curMax)
{
curMax = d;
action = matches[i];
}
//There used to be a clause here to reuse sideways animations.
//It is with us no longer.
}
}
}
if(clean)
//Clean up for the next time someone wants to use this.
mAnimSearchState.cleanState();
return action;
}Now, going back to the example in Player::setState. The new case looks like this:case RecoverState: {
mRecoverTicks = recoverTicks;
mReversePending = U32(F32(mRecoverTicks) / sLandReverseScale);
mAnimSearchState.dir.set(delta.move.x,delta.move.y,0);
setActionThread(findActionAnimation(ActionAnimationData::Land), true, false, true, true);
break;
}Notice that when we set an action thread, we replace the static enum with a call to findActionAnimation of the type Land. Also, I made use of the search system to allow different land animations when we’re trying to move in different directions. This could be used for, for example, making the character roll if you’re moving forward when you land (note, by ‘moving forward’ I mean pushing W, not necessarily having forward velocity).Now, one more example. In Player::updateMove, you should find this (after the find/replacing above):
if (moveVec.x + moveVec.y + moveVec.z != 0.0f &&
(mActionAnimation.action >= mDataBlock->actionData->numValidAnimations
|| mActionAnimation.action == PlayerData::LandAnim))
mActionAnimation.action = ActionAnimationData::NullAnimation;Here we compare directly to an enum type – the code is valid if our animation is equal to the LandAnim. Now instead, we use compareActionType to see if our animation’s type is ActionAnimationData::Land. Here’s the new code for that section:(mActionAnimation.action >= mDataBlock->animationData->numValidAnimations
|| compareActionType(mActionAnimation.action,ActionAnimationData::Land)))
mActionAnimation.action = ActionAnimationData::NullAnimation;Now, there’s lots of spots where the table enums are used for different things. I’m not going to list every one of them. Just go through finding every instance of each enum value (SideLeftAnim, RunForwardAnim, FallAnim, etc.), figuring out how it is being used, and replacing it with a call to compareActionType or findActionAnimation. (The easy way, as I did it, is to complete the rest of the code changes, then compile and see where you get errors for not having defined the enums.) You can also figure out whether it would be best to specify the animation by direction. One example of where I did that was in the jump animation clause, previously:
setActionThread((mVelocity.len() < 0.5f) ?
PlayerData::StandJumpAnim : PlayerData::JumpAnim, true, false, true);Now:Point3F vec;
mWorldToObj.mulV(mVelocity,&vec);
if(vec.isZero())
vec.set(0.0f,0.0f,1.0f); //If we're not moving, we should jump vertically
mAnimSearchState.dir.set(vec);
setActionThread(findActionAnimation(ActionAnimationData::Jump), true, false, true);This uses our relative velocity when we jump as the direction to search for. That way you can differentiate between standing and moving jump animations.One more place I’m going to mention directly is the pickActionAnimation method, because it’s undergone significant changes with this new regime. I basically moved the direction-choosing logic out of it when writing findActionAnimation. So here’s the new method:
void Player::pickActionAnimation()
{
// Only select animations in our normal move state.
if (mState != MoveState || mDamageState != Enabled)
return;
U32 rootAction = findActionAnimation(ActionAnimationData::Root);
if (isMounted())
{
// Go into root position unless something was set explicitly
// from a script.
if (!compareActionType(mActionAnimation.action,ActionAnimationData::Root) &&
mActionAnimation.action < mDataBlock->animationData->numValidAnimations)
setActionThread(findActionAnimation(ActionAnimationData::Root),true,false,false);
return;
}
bool forward = true;
U32 action = rootAction;
if (mFalling)
{
// Not in contact with any surface and falling
action = findActionAnimation(ActionAnimationData::Fall);
}
else
{
if (mContactTimer >= sContactTickTime) {
// Nothing under our feet
action = rootAction;
}
else
{
// Our feet are on something
VectorF vel;
mWorldToObj.mulV(mVelocity,&vel);
vel.normalizeSafe();
mAnimSearchState.dir = vel;
action = findActionAnimation(ActionAnimationData::Move);
}
}
setActionThread(action,forward,false,false);
}That’s it for code changes! Recompile, and watch the wheels fall off. Seriously, every time I do stuff like this, I spend more time fixing typos than actually writing the code in the first place. Now, I haven’t held your hand through this process, and I expected you to be able to do some stuff for yourself. So when something breaks, don’t post here about it first, try and work out what the error message is telling you and fix it. Try searching for the error message to find similar problems and see how they were fixed – it’s worked for me before. *Then* come and post it here and I’ll do what I can for you. I’ve gone through the resource following all my own instructions, and the build worked fine – so I have faith that you can do the same!
That said, please do speak up if you find any of my instructions misleading, confusing or unclear. I of course know exactly what I mean when I say things, but that doesn’t apply to everyone.
Sorry about that little tirade ;P.
But now you’re finished with Part 2! At this point, if you load a mission, well, you won’t. Because we haven’t added an animationData object to our PlayerData, so its preload fails and when you try to join a mission, you’re stuck in limbo at the ‘loading objects’ phase, because your Player never gets added to the mission. You should see a nice console error, though.
So let’s remedy that with
Part 3. Script changes
There really isn’t much to do here, but it’s very important. Open playr.cs and add this somewhere before the definition of PlayerBody:
$temp = 0;
datablock ActionAnimationData(DefaultAnimation)
{
name[$temp] = "root";
type[$temp] = Root;
name[$temp++] = "run";
direction[$temp] = "0 1 0";
type[$temp] = Move;
name[$temp++] = "back";
direction[$temp] = "0 -1 0";
type[$temp] = Move;
name[$temp++] = "side";
direction[$temp] = "-1 0 0";
type[$temp] = Move;
name[$temp++] = "fall";
type[$temp] = Fall;
name[$temp++] = "jump";
direction[$temp] = "0 1 1";
type[$temp] = Jump;
name[$temp++] = "land";
type[$temp] = Land;
};And add to the definition of PlayerBody:animationData = DefaultAnimation;Now you should be able to load a mission, and if you’ve done everything correctly, Kork won’t animate when moving to the right. Why’s that? I should have mentioned that a side-effect of this resource is that you lose the reuse of sideways animations. In effect, we now need to define two different animations for sideways movement, left and right.
Oh no! That would usually require source change, and there’s an excellent resource here telling you just how to do it. But with our handy dandy new scripted animation definitions, it’s as simple as changing this:
name[$temp++] = "side"; direction[$temp] = "-1 0 0"; type[$temp] = Move;
To this:
name[$temp++] = "sidel"; direction[$temp] = "-1 0 0"; type[$temp] = Move; name[$temp++] = "sider"; direction[$temp] = "1 0 0"; type[$temp] = Move;And we need to define that animation in the TSShapeConstructor for Kork in data/shapes/player/player.cs:
sequence3 = "./player_side.dsq side";Becomes
sequence3 = "./player_side.dsq sidel"; … sequence30 = "./player_side.dsq sider";(It’s be great to add sider right next to sidel, but this would mean changing all the other sequence numbers :P. That’s why I defined my ActionAnimationData block with $temp. Shame that doesn’t work for TSShapeConstructor.)
And now you’ll see that Kork is animating when moving both directions! Of course, you’ll need an actual animation for moving right, otherwise the direction is wrong. But you should see how easy it is to add new animations now.
Right now, if you have the right animations, you could add eight-point direction movement animations to Kork, to avoid the odd ‘running forward moving diagonally’ thing Kork has going on. Also, see if you can add a different animation for jumping when standing still (hint: it’s all about the direction), since Kork already has an animation for that.
Fantastic! You’ve finished!
Now I’m going to go on and show you how to extend what I’ve made for your own purposes. I’m going to add a ‘speed’ member to ActionAnimationDef, so that different animations can be used at different movement speeds (if your Player has low acceleration, or if you’ve got AIPlayers set to a low movement speed).
We’ll start with the definition of ActionAnimationDef in player.h:
struct ActionAnimDef {
const char* name; ///< The name of the animation as used by TSShapeConstructor
Point3F groundDir; ///< The direction our ground transform goes in
AnimationType type; ///< What type of animation we're in
};Add at the bottom of the definition:F32 speed; ///< What fraction of our top speed we're going at, ideallyAnd after the array definitions:
/// Three arrays to store the data we get from scripts when we're created - we /// then transfer the data over to the struct array. const char* names[MaxAnimations]; Point3F directions[MaxAnimations]; AnimationType types[MaxAnimations];Add one more:
F32 speeds[MaxAnimations];
I’m not going to walk you through all the changes to the ActionAnimationData code changes in player.c, but here’s a list of what you need to do:
1. Initialise speeds and ActionAnimationDef.speed in constructor
2. Add a field in initPersistFields
3. Copy from the speeds array into ActionAnimationDef.speed in preload
4. Pack and unpack
If you’ve gotten through the rest of this resource, I’ll assume you know well how to do all that ;).
Now, we want to be able to find animations with a given speed. This ‘speed’ value is going to be a float from 0 to infinity, representing the percentage of our top movement speed we’re travelling at. So when the value is 1, we’re at full speed; 0.5, we’re at half speed, etcetera. We’ll choose the animation with the closest speed value o our actual value.
This requires two modifications: a new member in ActionAnimationSearchState, so we can specify a speed when we’re searching for animations, and a change in the way pickActionAnimation finds a suitable animation – it now needs to take speed into account as well as direction.
Easy changes: in the definition of ActionAnimationSearchState in player.h, add this:
F32 speed;And in the method ActionAnimationSearchState::cleanState in player.cc, initialise speed to zero.
Now we can take a look at findActionAnimation.
At the moment, the method reads somewhat like this:
1. By default, return the datablock’s root action
2. Add all actions of the correct type to a ‘matches’ list
3. If we have no matches, then bug out
4. If a non-zero direction was specified, set the return action to the one with the best dot product with our desired direction (dot products less than 0.1 (i.e. greater than 85 degrees or so away from our target direction) not considered at all)
5. If we were told to clean up, do so, and return any action we’ve found
We need to change this to factor in speed as well. I’m going to restructure the code a little. Now instead of getting matches and choosing the best one, we’re going to get matches, then remove all the bad ones according to the dot product (‘bad’, in this case, is defined as smaller than the largest dot product we’ve got so far. This allows us to have multiple animations remaining on the matches list if they are all going in the same direction). *Then* we choose the best one according to speed.
This seems to be a good standard way to choose items in order of priority. We remove entries not satisfying the dot test because we really don’t want to be playing our sideways animation when moving forwards, just because there’s no forward animation defined. However, it’s acceptable to play a running animation in place of a missing walking animation.
So in short, when adding your own checks – check in order of priority, most to least important. In all checks except the last, remove items from the matches list. In the last check, choose the best match from the list.
Here’s how that works in practise:
U32 Player::findActionAnimation(ActionAnimationData::AnimationType type,bool clean)
{
//At the moment all we have is a direction to search with, so match it by best dot.
U32 action = mDataBlock->rootAction;
//First traverse our animation list and find ones that match the type
Vector<U32> matches;
for(U32 i = 0; i < mDataBlock->animationData->numValidAnimations; i++)
{
if(mDataBlock->actionList[i].def->type == type)
matches.push_back(i);
}
// Remove animations that aren't the best dot to velocity
if(!mAnimSearchState.dir.isZero())
{
//The starting bid for dot-product matches (0.1 is maybe around 85 degrees)
F32 curMax = 0.1f;
//Normalize it just to be sure. There are irresponsible people out there...
mAnimSearchState.dir.normalize();
//Direction iterator
U32 dIter = 0;
//Number of matches
F32 size;
//We're going to iterate with a while loop so we can do it as much as we need until
//all the bad entries are culled
while(dIter != (size = matches.size()))
{
//Only proceed if we have multiple matches left
if(size > 1)
{
//Get the ActionAnimation from the datablock list
PlayerData::ActionAnimation &anim = mDataBlock->actionList[matches[dIter]];
//Don't match if there's no sequence there, or the sequence is omnidirectional
if (anim.sequence != -1 && !anim.def->groundDir.isZero())
{
//Compare the entry's direction and our desired direction
F32 d = mDot(mAnimSearchState.dir, anim.def->groundDir);
//If we didn't make the cut
if(d < curMax)
{
//Get rid of this entry and restart the iteration process
matches.erase(dIter);
dIter = 0;
continue;
}
//We passed
else if(d > curMax)
{
//If d is better, set the new current max, remember this action and restart
curMax = d;
action = matches[dIter];
dIter = 0;
continue;
}
//The case not accounted for here is if d == curMax. In this case we're allowed to pass by default
}
//Increment iteration
dIter++;
}
//Break if we only have one animation left
else
{
action = matches[0];
break;
}
}
}
//Now we can pick the best animation by getting the best match to our desired speed
if(mAnimSearchState.speed != 0.0f)
{
//Smallest difference between found and desired speeds
F32 smallestDiff = 1000;
F32 diff = 0.0f;
//We can use a simple once-through iteration this time, since we're choosing the best
for(U32 i = 0; i < matches.size(); i++)
{
//Get the ActionAnimation from the datablock list
PlayerData::ActionAnimation &anim = mDataBlock->actionList[matches[i]];
//If we've actually got an animation for this
if(anim.sequence != -1)
{
//Is it the closest yet?
if((diff = mFabs(mAnimSearchState.speed - anim.def->speed)) < smallestDiff)
{
//Yes! So remember it and its special achievement
smallestDiff = diff;
action = matches[i];
}
}
}
}
if(clean)
//Clean up for the next time someone wants to use this.
mAnimSearchState.cleanState();
return action;
}Note the weird style of iteration for direction. The idea is that we search through the list until we find a bad’un, then remove it and start at the top again. If we’d just done a once-through, we’d run into problems because we’re modifying the size of our iteration pool as we iterate.Now, to actually use this. The best place to specify a speed I can think of is in pickActionAnimation. So let’s do that. Change this:
// Our feet are on something
VectorF vel;
mWorldToObj.mulV(mVelocity,&vel);
vel.normalizeSafe();
mAnimSearchState.dir = vel;
action = findActionAnimation(ActionAnimationData::Move);To this:// Our feet are on something
VectorF vel;
mWorldToObj.mulV(mVelocity,&vel);
F32 speed = vel.magnitudeSafe() / mDataBlock->maxForwardSpeed;
vel.normalizeSafe();
mAnimSearchState.dir = vel;
mAnimSearchState.speed = speed;
action = findActionAnimation(ActionAnimationData::Move);This is a very crude way of working out speed, but I wanted something quick. You’d probably want to modify this to suit your Player’s movement better, especially in different directions.Now if you recompile you should again see no changes. That’s because our scripts haven’t been updated. So in the ActionAnimationData, change this:
name[$temp++] = "run"; direction[$temp] = "0 1 0"; type[$temp] = Move;To this:
name[$temp++] = "run"; direction[$temp] = "0 1 0"; type[$temp] = Move; speed[$temp] = 1.0; name[$temp++] = "walk"; direction[$temp] = "0 1 0"; type[$temp] = Move; speed[$temp] = 0.1;This defines a new ‘walk’ animation that will be played when the character’s movement speed is closer to 0.1 than to 1.0 in the forward direction. Just to test it, since I didn’t have any walk animations, I stuck the backwards animation in there:
sequence31 = "./player_back.dsq walk";To test it, it’s easiest to tone down Kork’s acceleration, or to get an AIPlayer and set his movement speed low down at 0.2 or so. And watch Kork moonwalk (sort of…)!
Well, that concludes this resource. I sincerely hope you find it useful, and that my little example implementation sparks people to adapt this basic system for their own nefarious purposes. I know this has certainly been not the most user-friendly resource (being able to upload code might help, GG…), but if you’ve gotten through it and are now basking in the scripted sunlight, congratulations. I did it – I know how much hard work it is!
See you next time.
About the author
Studying mechatronic engineering and computer science at the University of Sydney. Game development is probably my most time-consuming hobby!
#2
08/21/2009 (4:55 am)
Thanks Daniel for the hard work you've put into this resource. I can't wait to play with it!
#3
08/21/2009 (7:30 am)
Probably the most well-written resource I've ever read here. Excellent work and I'll definitely be implementing this. Nice one Daniel!
#4
08/21/2009 (12:43 pm)
thanks, ill try it!...
#5
Konrad and Eikon - the writeup was almost more work than the code itself ;P! I guess it was worth it.
08/21/2009 (12:43 pm)
Thanks all for the nice comments! The complaints will start soon, so I'll enjoy the calm while it lasts ;). I hope you all find a way to put this to good use.Konrad and Eikon - the writeup was almost more work than the code itself ;P! I guess it was worth it.
#6
08/21/2009 (12:44 pm)
Now this is the kind of resource I can sink my mind into. Great work Daniel!
#7
08/21/2009 (5:00 pm)
Owh yeah, sweet resource. Thanks a lot!
#8
EDIT: I updated the resource with a fix in findActionAnimation for a case when the Player was getting stuck in the root animation after adding my extra 'speed' changes. I added a check when removing elements from the matches vector to see if there was only one entry left - if so, then we break from the loop so we can have at least one correct-ish animation.
Also did some thinking of the logic of that method. While choosing does need to prioritised so that the least important choice is made last, all the oter choices are equal in that if they are not satisfied, the entry is removed. So before your least important choice, all other means of eliminating animations should be chosen in order of least complex to most complex, so that the simple, quick checks are performed on the large pool of animations, and the more complicated and expensive checks are only performed on a reduced list of entries.
EDIT: Made one more update to the same method. Now animations with no specified ground transform cannot be eliminated in the direction check. This allows you to have an animation that is 'omnidirectional' - a jump animation might be a good example.
08/21/2009 (9:02 pm)
Just realised that I can host files at EB's Torque file system, so there's a link to download them in the resource now.EDIT: I updated the resource with a fix in findActionAnimation for a case when the Player was getting stuck in the root animation after adding my extra 'speed' changes. I added a check when removing elements from the matches vector to see if there was only one entry left - if so, then we break from the loop so we can have at least one correct-ish animation.
Also did some thinking of the logic of that method. While choosing does need to prioritised so that the least important choice is made last, all the oter choices are equal in that if they are not satisfied, the entry is removed. So before your least important choice, all other means of eliminating animations should be chosen in order of least complex to most complex, so that the simple, quick checks are performed on the large pool of animations, and the more complicated and expensive checks are only performed on a reduced list of entries.
EDIT: Made one more update to the same method. Now animations with no specified ground transform cannot be eliminated in the direction check. This allows you to have an animation that is 'omnidirectional' - a jump animation might be a good example.
#9
08/22/2009 (4:36 pm)
Read this through a couple of times now (there's a lot to parse in one go!) and this is a sweet resource. Kudos for the taking the time to document it so clearly.
#10
My changes have involved starting with NullAnimation instead of the root animation (which is probably a good idea anyway, because your datablock might not have a root action defined), and setting action = matches[0] after each check we don't perform (i.e. if dir happens to be a null vector).
EDIT: Found a big bug - I forgot to initialise mAnimSearchState by calling cleanState in the Player constructor :P.
Also, it seems that for some reason all ellipses (...) in code blocks are replaced by '…' :P
08/23/2009 (5:42 am)
Hmm. I'm running into more trouble with findActionAnimation, but they may be caused by the more demanding set of requirements I'm adding to it. If you use this resource, beware that that section will take a lot of thinking. I'm pretty sure it works well for the basic requirements, though.My changes have involved starting with NullAnimation instead of the root animation (which is probably a good idea anyway, because your datablock might not have a root action defined), and setting action = matches[0] after each check we don't perform (i.e. if dir happens to be a null vector).
EDIT: Found a big bug - I forgot to initialise mAnimSearchState by calling cleanState in the Player constructor :P.
Also, it seems that for some reason all ellipses (...) in code blocks are replaced by '…' :P
#11
11/16/2010 (12:06 pm)
Has anyone tried this with T3D ? 
Torque Owner deepscratch
DeepScratchStudios
excellent resource.
well done!!