Friday 7 June 2013

To think or not to think

    This post will be delving into the journey from mindless inanimate characters to one of slightly stupid, artificially minded characters.  That's right, it's AI time!  Read all about it after the break (there's even a video for you lucky, lucky people!).
   
     The original plan for the game was to be a very grand re-imagining of an AI technique called STRIPS (which stands for Stanford Research Institute Problem Solver).  In a nutshell it's a system to make decisions based on a three elements:  The desire, the action, and the state of the world.  I won't cover it any more as there are plenty of resources online.
   
    So the plan was to rework STRIPS into a more game friendly system.  It works too.  It creates a very subtle and flexible form of AI, as each desire is considered and actioned upon based on the state of the world.  Ultimately though, it had one drawback that I decided was a bit too much for a game like LOTWK:  It doesn't have many rules.  STRIPS is a great system for creating unexpected and emergent patterns of behaviour, but its core strength lies in a lack of hard and fast rules.  This has the side effect of game AI characters making decisions that seem 'wrong'.  They would achieve the goal of the character, but not in an expected way.  It made it very difficult to predict the actions of characters; which sounds great on the surface, but you then realize that predictability is what makes AI seem human.  If the decisions are too random, it seems more like a roll of the dice than a decision.
   
    So STRIPS was out and your standard behavioural tree was in.  I had spent much time working on the STRIPS approach, and then spent more time working on the behavioural tree system.  It was a very commonly used setup for game AI - you have a series of states that make the decisions, and the characters swap between states based on various conditions.  The conditions are suitable nebulous and are 'soft' limits (they use ranges of acceptable values instead of hard limits), which is often referred to as Fuzzy Logic.  So I had my fuzzy logic behavioural tree up and running, and working great.  But there was a problem, and wouldn't you know it: the problem was the behavioural tree was TOO predictable!  The AI had gone from random to rigid, both of which were unacceptable to me.  Back to the drawing board.
   
    Eventually I found the solution - the bastard love child of STRIPS and fuzzy behavioural trees.  And so was born the Goal-based Situationally-Aware Fuzzy-Logic Micro-state AI (I don't think I'll bother with an acronym).
   
    This new system had at its core three attributes:

  •      Goal based decision making - taken from the STRIPS system, each character has a set of goals and a series of actions that might assist in the completion of those goals.
  •      Situational Awareness - this is really a system to break up the goals into a form of finite state machine, so that the characters only consider goals that are appropriate for their current situation.
  •      Fuzzy-logic powered micro-states.  To better fit the different behaviours into the goal-oriented system, I decided to break them up into tiny states that have a simple, or sometimes singular, purpose.  Gone are the complex states of a behavioural tree, and in are the simple states serving as actions for the completion of goals.
    The goal based decision making is really the 'meta' side of the AI.  It does virtually nothing in regard to decision making, but it does serve as a good way to treat decision making as wish fulfilment.  A simple example is the goal of 'find cover'.  Any unit wishing to fulfil this goal will enter a state called 'finding cover' in which the decisions will be made about where to move to.  The main advantage of using goals like this is that they are very easy to define using text files.  For example, this is the XML that defines the normal or 'open' state for healer AIs:
   
    <situation id="open">
        <goal id="moveToRangedCover" />
        <goal id="healOthers" value="0.75" />
        <goal id="healSelf" chance="0.8" value="0.65" />
        <goal id="equipRanged" />
        <goal id="rangedAttacks" />
    </situation>
   
    Each goal is simply the name of a desire, which the game maps to a micro-state that would best fulfil that goal.  Each goal/state pair also has two specific methods that help in the decision making: isComplete and isValid.  As the code iterates through the goals relevant for the current situation, it first checks the isComplete result.  If the goal is complete, we don't need to do anything more and can move on to the next goal.  Then it checks the isValid result.  This method serves a dual purpose - first it checks if it's even possible for the current character to perform the actions required, and secondly it checks more specific requirements for the state.  For example, in the goal 'scavenge weapon', the isValid check will look for weapons on the floor or in containers that the AI could collect and use.  If there are none, then the 'scavenge weapon' goal is invalidated and ignored.
   
    Then we come to the situationally aware part of the AI.  This is a very simple and abstracted form of awareness in which the AI decides which goals to pursue based on its current situation.  The situations are simple affairs such as 'out in the open', 'behind cover', ' under threat', and 'in melee combat'.  Mostly the situations serve as a way to re prioritize the states, but in some cases a situation might warrant goals that are not found in other situations.  For example, a ranged unit will dislike fighting in melee, so the only time you'll see the 'equip melee weapon' and 'melee fighting' goals for a ranged unit is in the 'melee combat' situation.  The rest of the time he'd prefer to be picking off targets from afar.
   
    Finally we have micro states.  The real bonus for this system is that it abstracts the state-changing decisions and moves them away from the states.  States no longer care if they would be better off in a different state, instead they focus on the meat of the AI - deciding how to f*** s*** up!  The states otherwise follow a fairly standard AI approach - viable moves are chosen for the character, then they are scored based on a variety of aspects (range, potential for damage, accuracies, obstructions, etc).
   
    There are some great benefits to this system:
   
    * Code re-use.  The micro states are small and simple enough that they can be used by most of the AIs even though any two AIs might have wildly different ideas about how to win.  This cuts down on the amount of duplication in the code, and keeps things simple.  Simple is good.  At least that's what my mother always told me when I brought my report card home.
   
    * Simple data definitions.  The ability to define the AI through simple XML files is a great time saver.  I can tweak values and re-order the decisions with a simple text edit.  I can then reload the AI data without having to reload the entire game.  Very useful for tweaking and refining.
   
    * The code is a lot cleaner and simpler this way.  Everything is neatly compartmentalized, making it easier to track down bugs, and easier to make changes without worrying about knock-on effects.  The use of micro-states in particular makes the code simpler and more maintainable.



    Here ends my rambling.  I hope you enjoyed the insight into the AI development for The Last of the Warlock Kings.

    If you would like to see the AI in action, there's this short video where two AI teams battle it out:

    Please bear in mind that this is still an extremely early phase of the game's development, and there are bugs, animation glitches, lack of textures, lack of audio, and other issues apparent in the video.

No comments:

Post a Comment