Introduction: Why Ability Systems Must Be Flexible

In game development, the ability system is often one of the most demanding components in terms of flexibility. At the design stage, it’s nearly impossible to predict what spells, abilities, or skills will exist in the final version — or what updates will introduce in the future.

This article is about how I approached this uncertainty by abstracting the process of executing abilities.

At its core, an ability is nothing more than a set of actions. A minimalistic ability interface might consist of a single method like apply(). But in practice, things are rarely that simple. The complexity doesn't lie in calling the ability — it lies in determining whether the ability can or should be used at all.

In order to manage this complexity and allow for future expansion, we need a flexible, modular approach that decouples ability execution from the conditions under which it may or may not proceed. This leads us to rethink how to structure ability logic from the ground up.

This article is part of a broader discussion in my book Safe by Design: Explorations in Software Architecture and Expressiveness. If you like it, you might enjoy the full read for free on GitHub.

The First Layer: Ability Checks as Chainable Components

Every ability begins with a series of checks that determine whether it can be used. These checks are usually things like:

Right away, it becomes obvious that not every ability needs every check. For instance, some abilities might not require mana, or may be usable at any distance.

This means different abilities require different sets of preconditions. However, many of these checks are reused across multiple abilities. Cooldown, mana, and range checks are common across dozens of spells. If these checks are duplicated everywhere, any change to their logic must be applied in many places — creating fragility.

To avoid duplication and enable flexibility, we can extract each check into its own object implementing a shared interface. Then, we link them together in a single, ordered chain.

This is the classic Chain of Responsibility pattern.

Here’s what such an interface might look like:

interface CastChecker {
    CastChecker nextChecker { get; set; }
    bool check();
}

Enter fullscreen mode Exit fullscreen mode

And here’s an example of a simple chain:

CooldownChecker → ManaChecker → CastRangeCheckerEach checker performs a specific validation and, if successful, passes control to the next in the chain.

This structure allows for reuse, recombination, and centralized changes — the foundation of a truly flexible system.

Executing the Chain: Sequential Validation and Error Handling

Once we’ve assembled a chain of CastChecker objects, the system can process them sequentially to validate whether an ability can be used.

Each checker in the chain follows the same logic:

Here’s a simple implementation outline:

bool CastChecker.check() {
    if (!thisConditionIsMet()) {
        showErrorMessageToPlayer();
        return false;
    } else if (nextChecker != null) {
        return nextChecker.check();
    } else {
        return true;
    }
}

Enter fullscreen mode Exit fullscreen mode

This design introduces a few key benefits:

1. Composable and Maintainable Checks: You can build a custom validation pipeline per ability without rewriting shared logic. For example:

A fireball might need mana, cooldown, and range.A healing spell might only need cooldown and line of sight.

2. Readable Flow: Since each check is self-contained, its logic stays focused and understandable. The CastChecker interface allows adding new conditions without modifying existing ones.

3. Centralized Error Handling: Each checker can report its own failure reason — giving clear, targeted feedback to the player.

This modularity is what sets the system apart from ad hoc validation logic. We’re no longer writing giant if statements or switch-cases. Instead, we assemble abilities like LEGO blocks — combining reusable, testable pieces.

Abstraction via SkillCastRequest

Now that we’ve covered how to validate an ability using a chain of checkers, we need to think about how the ability actually gets executed — and more importantly, how to represent that execution as an abstract, independent process.

Let’s introduce a new interface: SkillCastRequest.

This interface doesn’t care whether the ability is an instant fireball or a multi-phase ritual. It simply represents “a request to perform an action,” and exposes a standard way to start or cancel it:

interface SkillCastRequest {
    void startRequest();
    void cancelRequest();
}

Enter fullscreen mode Exit fullscreen mode

This abstraction lets us treat the execution logic as a first-class citizen in our architecture.

Instead of having every ability directly embed its own complex execution logic (animations, delays, input windows, etc.), we separate that into a reusable request object.

Benefits of this approach:

In essence, this abstraction decouples what the skill does from how it gets initiated — a critical distinction for building flexible gameplay systems.

TerminalChecker and Executing the Skill

We now have two powerful tools in our toolbox:

A chain of CastCheckers that validates whether a skill can be used.A SkillCastRequest that encapsulates the process of executing that skill.But how do we tie them together in a way that guarantees execution only happens if all checks pass?

That’s where the TerminalChecker comes in.

It’s a special node in the chain — always placed at the end — whose job is to trigger the actual startRequest() call when all prior checks succeed.

Example:

class TerminalChecker implements CastChecker {
    CastChecker nextChecker = null;
    SkillCastRequest request;

    bool check() {
        request.startRequest();
        return true;
    }
}

Enter fullscreen mode Exit fullscreen mode

In a full chain, it might look like this:

CooldownChecker → ManaChecker → RangeChecker → TerminalCheckerOnly if the first three validations pass will the request begin.

Why separate the final execution?

If you’re enjoying this so far, there’s a lot more in the book — same tone, just deeper. It’s right here if you want to peek.

Binding the Skill and the Request

We’ve now split ability logic into two distinct domains:

But how do we represent an actual skill — something the player can activate?

Simple: we bind both parts together under a unified interface.

Defining the Skillinterface:

interface Skill {
    string name;
    SkillCastRequest request;
    CastChecker checker;

    bool cast() {
        return checker.check();
    }
}

Enter fullscreen mode Exit fullscreen mode

When the player tries to use a skill:

  1. The cast() method is called.
  2. The checker chain is executed.
  3. If the final TerminalChecker is reached, it starts the SkillCastRequest. This design gives us complete separation of concerns:
  4. The ability’s name and metadata live in the Skill object.
  5. Validation logic lives in its checker chain.
  6. Execution logic lives in the request. Why this is powerful:
  7. You can reuse checkers and requests across multiple skills.
  8. You can dynamically assemble or swap out parts at runtime.
  9. You can subclass or wrap Skill objects to add logging, cooldown tracking, analytics, or multiplayer synchronization — without changing the base structure. This turns your skills into pure data + behavior composition, making them ideal for designers, modders, and procedural generation.

Example and Conclusion: A Universal Execution Framework

Let’s put it all together with a concrete example: the TeleportationSkill.

Teleportation is a perfect case because it breaks common assumptions:

Using our architecture, this complex behavior is no problem.

We assemble it like this:

Checkers:

Request:

Skill object:

Skill teleport = new Skill(
    name = "Teleport",
    checker = new CooldownChecker(
        next = new InCombatChecker(
            next = new SurfaceChecker(
                next = new TerminalChecker(request = teleportationRequest)
            )
        )
    ),
    request = teleportationRequest
);

Enter fullscreen mode Exit fullscreen mode

This entire skill is fully declarative and composable. No tight coupling, no duplicated logic. If we later want to use the same teleportation behavior for enemies or items — we just plug in the same request.

Final Thoughts

By separating validation, execution, and composition:

This is a universal framework not just for spells or attacks, but for any game mechanic where action depends on conditions.

You can use this to build skill trees, item usage systems, interaction mechanics — anything where “can I do this?” must be evaluated before “do this.”