Using Resources for Character Stats: Does This Approach Make Sense?

Godot Version

4.5 Stable

Question

I want to use a Resource to define all my characters’ stats, then save those as .tres files to be read from at runtime. Basically, like this:

class_name StatData
extends Resource

@export var max_hp: float
@export var attack: float

But I have a few issues.

  1. Stats in my game aren’t just simple floats, but RefCounteds with their own data like lists of modifiers, methods to recalculate, etc.
  2. I’m pretty sure I shouldn’t and don’t want to use the actual Resource itself at runtime when changing the stats, like by dealing damage or equipping items. I hear it’s bad practice to modify Resources at runtime, and I can see why. I want the Resource to be a simple data container that lets me set up the base stats, and maybe I can even read from it during the game to display said base stats for each character, but I don’t think I should be changing it around in-game. I feel like I should have a separate class like a RefCounted or even a Node that gets modified instead. Right?

So, I ended up doing something that feels kind of silly. I have the Resource shown above, and then I have a node called a StatManager that reads from that at runtime to set itself up, like this:

class_name StatManager
extends Node

var max_hp: Stat
var attack: Stat

func setup(stat_data: StatData):
     max_hp = Stat.new(stat_data.max_hp) #sets the base value of the Stat class
     attack = Stat.new(stat_data.attack)

Then I got to thinking… this feels kind of convoluted and tightly coupled. It is, right? If it is, then how should I go about creating a class that’s safe and simple to modify during runtime with the stats in it?

One idea is to use inner classes. I think this might be good, but I’m asking here. Like this:

class_name StatData
extends Resource

@export var max_hp: float
@export var attack: float

class Stats:
     var max_hp: Stat
     var attack: Stat

func create_stats() -> Stats:
     var new_stats := Stats.new()
     new_stats.max_hp = Stat.new(max_hp)
     new_stats.attack = Stat.new(attack)
     return new_stats

...

class_name StatManager
extends Node

var stats: StatData.Stats

func setup(stat_data: StatData):
     stats = stat_data.create_stats()

This fixes the coupling issue. I’m just not sure if it’s overly complicated. Does this seem reasonable?

I think I was actually told by someone much more experienced than me that the above method would work for me, if I’m not mistaken about what they meant. I just didn’t get it yet at the time.

The reason I’m not just directly exporting from within the node is that I am keeping my data separate from some of my generic enemy scenes, so I’m purely just using .tres files to define one base scene for multiple enemies instead of making a scene for each one. This is important.

What are your thoughts?

1 Like

There is nothing inherently wrong with modifying resources during runtime, as long as you are aware of what that implies.

Godot will only load a resource once, and then pass that instance to all object using it. That is very useful if you have, for example, a default Theme used by many nodes and scenes throughout your project. It won’t have to be loaded again and again every time a node needs it.

What this also means, is that if you modify something in that resource, it will get modified for all objects currently using it, since they all reference the same instance:

# stats.gd

class_name Stats extends Resource

@export var health: int
@export var mana: int

Create orc_stats.tres and set default stats for each orc in the game:

Create orc_warrior.gd. It works the same way if you have a Node instead of a RefCounted, @export var stats: Stats, and drag the orc_stats.tres resource into it:

class_name OrcWarrior extends RefCounted

var orc_name: String

# Load the resource containing the default orc stats
var stats: Stats = load("res://orc_stats.tres")

Create two different orcs:

# main.gd

func _ready() -> void:
	# Create two new orcs
	var orc_mike := OrcWarrior.new()
	var orc_tina := OrcWarrior.new()
	
	# Change the mana for Mike
	orc_mike.stats.mana = 100
	
	# Print the mana for Tina
	print(orc_tina.stats.mana)

This will print 100 instead of the expected 25, because there is only one instance of orc_stats.tres loaded. This is expected.

If you want unique resources created from the same .tres file, one way to do that is to duplicate it. For example, in orc_warrior.gd you would write:

var stats: Stats = load("res://orc_stats.tres").duplicate()

The same thing can be done in the inspector by right-clicking the resource and selecting Make Unique.

You could also avoid all of that, by simply writing:

var stats := Stats.new()

This will create a new unique resource, which you will have to initialize to default values yourself, either by reading them from a file, or simply assigning the values in the class’ _init() method. Most likely though, this would be done by a spawner-like object.

There are many ways to approach this. I suggest looking into data driven development.

I’m not sure I understand this. Why doesn’t having a generic scene with default stats, exported so you can modify it from the editor, work for you in this case?

Thanks for the reply!

So, what you’re saying is that essentially, I can also just make my Stats resources, too, which export things to customize, and nest them in a general StatsData resource that contains all the stats for a character. Then I can just use duplicate(true) to get a deep duplicate at runtime, yes? So like this:

class_name Stat extends Resource

