Desktop Application

Desktop Application using Artificial Intelligence

By
Thomas Vermeij

Artificial intelligence (or AI for short) is a concept where computers and machines mimic the problem solving and decision-making capabilities of the human mind. This is a concept that has been researched and improved upon over the last few decades. Of course, the depth and capabilities of an AI differs from case to case. Between the final boss of a game like Dark Souls and Stable Diffusion, an AI art creator, this difference is quite visible. Today we will dive a bit deeper on the concept of video game AI and how to make one.

AIs used in video games come in quite a wide variety, ranging from simple NPC’s walking around in the world to enemy bosses that noticeably take decisions to make your life worse.
Both differ hugely in complexity but fit their purpose. For example, if you gave every NPC in a game like Lost Judgement its own complex pathfinding and decision-making algorithm then your performance will hit rock bottom as soon as you encounter an area with quite a few NPC’s. On the other side, if all a final boss is capable of is surface level decisions, then players will quickly figure out how to use that to their advantage.

While the latter of the two examples has quite a bit of complexity, the former can be just as intriguing. The question of “How do you create an AI that feels lively but with low complexity?” was not as easily answered as I initially thought. Today we will dive into that question to see what answers lie beneath.

Table of Contents

Chapter 1: AI in games
Chapter 2: Behaviour Trees
Chapter 3: Creating simple behaviour
Chapter 4: Adding User Input
Conclusion

Chapter 1: AI in games

Screenshot of the NPC's in the game Hitman III, Dubai level.

You

can use various methods to create an AI in video games. Pathfinding is one example that already has various methods to achieve its goal. The AI we are looking at today is another method, a simple AI that acts like a humanoid and makes the world feel alive. These AIs are often used al NPC's walking around the world and reacting to whatever the player is doing. Examples of these types of AI are the NPCs in the Hitman games or in the Yakuza series. These NPC's walk around, going about their day until the player performs an action that would influence them to give a reaction. In the case of Hitman that is the player taking out a weapon or attacking someone, and the reaction being that everyone tries to run away from the player.
Another example is enemy AIs in fighting games like Dark Souls or the previously mentioned Yakuza. These have a deeper complexity than world NPC's because they need to react to what the player does and need to add enough variety, so the player experiences an adequate level of difficulty. In the case of Elden Ring, a particular boss would take different actions depending on what the player does, is the player far away? attempt to close the distance and attack.
The difference between these two lies in their complexity. The former has low complexity but can be applied to a large amount of NPC's without losing out performance. While the latter can can't be used in mass, they add much more variety to the game simply by existing.

Example of a State Machine made using the Unity animator.

To create AI behaviour, you can use multiple tools available online or within the particular engine you are working in. One such tool is a State Machine. State machines consist of various states that run code in a particular state of the AI (For example a state called "Sit" would make the character sit). State machines also consist of state transitions. These are needed to move between various states (For example, a transition from sit to stand would make the character stand up).
The example on the right shows a state machine we made. It consists of various states represented by a particular behaviour of the AI, and transitions which are represented by the white arrows.

Our goal is to create an AI that feels lively, reacts to the world around them without going too deep into complexity.
When we started this project, we used a state machine to create the first parts of our AI (The figure on the right). The Problem arose when we noticed that adding one particular state would mean we would need to add connections to all other states and edit the code in a way that would allow those connections to function properly. On top of that the state machine quickly turned into quite the mess that made it hard to understand what exactly it does. While a state machine like the one we used would be perfect if this was my end goal, the moment you try to add more complexity, the number of connections you would have to make would increase drastically.
With this problem in mind, we started researching possible solutions until we found an article by the developer of Project Zomboid went deep into the particular problem we was having.

Chapter 2: Behaviour trees

