Tags

, , ,

I ran into a design problem while trying to decide how to make the AI for the different enemy types. So, I posted a thread on the Unity forums asking which of the following would be the best design:

  • Reuse the same finite-state machine (FSM) for ALL the units, but adjust variables such as health & detection speeds;
  • Create a basic FSM, and then give each unit a specialised FSM that inherits or extends it;
  • Create entirely separate FSMs from scratch for each type.

Based on the responses there, I’ve come up with an idea of how to do it. For the benefit of anyone who doesn’t understand any of this, I’m going to explain it along the way.

I’ve decided to use a single finite-state machine for all the enemy types. A FSM is a program which can be in a finite number of states, such as (to use my code as an example) attacking, idle, patrolling and searching. They’re a pretty basic form of artificial intelligence for a video game, mainly because they can be easily extended to include new states. Using one single script means I will probably have less debugging to do.

Now, this does raise the issue of how give the units specific actions, such as their response to the sockpuppets/decoys. For instance, I have decided that the Admin-type enemies can trace a decoy’s launch position, and will do so after a brief period to check it’s a false alarm, or they will trace it immediately if they’ve had too many false alarms. Meanwhile, the n00b-type enemies just go over to it and stare gormlessly at it for a few seconds.

My solution to this, after somebody suggested using different methods/functions for each unit, is to use delegates. In object-oriented languages like C# (the language I use in Unity) or C++, Java and so on, a delegate is a helper object that does a particular task for another object, with specific parameters and return types. What that basically means is that class A tells class B “Right, here’s variable_name, deal with it”, and then ignores how class B actually deals with it. Borrowing two C# examples from the Unity tutorials here, with my comments added:


// define a delegate MyDelegate, of return type void
// and taking a single integer as it's parameter
delegate void MyDelegate(int num);
MyDelegate myDelegate; // create an instance of it

void Start ()
{
// this while print "Print Num: 50" when running
myDelegate = PrintNum;
myDelegate(50);

// this will print "Double Num: 100"
myDelegate = DoubleNum;
myDelegate(50);
}

void PrintNum(int num)
{
print ("Print Num: " + num);
}

void DoubleNum(int num)
{
print ("Double Num: " + num * 2);
}

When this programme runs, the delegate myDelegate would point first towards PrintNum, causing the Editor console to print out “Print Num: 50” when myDelegate(50) is executed. Right after that, myDelegate would point towards DoubleNum, and then the Editor console will print “Double Num: 100”. In each case, the Start method is saying “Just do something with the number 50, I don’t care what, and I don’t want any variables back”, and handing 50 to the two methods. The key part is that both these methods are of type void, meaning they do not return any variables or objects, and take an integer as a parameter, and therefore match the delegate. If DoubleNum had been written as follows:

int DoubleNum(int num)
{
return (num * 2);
}

Then myDelegate could not point to DoubleNum, because the return types do not match.

What’s the point of this? Quite simply, I have separate search methods which will define how each enemy type responds to the decoys. So, if I associate each particular enemy type with a variable unique to that type, then I can assign which method they use once at start-up, and they need not worry themselves with how each other acts. This means that if an enemy isn’t acting properly when in a particular state, I just have a single method to debug rather than a separate script, and I can reuse as much code as possible. Why reinvent the wheel when I have one at home?

Advertisements