AI:Development:Lang:Java

From Spring
Revision as of 12:15, 11 February 2010 by Abma (talk | contribs) (→Links)
Jump to navigationJump to search

Development < AI Development < Java AI Development

Getting Started on your own Java AI

Note: This tutorial assumes you have Spring working on your machine. The Spring directory will be referred to as 'Spring' from here on out.


Setting up a Java Project

To start working on your own Java AI you should first set up a project in your favourite Java editor. You need to import the Java Interface libraries, then create an AI class and an AI Factory.


Importing the interface libraries

In order to use the Java Interface, you will need to import the Java Interface libraries, located in 'Spring\AI\Interfaces\Java\[version]'. Replace [version] with the number of the latest Java Interface version. As of this writing, only 0.1 is available.

The file you need is AIInterface.jar. You also need the additional libraries vecmath.jar, located in the 'jlib' subfolder, as well as jna.jar in 'jlib\jna'. Optionally, you can attach the file 'AIInterface-src.jar' as source to the AIInterface.jar file for javadoc support in your editor.

Your project should now contain the following libraries:

  • Spring\AI\Interfaces\Java\[version]\AIInterface.jar
  • Spring\AI\Interfaces\Java\[version]\jlib\AIInterface-src.jar (optional)
  • Spring\AI\Interfaces\Java\[version]\jlib\vecmath.jar
  • Spring\AI\Interfaces\Java\[version]\jlib\jna\jna.jar


Creating the required classes

Now that you have the libraries, next step is to create your AI's main class. Firstly, you may want to create a package to put your classes in, for example, 'myjavaai.' Create a new class, named MyJavaAI (or whatever you would like your AI to be called). MyJavaAI should extend com.springrts.ai.oo.AbstractOOAI. After that you'll need another class, named MyJavaAIFactory. This class should extend com.springrts.ai.oo.OOAIFactory. The class MyJavaAIFactory only has to implement a single method: 'public OOAI createAI(int teamId, OOAICallback callback).' This is the method that the Java interface calls to get an instance of your AI. If you wanted to, you could create a different AI depending on circumstances, but for now it is probably easiest to simply return a new MyJavaAI(), until you decide you require something more elaborate. You should not have two classes: myjavaai.MyJavaAI and myjavaai.MyJavaAIFactory.


Adding your AI to Spring

Your AI needs to have its own folder, and the engine will need to know your AI's attributes.


Creating an AI folder

Every Spring AI is stored in 'Spring/AI/Skirmish'. You will have to create a folder for your AI as well. It is easiest to go to the directory 'Spring/AI/Skirmish', make a copy of the folder NullOOJavaAI, and rename the copy to MyJavaAI.


Telling the Engine about your AI

The MyJavaAI folder you created contains the different versions of your AI. As of this writing, the NullOOJavaAI only has a version 0.1, so there should be a folder '0.1' inside the MyJavaAI folder. Inside you will find a number of files. These files are:

  • SkirmishAI.jar: the jar file that contains the AI code.
  • AIInfo.lua: contains information about the AI.
  • AIOptions.lua: you can specify your AI's options in here.
  • Possibly some text files: these are log files generated by the NullOOJavaAI. If you don't plan on using the same files, you may as well delete these.

Additionally, there is a folder named 'jlib.' This is where you should place any additional libraries you use in your AI.

In order to let Spring recognize your AI, you will have to edit AIInfo.lua. You can do this with any regular text editor. When you open it, the file will look something like this:

local infos = {
	{
		key    = 'shortName',
		value  = 'NullOOJavaAI',
		desc   = 'machine conform name.',
	},
	{
		key    = 'version',
		value  = '0.1', -- AI version - !This comment is used for parsing!
	},
	{
		key    = 'className',
		value  = 'nulloojavaai.NullOOJavaAIFactory',
		desc   = 'fully qualified name of a class that implements interface com.springrts.ai.AI',
	},
	{
		key    = 'name',
		value  = 'high-level Java stub Skirmish AI',
		desc   = 'human readable name.',
	},
	{
		key    = 'loadSupported',
		value  = 'no',
		desc   = 'whether this AI supports loading or not',
	},
	{
		key    = 'interfaceShortName',
		value  = 'Java', -- AI Interface name - !This comment is used for parsing!
		desc   = 'the shortName of the AI interface this AI needs',
	},
	{
		key    = 'interfaceVersion',
		value  = '0.1', -- AI Interface version - !This comment is used for parsing!
		desc   = 'the minimum version of the AI interface required by this AI',
	},
}

