A note about the implementation of the ability system in games

Game developers are often faced with the need or desire to implement an ability system in their projects. This mechanic is quite popular, especially in the midcore+ segment. However, despite the availability of a ready-made framework Gameplay Ability System (G.A.S.) in Unreal Engine, on other engines you often have to use “homemade” decisions.
I am convinced that there are often cases when it is not clear either how to approach at the beginning, or how to add a new one to the game later.”brilliant“the ability about which”have not previously agreed“, without breaking those already added. At a minimum, I myself managed to go through these difficulties more than once on single-player and multiplayer projects.

Disclaimer:

In this note I share my experience and current situation personal opinion (which can be updated with new experience).

I'm not trying to build a framework, prove something or insist on the truth and correctness of my judgments.

The code in this material is for example only. It can be considered pseudocode for C#.

Background note:

Recently on Unity Architect channel was published post about the ability system . I left my comments there and answered a few questions. To save the ideas that arose, I decided to create a note on my blog.

I got carried away a little, and a simple note, collected from several short comments, grew into a large discussion that went beyond the scope of one message. So this note ended up here.

My shorter notes and other content can be followed at VK / Telegram / DTF.


Ability System Difficulty:

It would seem that there is so much complexity in the system of abilities. But the answer comes only after the first project with such a system, when the development department before documentation for the next version begins to look something like this:

In practice, it turns out that to successfully solve this problem, ….. experience is required! Or carefully thought out and well-described documentation on such massive mechanics, indicating all possible applications. But in order to write such documentation, a game designer needs… experience!

If neither the Dev nor the GD have done this on their own before in their careers, then there will likely be a lot of new, unpredictable context ahead that may be very difficult to fit into the previously established foundation.

The ability system requires maximum flexibility because there is great variability:

Any flexibility leads to complexity of the system, and this negatively affects its support. It is pointless and expensive to introduce a maximally flexible system. In addition, it requires a lot of experience. Therefore, every new excellent project will have its own nuances and pitfalls that you will have to fight (or get acquainted). This means that having made a system of abilities once, there is no guarantee that the next time it will turn out faster or easier.

In addition to the ability system in the game there may be there is also other systems: character levels, equipment bonuses, special events, etc. All this also dynamically modifies the game parameters, and it all must also be able to interact with each other.

Example: a movement speed modifier from an ability should not override speed modifiers from other sources unless it is required to do so.

Mortal Kombat X and Blue Protocol

Mortal Kombat X and Blue Protocol

It is difficult to initially predict the scale of the system. Some requirements only appear over time. And something for game designers”implied“from the very beginning, but it becomes clear to the performers only after a conditional six months.

Example: There is a ready-made system of abilities that works on the principle of buffs that change the parameters of characters. At some point, it becomes necessary to make a buff that allows the character to pass through opponents and receive acceleration from this. Those. a buff that stands out from the general system.

Example: There is a ready-made system of abilities and the ability to set an opponent on fire. Later in the game all sorts of props appear. Some break down and have HP. Some are not. But now you need to be able to use the arson ability for props as well. For everyone. But the developers did not initially plan for this.

Crysis and Victor Vran

Crysis and Victor Vran

I'll try to fantasize about my first experience.

The developer will think: “I will make each ability as a separate entity. What could be more flexible?“.

Then a large number of similar classes accumulate. The developer will begin to somehow systematize this, create several basic abstractions, and put the general logic there.
Then comes an ability that is a little bit of this and a little bit of that. And the Unity developer has no multiple inheritance. Inheritance turned out to be a bad idea – composition should have been used. But it’s too late, because the deadline, as luck would have it, is something like “yesterday”.

Later in the game other systems appear with similar functionality, but still different. And this must be able to coexist with the system of abilities.

And so nuance after nuance, until the first implementation of the system turns into something like this:

And in the end, multiplayer sits on top of it, and everything breaks.


How to approach the system:

IN original post The importance of separating data from business logic was noted. I fully support this. And not only in the context of the ability system. In my article on saving progress I wrote:

From a technical point of view, a game is simply data and operations on it. Everything in the game is described by data. Everything that happens in the game is described by operations on data. Everything that the player physically interacts with is implemented through a variety of input and output devices.

For me, at least in CoreGame,Data separation is a core concept. Especially when it comes to possible multiplayer. After all, synchronization in multiplayer is based on data exchange between nodes. And this is done most conveniently and efficiently when the data is separated from everything else.

Therefore, it is important to learn a specific ability to represent as a set of comprehensive data. For now I have focused on the decomposition of abilities into the following component effects:

  • Statuses (aggro, invisibility, etc.);

  • Modifiers (+speed, -damage, etc.);

  • Actions (causing damage to targets, summoning new entities, re-applying, etc.).

