How to save customized instances of a base scene to disk without scene inheritance?

Godot Version

4.5.1

Question

I have a base Actor scene, containing things that will be common to all actors like its sprite node and health bar. I need to have different types of actors - different kinds of monsters and NPCs - using this base scene. I put everything unique to a type of actor like its stats and texture in an ActorData resource that the Actor scene can load.

When building levels, I’d like to drag and drop actors into the editor. At first what I’d do is make an inherited scene based on the main actor scene where I’d set its ActorData export, except that scene inheritance is supposedly buggy.

The only alternative I can come up with though for level building is adding instances of the main actor scene and manually assigning their actor data. Is there another way to do this workflow?

To illustrate, this is my main actor scene:

This is an actor data resource:

And this is me editing a level. Here I’m creating instances of the actor scene and manually assigning actor data files to them:

Other strategies I’ve heard of is (a) having Actor be a single tool script class instead of a whole scene that creates all its necessary child nodes in code, and (b) somehow automating the process of adding an actor instance to the scene being edited with the right actor data assigned (so essentially an editor plugin).

Note: I also asked this in the Godot discord but wanted to see if I can get other answers here.

Why is manually assigning the resource a problem? Assign once and then just duplicate the scene instance.

I haven’t experienced any bugs in scene inheritance. It’s after all the main mechanism used for 3d asset import. In what way is it buggy?

I just don’t like how I need two steps - adding the actor scene itself, then selecting the actor’s data to make it a valid actor - to add actors to my levels when editing them. I was hoping there’d be a more streamlined way.

Last time I did use scene inheritance, whenever I made changes to the base scene the changes did’t always seem to get cleanly propagated to the child scenes. And I’ve read people elsewhere recommend against using scene inheritance.

Then again when I last tried using scene inheritance I was doing more extensive changes to the child scenes like adding new nodes and so on, instead of bundling all customizations in a single resource file. So maybe in this particular case it won’t be a problem?

Don’t use the resource then. Put all properties directly into the actor class. Configure once and duplicate.

You can make a tool script that packs the configured scene and saves it as a scene resource.

I’m not sure I understand. You’re suggesting to:

  • Take all the properties I have in my resource file - stats, texture, etc. - and put them in the actor class itself
  • Whenever I need a new type of actor, duplicate the base actor scene file and in the duplicate make all the customizations I need

Is that right?

That’s the easiest way, yes. If you don’t want to do that then you’ll need to automate it through a plugin.

Example:

@tool
extends EditorPlugin


var drag_data: Variant


func _notification(what: int) -> void:
	match what:
		NOTIFICATION_DRAG_BEGIN:
			# Get the drag data when a drag begins
			var data = get_viewport().gui_get_drag_data()
			# Check that it's a dictionary and has a "files" value
			if data is Dictionary and data.has("files"):
				drag_data = data

		NOTIFICATION_DRAG_END:
			# Check that we have drag_data when a drag ends
			if not drag_data:
				drag_data = null
				return

			# Check that we the drag ends over the 2d viewport editor
			var hovered = get_viewport().gui_get_hovered_control()
			if not hovered.get_class() == "CanvasItemEditorViewport":
				drag_data = null
				return

			# Grab the first file in the "files" entry
			var filepath = drag_data.get("files", [""])[0]
			# Load it
			var res = ResourceLoader.load(filepath)
			# Check that if it's an ActionData resource
			if res is ActorData:
				# Grab the root node in the edited scene
				var root = EditorInterface.get_edited_scene_root()

				# Instantiate the actor scene
				var actor = preload("res://test_actor.tscn").instantiate()
				# Load the ActorData resource into its actor_data
				actor.actor_data = res

				# Add the actor scene to the root node
				root.add_child(actor, true)
				# And setup its owner
				actor.owner = root

				# Grab the mouse position of the viewport
				var mouse_position = EditorInterface.get_editor_viewport_2d().get_mouse_position()
				# And set the actor's global_position to it
				actor.global_position = mouse_position

			# Clean up drag_data
			drag_data = null

Result:

1 Like

On the one hand I was hoping a whole editor plugin wouldn’t be necessary. On the other hand your example is far easier than I thought it would be (I’m fairly new to editor plugins) and I was kind of thinking about having an editor plugin related to adding actors to a level scene during editing anyway for other reasons.

