DevBlog: Multi-file Animations

Edict

Now that we have the technical capability for multi-file animations we need to integrate this functionality into our engine.

Our current convention is to use files with the name 'model.fbx' as geometry sources, and 'model@animation.fbx' as animation sources. [1] And up until this point we’ve gotten away with directly specifying that uninfected agents used 'manModel.fbx' and infected agents used 'Zombie.fbx' within the simulation code (I know, I know…​).

Loading

For FBX file used in Edict we create a corresponding JSON configuration file. These are not provided to players, but drive the compilation parameters for each asset before it is shipped.

This setup has a number of useful properties:

  • JSON is a very flexible way to specify these parameters.

  • It keeps the configuration directly adjacent to the asset rather than deeply within the buildsystem.

  • It allows us to request alterations to a file’s contents without actually baking these changes into the original file. It is better to keep the original files pristene.

Geometry
manModel.json
{ "scale": 0.1687558009806587 }

All FBX assets support the 'scale' attribute which requests resize of the entire model/skeleton.

I try to keep units within the engine in metres. But sometimes this isn’t convenient for an animator, or we need to use models from outside the project which can’t be expected to conform to all our local conventions.

Animation
manModel@walk.json
{
  "animation": {
    "rules": [
      { "rule": "rename",   "name": "Take 001", "target": "walk" },
      { "rule": "velocity", "name": "walk",     "value": 1.8 }
    ]
  },
}

FBX assets that contain animations have access to a collection of post-processing rules, specified as an ordered list of updates. This allows us to conform the contents to our asset conventions, and specify paramater values which aren’t otherwise available (eg, target velocity of an animation).

Some of the supported rules include:

rename

Change the name of the animation. It allows us to rename animations that would otherwise clash, or to update names that aren’t suitably descriptive.

delete

Remove the animation from further processing. Some external assets include animations that we don’t (yet) support. It’s often easier to just remove them.

velocity

Set the ground speed at which an animation expects the model is moving. This is required so that we know how fast to play back the animation to avoid skating.

At the end of the model/animation compilation process we end up with a file with a name like manModel@walk.nto. It contains graphics data formatted in a way that’s trivial to load, and with the post-processing rules baked in.

When the game starts the engine will scan the installed directory that contains the compiled resources. Each file will be installed into a cache in memory that is indexed by keys consisting of the filename (to simplify referencing the asset from other configuration files), and a runtime generated integer (for internal use where efficiency is paramount).

Linking

Now that the raw data is accessible to the engine we need a way to describe what model and animation each agent type should use. To this end we have one more configuration file; anything residing in the directory render/agent will name the resources that are used to render each agent.

zombie.json
{
    "model": "manModel",
    "animations": {
        "run":  "zombieRun",
        "walk": "zombieWalk",
        "idle": "zombieIdle"
    }
}

The files are named after the infection types that the simulation uses and are automatically associated with agents based on their current infection status. [2]

The configuration file specifies the key for the model, and the key for each known animation type (currently 'idle', 'walk', 'run'). [3] When we load this at runtime we combine the two keys so that, for example, 'zombieRun' becomes '\manModel@zombieRun'. This keeps the names we use for our asset storage and the engine cache consistent, and should reduce the chance of referencing the wrong file.

When it comes time to actually render the agent it’s mostly a question of indexing a few arrays. We look up the infection status, then the bundle of model/animation keys; lookup the current animation, and upload the interpolated bone transforms; finally dispatch a request to render the model key.

Testing

I had a few false starts when specifying this system. One option allowed arbitrary animation names but this flexibility complicated the implementation. Another option allowed much more flexibility in the way asset keys were specified, but that would have resulted in two methods of referring to assets and made it easier to specify mismatched models and animations.

The last experiment was a lot easier to implement than I was expecting, and was the most robust. While there are a few issues with the exact values we use for zombie animation speed and velocities, the underlying features seem to meet our current needs.

TODO

For at least the coming week I’ll be focusing on features and fixes that make the current iteration of Edict substantially easier to interact with. We want to get binaries in the hands of testers soon, and keep the build up-to-date for rapid feedback and gameplay iteration.

Most immediately this will involve the implementation of a fixed camera system, visual feedback on the purpose of regions that the user has placed, and smoothing out the rough edges on the current UI system.


1. We also have the ability to extract any animations present whatever the expected intention was so that we can use external assets more easily. For example the excellent packs from Quaternius and Kenny tend to come as single files. However we prefer the split-file approach where possible.
2. The engine has a concept of 'uninfected', 'latent', 'symptomatic', 'zombie', and 'immune'. However 'zombie' and 'uninfected' are of primary importance to the current iteration of the simulation.
3. I’ve got a note to allow arbitrary animation names at some point in the future, but fixed size arrays tend to be a safe and efficient way to start with.