Thursday, July 5, 2012

EPI #12: Events, part 1

Before I'm done with my maps, I need to implement an event system.  What is that, exactly?  The event system is the interface by which designers will direct the game engine to do things in response to player actions.  In simpler English, the event system is how designers tell the game when to do stuff and what stuff to do.  The event system is made up of two main parts: events and listeners.  Events are (generally) generated by player activity.  Listeners specify a kind of event to listen for, and an action to take.

So, naturally, I made a class EPIEvent:
  • EPIEvent
    • source - the object the event originated from
    • type - the kind of event
    • target - the event's target (if it's a targeted event)
I'll get to Listeners in a moment.  EPIEvents are really simple, but this will probably make more sense if I give an example.  The only kind of event I have implemented so far is the "move" event.  This occurs whenever the player attempts to move on a map.  Whenever the player press a key that's bound to a direction to move in, the key_press_handler figures out the direction, calls the move() function, and then emits an EPIEvent.  This EPIEvent's source is the map that handled the move() call, its type is "move", and its target is the player party's new location.  The move event is an example of a targeted event-- it's only relevant to the target tile.  In this case, the move event is conveyed to the map on which it occurred, which may or may not have an EPIEventListener attached to the appropriate tile.  Other events, such as the "level_up" event, are untargeted.  Untargeted events are conveyed to every entity in the game.  If you're wondering how exactly the event "emission" is handled, don't worry (you're in good company; so did I), I'll get to that in a moment.  But first I need to talk about EPIEventListeners.

Events are only one half of the event system.  While events tell the engine when to do something, listeners receive events and tell the engine what to do.  class EPIEventListener:
  • EPIEventListener
    • verb - the category of action to take
    • direct object(s), depending on the verb
    • prepositional phrase(s), depending on the verb
Listeners also have two parts-- the event they're listening for, and the action they take.  EPIEventListeners don't actually keep track of event they're listening for themselves because they're stored in a dictionary of EPIEventListener lists indexed by event type.  It's a bad idea not to have EPIEventListeners keep track of the conditions under which they evaluate their action.  I'll probably fix this as soon as I'm done writing this post.

As far as actions are concerned, I've chosen a natural language approach to defining listener actions.  There will be a finite list of verbs designers can specify, and the rest of the listener action definition depends on the verb.  An example of this is the CHANGE_MAP verb (which, coincidentally, is the only one I've implemented so far).  The archetype for a CHANGE_MAP action looks like this:  CHANGE_MAP AT [src_x] [src_y] TO [dest_x] [dest_y] IN [path].  This has the effect of moving the player party to (dest_x, dest_y) on the map specified by path when they're at (src_x, src_y) in the current map.

This is a terrible example for a couple of reasons.  First, the CHANGE_MAP verb doesn't have a direct object.  Instead, it has three prepositional phrases.  This is unusual; I expect most of the verbs I implement to take a direct object of some sort.  Second (and more important), this CHANGE_MAP archetype tries to assert something about the condition under which it happens in the AT [src_x] [src_y] prepositional phrase.  This is problematic because what if, for example, I want to CHANGE_MAP in response to an event which has no location associated with it?  At best, the CHANGE_MAP would never happen because it wouldn't be able to confirm that the player party was at (src_x, src_y).  At worst, it would crash the game.

A note on listener action definition parsing:  Since listener action definitions essentially amount to a very simple scripting language, they're an ideal application for a grammar.  If I wanted to parse them right (and make the most of formal Computer Science training), I'd define a grammar for them and then write a class to parse that grammar and attach semantics to it.  However, given the scope of my project, this approach requires way too much effort. Instead, I parse these with a combination of switch statements and regular expressions.

Currently, XML listener definitions look like this:

<listener type='EVENT_TYPE'>ACTION_ARCHETYPE</listener>

An example:

<listener type='move'>CHANGE_MAP AT 0 1 TO 11 2 IN ./content/test/map_02/map_02.xml</listener>

Sorry about the line wrapping.  In order to fix the problem I mentioned last paragraph, I'll need to change the XML listener definition to look more like this:

<listener type='EVENT_TYPE' [arg_key_1='ARG_VAL_1' arg_key_2='ARG_VAL_2' ...]>ACTION_ARCHETYPE</listener>

The stuff in brackets is optional and depends on EVENT_TYPE.  The example, translated to the new format:

<listener type='move' x='0' y='1'>CHANGE_MAP TO 11 2 IN ./content/test/map_02/map_02.xml</listener>

Again, I'll implement this change when I'm done writing.

Throughout this post I've been talking about how player actions cause events to be "emitted" and how listeners "receive" events and act on them.  Even though these concepts are relatively simple, implementing them in code turns out to be a challenging task.  One of the hallmarks of good class design is keeping the class aware only of the information it needs to do its job.  Events, however, need to be communicated to every single thing in the game that's capable of having a listener attached to it.  These two goals are somewhat at odds with each other.  One option is to make a global list of everything that can listen for events.  That way, whatever's signaling an event can just reach into the list and pass its event on to each object in turn.  However, this also requires all of these objects to be capable of handling the events they receive.  Also, global variables are generally frowned upon.  Instead, I wrote class EPIEventHandler:
  • EPIEventHandler
    • entities - a list of all the MapEntitys in the game.  As I realize that there are more things that can receive events, I'll need to add them.
    • handle_event(EPIEvent)
    • eval_listener( EPIEvent, EPIEventListener)
After the MGEET initializes the scenario, it creates an EPIEventHandler and gives it to things which can emit events as it creates them.  This way, when different widgets need to emit an event, they can just access their reference to the EPIEventHandler and pass an appropriate EPIEvent to handle_event().  Also, this approach abstracts listener interpretation into the EPIEventHandler.  Where before entities would have had to be responsible for handling their own events, now the EPIEventHandler is responsible for evaluating listeners, and entities only need to keep track of which listeners are their own.

Right.  So that's an introduction to the events.  I apologize if is confusing to the point of making no sense.  I'm still a little confused about it myself.  I'm gonna go implement the changes I talked about in this post, and come back with a working example and some screenshots!

No comments:

Post a Comment