I’m working on a really rudimentary AI using a state machine, and the basic framework is really straightforward but I’m having a little trouble working out how I should handle complex behavior. Take, as a trivial example, “Attack enemy A”. That behavior requires two sequential actions to complete successfully (traverse until A is in range, then attack him), but I don’t want a giant conditional tree that executes every time the AI updates.
Thus far, my best idea to solve this has been to make some class BaseState with a virtual Execute method, and make a child for each unique behavior type whose Execute override encapsulates that particular behavior’s complete sequence of activity, such that Attack’s execute might pseudocode:
if (!InRange(enemy)){
MoveTowards(enemy)
} else{
Attack(enemy);
}
So is this a reasonable way to approach the problem? I can’t see anything obviously wrong with doing this, but it feels a little brute-forcey to lump all of the logic in one giant Execute function for each action.
The design with the “Execute” method and subclassing makes sense. I wouldn’t split up the chase and attack states unless there’s a very good reason for it. Instead, the InRange, MoveTowards and Attack calls should probably be handled by a different component.
Having a different attack and chase state often makes agents behave erratically if the player or other targets moves in and out of their attack range often.
You can think of an AI state machine pretty much like a MonoBehaviour. You’ll want an Execute (or Update) method in each state that’s updated regularly. You’ll want each state to be able to react to events (like the AI getting damaged, or the number of enemies in range changing, etc.). Finally, you’ll probably want a setup method where you can cache data you’ll need in the state. So if you want the AI to only chase the player a certain distance, you’ll need to cache the AI’s starting position when it enters the chase state.
Thank you for clarifying, this is superbly helpful!
One thing I wanted to confirm- when you suggest placing InRange, MoveTowards, and Attack calls in a different component, do you mean that those three in particular should be handled as unique cases in anticipation of all the weird edge cases that often arise? I’m picturing doing something like making each of them a method on the base NPC class, then invoking them from whatever BaseState children need the functionality.
What I’m thinking is that you’ll want a Movement component base class that handles moving places and a Weapon component base class that handles checking range and attacking.
This will allow you to use the same AI for a flying NPC with a ranged weapon and a grounded NPC with a melee weapon.
Putting that in the base NPC class will bloat that class, and also end you up with NPCs that cannot Attack still having an Attack method.