Due to unpredictable nature of AI, it is difficult to review behavior of bot, or replicate possible bugs in their logic. In order to overcome this problem, Pogamut has ability to record information about bots in UT2004 and to review what happend and why. This functionality is called timeline. In this section, we will demonstrate how to use timeline utilizing the CTF bot created in previous section.
To create new timeline, you have to be connected to some server. Once you are connected, click on the tree node
and select name of a file, into which the data will be stored (e.g. current_timeline.ptl). After selecting the name, visualization of timeline will appear. The visualization has two views, one for generic log events and the other for map events.
Created timeline doesn't contain any data, you have to manually start and stop data collection. You can do that from context menu of created timeline.
Timeline can't be "concatenated" from multiple discrete intervals, i.e. one timeline can be only started and stopped once.
You can change position and rotation of camera in a map view by dragging mouse.
This is basically the same behavior as in the Unreal editor.
Timeline enables you to record, save and load following data:
Timeline is automatically recording values of introspectable variables, so you can review them later.
In order to declare your variable introspectable, e.g.
// introspectable variable for counting how many times was bot killed @JProp protected int fragged = 0; @Override public void botKilled(BotKilled event) { ++fragged; }
Let's try it out:
Every bot timeline registered during recording is added as a child node of the timeline node in the GUI. In this case, timeline should have recorded two bots, CTFBot and your player, both of them should be displayed as children of the timeline node.
Children of recorded bot are his introspection properties, in our case we have only root introspection folder. Right-click on root node of CTFBot and you will see the fragged property along with count of frags.
The showed number "fragged" in the Properties window is number of frags for the current time of timeline (in this case, last time timeline has recorded new info from environment - time when you stopped recording). Current time can be changed in map view. Switch timeline into map view and note the slider under map. The slider is representing current time of timeline.
Drag the slider and you should see the "fragged" property change to values it had during recording. At the start, it was zero and it has increment few times.
There is a "Play" and "Stop" button. They are for replaying recorded timeline. Change current time to the start and click "Play." The spheres representing the bots will be at move just like they did during recording and "fragged" property will be updated to reflect ammount of kills. Replay will end when current time reaches end of recording or if you click on "Stop" button.
Another very useful feature of timeline is ability to record events, there are two types of events:
Map event is an event that is diplayed in the timeline map view as cube in color of the bot who raised it. Every map event needs to specify event message, location, and duration. There are following subtypes, based on location and duration requirements:
All of these events can be created using methods of
class LogCategory
. Preferrably, use
getLog()
of
UT2004BotController
(ancestor of every UT bot
controller class, such as CTFBot
). If you need
to use another category, get one from
IAgentLogger
(instance for bot can be
retrieved bot
.
getLogger()
).
If you want to create new map event with fixed location and
duration, use addMapMark()
, first parameter
is a level of the event, if level is lower than cutoff level of
logger, event will be discarded.
@Override public void botKilled(BotKilled event) { ... // When bot is killed, show cube at that place for 10 seonds getLog().addMapMark( Level.SEVERE, getAgentInfo().getName() + " was killed here", getAgentInfo().getLocation(), 10000); }
Events with variable duration require more work, because there
can be multiple events with variable duration, you need to specify
which event should end. When event with variable duration is added
using addMapMark(Level level, String eventMessage,
Location location)
(notice missing duration), it returns
object associated with event. The returned object is used to end the
event using removeMapMark(mapMark)
.
private final IWorldObjectEventListener<Self, WorldObjectEvent<Self>> healthEvent = new IWorldObjectEventListener<Self, WorldObjectEvent<Self>>() { private LogMapMark mapMark; public void notify(WorldObjectEvent<Self> event) { if (mapMark == null) { if (event.getObject().getHealth() < 50) { // start new event that will be shown in the map at current position of bot mapMark = getLog().addMapMark(Level.SEVERE, "Low health", null); } } else { if (event.getObject().getHealth() > 50) { // end the event getLog().removeMapMark(mapMark); mapMark = null; } } } }; @Override public void botInitialized(GameInfo info, ConfigChange config, InitedMessage init) { ... // register the listener getWorldView().addObjectListener(Self.class, healthEvent); }
In code sample above, we are using null instead of
Location
object. That is how we decide if event
should be displayed at some distinct location or if it should be
displayed above player sphere in the map.
Following image is showing two events, map event at place where CTFBot has been killed and map event "low health":
Log events are displayed in time view of timeline. There are following subtypes:
To create new log message, use addLogMessage(Level
level, String text)
.
private final IWorldEventListener<AddInventoryMsg> newInventoryMessage = new IWorldEventListener<AddInventoryMsg>() { public void notify(AddInventoryMsg event) { getLog().addLogMessage(Level.SEVERE, "Added inventory " + event.getType()); } }; @Override public void botInitialized(GameInfo info, ConfigChange config, InitedMessage init) { ... getWorldView().addEventListener(AddInventoryMsg.class, newInventoryMessage); }
getLog()
. There can be multiple log lines
for every bot.log
message
that was put into the repsective log. Because
log message is only small square, whole message is shown in a
tooltip. It is convinient to create new log for specific type of
messages to avoid cluttering in the User log line.
Log event with fixed duration is created using
addLogEvent(Level level, String text, long
duration)
, log event with variable duration are started
by calling addLogEvent(Level level, String
text)
. That method returns object identifying the event,
which is used for ending the event in the same fashion as map events
do (use removeLogEvent(LogEventMark)
instead
of removeMapEvent(LogMapMark)
).
Following code will create variable log events for FSM states you could see in description of timeline view.
private String currentState; private LogEventMark currentStateEvent; @Override public void logic() throws PogamutException { // make a step in execution of FSM, run statem check transitions, if necessary // switch from one state to another... // Passed value is fsm context. fsmExecutor.step(this); IFSMState<CTFBot> state = fsmExecutor.getState(); if (!state.getName().equalsIgnoreCase(currentState)) { currentState = state.getName(); if (currentStateEvent != null) getLog().removeLogEvent(currentStateEvent); currentStateEvent = getLog().addLogEvent(Level.SEVERE, currentState); } }
The key aspect of timeline is ability to save and load recorded data.
You have probably noticed the node Timelines
in the project
tab, it is the node under which are
avaiable all timelines that have been created during current Pogamut
session, meaning all timelines under all servers and all loaded
timelines as well.
In the image, there is a timeline created during session (localhost_timeline) and one loaded timeline (sample_timeline_12).
Saving timeline is done by selecting the "Save" menu item from context menu of the respective timeline.
Loading is done through the standard Netbeans Timelines
node.