Here’s how I did it. The plug-looking thingies you see are all instances of the following scene:
VoltagePoint inherits from Area2D and has the following script:
Click to show.
class_name VoltagePoint extends Area2D
signal check_power_source_request(do_not_inform : VoltagePoint, lost_power_source : VoltagePoint)
enum POINT_TYPE
{
SOURCE = 0,
TRANSFER = 1
}
enum STATE
{
DISABLED = 0,
ENABLED = 1
}
@export var point_type : POINT_TYPE = POINT_TYPE.TRANSFER
@export var state : STATE = STATE.DISABLED
var previous_state : STATE = state
@export var low_voltage_electric_lines : Array[LowVoltageElectricLine] = []
@export_category("Switch behavior")
@export var is_switch : bool = false ## Set to [code]true[/code] to make the [code]VoltagePoint[/code] fire a signal if turned on or off. Only works for a [code]VoltagePoint[/code] of type [code]TRANSFER[/code].
@export var switch_name : String = "" ## Identifier to use in signal.
@export_category("Crackle SFX")
@export var audio_stream_player_2d : AudioStreamPlayer2D = null
func set_point_type(new_value : POINT_TYPE)->void:
point_type = new_value
func set_state(new_value : STATE)->void:
state = new_value
func set_is_switch(new_value : bool)->void:
is_switch = new_value
func set_switch_name(new_value : String)->void:
switch_name = new_value
var power_sources : Array[VoltagePoint] = []
var neighbours : Array[VoltagePoint] = []
var nearest_point : VoltagePoint = null
var dont_draw_a_line : Array[VoltagePoint] = []
var tapped_out : bool = false
func _ready() -> void:
area_entered.connect(_on_area_entered.bind())
area_exited.connect(_on_area_exited.bind())
low_voltage_electric_lines.shuffle()
func _physics_process(_delta: float) -> void:
previous_state = state
if point_type == POINT_TYPE.TRANSFER:
power_sources = clean_up(power_sources)
neighbours = clean_up(neighbours)
for node : VoltagePoint in neighbours:
if not node.check_power_source_request.is_connected(_on_check_power_source_request.bind()):
node.check_power_source_request.connect(_on_check_power_source_request.bind())
if power_sources.is_empty():
state = STATE.DISABLED
elif power_sources.any(func(element : VoltagePoint): return element.state == STATE.ENABLED):
state = STATE.ENABLED
else:
state = STATE.DISABLED
if state == STATE.ENABLED:
for neighbour : VoltagePoint in neighbours:
for power_source : VoltagePoint in power_sources:
neighbour.add_power_source(power_source)
if point_type == POINT_TYPE.SOURCE:
if state == STATE.ENABLED:
var points_near_power_source : Array[Area2D] = get_overlapping_areas()
for area in points_near_power_source:
if area is VoltagePoint:
var point = (area as VoltagePoint)
if point.point_type == POINT_TYPE.TRANSFER:
point.add_power_source(self)
update_low_voltage_lines()
if point_type == POINT_TYPE.TRANSFER and is_switch == true:
if previous_state != state:
MessageBus.transfer_point_changed_state.emit(switch_name, state)
if audio_stream_player_2d != null:
if state == STATE.ENABLED:
audio_stream_player_2d.play()
else:
audio_stream_player_2d.stop()
func _on_area_entered(area : Area2D)->void:
if point_type == POINT_TYPE.SOURCE:
if state == STATE.DISABLED:
if area is VoltagePoint and area != self:
var voltage_point = (area as VoltagePoint)
if voltage_point.point_type == POINT_TYPE.TRANSFER:
voltage_point.add_power_source(self)
if point_type == POINT_TYPE.TRANSFER:
if area is VoltagePoint and area != self:
var voltage_point = (area as VoltagePoint)
if voltage_point.point_type == POINT_TYPE.TRANSFER:
voltage_point.add_neighbour(self)
for power_source : VoltagePoint in power_sources:
voltage_point.add_power_source(power_source)
func _on_area_exited(area : Area2D)->void:
if point_type == POINT_TYPE.SOURCE:
if area is VoltagePoint and area != self:
var voltage_point = (area as VoltagePoint)
voltage_point.remove_power_source(self)
if point_type == POINT_TYPE.TRANSFER:
if area is VoltagePoint and area != self:
var voltage_point = (area as VoltagePoint)
voltage_point.remove_neighbour(self)
# Quite a bit of code. Simply removing all voltage points would also suffice because
# wrongful deletions are reverted the next frame.
for power_source : VoltagePoint in power_sources:
if power_source == null: # Skip because the reference will be deleted later anyway.
break
var points_near_power_source : Array[Area2D] = power_source.get_overlapping_areas()
if power_source.state == STATE.DISABLED:
voltage_point.remove_power_source(power_source)
if power_source.state == STATE.ENABLED and points_near_power_source.find(voltage_point) == -1:
voltage_point.remove_power_source(power_source)
func update_low_voltage_lines()->void:
var areas : Array[Area2D] = get_overlapping_areas()
areas.erase(self)
areas = areas.filter(func(element : Area2D): return element is VoltagePoint)
areas.sort_custom(func(a : Area2D, b : Area2D): return global_position.distance_to(a.global_position) > global_position.distance_to(b.global_position))
var points = (areas as Array[VoltagePoint])
for dont_draw : VoltagePoint in dont_draw_a_line:
points = points.filter(func(element: VoltagePoint): return dont_draw != element)
if state == STATE.ENABLED:
for i : int in low_voltage_electric_lines.size():
if i < points.size():
if points[i] != null and not points[i].tapped_out:
low_voltage_electric_lines[i].update_line(global_position - global_position, points[i].global_position - global_position, true)
points[i].dont_draw_a_line.append(self)
else:
low_voltage_electric_lines[i].update_line(Vector2.ZERO, Vector2.ZERO, false)
else:
for i : int in low_voltage_electric_lines.size():
low_voltage_electric_lines[i].update_line(Vector2.ZERO, Vector2.ZERO, false)
if low_voltage_electric_lines.all(func(element : LowVoltageElectricLine): return element.visible == true):
tapped_out = true
else:
tapped_out = false
dont_draw_a_line.clear()
func add_neighbour(neighbour : VoltagePoint)->void:
if not neighbours.has(neighbour):
neighbours.append(neighbour)
if not neighbour.check_power_source_request.is_connected(_on_check_power_source_request.bind()):
neighbour.check_power_source_request.connect(_on_check_power_source_request.bind())
func remove_neighbour(neighbour : VoltagePoint)->void:
if neighbours.has(neighbour):
neighbours.erase(neighbour)
if neighbour.check_power_source_request.is_connected(_on_check_power_source_request.bind()):
neighbour.check_power_source_request.disconnect(_on_check_power_source_request.bind())
func add_power_source(power_source : VoltagePoint)->void:
if not power_sources.has(power_source):
power_sources.append(power_source)
func remove_power_source(power_source : VoltagePoint)->void:
if power_sources.has(power_source):
power_sources.erase(power_source)
check_power_source_request.emit(power_source)
func clean_up(source_array : Array[VoltagePoint])->Array[VoltagePoint]:
var result : Array[VoltagePoint] = []
for element in source_array:
if is_instance_valid(element):
result.append(element)
return result
func _on_check_power_source_request(lost_power_source : VoltagePoint)->void:
if lost_power_source == null: # Skip because the reference will be deleted later anyway.
return
var points_near_power_source : Array[Area2D] = lost_power_source.get_overlapping_areas()
if lost_power_source.state == STATE.DISABLED:
remove_power_source(lost_power_source)
if lost_power_source.state == STATE.ENABLED and points_near_power_source.find(self) == -1:
remove_power_source(lost_power_source)
(Please note the script needs a good refactor and there are probably a whole bunch of checks that are pointless and can be removed. The checks exist because I tried to chase down a weird bug where some electric bolts ended up getting drawn twice. This turned out to be an issue unrelated to the script-- I had accidentally manually added a point to the Line2D that I use to draw bolts.)
The three LowVoltageElectricLine nodes are all instances of a LowVoltageElectricLine scene. It’s real simple: just a scene with a Line2D as its singular node. The following script is attached to the Line2D node:
Click to show
class_name LowVoltageElectricLine extends Line2D
@export var start_point : Vector2 = Vector2.ZERO
@export var end_point : Vector2 = Vector2.ZERO
func _ready() -> void:
hide()
add_point(start_point)
add_point(end_point)
func update_line(new_start_point : Vector2, new_end_point : Vector2, line_is_visible : bool)->void:
start_point = new_start_point
end_point = new_end_point
set_point_position(0, start_point)
set_point_position(1, end_point)
if line_is_visible:
show()
else:
hide()
The line really needs just two points: a start and an end. (And you really don’t want to add more, unless you love debugging and/or have high pain tolerance.) Those are already added by the _ready function. Changing their values can be done though update_line.
The real magic in making the bolt look like electricity is its shader. In the LowVoltageElectricLine scene, open the inspector and under the fill tab, add a Texture2D and set the Texture Mode to stretch. (It really doesn’t matter what image you use. I used the default Godot logo.)
Under CanvasItem->Material, click the Material field and add a new ShaderMaterial. Then under Shader, add a new Shader script. The one you see in the video is nice, but I currently use an even nicer one that has fixed start and end positions. You can find it here.
Once you add the shader, a bunch of new fields appear. In case you don’t feel like experimenting, here’s the settings I currently use:


