Although it takes time away from coding, writing this process up is already bearing fruit. Sharing the code is forcing me to be a better coding citizen (e.g. actually commenting how stuff works), and describing the inner workings forces me to rationalize decisions. In this case, the process of writing up how Events work made me realize that I had an entirely unnecessary level of abstraction (derived EventInfo classes). I found I was able to simply collapse EventInfos into derived Event classes, and remove a whole object type with no extra chaff. So, even though no one is actually reading this: thank you :).

Taxonomy

Action: something that an Actor wants to Perform.  When the user taps on a Monster, an “Attack Action” will be performed; when a Monster wants to move, a “Move Action” will be performed.
Event: An occurrence that can be triggered (Fired) . When fired, Events travel through the EventMgr, allowing environmental elements (Level, Cell, Performer, and Target) to modify or stop the Event from occurring. These are distinct from Action because Events can occur unrelated to Actions – e.g. a Timed Event on a Level where after 20 turns, doors open and monsters come pouring out…
Object Pool: because Events and Actions can occur a lot, I’ve leveraged an object pool to manage allocation.  It complicates the code a bit, but it greatly reduces the number of allocations that happen each turn.  I’ll discuss the object pool in another post.

Steps in performing an Action

What happens when the user wants to perform an Action?  Answer: A bunch.  This looks like a lot of code just to move from one Cell to a neighboring Cell, but it provides the basis for a pretty powerful infrastructure in which events can be hooked, modified, and even cancelled by outside factors.  e.g. if the player performs a “move action,” a Cell could have an effect on it which reduces movement speed; or if a player tries to perform magic, a Level could have a restriction that disallows any magic-based events.  This is leveraged more comprehensively by the Attack Action (which will be broken into a series of hookable events: WeaponAttack, AttemptStrike, DoDamage, …), each of which can be hooked and modified by environmental effects.

For example, here’s the series of things that occur when the user wants to perform a MoveAction.

First, the desire to perform the event occurs; e.g. user performs a user action (e.g. move left one cell) or monster decides on an action (attack player).
This is done via MoveAction.Create, which initializes and returns an Action object. This Action object has not yet been acted upon; simply created:

/// Whenever an Actor wants to move, it sets its next action to a new MoveAction.
public class MoveAction : ActorAction
{
    // pool: MoveActions are pooled objects; this stores the set of previously allocated objects
    static ObjectPool pool = new ObjectPool();

    /// Creates a new MoveAction object; uses the object pool to minimize allocations.
    static public MoveAction Create(Actor performingActor, Direction moveDir)
    {
        // get free object; create a new one if none exists
        if (pool.IsEmpty)
             return new MoveAction(performingActor, moveDir);

        // Get the next free object from the pool
        MoveAction obj = pool.GetFreeObject();

        // Initialize the move information
        obj.Initialize(performingActor, moveDir);

        return obj;
    }

    /// Initializes the Action
    private void Initialize(Actor performingActor, Direction moveDir)
    {
        // Track who's performing the Action
        PerformingActor = performingActor;

        // Track which direction we're moving
        MoveDir = moveDir;
    }
    ...
}

2) Second, the newly created Action is set as the Next Action that the Actor wants to Perform

Direction nextDir = ExplorePath.Pop();
SetNextAction(MoveAction.Create(this, nextDir));

This doesn’t actually perform the action, it instead enqueues it. This will then be popped when it’s the Actor’s turn to perform an action. Eventually I want to move to an energy-based system in which an Actor may be able to perform multiple actions before other Actors.

    public class Actor : GameObject
    {
        public void SetNextAction(ActorAction nextAction)
        {
            NextAction = nextAction;
        }
	...
}

3) Third, at some point (for now: the next world turn), the Actor is told that it can perform its next action. Level’s update function:

    internal void Update()
    {
        // simplied action handling; everyone gets a turn.
        // tbd-later: energy-based action handling
        foreach (Actor actor in Actors)
        {
            actor.DecideNextAction();
            if (actor.NextAction != null)
                actor.PerformNextAction();
       }
    }

And the Actor class’ PerformNext action simply calls Perform on the NextAction:

    virtual public void PerformNextAction()
    {
        // perform the action
        NextAction.Perform();
    }

