#3 Enemy AI

godot devlog hammer-time

Minimum Viable Enemy

Before I continue adding more stuff to the player character or exploring other game mechanics, I wanted to start with another key feature of the game, the enemies characters.

It is vital to include a simple enemy this early into the development of the project, as its implementation is going to influence how other parts of the game are designed later on. In the case of this prototype, a simple enemy should:

  • Receive player attacks
  • Wander around a specific area
  • Spot the player if it walks in front of it
  • Chase and attack the player
  • Flee from the player

Choosing Behaviour Trees

After refactoring the player character controller using a finite state machine the first thought that crossed my mind was to use another one to implement the enemy AI, as that would be easy to do.

To have a clear perspective of the requirements, I draw a graph with the different states the enemy could do and then I started drawing lines representing the transitions from state to state.

A graph depicting the different states this ememy would have and some of the transitions between them.

I wasn’t finished drawing all possible lines when I noticed the number of transitions was growing exponentially. If this was supposed to be a “simple” enemy, I didn’t want to imagine a complex one. Clearly, I had bumped into one of the limitations of FSMs.

After doing some research on design structures for enemy AI, I decided to go for Behaviour Trees. I wanted to focus entirely on the design of the enemy, and not on writing my own implementation of BTs, so I decided to use the LimboAI asset.

My first impression using this asset was revealing: All my previous attempts writing my own BT tools were not good enough. I highly recommend this addon. It is feature rich, well documented and easy to use.

Designing Behaviours

It took me a while to get a grasp of the workflow, as my head was stuck in the “state-transition” mentality from FSMs. However, once it clicked, everything went smooth and I was able to implement all the behaviours I listed at the beginning of this devlog entry and more.

A screenshot of the Godot editor showing a section of the Behaviour Tree I designed for the enemy.
A screenshot of the Godot editor showing a section of the Behaviour Tree I designed for the enemy.

Before I show you some of the cool additions I included, let me first write a quick overview of how everything is structured inside the enemy scene; Next to the character model, its CollisionShape3D and AnimationPlayer nodes, there are now two other elements: A node called BTPlayer where the tree is defined including all actions and conditions, and an empty node which parents what I refer to as “Shared Components”.

These shared components are individual bits of functionality that might be used by one or more tasks of the tree. For example, one shared component is NavigationMovement and it moves and rotates the character. Then, some of BT actions I wrote like NavigateTowards, Chase, Flee and WaitForNavigationFinished simply access this component to achieve their purpose.

These components can also influence how the tree is travelled by writing data into its Blackboard. For example, when the player attacks the enemy, AttacksReceiver toggles a flag on it which causes the BT to travel to the branch “Flee from player character”.

Showcase

Now that I covered how everything is put together behind the scenes, I can finally describe how this simple enemy is supposed to act during gameplay:

  • By default, this enemy will move inside a circular area. If it spots the player character, it will give chase.
    • If the enemy can’t catch the player, it will give up the chase after a couple of seconds and will return to its patrolling area.
    • If the enemy catches the player, it will start attacking at them.
  • If the player character attacks the enemy with a light attack, it will temporarely turn into a ragdoll and then flee the moment it recovers from the attack.
  • If the player character attacks the enemy with a heavy attack, it gets defeated.
PeerTube Hammer-time devlog #3 - Enemy AI

The above video shows the game running in the Godot editor, with visible collision shapes and the BT debug menu enabled. This way, it is easier to understand and visualise the AI of the enemy in action.

Ragdolls

About that ragdoll feature, the way it works is very interesting. I built this feature by using the delegation design pattern. Because this enemy receives player attack signals through the InteractableHurtbox node I covered in the entry #1 of this devlog, it can simply forward these signals to other interactable implementations.

A screenshot of the Godot editor showing the hierarchy of the simple enemy.
A screenshot of the Godot editor showing the hierarchy of the simple enemy.

In this case, a LaunchablePhysicsInteractable instance which I placed inside the enemy itself. This interactable implementation already handles physcis collisions, so it simply needs to be forwarded some of the attack signals from the AttacksReceiver node.

Whenever an attack is received, the enemy disables its CollisionShape3D and lets the interactable inside it work for a couple of seconds. The enemy simply has to copy its Transform3D during this time. Another benefit of this implementation is that it allows players to launch enemies towards other enemies to chain attacks and cause more damage.

Exploring Enemy Ideas

Developing this simple enemy has been a lot of fun. Nonetheless, it is nothing but a proof of concept of what could be built using this workflow. While I was working on it, I actually came up with some cool ideas for “real” enemies I would love to include in the game if I had the resources for it. These are a few of them:


Previous#2 Aim Assist