In previous tutorial we have shown how to issue commands using
IAct
interface. 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. These kinds of
events are:
someone/something hit the bot – 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 ways how the bot can detect a change:
actively check state of given object
register a listener and wait until the change occurs
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 tutorial presents both approaches.
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}?"
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. In 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 (eg. player 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.
IWorldView
interface together with
IAct
represent the basic API for accessing the
world, hence you should get familiar with them.
To see how the listeners are registered on bot's world view, find
postPrepareBot()
method. In the body of this
method there are three calls adding different listener types.
protected void postPrepareBot(GameInfo info) { // register all listeners on the WorldView getWorldView().addEventListener(Bumped.class, bumpedHandler); getWorldView().addObjectListener(Player.class, WorldObjectAppearedEvent.class, playerAppearedHandler); getWorldView().addObjectListener(Player.class, WorldObjectUpdatedEvent.class, playerUpdatedHandler); }
Simillarly to getAct()
the
getWorldView()
returns the
IWorldView
implementation associated with this
bot. First line adds a listener for Bumped
event,
this event is raised when the bot hits a wall or some other obstacle.
The listener itself is referenced by bumpedHandler
variable. Second and third line adds object event listeners. Both
listeners are on objects of type Player
, the
second listener will be called each time the player was updated (eg.
when it moved), and the third when player enters bot's field of view.
Note that besides add*Listener(...)
methods
there are also remove*Listener(...)
methods
that let you stop listening for an event.
Now let's explore implementation of listeners that were just
registered. Click the bumpedHandler
variable while
holding Ctrl key to see the listener definition:
/** * Listener called when someone/something bumps into the bot. The bot * responds by moving in the opposite direction than the bump come from. */ IWorldEventListener<Bumped> bumpedHandler = new IWorldEventListener<Bumped>() { public void notify(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(getLocation()).scale(5); Location target = getLocation().sub(v); // make the bot to go to the computed location while facing the bump source getAct().act(new Move().setFirstLocation(target).setFocusTarget(event.getId())); } };
The bumpedHandler
variable is of type
IWorldEventListener
parameterized by the
Bumped
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 simple vector mathematics. Bot is made to move in
an opposite direction to the object that caused the bump event.
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 playerAppearedHandler
definition.
/** * Listener called when a player appears. */ IWorldObjectListener<Player, WorldObjectAppearedEvent<Player>> playerAppearedHandler = new IWorldObjectListener<Player, WorldObjectAppearedEvent<Player>>() { public void notify(WorldObjectAppearedEvent<Player> event) { // greet player when he appears getAct().act(new SendMessage().setText("Hello " + event.getObject().getName() + "!")); } };
You can see that each time a player appears, he is greeted by our
bot. You should already be familiar with the
getAct()...
construct. The only new code is
event.getObject().getName()
. First method call –
event.getObject()
– returns an object that has just
appeared, in this case Player
instance, and the
getName()
call returns the player's name. The
result is that the bot greets player with his name. Now inspect the last
listener:
/** * Listener called each time a player is updated. */ IWorldObjectListener<Player, WorldObjectUpdatedEvent<Player>> playerUpdatedHandler = new IWorldObjectListener<Player, WorldObjectUpdatedEvent<Player>>() { /** * Flag indicating whether the player was also close to the bot last * time it was updated. */ boolean wasCloseBefore = false; public void notify(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(); if (player.getLocation().getDistance(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) { getAct().act( new SendMessage().setText("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, eg. when it
changes location or any other associated property. 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.
Responsive bot's doLogic()
method
demonstrates the second approach that can be taken in event detection
active waiting. Active waiting follows this general schema:
store value of variable X
to variable
lastX.
periodically evaluate X != last
, if it is true
then:
raise event "X
has changed",
lastX = X
,
continue with step 2.
That is exaclty what happens in
doLogic()
, where we want to detect health
decrease, when the health decreases the bot will respond by running away
to a safe location. In order to fully understand this code we have to
discuss querying of objects using the IWorldView
interface.
So far we have used the IWorlView
only for
listening for changes, but the doLogic()
method
introduces another important use case retrieving lists of various
objects present in the world.
protected void doLogic() throws PogamutException {Self self = getWorldView().getSingle(Self.class); int health = self.getHealth(); if (health < lastHealth) { // the bot was injured // find second nearest nav point and run to it double first = Double.MAX_VALUE; double second = Double.MAX_VALUE; double dist = 0; NavPoint fisrtNav = null, secondNav = null; for (NavPoint nav :
getWorldView().getAll(NavPoint.class).values()) { dist = self.getLocation().getDistance(nav.getLocation()); if (dist < first) { second = first; secondNav = fisrtNav; first = dist; fisrtNav = nav; } else { if (dist < second) { second = dist; secondNav = nav; } } } // always check even for improbable conditions if (secondNav == null) { throw new PogamutException("No second closest navpoint found. there are too few navpoints", this); } // issue the command that will make the bot run getAct().act(new Move().setFirstLocation(secondNav.getLocation())); lastHealth = health; } }
In this method we periodically check, whether the bot was injured (his health was decreased) and if so, we presume that this happened because someone fired on the bot and we will try to respond by running to the second closest known safe location. We choose the second location, because it is likely that we are standing on the first closest location so choosing the closest one will in most cases result in standing on the same place.
We can see two distinct uses of IWorldView
for retrieving object:
??? |
We use call
|
??? |
|