How to leverage the scalability of SVG in Godot

I tried to run with the above idea, and this is my result (for some reason I couldn’t upload a video, so it’s a gif):
170924-182523

I tried to create a setup utilizing the above philosophy. Caching the scaled images to files failed because it seemed that no matter how I tried to re-load the files (besides loading them as bytes, which defeats the point) they would always come out as they had been originally imported in the editor. Godot apparently completely lacks logic for re-loading modified files (maybe if I used the importer it might work, but by that point it is not worth the hassle anymore).

What I did instead was: I have a master scaling script, and some ‘generic’ svg sprite script code that would need to be contained within all sprites that are going to use SVGs (nothing is hardcoded so it can just be copy-pasted to all such sprites, but if a sprite does not use an SVG as it’s default texture your game will crash so careful with that).

In the master script I have a dictionary that’s set up so that the Key is always the path to the SVG, and it’s value is an array that at index 0 contains the SVG as bytes, at index 1 it contains the scale at which the SVG was imported by the editor (so it respects that), and the remaining array items are the nodes that are using the SVG that the Key refers to.

Upon the sprite’s creation, it creates a key for it’s svg in the svg tracker dictionary of the master scaler if it doesn’t already exist and then it adds itself to the tracking list. (So the creation of dictionary keys and values, and the removal of values as well is generally called by the sprite, not the master script, even if the code for it is mostly in the scaler script.)

The master script then keeps track of whether or not the window size has changed, if it has then it triggers the rescaling logic which is more or less the same as in the original post (biggest exception is that it’s positional adjustment is keeping it’s position relative to the window’s edges rather than just relative to the bottom edge) and it sends a signal to all the sprites to inform them that they need to update their position and texture.

Master SVG Scaler script (it’s my root node2d running it)

extends Node2D

signal rescale(File:String,OutTexture:ImageTexture,LastRes:Vector2, LastScale:Vector2) # Create a signal to be used to tell sprites when they need to update their texture and position, and send them the data to do so.

var SVGTracker: Dictionary = {} # A dictionary to keep track of which SVGs are in use, at what scale, and what nodes are using them.

@onready var MainWindow: Window = get_window() # Store window reference in variable because we will use it a lot.
@onready var DevelopmentResolution:Vector2 = Vector2(MainWindow.content_scale_size) # Original resolution of the project
@onready var ActiveResolution:Vector2 =  Vector2(MainWindow.size) # Buffer to store the last resolution used for scaling
var LastScale:Vector2 # Buffer to store last used scale value for both axis
var Bitmap: Image = Image.new() # Bitmap for SVG->Bitmap conversion

func _ready() -> void:
	get_tree().get_root().connect("size_changed",window_size_changed) # Hook up the size_changed event for the root to the window_size_changed function

func window_size_changed():
	LastScale = ActiveResolution / DevelopmentResolution # Store the last used scale for both axis
	ActiveResolution = MainWindow.size # Update Active Resolution
	for key in SVGTracker: # For every SVG being tracked by this script
		Bitmap.load_svg_from_buffer(SVGTracker[key][0],(ActiveResolution.y / DevelopmentResolution.y)*SVGTracker[key][1]) # Convert SVG to Bitmap with updated scaling settings
		rescale.emit(key,ImageTexture.create_from_image(Bitmap),ActiveResolution,LastScale) # Forward the SVG File Path, Bitmap(as texture), Window Resolution and Last Scale values to all nodes connected to the rescale signal.
		'''
		# Alternate way which does not require a signal connection, i'm not sure which is more efficient.
		for node in SVGTracker[key]: # For each node using SVG
			if node is Object: # Make sure it's actually a node (first 2 items in the array are never nodes, which is why this is needed)
				node._on_rescale(key,ImageTexture.create_from_image(Bitmap),ActiveResolution,LastScale) # Activate rescale function with all the relevant data (doing this actually does not require the signal to be connected)
		'''

func add_svg(file: String): # Adds a new SVG file to the tracking dictionary (Generally needs to be called from the child)
	if !SVGTracker.has(file): # Only run if the key does not already exist (svg not already being tracked)
		SVGTracker[file] = [FileAccess.get_file_as_bytes(file), get_import_scale(file)] # create dictionary Key:SVG File Path, Value: [SVG as bytes, Importer Scale Setting]


func remove_svg(file: String, node: Object): # Removes node from tracking dictionary, and deletes the svg from tracking if no nodes are using it.  (Generally needs to be called from the child)
	SVGTracker[file].remove_at(SVGTracker[file].find(node,2)) # Remove the target node from the list of nodes using the target SVG 
	if SVGTracker[file].size() < 3: # If no nodes are using this SVG anymore
		SVGTracker.erase(file) # Delete the SVG file from the tracking dictionary


func get_import_scale(file: String): # Gets the scale at which the SVG was imported into the editor.
	var ImportSettings: ConfigFile = ConfigFile.new() # Blank config file buffer
	ImportSettings.load(file+".import")	# Load import settings to config file buffer
	return ImportSettings.get_value("params","svg/scale") # Read import scale value

And a sprite that uses the master scaler:

extends Sprite2D

var SourcePath: String = texture.resource_path # Store the path of the SVG this sprite is using.
@onready var SVGScaleMaster: Node2D = $".."  # A reference to the node containing the SVG Scaling script, in this case it is the root node

func _ready() -> void:
	SVGScaleMaster.connect("rescale",_on_rescale) # Connect to the root node's rescale signal
	SVGScaleMaster.add_svg(SourcePath) # Add a new entry to the SVG Tracker in the SVG Scale Master script if one does not already exist for this SVG.
	SVGScaleMaster.SVGTracker[SourcePath] += [self] # Add self to list in the root node that keeps track of which nodes are using this SVG

func _on_rescale(SVG:String,TEX:ImageTexture,AR:Vector2,LS:Vector2) -> void:
	if SourcePath == SVG: # If the modified SVG is the same as the one this sprite is using
		texture = TEX # Update the displayed texture with the re-scaled one
		#position.y = position.y/ LS.y * AR.y / get_window().content_scale_size.y # Keep relative sprite positioning on Y axis only
		position = position / LS * AR / Vector2(get_window().content_scale_size) # Keep relative sprite positioning (works best if aspect ratio is locked)

func _on_death() -> void:
	SVGScaleMaster.remove_svg(SourcePath,self) # Remove self from tracking for the SVG Scaling script
	call_deferred("free") # Mark for deletion at the next opportunity (safer than queue_free())

A big benefit imo of this approach is that once you’ve created and applied this code, using SVGs with sprites becomes fully intuitive, for instance let’s say you want to make a new sprite that uses an SVG.

The process is to add the sprite code above as it’s script (this can actually in many cases probably just be one shared script if they don’t need any logic other than the scaling logic).

Then just set the SVG you want as this sprite’s texture in the editor.

That’s it. That’s the whole process, you now have a sprite using an SVG that’ll scale like in my OP and the gif at the top of this post.

The biggest downside is that scaling is shared for each sprite using an identical SVG, there’s no easy way around this so the simplest solution to make a sprite using the same SVG operate at a different scale is to duplicate the SVG and reimport the duplicate at the scale you want for that particular sprite, then use that duplicate as that sprite’s texture.

Edit: Cleaned up the scripts a bit by moving more code to the master scaler script