Lightning bolt effect in PICO-8

There are several methods to creating a lightning bolt effect, such as using fractals or midpoint displacement. The method I’ll be using is dividing a line into segments, offseting each vertex, and drawing several of these segmented lines on top of one another.

First, since we’re using PICO-8, we’ll need some helper functions for getting the length of a line, slope of a line as a unit vector, and the unit normal of a line.

--Helper functions
function magnitude(x1,y1,x2,y2)
 return sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))
end

function slope(x1,y1,x2,y2)
 local l=magnitude(x1,y1,x2,y2)
 local dx=(x2-x1)/l
 local dy=(y2-y1)/l
 return {dx,dy}
end

function normal(x1,y1,x2,y2)
 local l=magnitude(x1,y1,x2,y2)
 local dx=(x2-x1)/l
 local dy=(y2-y1)/l
 return {-dy,dx}
end

Next, in a new tab, we’ll be making a Lightning object, which represents a line with two endpoints and evenly-spaced vertices in between. In the constructor, which I’m calling new(), we’ll add arguments for the two endpoints, the number of segments this object will have, and the color. Then we add a points array, which will hold the vertices of the line’s segments, and then generate these vertices by calculating the slope of the line, then breaking up the line into equal-length segments, and adding each vertex to the points array.

lightning={}
function lightning:new(x1,y1,x2,y2,n,c)
 local o={}
 setmetatable(o, self)
 self.__index=self
 
 o.x1=x1
 o.x2=x2
 o.y1=y1
 o.y2=y2
 o.color=c
 o.points={}
 
 --create the line segments
 --make sure the minimum is 1
 o.num_segments=flr(max(1,n))
 local len=magnitude(x1,y1,x2,y2)/o.num_segments
 local p1={x1,y1}
 local p2={x2,y2}
 add(o.points,p1)
 local vec=slope(x1,y1,x2,y2)
 for i=1,o.num_segments do
  local p={p1[1],p1[2]}
  p[1]+=i*len*vec[1]
  p[2]+=i*len*vec[2]
  add(o.points,p)
 end
 
 return o
end

Now we’ll add a function to “generate” the lightning bolt, which really just means offsetting each vertex by adding the unit normal of the line to each one, scaled to a random amount. Note that each time this method is called, we also need to reset the positions of each vertex because otherwise, they’ll keep moving farther and farther away.

function lightning:generate()
 local x1=self.x1
 local y1=self.y1
 local x2=self.x2
 local y2=self.y2
 local nor=normal(x1,y1,x2,y2)
 local vec=slope(x1,y1,x2,y2)
 local len=magnitude(x1,y1,x2,y2)/self.num_segments
 for i=1,#self.points do
  local p=self.points[i]
  --First reset back to normal
  p[1]=x1+(i-1)*len*vec[1]
  p[2]=y1+(i-1)*len*vec[2]
  --Then offset the point by adding the normal vector
  local off=flr(rnd(12))-6
  p[1]+=off*nor[1]
  p[2]+=off*nor[2]
 end
end

The offset applied to each vertex is hard-coded to be between -6 and 5, but you can change this to whatever you want, or even make it dynamic. Also note that each point, including the two endpoints, are offset. If you want an effect where the endpoints stay in place, you can change for i=1,#self.points do to for i=2,#self.points-1 do instead, which will ignore the first and last points.

Finally, we’ll add a draw() function to the Lightning object that will be called in the main _draw() function. In the draw function, we call PICO-8’s line() function by passing each pair of vertices in order.

function lightning:draw()
 for i=1,#self.points-1 do
  local p1=self.points[i]
  local p2=self.points[i+1]
  line(p1[1],p1[2],p2[1],p2[2],self.color)
 end
end

Now we can move on to the main loop. First we’ll create an array to hold Lightning objects, and in the _init() function we’ll create three Lightning objects and add them to this array.

lightning_bolt={}
function _init()
 bolt=lightning:new(16,16,112,112,7,10)
 bolt2=lightning:new(16,16,112,112,7,10)
 bolt3=lightning:new(16,16,112,112,7,10)
 add(lightning_bolt,bolt)
 add(lightning_bolt,bolt2)
 add(lightning_bolt,bolt3)
end

Next, in the _draw() function, we loop through each Lightning object in the array and call their draw() function.

function _draw()
 cls()
 for v in all(lightning_bolt) do
  v:draw()
 end
end

Finally, in the _update() function, for demonstration purposes, we’ll check if button 4 (Z/C/N) is being pressed, and if so, call generate() on all the Lightning objects in the array. That way, if you hold down the button, each lightning bolt will regenerate its vertices’ offsets, creating a lightning bolt effect.

function _update()
 if btn(4) then
  for v in all(lightning_bolt) do
   v:generate()
  end
 end
end

After implementing the above Lightning object, you can freely reuse it in any other projects too. You can add an update() function to it with custom behavior, or make generate() run constantly every set interval, or even manipulate the endpoints to make the lightning move around.

Leave a Reply

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