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 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) # > (ja::play) ; I can do it again after closing it cleanly. ```