Action Lists Part 2
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/* 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* 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.