What would be the best way to make an ability system for a game?

Godot Version

4.2.1

Question

If I’d want a character to have a single ability slot into which a large variety of abilities could be inserted. What would be the best way to store the data for the abilities?, how would you handle the different animations? and what would be the best way to implement a system for acquiring/picking up the ability?

Here are my immediate thoughts

  1. My first thought would be to either code every ability into the ability script and just let a single variable decide which ability is currently in use sort of like so:
    (the code below is just an example of what I’d want to happen)
var current_ability = null

func _process():
  on ability_picked_up:
    current_ability = new_ability
  1. My second thought would be to use a system like SQL.

But I’m not sure if godot has some ways to handle an issue like this since I’m relatively new to godot.

Any and all suggestions are appreciated :slight_smile:

Why SQL?

There is a book out there called “Design Patterns: Elements of Reusable Object-Oriented Software”, which I would recommend as a general catch-all for questions like this.

But more specifically to your question: Your idea with an ability script is not bad if you have only a handful of abilities. It will probably go off the rails as you approach 10 abilities, and making changes will be quite painful at that point.

You should first think about what all the abilities will have in common, and pull that mess of stuff into its own class. Each ability will presumably have a name, a text description, a sprite for the UI, and stuff like that.

Abilities may differ in how they are used, i.e. some abilities may do damage to enemies, while others might buff the player, and others might be utility like showing more of the map. For this reason, you might want a general catch-all method like use_ability() that differs from ability to ability.

Abilities might also only be usable sometimes. Like, maybe you can only use the bazooka once every 10 seconds, or maybe the flashlight only works in a dark room. So maybe a catch-all can_use_ability().

This is just a “for instance”, because your question is quite general and does not provide any specifics.

So you’d end up with a class

class BaseAbility:
    var name:String
    var description:String
    var ui_sprite:Texture
    func can_use_ability():
        pass
    func use_ability()
        pass

Then in any code that uses abilities, like the handler for a keystroke, you could do something like

func _on_use_ability_key_pressed():
    if current_ability.can_use_ability():
        current_ability.use_ability()

You’d then create abilities by subclassing BaseAbility, and set current_ability to one of these objects.

I want to stress, though, that this is just one of like, a million different ways to do this. Thus the book recommendation at the beginning. (There’s also a book called “Game Programming Patterns” which I’ve also found useful for game dev). The “correct” way will depend on which dimensions you need flexibility in, how many abilities you plan on having, and lots of other factors.

1 Like

Have a look at the gdscript Callable type. It lets you put a reference to a function into a variable. You can make current_ability a Callable, and then just do current_ability.call() when you want to use the skill.

The main thing you’ll probably find is that unless abilities are instantaneous things, you’ll need to have something managing cooldown, ongoing skill effects, and so on. I’d suggest maybe something like:

var ability_start:  Callable = _null_start
var ability_update: Callable = _null_update
var ability_cooldown = 0
var ability_active = false

func _null_start():
    pass

func _null_update() -> bool: # returns whether ability should continue
    return false # empty ability, ends immediately

func set_ability(start: Callable, update: Callable):
    ability_start  = start  # called for ability setup
    ability_update = update # called every update

func _process(delta: float):
    [...]

    if ability_active:
        ability_active = ability_update.call()
    elif !ability_cooldown && Input.is_action_just_pressed("ability"):
        ability_active   = true
        ability_cooldown = COOLDOWN_TIME # could be another function...
        ability_start.call()
    else:
        if ability_cooldown: ability_cooldown -= 1

    [...]
2 Likes

Sorry for the late reply. I will definitely look into those! but yeah I get understand that answering the question can be bit tough when there’s very little to go off of. The reason for this is because I have a lot of different directions I can choose and I’m not sure which direction I want to take

The reason for SQL is that I have a bit of previous experience with it and thought that it could be used in a manner somewhat similar to what you described where the variables for the ability could be retrieved from a database with SQL so as to prevent issues with more abilities.

I think the hope I have with SQL would be that I could make a catch-all system that could handle any ability that would buff the player, perform an action, debuff an enemy, or any other thing you could think of, In such a way that in the future all you’d need to do, was to insert it to the DB.
I’m not sure if it would be possible to save lines of code in a DB and then using SQL to retrieve that code so that godot could execute it.

(It’s been a long time since I coded with SQL so this is very unlikely to be correct)

class BaseAbility:
  #then the current ability would be determined by an ability_id that's also saved and called from the database upon pickup  

    var name: String = get_from_database(current_ability.Name)
    var description: String = get_from_database(current_ability.description)
    var ui_sprite: Texture = get_from_database(current_ability.sprite)

    func can_use_ability():
        pass
    func use_ability()
        pass

Wow thanks! this looks really great :slight_smile:
Where does callable call from?
Is it from an ability scene? or would it be from the code directly?

The main issue I’m worried about in any scenario would be functionality later on and how flexible the ability system needs to be. I’m imagining that the player could create an explosion 5 meters in front of him. Thats would need an area2d to detect if an enemy is hit. And if that ability is switched out with another, would it then be possible to transform the area2d to fit the other ability? (I’m relatively new to godot and I’ve not really gotten used to the technical wording that’s used in the documentation and I’ve just skimmed the start of the callable documentation)

(btw this long rant, is not meant as criticism. I just realised I may have been too vague about my project in the original post and that I may not have put enough details into the post)
Thank you so much for the help :))

The sample code I wrote above was assuming it was in player.gd or wherever your script is driving the thing with abilities. You could structure things differently if you wanted to.

The callable itself should run in the context in which it’s called, so as part of the object it’s called inside, but see the docs for details.

You can create nodes at runtime:

var ability_scene: Node

# Fireball: Player shoots a fireball.  They can charge it up by holding down
# the ability button, once they release it, it launches.

func _fireball_start():
    ability_scene = preload("res://fireball.tscn").instantiate()
    add_sibling(ability_scene)

# In retrospect, probably want to hand delta into the update functions...

func _fireball_update(delta: float) -> bool:
    if Input.is_action_pressed("ability") && ability_scene.power < FIREBALL_MAX:
        ability_scene.power += delta
        return true # player is still charging the fireball
    else: # release!
        ability_scene.launch() # Tell the fireball it's go time...
        return false           # ...and we're done with it.

# Health: Player can heal themselves for up to 8 hp.  Healing happens
# immediately, but plays a spell effect and does the green number floating
# up thing.

func _heal_start():
    ability_scene = preload("res://heal_spell.tscn").instantiate()
    if health + 8 > max_health:
        ability_scene.health_up(max_health - health)
        health = max_health
    else:
        ability_scene.health_up(8)
        health += 8
    add_sibling(ability_scene)

func _heal_update(float: delta) -> bool:
    return ability_scene.animation_complete()
2 Likes