3
\$\begingroup\$

I want to create a an ai with finely customizable character. The ai should be able to handle generic behaviors, like scheduled activity, shared across all character, but have specific override for group of character called persona (for example artist for art club activity), and override for unique character behavior (named characters). There is hundreds of characters simulated at the same time.

I'm using an "if tree" with behavior tree pattern, (grew organically from prototype). I originally wanted to refactor into a bunch of "cards", as an abstraction. Cards have a check, then a body of actions when the check is passed, cards can be nested as actions of other card, break() is a specific action that terminate the execution of the whole tree. But I'm stumbling on some issue refactoring...

Override for specific characters, could happen at any branch of the "if tree", and it makes it harder to write code that separate specific and unique code, because a top level card can have specifics to a character, but lower behavior be a mix of generic and specific, making it hard to untangle the two. It also makes it hard to propagate change during dev, because number of behavior card can reach up to tens of thousands. That makes it that a lot of card would be identical variants save for a few actions, and updating them all would be a nightmare (for example, gameplay adjustement). On top of that, breaking the ai code into multiple files makes it harder to track the logic and debug specific vs generic behavior, especially when part the tree becomes deep (for example, there is an investigation routine, broken into persona and specific character).

I haven't even started to touch on how to do character interactions, from specific to generic and vice versa, and specific to other specific.

Right now I have just a single file with a big pile of if, where I intercept the logic with hardcoded exception (if characterID or personaID is ... then execute body), and also manage group interactions ad hoc.

What architecture or programming design pattern you would recommend to cope with these difficulties? Specifically how to manage the explosion of card variants? So far anything I tried collapsed.

\$\endgroup\$
7
  • \$\begingroup\$ Can you perhaps elaborate a bit more on what exactly a "card" is in your architecture? That's not a term I have heard being used before in the context of game AI. \$\endgroup\$
    – Philipp
    Commented Feb 14, 2023 at 11:16
  • \$\begingroup\$ It's explained: "Cards have a check, then a body of actions when the check is passed, cards can be nested as actions of other card." It's just an "if block" turned into object class. Has a list of conditions, and a list of actions which is only run if the conditions return true. Cards are a type of action (which are "functions" turned into class objects) therefore can be nested in body of other cards. It's a modular implementation of an if tree. Turns out it's REALLY just that and don't solve anything architecturally or practically (for solo dev). Folding blocks is as efficient, if not more. \$\endgroup\$
    – user29244
    Commented Feb 14, 2023 at 12:06
  • 1
    \$\begingroup\$ I am curious about "if tree" with behavior tree pattern. Can you please add some sample code for others to understand better? \$\endgroup\$
    – Mangata
    Commented Feb 14, 2023 at 12:22
  • \$\begingroup\$ A behavior tree is really just a fancy if tree, which implementation is useful when you want to make visual tools. A priority pattern is basically a list of if that return if the check succeed-> if (true){ action(); return;} that is the first to execute exit the function, a sequence is a list of if that return on else if (true){action();} else {return;} that is it will continue until one fails then exit the function. \$\endgroup\$
    – user29244
    Commented Feb 14, 2023 at 12:33
  • \$\begingroup\$ @user29244 every decision making architecture is in the end "really just a fancy if-tree". And just like any other decision making architecture, behavior trees are a set of tools to abstract common patterns in if-trees in more readable ways. Just because selectors and sequences can be rewritten as functions with if-else trees doesn't mean they should. \$\endgroup\$
    – Philipp
    Commented Feb 14, 2023 at 15:48

1 Answer 1

2
\$\begingroup\$

First of all, game AI with lots of actors with partially shared and partially unique behaviors is never a simple problem to solve. Especially considering that it's probably an area of your game you want to do a lot of experimentation and iteration on, which means it needs to be very flexible and easy to maintain.

There is no one architectural pattern that is going to solve all your problems. Most projects mix several patterns. Some patterns you might want to look into are:

Object-oriented programming with inheritance.

When you have an AI actor that should behave mostly like the default actor, but behaves differently in certain situations, then it can make sense to handle that using polymorphy.

First, create a class "AIBehaviorBase" that implements a "default" AI actor. Design its public interface so that it properly encapsulates the whole AI decision making process.

When you want to create another AI actor that acts differently, then:

  1. Create it as a new class inheriting from "AIBehaviorBase".
  2. Identify the code in "AIBehaviorBase" which you need to act differently in the new class.
  3. Refactor "AIBehaviorBase" to move that code into methods (in some cases, those methods might not actually do anything at all - they basically act as "injection points" for additional logic).
  4. Override those methods in the new class. Make sure you properly follow the Liskov Substitution Principle while you do that. Any code that uses your base class must still work when it uses your inheriting class instead.
  5. Use an instance of that new class as the behavior for that actor.

