Plug-In Monster AI

From RogueBasin
Revision as of 13:39, 15 September 2006 by Kisielewicz (talk | contribs) (Wikified ;-))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Lets face it, monster AI in most games leaves a lot to be desired..... the beasties are either too stupid, too predictable or just so uninteresting that the game becomes an endless, repetitive hack and slash. I don't object to mindless violence, of course. But it would be nice to think that some the talented coders out there could come up with something a little more impressive and atmospheric.

What I'm going to suggest here is a system of 'plug-in' AI that might just make doing some interesting monsters a little bit easier.

I'm going to write the examples in Java. This is partly because it's my new language of the moment. But mainly it's because having a good object-oriented model with garbage collection makes life much more pleasant. Java also lends itself to making quite elegant solutions to problems of this type.

What's good about this model? Here are the nice points.....

  • Allows for an infinite variety of friendly and unfriendly behaviour
  • Easily extensible
  • Relatively little extraneous coding, so you can focus on the AI itself
  • Once created, monsters can just take care of themselves
  • No huge switch statements (Hooray!!)

Fantastic, you say, but what is the downside? Well, it is quite a difficult concept at first, and it needs quite a lot of care in the implementation. But that, of course, is where the real fun starts. :)


The AI Model

The basic idea is this: Each mobile (creature) in the game is represented by an object that maintains a reference to a separate AI object.

The basic stripped down class definitions are:

// this is the definition for our basic mobile class 

class Mobile extends .... implements ..... {
  
  // this is the key line.... a reference to the current AI object
  public AI ai;

  public void setAI(AI newAI) {
    ai = newAI
    // previous AI object will be automatically garbage collected

    // and some other stuff as required
  }

  // All other fields and methods
  // e.g. stats, inventory
}
// And this is the generic AI object from which all other AI objects
// will inherit

class AI extends Object implements ...... {

  public void action(Mobile m) {

    //This is where the intelligence goes

  }

  // other AI functions go here, e.g psychology effects etc.
}

Whenever it is the particular mobile's turn to move, you just make the call

 monster.ai.action(monster);

This instructs the AI object to work it's magic, and make the monster perform whatever behaviour it is currently following.

Well, that's the basic theory, now we move on to:

Real-World Example

So far so good..... but what's the point? Well, what we want to achieve is a way of quickly and simply specifying a creature's behaviour. More importantly for realism and the sake of interesting AI, it allows us to change the behaviour of creatures dynamically as circumstances dictate.

We can do this by creating subclasses of the AI object, inheriting all the generic behaviour and adding whatever specific behaviour we desire. The most obvious AI to implement is that of the standard monster, who justs moves towards the Hero and attacks automatically.

class AttackPlayerAI extends AI {
  // Just override the action method
  public void action (Mobile m) {
    //call standard monster movement function if player is visible
    if m.canSee(Hero)
    {
      m.moveTowards(Hero.x,Hero.y);
    } else {
      //Just lurk here and do nothing
    }
  }
}

If we now want to create a kobold at location (10,10) with this simple behaviour, we just write:

 Mobile m = new Kobold(10,10);
 m.setAI(new AttackPlayerAI());

Et voila.... one particularly unfriendly Kobold has just been summoned into existence.

As a second example, this class makes a creature that walks between two points, e.g. a guard patrolling a side of a castle.

class GuardAI extends AI {
  // these hold the co-ordinates of target squares (tx1,ty1) and (tx2,ty2)
  int tx1;
  int ty1;
  int tx2;
  int ty2:
  // direction==1 means move towards target 1 else move towards target 2
  int direction  

  // Constructor for AI to walk between (x1,y1) and (x2,y2)
  public GuardAI (int x1, iny y1, int x2, int y2) {
    tx1=x1;
    ty1=y1;
    tx2=x2;
    ty2=y2;
    direction=2; //start walking from 1st to 2nd target
  } 

  // Now override the action method
  public void action (Mobile m) {
    //work out current destination
    int tx = (direction==1) ? tx1 : tx2;
    int ty = (direction==1) ? ty1 : ty2; 

    //call standard monster movement function
    m.moveTowards(tx,ty)

    //change direction if target reached
    if ((m.x==tx)&&(m.y==ty)) {
      direction = (direction==1) ? 2 : 1;
    }
  }
}

