Editor-Visible Wire Links (Godot 4)

When you need to show a relationship between two 3D nodes (crate -> drop-off, switch -> door), a live wire in the editor is a fast way to debug layout and gameplay flow. This post covers a simple, reusable line-link scene that lets you pick a start and end node in the inspector and see the wire immediately.

Strategy

  • Keep it scene-based so it can be reused across levels.
  • Use @tool so it runs in the editor and updates live.
  • Use NodePath exports so designers can pick start/end nodes directly.
  • Draw with a cheap mesh (cylinder) and align it to the direction vector.
  • Avoid reliance on non-existent nodes (e.g., Line3D is not a Godot 4 node).

The Scene

res://scenes/line_link.tscn contains a single MeshInstance3D with the script below. It is instanced wherever you need a link.

The Script

@tool
extends MeshInstance3D
class_name LineLink

@export var start_path: NodePath = NodePath()
@export var end_path: NodePath = NodePath()
@export var line_color: Color = Color(1.0, 0.6, 0.2, 1.0)
@export var line_width: float = 0.05
@export var emission_strength: float = 1.0

var _line_mesh: CylinderMesh = null
var _line_material: StandardMaterial3D = null

func _ready() -> void:
	set_process(true)
	_ensure_resources()
	_update_line()

func _process(_delta: float) -> void:
	_update_line()

func _ensure_resources() -> void:
	if not _line_mesh:
		_line_mesh = CylinderMesh.new()
		_line_mesh.height = 1.0
		mesh = _line_mesh
	if not _line_material:
		_line_material = StandardMaterial3D.new()
		_line_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
		_line_material.emission_enabled = true
		material_override = _line_material

func _update_line() -> void:
	_ensure_resources()
	_line_material.albedo_color = line_color
	_line_material.emission = line_color
	_line_material.emission_energy_multiplier = emission_strength
	_line_mesh.top_radius = max(0.001, line_width * 0.5)
	_line_mesh.bottom_radius = _line_mesh.top_radius

	if start_path == NodePath() or end_path == NodePath():
		visible = false
		return

	var start_node = get_node_or_null(start_path)
	var end_node = get_node_or_null(end_path)
	if not (start_node is Node3D) or not (end_node is Node3D):
		visible = false
		return

	var start_pos = (start_node as Node3D).global_position
	var end_pos = (end_node as Node3D).global_position
	var to_end = end_pos - start_pos
	var distance = to_end.length()
	if distance <= 0.01:
		visible = false
		return

	visible = true
	var midpoint = start_pos + to_end * 0.5
	var direction = to_end.normalized()
	var up = Vector3(0, 1, 0)
	if abs(direction.dot(up)) > 0.99:
		up = Vector3(0, 0, 1)
	var x_axis = up.cross(direction).normalized()
	if x_axis.length() <= 0.001:
		up = Vector3(1, 0, 0)
		x_axis = up.cross(direction).normalized()
	var z_axis = x_axis.cross(direction).normalized()
	global_transform = Transform3D(Basis(x_axis, direction, z_axis), midpoint)
	scale = Vector3(1.0, distance, 1.0)

Usage

  1. Instance LineLink into your level.
  2. In the inspector, set start_path and end_path by picking nodes in the scene tree.
  3. Adjust line_widthline_color, and emission_strength for visibility.

Now the line updates in the editor and runtime, so you can position gameplay objects with immediate visual feedback.

Notes

  • The line uses a CylinderMesh so it reads as a wire rather than a single-pixel debug line.
  • The line is fully editor-driven; no runtime scripting or extra setup required.
  • This pattern is easy to reuse for puzzles, objectives, and navigation cues.

Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *