5
\$\begingroup\$

Imagine a game with different types of characters or "actors" which can perform actions like walking and shooting. They are implemented using the state pattern to reduce the amount of coupling: each actor has a current state which can be one of the different possible state objects like DefaultState, ReloadingState or StunnedState which determine what happens when its methods like Reload(), Shoot() or Walk(direction) are called. Every frame the current state object updates the actor and returns the next state.

This works great for the player who doesn't need to know anything about the internals. However, an AI controller needs to know its character's current state to make intelligent decisions. For example, in the DefaultState the AI character would walk towards the nearest enemy while in StunnedState it would flee. It seems like every state should have its own AI routine, but how should this be implemented while trying to keep this logic separate from the actor class? The AI could query the actor for its current state and decide its actions based on that, but that would result in a bloated and messy if-else block. The State class shouldn't know anything about the AI so it cannot be in charge of changing the AI's state either. How can I implement an AI which chooses different actions based on the actor's current state while maintaining loose coupling?

\$\endgroup\$
  • 1
    \$\begingroup\$ Design patterns are often overused. I would suggest simply implementing a coherent and working AI, breaking it down to relatively short meaningful functions. Over-designing is a pitfall for indie developers. \$\endgroup\$ – wolfdawn Dec 28 '14 at 21:46
  • \$\begingroup\$ @zehelvion I'm not sure how complicated the games you refer to are, but in my case it was the opposite. Indie development I've been though failed at separate occasions precisely because there was a rush to an MVP, not considering requirement discovery, hence making the code simply untouchable. Investing 2-3 months to have a sane code design goes a long way. If you're an indie developer, be prepared to invest years in the business to achieve something worthwhile. \$\endgroup\$ – Mazyod Jul 5 '15 at 19:40
  • \$\begingroup\$ @Mazyod And Design Patterns === sane code? I mean, there was never an instance in your life where Design Patterns complicated code needlessly? Or where code that didn't include any specific design patterns, was readable and maintainable because (quoting myself), it was broken down into short and meaningful functions? Rushing to an MVP is not contradictory to making poor use of design patterns. The problem you describe is orthogonal to the number of design patterns the programmer used. \$\endgroup\$ – wolfdawn Jul 8 '15 at 15:33
  • \$\begingroup\$ @Mazyod For instance, it's not clear how the suggested pattern can be extended to promote cooperation between mobs (pack strategy). It sounds like the main goal was code prettification. Good use of design patterns is supposed to promote flexibility to the extent needed and in the places it's needed. \$\endgroup\$ – wolfdawn Jul 8 '15 at 16:02
  • 1
    \$\begingroup\$ @Mazyod My responsibility was the internal testing framework two jobs ago. It just seems to me, code beautification is a fallacy, especially for a small team on a budget; it is a form of mental procrastination. What's important is keeping functions short and meaningful while preventing code duplication and leaving room from flexibility when it is needed. Testing and bug hunting / tracking is actually superiorly important, right up there with actually making a fun game (cause a fun game with ugly nasty bugs isn't fun). My point is that design pattern appreciating is a bad habit, they're a tool. \$\endgroup\$ – wolfdawn Jul 10 '15 at 17:31
7
\$\begingroup\$

A design pattern I've enjoyed using has two types of things: task management and task execution. Task management asks the question "What should I be doing right now?" And task execution asks, "How do I complete the current task?"

Task Managers

The job of the task manager is to constantly look at the AI's internal state (read-only) to determine whether new tasks should be assigned. Tasks have priorities, and are placed on a Priority Queue when assigned. Tasks are also unique, so that an agent can't be assigned exactly the same task twice.

For instance, the Task Manager could constantly be checking to see if the agent has sensed any new enemies that must be attacked. If enemies are sensed, it adds high-priority DestroyThreat tasks to the task queue.

When the agent is done with the current task, it pops another one off the task queue. Task managers can also pre-empt the current task by putting it back on the queue if an extremely high priority task overrides the current task.

Task Executors

Your agent will have several actions which are merely functions, bits of behavior tree, or states in a state machine. Every action can either fail, or succeed. These are implemented inside the AI agent itself. They should be atomic (like "play animation" or "attack entity").

The task executor will simply tell the AI agent what actions to take given a current task. It does not need to directly know about the agent's state, but only the return values of the actions the agent takes. The executor can either fail the task, or succeed, based on whether the current action fails or succeeds. If the task fails, it is either put back on the task queue or is destroyed, depending on the desired behavior.

\$\endgroup\$
  • \$\begingroup\$ I exposed fields like movementSpeed and canAttack for the Actor, these will be modified by its current state. The fields are used by the TaskManager do decide which tasks to add, e.g. fleeing if unable to shoot and slower than enemies, as caused by StunnedState. Thanks for the answer. \$\endgroup\$ – Dan Dec 30 '14 at 10:31
2
\$\begingroup\$

Depending on the language you're using use a virtual function, a function pointer, or a delegate for your actor's "do something" function (some engines call it Process() or Update() )

in C++

class BaseAI {
protected:
  //...
  virtual void ProcessFrame() = 0;
public:
  static void ProcessAIs(); // this call the ProcessFrame of all existing BaseAIs

  //...
};


class Monster : public BaseAI
{
protected:
  void ProcessFrame() {
    // do monster stuff
  }

  //...
};

class BigBoss : public BaseAI
{
protected:
  void ProcessFrame() {
    // do big boss stuff
  }

  //...
};

That's what function pointers / virtuals / delegates are for.

\$\endgroup\$
  • \$\begingroup\$ The problem is implementing ProcessFrame(). Let's say the BigBoss should walk towards the player in DefaultState, but in StunnedState he should flee and in ReloadState he would try to maintain an optimal range. Adding methods like isReloading and isStunned to the Actor would add bloat and ProcessFrame() would become a big if-else block. \$\endgroup\$ – Dan Dec 28 '14 at 12:41
  • 1
    \$\begingroup\$ A switch-case of 4 or 5 states isn't a big issue. Otherwise you'll end up writing over 400 lines of "generic" hardly traceable code to avoid writing 5 lines of switch-case in each ProcessFrame() functions. Soon enough you'll hit special cases where you need to do something custom for StunnedState for 1 AI and then will have to add special hooks (another debugging nightmare) for these special handling cases rather than simply add 1 or 2 lines in the switch-case-blocks. If you don't want to end up in debugging hell you must be able to reasonably follow the AI's code flow without a debugger. \$\endgroup\$ – Stephane Hockenhull Dec 28 '14 at 17:49
  • 1
    \$\begingroup\$ I tell you this because Been there, done that. There's a fine balance between what can be made generic and what needs to be written every time as 3-4 lines of throwaway code that takes 2mins to type. if you find large code sections between AIs that can be merged cleanly into a utility function just refactor those lines of common into one or more functions as you go. If you end up with too many different AIs there is an issue in your game design, see if you can merge them with different mesh skins and AI parameters (aggression level, a few flags, etc) and eliminate some AI complexity. \$\endgroup\$ – Stephane Hockenhull Dec 28 '14 at 18:06
0
\$\begingroup\$

create corresponding states for the AI. for example:

AIDefaultState, AIReloadingState , AIStunnedState 

you can inherit from the normal states if seemed logically correct in your situation

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.