Teleporting between scenes simplified by a @tool script

Godot Version

v4.3.stable.official [77dcf97d8]

Question

I’m using coordinates and a scene path to load a new location with the following Teleporter class. I’m using Marker2D to manually copy and paste the coordinates into the teleporter.

I want to make this a better experience by having the editor check the destination scene and offer a dropdown of available Marker2Ds to teleport to.

It’s kind of working, but it’s breaking when I connect the other side of the teleporter. I feel there’s a kind of loop/recursion going on there, when the scenes contain each other:

@tool
class_name Teleporter
extends Area2D
## Needs a shape to choose where the teleportation should be triggered.

@export var door_width: int = 128:
	set(new_width):
		door_width = new_width
		# As the shape is made local to the scene it only changes for the instance
		($CollisionShape2D.shape as SegmentShape2D).b.x = new_width

## The scene which contains markers to which the player should be teleported.
## Drag and drop it from the FileSystem dock in the editor.
@export var target_scene: PackedScene:
	set(new_target_scene):
		target_scene = new_target_scene
		notify_property_list_changed()
		update_configuration_warnings()

signal teleported(target_room_path: String, destination_coordinates: Vector2)

## This is needed for the enum `destination_marker` to properly show the selected item.
var selected_destination_marker_name: String

var selected_destination: Vector2

func _get_property_list():
	
	var position_options = get_markers_from_scene().map(func(marker: Marker2D): return marker.name).filter(func(marker_name: String): return marker_name != "PlayerRespawn")

	var properties = []
	properties.append({
		"name": "destination_marker",
		"type": TYPE_STRING,
		"hint": PROPERTY_HINT_ENUM,
		"hint_string": ",".join(position_options),
	})

	return properties


func _set(property: StringName, value: Variant) -> bool:
	if (property == "destination_marker"):
		selected_destination_marker_name = value
		
		var marker: Marker2D = get_markers_from_scene().filter(func(m): return m.name == value).front()
		if marker:
			selected_destination = marker.position
		
		return true
	return false


## This needs to return the enum string so it knows what to show in the editor (it's called constantly).
func _get(property: StringName):
	if (property == "destination_marker"):
		return selected_destination_marker_name


func get_markers_from_scene() -> Array:
	var markers = []
	if target_scene and target_scene is PackedScene:
		var target_instance = target_scene.instantiate()
		if target_instance:
			for child in target_instance.get_children():
				if child is Marker2D:
					markers.append(child)
	return markers


func _get_configuration_warnings() -> PackedStringArray:
	var notifications: PackedStringArray = []

	if not (target_scene):
		notifications.append("Needs a target room to teleport to!")

	return notifications


func _on_body_entered(body: Node2D) -> void:
	if body is Player:
		PlayerGlobals.facing_direction_before_teleport = body.facing_direction
		PlayerGlobals.remaining_time_to_burn_before_teleport = body.burn_timer.time_left
		PlayerGlobals.has_teleported_while_burning = body.is_burning

		teleported.emit(target_scene.resource_path, selected_destination)

Together with some help from Claude AI I could prevent instantiating the scene. Still, using the PackedScene doesn’t seem to work properly as I’m getting these errors

E 0:00:02:0916   game_manager.gd:102 @ load_new_room(): res://environment/the_eye_level.tscn:39 - Parse Error: [ext_resource] referenced non-existent resource at: res://environment/hub_level.tscn
  <C++ Source>   scene/resources/resource_format_text.cpp:159 @ _parse_ext_resource()
  <Stack Trace>  game_manager.gd:102 @ load_new_room()
                 game_manager.gd:91 @ start_game()
                 main_menu.gd:29 @ _on_start_pressed()

and teleporting back doesn’t seem to work either.

Current script:

@tool
class_name Teleporter
extends Area2D
## Needs a shape to choose where the teleportation should be triggered.

@export var door_width: int = 128:
	set(new_width):
		door_width = new_width
		# As the shape is made local to the scene it only changes for the instance
		if has_node("CollisionShape2D"):
			($CollisionShape2D.shape as SegmentShape2D).b.x = new_width

## The scene which contains markers to which the player should be teleported.
## Drag and drop it from the FileSystem dock in the editor.
@export var target_scene: PackedScene:
	set(new_target_scene):
		target_scene = new_target_scene
		# Clear the selected marker when the scene changes
		destination_marker = ""
		# Update the property list to refresh the marker options
		notify_property_list_changed()
		update_configuration_warnings()

var destination_marker: String:
	set(new_marker):
		destination_marker = new_marker
		_update_destination_marker()
		update_configuration_warnings()
	get:
		return destination_marker

signal teleported(target_room_path: String, destination_coordinates: Vector2)

var selected_destination: Vector2

func _update_destination_marker():
	# Safely find the marker without instantiating the entire scene
	if target_scene:
		var scene_state = target_scene.get_state()
		for i in scene_state.get_node_count():
			var node_name = scene_state.get_node_name(i)
			var node_class = scene_state.get_node_type(i)
			
			if node_class == "Marker2D" and node_name == destination_marker:
				# Use the scene's state to get the marker's position
				var marker_position: Vector2
				for idx in range(0, scene_state.get_node_property_count(i)):
					if "position" == scene_state.get_node_property_name(i, idx):
						marker_position = scene_state.get_node_property_value(i, idx)
					break

				if marker_position:
					selected_destination = marker_position
				break

func _get_property_list() -> Array:
	var properties = []
	
	# Only add marker selection if a target scene is selected
	if target_scene:
		var marker_names = _get_marker_names()
		
		if not marker_names.is_empty():
			properties.append({
				"name": "destination_marker",
				"type": TYPE_STRING,
				"hint": PROPERTY_HINT_ENUM,
				"hint_string": ",".join(marker_names),
			})
	
	return properties

func _get_marker_names() -> PackedStringArray:
	var marker_names = PackedStringArray()
	
	if target_scene:
		var scene_state = target_scene.get_state()
		for i in scene_state.get_node_count():
			var node_name = scene_state.get_node_name(i)
			var node_class = scene_state.get_node_type(i)
			
			if node_class == "Marker2D" and node_name != "PlayerRespawn":
				marker_names.append(node_name)
	
	return marker_names

func _get_configuration_warnings() -> PackedStringArray:
	var notifications: PackedStringArray = []

	if not target_scene:
		notifications.append("Needs a target room to teleport to!")
	elif destination_marker.is_empty():
		notifications.append("Select a destination marker!")

	return notifications

func _on_body_entered(body: Node2D) -> void:
	if body is Player:
		PlayerGlobals.facing_direction_before_teleport = body.facing_direction
		PlayerGlobals.remaining_time_to_burn_before_teleport = body.burn_timer.time_left
		PlayerGlobals.has_teleported_while_burning = body.is_burning

		teleported.emit(target_scene.resource_path, selected_destination)

EDIT: Using this together with

@export_file("*level*.tscn") var target_scene: String:

(and adding load() where needed) etc.) it is now working. I still value any thoughts on this.