Timeline

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.

How to create timeline

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.

Log events view of a new timeline

Map view of a new timeline

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.

Possible actions for a timeline

  • Start recording - start recording data into timeline
  • Stop recording - stop recording data into timeline
  • Save - save data from timeline into specified file

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.

  • Dragging mouse with pressed left button - by moving mouse up and down, you move camera forward and backwards, movements in horizontal direction will turn camera to left or right.
  • Dragging mouse with pressed left and right button - mouse movements in horizontal direction will move camera to the left or right, while maintaining angle at which camera looks at the scene, vertical mouse movements move camera up/down and also maintain camera's angle.
  • Dragging mouse with pressed right button - freely change angle of camera.

This is basically the same behavior as in the Unreal editor.

Timeline enables you to record, save and load following data:

  • Unreal map
  • Location, rotation and speed of bots
  • Introspection properties
  • Events in the map
  • Generic events

Introspectable variables

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:

  • Create new timeline
  • Start the modified bot
  • Start timeline recording
  • Kill the bot few times
  • Stop timeline recording

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.

Current time

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.

Events

Another very useful feature of timeline is ability to record events, there are two types of events:

  • Map events - event related with position, or something we want to show in the map
  • Log events - event related to time, often needs context

Map 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:

  • Fixed location, fixed duration - map event is tied to specific place in the map and lasts for specified duration, e.g. if I take the item from spawn point, I know that there won't be a new item at that particular spawn point for duration specified by spawn point.
  • Fixed location, unknown duration - map event is tied to specific place in the map, but there is no information about how long it will last, e.g. flag is at the base: we know the position of base and we know that enemy flag is there, but we don't know when flag will be picked up.
  • Player location, fixed duration - this type of event is shown at position of player for specified duration of event, i.e. no matter where the player will move, the event message will be displayed at his current position.
  • Player location, unknown duration - map event shown at position of player for duration of event. E.g. event "player is hurt".

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

Log events are displayed in time view of timeline. There are following subtypes:

  • Log message - message has no duration, it is instant event, e.g. bot picked up an item
  • Log event, fixed duration - event that lasts for some time, but duration is known at the time of creation
  • Log event, variable duration - we don't know how long will the event last, e.g. bot hurt, lost, state of FSM

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);
    }
                    

  • Time axist at the top is showing time since start of the recording
  • The big green line is showing lifespan of bot, basically when did bot first appeared in the environment and when did he left.
  • The gray line is log line, in our case it is line representing User log, which is the log from getLog(). There can be multiple log lines for every bot.
  • The black square on the log line is 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.
  • The green rectangles are log events, in this particular case log events displaying states of FSM bot. The whole log event message can be shown using tooltip, if rectangle is too small to display it.

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);
        }

    }
                    

Saving and loading

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 Open file... menu item under File menu. Once you select and open the timeline, it adds itself under the Timelines node.