State-machines

After the "humongous-if-else-tree" pattern, state machines are the next basic architecture for game AI. The basic idea is that each actor can be in one of several states, like for example "moving somewhere", "in combat", "making conversation" etc. While the actor is in a certain state, only the code for that state gets executed. Several events can lead to the actor changing into a different state. Those state transitions might also have logic.

Sometimes states might have state machines on their own. For example, in a hack&slash combat system, the "in combat" state might be implemented by a sub state-machine with states like "dodging", "parrying", "swinging" etc.. This architectural pattern is called "hierarchical state machines".

or some actors, standard states might be replaced by custom states. For example, different actors might have different combat strategies, which could be represented by different "in combat" states, which do however recycle some of the sub-states from the default AI for their internal implementation. So when you organize your AI as a tree of hierarchical state-machines, then AI customization could be implemented by overriding certain states or sub-states with alternative implementations.

Behavior Trees

These are one of my favorite patterns in game AI, because they allow you to define AI decision logic using very concise code (or even a UI) while you hide the nasty details of the implementation in abstract nodes. This is also really useful for iterative approaches.

As an introduction to this topic I recommend this article by Chris Simpson on GameDeveloper.com which helped me a lot to understand the pattern and build my own behavior tree framework which I have recycled for multiple projects. You might notice some similarities with the data-driven concept of your "modular implementation of an if tree", but behavior trees are far more powerful.

When you have lots of actors with different behaviors, then you would usually not try to represent all this in one super behavior tree with selectors or sequences that check for actor type and make decisions accordingly. That would soon turn into madness. No, what you usually do is create a separate behavior tree for each actor.

The reusability of behavior trees comes from the neat ability that they can be nested. So you can easily put a generic behavior tree as a sub-tree into multiple other trees. You can also replace individual branches or leaves of trees by creating a copy of a tree, modifying it, and then inserting it into a larger tree. But that usually gets a lot uglier and results in far less readable code.

The conclusion: Behavior Trees are great for customizing high-level behavior, but not so much for customizing low-level behavior.

Utility AI

I am not sure how well this fits into your particular design, but I would still like to mention it nevertheless as an alternative to the binary do-or-not-do principle of the aforementioned architectures.

The idea of a utility AI is:

  • You have several variables that feed into the decision making process. These can be variables about the state of the actor itself or about the world around them.
  • Each AI actor has a set of actions the actor can perform. "Do nothing" is usually such an action as well.
  • When an actor is about to make a decision which action to perform, then it uses a rating function for each action. That rating function uses those variables and returns a score representing the "utility" of that action, or in other words how useful it would consider this action right now.
  • Then it performs the action that got the highest "utility" score.

The "personality" of an AI actor is determined by the rating functions it uses. As an example, let's take a rating function for the "attack" action. Should or shouldn't the AI actor engage a certain enemy? A very bold AI actor looking for a real challenge might want to prove itself by picking only the strongest fights while it doesn't concern itself with "small fish". So it would use a rating function where the enemy strength is a positive factor resulting in a higher utility for attacking. A more careful actor might choose its battles more carefully and only engage when it knows it can win, so enemy strength would be a negative factor in its attack utility rating function.

\$\endgroup\$
2
  • \$\begingroup\$ I was looking for more specific advice from people who were in the same situation. I didn't explain the architecture, my problem is highly localized to the behavior variants problem. The architecture is: a world manager that manage entities, a utility tree for perception (match concepts instead of actions) that query the world and internal state, a BT that sets "state variables", and a state machine that actually implement actions based on those vars. Only missing is inheritance, I couldn't figure out how to add it without exploding the maintenance cost and the solve the exposed problem. \$\endgroup\$
    – user29244
    Commented Feb 14, 2023 at 22:18
  • \$\begingroup\$ I close relative to the game is Yandere Simulator, to look for an example. I lifted the hierarchy (generic -> persona -> unique characters) from it. It allows a mix of simulation and scripting (ie story, unique situation, unique interaction and behavior). BT is great because it's an abstraction between a planner, a script and a state machine, but I separated the state machine to handle actions, it's simpler to handle temporal aspects (wait the action to finish) and make transitions more explicit. Same for separating UT and BT, it's convenience to follow the character logic better. \$\endgroup\$
    – user29244
    Commented Feb 14, 2023 at 22:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .