In our first blog, we mentioned that we started with AI using decision tree. It was helpful, since we quickly made a basic enemy which had a couple of basic actions, it was overall interactable, and more importantly, it allowed us to initiate testing. However, each new action that we wanted it to perform hamstrung the AI, and even the smallest tweaks took a lot of work.
That’s when we discovered Utility AI. Like decision tree, it’s very simple to understand, but it provides a faster environment to modify the AI, which is a great plus for any iterative design.
The idea of utility decision making is that we measure all possible actions with a score, and the highest scoring action is chosen to be performed. For example, let’s say we have an AI that’s hungry, and two fast food places. First one sells burgers of 200 grams, and second one sells burgers of 300 grams. Our AI should put higher value on the second store, and lower value on the first one. However, our decision tree algorithm should be very efficient with that too: We should only have one condition that compares the weight of the burgers sold, and choose the appropriate action. Now, if we say that the places have different prices, and the burgers have different calorie values, fat values, sugar values… It gets more and more complex and our decision tree is bloating, getting harder to engage and modify. Utility is made for these cases, since it calculates total value of the action, given the parameters we entered, and chooses the highest scoring action.
The decision system consists of several components: scorers, qualiscorers and qualifiers.
Scorer gets an item to process, passes it through a function we’ve given it and calculates a score for that item. We should note that these scores should be uniform, and that’s why we clamp them to be between 0 and 1. Scorer’s function determines how scorer will calculate its value. So for the given example, we would draw the scorer’s functions so that we value low price, high weight and medium calories the most.
Qualiscorer gets its input from scorers and other qualiscorers, applies factors to gathered scores and, using a method of summation we’ve set, outputs a score. Method of summation could be as simple as just summing all scores and dividing by their count, or we could add more factors in, such as threshold, so that we output a score only if our score passes a threshold we’ve set. Each qualiscorer’s input has factor (or weight), so when calculating sum, qualiscorer applies that factor to that input’s value. In our example, we’ve decided that our weight of the burger is important (we give it factor of 0.78), price is less important (0.43) and calories are of very low importance (0.12)
Qualifier works the same as qualiscorer, except it also provides an action to be performed if chosen. Decision system runs by getting all qualifiers in the system, determining their scores, and choosing the highest-scoring one’s action.
Utility’s power to change and modify can be seen in the example above. When we made the first AI (one that only sees burgers by their weight), we had a diagram shown in the image above. When we started development of our modified AI, we wouldn’t need to scrap all our work or modify the system to fit our needs. We should only put in a few scorers that process whatever values we want, attach them to the same qualifiers and we’re good to go (image below). Utility takes care that the values are summed up and outputs us a score at the end. Of course, we should go through it a few times since we won’t pick good values instantly, but we’re allowed to quickly change values and try it out, so the design-and-test aspect is much, much faster.
You can find more about Utility here.
In the next Machine Spirit entry, we’ll cover AI’s flow and as an example we’ll use enemy AI we are creating for Hatchet.
If you have any questions, suggestions or just feel like saying hi, please write me at firstname.lastname@example.org