Hello! I’m Vladimir Popov, Client Developer on the War Robots Project. At the time of writing, War Robots have been around for several years, and dozens of new mechs have appeared in the game during this time. Naturally, the varied abilities of the robots are important, because, without them, the robots lose their uniqueness which makes the game more interesting.
In this post, I’ll share how the gameplay ability system works in our game – and how it has evolved. And, to make things more accessible, I’ll explain things in simple terms and without too much technical detail.
First, let's dive into the project history and look at an older implementation that’s no longer being used.
Previously, abilities were designed in a very trivial way: they had one component that was attached to the robot. This was a construction where the programmer fully described how the ability works: both its flow and how it interacts with other abilities. All the logic is described inside one component, and the game designer could simply attach this to the robot and configure the parameters as needed. It’s worth mentioning that it was not possible to change the flow of abilities – the game designers could only change parameters and timings.
An old ability could only exist in two states: active and inactive, and each state could have its action assigned to it.
Let's look at an example of the “Jammer” ability, which the robot “Stalker” previously possessed; it worked like this:
For a long while, this functionality was enough for us, but over time both the game designers and the programmers were no longer satisfied with this approach: it was difficult for the programmers to support these abilities because the code had become monstrous; this involved a very long legacy chain, where every situation had to be described. Additionally, game designers lacked flexibility – to make any change to an ability, they had to order modifications from the programmers, even if an adjacent ability had the same functionality.
So, we realized that something needed to change. Therefore, we developed a new system, where each ability was represented as a set of several related objects. The functionality was divided into state abilities and state components.
How does it work? Every ability has a main object. This central object connects other ability objects with the outside world, and vice versa; it also makes all the main decisions.
There can be any number of states. Essentially, the state in this iteration isn’t much different from the active/inactive states in the old version, but now there can be any number of them, and their purpose has become more abstract. We’ll note that an ability can only have one state active at a time.
The main innovation compared to the old system was the components: a component describes some action, and each state can have any number of components.
How do the new abilities work? An ability can only be in one of the states; the main object is responsible for switching them. The components that link to a state react to the activation/deactivation of the state and, depending on this, can either begin to perform some action or stop performing it.
All objects have become customizable; a game designer can mix states and components as they’d like, thus composing a new ability from pre-installed blocks. Now, programmers only need to enter the picture to create a new component or state, which makes writing code much easier: they work with small entities, describe some simple elements, and no longer build the ability themselves – the game designers do this now.
The flow has become like this:
Subsequently, this procedure is repeated again and again. For ease of use, a state not only serves as a component container, it also determines when to switch to another state and requests the main object to make the switch. Over time, this still turned out not to be enough for us, and the ability diagram was transformed into the following:
The main object, state, and components remained in their places, but new elements were also added.
The first thing that catches your eye is that we added conditions to each state and component: for states, these define additional requirements for leaving the state; for components, they determine whether the component can perform its action.
The charge container contains charges, recharges them, stops recharging if necessary, and provides charges for states to use.
A timer is used when several states must have a common execution time, but their own execution time is not defined.
It is important to note that all ability objects are optional. Technically, for an ability to work, only a main object and one state are needed.
Now, while there aren’t actually that many abilities entirely built without the involvement of programmers, development in general has become noticeably cheaper, because programmers now only need to write very small things: for example, one new state or two components – the rest is reused.
Let's summarize the components of our abilities:
The main object performs the functions of a state machine. It provides states and components with information about the world and provides the world with information about abilities. The main object serves as a link between states, components, and service parts of the ability: charges and external timers.
The state listens to activation and deactivation commands from the main object and, accordingly, activates and deactivates components, and also requests the main object to switch to another state. State determines when it needs to switch to the next one; to do this, it uses its internal condition: whether the player clicked on the ability button, whether a certain time has passed since the activation of the state, and so on, and external conditions linked to the state.
The component listens to activation and deactivation commands from the state and performs some action: discrete or long-term. Actions can be completely different: they might cause damage, heal an ally, turn on an animation, and so forth.
The condition checks what state the desired element is in and reports this to the state or component. Conditions can be complex. A state doesn’t request a transition to another state if the condition is not met. The component also doesn’t perform an action if the condition isn’t met. Conditions are an optional entity; not every ability has them.
The charge container holds charges, recharges them, stops recharging when necessary, and provides charges to states. It’s used in multi-charge abilities, when you need to allow the player to use it several times, but no more than n times in a row.
The timer is used when several states have a common duration, but it’s not known how long each of them will last. Any state can start a timer for n seconds. All relevant states subscribe to the timer end event and do something when it ends.
Now let's return to the ability diagram. How did its functionality change?
States can use charges as an additional transition condition. If such a transition occurs, the number of charges decreases. States can also use a common timer. In this case, the total time for their execution will be determined by a timer, and each state individually can last any time.
We didn't completely reinvent the wheel for the new ability UIs.
The main object has its own UI. It defines some elements that should always be in the UI and which do not depend on the currently active state.
Each state has its own pair in the UI, and the state UI is displayed only when its state is active. It receives data about its state and can display it in one way or another. For example, duration states usually have a bar and text in their UI that displays the remaining time.
In a case where the state is waiting for an external command to continue an ability, its UI displays a button, and pressing it sends the command to the state.
We’ll look at how the abilities work using specific examples; first, let's look at a robot called “Inquisitor”. We have four states that follow each other – above the states you can see their display in the UI. For two of them, we also see the components that belong to them; the other two states simply don’t have components.
Here’s the flow of the ability:
It all starts with the “WaitForClick” state. At this time, the ability does nothing; it just waits for commands.
As soon as such a command is received, the main object switches states. The next active state is “WaitForGrounded”.
This state has some components, and therefore, when activated, the robot jumps and plays a sound and animation. Among other things, while the state is active, the robot is affected by the Jammer effect, which prohibits aiming at the robot.
When the robot lands, its ability moves to the next state.
This state has three components: the already familiar Sound and Jammer, as well as Shake, which causes the camera to shake for all players within a radius of n.
Since this state has Duration, it works for n seconds, then the ability moves to the next state.
The last state also comes with a Duration, but it doesn’t have any components: it’s on a regular cooldown.
Upon completion, the ability returns to the first state.
Another example is “Phantom”. It’s a lot like Inquisitor, but there are some nuances:
We start with WaitForClick.
Then, Duration, in which the teleport is installed, the stats of the mech are changed, and sound and animation are played.
After this: DurationOrClick, in which the mech's stats are changed, animation and FX are played.
If a click was made, we go to another Duration, in which the mech teleports, stats change, and animation, FX, and sounds are played.
After this state (or after the time expires for DurationOrClick), we move to Duration.
The main difference here is that we see states with branching: DurationOrClick goes to state A if the specified time has passed, or to state B if the player has previously pressed the ability button.
While it would seem that our system has evolved from something simple to something quite complex, this change has simplified the lives of both programmers and game designers. Assistance from the programmers is now needed mostly when adding small components, while the latter group of team members have gained greater autonomy and can now independently assemble new abilities from existing states and components. As another bonus, at the same time, the players also received a profit in the form of more diverse and complex abilities of the mechs.