Annotated ECL SDL2 lisp game jam so far!


This is a copy of my own web mirrored phost here:

https://gopher.floodgap.com/gopher/gw.lite?=tilde.institute+70+302f7e73637265777...

wirrored from here: 

gopher://tilde.institute/1/~screwtape/208293581-day-1-lisp-game-jam-ecl-sdl2-annotated.txt

It's a little advanced. Please let me know if there is anything you would like me to wrap in an annotated example of. With this minimal boilerplate any followup topics can be made in a more introductory way.

---

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.
```

Get jam-no-theme eat berries collect treasure could technically control robots

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.