AI:Development:Lang:Java

From Spring

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.

You will also need to make sure that Java AI's run properly on your machine. This is most easily done by running a Spring game with the NullOOJavaAI that comes with the engine. You will need the JRE 1.5 or greater, and to set the JAVA_HOME environment variable on your system to point to the correct location.

Note: If you're using a 64 bit version of Windows, you will need a 32 bit Java Runtime Environment/Java Development Kit anyway, because Spring is (as of May 2014) not yet available as a 64bit version for Windows.


Setting up a Java Project

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

OUTDATED If you are using Netbeans IDE, you can find a detailed tutorial here (from this forum thread). Otherwise, please continue reading this page.

Importing the interface libraries

In order to use the Java Interface, you will need to import the Java Interface libraries (optionally, you can attach the sources for JavaDoc support in your editor)

  • Spring/AI/Interfaces/Java/[version]/AIInterface.jar (sources: jlib/AIInterface-src.jar)
  • Spring/AI/Skirmish/NullOOJavaAI/0.1/jlib/vecmath.jar (sources: vecmath-src.jar)
  • JavaOO-AIWrapper.jar (sources: TODO)

Creating the main class

Now that you have the libraries, the 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.

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.NullOOJavaAI',
      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.) The shortName needs to agree with the folder name (see below) and cannot contain the char '_'.

'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 four 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. 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

Whenever a unit has been built, the engine sends a unitFinished event. 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:

@Override
public int unitFinished(Unit unit) {
    if (unit.getDef().getName().equals("armcom")) {
        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;
}

Commands are issued by invoking the respective method on the Unit (or Group). In our case, that's the build method, which takes the following arguments:

  • UnitDef toBuildUnitDef: the kind of unit to build
  • AIFLoat3 buildPos: the location the unit should be built at (in our case, we use the commander's current position)
  • int facing: the direction the new unit will be facing (mostly matters for buildings, which can not turn after being constructed)
  • short options: a set of UnitCommandOptions. 0 means no option.
  • int timeout: the number of frames after which the command times out. We don't want a timeout, and therefore specify a suitably high number :-)

Therefore, our command looks like this:

   commander.build(solarDef, commander.getPos(), 0, (short) 0, Integer.MAX_VALUE);

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.

Building a metal extractor

Building a metal extractor is slightly more complicated. Assuming you're not on a metal map, you will need to find a suitable site to place the extractor. Generally, it's preferable to select a site closest to wherever your construction unit (commander) is located.

Modify your code that got a reference to the solar plant by making the following changes:

List<UnitDef> unitDefs = this.callback.getUnitDefs(); UnitDef solarPlant = null; for (UnitDef def : unitDefs)

  if (def.getName().equals("armsolar"))
  {
      solarPlant = def;
  }
  else if (def.getName().equals("armmex"))
  {
       metalExtractor=def;
  }

Again, you will need to create a Unitdef metalExtractor field by where you declared your solarPlant field.

Next, you need a method for determining the distance between two 3D coordinates (since Spring locations are three dimensional). A slightly modified version of a distance function is found below.

public float calculateDistance(AIFloat3 a, AIFloat3 b)
    {
    float xDistance = a.x - b.x;
    float yDistance = a.y - b.y;
    float zDistance = a.z - b.z;
    float totalDistanceSquared = xDistance*xDistance + yDistance*yDistance + zDistance*zDistance;
    return totalDistanceSquared;
    } 

The above code actually returns the distance squared, but it will work for our purposes. In your init function, you will want to add the following code above the return statement but below the line defining the callback:

checkForMetal();

This will call the following function you need to place in your code:

public void checkForMetal()
    {
    Resource metal=Resource.getInstance(clb, 0);
    availablemetalspots=clb.getMap().getResourceMapSpotsPositions(metal);
    if(availablemetalspots.isEmpty())
        {
        sendTextMsg("This is a map with no metal spots");
        metalmap=false;
        }
    else
        {metalmap=true;
        sendTextMsg("This is a map with metal spots. Listing Values...");
        for (AIFloat3 metalspot : availablemetalspots) {
            sendTextMsg("Metal Spot at X: "+metalspot.x + ", Y: "+metalspot.y +", Z: "+metalspot.z);
        }
        }
    }

For the above code to function properly, you will need to create a Boolean named metalmap and a List<AIFloat3> availablemetalspots. You will now have a function set up for calculating distance between two points, and a data structure holding a list of positions of metal spots. The next step is to tie these two together:

public AIFloat3 closestMetalSpot(AIFloat3 unitposition)
    {
    AIFloat3 closestspot=null;
    for (AIFloat3 metalspot : availablemetalspots) {
            if (closestspot==null)
                {
                closestspot=metalspot;
                }
        else if(calculateDistance(metalspot, unitposition) < calculateDistance(closestspot, unitposition) && metalspot.hashCode()!=unitposition.hashCode())
                    {
                    closestspot=metalspot;
                    }
        }
    availablemetalspots.remove(closestspot);
    return closestspot;

    }

The above function accepts an AIFloat3 containing the location of your construction unit (commander). It will then find the closest metal spot and return that location. The next step is to have your commander make use of this.

  if (frame == 600)
  {
       if(metalmap){       sendTextMsg("About to build mex at frame " + frame);
       AIFloat3 closestspot=closestMetalSpot(commander.getPos());
       commander.build(metalExtractor, closestspot, 0, (short) 0, Integer.MAX_VALUE);
  }

Place the above code in your update() method. When the frame count reaches 600, your commander will start to build a metal extractor at the closest metal spot. The extractor may not be right on top of where the metal spot appears to be, but you can mouse over the extractor to make sure it's drawing the metal properly.

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 in-between, including the unit under construction being destroyed, so it is important to remember this distinction.

Q: Do I have to copy and paste my jar file to the directory in Spring/AI/Skirmish/MyAI/0.1 each time I want to test my AI out?

A: Yes, but it is much easier if you automate the process. In Windows, a simple batch file can be created to move the files over for you. For example

copy C:\Users\msm8bball\Documents\NetBeansProjects\MyAI\dist\MyAI.jar "C:\Program Files (x86)\Spring\AI\Skirmish\MyAI\0.1"

del "C:\Program Files (x86)\Spring\AI\Skirmish\MyAI\0.1\SkirmishAI.jar"
rename "C:\Program Files (x86)\Spring\AI\Skirmish\MyAI\0.1\MyAI.jar" SkirmishAI.jar

Modify the above code to match your computer's directories. Place it in a text file and save it as MoveAI.bat. This will create a batch file that you can double click and it will automatically move your AI to the proper location and rename it. You might wish to put a shortcut to this in your Start menu or taskbar.

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

A: Each Java AI implementation is loaded by a separate class loader. This is used to load all the AI's 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 AI's own class-path, make sure to let these libraries use the class-path 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 AI's are run in the same Java thread, and therefore this will cause problems.


Links