The article written by Chris Simpson delves deep into the nature of behaviour trees and what their purpose is. Behaviour trees are often used to emulate some form of decision making for an AI. An example would be to find and lay down in a bed. First you need to find a bed, then walk to the bed and when the AI has arrived lie down in the bed.
The reason we opted to go for behaviour trees eventually is due to the nature of the product we were making. With behaviour trees, my problems of unsalability would be drastically reduced on top of allowing for me to create libraries of behaviours that can be chained together to provide very convincing AI behaviour.
The beauty of behaviour trees lies in the fact that you can invoke all types of methods separately instead of having to code them all into your behaviour like you have to do with a state machine. Because of this you can often reuse the same action blocks in multiple places, without having to clutter your project with redundant code.

Example of a sequence node

Behaviour trees are made up of multiple node types, two of these node types are composite nodes and leaf nodes. Composite nodes are nodes that allow you to branch into multiple branches and always have one or more children. There are many types of composite nodes but three main ones that stand out:

  • Sequence node
  • Selector node
  • Parallel node
Example of a selector node

The sequence node plays its child nodes in order from left to right, as shown in the example. If one fails, then they all fail.
The Selector first tries the first child node and if that doesn’t work it will try the second child node and so on. When one succeeds it will return a success to the parent.
Lastly there is the Parallel node. The parallel node, as its name implies, executes all it’s children at the same time, if one task fails then they all fail.

Example of all three composite nodes

At the end of a branch, the leaf nodes perform certain actions that are bound to them, These are often methods or scripts. In the above example this would be the "Find a bed" or "Walk to the bed" nodes.

Chapter 3: Creating simple behaviour

Blemishine, a character from Arknights

The goal of the app we want to make today is a desktop app where the characters can walk across the bottom of your screen and will always be on the foreground. The characters will have their own behaviour in walking around and reacting to the scenery around them and have a few of their own character traits which will improve the diversity between each other.
To create our AI, we will need a few tools and programs to work with. First, we will be using a plugin for unity called “Behaviour Tree Designer.” This plugin allows us to use the editor to create the behaviour tree. On top of that we will be using the Spine2D characters from a video game called Arknights. The characters are a perfect size for the app we want to build and come with prepared animations. The technology used to create these animations is called Spine2D, which also has a Unity plugin to help run everything smoothly and allow the developer to call different animations from code.

With our tools and our goal in mind we can set out to create the basis of the behaviour we want. The characters we used came with a few animations loaded into the characters’ skeleton:

  • Walk
  • Relax
  • Sleep
  • Sit
  • Interact

Every character has an energy value. This energy value is here to diversify the characters' decision making. Every character has it's own energy value and how quick that energy value depletes. Binding the current consumption rate to various actions in the behaviour tree makes it so the code that the code in the Character script itself remains relatively little. All these values can be called upon by using properties.

    [Header("Energy")]
    [SerializeField] private float energyConsumptionRate = 1f;
    [SerializeField] private float energyRecoveryRate = 1f;
    [SerializeField] private float maxEnergy;

    private void Update()
    {
        Energy += _currentConsumptionRate * Time.deltaTime;
    }

These five animations and the energy value will make a good basis for our behaviour and thus we will be focussing on implementing them into the behaviour tree. To start off we start by adding a behaviour tree component to the base of the the character.

Behaviour tree with basic AI behaviour

Above you can see the completed behaviour tree for the four of the five animations. we will walk through them one at the time.
To start out, the tree starts at the "Entry" node. This node will always run first and will run when the program starts. When the behaviour tree finishes a cycle (when it returns a success or fail to the entry node) It will start again from the entry node. From there the first state we encounter is the "Relax" state. The relax state is the base state, which will be called every time the tree starts a new cycle. This state is made up of three nodes, the "Set animation", "Reset energy consumption" and the "Wait" Nodes. These three nodes cause the character to reset back to the relax state after every cycle. By doing this we do not have to reset the character after every new behaviour.
The "Set Animation" Node sets, as its name implies, the animation of the character to a specified animation by calling the specific animation component on the current character.

    [SerializeField] private SharedString _animationName;
    private SkeletonAnimation _skeletonAnimation;

    public override void OnStart()
    {
        if (_skeletonAnimation == null)
        {
            _skeletonAnimation = GetComponent<SkeletonAnimation>();
        }
        _skeletonAnimation.AnimationName = _animationName.Value;
    }

