Tutorial

The outline:

Before you continue it is a good idea to make yourself familiar with the basic mechanics of UT2004.

HunterBot - setting up the example

This example is installed by Pogamut UT2004 installer. In NetBeans click New Project -> Maven -> Project From Archetype -> Local Archetypes Catalog and select 02-navigation-bot-archetype project. Moreover, as Pogamut 3 has been fully mavenized, you can try and run this example even without installing the Pogamut NetBeans plugin. However in that case you won't be able to use visualization as this is a part of Pogamut NetBeans plugin. To open up this example in NetBeans follow up the steps in Opening Pogamut Examples chapter. This archetype information is below.

For UT2004 example:

  • Group Id: cz.cuni.amis.pogamut.ut2004.examples

  • Artifact Id: 04-hunter-bot-archetype

  • Version: 3.3.1

  • Repository:http://diana.ms.mff.cuni.cz:8081/artifactory/repo

For UDK example only change Group Id: to cz.cuni.amis.pogamut.udk.examples and Version: 3.2.5-SNAPSHOT . The rest remains the same.

Note: You will find up-to-date list of available archetypes in Pogamut Maven archetypes catalog

HunterBot - overview

Here we will review the code of HunterBot and the most notable combat module WeaponPrefs.

HunterBot. Hunter bot is a simple reactive combat bot. He has six behaviors he can perform and he can switches between these behaviors according to the current state of the environment and his internal values. Moreover the behaviors have priority - some behaviors are more important then others. This means that if conditions for two behaviors holds and one has higher priority than the other, the behavior with higher priority will be triggered.

From implementation point of view, each behavior is defined in separate method prefixed with "state" word. The switching and triggering of the behaviors is handled in the logic() method. Eventhough each behavior is defined in "state" something method the bot IS NOT a finite state machine in its pure sense. There are not any explicit transitions between bot behaviors and each behavior has a fixed priority. That's why it is better to see the bot not as a finite state machine, but as a set of behaviors with priorities.

The six behaviors of the bot are ENGAGE, MEDKIT, HIT, PURSUE, SEE ITEM and RUN AROUND ITEMS.

Behavior ENGAGE (method name: stateEngage()) Behavior ENGAGE is fired when the bot sees any enemy. If the enemy that was attacked last time is not visible than the bot tries to choose a new enemy. For shooting weapon preferences module is used (about the module later). Bot also tries to run towards the target if he is far. If he is not, he will just stand still and shoot.

Behavior MEDKIT (method name: stateMedKit()) Behavior Medkit is triggered when the bot is low on health and is not in combat. The bot tries to reach some health item, if it exists on the map. Otherwise he just runs around items randomly.

Behavior HIT (method name: stateHit()) Behavior Hit is triggered when the bot is hit by someone. It causes the bot to turn around.

Behavior PURSUE (method name: statePursue()) Behavior Pursue is triggered when the bot is in combat, but the enemy is not visible for some reason, e.g. enemy was lost around the corner. This state will trigger the bot to pursue the enemy to the last known location. If the enemy can't be seen from there, the bot will stop pursuing. Bot will also not pursue if there are other enemies in sight.

Behavior SEE ITEM (method name: stateSeeItem()) Behavior is triggered when the bot sees an item and is not equipped enough. It will cause the bot to run toward visible item and pick it up.

Behavior RUN AROUND ITEMS (method name: stateRunAroundItems()) Behavior Run Around Items is the lowest priority behavior. It is triggered only when the bot is not in combat and not low on health. This behavior makes the bot to run around items - both weapons and general - in the map. Eventhough the bot does not prefer any concrete weapons or items, he will get geared up eventually.

