The Action List Data Structure: Good for UI, AI, Animations, and More

The action list is a simple data structure useful for many various tasks within a game engine. One could argue that the action list should always be used in lieu of some form of state machine.

The most common form (and simplest form) of behavior organization is a finite state machine. Usually implemented with switches or arrays in C or C++, or slews of if and else statements in other languages, state machines are rigid and inflexible. The action list is a stronger organization scheme in that it models in a clear manner how things usually happen in reality. For this reason, the action list is more intuitive and flexible than a finite state machine.

Quick Overview

The action list is just an organization scheme for the concept of a timed action. Actions are stored in a first in first out (FIFO) ordering. This means that when an action is inserted into an action list, the last action inserted into the front will be the first to be removed. The action list doesn't follow the FIFO format explicitly, but at its core they remain the same.

Every game loop, the action list is updated and each action in the list is updated in order. Once an action is finished, it is removed from the list.

An action is some sort of function to call which does some sort of work somehow. Here are a few different types of areas and the work that actions could perform within them:

Low level things like path finding or flocking are not effectively represented with an action list. Combat and other highly specialized game-specific gameplay areas are also things that one probably should not implement via an action list.

Action List Class

Here's a quick look at what should lay inside the action list data structure. Please note that more specific details will follow later in the article.

It is important to note that the actual storage of each action does not have to be an actual linked list - something like the C++ std::vector would work perfectly fine. My own preference is to pool all the actions inside of an allocator and link lists together with intrusively linked lists. Usually action lists are used in less performance-sensitive areas, so heavy data-oriented optimization will likely be unnecessary when developing an action list data structure.

The Action

The crux of this whole shebang is the actions themselves. Each action should be entirely self-contained such that the action list itself knows nothing about the internals of the action. This makes the action list an extremely flexible tool. An action list will not care whether it is running user interface actions or managing the movements of a 3D modeled character.

A good way to implement actions is through a single abstract interface. A few specific functions are exposed from the action object to the action list. Here is an example what a base action may look like:

The OnStart() and OnEnd() functions are integral here. These two functions are to be executed whenever an action is inserted into a list, and when the action finishes, respectively. These functions allow actions to be entirely self-contained.

Blocking and Non-Blocking Actions

An important extension to the action list is the ability to denote actions as either blocking and non-blocking. The distinction is simple: a blocking action ends the action list's update routine and no further actions are updated; a non-blocking action allows the subsequent action to be updated.

A single Boolean value can be used to determine whether an action is blocking or non-blocking. Here is some psuedocode demonstrating an action list's update routine:

A good example of the use of non-blocking actions would be to allow some behaviors to all run at the same time. For example, if we have a queue of actions for running and waving hands, the character performing these actions ought to be able to do both at once. If an enemy is running from the character it would be very goofy if it had to run, then stop and wave its hands frantically, then keep running.

As it turns out, the concept of blocking and non-blocking actions intuitively matches most types of simple behaviors required to be implemented within a game.

Case Example

Lets cover an example of what running an action list would look like in a real-world scenario. This will help develop intuition about how to use an action list, and why action lists are useful.

Problem

An enemy within a simple top-down 2D game needs to patrol back and forth. Whenever this enemy is within range of the player it needs to toss a bomb towards the player, and pause its patrol. There should be a small cooldown after a bomb is thrown where the enemy stands completely still. If the player is still in range another bomb followed by a cooldown should be thrown. If the player is out of range, the patrol should continue exactly where it left off.

Each bomb should float through the 2D world and abide by the laws of the tile-based physics implemented within the game. The bomb just waits until its fuse timer finishes, and then blows up. The explosion should consist of an animation, a sound, and a removal of the bomb's collision box and visual sprite.

Constructing a state machine for this behavior will be possible and not too hard, but it will take some time. Transitions from each state must be coded by hand, and saving previous states to continue later might cause a headache.

Action List Solution

Luckily this is an ideal problem to solve with action lists. First, let us envision an empty action list. This empty action list will represent a list of "to do" items for the enemy to complete; an empty list indicates an inactive enemy.