return infos


The file describes a number of attributes. Each attribute has a key, a value and a desc(ription). The key is the name of the attribute, the description tells you what the attribute is used for, and the value is what you will want to edit.

The shortName attribute is the name used for your AI internally. It's most logical to change this into to the same name you gave your AI class. (MyJavaAI.)

'version' tells the engine which version of your AI this is. If you're in the 0.1 folder, make this 0.1. Your AI should be located at AI/Skirmish/shortName/version/.

'classname' tells the interface which class is your AIFactory class. Take care that you use the fully qualified class name, or it won't work. In this example, this should be myjavaai.MyJavaAIFactory.

'name' describes your AI to the user. In this example, you might put 'A simple Java AI to learn about AI programming in Spring.'

'loadSupported' is rather self-explanatory. Since you have just started working on your AI, you may not get around to implementing loading support for some time, so it's best to put 'no,' for now.

'interfaceShortName' and 'interfaceversion' tell the engine about the interface your AI uses. Since MyJavaAI uses the same interface as the NullOOJavaAI, leave these as they are.


Finally, the last thing you should do is exporting your AI to SkirmishAI.jar. The way to do this varies depending on your Java editor.


Test your AI

After following all the above steps, you should be able to run your AI. Either use Springlobby to start a new game and see if you can add your AI as a bot, or run spring.exe to see if you can start the script MyJavaAI Test. Of course, since you haven't told the AI what to do, your starting unit(s) will sit there doing nothing.


Getting your AI to Do Something

After getting your AI running, you will want to make it do something. Traditionally, the first thing to get your AI to is to build a solar plant, assuming you want it to play BA. If not, it is quite straightforward to build other units once you have the general idea. You will need three things to get your AI to build a solar plant:

  • A reference to the callback
  • A reference to the commander (your starting unit in BA)
  • A reference to the solar plant definition object
  • To tell the commander to build the solar plant

However, before we go about building solar plants, first you should know about events. We will use these to get the required object references.


Events

Nearly everything that happens in the game is an event. When a unit is created, this is an event. Enemy spotted? Also an event. A player sends your AI a message? You got it, this is an event as well.

Every event is made known to your AI. Have a look at the MyJavaAI class again. MyJavaAI extends AbstractOOA. AbstractOOAI implements a number of methods like unitCreated(), enemyEnterRadar() and message(). Sound familiar? The method unitCreated(Unit unit, Unit builder) is called every time a unit is created, and tells you the unit that was created and the unit that built it (can be null). Similarly, enemyEnterRadar(Unit enemy) and message(int player, String message) tell you about an enemy unit entering radar range and receiving a message from another player respectively. There are many other events that all have corresponding methods. To have your AI respond to these events, simply override the method in your MyJavaAI class. The return type of these methods is an integer. The return value can be used for debugging. 0 means that no errors occurred, while any other value is threated as an error. This system can be used for debugging, but is it much easier to implement proper logging.

In the sections below you will see an example of the use of these events.


Getting a reference to the callback

The callback is the object through which the AI communicates with the engine. All information that does not change the state of the engine can be retrieved through individual methods in the callback, and state changing commands (eg to units) will go through a single, specialized method, which is in the callback as well. Getting a reference to the callback is easy. First create a class field OOAICallback callback to put the reference in, then overwrite the method init(int teamId, OOAICallback callback). The init method is called at the start of every game (right at the end of the countdown). As you can see, all that you need to do is set the parameter value callback to the field callback:

@Override public int init(int teamId, OOAICallback callback) {

   this.callback = callback;
   return 0;

}

Getting a reference to the commander

In order to have your AI build a solar plant, the first thing you need is a reference to your commander. You first need an instance field 'Unit commander' to put the reference in, then we need to get the reference itself. A UnitFinished event is triggered whenever a unit has been built. (This may sound similar to unitCreated(). For a quick explanation of the difference, see the FAQ below.) At the start of the game, the engine will send a UnitFinished event for every starting unit. When playing BA, this means you will get a UnitFinished event for the commander. Therefore, the most straightforward way of finding the reference to the commander is overriding the unitFinished() method.

To get a reference to the commander, you need to check if the unit is indeed the command, and if so, assign the value of the unit parameter to the commander field:

@Override public int unitFinished(Unit unit) {

   if (unit.getDef().getName().equals("armcom"))
       this.commander = unit;
   return 0;

}