Switching of the behaviors happens in the logic() method. Below you see the code from hunter bot with comments. Note that the highest priority has anti-stuck behavior that is not a behavior in fact, but just a routine reseting the bot internal variables in case the bot gets stuck somewhere in the map.

    public void logic() {
        // global anti-stuck?
        if (!info.isMoving()) {
            ++notMoving;
            if (notMoving > 4) {
                // we're stuck - reset the bot's mind
                reset();
                return;
            }
        }

        // 1) do you see enemy? 	-> go to PURSUE (start shooting / hunt the enemy)
        if (shouldEngage && players.canSeeEnemies() && weaponry.hasLoadedWeapon()) {
            stateEngage();
            return;
        }

        // 2) are you shooting? 	-> stop shooting, you've lost your target
        if (info.isShooting() || info.isSecondaryShooting()) {
            getAct().act(new StopShooting());
        }
		
        // 3) are you being shot? 	-> go to HIT (turn around - try to find your enemy)
        if (senses.isBeingDamaged()) {
            this.stateHit();
            return;
        }
		
        // 4) have you got enemy to pursue? -> go to the last position of enemy
        if (enemy != null && shouldPursue && weaponry.hasLoadedWeapon()) {  // !enemy.isVisible() because of 2)
            this.statePursue();
            return;
        }

        // 5) are you hurt?			-> get yourself some medKit
        if (info.getHealth() < healthLevel && canRunAlongMedKit()) {
            this.stateMedKit();
            return;
        }

        // 6) do you see item? 		-> go to GRAB_ITEM	  (pick the most suitable item and run for)        
        if (shouldCollectItems && !items.getVisibleItems().isEmpty()) {
        	item = getNearestVisibleItem();
        	if (item != null && fwMap.getDistance(info.getNearestNavPoint(), item.getNavPoint()) < 500) {
            	stateSeeItem();
        		previousState = State.GRAB;
        		return;
        	}
        }

        // 7) if nothing ... run around items
        stateRunAroundItems();
    }
	

Running multiple bots. When you run the bot you may notice strange thing - two bots will be spawned in the environment - not only one. This is because one hunter bot would be quite boring - he wouldn't have anyone to hunt. That's why we set hunter bot to be launched twice in UT2004BotRunner. You may alter this by changing the parameter in .startAgents(int botNumber) to .startAgents(1) or by using method .startAgent() instead of .startAgents(int botsNumber). UT2004BotRunner can be found in main(String args[]) method:

    public static void main(String args[]) throws PogamutException {
        // starts 2 Hunters at once
        // note that this is the most easy way to get a bunch of bots running at the same time        
    	new UT2004BotRunner(HunterBot.class, "Hunter").setMain(true).setLogLevel(Level.INFO).startAgents(2);
    }
	

Setting the bot aim level. When playing against HunterBot you may find it hard to defeat him at first - especially if you are an inexperienced FPS game player. Eventhough his combat behavior is simple his aiming is quite good. This is because his skill level is set to relatively high number - to five. In UT2004 you can set seven difficulty levels for the bots with 1 being the lowest and 7 being the highest. In Pogamut these difficulty levels affect bot aiming errors Setting the skill to 7 means the bot almost never misses. Setting skill to level 1 means he won't be able to hit you most of the time. To change the bot skill level, modify the Initialize command in getInitializeCommand() method by setting .setDesiredSkill(int skill) switch to desired number, e.g.:

    /**
     * Here we can modify initializing command for our bot.
     *
     * @return
     */
    @Override
    public Initialize getInitializeCommand() {
        // just set the name of the bot and his skill level, 1 is the lowest, 7 is the highest
        return new Initialize().setName("Hunter").setDesiredSkill(5);
    }	
	

Again: in Pogamut the skill of the bot will determine how well will the bot aim.

Summarizing. HunterBot is a simple example of a reactive combat bot in UT2004. He tries to solve all the important issues in FPS game - combat with enemy players, running around the map collecting items and reacting to important events such as "I've been hit". However he solves these problems only on a basic level and hence is not a very good player of UT2004. Some of the problems he has are:

  1. He runs around the map more or less randomly - not prefering the best weapons or armor.

  2. His combat behavior is low level - he only runs towards the enemy player and then stops and stands still - not a very good idea :-).

Next we will explain how to work with WeaponPrefs module - a module enabling to easilly define advanced combat behavior for the bot.

WeaponPrefs module

WeaponPrefs or weapon preferences module is very useful module enabling the user to easilly define the priority of weapons. Not only it allows to define general priority, it can also be used to set priority according to current distance between the bot and his target. WeaponPrefs can be accessed from the bot code by typing weaponPrefs. . In the first example we will show you, how to use weapon preferences to define general weapon preferences.

