This tutorial can be applied to PogamutUT2004 and will mostly hold for PogamutUDK examples.
This example is installed by Pogamut UT2004 installer. In NetBeans click New Project -> Maven -> Project From Archetype -> Local Archetypes Catalog and select 01-responsive-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 (if the archetype is not present, follow "adding new Pogamut example project" section in the same chapter). This archetype information is below.
For UT2004 example:
cz.cuni.amis.pogamut.ut2004.examples
01-responsive-bot-archetype
3.3.1
http://diana.ms.mff.cuni.cz:8081/artifactory/repo
For UDK example only change
to cz.cuni.amis.pogamut.udk.examples and 3.2.5-SNAPSHOT . The rest remains the same. You will find up-to-date list of available archetypes inIn previous tutorial we have shown how to issue commands using
IAct
interface or by command modules (body
). In this tutorial we will learn how
to create a simple bot that will be able to sense the surrounding world
and to react to several kinds of events that can occur. The bot behavior will be completely event-driven,
i.e., we won't utilize logic()
method.
The events we will react to are:
someone/something hit the bot (bumps to it) – bot will move in an opposite direction
player appeared in bot's field of view – bot will greet the player
player approached the bot – bot will ask him, what does the player want
bot was injured – he will try to run away
To handle these events we have to have some mechanism that will notify us about changes in the environment. In general, there are two+one ways how the bot can detect a change:
actively check state of given object
register a listener and wait until the change occurs
utilize method annotations and let the listeners be registered automatically using Java Reflection API, note that this method is similar to previous one but is more convenient (easier to use)
The listener design pattern should be used in favour of active waiting since it is usually more computationaly effective. However sometimes the underlying API doesn't provide a way for registering listeners on every possible event so active waiting is the only choice, this is usually true for all custom variables you declare inside your bot as Java does not allow you to sense changes on arbitrary variable. This tutorial present only the last two approaches, i.e., how to utilize listeners to various events.
Before we will inspect the source code we will observe bot's behavior in game:
Start UT.
If you are in spectator mode (this will happen if you start UT from Netbeans) then press Esc and click button, now you are connected as a standard player.
Find the bot and move close to him, these messages will be printed:
"Hello {YOUR_NAME}!"
"What do you want {YOUR_NAME}?"
If you don't see the messages, you have probably chat visualization turned off in UT2004. However, you will always see the messages in console (press tilde (~) character) or in the text bubble that can be turned on by pressing CTRL + U.
Now bump to the bot, he will move away from you.
If you do not have any weapon then find some and return back to the bot (you can switch between multiple weapons using mouse wheel or number keys on your keyboard).
Shoot the bot, he will move to avoid the fire.
Before we will register our first listener, we have to understand
Pogamut's abstraction of the world where the bot lives. Bot's view of
the world is provided through the IWorldView
interface. This interface serves as both bot's senses and a simple
memory. The abstraction used by IWorldView
the
world is represented by:
Objects (IWorldObject
) – eg. players,
items etc.
Events (IWorldEvent
) – eg. bot heard a
noise, bot bumped to a wall etc. Events are divided into two
categories:
Object events (IWorldObjectEvent
) –
there are five events of this type:
First encountered
(WorldObjectFirstEncounteredEvent
) –
raised when the bot encounters some object (eg.
Player
) for the first time.
Appeared
(WorldObjectAppearedEvent
) – object
entered bot's field of view.
Updated
(WorldObjectUpdatedEvent
) – object's
state was updated.
Disappeared
(WorldObjectDisappearedEvent
) –
object disappeared from bot's field of view.
Destroyed
(WorldObjectDestroyedEvent
) – object
was destroyed (e.g., the player has been disconnected from the
game).
Events not associated with any object (plain
IWorldEvent
) – example of such event can
be HearNoise
that is raised when the bot
hears some noise or BotBumped
that is sent
whenever the bot bumps into something (or someone bumps to the bot).
IWorldView
interface together with
IAct
represent the basic API for accessing the
world, hence you should get familiar with them.
There are two ways how to register listeners. Either you may annotate any method with one of the annotation
EventListener
, ObjectEventListener
, ObjectClassEventListener
,
ObjectClassEventListener
or ObjectListener
, or you may manually
register a listener via IWorldView
interface using one of the getWorldView().addEventListener()
or
getWorldView().addObjectListener()
methods.
To see how the listeners are registered on bot's world view, find
prepareBot()
method. In the body of this
method there is an example of manual listener registration.
public void prepareBot(UT2004Bot bot) { // register the botDamagedListener that we have previously created getWorldView().addEventListener(BotDamaged.class, botDamagedListener); }
Simillarly to getAct()
the
getWorldView()
returns the
IWorldView
implementation associated with this
bot. The first line adds a listener for BotDamaged
event,
this event is raised when the bot is hurt by somebody or something.
The listener itself is referenced by botDamagedListener
variable.
Now let's explore implementation of the listener that was just
registered. Click the botDamagedListener
variable while
holding Ctrl key to see the listener definition:
/** * Listener that is manually created and manually hooked to the {@link ResponsiveBot#getWorldView()} * via {@link IWorldView#addEventListener(Class, IWorldEventListener)} method * inside {@link ResponsiveBot#prepareBot(UT2004Bot)}. */ IWorldEventListener<BotDamaged> botDamagedListener = new IWorldEventListener<BotDamaged>() { @Override public void notify(BotDamaged event) { // the bot was injured - let's move around the level and hope that we will find a health pack // note that we have to acquire "SECOND" nearest navpoint, as the first one is the navpoint we're standing at NavPoint secondNav = DistanceUtils.getSecondNearest(getWorldView().getAll(NavPoint.class).values(), info.getLocation()); // always check even for improbable conditions if (secondNav == null) { // try to locate some navpoint move.turnVertical(30); } else { // move to it move.moveTo(secondNav); } } };
The botDamagedListener
variable is of type
IWorldEventListener
parameterized by the
BotDamaged
class.
IWorldEventListener
interface declares one
abstract method notify(...)
that is called each
time the event occurs with the event as the method's parameter. Body of
this method implements a way how to find the second nearest navpoint and run to it in hope
it will be reachable and far from the danger.
Now return back to the code block showing the listeners'
registration. You can press Alt + ← to get to the
previous position in the source code. Then again use Ctrl +
LMB to go to the botDamagedListener
definition.
Now for the second way of listener registration - more easier - annotations. There are three methods in the example
that are called when some event occurs: bumped(Bumped event)
,
playerAppeared(WorldObjectAppearedEvent<Player> event)
and
playerUpdated(WorldObjectUpdatedEvent<Player> event)
. Let's check how this magic
- auto method calling - happens.
The class UT2004BotModuleController
that is an ancestor of the ResponsiveBot
(and all other examples as well) is auto-initializing the AnnotationListenerRegistrator
. Now,
because you know how we may manually hook a custom listener to the world view, you might already have guessed what
the registrator is doing. It simply iterates through all methods the ResponsiveBot
is declaring
and searches for methods annotated either with EventListener
or ObjectEventListener
or ObjectClassEventListener
or
ObjectClassEventListener
or ObjectListener
annotation (that is the
magical @EventLister that is present above bumped(Bumped event)
method for instance).
Every such annotation refers to a certain category of events (it corresponds with methods of the world view that
you may manually use to register a listener):
EventListener
- reacts to one arbitrary IWorldEvent
that
is defined via eventClass
field of the annotation. I.e., to BotDamaged
or Bumped
events.
ObjectClassListener
- reacts to all events that happens on the IWorldObject
of the certain class that is defined via objectClass
field of the annotation. I.e.,
it allows you to receive all events regarding some object (as desribed earlier in the text - WorldObjectFirstEncountered
, ...).
ObjectClassEventListener
- reacts to events of the certain class that happens
on any IWorldObject
of a certain class. I.e., you may for instance react only to
WorldObjectUpdated
events that happens over objects of the Player
class,
such as the playerUpdated(WorldObjectUpdatedEvent<Player> event)
method is doing.
ObjectListener
- reacts to all events that happens over the certain object
that is identified by the string id (of course this is hard to use as it is hard to obtain the correct
string id, but we leave it there for the completness of the annotation solution).
ObjectEventListener
- reacts to all events of the certain class that happens
over the certain object that is identified by the string id (again, hard to use, but...)
For the concrete implementations:
/** * Listener called when someone/something bumps into the bot. The bot * responds by moving in the opposite direction than the bump come from. * * We're using {@link EventListener} here that is registered by the {@link AnnotationListenerRegistrator} to listen * for {@link Bumped} events. */ @EventListener(eventClass = Bumped.class) protected void bumped(Bumped event) { // schema of the vector computations // // e<->a<------>t // | | v | // | | target - bot will be heading there // | getLocation() // event.getLocation() Location v = event.getLocation().sub(bot.getLocation()).scale(5); Location target = bot.getLocation().sub(v); // make the bot to go to the computed location while facing the bump source move.strafeTo(target, event.getLocation()); }
The bumped(Bumped event)
method is
annotated by the @EventListener(eventClass = Bumped.class)
that means
that the method gets called whenever the Bumped
message is sent by the GameBots2004
to the bot (note how the field eventClass
inside the annotation is used to specify which
event you want to listen to and then the same class bumped
appears in the method
declaration.
The method just performs simple vector arithmetic to obtain a vector where to run to to stay away from the source of the collision.
/** * Listener called when a player appears. * * We're using {@link ObjectClassEventListener} here that is registered by the {@link AnnotationListenerRegistrator} * to listen on all {@link WorldObjectAppearedEvent} that happens on any object of the class {@link Player}. I.e., * whenever the GameBots2004 sends an update about arbitrary {@link Player} in the game notifying us that the player * has become visible (it's {@link Player#isVisible()} is switched to true and the {@link WorldObjectAppearedEvent} * is generated), this method is called. */ @ObjectClassEventListener(eventClass = WorldObjectAppearedEvent.class, objectClass = Player.class) protected void playerAppeared(WorldObjectAppearedEvent<Player> event) { // greet player when he appears body.getCommunication().sendGlobalTextMessage("Hello " + event.getObject().getName() + "!"); }
The playerAppeared(WorldObjectAppearedEvent<Player> event)
method is
annotated by the @ObjectClassEventListener(eventClass = WorldObjectAppearedEvent.class, objectClass = Player.class)
again see how the fields eventClass
and objectClass
are used and how they correspond
with the method parameters declaration.
The method is truly simple. You can see that each time a player appears, he is greeted by our
bot. You should already be familiar with the
body
module that contains the most commands for the bot.
Now inspect the last listener:
/** * Listener called each time a player is updated. * * Again, we're using {@link ObjectClassEventListener} that is registered by the {@link AnnotationListenerRegistrator} * to listen on all {@link WorldObjectUpdatedEvent} that happens on any object of the class {@link Player}. I.e., * whenever the GameBots2004 sends an update about arbitrary {@link Player} in the game notifying us that some information * about the player has changed (the {@link WorldObjectUpdatedEvent} is generated), this method is called. */ @ObjectClassEventListener(eventClass = WorldObjectUpdatedEvent.class, objectClass = Player.class) protected void playerUpdated(WorldObjectUpdatedEvent<Player> event) { // Check whether the player is closer than 5 bot diameters. // Notice the use of the UnrealUtils class. // It contains many auxiliary constants and methods. Player player = event.getObject(); // First player objects are received in HandShake - at that time we don't have Self message yet or players location!! if (player.getLocation() == null || info.getLocation() == null) return; if (player.getLocation().getDistance(info.getLocation()) < (UnrealUtils.CHARACTER_COLLISION_RADIUS * 10)) { // If the player wasn't close enough the last time this listener was called, // then ask him what does he want. if (!wasCloseBefore) { body.getCommunication().sendGlobalTextMessage("What do you want " + player.getName() + "?"); // Set proximity flag to true. wasCloseBefore = true; } } else { // Otherwise set the proximity flag to false. wasCloseBefore = false; } }
This listener is called each time a player is updated, it uses the same annotation as the previous one
with different arguments. When the player that is visible changes it locatuion (or any other associated property)
this method gets called. When the update event
is raised we check whether the player is closer than certain threshold,
in this case UnrealUtils.CHARACTER_COLLISION_RADIUS * 10
.
If this condition holds and it didn't hold last time the listener was
called then it means that the player came closer to the bot than he was
a while before. In that case the bot will ask him what does he
want.