@export base_value: float

...

class_name StatData extends Resource

@export max_hp: Stat
@export attack: Stat

Then let’s say I put THAT inside a data resource for enemies, and then create a .tres file for a specific enemy with it called orc_warrior.tres.

Then I could just do this (similar to what I’m actually doing in my game):

#enemy_spawner.gd

func spawn_enemy(enemy_data: EnemyData):
     var new_enemy = generic_enemy_packedscene.instantiate() as Enemy
     new_enemy.data = enemy_data.duplicate(true)
     #The scene will set itself up with enemy_data on _ready
     add_child(new_enemy)

And for example, just pass orc_warrior.tres into that function to spawn an orc warrior. Perhaps the enemy scene can actually just duplicate the resource on its own. Does this sound right?

This seems great, but I just heard from various places not to use duplicate for this… for some reason. Like that in principle, custom Resources are just for containing data and nothing else and not meant to be modified. I still don’t really get why I was told that.

Sorry, I was probably just not clear. What you said is exactly what I’m doing. I have a generic scene with default stats which I can modify from the editor. It’s just that, rather than exporting something like StatData on the StatManager node, I just export a single variable for something like EnemyData (for other stuff like a sprite and shape2D) in the root node. So I just create everything there. All I meant was that I wasn’t creating individual scenes for each enemy and just setting each of their stats in the StatManager node directly. Instead, I’m saving the .tres files that I create in the base scene, then reading from them at runtime. I just wanted to say that I must save .tres files. So your solution works perfectly fine for me, assuming duplicating resources is okay!

1 Like

I’ve use Resources for character stats before. I saw it in a tutorial and thought it was a great idea. I used it in a project, and it worked great. But then in future projects I found it to be unwieldy. What’s the point of storing speed, for instance, in a complex Resource when I could just attach it to the character?

I have found it is useful for things like spells, abilities and appearances (skins). I can store complex information and then apply these things to characters. For statistic like Str, Dex, Con, Wis, Int, Cha, hp, etc I find Resource objects to be overkill.

Yes that should work fine.

It’s just a great way to quickly demo a potential use case. It also helps keep things clean in the character class, and opens the door for composition.

1 Like

Actually, I do see an issue now that I’ve started fiddling with it a bit.

Let’s add one layer of complexity and say that each Stat needs a min and max value, and I want to make sure that some Stats have the exact same min and max value for everyone.

So if I had this:

class_name StatData
extends Resource

@export var max_hp: Stat

And this is a Stat:

class_name Stat
extends Resource

const MIN := -999.0
const MAX := 999.0

@export var base_value: float
@export_range(MIN, MAX) var min_value: float
@export_range(MIN, MAX) var max_value: float

Then I go to create a character’s max_hp. Now I have to put in what the min and max values are. But… I actually don’t want that. I want to make sure, for example, the min for any character’s max hp is ALWAYS 1.

So instead, I guess it might be a potential solution to make StatData like this, at first glance:

class_name StatData
extends Resource

const MIN := -999.0
const MAX := 999.0

@export var max_hp_base: float

var max_hp: Stat

func setup_stats():
     max_hp = Stat.new(max_hp_base, 1.0, MAX)

And then at runtime, after duplicating the .tres file, you run setup_stats() on it.

Which, I mean… I guess that would work, but then the character has access to all this extra stuff they don’t need, like the max_hp_base variable and the function. That doesn’t seem ideal.

So this would bring me back to my inner class idea which I had in the original post. Or maybe that could be combined with using Stats as Resources somehow? I’m not really sure.

You can have defaults:

class_name Stat
extends Resource

const MIN := -999.0
const MAX := 999.0

@export var base_value: float
@export_range(MIN, MAX) var min_value: float = 1 # Here
@export_range(MIN, MAX) var max_value: float = MAX # And here

You can also have setter functions that make sure when you modify a property it stays within a certain range. For example:

var health: int:
	set(new_value):
		health = clampi(new_value, MIN, MAX)

You might also be overcomplicating things with having each stat be a resource that simply holds a float, but I suppose it depends on the project.

The problem with that is that the Stat resource is generic. The default min might be 1.0 for max_hp, but not for say, defense.

This is just what I have right now and is not really the final version, as I’m still working through this problem. But just to give you an idea. (I call stats “attributes” by the way lol, I was just saying “stat” here because I thought it would be more understandable)


(More is cut off, but you get the idea)

If an Attribute is a generic class, create specific ones for each. We’ll just go with basic examples: health, mana, etc. Each extends Attribute and has custom stuff inside.

Then in the basic StatData resource you have:

@export var health: HealthStat
@export var mana: ManaStat
# ...

You can have generic methods in the base class, and you can override them in the other flavors of Stat such as HealthStat.

Again, many ways to go about it. It’s just a question of design.