Another strategy I thought of was having an “ActorProxy” script class that can be saved with an actor data resource and is what gets added to the level scene during editing, then at runtime when loading the level the actor proxy creates the true actor with the assigned data resource.

I was also partly looking to confirm whether or not inherited scenes really are as buggy as I thought they were. Or if I and others simply misused them somehow.

They aren’t buggy, they just stop being affected by the parent scene as soon as you alter an inherited node. This causes people no end of issues.

With any alteration of any inherited node?

Going back to my own example: My Actor scene has an ActorData export in its root script, and a child Sprite2D node. ActorData is a Resource that contains a Texture2D. When an Actor’s ActorData is set, it assigns the texture from the data to its child Sprite2D (the scene’s script is a tool script). If I create and save a scene that inherits Actor, would assigning it an ActorData count as altering an inherited node?

Basically I’m wondering what changes to an inherited scene I can make. Where if I need to make changes to the base scene later the changes are propagated to the child scenes with no issues.

It’s easy enough to test this.

No. Changing values assigned to exported values doesn’t affect it.

Now let’s say you assign a texture to the Sprite2D and it’s a little high, so you move the Sprite2D down 5 pixels - (0,5) transformation of the Sprite2D node. That version of Actor will no longer inherit any future changes made to the Actor base scene. Ever.

You’ll have to excuse @normalized who is a big fan of the “teach a man to fish” methodology. Or as I like to put it the “build a fire method”: “Build a man a fire, he’s warm for a night. Set a man on fire, he’s warm for a lifetime.” :smiley:

2 Likes

Hey, enough with those cavemen analogies! It’s called Socratic method in the civilized world of ancient Greek philosophy. Now, I’m perfectly aware of its shortcomings; utilizing it can end up with being forced to drink hemlock poison for the crimes of corrupting the youth :clown_face:

3 Likes

Asclepius got his Rooster.

3 Likes

So when I make an inherited scene, the child scene stays in sync with the parent scene after changing the properties of the root node but not after changing the properties of any child nodes?

What if the scene’s root script is a tool script and one of the exports is a setter that modifies the child nodes?

My Actor scene has an ActorData export at the root and a child Sprite2D node. My ActorData resource has a Texture2D export. When I assign an ActorData to an Actor the Actor script assigns the texture from the ActorData to its Sprite2D. Would this cause a desync in an inherited scene?

# actor_data.gd
@tool # Was getting errors in the Actor scene without this
class_name ActorData
extends Resource


@export var sprite: Texture2D

...
# actor.gd
@tool
class_name Actor
extends TileObject # TileObject extends Node2D


@export var data: ActorData:
	set(value):
		data = value
		_init_data()


@onready var _sprite := $ActorSprite as Sprite2D


func _init_data() -> void:
	if not is_node_ready():
		await ready

	# Does this cause a desync from the parent scene in an inherited scene?
	_sprite.texture = null
	if data:
		_sprite.texture = data.sprite
		...

...

No. The only thing you can change are exported variables on any of the nodes. You can add new nodes, and the entire scene will continue to inherit.

In that case, I would recommend that the setter create and add those nodes for you. Then you won’t have any issues.

Here’s an example from my CurvedTerrain plugin.

The actual CurvedTerrainObject was originally a Path2D root node, with a Polygon2D, a Line2D, a StaticBody2D with a CollisionPolygon2D shape. However as you can see from the screenshot, none of those show up in the tree. This is the code:

@icon("res://addons/dragonforge_curved_terrain_2d/assets/textures/icons/curved_terrain.png")
@tool
## Creates curved terrain by using a tile for the fill of terrain, and a tile for the edge of the 
## terrain. Also allows the setting of collision information so that objects can interact with it.
class_name CurvedTerrain2D extends Path2D

## The image to fill the inside
@export var fill_texture: Texture2D
## The image to use as the edge of the terrain.
@export var edge_texture: Texture2D
## The thickness of the edge terrain. Adjusting this will change the look and smoothness of the edge.
@export var edge_depth: float = 30.0
@export_category("Collision")
## The physics layers this [CurvedTerrain2D] is in. Curved terrain objects can exist in one or more of 32 different layers. See also [member CurvedTerrain2D.mask].
## [br][br][b]Note:[/b] Object A can detect a contact with object B only if object B is in any of the layers that object A scans. See Collision layers and masks in the documentation for more information.
@export_flags_2d_physics var layer: int = 1
## The physics layers this [CurvedTerrain2D] scans. Curved terrain objects can scan one or more of 32 different layers. See also [member CurvedTerrain2D.layer].
## [br][br][b]Note:[/b] Object A can detect a contact with object B only if object B is in any of the layers that object A scans. See Collision layers and masks in the documentation for more information.
@export_flags_2d_physics var mask: int = 1
## Turns visible collision shapes on and off in the editor. Has no effect in the game.
@export var show_collision_shape: bool = false

