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.
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:
- Is the ability off cooldown?
- Does the character have enough mana?
- Is the target within range?
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 → CastRangeChecker
Each 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:
- If its own condition fails, it stops the chain and reports an error (e.g. “Not enough mana”).
- If the condition passes, it calls the next checker, continuing the validation process.
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:
- Reusability: The same request logic (e.g., a charging bar or input sequence) can be used for multiple skills.
- Interruptibility: Requests can be paused, canceled, or restarted independently from the ability system.
- Asynchronicity: Since startRequest() doesn’t return anything, it can easily support coroutine-like or event-driven flows.
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?
- Keeps responsibilities clean. Each checker only checks; only the final node triggers execution.
- Easier to reuse. You can create different TerminalCheckers for different types of execution (e.g., networked requests, instant local effects, delayed effects).
- Supports asynchronous operations. For example, some skills might involve charging, targeting, or waiting for input before resolving. The request object can handle that without polluting the checker logic.
- This final step bridges the gap between should the ability run and go ahead and run it.
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:
- Validation logic — handled by the CastChecker chain
- Execution logic — encapsulated in a SkillCastRequest
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 Skill
interface:
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:
- The cast() method is called.
- The checker chain is executed.
- If the final TerminalChecker is reached, it starts the SkillCastRequest. This design gives us complete separation of concerns:
- The ability’s name and metadata live in the Skill object.
- Validation logic lives in its checker chain.
- Execution logic lives in the request. Why this is powerful:
- You can reuse checkers and requests across multiple skills.
- You can dynamically assemble or swap out parts at runtime.
- 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:
- It doesn’t require mana.
- It can’t be used in combat.
- It requires the player to stand on a teleportation pad.
- It has a long cooldown.
- It must wait for the player to confirm the destination.
Using our architecture, this complex behavior is no problem.
We assemble it like this:
Checkers:
- CooldownChecker
- InCombatChecker (custom logic: player must be out of combat)
- SurfaceChecker (verifies player is on correct surface)
- TerminalChecker (starts the request)
Request:
- TeleportationRequest, which:
- Opens a destination selection UI
- Waits for confirmation
- Moves the character
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:
-
We gain modularity: each component is testable and replaceable.
-
We gain extensibility: adding new checks or execution styles is trivial.
-
We gain clarity: game logic becomes declarative, not imperative.
Summary Diagram
Skill ├── name: "Teleport" ├── checker: │ └── CooldownChecker │ └── InCombatChecker │ └── SurfaceChecker │ └── TerminalChecker → request.startRequest() └── request: TeleportationRequest
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.”