It is important to think about how to "compartmentalize" the desired behavior into little nuggets. The first thing to do would be to get down patrol behaviors. Let's assume the enemy should patrol left by a distance, then patrol right by the same distance, and repeat.

PatrolRight will look nearly identical, with the directions flipped. When one of these actions is placed into the action list of the enemy, the enemy will indeed patrol left and right infinitely.

Here is a short diagram showing the flow of an action list, with four snapshots of the state of the current action list for patrolling:

The next addition should be the detection of when the player is nearby. This could be done with a non-blocking action that does not ever complete. This action would check to see whether the player is near the enemy, and if so will create a new action called ThrowBomb directly in front of itself in the action list. It will also place a Delay action right after the ThrowBomb action.

The non-blocking action will sit there and be updated, but the action list will continue updating all subsequent actions beyond it. Blocking actions (such as Patrol) will be updated and the action list will cease to update any subsequent actions. Remember, this action is here just to see whether the player is in range, and will never leave the action list!

The ThrowBomb action will be a blocking action that tosses a bomb towards the player. It should probably be followed by a ThrowBombAnimation, which is blocking and plays an enemy animation, but I've left this out for conciseness. The pause behind the bomb will take place of the animation, and wait a little while before finishing.

Let's take a look at a diagram of what this action list might look like while updating:

The bomb itself should be an entirely new game object, and have three or so actions in its own action list. The first action is a blocking Pause action. Following this should be an action to play an animation for an explosion. The bomb sprite itself, along with the collision box, will need to be removed. Lastly, an explosion sound effect should be played.

In all there should be around six to ten different types of actions that are all used together in order to construct the needed behavior. The best part about these actions is that they can be reused in the behavior of any enemy type, not just the one demonstrated here.

More on Actions

Action Lanes

Each action list in its current form has a single lane in which actions can exist. A lane is a sequence of actions to be updated. A lane can either be blocked or not blocked.

An action should have an integer to represent all the various lanes that it resides upon. This allows for 32 different lanes to represent different categories of actions. Each lane can either be blocked or not blocked during the update routine of the list itself.

Here is a quick example of the Update method of an action list with bitmask lanes:

This provides a heightened level of flexibility, as now an action list can run 32 different types of actions, where beforehand 32 different action lists would be required to achieve the same thing.

Delay Action

An action that does nothing but delay all actions for a specified amount of time is a very useful thing to have. The idea is to delay all subsequent actions from taking place until a timer has elapsed.

Synchronize Action

A useful type of action is one that blocks until it is the first action in the list. This is useful when a few different non-blocking actions are being run, but you aren't sure what order they will finish in. The synchronize action ensures that no previous non-blocking actions are currently running before continuing.

The implementation of the sync action is as simple as one might imagine:

Advanced Features

The action list described thus far is a rather powerful tool. However there are a couple additions that can be made to really let the action list shine. These are a bit of advanced and I don't recommend implementing them unless you can do so without too much trouble.

Messaging

The ability to send a message directly to an action, or allow an action to send messages to other actions and game objects, is extremely useful. This allows actions to be extraordinarily flexible. Often an action list of this quality can act as a "poor man's scripting language".

Some very useful messages to post from an action can include the following: started; ended; paused; resumed; completed; canceled; blocked. The blocked one is quite interesting - whenever a new action is put into a list, it may block other actions. These other actions will want to know about it, and possibly let other subscribers know about the event as well.

The implementation details of messaging is language-specific and rather non-trivial. As such, the details of implementation will not be discussed here, as messaging is not the focus of this article.

Hierarchical Actions

There are a few different ways to represent hierarchies of actions. One way is to allow an action list itself to be an action within another action list. This allows the construction of action lists to package together big groups of actions under a single identifier. This heightens usability and makes more complex action list easier to develop and debug.

Another method is to have actions whose sole purpose is to spawn other actions just before itself within the owning action list. I myself prefer this method to the aforementioned, though it may be a bit more difficult to implement.

Conclusion

The concept of an action list and its implementation have been discussed in detail in order to provide an alternative to rigid ad-hoc state machines. The action list provides a simple and flexible means to rapidly develop a wide range of dynamic behaviors. The action list is an ideal data structure for game programming in general.