I want this post to serve as a warning for anyone new to Godot who, like me, comes from a background of object-oriented programming, and tries to do the right thing by inheriting targeted resources from more general, shared resources.
In both business apps and using some other game development platforms, it’s common to create a base class which holds some very vague, very general content: functions, properties, constants, etc. which can be used across many different classes. This is called a base class because through the concept of inheritance, we can then create other classes which build on top of this base class. In doing so, we can use the base class as a kind of foundation, and any other class which inherits from it will have access to, and will expose by default, the properties, functions, and constants defined on the base class. Here’s a (really bad) pseudocode example:
class BaseClass() {
property characterName: String
property characterClass: String
function TitleAndName() {
console.log(`I am ${characterName} the ${characterClass}`)
}
}
class WizardClass() inherits BaseClass {
property characterSpellPower: int
function NewWizard(name: String, spellPower: int) {
characterName = name
characterClass = 'Wizard'
characterSpellPower = spellPower
TitleAndName()
console.log(`I have ${} spell points!`)
}
}
var doug = new WizardClass.NewWizard('Doug', 100)
// Prints "I am Doug the Wizard', new line, and then "I have 100 spell points"
Our base class has properties common to all classes. It has a function (generally a constructor) which allows us to pass property values in when we first use the class. WizardClass inherits from the base class, so it has access to everything that the base class has to offer, but we’re also individualizing this WizardClass by adding a new property, characterSpellPower. While the wizard class has access to the two properties and the function defined in the base, the base does not have access to the properties, nor the function, defined in the derived class.
In my project, I have been thinking in this way when dealing with my resources. As resources are just scripts, I had been creating “base” versions which held common properties and functions. Then, I would create more specific resources based on that base resource. One example is for entities, where I’d have a BaseEntity that contains info such as…well…
# EntityBase is the shared Resource for Player and NPCs
extends Resource
### class_name
class_name EntityBase
### --- Signals
### --- Enums
### --- Constants
### --- Exported variables
export(String) var entity_id
export(String) var entity_name
export(String) var entity_portrait
export(MinimapIcons.Type) var minimap_icon
export(String, MULTILINE) var description
export(Resource) var hull
export(Resource) var cockpit
export(Resource) var armor
export(Resource) var cargo_hold
export(Resource) var power_plant
export(Resource) var fuel_tank
export(Resource) var jump_drive
export(Resource) var scanner
export(Resource) var shield
export(Resource) var thruster
export(Resource) var transponder
export(Array, Resource) var weapons
export(Array, Resource) var cargo_items
### --- Public variables
### --- Private variables
### --- Onready variables
### --- Lifecycle methods
### --- Public methods
### --- Private methods
Basically, everything that makes an entity an entity. I was then looking to derive from this with more specific resources, such as EntityPlayer:
extends EntityBase
### class_name
class_name EntityPlayer
### --- Signals
### --- Enums
### --- Constants
### --- Exported variables
export(String) var current_system_id
export(int) var wallet
### --- Public variables
### --- Private variables
### --- Onready variables
### --- Lifecyxle methods
### --- Public methods
### --- Private methods
In OOP, this is considered good form, because if there’s certain info all entities should contain, or activities that an entity needs to perform, it’s best to define them once rather than in near-identical copies of a file. If a change needs to be made, changing it in the base class will affect all classes that derive from it.
Godot’s Resource “Feature”
Godot’s resources are a very useful tool which kind of behave like an inherited resource. The resource script defines the properties, but it’s the actual resource file (TRES) which holds the data.
In my development, I had planned to create a “StaticBase” resource. This would contain the game version property and a data Dictionary property, with the purpose being that Big Bang data would be stored in instances of this resource, things like the system map, planets, jumpgates, and stations. On top of the base resource, I added individual variants which had functions inside that were specific to working with the data that those instances hold: StaticSystems, StaticPlanets, StaticStations, and StaticJumpgates, all of which derive from StaticBase.
Here’s the curve ball: Once Godot loads a resource, that resource is cached so that subsequent calls for that resource in any way simply return the cached resource instance.
Seeing the issue? I certainly didn’t.
In my mind, each variation resource was its own creature. In fact, I originally created each resource as its own resource in four individual scripts and everything was fine. It wasn’t until I tried to rework the system so that each of the four resources derived from a common base resource that things went sideways. My goal was to have data loading and saving features inside the base resource so that each themed resource could manage its own lifecycle.
The problem, I believe, is that even though Godot is told to load up the specific resource instance — StaticSystems, StaticPlanets, StaticStations, and StaticJumpgates — the fact that they all redrive from StaticBase means that Godot is caching them all — including StaticBase. See, when I attempted to load up StaticSystems and assign solar system data at the end of the Big Bang, it went fine as the first data assignment. As the Bang moved on to generating planets, and assigning them to StaticPlanets, I got errors that my keys were all off. When I added breakpoints and checked the internal variables at runtime, the data property — defined within the StaticBase — of the StaticPlanets was showing the data previously assigned to StaticSystems. Godot had cached StaticBase with the first values assigned therein, so attempting to use that data, expecting planetary data, returned solar system data instead.
In hindsight, this makes sense from Godot’s “I told you so” perspective: each resource, no matter where it is, or how it got into the runtime, is treated as a first-class resource. StaticBase is not merged into a more unique StaticSystem or StaticPlanet; the engine recognizes three resources: Base, Systems, and Planets. Each one is cached on its own, so any properties defined in Base are shared between Systems, Planets, Stations, and Jumpgates.
In some ways, this could lead to some questionable but interesting dynamics. If I were a gambling man, I could define one property for each data bucket on the Base, and then keep more specialized derivatives for working with the buckets. Of course, at that point, I could just have a Base resource and cram all functions in there. Six of one, half-dozen of another. My solution was to move the data properties into the derived resources but left the loading and saving methods in the base resource. I can call loading and saving front-end functions defined in each derived resource which basically just pass the data along from their individual data properties to the base class heavy lifters, and everything works…Just not as ideally as I had hoped it would.
Other Godot users might have run into this and not known what was going on. Other Godot users might have always known, or maybe surmised, or guessed, or assumed. As I said, looking back on it now, everything is working “as advertised”, but I’m a bit let down that the engine doesn’t recognize the end product of a inherited resource as a completely unique entity.
2 Comments
DurzoB4
April 4, 2023 - 5:43 PMHow did you get around the issue of the base Resource being cached and reused by your derived classes?
Scopique
April 6, 2023 - 7:01 PMUnfortuanately, I didn’t. I have put this project on the back burner, but other projects using Godot 4.0 haven’t reached this stage quite yet. The bottom line is that I don’t know/think standard OOP inheritance is possible with Godot, but I don’t know if anything has changed in 4.0.