Sequences Introduction and Initialization - UberWaffe/OpenRA GitHub Wiki

All animations in OpenRA are sprite-animations. An animation, therefore, is just a sequence of sprites played from beginning to end, very much like a flip-book animation. These sprites animations, basically just a bunch of sprites in the correct order, are stored in .SHP files (which is Westwood's original sprite format). These SHPs can be found in the MIX files. These SHPS, however, may (and usually do) contain several of the sprite-animations for a particular unit. Some kind of meta-data is needed to read the sprites in, and organize them according to animation... to tell the engine how specific animations map to a range of sprites. This is where sequences.yaml comes in.

Sequences.yaml

Each top-level node in that YAML corresponds to a unit/building, with the next sub-level defining the available animations for that unit. As an example, take a look at the second node in the file called "fact". That just happens to be the internal name for the Construction Yard. The second level of nodes are: idle, make, build, build, damaged-idle, damaged-build, which represent the animations associated with the Yard. Let's take a look at them:

idle:
	Start: 0
make: factmake
	Start: 0
	Length: *
build:
	Start: 1
	Length: 25
damaged-idle:
	Start: 26
damaged-build:
	Start: 27
	Length: 25

That's what a typical unit/building's entry in Sequences.yaml looks like. While defining sequences, the 'Start' key's value points to the starting frame/sprite number in the sprite list. 'Length' is just that; it defines the number of sprites to be picked up, beginning from the start frame. 'Length' can have a value of '*' which basically means that the animation extends till the last frame available. Another property you'll find is 'Facings'. This defines the number of directions that artwork has been provided for. Typical this value is 8 in case of infantry units and 16/32 in case of vehicles. For example, see e1's entry (Rifle Infantry). Another property called 'Tick' is also available, though its not used as frequently. It can be used to override the rate at which the sprites are update/animated. It defaults to a value of 40ms if nothing is specified. e3's (Rocket Soldier) idle1 animation, for example, sets time to 120. Which means that that idle1 animation will only be updated once in three update cycles.

SequenceProvider Class

This class is what maintains the list of sequences for all the units present in the game, in a static member variable called units. Here's the declaration for units:

static Dictionary<string, Dictionary<string, Sequence>> units;

While initializing the sequences the chink of the work lies in reading the sequence data from the yaml files provided, using this data to create instances of the Sequence class and then populating the units dictionary with these sequences.

SequenceProvider.Initialize(string[] sequenceFiles, List sequenceNodes):

is what achieves this for us. The code flow which leads to this function from startup is as follows:

Program.Main() -> Program.Run() -> Game.Initialize() -> Game.StartGame() -> ModData.PrepareMap() -> SequenceProvider.Initialize(Manifest.Sequences, map.Sequences)

SequenceProvider's Initialize method is passed an array of strings called sequenceFiles which contains paths to yaml files from which the sequence must be generated. Paths to sequence-yamls typically look like this:

"mods/ra/sequences.yaml"    (if mod = ra)
"mods/cnc/sequences.yaml" (if mod = cnc)

The following line of code goes through all of those yamls, parses through them and returns a list of MiniYamlNodes, where each MiniYamlNode represents a single unit. Here’s that line of code:

sequenceFiles.Select(s => MiniYaml.FromFile(s))

The function then loops through these MiniYamlNodes and calls the LoadSequencesForUnit() function on each of the key-value pairs of the MiniYamlNodes. The key (a string) is the unit name and the value is a reference to a MiniYaml object which contains the meta-data for the sequences for that unit.

SequenceProvider.LoadSequencesForUnit(string unit, MiniYaml sequences):

This static function is called from within Initialize() on all of the (unitname-MiniYaml) pairs that it generates from the given sequences.yaml file. It's main purpose is to replace the MiniYaml object passed in with an actual Dictionary<string, Sequence> object which implements a SequenceName-SequenceObject mapping. This is achieved primarily with the following line of code:

var seq = sequences.NodesDict.ToDictionary(x => x.Key, x => new Sequence(unit,x.Key,x.Value));

Here, sequences is the MiniYaml object that was passed down to it from Initialize() as the second parameter. It is representative of all the sequences associated with that unit. As you may notice, the Sequence class' constructor is called on each Sequence's MiniYaml node. Therefore, the final job of parsing through the Sequence's yaml meta-data and populating the sequence's start, length and other parameters is done by the Sequence class' constructor itself. The object that the constructor returns is then added to the seq Dictionary.

Once all sequences for the unit have been added to the seq dictionary, the unit name and its dictionary of sequences are added as a pair to the units Dictionary, which as mentioned earlier is a static member of the SequenceProvider class. Once, LoadSequencesForUnit() has been called on all the units iteratively, our sequence initialization job is essentially done. :)