Abstract
LISPBUILDER-SDL comprises several of packages that allow game development using Common Lisp. LISPBUILDER-SDL provides a set of bindings and Lispy abstractions for SDL and other graphics, sound, physics, character animation and 3D libraries. LISPBUILDER-SDL core functionality includes window and event management, 2D graphics, 3D graphics using OpenGL and sound support. The goal for the LISPBUILDER-SDL project is to become a useful resource for the development of games in Lisp.
SDL provides the low-level 2D rendering support. LISPBUILDER-SDL adds 2D graphical effects such as rotation, rendering circles, polygons, squares, Bezier and Cuttmull-Rom curves as well as bitmap font support. Additional packages provide native C drawing functions, True Type font rendering, loading of multiple image formats, a sound mixer and networking support. The lispbuilder packages are meant to work together with each package providing a specific core set of functionality. For example, an image that is loaded by lispbuilder-sdl-image may be rotated using lispbuilder-sdl-gfx. Text may be rendered to a surface using lispbuilder-sdl-ttf and finally blitted to the display using lispbuilder-sdl.
- lispbuilder-sdl-gfx: 2D graphical effects such as zooming, rotation, circles, polygons and squares, using SDL_gfx
- lispbuilder-sdl-image: Support for multople image formats such as PNG and JPG, using SDL_image
- lispbuilder-sdl-mixer: Sound mixing, using SDL_mixer a multi-channel audio mixer library
- lispbuilder-sdl-ttf: True-type font rendering, using SDL_ttf
- lispbuilder-sdl-net: Networking, using SDL_net a cross-platform, blocking network library
Current Version: The version of LISPBUILDER-SDL to use is the latest version in SVN.
This example was created with the following code:
(sdl:with-init () (sdl:window 320 240) (sdl:draw-surface (load-image "lisp.bmp")) (sdl:update-display) (sdl:with-events () (:quit-event () t) (:video-expose-event (sdl:update-display))))
The following table describes the status of LISPBUILDER-SDL on the major Common Lisp implementations:
Lisp Implementation | LISPBUILDER-SDL Status | Comments | ||
---|---|---|---|---|
Win32 | Linux | MacOS | ||
CLISP v2.38 | Working | Working | Working | |
Lispworks v4.4.6 Personal | Working | Working | Working | |
Allegro Express 8.0 | Unknown | Working | Unknown | |
OpenMCL | NA | NA | Unknown | |
SBCL | Working | Working | Unknown | |
CMUCL | Not Working | Not Working | Not Working |
See the LISPBUILDER documentation for downloads.
See the LISPBUILDER documentation for installation instructions.
Enter the following at the REPL to compile and load the LISPBUILDER-SDL package:
(asdf:operate 'asdf:load-op :lispbuilder-sdl)ASDF will take care of loading the CFFI and :LISPBUILDER-SDL dependencies. The SDL.dll library will also be loaded into the Lisp image at this time.
Enter the following at the REPL to compile and load the examples included in the LISPBUILDER-SDL-EXAMPLES package:
(asdf:operate 'asdf:load-op :lispbuilder-sdl-examples)
Run the examples by entering any of the following at the REPL:
(SDL-EXAMPLES:BEZIER) (SDL-EXAMPLES:BMP-SAMPLE) (SDL-EXAMPLES:CIRCLE-1) (SDL-EXAMPLES:CIRCLE-2) (SDL-EXAMPLES:CIRCLE-3) (SDL-EXAMPLES:CIRCLE-4) (SDL-EXAMPLES:CIRCLE-5) (SDL-EXAMPLES:DISTANCE-2D) (SDL-EXAMPLES:FLOOD-FILL) (SDL-EXAMPLES:INBUILT-FONTS) (SDL-EXAMPLES:LINE-DRAWING) (SDL-EXAMPLES:MANDELBROT) (SDL-EXAMPLES:METABALLS) (SDL-EXAMPLES:MOUSE-2D) (SDL-EXAMPLES:MOUSE-PAINTER) (SDL-EXAMPLES:PIXELS-1) (SDL-EXAMPLES:PIXELS-2) (SDL-EXAMPLES:PIXELS-3) (SDL-EXAMPLES:PIXELS-4) (SDL-EXAMPLES:POINTS-AND-LINES) (SDL-EXAMPLES:RANDOM-RECTS) (SDL-EXAMPLES:RANDOM-BOX-1) (SDL-EXAMPLES:RANDOM-BOX-2) (SDL-EXAMPLES:RANDOM-BOX-3) (SDL-EXAMPLES:RANDOM-BOX-4) (SDL-EXAMPLES:RECURSIVE-RECTS) (SDL-EXAMPLES:SETUP-AND-DRAW) (SDL-EXAMPLES:SIMPLE-FONT-DEMO) (SDL-EXAMPLES:STROKE) (SDL-EXAMPLES:VERTICES) (SDL-EXAMPLES:WIDTH-HEIGHT)
None.
LISPBUILDER-SDL is distributed under the MIT-style license.
Functions and symbols exported from the LISPBUILDER-SDL package are
accessible from the LISPBUILDER-SDL:
prefix or the
shorter form SDL:
nickname.
The cffi/
directory contains the raw CFFI bindings.
These bindings may be automatically generated by SWIG or created by hand.
CFFI translation functions perform simple low-level conversions for example,
converting Lisp strings to C strings and back (see cffi-translate.lisp
).
All functions in cffi/
accept and return foreign objects only.
The base/
directory defines wrappers over the functions in cffi/
.
The functions in base/
accept keyword arguments and accept NIL instead
of CFFI:NULL-POINTER where appropriate. Generally functions in base/
accept and return foreign
objects. base/
may perform some type checks (IS-VALID-PTR SURFACE) but
this layer is meant to be lean. Someone who implements a graphics engine
might use this layer instead of sdl/
if speed is a concern.
There are no fancy drawing primitives in this layer.
The sdl/
directory defines the abstractions over cffi/
and base/
.
Foreign objects are passed around sdl/
neatly wrapped in CLOS objects, using TRIVIAL-GARBAGE for
automatic garbage collection (minimize foreign objects being left on the
heap). There are no functions in sdl/
that accept or return foreign
objects (with the exception of the functions that create the CLOS
wrapper objects) Functions in sdl/
call down into
cffi/
and base/
as appropriate. All LISPBUILDER-SDL symbols available in
SDL:
are exported from sdl/
, with symbols imported into sdl/
from
cffi/
and base/
as appropriate (e.g. WITH-EVENTS). All drawing primitives are
defined in this layer; circles, rectangles, lines, triangles, with-bezier etc.
Functions in sdl/
implement a lot of type checking.
An example of the difference between base/
and sdl/
is
WITH-RECTANGLE. The WITH-RECTANGLE macro in base/
creates and destroys a foreign
SDL_Rect. The WITH-RECTANGLE macro in sdl/
will create and destroy
a RECTANGLE object.
The SDL library and SDL subsystems must be initialized prior to use. This is handled automatically when using the macro WITH-INIT. WITH-INIT also uninitialises the SDL library upon exit. More detailed information on the LISPBUILDER-SDL initialisation process can be found in the documentation for WITH-INIT.
(WITH-INIT (SDL-INIT-VIDEO) ...)
A window must be created to display any kind of output. A window of a specific pixel width and height is created using the function WINDOW. WINDOW takes additional keyword parameters to set the requested video mode, color bit depth, the window title and the icon title. More detailed information can be found in in the documentation for WINDOW.
(WITH-INIT (SDL-INIT-VIDEO) (WINDOW 320 240))
LISPBUILDER-SDL provides the WITH-EVENTS macro that simplifies the game loop and handling SDL events. Event handlers are defined by specifying the event type a well as an optional list of fields specific to that event.
More detailed information on all SDL events (Keyboard events, Joystick events, User events etc.) can be found in in the documentation for WITH-EVENTS.
For example, to process the current x and y mouse position when the mouse is moved:
(:MOUSE-MOTION-EVENT (:X X-POS :Y Y-POS) ...)
Or to process the x and y relative mouse positions when the mouse is moved:
(:MOUSE-MOTION-EVENT (:X-REL relative-x :Y-REL relative-y) ...)
The only event that must be handled with WITH-EVENTS is the :QUIT-EVENT. If :QUIT-EVENT is ignored then it is impossible for the user to close the SDL Window or exit the game loop.
(WITH-EVENTS () (:QUIT-EVENT () T) (:MOUSE-MOTION-EVENT (:X mouse-x :Y mouse-y) ...))
In addition to handling events, WITH-EVENTS also manages the game loop. Introducing the :IDLE event. :IDLE, although not technically an actual event, is executed once per game loop after all events have been processed in the SDL event queue. :IDLE is where the user can place code that needs to be executed once per game loop and that is not 'event' driven. This code may involve updating world physics, updating the state of game objects and rendering sprites to the display.
(WITH-EVENTS () (:QUIT-EVENT () T) (:MOUSE-MOTION-EVENT (:X mouse-x :Y mouse-y) ...) (:IDLE () (UPDATE-DISPLAY)))
WITH-EVENTS can also limit execution of the the game loop to a specific number
of iterations a second using FRAME-RATE. This effectively limits the number of
frames displayed per second. To set the frame rate to 60 frames per second,
(SETF (SDL:FRAME-RATE) 60)
. To unlock the frame rate effectively running the game loop as fast
as the CPU will allow set the FRAME-RATE to NIL, (SETF (SDL:FRAME-RATE) NIL)
The SDL display surface needs to be updated once per game loop, as described in the section SDL Display.
(SETF (FRAME-RATE) 30) (WITH-EVENTS () ...)
LISPBUILDER-SDL supports the SDL RECTANGLE and COLOR primitives. These are described below.
A new rectangle is created by the function RECTANGLE. The function RECTANGLE-FROM-EDGES will create a new rectangle from the specified top left and bottom right coordinates. The function RECTANGLE-FROM-MIDPOINT-* will create a new rectangle from midpoint.
The macros WITH-RECTANGLE and WITH-RECTANGLEs will create a new rectangle and free this rectangle when it goes out of scope.
The rectangle position and width and height can be inspected and modified using the following functions: X, Y, WIDTH and HEIGHT. X2 and Y2 will return the (+ x width) and (+ y height) of the rectangle respectively.
Additional functions that operate on rectangles are as follows:
A new SDL color is created by COLOR, returning either a new RGB or RGBA color.
RGB/A color componets can be manipulated using the functions R, G, B, A, SET-COLOR and SET-COLOR-*. Two colors may be compared using COLOR=.
Functions that accept a color parameter will most likely bind color to *DEFAULT-COLOR* when unspecified. The macro WITH-COLOR will bind a color to *DEFAULT-COLOR* within the scope of this macro. When *DEFAULT-COLOR* is bound to a color, all subsequent drawing functions will use this implied color while it remains in scope.
Additional functions that operate on colors are as follows:
LISPBULDER-SDL contains several predefined colors; *BLACK*, *WHITE*, *RED*, *GREEN*, *BLUE*, *YELLOW*, *CYAN*, *MAGENTA*.
LISPBUILDER-SDL provides low-level drawing support for several primitives. Most primitives can be drawn with or without alpha transparency. In addition, the filled primitives can be drawn with a single pixel outline (or stroke) that is a different color to the fill color.
Drawing functions require a target surface and color. LISPBUILDER-SDL uses defaults for both target surface (*DEFAULT-SURFACE*) and color (*DEFAULT-COLOR*) unless these are otherwise specified by the user. The macros with-surface, and with-surfaces will bind a surface to *DEFAULT-SURFACE* within the scope these macros. For example, instead of specifying a target surface for each draw-* function a user may bind *DEFAULT-SURFACE* to a surface once and subsequent draw-* functions will use this implied surface while it remains in scope.
(DRAW-POINT (sdl:point :x x1 :y y1) :surface a-surface :color *white*) (DRAW-POINT (sdl:point :x x2 :y y2) :surface a-surface :color *white*) (DRAW-POINT (sdl:point :x x3 :y y3) :surface a-surface :color *white*) (DRAW-POINT (sdl:point :x x4 :y y4) :surface a-surface :color *white*)
Now, we use WITH-SURFACE to bind a surface to *DEFAULT-SURFACE*.
(WITH-SURFACE (a-surface) (DRAW-POINT (sdl:point :x x1 :y y1) :color *white*) (DRAW-POINT (sdl:point :x x2 :y y2) :color *white*) (DRAW-POINT (sdl:point :x x3 :y y3) :color *white*) (DRAW-POINT (sdl:point :x x4 :y y4) :color *white*))
We can use WITH-COLOR to bind *DEFAULT-COLOR* to *WHITE*:
(WITH-SURFACE (a-surface) (WITH-COLOR (*white*) (DRAW-POINT (sdl:point :x x1 :y y1)) (DRAW-POINT (sdl:point :x x2 :y y2)) (DRAW-POINT (sdl:point :x x3 :y y3)) (DRAW-POINT (sdl:point :x x4 :y y4)))
Finally, if the target surface is the display then *WITH-SURFACE* is not required as :SURFACE when NIL will be bound to *DEFAULT-DISPLAY* as a default. So the above code can be shortened as follows:
(WITH-COLOR (*white*) (DRAW-POINT (sdl:point :x x1 :y y1)) (DRAW-POINT (sdl:point :x x2 :y y2)) (DRAW-POINT (sdl:point :x x3 :y y3)) (DRAW-POINT (sdl:point :x x4 :y y4)))
LISPBUILDER-SDL contains several bitmap fonts of different sizes and faces (italics/bolded). See INITIALISE-FONT for the complete list of built-in fonts.
Fonts are initialised using INITIALISE-DEFAULT-FONT, or INITIALISE-FONT. The resouces allocated to a font are freed by FREE-FONT. A font must be initialised prior to use.
LISPBUILDER-SDL provides the macros WITH-DEFAULT-FONT and WITH-FONT for the above functions that bind *DEFAULT-FONT* to font within the scope of the macro.
LISPBUILDER-SDL supports the rendering of colored text over a transparent background (Solid rendering), and the rendering of colored text over a solid colored background (Shaded rendering). Text may be left, right or center justified. Text can be drawn directly to a target surface using DRAW-STRING-SOLID and DRAW-STRING-SHADED. Text can be rendered to a new surface of character height and string width using RENDER-STRING-SOLID and RENDER-STRING-SHADED. This new surface may be optionally cached in the font object. A cached surface font can be accessed using CACHED-SURFACE) and can be blitted to a target surface using DRAW-FONT.
The font rendering functions accept a font object. This font parameter is bound to *DEFAULT-FONT* when unspecified. The function INITIALISE-DEFAULT-FONT and the macros WITH-FONT and WITH-DEFAULT-FONT will bind a font to *DEFAULT-FONT* within the scope of these macros. When *DEFAULT-FONT* is bound to a font, all subsequent font drawing or font rendering functions will use this implied font while it remains in scope.
(WITH-COLOR (*WHITE*) (WITH-FONT (*FONT-8x8*) (DRAW-STRING-SOLID-* "Font is 8x8." 10 10) (WITH-FONT (*FONT-10x20*) (DRAW-STRING-SOLID-* "Font is 10x20." 10 20)) (DRAW-STRING-SOLID-* "Font is 8x8 again." 10 40)))
An SDL surface, SURFACE, represents an area of graphical memory that can be drawn or rendered to, for example the video framebuffer or an image that is loaded from disk.
A surface is created:
Functions that accept a surface parameter will most likely bind surface to *DEFAULT-SURFACE* when unspecified. The macros WITH-SURFACE and WITH-SURFACES will bind a surface to *DEFAULT-SURFACE* within the scope of these macro. When *DEFAULT-SURFACE* is bound to a surface, all subsequent drawing functions will use this implied surface while it remains in scope.
A surface can be explicitely freed by calling FREE-SURFACE.
BMP images are loaded from disk using LOAD-IMAGE. Support for additional image formats is provided in the LISPBUILDER-SDL-IMAGE package. A surface is saved to disk as a BMP image using SAVE-IMAGE.
(DRAW-SURFACE (LOAD-IMAGE "sdl.bmp") :surface *default-display*)
A SURFACE stores its own position X/Y coordinates. These coordinates can be inspected and modified using the following functions: X, Y, WIDTH and HEIGHT.
Additional functions and macros that manage surface coordinates are as follows:
The functions BLIT-SURFACE and DRAW-SURFACE will blit or draw a source surface onto a destination surface using the position coordinates stored in the source surface. DRAW-SURFACE-AT will draw the source surface at a specified position.
The composition rules that determine how the source surface is composed over the destination surface are described in the description of BLIT-SURFACE. FILL-SURFACE fill will the target surface with a single color. Drawing Primitives describes how *DEFAULT-SURFACE* is used when calling blitting operations.
Use SET-COLOR-KEY, CLEAR-COLOR-KEY and SET-ALPHA to modify the key color and alpha transparency properties of a surface after the surface has been created.
Set a clipping rectangle to limit the draw area on a destination surface. The clipping rectangle for a surface is manipulated using GET-CLIP-RECT and SET-CLIP-RECT. When this surface is the destination of a blit, only the area within the clip rectangle will be drawn into.
Set a cell rectangle to limit the surface area on the source surface. The cell rectangle for a surface is manipulated using CLEAR-CELL and SET-CELL. When this surface is the source of a blit, only the areas within the cell rectangle will be used. The cell is useful when only a small area of the source surface needs to be blitted to the destination surface. For example a sequence of images composed into a single sprite sheet and only the current frame of animation is to be drawn to the display at any one time.
The functions UPDATE-DISPLAY and UPDATE-SURFACE update the SDL display surface (or OpenGL context) and general SDL surfaces respectively.
The SDL display surface must be updated at least once each game loop using the function UPDATE-DISPLAY. This function will call SDL-GL-SWAP-BUFFERS to update the OpenGL display, or SDL-FLIP to update the SDL surface depending on the current *OPENGL-CONTEXT*. The display can be filled with a background color using CLEAR-DISPLAY.
The properties of an SDL surface can be queried using SURFACE-INFO. The properties of the SDL display and video hardware can be queried using VIDEO-INFO. The screen resolutions supported by a particular set of video flags can be retrieved using LIST-MODES. A pointer to the native SDL window can be retrieved using GET-NATIVE-WINDOW. The name of the video driver can be retrieved as a STRING using VIDEO-DRIVER-NAME.
The the current state of the cursor is returned using QUERY-CURSOR, and is set using SHOW-CURSOR.
Putting this all together, we can write short example showing a white rectangle that follows the users mouse movements within an SDL Window. Exit the example by closing the window or pressing the Esc key on the keyboard.
;; Initialise SDL and the Video subsystem (WITH-INIT (SDL-INIT-VIDEO) (WINDOW 320 240) ; Open a window, 320 x 240 (SETF (FRAME-RATE) 30) ; Lock the frame rate to 30 fps (WITH-EVENTS () (:QUIT-EVENT () T) ; Absolutely have to handle the :QUIT-EVENT (:KEY-DOWN-EVENT (:KEY key) (WHEN (KEY= key :SDL-KEY-ESCAPE) (PUSH-QUIT-EVENT))) (:MOUSE-MOTION-EVENT (:X mouse-x :Y mouse-y) (CLEAR-DISPLAY *black*) ;; Draw the box with center at the mouse x/y coordinates. (DRAW-BOX-* (- mouse-x (/ 50 2)) (- mouse-y (/ 50 2)) 50 50 :color *white*)) (:IDLE () (UPDATE-DISPLAY))))
...
~:*~A
~}