The "Reset Energy Consumption" Node will set the currentEnergyConsumption value in the base character script to zero.
And lastly the Wait function is a built-in function in the behaviour tree that allows the tree to pause for a specified amount of time. This is here to prevent characters from performing spastic actions and creates a more natural flow to the characters' behaviour.

External behaviour tree for the "Walk" behaviour

Next up is the "Walk" state. The sleep state is called using an external behaviour tree. An external behaviour tree is a tree that is not bound to an object but can be called in the tree itself. By doing this you can reuse the same behaviour on multiple places in the tree, which we will encounter later in other behaviours.
The walk behaviour is made up of four different nodes. These are the "Get target", "Set Animation", "Energy Consume" and the "Walk Behaviour" nodes. The middle two nodes, "Set Animation" and "Energy Consume" are the same as before, the only key difference being that this time it sets the characters current energy consumption to drain the energy rather than resetting it.

    public override TaskStatus OnUpdate()
    {
        _knight.CurrentEnergyConsumptionRate = -_knight.EnergyConsumptionRate;
        return TaskStatus.Success;
    }

The "Get Target" node is a node that will check if there is a current target available, if there is not it will select a random target on the screen and then send that target to the "Walk Behaviour" node.

    private float SetRandomTarget()
    {
        var targetX = Random.Range(walkRange.x, walkRange.y);
        while (Mathf.Abs(_transform.position.x - targetX) <= minWalkDistance)
        {
            targetX = Random.Range(walkRange.x, walkRange.y);
        }
        return targetX;
    }

The 'Walk Behaviour" Node will then set the character onward towards the target at the character's speed.

    private void Move()
    {
        _transform.position = _lookingRight ? new Vector3(_transform.position.x +
        _knight.Speed * Time.deltaTime, _transform.position.y, 0f) :
            new Vector3(_transform.position.x - _knight.Speed * Time.deltaTime,
            _transform.position.y, 0f);
    }
The "Sleep" External Behaviour Tree

Next up is the "Sleep" behaviour. The sleep behaviour is made up of seven different nodes. Three of these node's we have seen before, three others are new, and one is an External Behaviour Tree.
To start of with the Sleep Randomizer, this node will perform a check to see if the character in question's energy is low enough to need to sleep. If it is then it will roll some dice to see if the character will sleep, if not then the randomizer will fail, and the entry point will return a fail to the main tree.

public override TaskStatus OnUpdate()
    {
        if (_knight.EnergyPercentage < 0.3)
        {
            return TaskStatus.Failure;
        }
        return _knight.EnergyPercentage < Random.value ? TaskStatus.Success :
        TaskStatus.Failure;
    }

The "Sleep Behaviour" is the core of this tree, this script will see if there is a bed available, select a bed out of the available targets and randomize a sleep time to see how long the character will be sleeping.

    public override TaskStatus OnUpdate()
    {
        target.Value = SetTarget().InteractPosition.position.x;
        sleepTimer.Value = SetRandomSleepTimer();
        return TaskStatus.Success;
    }
    
    private Furniture SetTarget()
    {
        sharedFurniture.Value = furniture;
        sharedFurniture.Value.SetOccupant(_knight);
        return sharedFurniture.Value;
    }

    private float SetRandomSleepTimer()
    {
        return Random.Range(15f, 30f) * _knight.Laziness;
    }

When the target is set, the External behaviour tree will play, you can pass a value from the current tree into the external behaviour tree. By doing this we can bring the Target value into the "walk" behaviour which will prompt the behaviour to use that target instead of generating a new one.