Note that general weapon preferences are meant to be set up once, preferably before the bot running around in the map. You cannot change general weapon preferences, however you can clear them and set them up again. The example below shows a simple example of defining and using general weapon preferences in your bot.

	  
    public void prepareBot(UT2004Bot bot) {	  
        // DEFINE WEAPON PREFERENCES
        // We will define only general preferences stating which weapon + firing mode should be preffered.
        // The priority is from the top to the bottom.
		
        // Top priority weapon is MINIGUN with secondary firing mode - the boolean means whether we should use primary firing mode or not
        weaponPrefs.addGeneralPref(ItemType.MINIGUN, false);
        // Second priority is Link gun with secondary mode
        weaponPrefs.addGeneralPref(ItemType.LINK_GUN, false);
        // Third is LIGHTING_GUN but now we use primary firing mode (bUsePrimary is set to true)
        weaponPrefs.addGeneralPref(ItemType.LIGHTNING_GUN, true);
        weaponPrefs.addGeneralPref(ItemType.SHOCK_RIFLE, true);
        weaponPrefs.addGeneralPref(ItemType.ROCKET_LAUNCHER, true);
        weaponPrefs.addGeneralPref(ItemType.ASSAULT_RIFLE, true);        
        weaponPrefs.addGeneralPref(ItemType.FLAK_CANNON, true);
        weaponPrefs.addGeneralPref(ItemType.BIO_RIFLE, true);
    }
	
    public void logic() {
        // Here you can see, how you can use your predefined weapon preferences when shooting other player.
        // Your bot will use the best weapon he has according the weapon preferences. 
        if (players.canSeePlayers()) {
            Player target = players.getNearestVisiblePlayer();
            shoot.shoot(weaponPrefs, target);
        } else if (info.isShooting())
            shoot.stopShooting();		
    }
    

Defining weapon preferences for range. The general weapon preferences are useful, but if you want to code first class deathmatch bot it probably won't be enough. Ideally you would like your bot to prefer different weapons according to the distance there is between him and his enemy. Some weapons are very strong at short distances, but are almost useless at longer ranges, e.g. Flak Cannon and vice versa. Next example shows how to use weapon preferences module to define weapon priorities according to the distance between the bot and the target.

Defining weapon preferences with range works as follows: First you define new range class by setting the maximum distance the class will be considered at and then you add weapon types to this range class. The priority is again from top to the bottom (weapons you add first have higher priority). Lets explain this more thoroughly on the example below:

    public void prepareBot(UT2004Bot bot) {	  	  
        // First range class is defined from 0 to 80 ut units (1 ut unit ~ 1 cm)
        weaponPrefs.newPrefsRange(80)
                .add(ItemType.SHIELD_GUN, true);
        // Only one weapon is added to this close combat range and it is SHIELD GUN		
			
        // Second range class is from 80 to 1000 ut units (its always from the previous class to the maximum
        // distance of actual class
        weaponPrefs.newPrefsRange(1000)
                .add(ItemType.FLAK_CANNON, true)
                .add(ItemType.MINIGUN, true)
                .add(ItemType.LINK_GUN, false)
                .add(ItemType.ASSAULT_RIFLE, true);        
        // More weapons are in this class with FLAK CANNON having the top priority		
				
        // Third range class is from 1000 to 4000 ut units - that's quite far actually
        weaponPrefs.newPrefsRange(4000)
                .add(ItemType.SHOCK_RIFLE, true)
                .add(ItemType.MINIGUN, false);
        // Two weapons here with SHOCK RIFLE being the top
				
        // The last range class is from 4000 to 100000 ut units. In practise 100000 is
        // the same as infinity as there is no map in UT that big
        weaponPrefs.newPrefsRange(100000)
                .add(ItemType.LIGHTNING_GUN, true)
                .add(ItemType.SHOCK_RIFLE, true);  	  
        // Only two weapons here, both good at sniping
    }	  
    

