One of Godot’s oddest features must be scene inheritance.
Any scene in Godot can be extended and used as the base for another scene.
(Scene inheritance example)
The nodes in yellow are carried over from scene A to B. You could think of this as creating a kind of “scene template”.
But, at the time of writing, it is Godot’s black sheep:
As such, their inclusion in Godot’s feature set is odd. And, the community’s general response to scene inheritance is to avoid it!
But what could we use them for? Is there anything that they could excel at? Must we fear what we not understand?
Allow me to take you on my journey of discovering that scene inheritance can indeed be a very viable data model!
For me…
Godot development is a balancing act of data and systems.
I want to use simple data models for content, like holding an entire weapon definition in one folder (visual and behavior).
To this extent, Godot expects you to use custom Resources.
Define a custom Resource class, add whatever exports you’d like, and then they can be manipulated in the inspector.
They are very versatile! However, Resources have maintenance limitations for gameplay modelling.
To demonstrate, let’s use data to define a badass scythe.
(Badass Scythe)
Yes… our scythe truly is badass.
This data model is textbook: we define the 3D model in another scene, the gameplay damage/swing duration, the point where our character holds the weapon, and the swing/hit sounds.
The root node of the player weapon scene might have a script like this for initializing itself:
class_name Weapon
extends Node3D
@onready var swing_sfx: AudioStreamPlayer3D = %SwingSFX
@onready var hit_sfx: AudioStreamPlayer3D = %HitSFX
var model: Node3D
var def: WeaponDefinition
func set_weapon(_def: WeaponDefinition):
def = _def
model = _def.model.instantiate()
add_child(model)
position = _def.grab_point
swing_sfx.stream = _def.swing_sound
hit_sfx.stream = _def.hit_sound
And this works! So here’s why it sucks:
grab_point
.@tool
script, then use emit_changed()
on the setter of the grab_point
export, then update Weapon.set_weapon
to listen to the changed signal of WeaponDefinition
and update the grab point.
(There are some exports missing)
@export_group("Swing Sound")
@export var swing_sound: AudioStream
@export_range(-80.0, 6.0, 0.1, "suffix:dB") var swing_sound_volume := 0.0
@export_range(0.1, 100.0, 0.1) var swing_sound_unit_size := 12.0
@export var swing_sound_attenuation_model := AudioStreamPlayer3D.ATTENUATION_LOGARITHMIC
@export_range(0.0, 3.0, 0.1) var swing_sound_panning_strength := 1.0
@export_range(1, 20500, 1, "suffix:Hz") var swing_sound_cutoff_hz := 5000
@export_range(-80, 0, 0.1, "suffix:dB") var swing_sound_cutoff_db := 0
@export_group("Hit Sound")
@export var hit_sound: AudioStream
@export_range(-80.0, 6.0, 0.1, "suffix:dB") var hit_sound_volume := 0.0
@export_range(0.1, 100.0, 0.1) var hit_sound_unit_size := 12.0
@export var hit_sound_attenuation_model := AudioStreamPlayer3D.ATTENUATION_LOGARITHMIC
@export_range(0.0, 3.0, 0.1) var hit_sound_panning_strength := 1.0
@export_range(1, 20500, 1, "suffix:Hz") var hit_sound_cutoff_hz := 5000
@export_range(-80, 0, 0.1, "suffix:dB") var hit_sound_cutoff_db := 0
func set_weapon(_def: WeaponDefinition):
...
swing_sfx.stream = _def.swing_sound
swing_sfx.attenuation_model = _def.swing_sound_attenuation_model
swing_sfx.volume_db = _def.swing_sound_volume
swing_sfx.unit_size = _def.swing_sound_unit_size
swing_sfx.panning_strength = _def.swing_sound_panning_strength
swing_sfx.attenuation_filter_cutoff_hz = _def.swing_sound_cutoff_hz
swing_sfx.attenuation_filter_db = _def.swing_sound_cutoff_db
hit_sfx.stream = _def.hit_sound
hit_sfx.attenuation_model = _def.hit_sound_attenuation_model
hit_sfx.volume_db = _def.hit_sound_volume
hit_sfx.unit_size = _def.hit_sound_unit_size
hit_sfx.panning_strength = _def.hit_sound_panning_strength
hit_sfx.attenuation_filter_cutoff_hz = _def.hit_sound_cutoff_hz
hit_sfx.attenuation_filter_db = _def.hit_sound_cutoff_db
THIS MAKES ME DIZZY!! Maybe we could make a custom AudioStreamPlayer3DData
resource to hold these parameters instead, but we still haven’t resolved the opaque preview issue.
Let me reiterate:
Godot development is a balancing act of data and systems.
In this case, I feel as though Resources have failed us. Our data and systems are clashing, and we are forced to design through a very opaque interface. And representing something simple like audio requires a lot of boilerplate.
In general, custom Resources are suboptimal for modeling nodes. If only there was a Resource that was designed for modeling nodes…
… Hey, PackedScenes are Resources!
Let’s not reinvent the wheel here. Let’s use a scene to model our badass scythe.
(This represents the tricky WeaponDefinition data)
Suddenly, our problems go away:
Unfortunately, this approach backslides and introduces new issues:
WeaponDefinition.model
can now reference our new scythe scene instead.For this scenario, Scenes are better than Resources, yet imperfect. There is a lot of maintenance involved in creating new weapons and adding features to existing ones. God will kill me for adding ambient sounds after I make Badass Scythe #37.
We are stuck making scenes with no reference nodes, and we must suffer through the maintenance hell of adding any feature late in development.
If only there was a way for us to create a “scene template” for the scythe. Then, we could work off of this template for creating a new weapon, or modify the base template to add a MissSFX node.
This is precisely what Scene Inheritance is fantastic for.
Scene Inheritance allows us to create “base” scenes for templating pieces of content.
We can assume that all weapons will have a hit sound, a hurt sound, a grab point, etc etc. So we can define these nodes in our base weapon scene!
Then, we can extend from it later to easily setup a new weapon, or add new nodes in this base scene to quickly implement new features.
We also are given new tools at our disposal: if we create a new audio bus for weapon sounds, then we can modify AudioStreamPlayer3D.bus
on the base scene, and the change is (ideally) propagated to all weapons.
Here’s a quick example for how far you could take this: what if we have different weapon categories?
(Layers of weapon scenes)
Our pencils, swords, and scythes may have exclusive functionality. Scythes may require special Control UI for charging, or pencils could have a rapid attack mechanism that could be modelled within the weapon scene itself.
And all of these categories can share the expectations that weapon_base.tscn
declares.
If you’re still confused on when you would want to use scene inheritance, I have a golden rule for you:
If your data models themselves start to model a scene, consider using scenes or scene inheritance instead.
Scene Inheritance, being both undocumented and underutilized, gets no attention, and remains unstable.
From my research, it appears that one of the main appeals of scene inheritance (clean property propagation) is bugged in nested scene inheritance. See these issues for more information: #1 #2. There may be other issues I don’t know about yet either.
This sucks so bad!! On paper, scene inheritance could be a development game changer!
Still, this pattern can be utilized. There’s a few workarounds:
I also wish that scene inheritance had more refactoring options, such as splitting a base scene out of an existing one, or mapping an existing scene onto a base scene.
Scene Inheritance is a strong way of developing and maintaining node heirarchies throughout the development lifecycle, making them very viable for developing content in mass.
Unfortunately, they are currently littered with issues. However, they are still not as bad as they are made out to be — under the hood, model imports can use inherited scenes, and I consider this to be one of the more stable parts of Godot.
I have also had personal success using them in my own projects, even if maintaining them is harder than they should be, due to having to manually reverify all property propagation.
Hopefully raising awareness about their functionality will help bring more attention to improving and documenting them. I believe they could become a very viable tool for a Godot developer.