The Citizen’s Bomb. Long live the revolution.
In the past week or so, I had to jump back to the AI in order to implement key gameplay mechanics, namely the contraband check and the arrest warrant.
The contraband check (as it was in 2400AD) is a Robot approaching the Player, seemingly randomly, and conducting a spot-check whether the Player is carrying any items.
The arrest warrant — not in 2400AD — is similar in concept, but that it checks whether you have accrued too many demerits and therefore will be accosted and told to submit to imprisonment.
(In 2400AD, when you collect too many demerits, the Robots just shoot at you. The Robot Authority is slightly more civilised than the Tzorg.)
These two mechanics required AI to be improved.
But I required a Robot to determine a Player’s total demerits, or decide on conducting a contraband check based on a controllable game mechanic formula, it was apparent that I needed to expand the AI graph’s ability to call in-game functions where those computations could be made.
dofunc is a directive inside the graph that simply calls in-game functions. The format is simple:
dofunc <function> [<arg>]
The image below shows an example. The blue circles are
dofunc IsCheckCooldown as the example. Contraband checks should not occur too often, so a cooldown threshold is implemented. That function’s purpose is to check whether a contraband check cooldown threshold has been met.
The in-game function looks like this.
It returns either “0” or “1” (strings), and that forms a GlobalEventHandler name of
onfunc IsCheckCooldown 0 or
onfunc IsCheckCooldown 1 depending on the result. In the top image, you can see that a function called
RollForCheck does the same thing.
So that’s one application of the functions.
The other application is one that sets variables. In the previous AI version, variables were set using the
var directive. But what was needed was for functions to set variables as dictated by the AI.
So here’s an example of that. When a Checker Robot is attacked, it doesn’t fight back because it doesn’t have weapons. So it runs away. It tries to find a location that is out of sight of your weapon.
I created a function called
CheckerFindNearestHiddenTile. See the left blue circle below. Note that I use
hiddentile as the
<arg> parameter (the 3rd keyword).
The in-game function looks like:
One difference here is that I don’t use the GlobalEventHandler as a way to get the variable. Instead, the function uses the
<arg>, which is
"hiddentile" as the base name of the variables to set in the AI’s memory. In the function above, I set an X and Y variable using
"hiddentile" as my base name, and back in the AI graph, I use it to determine the coordinates for the Robot’s movement.
Engine’s the limit
I also use functions to do what would normally be tedious to do if I didn’t have the AI as the framework. For example, I am able to call a function
dofunc CheckerSayAttacked, which brings up a speech bubble above the Robot saying its being attacked.
The function capability now attached to the AI’s framework allows graphs to be re-used across different Robots, so that the same function can be used on another kind of Robot.
Functions are also used to raise the alert level on a global scope, they are used to bring up the Convo system so when the Robot comes close to the Player, and if the Robot intends to accost the Player, the Convo system comes up with the appropriate topic entry.
So far, the fanciest function I’ve done was a predictive search for the Player when the Robot has lost LOS of the Player. The power of engine is the limit, whatever the engine is. And because this AI is agnostic, I hope to be able to port this functionality in Unity when the time comes around to do it.
The video below shows a sample of the AI in action. The Player has accumulated enough demerits that the Checker will accost him. If the Player decides not to submit to detention, the Checker runs away, and the Minder (the other Robot) engages the Player in combat.
A short demo of the mechanism of revealing the Player behind certain elements in the scene. Still rather rough; some adjustments are needed, but the principle seems to work.
The image below illustrates the implementation.
The mechanism consists of 3 colliders, and elements that have a
reveal attribute that can either be
The 3 colliders are:
The procedure is:
- When the
reveal=frontelement, then that element becomes semi-transparent.
- When the
reveal=backelement, then that element becomes semi-transparent.
- When the
reveal=topelement, then that element becomes invisible.
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.
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 Citizen’s gameplay themes is the limitation of what weapons you can use. As a citizen of a dystopian, Robot-controlled city, you couldn’t freely carry dangerous goods and not expect to be accosted by Robot patrollers.
It was in the INV system that I first wanted to express that game concept. The INV system was originally conceived so that the Player can carry only a few weapons. So I delineated certain types of items can only be placed in certain ‘slots’. I also limited the number of slots for a particular category.
However, in time, I came to think that the limitation was a bit too extreme. It was complicated from the point of view of mechanism, but it also had logical game problems.
Nearly 3 years ago I was working on an RnD game project (dubbed Henry) which was supposed to feature multiple characters. The system allowed the viewing of different inventories within the same interface, and allowed trading between characters through a drag-and-drop mechanism. It featured multiple pages and a categorisation of items; weapons and armour were automatically put into the upper slots, and other adventure items were put underneath.
It was Henry‘s Inventory system that gave me my first experience in the in the difficulty in doing inventories, from the organisation of items, to the behaviours of drag-and-drop and how the logic of how things are arranged and displayed.
Also from Henry I took the idea of categorisation, which is the exclusive placement of items of a certain type into a section of slots in the interface.
But categorisation, I later decided, was not necessary if I was just simply gunning for weapon limitations. There were other ways of discouraging the Player from carrying too many, from the increased likelihood of getting checked by Robots, or simply the inability to use them effectively once the shooting started.
Because there were hundreds of ways to skin the limitation cat, I eased my rigid rules in the INV system. However, unlike Henry I had two other Inventory-related concepts that I had to address to introduced their own complexity: Readyslot, and Trade.
Without going into too much details about the the Readyslot’s mechanics, it is simply the place where weapons that will be used immediately for combat are put.
Switching weapons that are already in the Readyslot are done immediately. However, there’s a time-delay when you try equipping weapons from the INV, which may be akin to taking something from your backpack. This is how the game discourages the Player from swapping weapons from the INV which may potentially contain a lot of different weapons in the game.
There are other characteristics: there are only 3 slots in the Readyslot area, and that is significant. Pistols are 1-slot weapons, subguns (i.e. SMG) are 2-slot weapons, and rifles are 3-slot weapons. You can mix and match any weapon configuration that the number of slots numerically allows.
But there are special considerations for pistols, too: you can dual-wield pistols.
If you choose to equip a subgun, you can carry another pistol as ‘backup’.
If you choose a rifle, the most powerful weapons in the game, you are limited to that weapon only, and if you try to change weapons from the backpack, there’s a time-delay to get it.
The technical challenged associated with the Readyslot is how that in itself is an extension of the INV system even though it may not look like it visually. The Readyslot is a categorisation, so only weapons can be placed in there.
The Trade system is essentially the INV system, but using a different source for the contents of the INV. For example, NPCs have their own INV database, and even scene elements, like a rubbish bin that can potentially hold items, have their own INV system.
The Trade system is a little different from INV in that there is a variable slot designation that is dictated by some database (in this case it’s specified in Tiled). For example, a rubbish bin will have 2×2 Trade INV (a.k.a. TINV). A dead robot will have 1×1. Some may have 3×1, or 4×2, etc. And thus there were many considerations about how the system will respond if there was an attempt to populate the TINV with more slots that it could hold, or items that wouldn’t fit the dimension of the slots. For example, a 3-slot rifle cannot fit in a 2×2 TINV.
Slot size, width and height
In games like Diablo, items occupy ‘slots’ in the inventory. But Diablo’s system is very elaborate, as items have both width and height. For Citizen, I decided only to consider how many slots a certain item will occupy. For the most part, only rifles and subguns occupy more than one slot. This greatly simplified the system.
The reason why this is a big deal is because one of the challenges of making an INV is the correct display of items in their proper slots. When dragging a rifle (3-slots) at the right-most slot of the INV, you expect the system to compensate for the size; it must not place it the right-most slot, but 3-slots to the left in order to the rifle to fit the intended placement location.
Also, you have to consider if there are items currently in place in those slots. Will you allow items to be displaced? If so, how do you logically re-position them that makes intuitive sense?
If dragging a rifle from the INV to the Readyslot that is already full of weapons, will you make a swap? Or disallow it?
It’s questions like that, and every conceivable permutation of how one item is dragged from one place or another, dropped onto itself, or another dropped onto it, or something else entirely, all those things filled the 2 weeks I spent designing and iterating through the INV and Readyslot systems
The multi-page function isn’t yet implemented, but I have to make sure to what extent I implement it. How many items will I end up implementing in the game’s narrative and combat? How many pages will it fill? Should I have unlimited pages? Or is one page a good simply limit?
Lots of questions. But it’s all part of the fun, right?
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 above image shows Topics (yellow), Choices (green), Doers (cyan), ChoiceGroups (pink), Entry (white).
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. 5 ->::intro 6 ++worried ~text:: Worried 7 ~dostate::+met_player 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 ----- ->::intro # EntryTopic: Entry for cond ?@met_player,$!zak_quest_rejected ----- -> ?@met_player,$!zak_quest_rejected::intro_hi_again
# Topic: intro ----------------------- # ~text ==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. # ~choices ==intro -1 ~choices::worried # ~dostate ==intro -1 ~dostate::+met_player
> Choice ```yml # Choice: worried ----------------------- # ~text ++worried ~text:: Worried # Choice: subrailpass ----------------------- # ~text ++subrailpass ~text:: Sub-Rail Pass
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 initiated
onconvo 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.
(This is a follow-up post to my other one which explained the reasons for choosing to remain working in C2).
When I review this whole venture, it stems from the desire to create a game on my own. I want to create the graphics, the logic, the story, the words, the music — everything — like how those guy did it back in the day when I was a wee child playing their games.
I came to use C2 because of its simplicity, and the quickness in which I can throw something together and get results. There’s nothing like instant gratification that hooks you in.
But as project size increases, so do doubts about working in C2. Lots of niggles, lots of creaks and groans give me doubts as to appropriateness of the engine/framework for Citizen. A framework is a convenience. But everything out there is a convenience except C++. I could have approached Corona or Phaser, I could have used Godot or Unity, and some of them might have been more suited to the task.
When you turn to an established game engine like Unity, you don’t tend to have that many doubts that your main goal is achievable if you were clever enough to code/design your game well. That’s because you see the sort of games that have already been developed and you can’t really argue with the fact that Unity is established for a reason.
Then you take a gander to notice the sort of games that C2 is generally used for, which is not the sort of thing Citizen is. There are plenty of developmental previews and tutorials of isometric games, — none of them are serious enough to take umbrage — but no finished product as far as the Search Engine can see.
Then one day, you come upon some kind of undesirable behaviour that you have no control over (because Construct is a very blackbox environment). You start weighing in the facts: that C2 isn’t being developed any more; that critical bugs appear in the latest builds; that downgrading is the only recourse because the developers will likely not fix it because they are committed to C3, an app whose design philosophy doesn’t nearly tick enough of your own boxes for you to use with dignity.
These leave you imagining the sort of adventures you’ll have with this little boat, which will receive no more refits, as it takes you across the pond. What awaits you, who knows? But there are tales of show-stopping, insanity-inducing odds, and some are journalistic facts as the asset obfuscation that you won’t get.
At the end of the day, it’s a ‘use the right tool for the job’ situation. And as the days go by, C2 (and C3) are becoming less of the ‘right’ tool to actually publish a game. But as many C2 users know, C2 is great at prototyping. And so at prototyping it will be relegated to.
Even if from some miracle I complete the prototype and I am able to scale it to encompass the scope of the game I want to make, it will still be a hard-sell for me to publish the game in C2 because of the absence of even a rudimentary obfuscation method: another design philosophy that didn’t get ticked.
We shall see…