Using Behavior Trees to Create Retro Boss AI
In this post, I am going to talk about how our bosses determine which attack they will use, how I got to our current method, and why I choose this method. A little background on FireFight. FireFight is a top-down / pokemon-isometric 2D action shooter where you have to defeat waves of enemies. You also have a companion that helps you out by barking ( which acts like radar ), providing a shield to protect you in tough situations, and helps attack other enemies. Each level has unique enemies and a boss. The overall feel for the boss behavior was inspired by old-school / retro action games and retro Legend of Zelda games. Here is the outline of what we wanted: attacks happen in a predictable frequency, each attack has a very specific criteria for when it happens (such as low health or distance from player), there is some randomness if two attacks meet their criteria when determining to attack.
All of the AI in the game uses Behavior Trees (recommended but not required reading:
http://www.gamasutra.com/blogs/ChrisSimpson/20140717/221339/Behavior_trees_for_AI_How_they_work.php). Behavior trees are very nice because they are visually easy to follow and construct and are incredible flexible. Depending on the complexity of behavior however, they can become overwhelming and big.
When designed the AI for the first boss, my teammate and I talked about what attacks the boss would do and when they would happen. We settles on 3 main attacks, and one of them has a weaker variant. This was the basis for its behavior tree, which look like this:
It doesn’t seem too bad. The left half of the tree is for determining what type of attack to use and the right half is for moving around the map. The attack branch is also rooted with a timer decorator node so that successful attacks are spaced out. This worked fairly well. The main downside was that if I wanted to tweak the criteria for a particular attack, I would have to change the structure of the subtree managing that attack. On top of this, if I wanted to test one attack, there was no easy way to prevent the other attacks from happening aside from putting myself in the exact conditions to trigger the attack ( which isn’t always easy ). With all that said, it did work out well.
The issue came when I went to go implement the next boss’ AI. Although following a similar AI style ( spaced out attacks and attacks based on conditions ), the number and type of attacks were different, thus the right half of the tree could be copied, but the left half would need to be completely different. I felt like this was going to lead to more work than necessary considering the same style was going to be retained. Not only did I need to write a new attack branch, but also debugging the logic once the initial branch is done is not something that is easy to do ( remember testing one specific attack isn’t easy without changing the tree ).
Without an obvious solution, I started to write down a chart for the distance conditions for each attack in the hopes that something would spark. Here is the chart:
To complicate things, Heal had some other conditions that weren’t distance based.
One thing that I thought about was an implementation that would evaluate the criteria for each attack, keep ones that met their criteria, and randomly choose between those. This would not only keep the randomness, but I could also modify attack criteria so that only one attack was ever possible to be chosen. This would help debugging each attack easy as I could do it in isolation ( so to speak ). The downside here is that I would need to check all criteria every time I want to attack ( which could be every frame if the attack timer is up and the boss hasn’t chosen an attack ) and I would need to store attack choices that were possible ( not a big deal but I didn’t end up having to worry about this ).
This implementation ended up being very simple with a ‘Random Selector’ node in the behavior tree. This node would shuffle the possible attacks, and for each one check if its criteria were met and if so choose that one. Because of this system, I never need to keep a list of all possible attacks and I only check attack criteria until one attack’s criteria is met. In the worst case, no criteria is met so all are executed ( but still better than executed all of them every frame as stated in the previous paragraph ). Here is a diagram of the attack-selection part of the behavior tree with the new system:
It is a lot simpler, flexible, and readable. The only added complexity that human interpreters of this tree need to know is that each leaf in the “Choose Action” branch has the following form:
if attack doesn’t meet conditions
I did this purely to keep the tree simple as trying to implement this purely on behavior trees would look like this in place of each leaf:
Not only does this make debugging / testing specific attacks easy, it easily holds up to adding more attacks. The overall structure is also not boss specific which means I can ( and will ) use it for additional bosses. I still have to determine when and how the attacks should be performed, but I was going to have to do that anyways.
Although there is a lot of flexiblity with Behavior Trees, I’m sure this isn’t the only solution others have come up with. I would love to hear if and how others approach boss AI design.（source：gamecareerguide）