If you are confused a little bit by the syntax then be aware that .newPrefsRange(double maxRange) returns the weapon preferences range object as well as the .add(ItemType weapon, boolean bool) method. Line ends doesn't break this chain, so the code above could be rewritten as below and it would be the same (but more confusing perhaps).

    public void prepareBot(UT2004Bot bot) {	  	  		
        weaponPrefs.newPrefsRange(80).add(ItemType.SHIELD_GUN, true);
		
        weaponPrefs.newPrefsRange(1000).add(ItemType.FLAK_CANNON, true).add(ItemType.MINIGUN, true).add(ItemType.LINK_GUN, false).add(ItemType.ASSAULT_RIFLE, true);        
		
        weaponPrefs.newPrefsRange(4000).add(ItemType.SHOCK_RIFLE, true).add(ItemType.MINIGUN, false);

        weaponPrefs.newPrefsRange(100000).add(ItemType.LIGHTNING_GUN, true).add(ItemType.SHOCK_RIFLE, true);  	  		
    }	  
    

The range of ranged weapon preference is defined from the previous ranged weapon preference maximum to current weapon preference maximum. The maximum is defined in newPrefsRange(double maxium) method.

How are these ranged weapon preferences used? The same way as the general weapon preferences (they are defined on the same object). WeaponPrefs object always prefers more concrete before general. That means that if you define ranged weapon preferences and general weapon preferences and you have some weapon that is currently in range, the weapon preferences will prefer this weapon before the general weapon preferences weapon.

    public void logic() {
        // Using ranged weapon preferences is the same as using general weapon preferences
        if (players.canSeePlayers()) {
            Player target = players.getNearestVisiblePlayer();
            shoot.shoot(weaponPrefs, target);
        } else if (info.isShooting())
            shoot.stopShooting();		
    }
    

Now what happens if your bot has defined ranged weapon preferences but no general weapon preferences and none of his ranged weapon preferences weapons is in range? In this case nothing happens - weapon preferences won't force the bot to change the weapon and the bot will fire his current weapon. This example shows that it is rather a good idea to define both weapon preferences - general and ranged. You can set the general weapon suitability in general weapon preferences and tweak only some weapons in ranged weapon preferences. This way even if no weapon is in range, the bot will still have his general weapon preferences to tell him, which weapon is more suitable in general. Putting all of this together you could end up with the following code:

    public void prepareBot(UT2004Bot bot) {	  
        // FIRST we DEFINE GENERAL WEAPON PREFERENCES
        weaponPrefs.addGeneralPref(ItemType.MINIGUN, false);
        weaponPrefs.addGeneralPref(ItemType.LINK_GUN, false);
        weaponPrefs.addGeneralPref(ItemType.LIGHTNING_GUN, true);
        weaponPrefs.addGeneralPref(ItemType.SHOCK_RIFLE, true);
        weaponPrefs.addGeneralPref(ItemType.ROCKET_LAUNCHER, true);
        weaponPrefs.addGeneralPref(ItemType.ASSAULT_RIFLE, true);        
        weaponPrefs.addGeneralPref(ItemType.FLAK_CANNON, true);
        weaponPrefs.addGeneralPref(ItemType.BIO_RIFLE, true);
		
        // AND THEN RANGED
        weaponPrefs.newPrefsRange(80)
                .add(ItemType.SHIELD_GUN, true);

        weaponPrefs.newPrefsRange(1000)
                .add(ItemType.FLAK_CANNON, true)
                .add(ItemType.MINIGUN, true)
                .add(ItemType.LINK_GUN, false)
                .add(ItemType.ASSAULT_RIFLE, true);        

        weaponPrefs.newPrefsRange(4000)
                .add(ItemType.SHOCK_RIFLE, true)
                .add(ItemType.MINIGUN, false);

        weaponPrefs.newPrefsRange(100000)
                .add(ItemType.LIGHTNING_GUN, true)
                .add(ItemType.SHOCK_RIFLE, true);  	  		
    }
	
    public void logic() {
        // Shooting with weapon preferences yet again
        if (players.canSeePlayers()) {
            Player target = players.getNearestVisiblePlayer();
            shoot.shoot(weaponPrefs, target);
        } else if (info.isShooting())
            shoot.stopShooting();		
    }
    

... definining both general and ranged weapon preferences. Note that the preferences above are certainly not the most optimal ones for UT2004.