After the walk tree has returned a success, the furniture interact will run, this is a small script that will set the height of the character to the corrosponding furniture.

    public override TaskStatus OnUpdate()
    {
        if (sharedFurniture.Value == null)
        {
            return TaskStatus.Failure;
        }
        var target = sharedFurniture.Value.InteractPosition;
        _transform.position = new Vector3(target.position.x, target.position.y,
        _transform.position.z);
        return TaskStatus.Success;
    }

After this the energy consumption will be set to recover energy and the sleep timer value will be passed on to the "Wait" function where the tree will pause of a certain amount of time.

Furniture interact behaviour

The last behaviour that is not regulated by user input is the "Sit" behaviour. The sit behaviour is the same as sleep with a few minor key differences.
The biggest difference between the two behaviours is the way they select furniture to sit/sleep upon. While the sleep function looks for a bed that is not occupied, the sit function will look through a list of furniture and will choose one to sit on. This can be either a bed or a chair.
Because of the way that furniture is selected, the animation setter is a bit different from what we used previously.
Now it will check which furniture type got selected, and then look through a list of animations that have a piece of furniture bound to them. When it retrieves the correct animation, it will set the animation.

    [SerializeField] private List<InteractAnimation> animations;
    [SerializeField] private SharedFurniture _sharedFurniture;
    private SkeletonAnimation _skeletonAnimation;

    public override void OnStart()
    {
        var selectedAnimation = animations.Where(x => x.FurnitureType == 
        _sharedFurniture.Value.furnitureType).OrderBy(x => 
        Random.value).FirstOrDefault();
        _skeletonAnimation.AnimationName = selectedAnimation.AnimationName.Value;
    }

Besides the new animation setter, the other key difference is the Energy Consumption method.
This method has been changed slightly to check if there is a given furniture, if there is it will adapt the current recovery rate to the given furniture's recovery modifier, if not then it will simply reset the current recovery rate back to zero.

    [SerializeField] private SharedFurniture sharedFurniture;
    private Knight _knight;
    
    public override TaskStatus OnUpdate()
    {
        _knight.CurrentEnergyConsumptionRate = sharedFurniture.Value == null ? 0
        : _knight.EnergyRecoveryRate * sharedFurniture.Value.recoveryModifier;
        return TaskStatus.Success;
    }


Below you can see the current behaviour tree in runtime. This is the completed tree but just as an example of what it would look like in runtime.

The current behaviour tree in runtime

Chapter 4: Adding user input

With all these behaviours implemented, the units will now simply walk across the screen and go about their day, without any user input. But of course we don't have the ability to place furniture on our own yet and we are still missing the "Interact" behaviour.

Starting off with the "Interact Behaviour", the interact behaviour is a bit different from the other behaviour states for two main reasons: There is user input, and it needs to interrupt other functions. Because the interact behaviour needs to interrupt other functions, this function is made up of two different blocks. One block performs the interruption while the other block actually performs the behaviour itself.
The former of these, the Interrupt block, is made up of four different nodes we haven't seen before: these are the "Interrupt check", "Interrupt", "Perform Interruption" and the "Parallel Selector".
The Parallel selector is similar to the normal selector but as its name implies it executes its child functions at the same time until one of them returns a success or all of them fail. This node is needed because in the case that there are no interruptions, the tree will stop checking the interrupt check and start a new cycle.
The Interrupt checks for interruptions by checking a Boolean that has been assigned to the interact function. It returns a "Running" status if there have been no interruptions. As soon as there is an interruption it will return a success and move on to the next node which will execute the interruption.

    public override TaskStatus OnUpdate()
    {
        return interrupted.Value || isDisabled.Value ? TaskStatus.Success : 
        TaskStatus.Running;
    }

