Smaller update today, as I’m working on a refactoring of the load/save paths to support multiple levels and multiple characters within both pause/resume (tombstone) and launch/exit scenarios. I’ve got most of it on paper, but it’ll take more than a day to revamp things. In the meantime, I added a new Action.

Rest Action

Occasionally you want your character to sit still for a turn; this might be to regenerate health, or it might be to allow a monster to step closer (giving you first-attack advantage).  To support this, I added the Action_Rest class; users trigger it by tapping on their character.

What’s interesting here is Action_Rest pushes the minimum-extreme of the Event system; there’s about 90 lines of code below that ultimately get to the point of doing the action, at which point we do: nothing.  We just let a Turn pass.  Which begs the question – why in the world go through all of that effort; can’t you just, I don’t know, return or something?

There are a set of factors which drive this model:

  • Actions don’t occur immediately; they get enqueued and are popped when it’s the Actor’s time to take a turn.  Thus we need the Create function rather than just performing the action immediately
  • Actions are pooled objects; this is to minimize the number of allocations that need to happen.  This necessitates the object pool code and the Release codepath
  • When it’s time to perform the action, we don’t actually just ‘do’ the action, because we want the Event subsystem to hook into it and give anything the ability to modify/cancel the Action.  Imagine a block of Cells with a CellEffect that disallows Resting, or opens a door if the user Rests, or zaps them with lighting.  This is why Perform creates a new Event_Rest object and fires the event.
  • Events are also pooled objects, so we need to create/Release them as well.
  • Finally, if everything worked, we get to the actual DoRest function which performs the critical task of doing nothing.

Action_Rest is a little different than Action_Move in that it’s a relatively rare action, so object pooling may not be necessary here.  Other Actions are likely to be rare as well, so I may eventually adopt a second design pattern for Actions in which there’s no pooling.  For now though, I figure there’s actually less room for bugs if I follow the same pattern everywhere.

Things I’d love to figure out but couldn’t:

  • How to move the object pooling infrastructure up into the base ActorAction class, rather than repeating it in each Action class.  I couldn’t figure out how to do that with the Create factory that each Action implements.  I suppose I could create a static ActorAction.CreateAction() function which takes an enum e.g. (ActionType.Move) and does a big switch to gen up the correct derived type.  It’s less object oriented-y, but maybe that would localize the grossness and cleanup the actions and events themselves.  I’ll try that later and see if it looks better.
  • How to move Perform up and into the base ActorAction class.  There’s a lot of boilerplate code in each Action’s Perform function, but there’s a little bit of custom per-Action code in each as well.  Ah, maybe this is it: Move the creation of the custom event (Event_Rest in the code below) into the Action_Rest constructor, and then ActorAction.Perform() could act on it in an Event-agnostic fashion.  I like that; I’ll try refactoring things to see if that works.  Yet again, the blog pays off :).

Here’s the code (there’s also a corresponding Event_Rest class which is basically empty so I’ve left it out):

    /// <summary>
    /// Whenever an Actor wants to rest, it sets its next action to a new RestAction.
    /// </summary>
    public class Action_Rest : ActorAction
    {
        // pool: RestActions are pooled objects; this stores the set of previously allocated objects
        static ObjectPool<Action_Rest> pool = new ObjectPool<Action_Rest>();

        /// <summary>
        /// Creates a new RestAction object; uses the object pool to minimize allocations.
        /// </summary>
        /// <param name="performingActor">Actor performing the rest</param>
        static public Action_Rest Create(Actor performingActor)
        {
            // get free object; create a new one if none exists
            if (pool.IsEmpty)
                return new Action_Rest(performingActor);

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

            // Track who's performing the Action
            obj.PerformingActor = performingActor;

            return obj;
        }

        /// <summary>
        /// Releases the specified RestAction object back into the object pool
        /// </summary>
        /// <param name="obj">RestAction object to release back into the object pool</param>
        static public void Release(Action_Rest obj)
        {
            pool.Release(obj);
        }

        /// <summary>
        /// Constructor is private to force caller to go through the static Create function.
        /// </summary>
        /// <param name="performingActor">Actor performing the rest</param>
        private Action_Rest(Actor performingActor)
            : base(performingActor)
        {
            // Track who's performing the Action
            PerformingActor = performingActor;
        }

        /// <summary>
        /// Performs the Action
        /// </summary>
        override public void Perform()
        {
            // After we perform the Rest, there's no next action
            PerformingActor.SetNextAction(null);

            // Rather than just doing the action, "Perform" fires an Event, which allows the eventing
            // system to hook into the action, possibly modifying or stopping it.

            // Create the Rest Event
            Event_Rest evt = Event_Rest.Create(DoRest);

            // Fire the actual event.  If it is allowed, then it will get to the callback (DoRest) specified
            // in the Event
            EventMgr.FireEvent(evt, PerformingActor, null);

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

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

        /// <summary>
        /// Do the actual Rest (finally!)
        /// </summary>
        /// <param name="evt">Information about the Rest action</param>
        /// <param name="performerObject">who's performing the Rest</param>
        /// <param name="target">Ignored (doesn't impact this Action)</param>
        public void DoRest(Event evt, GameObject performerObject, GameObject target)
        {
            // Yeah, believe it or not, after aaaaall that, we don't actually do anything!  We
            // went through all that to enable hooking into the Event system.
        }
    }

I’ve got some of the load/save refactoring in the code already, so no new code drop today (unless I can get that wrapped up tonight).

Cheers,
Jeff

Advertisements