I have a Resource for Stats, with variables for health, speed, etcetera:

extends Resource
class_name Stats

signal stats_changed(property, original_value, new_value)

@export var health : int = 100: set = set_health
@export var max_health : int = 100: set = set_max_health
@export var defense : int = 10: set = set_defense
@export var stealth : int = 0: set = set_stealth
@export var weight : int = 10: set = set_weight
@export var speed : int = 100: set = set_speed
@export var dodge_speed : int = 180
@export var money: int = 0: set = set_money
@export var max_money: int = 999999

enum Properties { 
    HEALTH,
    MAX_HEALTH,
    DEFENSE,
    STEALTH,
    WEIGHT,
    SPEED,
    DODGE_SPEED,
    MONEY,
    MAX_MONEY
}

func set_money(value):
    emit_signal("stats_changed", Properties.MONEY, money, value)
    money = clamp(value, 0, max_money)

func set_max_health(value):
    emit_signal("stats_changed", Properties.MAX_HEALTH, max_health, value)
    health = clamp(value, 0, max_health)
    max_health = value

func set_health(value):
    emit_signal("stats_changed", Properties.HEALTH, health, value)
    health = clamp(value, 0, max_health)

func set_defense(value):
    emit_signal("stats_changed", Properties.DEFENSE, defense, value)
    defense = value

func set_speed(value):
    emit_signal("stats_changed", Properties.SPEED, speed, value)
    speed = value

func set_stealth(value):
    emit_signal("stats_changed", Properties.STEALTH, money, value)
    stealth = value

func set_weight(value):
    emit_signal("stats_changed", Properties.WEIGHT, weight, value)
    weight = value

func get_current_speed():
    return speed - weight

func get_current_dodge_speed():
    return dodge_speed - weight

func get_stat_color(property : int):
    match property:
        Properties.HEALTH:
            return Color.DEEP_PINK
        Properties.MONEY:
            return Color.YELLOW
        Properties.WEIGHT:
            return Color.YELLOW_GREEN
        Properties.DEFENSE:
            return Color.DEEP_SKY_BLUE
        Properties.STEALTH:
            return Color.WEB_PURPLE

As you can notice, the code gets very repetitive. So, I thought of a dictionary of arrays/dictionaries/objects. The key would be an enum item (int), or string. And the value could be an array or dictionary. This way, I could link one property with others directly related, like the max value, current_max_health or the color, to not have to use a huge if statement for that. I have two max variables because in the game, your max health can be limited temporarily with certain effects.

When setting a new property with a custom method, the stats_changed signal would be called:

var stats := {
    Properties.HEALTH: [100, 100],
    Properties.DEFENSE: [10, 10],
    Properties.STEALTH: [0, 100],
    Properties.WEIGHT: [10, 100],
    Properties.SPEED: [100, 200],
    Properties.DODGE_SPEED: [180, 200],
    Properties.MONEY: [0, 999999],
}

signal stats_changed(property: int, old_value, new_value)

func set_stat(property: int, value):
    var old_value = stats[property][0]
    var max_value = stats[property][1]

    stats[property][0] = clamp(value, 0, max_value)

    emit_signal("stats_changed", property, old_value, stats[property][0])

func get_stat(property: int):
    return stats[property][0]

func get_max_stat(property: int):
    return stats[property][1]

I don't know if using a dictionary of objects is better instead, as not all properties have the same variables (Some don't have max value, some have two...).

It's one of those times I don't know if it's a great or awful idea. What do you think?

  • xyz replied to this.
  • SebSharper
    You can do it with dictionaries but they are kind of messy to edit in the inspector.
    As a general advice, always aim for architecture that results in simple and expressive code when used.

    For this, I'd make a property class derived from Resource, that contains everything that a property could have, then use only needed stuff for each specific property. Something like this:

    class_name Prop
    extends Resource
    
    @export var value = 1: set = set_value
    @export var min = 0
    @export var max = 1
    @export var color = Color.RED
    	
    func set_value(v):
    	value = clamp(v, min, max)

    Now you can simply declare a bunch of variables of that type in your Stats resource:

    class_name Stats
    extends Resource
    
    @export var health: Prop
    @export var speed: Prop
    @export var money: Prop
    # etc

    The code that uses this data will look simple and self explanatory:

    stats.health.min = 0
    stats.health.max = 10
    stats.health.value = 4

    SebSharper
    You can do it with dictionaries but they are kind of messy to edit in the inspector.
    As a general advice, always aim for architecture that results in simple and expressive code when used.

    For this, I'd make a property class derived from Resource, that contains everything that a property could have, then use only needed stuff for each specific property. Something like this:

    class_name Prop
    extends Resource
    
    @export var value = 1: set = set_value
    @export var min = 0
    @export var max = 1
    @export var color = Color.RED
    	
    func set_value(v):
    	value = clamp(v, min, max)

    Now you can simply declare a bunch of variables of that type in your Stats resource:

    class_name Stats
    extends Resource
    
    @export var health: Prop
    @export var speed: Prop
    @export var money: Prop
    # etc

    The code that uses this data will look simple and self explanatory:

    stats.health.min = 0
    stats.health.max = 10
    stats.health.value = 4

      xyz Thanks, I thought about that too. But I was unsure. How would I handle the signal, when the value chages, though?

      • xyz replied to this.

        SebSharper Thanks, I thought about that too. But I was unsure. How would I handle the signal, when the value chanegs, though?

        Depends on why you need the signal for. Do you need it at all?
        If your stats management is centralized then the code that manages the values should send the signal.

          xyz I need the signal to know when a value has changed, so I can display a floating text

          • xyz replied to this.

            SebSharper
            Prop can signal to Stats from the setter method and Stats can then pass the signal further.
            Prop:

            class_name Prop
            extends Resource
            
            signal value_changed(prop)
            
            @export var value = 1: set = set_value
            @export var min = 0
            @export var max = 1
            @export var color = Color.RED
            	
            func set_value(v):
            	value = clamp(v, min, max)
            	value_changed.emit(self)

            Stats:

            class_name Stats
            extends Resource
            
            @export var health: Prop
            @export var speed: Prop
            @export var money: Prop
            
            func _on_prop_value_changed(prop):
            	print("VALUE CHANGED ", prop.get_meta("name"))
            	# pass the signal further if needed
            
            func _init():
            	call_deferred("initialize")
            
            func initialize():
            	for p in get_property_list():
            		if p["hint_string"] == "Prop":
            			var prop = get(p["name"])
            			if prop:
            				prop.set_meta("name", p["name"])
            				prop.connect("value_changed", _on_prop_value_changed)