Teleporter Tooling
This is the fifteenth tutorial in a series that covers the creation of a simple true top-down 2D game with pixel graphics, using the Godot Engine, version 4. It follows Pausing and enhances teleporter editing.
This tutorial uses Godot 4.6, the regular version, but you could also use the .NET version.
Setting Up Visuals
Currently editing teleporters is inconvenient because their colors and connection lines only show up when running the game. We use editor tooling to make editing conveyors easy, so let's use the same approach to also make editing teleporters easy. Teleporters are a bit more complex though, because we have to link sources and destinations together.
To making the visuals immediately respond to changes we're going to edit teleporter_source.gd
. The simplest thing to support is the invalid color, by adding a setter function to its variable. We check if the new color is different and if so update the invalid color. Then if there is no destination we immediately set the sprite's modulation color to show its invalid state.
@export var invalid_color := Color(Color.MAGENTA, 0.5) :
set(new_color):
if new_color != invalid_color:
invalid_color = new_color
if not destination:
sprite.modulate = invalid_color
Also give a setter function to the valid color variable. It works the same as the invalid color, except that it also has to set the color of the particles. And if there is a destination then its colors have to be set as well.
@export var valid_color := Color(Color.CYAN, 0.5) :
set(new_color):
if new_color != valid_color:
valid_color = new_color
particles.modulate = valid_color
if destination:
sprite.modulate = valid_color
destination.sprite.modulate = valid_color
destination.particles.modulate = valid_color
line.modulate = valid_color
If the destination changes we also have to update all the colors accordingly, so create a setter function for its variable as well.
@export var destination: TeleporterDestination :
set(new_destination):
if new_destination != destination:
destination = new_destination
if not destination:
sprite.modulate = invalid_color
else:
sprite.modulate = valid_color
destination.sprite.modulate = valid_color
destination.particles.modulate = valid_color
line.modulate = valid_color
We have to make sure the teleporter line also connects properly. We assume that the two points of the line already exist and update the last point. If there is no destination we collapse the line by making the last points equal to the first point, by setting it to zero.
@export var destination: TeleporterDestination :
set(new_destination):
if new_destination != destination:
destination = new_destination
if not destination:
sprite.modulate = invalid_color
line.points[1] = Vector2.ZERO
else:
sprite.modulate = valid_color
destination.sprite.modulate = valid_color
destination.particles.modulate = valid_color
line.modulate = valid_color
line.points[1] = destination.position - position
To make this work configure the line to have two points set to zero in teleporter.tscn
.
Now we can remove all code related to visualization from _ready().
func _ready() -> void:
if not destination:
is_active = false
#sprite.modulate = invalid_color
set_physics_process(false)
else:
destination.validity_changed.connect(_on_destination_validity_changed)
#…
To activate the visualization in the editor turn teleporter_source.gd
into a tool script.
@tool
class_name TeleporterSource
This will start generating errors that complain about invalid access to the position_changed signal of teleporter_destination.gd
. This happens because our tool script tries to connect to a signal from a non-tool source. The easy way to solve this is by making the teleporter destination a tool as well. This is fine because the destination will have to become a tool anyway.
@tool
class_name TeleporterDestination
Moving the Source
The teleporter sources and destinations now get colored correctly in the editor. However, when moving the teleporter source its line doesn't get updated, which disconnects it from the destination.
To fix the line we have to respond to position changes of the teleporter source. There is no dedicated function for this, but there is a catch-all function that we can define to listen for all notifications that Godot sends to the node. This function is _notification() and it has an int parameter for a notification code, called what by convention. We're interested in the NOTIFICATION_TRANSFORM_CHANGED notification. This could also represent other changes to the node's transformation, but we assume that teleporters are only moved.
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED:
pass
Does all movement send this notification?
Yes, but the notification has to be enabled per node via set_notify_transform(). We don't have to do it ourselves because Area2D nodes already do this.
If we detect this notification and there is a destination then we update the line.
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED and destination:
line.points[1] = destination.position - position
Moving the Destination
We have solved moving teleporter sources, but teleporter destinations can also move and this still causes the lines to become invalid.
We have to notify the teleportation sources that their destination has moved. It isn't possible to directly get notified when a different node moves, so we'll add a position_changed signal for this to teleporter_destination.gd
. As it is a tool script it can emit signals in the editor.
signal position_changed
signal validity_changed(valid: bool)
Now we can listen for transform changes and emit the signal when appropriate.
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSFORM_CHANGED:
position_changed.emit()
Add a function to teleporter_source.gd
that responds to the signal by updating the line.
func _on_destination_position_changed() -> void:
line.points[1] = destination.position - position
And connect it to the signal in the destination setter.
@export var destination: TeleporterDestination :
set(new_destination):
if new_destination != destination:
if not destination:
sprite.modulate = invalid_color
line.points[1] = Vector2.ZERO
else:
…
destination.position_changed.connect(
_on_destination_position_changed
)
Although not necessary, let's also connect the validity-changed signal here for consistency.
destination.position_changed.connect(
_on_destination_position_changed
)
destination.validity_changed.connect(
_on_destination_validity_changed
)
And no longer do this in _ready().
func _ready() -> void:
if not destination:
is_active = false
set_physics_process(false)
#else:
#destination.validity_changed.connect(_on_destination_validity_changed)
However, if we're already connected to the signals of a destination we should disconnect from the old signals before replacing the destination, by calling disconnect() on its signals, passing along the functions to disconnect.
@export var destination: TeleporterDestination :
set(new_destination):
if new_destination != destination:
if destination:
destination.position_changed.disconnect(
_on_destination_position_changed
)
destination.validity_changed.disconnect(
_on_destination_validity_changed
)
destination = new_destination
…
Why are the labels for the sources also shown?
Because the cursor hovers over their lines while moving the destination.
Editing teleporters has now become a lot easier. This also made it possible to move and reconnect teleporters as a gameplay feature, but we won't take advantage of that. We'll add a different and final gameplay element in the future.