One of the biggest features lacking in most small games and even larger games is some form of intelligent computer player. This article series is aimed to get you, the developer, to start thinking outside of the box when it comes to creating your game’s AI players. We will eventually be discussing what a genetic algorithm is as well as neural networks but for now, we need to start with the basics.
One common misconception is that an AI character must be scripted, that they have to know what to do in every single case. While this is fantastic for those that know every possible case and want to know every possible outcome, it does nothing for emergent gameplay. Consider the possibilities if the AI didn’t know what they were doing the first time they did it. Hopefully you have arrived at the conclusion that an AI character starts to think and act like a human player with one giant flaw: the AI simply does not have the brain of a human. If you thought of this, you are right. However, what if we were to give the AI the ability to learn and thus be able to create a brain? Excited yet? Let’s start building us a framework.
To start us off I am going to create an empty solution in Visual Studio 2008 as shown in Figure 1. If you are unable to do this, create an Xna 3.0 Windows Game Library instead, calling it IntelligenceFramework. I start with an empty solution because it allows me to have more control over the projects created. The next step is just adding a Game Library called IntelligenceFramework. And to finish off with the creation stage, I will go ahead and remove the content project and the default Class1.cs file.
When we consider a human on a basic level, a conclusion can be drawn that humans can do a few generic things: use something, consume something, wear something, and release something. We can use these abstractions to create some interfaces. The first thing we will need is what represents the character, or human. For now, this IActor interface will have two methods with which we can add and remove objects that follow one of the above actions.
1 2 3 4 5 6 7 8 | namespace IntelligenceFramework { public interface IActor { void Add(IUseable useable); void Remove(IUseable useable); } } |
Clearly we need to create the IUseable interface now. This interface will serve as the basis for many other interfaces. We need to abstract all of them (IConsumable, IWearable, IReleaseable, et al) to a common level of functionality so the actor classes can deal with them in a single manor. The two operations needed are Deploy and Retract for the simple reason that the Actor class needs to be able to use (Deploy) the item and then [possibly] get rid of it (Retract).
1 2 3 4 5 6 7 8 | namespace IntelligenceFramework { public interface IUseable { void Deploy(IActor actor); void Retract(); } } |
This is probably getting more and more abstract and thus it is time to throw it into a real world situation! Let’s add a class to the project and call it Actor. The Actor class will implement that IActor interface we created before. If you are asking yourself why I have taken the route of using interfaces instead of just creating the Actor class itself, it is because I don’t want to limit users to my idea of how the implementation should be. For instance, you will see in the following code that I do not Deploy or Retract the item when it is added or removed. This is, of course, a possibility depending on what kind of IUseable classes you are creating.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class Actor : IActor { List<IUseable> inventory = new List<IUseable>(); public void Add(IUseable useable) { if (useable == null) return; for (int i = 0; i < inventory.Count; i++) if (inventory[i] == useable) return; inventory.Add(useable); } public void Remove(IUseable useable) { if (useable == null) return; inventory.Remove(useable); } protected IUseable[] Inventory { get { return inventory.ToArray(); } } } |
Now that we have a base class, create a new project so we can test it out! I am going to create a Console Application and call it RpgDriver for reasons you will soon find out. I am using a Console Application so as not to overload you with other things that would have to be done. The point of this project is to test out our framework easily and quickly. After referencing the IntelligenceFramework project, add a class to RpgDriver called Character. The Character class will inherit Actor and implement some new functionality.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Character : Actor { public Character() { Health = 25; MaxHealth = 100; } public void Use(Type typeOfUseable) { for (int i = 0; i < Inventory.Length; i++) { IUseable item = Inventory[i]; if (item.GetType() == typeOfUseable) { item.Deploy(this); break; } } } public int Health { get; set; } public int MaxHealth { get; set; } } |
Here we are adding a function, named Use, that allows us to deploy a single instance of a certain type of item. We are also giving the character some health, however notice we are not filling the health to the max. The next step is to create our item: the health potion! Add a new class, called HealthPotion, to the project that implements the IUseable interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class HealthPotion : IUseable { public void Deploy(IActor actor) { Actor = actor; Character character = Actor as Character; if (character != null) { character.Health += 50; if (character.Health > character.MaxHealth) character.Health = character.MaxHealth; } Actor.Remove(this); Retract(); } public void Retract() { Actor = null; } private IActor Actor { get; set; } } |
Fortunately the implementation is rather simple. On Deploy being called, the HealthPotion attempts to up the characters health by up to 50 points and then removes itself from the Actor’s inventory. The last thing to do with our test project is to write some test code! In Program.cs, type in the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Program { static void Main(string[] args) { Character myChar = new Character(); myChar.Add(new HealthPotion()); myChar.Add(new HealthPotion()); myChar.Add(new HealthPotion()); myChar.Add(new HealthPotion()); Console.WriteLine("{0:000}/{1:000}", myChar.Health, myChar.MaxHealth); myChar.Use(typeof(HealthPotion)); Console.WriteLine("{0:000}/{1:000}", myChar.Health, myChar.MaxHealth); myChar.Use(typeof(HealthPotion)); Console.WriteLine("{0:000}/{1:000}", myChar.Health, myChar.MaxHealth); } } |
Now run it! You will see the health of the character move up and hit a max of 100 as the inventory length drops as potions are used.
1 2 3 4 | 025/100 075/100 100/100 Press any key to continue . . . |
What is important to take away from all of this is that the Actor and Character classes do not know what they are actually using. All they know is that they can use the item and it may or may not have an effect on their properties. In this case we added a simple HealthPotion class that ups the health of the Character class and used the item manually. In the next part of this series I will cover how we can extend the base interface[s] to allow a health potion to be automatically used as well as used in certain situations. Further down the line we will cover how we can use genetics to get an AI to pick up items, use them and learn whether they are good or bad for the future.
