Adding chain lightning

A fun addition to our lightning effect is having it bounce between objects.

Updating the Lighting Beam scene

The first thing we need to do is define a jump range. Open the LightingBeam scene and add an Area2D as a child. As a child of that, add a CollisionShape2D.

Over in the inspector, we add a new CircleShape2D and set the radius. I used 128.

This means if there’s a body within 128 pixels of the end point, we can jump to it.

Updating the Lightning Beam script

In LightningBeam.gd, we define the maximum times the lightning can bounce and cache JumpArea.

#...
export (int, 0, 10) var bounces_max := 3

#...
onready var jump_area := $JumpArea

We make sure the jump area’s position is always the same as the target point, as that’s where we’re jumping from.

func _physics_process(delta):
    #...

    jump_area.global_position = target_point

The shoot() function receives the most changes. We store all static bodies from jump_area in _secondary_bodies, but remove _primary_body because we don’t want the lightning to jump to the first target. In this case, it wouldn’t jump at all!

If the ray cast has a collision, we update the target point to its position.

func shoot() -> void:
    var _target_point = target_point

    var _primary_body = get_collider()
    var _secondary_bodies = jump_area.get_overlapping_bodies()

    if _primary_body:
        _secondary_bodies.erase(_primary_body)
        _target_point = _primary_body.global_position
    #...

We’re going to handle jumping jolts a little differently. Rather than having them start at the end of the last jolt, we’ll have them originate from the previous body’s position. I chose to do it this way as it looks like the body is taking in the lightning before expelling it to the next target.

First, we introduce the _start variable, which will change as we instance jolts.

func shoot() -> void:
    #...

    for flash in range(flashes):
        var _start = global_position
        
        var jolt = lightning_jolt.instance()
        add_child(jolt)
        jolt.create(_start, target_point)
        

Under this code, we have the following loop.

This loop works much like how our lighting jolt is shaped: we grab a body from secondary bodies and instance a jolt to its position before updating the start point again and moving on.

func shoot() -> void:
    #...
    for flash in range(flashes):
        #...
        
        # reset the starting point
        _start = _target_point
        # loops for bounces_max, or number of bodies; whichever is smaller
        for _i in range(min(bounces_max, _secondary_bodies.size())):
            var _body = _secondary_bodies[_i]
    
            jolt = lightning_jolt.instance()
            add_child(jolt)
            jolt.create(_start, _body.global_position)
    
            _start = _body.global_position
    
        yield(get_tree().create_timer(flash_time), "timeout")

We have a slight issue, however. If you run a scene with multiple static bodies, you may notice the sparks are sent to the center of the secondary bodies. We want our sparks to appear at the point of collision. To solve this, we’ll add a RayCast2D to our LightningJolt scene so it can work out the collision point itself.

Updating the Lighting Jolt scene

Add a RayCast2D to the LightningJolt scene.

Updating Lightning Jolt script

We cache our RayCast2D.

extends Line2D
  
#...
onready var ray_cast := $RayCast2D

Then, we update the ray cast in the create() function.

func create(start: Vector2, end: Vector2) -> void:
    
    ray_cast.global_position = start
    
    ray_cast.cast_to = end - start
    ray_cast.force_raycast_update()
    
    if ray_cast.is_colliding():
        end = ray_cast.get_collision_point()
    #...

And there we have it: lightning that bounces between static bodies!

Final updated LightningBeam.gd script

extends RayCast2D

export (int, 1, 10) var flashes := 3
export (float, 0.0, 3.0) var flash_time := 0.1
export (int, 0, 10) var bounces_max := 3
export var lightning_jolt: PackedScene = preload("res://LightningBeam/LightningJolt.tscn")

var target_point := Vector2.ZERO

onready var jump_area := $JumpArea


func _physics_process(delta) -> void:
    target_point = to_global(cast_to)

    if is_colliding():
        target_point = get_collision_point()

    jump_area.global_position = target_point


func shoot() -> void:
    var _target_point = target_point
    
    var _primary_body = get_collider()
    var _secondary_bodies = jump_area.get_overlapping_bodies()

    if _primary_body:
        _secondary_bodies.erase(_primary_body)
        _target_point = _primary_body.global_position

    for flash in range(flashes):
        var _start = global_position

        var jolt = lightning_jolt.instance()
        add_child(jolt)
        jolt.create(_start, target_point)

        _start = _target_point
        for _i in range(min(bounces_max, _secondary_bodies.size())):
            var _body = _secondary_bodies[_i]

            jolt = lightning_jolt.instance()
            add_child(jolt)
            jolt.create(_start, _body.global_position)

            _start = _body.global_position

        yield(get_tree().create_timer(flash_time), "timeout")

Final updated LightningJolt.gd script

extends Line2D

export (float, 0.5, 3.0) var spread_angle := PI/4.0
export (int, 1, 36) var segments := 12

onready var sparks := $Sparks
onready var ray_cast := $RayCast2D


func _ready() -> void:
    set_as_toplevel(true)


func create(start: Vector2, end: Vector2) -> void:
    
    ray_cast.global_position = start

    ray_cast.cast_to = end - start
    ray_cast.force_raycast_update()

    if ray_cast.is_colliding():
        end = ray_cast.get_collision_point()
    
    var _points := []
    var _current := start
    var _segment_length := start.distance_to(end) / segments

    _points.append(start)
    
    for segment in range(segments):
        var _rotation := rand_range(-spread_angle / 2, spread_angle / 2)
        var _new := _current + (_current.direction_to(end) * _segment_length).rotated(_rotation)
        _points.append(_new)
        _current = _new

    _points.append(end)
    points = _points

    sparks.global_position = end