When I started to think about the hit-chance system for combat, I wanted to address the ‘consecutive hit/miss’ issue that pops up in turn-based games. Even some of my own favourites games, like X-COM have players complaining of their questionable bad luck (naturally, no one complains of good luck).
Citizen is a real-time game. The random-hit issue presents itself differently from a turn-based game. I think the issue is more glaring in a turn-based game because every shot is felt. Nevertheless, as my iterative test showed, this issue is still relevantly noticeable in real-time game.
Luck vs skill
I think that the root of the issue with this game design is the distinction between luck vs skill. If we say that I have a 25% chance of hitting, and then proceed to roll a d8, and hope to get <= 2, then, that’s the concept of luck as all dice rolls are.
But in combat, it’s not all about luck; rather, it’s more about skill. When we say that we can hit a bulls-eye 25% of the time — contingent on factors affecting accuracy — we are likely referring to a fact, not a chance. To say that I can hit so-and-so 25% of the time means that I would have gone to a firing range, shot some rounds, and observed how many rounds have gone in the #1 ring and how many have landed outside. Then I would repeat this process until I get an average percentage, which becomes a quantitative representation of my skill level. This is the principle of skill, which I wished to represent in the Give-Me-A-Chance (GMAC) system.
Some folks think that the problem with pseudo-random numbers (also referred to RNG — random number generation) is that they are ‘pseudo’, rather than ‘true’ random numbers. For the purposes of rolling for a hit that technical distinction is irrelevant. The important aspect about RNG is that whether they are ‘pseudo’ or ‘true’, they they depict luck rather than skill.
When you view a graphical representation of random numbers, they do not form any coherent shape, making them suitable for unpredictability.
But in a combat system some unpredictability is necessary. But the unpredictability needs to be felt in the details of combat to give it an organic touch. But unpredictability shouldn’t be felt on the wider scope of the whole combat system, which is the complaint of most players.
Enter GMAC, which tries to balance the hit-chance based on previous hits/misses. This means that the hit-chance (i.e. the skill level) is being modified based on the success rate of hits. In short, GMAC is intended to make hit-chance feel like the percentage it is depicting.
As an example of why RNG without modifications feels like luck, we run a random number generator for 100 iterations. For each iteration, it compares a random number to 75%; if the random number falls below 75%, then that’s considered a hit. Then the average percentage for those 100 iterations is shown. Then we run it 10 more times (10 cycles). This is an example of the results:
Each line represents a single cycle. Remember that the percentages are averages of 100 comparisons per single cycle. This means that for cycle # 3, the average percentage was 7% higher than the target. And at cycle #9 it was 5% lower, giving a total range of 12% difference, which is rather a big deal.
The principle behind GMAC is counting the success rate of shots. The basic principle is this: when you shoot the first round, you roll against your hit-chance, which is your basic skill level, which we’ll set as 50%. If you hit the target, you would have had a 100% success rate (1 out of 1 shot). Because you are 100% successful so far, and your skill level is 50%, your next hit-chance would be penalised to, say, 1%. If you make the second shot (roll below 1% — yes it’s actually possible!), then you would continue to have a 100% success rate, and your hit-chance continues to be penalised to 1%, which you will likely not keep on hitting for long.
On the flip-side, if you missed the first shot, your success rate is 0%, and your hit-chance is given a slight bonus so as to make you more likely to hit.
Like a true socialist system, it helps you when you’re down, and when you’re getting too successful, it shuts you down.
Compared to the previous random hit-chance test, the above image shows GMAC modifying the hit-chance. hit_chance is incremented, and average is the result of the GMAC operation. The closer average is to hit_chance means that it is working.
However, you can see that in the hit_chance:12 there is a large 3% discrepancy. This occurs in the lower percentages (< 20%). GMAC represents higher percentages far more accurately than low ones.
I had to decide how many shots I wanted to track in order to compute the percentages accurately. As I’m tracking each shot that fires, I have to know when to ‘give up’ and reset the counter. The primary reason for this is the assumption that the hit-chance changes over time. For example, if the Player is moving towards the enemy, the hit-chance increases. If I track too many shots, then I would potentially be comparing the success rate of another hit-chance that is drastically different from the current one.
Of course, the downside of tracking too little is that I won’t be able to represent the accuracy of certain the percentages because there wouldn’t be enough ‘resolution’ for the math to compute.
In the actual GMAC v1 algorithm, however, once properly tweaked, it seemed like 10 shots satisfied all requirements; resolution was good especially when I simulated the shot series over a thousand times.
There was a peculiarity with the algorithm, which I think was related to the fact that I decided on counting 10 shots.
The lowest percentage I could track was 10%, which is logical, since any hit-chance above 0% is going to be adjusted to have at least once in 10 shots, giving it always a minimum of 10%. Thus, combat skills should always resolve to anything above 10%.
Hit-chance and GMAC
Note that GMAC is a modifier. The actual hit-chance is calculated by the factors governing the combat mechanics; weapon accuracy, weapon range, Player skills, etc. Then that ‘base’ hit-chance is passed onto GMAC which takes care of the balancing for success rate.
I think the GMAC modifier is a good RPG combat concept. It allows randomisation on a shot-by-shot basis, but will endeavour to reflect your actual skill level by increasing or decreasing likelihood of hitting based on on your success rate. Hopefully, this replaces the feeling of being unlucky with the feeling of being unskilled.
One of the significant progress milestones I’ve done since the beginning of March was the implementation of a system I call Convo and a rudimentary NPC AI that allows NPCs to move/wander and do things randomly, if their base purpose allows it.
To achieve the implementation, two fundamental concepts had to be developed. The first is SNTX, and the second is the usage of the TGF to express nodal networks that are are interpreted at runtime.
SNTX is a procedural markup language designed to be injected into dicts. The resulting dict keys are procedurally looked-up to get to a resulting value.
SNTX is a significant upgrade to the TalkDialogue system that I developed in 2015. The difference lies in the robustness of handling Conditions, as well as clarity. The idea behind the TalkDialogue and SNTX markups was the ability to author a dialogue tree using a text file. By and large this has been possible, but the branching nature of dialogue trees makes writing everything down in one linear text file still confusing. It was this reason that I looked to yEd in order to visualise the dialogue tree .
One of the important aspects of SNTX is the concept of Conditions. Conditions are simply asking: is this node valid? If a node has a Condition, the Condition must be True in order for the node to be processed by the system.
Conditions check 3 things:
Accomps – a global dict that represent arbitrary ‘accomplishments’.
State – the NPC’s ‘state’ variable which is essentially a CSV string, and can comprise any number string tokens describing its state
INV – the Player’s Inventory can be searched for a particular item, for a particular quantity.
The other side of Conditions are Doers. Doers set Accomps or an NPC’s State. Within the Convo (and Astrip) systems, a node is capable of executing a command telling to either add/remove/modify a key in the Accomps, or amend an NPC’s State. It also allows transferring of items from NPC to Player, and vice-versa.
With Doers and Conditions combined, it allows me to script an interactive storyline.
In fact, I have completed a sample quest using all these systems as an early-stage trial for the prototype. I’m happy to say that it also involves having to kill a Robot.
yEd and TGF
yEd is a very capable diagramming application. It has become my weapon of choice because it is one of the very few programs that allow TGF export. TGF (Trivial Graph Format) is a super-lightweight nodal graph format that, when coupled with a well-thought-out markup, can solve a large number of data relationship issues.
My usage of yEd and TGFs began with simply creating nodes that were labeled as SNTX keys. I drew edges that served as annotations to their relationships, though they didn’t actually define the relationship. That is, except for Choices: for every Topic a line can be drawn to a Choice, which will then be recognised by the TGF2Convo converter tool (explained later).
The labels in the nodes reflect directly as it is written in SNTX. When it is written in TGF it looks something like:
4 ==intro -1 ~text:: [The man seems to be so worried that he hardly notices you when you come up to talk.]\nWha? Oh, hi. You must be the new recruit. My name's Zak.
6 ++worried ~text:: Worried
8 -> ?@met_player,$!zak_quest_rejected::intro_hi_again
9 ==intro_hi_again -1 ~text::Hi, again.
10 ==worried -1 ~text:: I've lost the Sub- Rail pass that I was issued with. My team leader is going to kill me.
11 ++subrailpass ~text:: Sub-Rail Pass
12 ==subrailpass -1 ~text:: I don't know where I might have dropped it. I swear it was in my pocket.
13 ++quest ?@!questaccepted ~text:: [...]
This is translated using a Python script called TGF2Convo, and the output looks like this:
# EntryTopic: Entry for cond -----
# EntryTopic: Entry for cond ?@met_player,$!zak_quest_rejected -----
# Topic: intro -----------------------
==intro -1 ~text::[The man seems to be so worried that he hardly notices you when you come up to talk.]\nWha? Oh, hi. You must be the new recruit. My name's Zak.
==intro -1 ~choices::worried
==intro -1 ~dostate::+met_player
The TGF2Convo tool uses Markdown syntax so that when a Markdown viewer is used, it is easier to understand.
Ultimately, the dict is populated with these values, and the runtime uses the dict to determine the path of the dialogue.
However, this is not my ideal way. I had developed SNTX ahead of using TGF. Since using TGF with AI, I realised that utilising TGF fully would be a better way to go for dialogues, but this requires a re-working of the Convo system. This might be done at the end of the prototype phase.
AI and TGF
After the Convo system and SNTX were developed, I had to jump into AI. At that point I had two choices: I could go and write the AI in C2 as events, or I could attempt something much harder, ultimately more flexible, and platform-agnostic. I chose the latter.
When looking at what I wanted to do with the AI, I decided that I just needed a very simple system of controlling the actions and movements of NPCs (not the enemy NPCs). I outlined the requirements of what it would take for an NPC (Zak) who has lost something and is wandering around a given area.
First, there is the point of movement. The NPC should be able to use waypoints. Second, the NPC should have some random ability to use waypoints. Third, the NPC should have some random wait times. Fourth, the NPC should have random animation.
With all that in mind I went into the specifics of what components need to exist to make that happen. I needed:
ability to set NPC variables
ability to query NPC variables from within the AI graph
ability to initiate a ‘move’ (pathfinding) command from the AI graph to the runtime
likewise, the ability to ‘wait’
the ability to stop
the ability to have AI graph randomly choose between named choices
the ability to receive event handlers from the runtime
When I started developing the AI, I would click on Zak to talk to him. A ‘Talk’ icon would appear, but Zak kept on moving. Though I could have effectively paused the game so that I could properly select the icon before Zak walked away, I thought it would be better to tell the AI to stop Zak.
That’s when event handlers came into the picture, which also brought forth a host of different possibilities. For example, I created an event handler called onastrip, which is called when you try to initiate an interaction. In the AI graph, this event handler is connected to make Zak stop. If the icons are ‘aborted’, the event onastripend is fired, and the AI graph is wired to make Zak resume where he left off. When a Convo is initiatedonconvo is fired, and this stops him, too. When Convo is ended, onconvoend is fired.
If a waypoint is missing, there is a wpmissing event that fires, which allows the AI to adjust itself in order to get a proper waypoint index.
The idea of ‘events handlers’ also gradually slid into the other aspects of the AI graph, where ‘events’ are triggered as a result of an operation (a Doer). For example, a choose node chooses between one of any number of events to fire. As long as that event handler is present, then it is a valid event.
There is an immense satisfaction in working this way. There’s definitely a lot more work involved, but it brings the systems I’m working on at a higher level of flexibility, while at the same time, it’s still within the realm of my understanding since I’m the one developing it.
Although I don’t know how much of the AI, in particular, will make it through the Unity alpha, it is undoubtedly a very useful piece of development because it informs me of the kinds of behaviours I may need to do; which aspects to simplify, and which aspects need more complex behaviours.
The C2 manual references the project file folder which contains other files other than its default.
Even though files and subfolders can be created in this folder, it doesn’t automatically become part of the caproj unless it is actually specified/registered inside the caproj. Inside C2, it is possible to ‘auto-import’ files, but this only works at the root level of /Files; directories aren’t traversed, making this mechanism suitable for single files, like configuration files.
However, when using the Files folder in other ways, such as replacing animation using Rex’s Animation Loader, it would be a monumental task to get all these files in. So I’ve written a Python function that traverses any given folder and writes out a block that can be copied and pasted into the caproj, which is near the end of the caproj. Perhaps in future, I will make the procedure more seamless; right now, the manual copy-paste is for security reasons.
The code below is very unpolished, but gets the idea across.
''' Create a folder structure in caproj/xml format with a given directory
The intention is to create a sprite animation folder in the /Files
project folder, and have that referenced as imported files in the caproj.
The output of this function is to be copied and pasted into the caproj.
gb = glob_buffer()
ext = '.png'
filesdir = 'X:/GAME_PROJECTS/c2/Files/'
unitdir = 'hero_w'
rootdir_name = '%s%s' % (filesdir, unitdir)
rootfolder = CaprojFileFolder(rootdir_name, ext, filesdir)
gb.buffer += '\n%s' %rootfolder.xml
fn = 'c:/outputcaproj.txt'
f = open(fn,'w')
for b in gb.buffer:
for c in folder.content:
if isinstance(c,CaprojFileFolder) == True:
gb.buffer += '\n%s' %c.xml
gb.buffer += '\n\t%s' %c
gb.buffer += '\n</file-folder>'
''' The folder class contains info about the folder, eg content, name '''
def __init__(self, path, ext, rootdir):
# code below considers trailing / separator like c:/test/folder/
# where the last -1 index will contain ''. code below doesn't allow that ''
self.rootdir = rootdir # root for relative path
self.ext = ext # allowed file extension
self.name = [x for x in path.split('/') if x != ''][-1]
self.xml = '<file-folder name="%s">' % self.name
self.path = path
self.content = 
mf = matchfiles_full(self.path,'*')
for m in mf:
if os.path.isfile(m) == False: # folder
newfolder = self.__class__(m,self.ext, self.rootdir)
fl = [x for x in m.split('/') if x != ''][-1]
# check if extension is allowed
relpath = self.path[len(self.rootdir):]
if fl.endswith(self.ext) == True:
fls = '<file name="%s/%s" />' % (relpath,fl)
The class CaprojFolder represents a folder and the contents of the folder (stored in the content list). An element in content may either be another CaprojFolder object, or may be a path to a file. The caproj code snippet is written in a text file in the C: drive (!).
Once this snippet is pasted over to the caproj new folders and subfolders would be created in the /Files folder the matches the one found in the file system. The only major difference is the name of the actual files, which I explain below.
Referencing the files
Though the script mimicks the file system folder structure, C2 does not use these folders as a path to the file. In other words, C2’s folders structure is purely for visual organisation within the C2 editor. The files themselves are treated as though they were in the root directory. Therefore, I opted to name the files to represent their full relative path.
For example, say the script references a file: hero_w/run/000.png
This file is put under /Files/hero_w/run. But it is also named, literally: hero_w/run/000.png, and not just 000.png as you would normally expect. If I had named the file 000.png, there would be no way to distinguish this 000.png with other files in other C2 File subfolders. So a unique name was necessary.
As described in Rex’s own site about GridMove.Direction, this expression is only valid if the movement is to a neighbouring tile. In the Slidewalk system, however, although the construction of the Slidewalk path may be straight, the target tiles may be separated from each other for efficiency sake (ie less node paths to draw in Tiled).
It is inefficient to ‘connect-the-dots’ in C2 by tagging the tiles that the path goes over, so I’m instructing GridMove to move to the Slidewalk target tiles that are not neighbours, This makes GridMove.Direction invalid.
What I did, instead, was to do my own GridMove direction by comparing GridMove’s SourceLX/Y and DestinationLX/Y expression and generate a direction using conditions.
At this point, however, the conditions assume that the nodes are positioned in a way that it follows the lines for 8 directions, as it checks how the target tile’s LX and LY are different for the source’s corresponding LX and LY.
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 () 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.
Given an intend_list:
This is used when switching from a small weapon to a medium weapon, and only applies when a small weapon is equipped.
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.
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.
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.
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 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:
‘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:
Then PlayerSetAnimation is called with that intend_list, in which is goes back to idle.
Let’s try another intend_list going through the same process:
‘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:
After the lookup is processed, it is played and then checked like Example 1.
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:
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.
Given the this intend_list:
These are just Pose and Speed changes, so they are considered separately.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Knowledgebase for creating a Construct 2 isometric game.