4) Fourth, to Perform an Action, we fire an Event signifying that the Action is occurring – this allows the Eventing system to hook into the Action and modify or disallow it if so desired. e.g. a particular Level might disallow magic, and the Eventing system would allow it catch any events of type ‘Magic’ and disallow them. Another example: a Cell might have a “buff damage” effect on it – the Cell could modify “AttackAction” events to increase the amount of damage done…
a. We capture specific information about the Action in a derived Event object; this allows the Event system to manage it agnostically:

    // Create the Move Event
    Event_Move evt = Event_Move.Create(MoveDir, DoMove);

b. We then fire the event (more details below)

    // Fire the actual event.  If it is allowed, then it will get to the callback (DoMove) specified
    // in the EventInfo structure that was passed to the Event constructor
    EventMgr.FireEvent(evt, PerformingActor, null);

c. And then we clean up after ourselves (since our objects are pooled):

    // Events are pooled objects (to reduce allocations) - so they need to be
    // released back into the pool.
    Event.Release(evt);

    // Finally; MoveActions are also pooled objects - so release this one back into the wild as well.
    Release(this);

5) Fifth, the actual firing of the Event (performed by TurnMgr.Update, seen above), which will eventually allow hooking (but is mostly commented out now). Note that firing an Event can also fire other Events, which is why the below is implemented as a Queue. Multiple objects can impact the event – the Level, the cell, the performer, and the target. Each object can modify or filter the event. If the event gets through unfiltered, then it’s finally fired at the bottom.

class EventMgr
{
    static public void FireEvent(Event evt, GameObject performer, GameObject target)
    {
        Queue evts = new Queue();
	evts.Enqueue(evt);
	while (evts.Count > 0)
	{
	    Event currentEvent = (Event)evts.Dequeue();

	    // let Map impact the event.  Returning false == event blocked from continuing
	    //tbd-later... if (!GameMgr.Map.HandleEvent(ref currentEvent))
	    //    continue;

	    // Let the performer's location (Cell) impact the event.  Returning false == event blocked from continuing
	    //if (performer != null && (performer.Location == null || !performer.Location.HandleEvent(ref currentEvent, performer, target)))
	    //    continue;

	    // Let the target impact the event.  Returning false == event blocked from continuing
	    //if (target != null && !target.HandleEvent(ref currentEvent))
	    //    continue;

	    // Let the performer impact the event.  Returning false == event blocked from continuing
	    //if (performer != null && !performer.HandleEvent(ref currentEvent))
	    //    continue;

	    // Event has been allowed to occur (and the currentEvent structure has been updated to reflect any
	    // impacts from the Map, target, or performer).  So perform the event's actions
	    currentEvent.Fire(performer, target);
	}
    }
}

6) Finally, if the Action’s Event is allowed to pass through unfiltered, we get to the actual Firing of the event, which causes the Action to be performed:

    /// Do the actual Move (finally!)
    public void DoMove(Event evt, GameObject performerObject, GameObject target)
    {
        Actor performer = performerObject as Actor;
        Event_Move moveEvent = evt as Event_Move;

        // Get the cell that's in the direction of the Move
        Cell destCell = performer.Location.Neighbor(moveEvent.MoveDir);

        // Make sure the destination is a valid location (e.g. Floor); otherwise fail to Move
        if (!destCell.CanWalkOn())
        {
            if (destCell.CellType is CellType_Door)
            {
                OpenDoorAction action = OpenDoorAction.Create(performer, moveEvent.MoveDir);
                action.Perform();
            }
            else
                performer.MoveMode = MoveMode.NotMoving;
        }
        else
            performer.Level.MoveObject(performer, destCell);

        // If the Actor is in 'explore' or 'MoveTo' mode, then continue moving
        if (performer.MoveMode != MoveMode.NotMoving)
            performer.ExploreNextStep();
    }

That’s a lot of work (even more so when you realize that a simply action like “Action_RestOneTurn” will go through much of the same work. However, it’s worth it as the infrastructure will allow easy addition of complex features later on (e.g. CellEffects, Skills (which will also trigger events), etc).

Here’s the updated source code with the EventInfo class deprecation: http://wanderlinggames.com/files/dungeon/dungeon-3-14b-11.zip

Advertisements