Complex and dynamic behaviours can be difficult to achieve, there are hieratical FSMs, behaviour trees, utility function, GOAP, etc.. Is there an easier way? In this article I’d like to go over how I have been doing this. This was written for Unity game engine, but could be modified for other engines.
I first discovered this pattern watching one of my favorite GDC talks by valve: AI-driven Dynamic Dialog through Fuzzy Pattern Matching. In it they describe it as “A system for tracking lots of facts and then picking a rule from a database based on matching criteria”, they also state that they created it to be extremely simple, trying to take the simple route most the time in development, which was also what I try to do.
With this pattern you can make anything in your game into a Condition, health status, day/night cycle, position, anything! It just needs to return a bool, so Conditions typically ask a yes no question, is health less than 10? Is the player closer than 20 meters? Etc..
Act are the actions to be taken. They have a list of Conditions that must all be true for the Act to be executed, this is done in a Verify method. In the valve version they have the number of conditions also a condition so they could evaluate actions with more conditions first. I simplified this by sorting the list of Acts based on how many Conditions it has, or conditions.Count, this mean that if you have an Act to say something when they see a tree and an act to say something when they see a tree and the tree is a Christmas tree, the second Act will always be checked first, because it has more Conditions.
There can be issues when Acts have the same amount of Conditions, but can be easily remedied. If you have an Act to rush attack if a player is in range, but you also have another Act that to run away from the player if health is less than a certain amount, they both have 1 condition and whichever was sorted higher on the list will be executed first every time, to fix this you need to add in another condition to the attack Act, if health is not less than a certain amount, so then if a player is in range and it's health is not low it will rush attack, if the player is in range but the NPC's health is less than a certain amount it will run away.
Now we come to the last part, the state machine, or loop. Each Act can be though of as a state and the Conditions are how we select which state(s) we are in. Each character has a list of Acts and they are sorted descending based on how many conditions there are. Then it does a linear search and returns the first match. A match occurs the Verify method returns true, meaning all conditions of the Act are verified. When an Act's Verify method returns true the PerformAct method is called. This is where things deviate based on the type of state machine you want, once PerfomAct has been called, if you want a finite state machine (FSM) you break out of the loop, if you want a Fuzzy State Machine (FuSM), allowing more than one state at a time, then you do not want to break out of the loop, you want it to keep going and execute all the Acts that Verify true. FuSM can be useful when you want to do multiple tings at once, such as running and shooting, with a FSM you would need a state for running, a state for shooting and a state for running and shooting, with a FuSM you just need the running and shooting state and then you run both states at once. One more issue that can arise is no Act is verified and nothing happens, a fix for this is making a default Act that has no conditions, it will only run if nothing else has, if using a FSM setup.
I quickly realized this system could be used to drive nearly anything! Behaviours, reactions, animation, sound, dialog, Ai, gameplay logic and pretty much anything!
I used ScriptableObject Architecture to to make it easy to use for programmers and non programmers to use and work together. This allows all the Conditions and Acts to be modified live in play mode and all changes are persistent, great for those final adjustments.
In my Dynamic Behaviour demo I use the same system for NPCs and players, for the player I made Conditions about inputs, like isButtonDown, that way I can use the same Acts for players and NPCs, I just have to change the conditions.
In my Ask/Tell Dynamic Dialog asset, I implement memory as a list of Dialog. Then you can see what was the last thing said by checking the last item on the list, or you can see if the list contains that Dialog to see if it was said before, how many times it was said and all these things are used as conditions to create dialog that responds to player actions and world state. The NPC replies to what the character said, like any other NPC, but can also say something different if the character keeps repeating the same thing, giving more information or getting annoyed, all kinds of possibilities.
This system is holistic, meaning that you can use it to control a player or npc/enemy. This is because a condition is abstract enough it can be an Ai sensory input or a player pressing a button. That allowed me to use it to trigger automatic responses for my players, like the idle animation and the auto reload in my game Vampire Dystopia as well as use the same system to control the npc, I could take control with the second player and go back to Ai control just by changing out one list, all in gameplay using ScriptableObjects.
An abstract Condition base class that contains a virtual Verify method that returns a bool. This way all conditions can return a true or false state. The verify method is virtual so it can be defined as whatever it needs to be, but it must return a boolean value. This way anything can be a condition! IsHealthLessThanFive, IsGrounded, IsRunning, HasAmmo, even player controls: IsPressing A, IsHoldingShift.. Environmental variables: IsDay, IsOver90Degrees, etc.. you can make anything in your game a Condition.
The Condition Verify method needs a parameter for the GameObject or character class that is being evaluated.
abstract class Condition
virtual bool Verify(GameObject go)
//code to set is true goes here
Act is defined as an abstract base class as well. An Act has a list of conditions. The Act also has a Verify method that verifies all conditions in the list. The Act methods need a parameter for the GameObject or character class that is being evaluated or acted upon.
abstract class Act
bool Verify(GameObject go)
foreach(Condition condition in conditions)
virtual void PerformAct(GameObject go)
//your code goes here
The state machine is the easiest part, this goes in your character code. The state machine passes itself as the parameter for Act and Conditions so everything is setup for the correct object.
For a FSM you want this:
foreach(Act act in acts)
For a FuSM you would omit the return like this:
foreach(Act act in acts)
And that is all the code!
One more thing, I also mention I sort the list of Acts descending based on their how many Conditions they have, I made a method for this and I call it OnEnable() this keeps the list sorted correctly.
To make things even easier, all these Acts can be designed in a simple spreadsheet. You just make a table with a column for the Act and the list for Conditions that must be met for it be performed.
An Ai jump Act may look something like this:
Jump IsGrounded, IsMovingForward, HoleDetected
So when Jump is evaluated, it will check if the Ai is on the ground, moving forward and there is a hole ahead of them. This can also be used for dialog.
Let's say we want to create the following conversation:
“who are you?”
“who are you?”
“I’m Bob Bobertson, from Boberton, I’m the blacksmith here in town, if you need some weapons come see me.”
“who are you?”
“Look, I told you who I am already! Ok?”
DetailedAnswer AskedWhoAreYou, AskedBefore, NotAskedRepeatedly
Annoyed AskedWhoAreYou, AskedRepeatedly
So the table is really simple, you need three Acts and5 Conditions. Asked who are you just checks if the dialog matches, AskedBefore check the memory if they were asked before, AskedRepeatedly checks if the memory if they were asked this same question more than two times, NotAskedRepeatedly checks if the memory if they were not asked this same question more than two times, this is needed so that the DetailedAnswer will not run when the Annoyed should. You can make all kinds of Conditions for this, if you want replies to specific number of times something has been repeated, you could make Conditions for each one you want such as, AskedOneTIme, AskedTwoTimes, AskedFiftyTimes..
In Utility Function each action is evaluated separately, then compared against each other and the highest scoring action is chosen.
In this system each action is evaluated separately as well, but they are not scored, it evaluates actions separately until is gets a match, so not all actions will be evaluated unless using a FuSM setup. I think that by only analyzing some of the list and not comparing them against each other there is a lot less calculations to do which may make it more efficient than a utility function, but I have not done any testing on this.
This system is really simple, easy to setup and use. It's very dynamic and can be tweaked in play mode, Acts and Conditions can be easily modified to adjust behaviour without writing any code! Public variables are be setup in Conditions for easy adjusting, and the Conditions an act uses can be swapped out even live in play mode! Great for designers and non programmers to make all those adjustments without the need of a programmer.
I feel this pattern needs to be explored, it’s easy to use and powerful, but what are its limitations and weaknesses? How does it rate for efficiency compared to other solutions? The only issue I've run into is multiple Acts qualifying at once, which is easily fixed with more conditions. Every system has a failing, this one's complexities my exist within the condition lists.
I have used this system in Vampire Dystopia, Ask/Tell Dynamic Dialog and Dynamic Behaviour. All are available for free if you want to check any of them out.
Valve Bark system:
Vampire Dystopia: https://logicandchaos.itch.io/...
Ask/Tell Dynamic Dialog: https://assetstore.unity.com/p...
Dynamic Behaviour: https://assetstore.unity.com/p...