Tags: lisp programming artsy
<< previousSome Lisps, like Common Lisp, have a powerful feature that tends to go underappreciated amidst all the talk about macros: the ability to recompile your program while it's running, without restarting it. For the purposes of this post, and because it sounds cool, let's call this ability livecoding1.
Entering this strange land where the programs never stop, we'll first take a brief tour of Common Lisp and one of its graphics frameworks, Sketch, before walking through a livecoded implementation of the Boids algorithm.
Consider the typical workflow needed to modify a running application, like a videogame.
In a livecoding environment, the application is never stopped, which eliminates steps 1, 4 and 5. Instead, small code changes (which can be as granular as recompiling a single function) are immediately reflected in the running program. Step 3 is often instantaneous because only the changed parts of the program must be recompiled. In theory, then, you can develop an entire application while it continuously runs in the background, without ever waiting for code to recompile. This makes the development process more fluid and interactive, with minimal downtime.
In Common Lisp, the workflow might look something like this:
For an example of this workflow in action, check out Common Lisp and Emacs being used as an environment for live musical performance. You can hear about a Lisp program being debugged remotely while running in deep space. Livecoding (or hot reloading, or whatever you like to call it) is also available in other languages, like Smalltalk and Erlang.
Before jumping into Boids, let's take a brief look at Sketch, our Common Lisp graphics framework of choice. We'll be more concerned with big ideas than with code minutiae, but if you're unfamiliar with Common Lisp and want to understand the code samples, then please take a detour through Learn Common Lisp in Y Minutes.
So, Sketch. The Sketch API is heavily based on that of Processing. Its primary entry point is the defsketch
macro. The code below defines a "sketch" called my-sketch
.
(defsketch my-sketch
((width 200)
(height 200)
(n 5))
;; ...drawing code here...
)
After the name of the sketch comes a list of bindings that define its state and configuration. Here, the window properties width
and height
are set to 200
, while n
is an attribute we've added for our own use.
Then comes the drawing code. This gets run in a loop while the sketch is running, once per frame. The following snippet draws 5 red circles on a black background, each of radius 10 and in random positions.
(background +black+)
(loop repeat n
do (with-pen (make-pen :fill +red+)
(circle (random width) (random height) 10)))
After painting the background black, the all-powerful loop
macro is used to draw n
circles. The with-pen
macro (defined by Sketch) configures drawing properties like fill colour, stroke width and stroke colour. It takes a "pen" object as an argument.
Here's all the code together:
(defsketch my-sketch
((width 200)
(height 200)
(n 5))
(background +black+)
(loop repeat n
do (with-pen (make-pen :fill +red+)
(circle (random width) (random height) 10))))
Finally, to run the sketch, we compile our code and execute (run-sketch 'my-sketch)
from the REPL, resulting in...
That's all we need to know about Sketch for now!
Boids is an algorithm from 1986 for simulating flocks of birds. In its essence, it consists of applying 3 forces to the simulated birds. Quoting Wikipedia2, these forces are:
- separation: steer to avoid crowding local flockmates
- cohesion: steer to move towards the average position (center of mass) of local flockmates
- alignment: steer towards the average heading of local flockmates
How can we implement this ourselves? First, we need a canvas to draw on!
(defsketch boids
((width 400)
(height 400)
(restart-on-change nil))
(background (gray-255 230))
The only mysterious thing in this code is the restart-on-change
parameter, which is available in my fork of Sketch. When its value is nil
(false), the sketch's state - like the boid positions - won't be reset when we recompile our code.
Compiling the defsketch form in Emacs (with the Ctrl-C Ctrl-C shortcut) and executing (run-sketch 'boids)
at the REPL gives us... 🥁... a gray background. Wonderful.
(Note: all going well, this modest window will run continuously throughout the entire development lifecycle).
Now let's create some boids to populate our world. We add a boid
class to store their position and velocity, as well as a convenience function make-boid
to create a boid from x & y co-ordinates. These rely on a hopefully self-explanatory implementation of 2d vectors, which are created using the vec2
function.
(defclass boid ()
((pos :initarg :pos :accessor pos)
(velocity :initarg :velocity
:initform (vec2 0 0)
:accessor velocity)))
(defun make-boid (x y)
(make-instance 'boid :pos (vec2 x y)))
To the sketch itself, we add 20 boids in random positions, and pass them to the draw-boids
function in the drawing loop.
(defsketch boids
((width 400)
(height 400)
(restart-on-change nil)
(boids (loop repeat 20
collect (make-boid (random width) (random height)))))
(background (gray-255 230))
(draw-boids boids))
If we then recompile defsketch (with Ctrl-C Ctrl-C)...
...we get an error! Woops.
But of course! We forgot to define draw-boids
. The program doesn't crash, however, and we'll soon be able to recover from this setback.
Here's an implementation of draw-boids
. We don't need to get into the weeds of how it works. For each boid, it does some unwieldy vector math to figure out which direction the boid is facing and draws a triangle pointing in that direction.
(defun draw-boids (boids)
(let ((boid-width 10)
(boid-length 20))
(loop for boid in boids
do (with-slots (pos velocity) boid
(with-pen (:fill +black+)
(let* ((dir (if (zerop (v-length velocity))
(vec2 0 -1)
(v-normalise velocity)))
(p1 (v+ pos (v-rescale (/ boid-length 2) dir)))
(p2 (v+ pos
(v-rescale (- (/ boid-length 2)) dir)
(v-rescale (/ boid-width 2)
(perpendicular-anticlockwise dir))))
(p3 (v+ pos
(v-rescale (- (/ boid-length 2)) dir)
(v-rescale (/ boid-width 2)
(perpendicular-clockwise dir)))))
(polygon (vx p1) (vy p1)
(vx p2) (vy p2)
(vx p3) (vy p3))))))))
As soon as we compile draw-boids
, the error screen disappears and our lovely boids are drawn into place. And we didn't have to restart the program to fix it!
There are two Common Lisp features that enable us to fix errors on-the-fly like we've done here:
Anyway, a worthy discussion of the condition system would take up a full blog post of its own. Back to Boids!
Now that our boids are drawn correctly, we want them to move around and do boid things. First, we implement an update-positions
function, which basically adds the velocity of each boid to its position (so that the boid moves), and applies the 3 Boidian forces to update the boid's velocity. For now, the functions implementing these forces are stubbed out.
(defun update-positions (boids)
(let ((max-velocity 10))
;; Update boid positions.
(map nil
(lambda (boid)
(setf (pos boid) (v+ (pos boid) (velocity boid))))
boids)
;; Update boid velocities.
(loop for boid in boids
do (setf (velocity boid)
(v-clamp max-velocity
(v+ (velocity boid)
(rule1 boid boids)
(rule2 boid boids)
(rule3 boid boids)))))))
;; Stubs! (For now).
(defun rule1 (boid boids)
(vec2 0 0))
(defun rule2 (boid boids)
(vec2 0 0))
(defun rule3 (boid boids)
(vec2 0 0))
We then have to modify the drawing loop to call update-positions
.
(defsketch boids
((width 400)
(height 400)
(restart-on-change nil)
(boids (loop repeat 20
collect (make-boid (random width)
(random height)))))
(background (gray-255 230))
(draw-boids boids)
(update-positions boids))
So far, these changes haven't affected the boid behaviour, so let's circle back and implement rule-1
, which can be summarised as "stay away from other boids". When a boid is less than 10 pixels from another boid, we push them away from each other to avoid crowding.
(defun rule1 (boid boids)
(let ((v-sum (vec2 0 0)))
(loop for boid2 in boids
for offset = (v- (pos boid) (pos boid2))
for dist = (v-length offset)
when (and (not (eq boid boid2)) (< dist 10))
do (v+! v-sum offset))
v-sum))
(Note: the vector functions ending in !
, like v+!
, follow the convention of storing the result in the vector passed as the first argument).
When we recompile this function...
...a pair of boids that happen to be too close to each other are sent flying off into the void. There's no counterforce to bring them back, just yet.
Next, we implement rule-2
: boids should fly towards the average position of other boids. Our implementation could be more efficient by summing the boid positions just once, rather than doing it for every single boid, but I can't be bothered.
(defun rule2 (boid boids)
(let ((center (vec2 0 0)))
(map nil
(lambda (boid2)
(when (not (eq boid boid2))
(v+! center (pos boid2))))
boids)
(v-scale! (/ (1- (length boids))) center)
(v-! center (pos boid))
(v-scale! (/ 200) center)
center))
Recompiling rule-2
, we get...
Yes! This is starting to look vaguely like Boids. Let's add the final rule, rule-3
: boids should match their velocity to all the other boids. Implementation note: we probably shouldn't update the velocities until all the new velocities have been calculated, but this doesn't seem to matter too much.
(defun rule3 (boid boids)
(let ((result (vec2 0 0)))
(map nil
(lambda (boid2)
(when (not (eq boid boid2))
(v+! result (velocity boid2))))
boids)
(v-scale! (/ (1- (length boids))) result)
(v-! result (velocity boid))
(v-scale! (/ 8) result)
result))
Recompiling, we see the Boids calm down a little bit.
Since it's not very bird-like to fly around in a vortex of death, we could also give the boids a purpose by making them follow the mouse position. The result of these changes can be seen at the top of the post.
(defsketch boids
((width 400)
(height 400)
(restart-on-change nil)
(boids (loop repeat 20
collect (make-boid (random width)
(random height))))
(mouse-pos (vec2 200 200)))
(background (gray-255 230))
(draw-boids boids)
(update-positions boids mouse-pos))
(defmethod on-hover ((instance boids) x y)
(setf (boids-mouse-pos instance) (vec2 x y)))
(defun update-positions (boids mouse-pos)
(let ((max-velocity 10))
(map nil
(lambda (boid)
(setf (pos boid) (v+ (pos boid) (velocity boid))))
boids)
(loop for boid in boids
do (setf (velocity boid)
(v-clamp max-velocity
(v+ (velocity boid)
(rule1 boid boids)
(rule2 boid boids)
(rule3 boid boids)
(v-rescale 0.1 (v- mouse-pos (pos boid)))))))))
And with that, we have a complete implementation of Boids! At the risk of beating a dead horse, I'll re-emphasise that we did the whole thing without once restarting our program or waiting a perceivable amount of time for code to compile.
I hope, in this brief demonstration of livecoding, I've given you a taste of how useful and fun this feature can be, whether you're developing a graphics application or mundane accounting software. Like I've said, it's not unique to Common Lisp, as at least Smalltalk and Erlang have similar capabilities. It's also possible to bridge the gap in less interactive languages by making applications automatically restart themselves when a code change is detected, or by bolting on a scripting language. Just do me a favour and ask yourself, the next time you're waiting the requisite time units for your code to recompile: How can I make this workflow more interactive? How can I make it more... like Common Lisp?
See the Wiki page, and also interactive programming. ↩
I also heavily leaned on this webpage for my implementation. Thanks, Conrad! ↩
I'd be happy to hear from you at galligankevinp@gmail.com.