2 Likes

Yeah I also have found composition, like having a health component to be inferior in actual use in Godot. Because now I have to tie signals or functions from the player/enemy to the health component, or I have to break encapsulation to allow whatever does damage to know about and be dependent upon the implementation of health components.

Just my opinion.

Inner classes IMO are a poor programming pattern. They obfuscate your code. More than once as part of a paying job, I have had to read code written with inner classes by someone else and fix it (in GDScript, C++, and Java). Inner classes increase the cognitive complexity of code and do not provide any compiler benefits.

Ostensibly, inner classes are supposed to help by creating classes that only their outer class can access, but in GDScript there are no private/protected accessors. Everything is public. They are also supposed to allow separate inheritance from their outer classes, but AFAIK that is not supported in GDScript.

Creating classes as their own files is helpful when you go back to fix code written months ago - whether by you or someone else. And in fact, Resource objects are such classes, and a more appropriate way to handle something that you want to be an inner class. Remember, Resource objects are not C Structs. They contain data, logic, and an interface.

1 Like

Don’t introduce new classes for things like Stat. That’s too much granularity. Always flatten your hierarches and dependency chains as much as possible.

You can put all functionality flattened into a resource class.

You can use this resource in two ways. Either as a static data container that initializes properties in other objects (typical use) or you can store both; initializer values and actual live data there. In the latter case you’ll need to duplicate the resource per use. There will be some redundancy there but it may be an acceptable price for flattening dependency/reference chains and reducing the number of classes in your system.

1 Like

I see, this makes sense to me! So I guess then for me, each specific class would pretty much literally just define the min and max in the _init function. I don’t think I would need to do anything else there.

I have one final caveat, and that is that player characters (of which there are multiple to choose from and each has their own scene) each have their own unique personal stats. So Bob might have bob_power as a stat. I guess with this method, I really could just create a whole script extended from Attribute for BobPower. But I’m wondering if that’s going to be a headache to keep track of. Would be nice if I could just put BobPower in the Bob scene somewhere…

Hm. Actually, I easily could. I could just export a variable for it and set it up in his base script by using the resource. Never mind. That’s not an issue, then. I guess this is sounding like the best solution so far!

I thought about this for a bit. And yes, it would totally be preferable to reduce the number of classes and simplify things. Though, isn’t the first way you’re talking about just the first thing I have in the original post?

A static data container that initializes properties in another object. This would be ideal in my opinion. But what does that actually look like?

And if I don’t have a class for Stat, where does all the functionality for recalculating, storing modifiers for stats, etc. go?

Do I just have a StatManager node full of floats that have setters and getters, and arrays of modifiers for each of them, and all the logic for handling them in the node? Then have a separate Resource that is coupled to that and used to initialize the floats? I don’t think that’s what you mean, because that feels like a bit of a mess… I felt like stats in my game needed their own class because they interact with each other and have a lot of other functionality attached to them. You can kind of see a general idea of what I mean in a picture I sent above.

Looking around online, it seems like a very common pattern to have a stat class in any given language or engine, so long as your stats need that complexity and aren’t just numbers.

It pretty much is. I just systemized it :smiley:
Don’t overthink and just do it like that. It’ll be fine.

With the static data container, each object that receives that data is responsible to do with it whatever they need. The resource in this case works like an initializer.

And don’t introduce any “managers” because you’ll again cram in proxy abstract classes that really serve no purpose.

If a character needs stats, it gets them from the resource and then handles the rest itself.

class_name Stats extends Resource

@export var hp_min := 1
@export var hp_max := 10
@export var hp_initial := 5
class_name Character

@export var stats: Stats
var hp: int
	set(value):
		hp = clampi(value, stats.hp_min, stats.hp_max)

func _init():
	hp = stats.hp_initial

I think this is pretty much what @zigg3c already suggested.

1 Like

Alright, thank you, I do like this better. When I was doing it, it just felt wrong somehow to have the character rely on whatever was in the resource, but I guess… I’m just overthinking it. This is really the simplest thing! I think I’ll just do it like this after all.

I just don’t think in my case that simple floats will do. Stats need to have a base and current value (the base is used to recalculate them), arrays of modifiers to use when recalculating, other linked stats to use when recalculating, a recalculate function, signals for when they change, signals for when modifiers are added to them, and modifiers also can be modified themselves (they just extend Stat), so they need to connect their own signals to the recalculate function of the stat they modify…

I might be able to separate out some of that stuff into the Character class instead (for example, maybe only the character needs a “recalculate_stat” function and it doesn’t need to be in the stat itself…?)

But! I totally get what you’re saying, and yeah, I don’t need a weird proxy abstract class. I’ll get rid of that, and also simplify my StatData resource the way you’re saying, and just keep what I was doing with having objects read that data to do whatever they need with it.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.