GameMaker: Lightning between points

(mouseover/click to play GIF) A 15fps preview of lightning

This is a little post about making a lightning/electric arc effect in GameMaker.

The idea

The lightning is drawn in segments from the last point to the new one.

First and last point are fixed.

Each iteration, we step toward the destination by xmin..xmax pixels and pick an ymin..ymax towards either side.

Sideways offset is also multiplied by -sin(pi*position) to make the lightning "wider" on average near the middle.

The code

For GMS2.3+:

/// @param {number} x1  source X
/// @param {number} y1  source Y
/// @param {number} x2  destination X
/// @param {number} y2  destination Y
/// @param {number} xmin  minimum distance step (>0)
/// @param {number} xmax  maximum distance step (>xmin)
/// @param {number} ymin  minimum "side" step (lightning "spread")
/// @param {number} ymax  maximum "side" step (lightning "spread")
/// @returns {int} number of segments drawn.
function draw_lightning(x1, y1, x2, y2, xmin, xmax, ymin, ymax) {
    var _dist = point_distance(x1, y1, x2, y2);
    if (_dist <= 0) return 0;
    var _fwd_x = (x2 - x1) / _dist;
    var _fwd_y = (y2 - y1) / _dist;
    var _side_x = -_fwd_y, _side_y = _fwd_x;
    var _alpha = draw_get_alpha();
    var _last_x = x1;
    var _last_y = y1;
    var _pos = 0;
    var _drawn = 0;
    repeat (5000) { // line segment limit
        _pos += random_range(xmin, xmax);
        var _next_x, _next_y;
        if (_pos < _dist) {
            var _side_offset = choose(-1, 1) * lerp(ymin, ymax,
                // this instead of just random(1) makes the lightning
                // spread out more close to the middle of it
                random(sin(-pi * _pos / _dist))
            _next_x = x1 + _fwd_x * _pos + _side_x * _side_offset;
            _next_y = y1 + _fwd_y * _pos + _side_y * _side_offset;
        } else { // (last point)
            _next_x = x2;
            _next_y = y2;
        // you can replace this with custom line drawing - these magic numbers just look alright
        draw_set_alpha(_alpha * 0.3);
        draw_line_width(_last_x, _last_y, _next_x, _next_y, 3.7);
        draw_set_alpha(_alpha * 0.7);
        draw_line_width(_last_x, _last_y, _next_x, _next_y, 1.3);
        if (_pos >= _dist) break;
        _last_x = _next_x;
        _last_y = _next_y;
        _drawn += 1;
    return _drawn;

The GIF was produced by the following:

repeat (3) draw_lightning(50, 50, mouse_x, mouse_y, 5, 15, 3, 20);

For older versions (older/less clean code):

/// draw_lightning(x1, y1, x2, y2, xmin, xmax, ymin, ymax)
//                  0   1   2   3     4     5     6     7
// Draws a lightning between points.
/// @param {number} x1  source X
/// @param {number} y1  source Y
/// @param {number} x2  destination X
/// @param {number} y2  destination Y
/// @param {number} xmin  minimum distance step (>0)
/// @param {number} xmax  maximum distance step (>xmin)
/// @param {number} ymin  minimum "side" step (lightning "width")
/// @param {number} ymax  maximum "side" step (lightning "width")
/// @returns {int} number of segments drawn.
var i, r, c, l, dx, dy, sx, sy, px, py, cx, cy, alpha;
// avoid drawing outside view:
if (max(argument0, argument2) < view_xview[view_current] - 10) return 0
if (max(argument1, argument3) < view_yview[view_current] - 10) return 0
if (min(argument0, argument2) > view_xview[view_current] + view_wview[view_current] + 10) return 0
if (min(argument1, argument3) > view_yview[view_current] + view_hview[view_current] + 10) return 0
// find distance between points (used for loop)
l = point_distance(argument0, argument1, argument2, argument3)
if (l == 0) return 0
// main direction:
i = point_direction(argument0, argument1, argument2, argument3)
dx = lengthdir_x(1, i); dy = lengthdir_y(1, i)
// side direction:
i += 90
sx = lengthdir_x(1, i); sy = lengthdir_y(1, i)
// first point coordinates:
px = argument0; py = argument1
c = 0
i = random_range(argument4, argument5)
alpha = draw_get_alpha()
repeat (5000) { // edit for line segment limit
    r = choose(-1, +1) * (argument6 + (argument7 - argument6)
        * lengthdir_y(random(1), i / l * 180)) // this part makes lightning "wider" near middle
    // current point coordinates:
    if (i < l) {
        cx = argument0 + dx * i + sx * r
        cy = argument1 + dy * i + sy * r
    } else { cx = argument2; cy = argument3 } // end point
    // line drawing code here:
    // can be just a single draw_line or something fancy like this:
    draw_set_alpha(alpha * 0.3)
    draw_line_width(px, py, cx, cy, 3.7)
    draw_set_alpha(alpha * 0.7)
    draw_line_width(px, py, cx, cy, 1.3)
    // exit condition:
    if (i >= l) break
    // update previous point coordinates:
    px = cx; py = cy
    // increments:
    c += 1
    i += random_range(argument4, argument5)
return c

Original GM8 example:

Download GMK

Related posts:

18 thoughts on “GameMaker: Lightning between points

  1. Neat script! Gotta say though, the naming conventions leave something to be desired. There’s a reason naming is one of the hardest things in programming :)

    • Admittedly, this script is pretty old – it tells through it’s variable declarations on top of the script (because GM<=8.0 did not support `var name=value` syntax) and general absence of trailing semicolons on lines.

      I’d love to say that I’ve also since moved away from giving 1-2 letter names to local variables, but, alas – auto-completion and syntax highlighting for those only made it’s entry recently, and old habits don’t go easily.

      I’ll get around to reorganizing this a bit and making an interactive demo one of these days.

      • Did you manage to rewrite it? I failed miserably at it and i would love to see how its supposed to be

        • Not yet, there’s a lot happening right now. It still works fine, it’s just that the code looks bad.

          Adding caching would require vertex buffers and a couple other things.

  2. It works so well, thank you very much !

    I have a concern however : this script seems very heavy on CPU when you multiply its use (for my game, I use it a hundred times at once… ^^). Any clue on how make it lighter?

    Thank you again for this blog!

    • If you use it a lot, you’ll need to rewrite the script to work a bit more efficiently – in a good case to use vertex buffers, or at least primitives. I don’t have time to make a new version of this script right now, but you can ask for help on Discord if you get stuck.

      • Thanks for your answer anyway. Your blog is such a valuable ressource, I hope you’ll get more time in the future.

    • Hey, your result looks so nice and some friends and I were wondering how you implemented the effect.
      I know it is an old post, but could you tell us something about that? Thanks!

  3. thanks a lot it is amazing i run it in GM studio after i change execute_string on string function

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.