Now to create a guard that patrols between (10,10) and (20,5), just use:

 g = new Guard(10,10);
 g.setAI ( new WanderAI(10,10,20,5) );

If the player attacks the guard, however, then we want to change the guard into a hostile creature, represented by a different AI. This is done simply as follows:

 g.setAI ( new AttackPlayerAI() );

where AttackPlayerAI is just a standard AI descendant that makes the creature charge towards the player in a berserk rage......


Extensibility

Of course, inheritance is a very useful concept in programming, and never more so than here. Withg only minor modifications, we can make a HostileGuard who patrols like a normal guard but attacks the player as soon as he/she is visible:

class HostileGuardAI extends GuardAI { 

  // override action
  public void action(Mobile m) {
    
    if (m.canSee(Hero)) {

      // Charge! Install new AI into the mobile.
      m.setAI (new AttackPlayerAI()); 
      Hero.message("The " + m.name() + " yells with rage!");

    } else {

      // call parent method to continue patrol....
      super.action(m);

    }
  }
}

The really neat thing about this system is that it is almost infinitely extensible. You can make a new AI object for almost any type of monster imaginable. Just a few ideas for AI objects:

CowardlyMageAI ()

Stays still and casts spells at the player UNLESS there are no cretures between it and the angry hero, in which case it makes for the nearest escape route.

SleepAI (int time, AI wakeup)

Sleeps for time periods, then gets up uses the new AI object given. Brilliant for a mesmerize spell, for example, that makes the creature friendly upon waking.

LurkAI (int dist, AI whatnext)

Waits on the same spot, until the player comes within a specified distance. Then does something completely different, e.g. run away, attack, or offer to trade.....

FleeAI ()

Run away! This AI object makes sure that the creature stays as far away from the player as possible. You could give the creature a valuable object, and make it play a fun game of hide+seek all round the dungeon.

SequenceAI (AI firstAI, int time, AI secondAI)

Use firstAI for the specified number of time periods, then swotch to secondAI. Nested SequenceAI objects could be used to create complex behaviour.

The list of possibilities is endless.......

Having thought about it for a while, I think that psychological effects such as certain spells should probably be passed to the AI object, which can deal with them as approprite. For example, UndeadAI might filter out any panic or confusion effects. If necessary, the AI could react by installing a new AI object. For example an initially hostile goblin who is hit by a panic spell could be given a FleeAI object.

It is probably useful to create a default AI for each type of creature, so that you don't need to explicitly create a new AI object whenever you create a mobile. Behaviour could also be allowed to return to this default AI when psychological effects wear off.

Extensions

There's lots of things that could be added to this AI model. I'd like to describe just one of them very briefly that I think could be a useful way of defining behaviour for roguelike creatures.

Since each mobile contains only a *reference* to it's AI object, the thought naturally occurs that several mobiles could share the same AI object.

If this was a simple AI with no internal data, e.g. AttackPlayerAI, this would make no real difference, apart from saving the overhead of constructing a seperate AI object for each hostile monster.

But you could create a special set of "Group" AI objects that would allow a set of monsters to act as a pact, with a built-in flocking instinct and some idea of co-operation in battles. Basically, you would construct a single AI object and assign it to all the monsters in the group, who would then take on the relevant shared behaviour.

e.g.

 AI ai = new HositlePackAI ();
 Mobile m1 = new Orc(10,10);
 Mobile m2 = new Orc(10,10);
 Mobile m3 = new Goblin(10,10);
 Mobile m4 = new Goblin(10,10);
 Mobile m5 = new Troll(10,10);
   
 m1.setAI(ai);
 m2.setAI(ai);
 m3.setAI(ai);
 m4.setAI(ai);
 m5.setAI(ai);

This kind of system would be brilliant for outdoor skirmishes. You could monsters capable of laying an ambush. You could have them charging and retreating as a single unit. I leave it to your imagination to dream up all of the evil ways you could get fighters to flock in defence of a vulnerable mage,

Conclusion

Well, I hope this gives a little bit of inspiration to everyone out there who is trying to craft the ultimate creature from the pit. I hope that I've shown that although there's a lot of different things that you need to consider when building an AI system, the problem really can be manageable and flexible if you find the right structure.

Happy Coding!

Mike Anderson

mike@mikera.net

5th March 1999