var polygon_2d: Polygon2D
var line_2d: Line2D
var collision_static_body_2d: StaticBody2D
var collision_polygon_2d: CollisionPolygon2D


func _ready() -> void:
	DPITexture
	curve.bake_interval = 20
	polygon_2d = Polygon2D.new()
	polygon_2d.texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
	add_child(polygon_2d)
	
	line_2d = Line2D.new()
	line_2d.texture_repeat = CanvasItem.TEXTURE_REPEAT_ENABLED
	line_2d.texture_mode = Line2D.LINE_TEXTURE_TILE
	add_child(line_2d)
	
	collision_static_body_2d = StaticBody2D.new()
	add_child(collision_static_body_2d)
	collision_polygon_2d = CollisionPolygon2D.new()
	collision_static_body_2d.add_child(collision_polygon_2d)
	
	texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
	_regenerate()


func _process(_delta: float) -> void:
	if not Engine.is_editor_hint():
		set_process(false)
		return
	_regenerate()


func _regenerate() -> void:
	if !curve or curve.point_count == 0:
		collision_polygon_2d.polygon = []
		polygon_2d.polygon = []
		line_2d.points = []
		return
	
	var points = curve.get_baked_points()
	var collider_points = curve.get_baked_points()
	
	if points.size() > 1:
		points.append(points[1])
	
	collision_static_body_2d.collision_layer = layer
	collision_static_body_2d.collision_mask = mask
	
	polygon_2d.polygon = points
	polygon_2d.texture = fill_texture
	
	line_2d.points = points
	line_2d.texture = edge_texture
	line_2d.width = edge_depth
	
	if collider_points.size() > 2:
		collision_polygon_2d.polygon = collider_points
	collision_polygon_2d.visible = show_collision_shape

All the things I want to be able to change are raised up to @export variables on the root node in the inspector:

Based on your code, no, but I’d do it a little differently.

# actor.gd
@tool
class_name Actor extends TileObject # TileObject extends Node2D

@export var data: ActorData:
	set(value):
		data = value
		if not is_node_ready():
			await ready
		_sprite.texture = data.sprite # If data is null, the sprite will be set to null anyway.

@onready var _sprite := $ActorSprite as Sprite2D

Formatting-wise, I try to follow the GDScript style guide, so only one space between different variable types (@export, var, @onready). However while it’s not in the style guide, it is common practice to put the class_name and extends on the same line. (I think it reads better.)

In my code example, I don’t have to wait for the node to be ready, because I initialize everything in _ready(). Since you have a tool script, and you’re setting a value that may not be constructed yet, you definitely need to wait for it to be ready. It typically won’t be an issue when editing, but it will be when the object is constructed in the game.

That’s why in my script I’m constructing the subnodes inside the _ready() function. But I’m using the _process() function instead of setters, because that way I’m assured that any changes in the editor are caught. Then I turn the _process() function off when the game is running.

I’m…still unclear about what I can or cannot alter in an inherited scene and still have the scene sync with the parent scene. Earlier you said:

So it sounded like you said altering any child node in the scene causes the scene to desync, while altering the root node doesn’t.

Note that when I talk about altering a node I just mean general editing like modifying its properties in the inspector or moving and rotating it around in the main editor view.

But now you just said:

Which sounds like you’re saying I can alter the child nodes (and add new nodes) in the inherited scene? I’m unclear about your phrasing here.

Basically, what exactly can I do or cannot do to an inherited scene and still have it be in sync with the parent scene?

Anyway, your change to my Actor code merely got rid of the _init_data() function, which actually contains more logic than just setting the sprite texture. Here’s the whole thing just so I’m clear, and it’ll probably have more added to it as development continues and I develop actors and actor data further:

...

var _stats: Stats

...

