More Rambling About Enemy Behavior

This has been living in the back of my head for weeks and I haven't had the bandwidth to devote to solutions but here's a #longpost about the specifics and some other thoughts.


First off, how do targets and actions work?

Main Article: The Big Complicated Chronicles Actions System
An ability executes a list of Action Tokens in order which compiled from an asset[1]. One type of token can be a Target Request which is a ton of configuration options for what is selectable in that request; range, direction, line of sight, that sort of thing. The two main genres of target request are defined by their decision-type, which is either “Tile-Pick” (ie. pick a tile within 4 tiles of the user) or “Directable” (ie. N, S, E, W)

  1. I say “asset” here because it's not super relevant, you could also call it an action script, but for 99% of cases I'm dealing with now the action script is 1:1 equivalent to the compiled token set.

Other tokens like Damage or Push will then reference the results of a previously-declared target request token.

Why does this make behavior hard?

Calculating AI for “Move/Attack” is trivial because it's a single directional target request and you just plug in the direction of the player, but that's hard-coded. Something like “Blink Strike” may have a token list more like this:

  1. Declare target tar which is any occupied tile within 10 tiles of the user.
  2. Declare target dir which is a range-1 directable empty location directed from the result of tar
  3. Play “blink” animation from origin to dir
  4. Teleport actor at origin to dir
  5. Play “attack” animation from dir to tar
  6. Damage actor at tar

You can read this action set and get an idea of what the ability does but ask yourself, how do you programmatically reason with this ability's function? The token system is granular and abstract enough that extremely complicated multi-stage dependent abilities can be made with it, which is an important part of the variety I want in the abilities in the game!

How does the NPC behavior decision step determine which tile position to plug into tar and what direction to plug into dir??? Why did it even pick Blink Strike in the first place? How did it know it could possibly be in range to use that? How do you even reason that one target will move you but another will damage a target? Maybe you can start to picture some high level solutions here where you need to break down the state of the board to determine what is available but guess what? You cannot know what tiles are available to pick for dir until you simulate the entire turn up until that point! Decisions are made based on the expected board state at the time of the actor acting, and then saved until all decisions are made.

A 3-step targeted ability consisting of a tile-pick within 10 tiles away, followed by a dependant direction, followed by 5-away tile pick gets you to 50 thousand possible decisions, and it explodes from there if you want to do multi turn solving. If each combination requires you to copy the game state to simulate the outcome, well you're sunk!

How do you select what ability to even use?

There's a very large spectrum of how the enemy behavior will ultimately work. The dream was to be able to define “Personality” assets where the actor will have a set of goals and will be able to reason with the abilities they have and their resources and cooldowns to determine correct usages of those abilities to accomplish the goals of themselves or their faction. I still think this is attainable!

There's also a lower-tech approach, which is very simple rules-based priorities. A behavior can be a list of states in priority order. Each state would contain:

  1. A list of conditions to satisfy that are easy to calculate: “further than X tiles away from enemy”, “not in line-of-sight from enemy”, “there's a nearby empty choke-point”, etc.
  2. An explicit ability to use: “move/attack”, “blink strike”
  3. A list of desired outcomes that are easy to calculate: “Target attacked” “User moved closer to target” “User is covering choke point”

To determine the decision on a turn, iterate over the list until the conditions are met, attempt to satisfying the outcomes by solving the possible target combinations of the ability, being unable to satisfy the outcomes counts as a failed condition, move on to next state.

I think the important thing to note about both approaches, one being dynamic ability selection from a personality solver, the other being an explicit rules-based approach, is that both still require a “Fill in the target requests from this ability programmatically” and ultimately that is the actually hard part!

So, regardless of how we got to the answer of “this is the ability we want to use” we still have to optimize the target request decision-making.

Making an ability solvable

Rather than worrying about how to solve all the possible combinations of target decisions, we can just do what A* does, which is adding a heuristic.

If we think back to solving a serious of target requests, the difficult cases are always with the tile-pick choices. On a graph of possible solutions to traverse, a directional choice has up to 4 neighbor nodes, but a tile pick could be any tile within a range and can scale up very quickly. Picking a tile within 5 tiles of the user has 60 neighbors!

Similar to pathfinding on a 2D grid, if all target requests were directional there would be far less concern about combinatorial explosion. And even if my computing power were infinite I would still have a problem where I can tile-pick until a range condition is satisfied, but I can't solve for the best tile-pick (such as closest or furthest).

One solution to this could be actually as simple as tagging the target requests in the ability asset! I could very easily tag a tile-pick target request token with “Prefer closest to/furthest from enemy/ally/choke-point/wall” and tag directional requests with “away from/toward” similarly. This way I could encode the intent of a target request into the ability definition.

Onto the solver, for tile pick requests, I can now sort the potential tiles by how close they are to the tagged reference. I can even choose the closest tile as the new root and only allow up to 4 neighbors from that root. Essentially, by declaring the intent of the tile-pick, I can tightly-constrain the considered neighbors and get it closer to the requirement of directional requests.

In Conclusion

At some point I need to sit down and actually get coding! But I feel a little less lost with this outline and some of my chief concerns are addressed. It's worth a shot now :host-nervous:

Thanks for reading I hope this was interesting! Feedback and criticism are all welcome! :host-love:

#gamedev #chron4 #longpost

| 🌐 | 🙋‍ | @britown@blog.brianna.town