Functions and variables in AI

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.

Functions — dofunc

When I look back at Zak’s AI graph, I realise that the scope was very limited to moving, which was all I wanted from it at the time.

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.

Enter dofunc. 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 directives.

Let’s take 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.

Variabes

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.

Advertisements

Isometric reveal mechanism wip

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 always, back, front, top.

The 3 colliders are:

  • frontface_reveal_collider
  • backface_reveal_collider
  • top_reveal_collider

The procedure is:

  • When the frontface_reveal_collider overlaps a reveal=front element, then that element becomes semi-transparent.
  • When the backface_reveal_collider overlaps a reveal=back element, then that element becomes semi-transparent.
  • When the top_reveal_collider overlaps a reveal=top element, then that element becomes invisible.

G.M.A.C. – Give-Me-A-Chance™ Hit System

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.

Randomness

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.

Give-Me-A-Chance

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.

Success rate

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.

Shot count

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.

Low percentage

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.