@onready var _sprite := $ActorSprite as ActorSprite
@onready var _stamina_bar := %StaminaBar as Range

...

func _init_data() -> void:
	if not is_node_ready():
		await ready

	_sprite.texture = null

	if not data:
		return

	_sprite.texture = data.sprite

	_stats = Stats.new(data.base_stats)

	_stamina_bar.max_value = stats.max_stamina
	_stamina_bar.value = stats.stamina
	_stats.stamina_changed.connect(_on_stamina_changed)

I’m also trying to understand CurvedTerrain2D class. It seems instead of using setters to initialize things based on the export variables, you’re enabling processing when the script is in the editor and you’re actually regenerating everything affected by the exports every frame (which would capture any changes to the exports without a setter)? And that the nodes affected by the export data is constructed completely at runtime, such that you don’t even set their owner properties which would prevent them from being saved in the scene file?

I will try to be clearer. In my experience (because this feature was not well-documented when I learned it), when inheriting a scene to make a new scene, and the scene is the root of the scene:

  • You can edit @export variables in an inherited scene’s nodes without detaching from inheritance.
  • You can add new nodes to an inherited scene without detaching from inheritance.
  • You can edit any inspector properties, on the root inherited node without detaching from inheritance.
  • You can attach a new script to the root inherited node without detaching from inheritance.
  • You cannot edit any transform properties, on any of the inherited nodes. You will detach from inheritance.
  • You can make changes through code, to add nodes or alter node export values without detaching from inheritance.

Example:

This is the Enemy2D base scene for my game Eternal Echoes.

It looks like this in the 2D view:

When the Goblin scene inherits the Enemy2D scene, it looks like this:

And because I have filled out the @export variable of sprite_root_folder for the inherited ChibiAnimatedSprite2D node, even though the code creates a sprite_frames object and fills it with animation frames, I have not broken scene inheritance because I did not do it myself in the editor.

And now the 2D view looks like this:

Furthermore, the yellow background is the CollisionShape2D for the VisionArea2D, which determines the range a Goblin can throw a dagger. Both of those nodes are new to the inherited Goblin scene, but do not break inheritance of the original scene, even though they are positioned in the middles of the inherited nodes. It looks like this in the 2D view:

To make the Goblin throw daggers, I had to add an EnemyStateRangedAttack node to the EnemyStateMachine that is inherited. That does not break inheritance.

On the Goblin node itself, I have changed some @export values for the Enemy2D

I have also changed the scale of the Goblin root scene because goblins are small.

Your Code

You get what you give. You don’t want to give the full script, nothing I can do about it. I made that code suggestion based on the fact that it’s the right place for it based on the context you gave. Having an initialization function makes sense in the wider context. The style guide comments are still valid. You do you.

CurvedTerrain2D

So the base of the object is a Path2D which has an @export variable of curve, which I cannot create a setter for. There is a tool that allows you to create points in the editor’s 2D view:

image

As I add points, it is redrawn every frame. Potentially I could have found a way to tie into the signal of the curve variable’s points changing, but this code was easier at the time. I disabled it during gameplay because it’s unnecessary to run in-game.

There are a couple questions in there.

First, you are asking if I am setting the owner property of the Polygon2D, a Line2D, a StaticBody2D with a CollisionPolygon2D nodes, correct? No, I am not. They are null. They do not need to be set.

Second, you are positing they would not be saved in the scene file. That is not true, which you would seen if you’d looked at my CurvedTerrain plugin link, which was a link to the full open source project on GitHub. It contains the example test scene from my screenshot above, in which three CurvedTerrain objects are saved in the scene and rndered without issue.

Keep in mind that those nodes are being constructed when the editor loads, and then again when the game loads. They exist in the scene because they are constructed from the @export variables.

Conclusion

Scene inheritance is tricky. As @normalized pointed out, you may have to play with it to see what affects it. It is a neat idea, that in practice (in my experience) has few applications that cannot be achieved another way.

Personally, at this point I tend to go with complex objects under the hood rather than inherited scenes for almost everything. I also am a big fan of custom icons so that I can tell when a node is custom.

For your actor class it may make sense to do what you’re doing. However as I have continued to use Godot, I tend to prefer composition over inheritance. So if I were to make that goblin again, I would might make it straight up with components, and then copy it and change the components for the zombie. If I change a component (like a state), it is inherited. But I’m not sure. That’s the fun of development.