Building an Animation Manager from scratch with no prior experience -- Owen Meyers

     The required system for this week (shortly after the completion of a bug fix that took almost the whole week to implement) was the animation management system. Simply put, there are 2 major problems to overcome for the integration and completion of this system. 1) Ensure that an entity that needs to instantiate an animation state machine can do so quickly and easily and 2) Ensure that the machines are all updated accordingly, with the data for joints loaded into the GPU-side storage buffer in the correct order. This required a shift in thinking for me, as I needed to figure out how to order the data in the buffers such that it would line up properly with the instanced draw call as well as figure out how to ensure that the CPU-side state machines were managed properly with no memory leaks. This proved to be an interesting design problem that took a large amount of problem solving to overcome.

    The solution was relatively simple. To solve the first problem, I essentially started with a hard-coded set of states and state machines that the entities can refer to. The states simply index into a buffer of animation clips to prevent duplicates of data, and can have child states that can be transitioned to either automatically or manually. When an entity requests a state machine for animation, i.e. an enemy is instantiated and requires the joint data to correspond with the actions the enemy is taking in the world, a message must be sent to the animation manager. Once there, the animation manager will allocate a new state machine, copied from a list of loaded machines (this prevents us from loading a state machine that will never be instantiated in the level, saving on RAM and preventing anything from loading from disk at runtime), and receives an index back with which to refer to the state machine. The reason for this is simple -- there isn't a need to organize the state machines themselves on the CPU side, and thus, having an index is the most cost-effective way to allow an entity to change its animation state. This then allows the entity to index into the collection of instantiated state machines and manipulate it in any way needed (i.e. queue an attack animation state, which will then transition back into walking or idling).

    To solve the second problem, I went back to the system that manages the world matrices for all entities in the engine. To keep GPU buffer binding to a minimum, we have a bindless world matrix buffer that every entity in the program reference. Keeping things organized is top priority then, as any misunderstandings between the CPU and GPU as to where world matrix data is within that buffer will lead to weird bugs. To solve that problem, I kept things simple -- skinned meshes will occupy the front of the buffer, followed by static meshes, and finally by any UI and particle world matrices (although as far as I understand it, they have their own buffer that is nothing but positions, as a matrix isn't needed). This means that the player is *guaranteed* to be at index 0 in this buffer, which means the draw call for the player will always use the instance id offset of 0, with every subsequent enemy starting at 1 + sum(entity offset + entity instance count), where entity is each subsequent enemy preceding the current one. This makes things relatively simple for the joint data, then, as that buffer *only* holds joint data, and as such, doesn't need to worry about shifting values for static meshes. Thus, the index that each entity holds that is a reference to its place in the world matrix buffer would then be used to place that joint data within the joint buffer. To make that even more simple, each state machine holds a pointer to the enemy that refers to it, and thus doesn't even need to store an index that *will* need to be updated when an enemy is killed. Instead, it will simply query the enemy every frame for its index. This allows one other advantage -- instead of having to clear the entire buffer of joint data every frame, the data can simply be overwritten, leading to the same results but with only one memcpy rather than 2 (one for clear, one for new data being copied up to the GPU). 

    The final result of solving both of those problems is an animation system that fires on all cylinders and functioned as intended on release of the code. As a side note, to make things simple for debugging, the debug version of the animation manager refers to animation states and machines through a string, while the release version only uses an index. This means that in debug, even though it runs slower (it MUST hash the position of the machine and state every frame), it can display the current state in a string pretty easily and human readable. In release, it simply passes a number around, and doesn't need to hash a whole string, leading to a significantly faster system.

The animation manager in action, displaying the character's current attack animation (a placeholder jump animation)


Comments

Popular posts from this blog

Adding assets to every enemy -- Owen Meyers

Sphere to AABB collision