Animation sheet commands – addendum # 1

Some animations are not meant to be played in their totality before allowing the player to change states. For example, when the player is drawing the weapon out, the player is not allowed to move or draw the weapon back in before the weapon has been fully drawn.

No Interrupt

To do this, a no_interrupt variable has been introduced in the animation map which allows a specific animation to have this behaviour; this is transferred to C2 during runtime, of course.

Thus, all player commands are issued through several kinds of ‘intend action’ functions. Currently, they are:

  • PlayerUserCommand – handles the SWAP actions; queries if an animation is ok to interrupt or not
  • PlayerUserCommandDirection – handles directional actions (ie ‘d:#’); it also sees if an animation can be interrupted, and sets an integer intend_dir variable for use with other things in the game.

Animation sheet commands

With the ongoing development in Animation Sheets, I’ve also formed a command syntax to allow flexibility in how actions are sequentially executed, while also being aware of sequence animation.

The flow in C2 is a bit convoluted so I want to document this for future reference.

intend_list

This is the variable that contains a sequence of ‘intended actions’ An example intend_list looks like this:

a:wepi,a:idle,w:as,a:wepo

Where:

<action_prefix>:<action>,<action_prefix>:<action>,...

An action_prefix can be ‘a’, ‘p’, ‘w’, ‘s’. This corresponds with ‘action’, ‘pose’, ‘weapon’, and ‘speed’, respectively. I’ll refer to this as SWAP for simplicity.

Actions can be move, wepi, wepo, shoot, idle.

Pose can be up, dn.

Weapon can be u, as, am, al (this is not yet implemented as of writing)

Speed can be walk, run.

C2 logic flow

Like I said, the flow jumps around.

The first, we start with a given animation. An animation is defined by SWAP, and SWAP are actually something like states. The SWAP is referred to in order to make the right decision which animation is to be played.

So even at the beginning, the SWAP is initialised manually and then PlayerSetAnimation, which is the function that does much of the decision-making is run, and in constantly run.

How it works

At the beginning of PlayerSetAnimation, the first item ([0]) in intend_list is read and then popped. (Since it is a string, it is implicitly tokenised; when I say ‘popped’ I mean that the first token is popped, since it is not really an array, though it is treated as such.)

The popped data is called command. The command is queried if it is an Action. If is not an Action, then the state variables of the SWP is set. For example, if the command is s:run, then the intend_speed variable, which is the internal C2 state variable for controlling actual speed, is set to "run".

One important note is that the intend_list commands continue to be popped until an Action is encountered (so that SWP are processed as state variable changes only).

However, if it is an Action, then it starts querying the SWP variables, then forming an appropriate lookup to the animation map list.

If it is Speed or Pose change then after setting the state variables, it will determine a lookup but uses the nominal animation with changed Pose or Speed variables.

The complicated bit

The complication lay in the combination of the implicit ‘next animation’ of one animation. Some important things to remember:

  • An animation, whether it has a sequence or not, will default to looping itself.
  • In the animation map, a ‘next animation’ can be specified. This makes one animation inherently connected to another.

I’ll demonstrate different intend_list examples and how they are processed, and what goes on beyond the PlayerSetAnimation function.

Example 1

Given an intend_list:

a:wepi,w:am,a:wepo

This is used when switching from a small weapon to a medium weapon, and only applies when a small weapon is equipped.

PlayerSetAnimation

When a:wepi is popped, ‘wepi’ is the intend_anim, which is the keyword. SWP variables are queried (although in this case only the pose and weapon are relevant). Then the animation lookup is set, in this case ‘am upwo’ (medium weapon, standing weapon out).

Remember that the intend_list is popped, so it now looks like this.

w:am,a:wepo

Then the lookup is used to get the seq_start/seq_end, and other variables needed to play the animation. The next_anim and is_seq variables are also populated.

PlayerPlayAnimation

Then PlayerPlayAnimation is called. What this function does is simply play the animation, and starts the playhead at the appropriate place. It doesn’t check anything; it just makes the animation run.

PlayerCheckAnimation

Then there is a On frame change C2 trigger, which calls the PlayerCheckAnimation function. This function checks to see if the sequence as ended. If the sequence’s parameters have been satisfied, it calls PlayerAdvanceSequence.

PlayerAdvanceSequence

PlayerAdvanceSequence is reponsible for determining what to do next. If there is a sequence of animation to be played (ie segmentations of the animation to be played discretely), then this function increments the sequence index. This is important, because as long as there is a sequence to be played, the system will continue to play the sequence.

If however, the sequence has been played out PlayerAdvanceSequence decides several things. First, does the current animation have a ‘next_anim’ name? In this example, the ‘wepi’ animation has ‘idle’ for its ‘next_anim’.

So if there is a ‘next_anim’ name, the next thing is to determine if there are any other Actions in the recently popped intend_list. In our case, yes. Remember our intend_list:

w:am,a:wepo

‘a:wepo’ is still present. So here, nothing special happens. The above intend_list is retained, and PlayerSetAnimation is called again.

Back to PlayerSetAnimation

When it is called again it sees ‘w:am’, and pops that. But we already know that PlayerSetAnimation will always keep on popping non-Action commands (setting variables as commanded by SWP). So after popping ‘w:am’, it sees the final command ‘a:wepo’ and pops that.

When the last is popped, intend_list is blank. But the whole process is repeated: the lookup is made, and then run. At some point, we end up in PlayerAdvanceSequence again, and then from here we ask the same questions. Does it have a ‘next_anim’ variable? Yes: ‘idle’.

Does it have any other Action in intend_list? No, it’s blank. When that happens, the default ‘next_anim’ is prepended to the intend_list (although because it’s blank, it doesn’t matter). Then the intend_list looks like:

a:idle

Then PlayerSetAnimation is called with that intend_list, in which is goes back to idle.

Example 2

Let’s try another intend_list going through the same process:

a:wepi,w:u
PlayerSetAnimation

‘a:wepi’ is processed and popped first in PlayerSetAnimation. Because it’s an Action, a lookup is immediately processed for it. Then the intend_list looks like:

w:u

After the lookup is processed, it is played and then checked like Example 1.

PlayerAdvanceSequence

Eventually we arrive at PlayerAdvanceSequence again when the ‘weapon in’ animation sequence is finished. The ‘weapon in’ animation has ‘idle’ for its ‘next_anim’.

PlayerAdvanceSequence queries if there are still any Action commands in the intend_list. There are none (ie ‘w:u’). So what happens is that the ‘next_anim’ is appended to the intend_list so that the PlayerSetAnimation knows it is the right time to use the ‘next_anim’. The intend_list now looks like this:

w:u,a:idle

So when PlayerSetAnimation is called with that intend_list, it processes and pops ‘w:u’,  which switches the weapon to ‘unarmed’, and then processes/pops the Action ‘idle’. This completes the command sequence for this example.

Example 3

Given the this intend_list:

p:dn,s:run

These are just Pose and Speed changes, so they are considered separately.

PlayerSetAnimation

Pose and Speed commands change the intend_pose, and intend_speed variables. But a lookup must also be made for them in the same way Actions are. But the main difference is that in Pose and Speed, they are popped and processed immediately at the beginning with the setting of the variables. In our example, before a lookup is created, the intend_list would be empty, though the variables have been set (ie intend_pose=”dn”, intend_speed=”run”)

The lookup is made in the same place where Actions are. The main difference is that Speed and Pose uses the state variables to create the lookup. And just as importantly, it relies on the current PLAYER.anim variable to know what the current animation is.

PLAYER.anim

PLAYER.anim is an instance variable in the sprite which contains the ‘base’ animation which specifies only the Pose and the Action. Some examples:

  • If player is idle, unarmed, is standing, PLAYER.anim=”up idle”.
  • If player is idle, unarmed, is crouched, PLAYER.anim=”dn idle”.
  • If player is idle, small weapon drawn, crouched, PLAYER.anim=”dn idle”
  • If player is walking, standing up, weapon drawn, PLAYER.anim=”up move w”
  • If player is walking, crouching, unarmed, PLAYER.anim=”dn move w”
  • If player is intending to run, crouching, unarmed, PLAYER.anim=”dn move w”

Note that PLAYER.anim doesn’t express whether a weapon is drawn or not.

Using PLAYER.anim, we can get the nominal state of movement. Then is a Pose change is required, then the Pose aspect of the PLAYER.anim is modified and PlayerPlayAnimation is called.

The rest goes through the same process again.

PLAYER.do_not_check

A variable called do_not_check is used for bypassing actual checking of frames during OnFrameChange. The reason behind this is because there is not much frame control when I switch animation folders. C2 only allows two options: start at the beginning of the animation folder, or the current frame. But because I’m using it as a map rather than as sequence, I’m jumping from one folder to another, so the frame numbers become arbitrary, based on the animation map. So either frame method is useless in this case. What was actually happening was that the frame being queried as the wrong value; when the the animation folder was switched to, and the play_start and play_end variables were populated (through the values in the map), the OnFrameChange event was triggered. The current AnimationFrame, at this point, is likely to be wrong, since I can only begin at the start, or at the current frame integer (which won’t correspond to a new animation folder). Therefore, I needed a way to prevent the check before I could properly set the frame at the next tick.

To do this, before calling an animation folder, I first put do_not_check=1, then call the animation folder (ie ‘Set animation (start at the beginning’). This has the instant effect of triggering OnFrameChange. But a condition checking the do_not_check variable will bypass it.

Conclusion

The main ideas are

  • intend_list is the the entry point for all animation changes
  • intend_list is popped and data is put into PLAYER.intend_anim
  • intend_anim is used to process what command this is
  • If a command is an Action, is processed normally
    • An animation lookup is constructed from the command itself
    • In addition, the other SWP variables are used
  • When a sequence has played out a check is made if there are still an Action command in the intend_list, to know whether the ‘next_anim’ animation needs to be played out.
    • If there are still Action commands, then the intend_list is fed back in PlayerSetAnimation.
    • If there are no other Action commands, the current animation’s ‘next_anim’ name is appended to intend_list and then is fed back to PlayerSetAnimation.
  • If a Pose or Speed change is made, the intend_pose and intend_speed variables are changed. Then the command is made a lookup using the nominal animation that is currently playing. The nominal animation is found out by referencing the PLAYER.anim variable.

 

Animation and intentions

Intentions

Animation being in sheets, can then be driven by separate variables that describe a few properties of an animation. These properties (also called intentions) are:

  • Action
    • idle
    • move
    • wepout
    • wepin
    • shoot
  • Pose
    • up
    • down
  • Speed
    • walk
    • run
  • Wep
    • unarmed
    • armed small
    • armed medium
    • armed large

Each of these properties are termed ‘intention’, which means that player intends, for example, to have a ‘walk’ speed, an ‘up’ pose, and ‘unarmed’. So when the ‘move’ action is called, it will find the appropriate animation to match ‘walk, up, unarmed’. If a ‘wepout’ action is called, that would not yield an animation, because the ‘intended’ weapon is ‘unarmed’. If the ‘wep’ was changed to ‘armed small’, then the animation will yield the player in standing pose, drawing a small weapon. The speed intention is not relevant here, of course; it’s only relevant when a specific action is needed to query that intention.

Multiple intentions

Multiple intentions are assumed in the system. This enables a number of actions and intentions to be strung up together to make a sequence of movements one after the other. Multiple intention are basically stringing up a series of intentions by commas; eg a:idle, a:move,p:dn,s:walk,w:u. Note the ‘a’, ‘p’, ‘s’, and ‘w’ prefixes which denote ‘action’, ‘pose’, ‘speed’, and ‘wep’, respectively. This specifically switches those intentions .

Note that even in animation maps, there is a key called ‘next_anim‘ which automatically moves from one animation to another. However, multiple intentions are prioritised first, so that if the intentions are written like a:wepin,a:wepout, then animation plays ‘wepin’ first, then ‘wepout’, but it will ignore the ‘next_anim‘ value of ‘wepin’. Since ‘wepout’ is the last intention, it will move to the animation of ‘wepout’s’ ‘next_anim‘, if any.

Animation ‘sheets’

In the test RND project, there were a fair amount of sprites used for the player character which featured variations that depicted a separate facing direction from the move direction.

In CITIZEN there are additions such as drawn weapon of a certain size, pose changes (standing, crouched), and speed. All of these combine into a huge animation sprite ‘sheet’.

I use ‘sheet’ to mean a series of sprites meant to be organised together. Much of the work has been how to organise the sheets in such a way as to easily reference them.

Organisation

The animation hierarchy looks something like this.

  • For every direction (8 directions)
    • (For every direction) A weapon state: unarmed, armed small, armed medium
      • (For every weapon state) A pose: stand, crouch.
        • (For every pose) An intended movement: idle, walk, run, weapon in, weapon out, shoot.

Due to the resulting number of images, I had decided against implementing a separate facing and moving direction, since this would somewhat triple the amount of frames.

In C2, the interface to manipulate sprites is not production-friendly, so the lesser number of Animation folders used the better. Therefore, I decided to arrange the animation by the last element in the hierarchy, which is the intended movement. This grouping was also closest to how it was being rendered in 3d, so the transfer of the sprites to C2 was simpler.

Animations grouped by ‘intended action’.

For the record, ‘wf‘ is ‘walk forward’, ‘rf‘ is ‘run forward’, ‘cf‘ is ‘crouch forward’, ‘wepio‘ is a combination of ‘weapon in’ and ‘weapon out’, and the rest are self-explanatory. ‘wepio‘ has been put together in one group because the frames used in ‘weapon in’ is the reverse of ‘weapon out’, and also the limited frame meant that another animation folder was unnecessary.

Inside any of these folders are the large number of sprites which depict the intended action (eg wf) in stand and crouch, and for each of that, in unarmed, armed small, and armed medium variations, and for each of those variations, all 8 directions.

It is organised as the hierarchy above indicates. Eg in the folder ‘wf’, the first number cycle of frames depict Direction 0, Unarmed, Stand, Walk forward. The second depicts Direction 1, Unarmed, Stand, Walk foward, etc.

Animation map

In order to to retrieve the sprite frame or sequence to play, an animation map had to be created in order for the C2 events to locate the frame and folder.

The animation map consisted of simple directives. Here’s an example:

# IDLE --------------------------------------
u upi anim:idle
u upi is_seq:0-0@5,0-3@3,4-5@3,6-7@2,7-5@2,5-0@2
u upi num_frames:8
u upi prev_frames:0

as upi anim:idle
as upi is_seq:0-0@5,0-3@2,3-6@2,3-0@2
as upi num_frames:7
as upi prev_frames:8

am upi anim:idle
am upi is_seq:0-0@5,0-3@.15,3-0@2,4-7@2,7-4@10
am upi num_frames:8
am upi prev_frames:15

Left of the colon ‘:’ is the directive, and at the right is the value.

The directive is in this template: <wep> <pose&action><key>.

<wep> can be ‘u‘, ‘as‘, ‘am‘.

<pose&action> is a combination of the pose and intended action. ‘pose‘ can either be ‘up‘ or ‘dn‘. ‘action’ can be ‘i‘ (idle), ‘wf‘ (walk forward), ‘rf‘ (run forward), ‘cf‘ (crouch forward), ‘s‘ (shoot), ‘wepi‘ (weapon in), and ‘wepo‘ (weapon out).

Then back in C2, a lookup string is constructed based on the state variables, and this is used to find the directive name, eg ‘u dni’, which means unarmed, crouched, idle.

The ‘key’ is the attribute of a particular animation. ‘anim‘ refers to the C2 animation folder that this animation will use. ‘is_seq‘ is a string that tells the system to play a sequence in a particular order, including a wait time. The syntax for is_seq is as follows.

<startframe>-<endframe>@<wait_time>

Note that the startframe and endframe tokens are relative to the sequence being looked up.

The ‘num_frames‘ key specifies the number of animation frames this sequence is supposed to have. This is used to find the proper offsets.

The ‘prev_frames‘ key specifies the number of total offset frames from the beginning of this animation folder. Note in the example above, notice how the next ‘prev_frames‘ value is the sum of the ‘num_frames‘ and ‘prev_frames’ of the previous entry. It is definitely easier to create the animation map in the order in which they are currently arranged in the 3d renders, so that the specification of offsets in the file is just a matter of adding on top of the previous.

(There are also two other keys that bear mentioning. One is ‘next_anim‘ and ‘endsignal‘. ‘next_anim‘ is used so that a specific animation be explicitly told to move to another animation after the sequence is finished. Currently, this is used to automatically move to the ‘idle’ animation after drawing or hiding the weapon. ‘endsignal’ is a text that is specifically used to trigger some action/event in C2. For example, this is used to signal that a weapon has completed being drawn or holstered. So in the YML file, the endsignal of wepin, is ‘wepin’, and in C2, an “AnimationEndSignal” function is used to look for that specific signal string. When it encounters it, it changes the weapon status variable.)

In relation to sorting renders out predictably, 3d renders are appended with a prefix that enables them to be alphabetically sorted in a predictable way. So, for example, if the ‘wf’ (walk forward) animation folder, the ‘u’ sequences go first, followed by ‘as’, then ‘am’. That sorting is enforced by prefixing 3d renders of ‘u’ by ‘a_’, ‘as’ by ‘b_’, and ‘am’ by ‘c_’. In addition to this, the direction angle (eg 0, 45, 90, 180, etc) are written with 3-digit padding (eg 000, 045, 090, 180). With these two modifications to the 3d render filename, this will always yield an correct sort. These naming conventions are incorporated into Janus configurations so there’s no need to do them manually.

The point is: renders need to be sorted right from the time they are rendered, so that when cropped and copied over, C2SpriteMan can rename them in the proper order they will appear finally in the animation sheet.

Primer on animation workflow

Now that the animation aspects being developed in C2 is becoming closer to how renders are named, the following information bears putting down.

First there are several applications involved in making character animation in C2 in CITIZEN: Maya (using Sandline for cache export), LightWave3D, Imagemagick, and C2SpriteMan. The following information is the workflow of data and formats leading up to the final graphics put into C2.

Maya animation

The animation workflow starts assuming that all characters have been modelled; texturing is done separately from animation. In Maya, the characters are rigged.

The organisation of the Maya scenes are such that only one scene file is used for all animation.

Because the Maya workflow uses Sandline, which uses namespaces in order to organise the cache files in the file system, multiple references of the character are brought in. (While I admit that this is an overhead, it is negligible because of the minimalist assets used in the game). These multiple references refer to the animation that the rig is intended to be. For example, the walk animation will have the PWALK (Player Walk) namespace, and the run animation is called PRUN. These referenced rigs will be animated as such, and then they are exported via Sandline, the cache folders they will reside in will be according to their namespace: /PWALK, and /PRUN. This makes it easier to identify the animation in the file system.

Furthermore, the animation timeline in Maya starts at frame 1. Each referenced rig, each with their own assigned animation, starts at frame 1 and animate for as long as necessary, as long it is no more than 100 frames long, which is the defined limit for this workflow.

Orientation of the character is +Z. Animation is only for one direction, too, since the multiple directions are rendered automatically using Janus in LW.

Once the cache files are exported it goes into LW to be set up.

LightWave rendering

There are too many steps that describe this workflow, but the main idea, is that the relationship between the animation done in Maya is related back to LW in several methods.

Animation ranges

It must also be noted that while in Maya, all animations started in frame 1, in LW, the animation is blocked out in discrete 100-frame chunks. For example, the walk animation (eg /PWALK) is assigned frames 1-100, the run animation (/PRUN) frames 101-200, the crouch (/PCROUCH) animation frames 201-300; the idle animation (PIDLE) is assigned frames 901-1000, etc.

A nodal network in LW is created that assigns each cache file to the appropriate geometry. It also offsets each cache (which starts at frame 1) to their corresponding mapped chunk as described above.

Janus

Once the cache files are assigned, the geometries are parented to nulls that enable it to rotate to 8 directions. Then Janus comes in where it is responsible for breaking out the master scene file into the varied animations.

There are two ‘map’ files configuring Janus to do this.

Timeframe definition

Actually called ‘CDEMO_timeframe_definition.txt’, this text file maps out the the entire duration for all animations.

For example, it defines where in the timeline you can find the ‘unarmed walk forward’. Basically, Janus uses this file to set the start and end frames (and frame step parameter) depending on the intended breakout.

The intended breakout, on the other hand, is defined in Janus’s FORFILEs.

FORFILE

Without getting into too specific with Janus, FORFILEs is a Janus capability to iterate through a text file and populate aspects of the breakout by the contents of the file.

In this usage, the FORFILE lists all the breakouts that need to happen. For example, the FORFILE lists that for every 8-direction angle, a ‘unarmed walk forward’ animation is exported. In Janus, it reads the tokens and then through its configuration it reads the timeframe definition (described above) and looks up the appropriate time frames to render.

It’s also in this FORFILE that all the specific naming conventions are applied to the final image output.

Workflow: Interaction triggers

In the RND test project one of the most important systems I developed was the interaction trigger system.

This system is simply a method of binding an action (ie “Interact”) and a specifier, and then wrapped to make a ‘broadcast signal’.

This broadcast signal is then sent. Because the broadcast signal can optionally contain a ‘target’, only those matching the target description can be made to respond to the signal.

The importance of a system like this is the ability to make level-specific scripts. I’ll give a test case from the RND project.

  • In Tiled, a marker is created with a name. This is the trigger name, which can be anything as long as it can be uniquely identified.
  • In C2, a ‘On GridMove reach target’ action is bound so that it wraps the reaching of the tile with the trigger name of the marker it has reached.
  • On reach target, the trigger is sent to a BroadcastTrigger function, which accepts the trigger name, and the intended target of the trigger, if any. The target is comma-delimited, so multiple targets can be specified.
  • The BroadcastTrigger function looks at the targets, tokenises them, and then applies the ‘receivedtrigger’ variable of each of the instances that are able to accept triggers. It applies them only to the targets specified, or all instances if no target was specified.
  • Note that a family called f_trigger_receiver was made and the receivedtrigger variable is called ‘f_receivedtrigger’ in order that BroadcastTrigger can efficiently send it to those concerned.
  • In the level-specific script, the intended target is waiting for its specific f_receivedtrigger to change. BroadcastTrigger would have changed it.
  • When it does, it fires off the events there.

In addition to the trigger, level-specific behaviours are specified, and can override the default AI of any object. This is why this is important, because the scripting is done in a separate event sheet (ie logic) and not predefined in the main logic.

Now, other actions are bound, as needed, to the BroadcastTrigger. For example, in the RND project, the On reach target trigger condition was the first one I implemented. But quickly afterwards, it was easy enough to bind the TalkToNPC function, or the InteractWithNPC function to the broadcast.

Of course, the trigger name changed. In the TalkToNPC trigger, the trigger name was "talk "&cmover.name in which the ‘talk’ keyword was appended by the actual variable name of the NPC that was talked to. The name of the NPC talked to was embedded in the signal and no target was specified because the logic was that either the player or the game world was the receiver. But, it is also possible, or even more beneficial if indeed the recipient of the ‘talk’ action was put in to the trigger target, as I did with the next implementation.

I implemented an ‘InteractWithNPC’ action in the same way, but included the recipient of the ‘interact’ action as the target. In the level script it was intended to add to the accomps to keep track who had been interacted with.

The BroadcastTrigger concept is just a concept, but seems to be a very flexible one, as I am using it currently to design a generic kind of interaction behaviour between a single ‘Useitem’ action to a host of different possible objects, each with their varying results. It’s this reason why BroadcastTrigger is useful, because behaviours are defined in the event sheet, and can be contextual as well as part of the main logic.