The "Perform Interruption" and the "Interrupt" nodes work together to perform the interruption. when the perform interruption is called it will return a true to the interrupt node which will then cut off all child functions and return a "Failure" to its parent node. In this case the parent node, which is a selector, will then continue with the interact segment of the tree.
The interact segment is made up of two new nodes, these are the "Interact check" and "Interact behaviour". The interact end and the wait node we have seen before, the interact end will reset the character's position back to the ground level and clear up any furniture they were using, and the wait waits for the duration of the animation before returning a success to its parent and thus restarting the tree.
The interact check will perform a check to see if the interruption was caused by an interaction, this is important for later when we will add a UI function to turn characters on and off. If the interruption was not caused by an interaction, it will return a failure and the tree will move on to the next segment.

    [SerializeField] private SharedBool isInteracting;

    public override TaskStatus OnUpdate()
    {
        return isInteracting.Value ? TaskStatus.Success : TaskStatus.Failure;
    }

The interact behaviour will then set the character's animation to interact and pass the duration of that animation to the wait function.

    public override void OnStart()
    {
        _skeletonAnimation.AnimationName = _animationName.Value;
        // get the length of the interact animation
        _animationDuration.Value = (int)_skeletonAnimation.AnimationState.
        GetCurrent(0).Animation.Duration;
    }

Now with the interact behaviour in place and working the last thing we need to do is add some user UI.
The two main functions we add to the UI is the ability to add furniture and the ability to turn characters on and off to ones own desire. The furniture spawns in a prefab that follows the mouse until the user lets go, this is when the furniture is added to the list of possible furniture from which the characters will then choose.

The ability to add or remove characters from the screen on the other hand requires some input in the behaviour tree since we wasn't content with simply turning them off. The idea was to make characters stop whatever they were doing and walk offscreen whenever you turn them off and the opposite whenever you would turn them on.

Walk off-screen behaviour

The walk off-screen behaviour runs with the same interrupt as the interact function, only this time it will skip over interact and go to the new branch. The walk off-screen branch is made up of two main functions: The walk off-screen and the function to cancel that behaviour. The cancel function is set up the same way as the interrupt check from before. The Walk off-screen behaviour however has two new nodes: Walk Off Screen Behaviour and Disable Character. The former checks the nearest offscreen point to the character and sets that as a destination for the walk function which is mostly like the other destination setters.
The disabled character function calls a Dictionary that contains all the existing characters and turns the corresponding character off.

    public static CharacterManager Instance { get; set; }
    private readonly Dictionary<GameObject, bool> characters = new();

    public void ToggleCharacter(GameObject knight)
    {
        Debug.Log(knight.activeSelf);
        var knightScript = knight.GetComponent<Knight>();
        if (!characters[knight])
        {
            EnableCharacter(knight);
            knightScript._behaviorTree.SetVariableValue("Disabled",
            value: false);
            knightScript._behaviorTree.SetVariableValue("Destination",
            value: 0f);
            characters[knight] = true;
        }
        else
        {
            knightScript._behaviorTree.SetVariableValue("Disabled", value: true);
            characters[knight] = false;
        }
    

When the character finishes walking off screen they will be turned off and thus the tree will stop functioning until they are turned on again.
When the character is turned on again, the tree will restart and send the character on screen and the cycles will start again.

Conclusion

Today we delved deeper into what a video game AI is and how to make one. The application we have right now is just a simple Idle desktop app that you can leave running with minimal input but in its simplicity it shines. Because of the minimal input, the characters really have a bit of their own personality. It is still basic but from here on out you can create more complex decision making or simply watch on as they go about their lives while writing an article on basic AI and behaviour trees.

Sources

Education, I. C. (2022, July 7). Artificial Intelligence (AI). https://www.ibm.com/cloud/learn/what-is-artificial-intelligence

Simpson, C. (2014, July 18). Behavior trees for AI: How they work. Game Developer. https://www.gamedeveloper.com/programming/behavior-trees-for-ai-how-they-work

Arknights. (n.d.). Retrieved November 6, 2022, from https://www.arknights.global/

Behavior Tree Designer. (2018, October 9). Opsive. https://opsive.com/support/documentation/behavior-designer/overview/

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts