Difficulty: Assumes you know both lisp and C.
And aren't afraid of getting dirty.
Embeddable Common Lisp lets me use a tiny bit of C to harness the
comprehensive SDL2 game library and start a game, but use lovely
common lisp for what happens in that game including logic and
graphics. Here I will explain what I jammed last night and this
afternoon.
The program is tiny, but I will go into considerable detail of the
entire project so far.
On Debian linux, the depencies are (which are easy to find out for
your platform)
```
sudo apt install libsdl2-dev ecl
```
These two are easy to get on any platform. In particular Android
phones are also a linux.
In common lisp, it's common to have a project folder in the
~/common-lisp/ directory. We will look at each file in this project
directory. Only one of them has much stuff in it.
```
$ cd ~/common-lisp/jam-no-theme/
$ ls
jam-no-theme.asd sdl-config.lisp jam-no-theme.lisp
```
the only funny looking file here is sdl-config.lisp which tells
embeddable common lisp to link a C library in the lisp program.
```jam-no-theme.asd
(defsystem "jam-no-theme"
:depends-on ("alexandria")
:components ((:file "sdl-config")
(:file "jam-no-theme"
:depends-on ("sdl-config"))))
```
This file tells us what filesystem files are in our project, and how
they fit together. With this, to interact live with our game in lisp
will be as easy as (require "jam-no-theme") . "alexandria" is a
famous external project
~/common-lisp/alexandria/
, and "sdl-config" and "jam-no-theme" refer to
~/common-lisp/jam-no-theme/sdl-config.lisp and
~/common-lisp/jam-no-theme/jam-no-theme.lisp
within the project folder.
Really I should have :version, :author, and :description but I don't
have to have those to start with.
```sdl-config.lisp
#-ecl(error "ECL only")
(ext:install-c-compiler)
(setf c:*USER-LD-FLAGS* "-lSDL2")
```
This is some non-portable lisp for the ECL compiler only. It tells
the compiler to link the C libSDL2, a famous game development C
library. I should insert #-ecl(error "ECL only") at the top here so
trying to compile with a different compiler would have an "ECL only"
error. #-foo(print "hello") is a common lisp thing: If the special
list *features* doesn't have :FOO in it, print "hello". There's also
#+foo(print "bar") similarly.
Now we can use all of libSDL2 in lisp files we compile.
And the game (so far)! I will intersperse #| block comments |#
```jam-no-theme.lisp
#| C language includes and variable declaration supporting SDL2. |#
(ffi:clines "
#include <SDL2/SDL.h>
SDL_Renderer *renderer;
SDL_Window *window;
SDL_Event e;
const Uint8 *state;
int mx, my; Uint32 mdown;
int quitted;
")
#| make a package for my game, but I only want to type ja: to use it.
Enter that package.|#
(Defpackage "jam-no-theme" (:use cl) (:nicknames :ja))
(in-package :ja)
#| This is the whole deal for getting to interleave C SDL2 and lisp.
|#
(defmacro game ((&rest shared-vars)
(&rest shared-declares)
&rest update-closures)
`(let ,shared-vars
,(append '(declare) shared-declares)
(ffi:c-progn ,(mapcar 'car shared-vars)
#|
in the rest of this form, any "string" is understood to be C
and any (list) is understood to be lisp. Lisp can mix into the C.
My next lines should be exactly the matching example C from
https://wiki.libsdl.org/SDL2/CategoryAPI
But with lisp errors instead of returns.
So check the SDL2 wiki if you like.
Note I have to escape \"\" to use them in C.
|#
"
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
\"Failed to init %s\",
SDL_GetError());
" (error "failed to SDL_Init(video)") "
}
if (SDL_CreateWindowAndRenderer(640,480,SDL_WINDOW_RESIZABLE,
&window, &renderer)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
\"Failed to create w & r%s\",
SDL_GetError());
" (error "failed to create window and renderer") "
}
"
#|
We got to the display loop
|#
"
quitted = 0;
for (;;) {"
#|
and event loop !
|#
"
while(SDL_PollEvent(&e))
if (e.type == SDL_QUIT) quitted = 1;
else if (e.type == SDL_KEYDOWN)
switch (e.key.keysym.sym) {
case SDLK_q:
quitted = 1;
break;
}
if (quitted) break;
mdown = SDL_GetMouseState(&mx, &my);
"
#|
It just breaks the loop on 'q' or exit signals, then if not it gets
the mouse state.
Next, clear the window with some random magic default color.
|#
"
SDL_SetRenderDrawColor(renderer, 0, 10, 20, 255);
SDL_RenderClear(renderer);
"
#|
Insert whatever was put (game () () #'over #'here)
into the game loop: We expect function closures.
|#
,@(loop for clos in update-closures collect `(funcall ,clos))
#|
Then render and delay a little bit.
|#
"
SDL_RenderPresent(renderer);
SDL_Delay(25);
"
#|
If we exit the window nicely, clean up.
Later, but not now, we have to guarantee this always happens.
|#
"
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
")))
#|
And that's that! Unless we add some interesting functions, the window
will be a black screen that you can easily close.
Everything from here on is just me experimentally jamming some game
functions. I control variable scope with respect to C in an
unhygeinic way. That's just how I roll. |#
(defmacro internally-counts (from below &aux (count (gensym)))
`(let ((,count ,from))
(lambda ()
(prog1 ,count
(when (equal (incf ,count) ,below) (setf ,count ,below))))))
(defmacro colorer (r g b a)
`(lambda ()
(ffi:c-inline (,r ,g ,b ,a) (:int :int :int :int) nil
"SDL_SetRenderDrawColor(renderer, #0, #1, #2, #3)"
:one-liner t)))
(defmacro line-drawer (x1 y1 x2 y2)
`(lambda ()
(ffi:c-inline (,x1 ,y1 ,x2 ,y2)
(:int :int :int :int) nil
"SDL_RenderDrawLine(renderer, #0, #1, #2, #3)"
:one-liner t)))
(defmacro incfer (var amount)
`(lambda () (incf ,var ,amount)))
(defmacro funcall-on-2 (function (&rest vars) form)
`(lambda ()
,@(loop for var in vars collect `(,function ,var ,form))))
(defun play ()
(game ((a 100) (b 200) (c 300) (d 400))
((:int a b c d))
(funcall-on-2 incf (a b c d) (1- (random 3)))
;; This would copy the variables, and end up doing nothing:
#| and is hence commented out.
(lambda ()
(loop for var in (list a b c d)
do (incf var (1- (random 3)))))
|#
(colorer 255 10 10 255)
(line-drawer a b c d)
(colorer 0 255 100 255)
(line-drawer 0 100 100 111)))
```
Now I can open a GUI window from interactive lisp like this:
```while in a shell
$ rlwrap ecl
> (require "jam-no-theme")
> (ja::play)
#<a window opens and blocks until q or the 'x' is pressed>
> (ja::play) ; I can do it again after closing it cleanly.
```