In my practice, these three types of effects have so far been sufficient to realize all the necessary abilities.

These effects can be applied to other systems, allowing interactions between systems to be handled. Those. effects can be thought of as more general underlying elements.

Each effect is described by its own data structure:

public readonly struct AgroStatus
{
  public readonly float Duration;
  public readonly uint TargetId;
}

public readonly struct HpModifier
{
  public readonly float Modifier;
  public readonly ModifierOp Operation; // Set, Add, Mult etc.
}

public readonly struct InstantiateAction
{
  public readonly uint ObjectId;
  public readonly Vector3 Position;
}

When a character wants to activate an ability, he accesses the controller for the desired ability. For example, like this:

Character.Abilities[0].Apply();

Inside the controller there are several modules that solve component tasks of the ability system:

  • Checking the terms of use: the ability can be blocked by level, equipment, amount of mana, cooldown, play zone, etc.;

  • Defining Scope (if it's an AoE ability): point use, global, across areas of different shapes, etc.;

  • Finding targets for use (incl. inside AoE): these could be other characters, groups of characters, the using character himself, dead or alive, some objects, or even the game world controller itself;

  • Calculating usage timings: when to look for targets, when to activate ability effects, etc.;

  • Handling usage cost: removal of mana/gold/health, etc.;

  • Transfer of data to purposes: Makes the target the bearer of effects. Because It's just data, and it's separate, so it's not difficult to exchange it.

Each effect or effect type has a certain handler that reacts to the presence of the necessary data on the target, controls the lifetime of this data and implements logic corresponding to the effect:

  • Modifiers: for each characteristic, it runs through all applied modifiers and calculates a new current value;

  • Actions: performs some predetermined action.


Example ability:

In discussions original post a question was received:

What if my ability is the Ball of Death, which falls on an area and should kill everyone who falls within the radius?

I can offer two solutions, which depend on more detailed context.

Solution 1:

If the Death Ball has an internal state and somehow interacts with the world (elastic collisions, sliding, number of targets depending on the number of collisions), then it is more profitable to make the Death Ball a separate game object that has a certain life cycle and can inflict fatal damage in its radius.

In this case, the ability will only be an action.”instantiate the Death Ball“. Its goal is the game world, which controls the life cycles of objects within the world. The world instantiates the Ball, and then it’s up to the Ball. If necessary, the ability handler can somehow subscribe to the Ball and track events from it – these are details.

Solution 2:

If the Death Ball does not interact with the world and has no internal state, then it turns out that the essence of this ability is to cause fatal damage to all targets in a given area after N seconds.

So ability comes down to action”cause fatal damage“. The target is the characters in the area. The controller finds them and distributes them DamageAction's for which the handler deals fatal damage.

The Ball of Death is just a visual embodiment that is implemented outside of logic. It can be replaced with flames coming from the ground, a cloud of poison, or something else that can deal area damage.

Addition about object instantiation:

Through the instantiation of new objects, very complex abilities can be made recursively. After all, these new objects may have their own abilities, which they can activate according to their own specific rules.

The same Death Ball, after being used, can scatter fragments around the area, which cause non-fatal damage over a slightly larger radius. If the fragments are separate objects, then the chain of calling abilities can be continued further.


Famous life hack:

Due to its flexibility (and monstrousness) the system of abilities sometimes begs to be used outside the context of abilities in the usual sense. In games, abilities can be considered many common actions: reload, shot, jump, somersault, etc. These actions also have a cost of use, restrictions on use and other parameters. Only the player himself is chosen as the target.

Deadshot Brigade

Deadshot Brigade

If this is the first attempt at developing an ability system, then this task is usually left for later, when the basic controls are already ready. But after implementing the ability system, you will really want to use it instead of jumps and other actions done otherwise. Because this will greatly simplify everything. Maybe not always – I'm not sure here. However, such processing can be expensive.

I don’t encourage you to set the task on the ability system before others – this will cause its own inconveniences. But if it is known in advance that a system of abilities will appear in the game, then it is worth developing with an eye to the appearance of such a system and the ability to hook up to it in certain places. Unfortunately, only personal experience will tell you exactly how. All systems are different and all require different approaches.


Data first:

When designing an ability, it is important to define what it is at the data level and how it changes other game data. Visual representation is secondary and interferes with the work of business logic (and developer) should not.

Such frequent attention to data seems to automatically move the narrative in the direction Data Oriented Design. And in this field there already exists an architectural pattern that allows you to concentrate on designing a game through data (inhaled) is Entity-Component-System, also known as ECS.

The ability system task is one of those tasks (yes, not from “all”), which can be solved well in ECS. There are many abilities and their effects, even more combinations of effects, and the targets for the effects are varied. Everything here is large-scale, very dynamic and combinatorially complex.

In such a paradigm, Statuses, Modifiers and Actions are components:

public struct CurseStatusComponent : IComponent
{
  public readonly float DamagePerSecond;
  public readonly long StartTimestamp;
  public readonly TimeSpan Duration;
}

Components are added to the target Entity:

CurseStatusComponent status =
  new(damagePerSecond, DateTime.Now.Ticks, duration);
character.AddComponent(status);

The processors of these components are the systems:

public sealed class CurseStatusSystem : UpdateSystem
{
  private Filter _filter;

  public override void OnAwake() =>
    _filter = World.Filter
      .With<CurseStatusComponent>()
      .Build();

  public override void OnUpdate(float deltaTime)
  {
    foreach (Entity entity in _filter)
      ...
  }
}

Life without ECS:

If the game is not already built on ECS, then for the sake of abilities alone, dragging it there is inappropriate. This will greatly complicate the overall project architecture. However, for abilities it is possible to build something similar within the “classical“approach.

For all effect carriers, you will need to create some kind of general contract with lists where superimposed effects can be placed. At the same time, Statuses, Modifiers and Actions will also require base abstract classes so that they can be stored in common collections.

The main thing is not to make abstraction through the interface, and implementation through structures. Then, when stored in the collection via the interface, a constant boxing/unboxing.

Those. you'll get something like this:

public interface IEffectTarget
{
  ICollection<ActionEffect> Actions { get; }
  ICollection<StatusEffect> Statuses { get; }
  ICollection<ModifierEffect> Modifiers { get; }
}

Or even this:

public interface IEffectTarget // или сразу IEntity
{
  ICollection<Effect> Effects { get; }
}

With processors it’s more complicated – there are significantly more different options, especially if you go into specifics.

If the handlers have some kind of internal state, then the handlers can be made individual for each media. Or even for each applied effect.

It's better when the handlers stateless. In this case, they can be made common to all media and the logic can be implemented centrally. Those. some semblance of systems from ECS. From my experience, this turned out to be more convenient in support, debugging, modification and provides scope for parallel processing.

Effect collections can be replaced with reactive ones. Then it will be convenient for processors to subscribe to changes and monitor the presence of the desired effects.


In multiplayer:

Because the entire work of the ability system depends on operating with data, then ideologically it is quite easy to integrate into multiplayer – it’s enough to replace the usual collection with a synchronized one:

public sealed class PlayerObject : IEffectTarget
{
  public PlayerState State { get; }
  ICollection<Effect> IEffectTarget.Effects => State.AppliedEffects;
}

public sealed class PlayerState : NetworkBehaviour
{
  public SyncList<Effect> AppliedEffects = new();
}

The example is artificial and simplified. In reality it's not like that”Just“: there are some nuances from framework to framework. But the gist is something like this: since the data is in a collection, and the logic is built from the data in this collection, then it is enough to synchronize this collection.

Solving issues related to optimizing replicated data, compensating for delays, etc. are subjects for a different discussion.


Visual:

If Data, Logic and Views are separated from each other, then the task of connecting the visual should not cause much difficulty. There is data, there are changes in it – we follow this and render a picture that matches the data.

Project Layers

Project Layers

However, there can be many abilities, each requires its own reaction, and some reactions are even repeated in different abilities, etc. You can try to decompose visual reactions into simple reusable commands (turn on VFX, shake the camera, update your health bar, etc.). Incl. reusable between different systems.

Commands are sent from Logic to the command bus, from where the Presentation layer will pick them up for processing (in this case, there is no direct connection between Logic and Representation). Or introduce an intermediary that will translate general Logic events into commands for the View. This will also make it possible to centrally resolve any collisions when different systems, for example, try to simultaneously shake the camera with different forces.

Everything again comes down to data and managing the flow of this data, only in the visual part.

The Iron Oath

The Iron Oath

It’s another matter when Presentation is somehow intertwined with Logic. Much more interesting complications may arise here. It seems to me that the best way to solve them is not to bring them to light. It is better for Logic and Data not to know about the existence of the Representation. And tasks by type “use the ability after the casting animation ends” decide by setting timings in Logic and shifting the emphasis to “play the casting animation before the ability is used directly“.

Also, independence from the View in multiplayer with a client-server architecture will allow the server to freely use ability data to calculate logic and change game data without unnecessary visual dependencies, and the client will be able to draw VFXs, HUDs, animations, etc. without unnecessary context.


Additional content:

Similar Posts

Leave a Reply

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