Action lists by themselves are very powerful. As I hopefully showed in part 1, they provide an easy way to chain multiple actions together to form more complex and interesting behavior. But, there are often types of actions that execute in similar ways. If we have an action that scales an object over time, as well as an action that changes an object’s color over time, we have two actions that are repeating the same kind of timing logic. If the time-based logic is abstracted and shared among all actions of that type, it makes things a lot simpler to use, as well as less error-prone. In this post, I’ll show how the interface for these actions can be formed, as well as show some examples of actions that I’ve found useful.

« Read Part 1

Interval Actions

Many times, you’ll want an action that runs for a given amount of time. Creating an action for sliding a menu in from the edge of the screen, or fading a character’s sprite in and out can become much simpler when working with actions instead of managing a timer manually. Here’s where sharing a common interface will really cut down on duplication, and therefore bugs.

 
/* an action that executes over a given interval */
class interval_action : public action
{
  public:
    // ctor
    interval_action(float time);

    // virtual methods
    virtual void on_startup(void) {}
    virtual void on_update(float interval) {}
    virtual void on_shutdown(void) {}

    // final methods
    void startup(void) final
    {
      _elapsed = 0.0f;
      on_startup();
    }

    void update(float dt) final
    {
      _elapsed += dt;

      // calculate percentage completed
      float interval = _elapsed / _total_time;

      // update action if not done
      if (interval < 1.0f)
        on_update(interval);
      else
        set_finished();
    }

    void shutdown(void) final
    {
      on_shutdown();
    }

  private:
    // private members
    float _total_time;
    float _elapsed;

}; // class interval_action

Here, we're simply overriding the startup, update, and shutdown functions of the action class. If C++11 is available, it's useful to mark these methods with the final specifier to prevent any accidental overriding. These methods keep track of a timer and increment its elapsed time every update. Then, we simply calculate the percentage the action is done stored in a value between 0 and 1, and pass that to a new virtual function.

Example Usage

Here is a simple example that moves an object with a transform by a given amount over a set amount of time.

 
/* moves a transform by a given amount */
class move_by : public interval_action
{
  public:
    // ctor
    move_by(transform& trans, const vec3& delta_pos, float time)
      : interval_action(time)
      , _trans(trans)
      , _delta_pos(delta_pos)
    {
    }

    // overriden methods
    void on_startup(void) override
    {
      _start_pos = _trans.position;
    }

    void on_update(float interval) override
    {
      _trans.position = _start_pos + interval * _delta_pos;
    }

    void on_shutdown(void) override
    {
      _trans.position = _start_pos + _delta_pos;
    }

  private:
  // private members
  transform& _trans;
  vec3 _start_pos;
  vec3 _delta_pos;

}; // class move_by

Instant Actions

We can create a similar class that handles actions that only execute once, such as an action that calls a function, or sets a value. I like to call these instant actions, because they perform their task and finish as soon as they are started.

 
/* an action that completes as soon as it's executed */
class instant_action : public action
{
  public:
    // virtual methods
    virtual void execute(void) {}

    // final methods
    void update(float) final
    {
      execute();
      set_finished();
    }

}; // instant_action

As you can see, the interface for instant_action is extremely trivial. In fact, one could argue that it's not a necessary addition at all. However, it defines a clear interface for this type of action. Instead of trying to keep track of whether or not to put the executing code inside of the action's update or startup, using this interface keeps everything clean and consistent.

Example Usage

Here is an example of an instant_action that destroys an object. This can be useful in a game where an object needs to be destroyed after a chain of actions.

 
/* destroys an object */
class destroy_object : instant_action
{
  public:
  // ctor
  destroy_object(game_object& obj)
    : _obj(obj)
  {
  }

  void execute(void) override
  {
    _obj.destroy();
  }

  private:
  // private members
  game_object& _obj;
  
}; // class destroy_object

Summary

Using the interval_action and instant_action interfaces, creating interesting actions becomes very straight-forward and cohesive. The interval_action can be further expanded upon for things like easing by calculating the interval in a non-linear fashion before passing it on.

« Read Part 1

Leave a Reply

Your email address will not be published. Required fields are marked *