Getting a reference to the solar plant definition

Every unit in Spring has a UnitDef (a 'unit defintion'). This object tells you all there is to know about the properties of the unit type, such as maximum speed, turn rate and what weapons it carries. One way to get the reference to the definition of a solar plant is to use callback.getUnitDefs(). This will give you a list of all the available unit types in the game that is currently being played. The name of the unit def we are looking for is 'armsolar,' so we can just iterate through the list and find the one named armsolar, like this:

List<UnitDef> unitDefs = this.callback.getUnitDefs();
UnitDef solarPlant = null;
for (UnitDef def : unitDefs)
  if (def.getName().equals("armsolar"))
  {
      solarPlant = def;
      break;
  }


Telling the commander to build the solar plant

Now, the only thing left to do is to tell the commander that we want it to build the solar plant, and where to place it. In short, we need to give the commander a build command.

We want to give this command only once at the first frame of the game. For this we can use the update() method, which is called at every frame of the game. All we need to do is overwrite the update(int frame) method and give the command if the frame is 1. This results in the following code:

@Override public int update(int frame) {

   if (frame == 0)
   {
       // give command here
   }
   return 0;

}

Giving a command is straightforward. All we need to do is to create the appropriate command object and pass it through the callback to the engine.

The command we need to tell commander to build a solar plant is a BuildUnitAICommand. This command takes a number of arguments:

Unit unit
the unit we want to give to order to
int groupId
the group we want to give the order to
List<AICommand.Option> optionsList
a list addional options. See the values of AICommand.Option for details
int timeOut
the number of frames after which the command times out
UnitDef toBuildUnitDef
the UnitDef of the unit we want to build
AIFloat3 buildPos
the position to build the unit at
int facing
the direction the new unit should face


As you can see above, we can give a command to either a single unit or a group of units. In this example we only have a single unit. This means that unit should be a reference to our commander, and groupId should be -1. To keep things simple, we will not use any options, set the timeout to a large number, and set facing to 0. The value of toBuildUnitDef should be the object reference stored int UnitDef solarPlant. That only leaves buildPos, for which we will just use the current position of our commander. This means that our BuildUnitAICommand looks like this:

@Override public int update(int frame) {

   if (frame == 0)
   {
       AICommand command = new BuildUnitAICommand(this.commander, -1,
           new ArrayList<AICommand.Option>(), 10000, solarPlant,
           this.commander.getPos(), 0);
   }
   return 0;

}

Next, we should give the command to the engine. We do this through the method callback.getEngine().handleCommand(). This method takes three arguments: the id of the recipient of the command, a commandID and the command itself. The command should be sent to the engine, and for now we will use a commandID of -1, as this is a feature which is not yet supported by spring (asynchronous command tracking). Our code will look like this:

@Override public int update(int frame) {

   if (frame == 0)
   {
       AICommand command = new BuildUnitAICommand(this.commander, -1,
           new ArrayList<AICommand.Option>(), 10000, solarPlant,
           this.commander.getPos(), 0);
       this.callback.getEngine().handleCommand(AICommandWrapper.COMMAND_TO_ID_ENGINE,
           -1, command);
   }
   return 0;

}


At this point you will want to see if your AI actually works. Export your code to Spring/AI/Skirmish/MyJavaAI/SkirmishAI.jar and start up a game. Your AI should now build a solar plant at its start position.


Frequently Asked Questions

Q: What is the difference between unitCreated() and unitFinished()?

A: The difference is that unitCreated() is called when construction of a unit starts, while unitFinished() when construction on the unit has been completed. Any number of things may happen inbetween, including the unit under construction being destroyed, so it is important to remember this distiction.

Q: I have problems with the class-loader in my AI, any idea?

A: Each Java AI implementaiton is loaded by a separate class loader. This is used to load all the AIs class files and dependency libraries (in the jlib dir). Some libraries though, use the Thread contexts class loader, instead of the one of the current class. Therefore, if you want to load something from the AIs own class-path, make sure to let these libraries use the classpath of your AI classes, eg:

ClassLoader myClassLoader = AIFactory.class.getClassLoader();
MySpecialUtil msp = new MySpecialUtil();
msp.setClassLoader(myClassLoader);

Please do not set this class-loader on the thread with Thread.currentThread().setContextClassLoader(myClassLoader), because all AIs are run in the same Java thread, and therefore this will cause problems.


Links