Interface and complete bindings to the Allegro 5 game programming library.
Check out how the source code is organized and compare it to the API reference.
al_*
becomesal:*
- Constants and types are shortened too, check the source code if you need help finding them
(al:rest secs)
is(al:rest-time secs)
because of symbol clash with#'cl:rest
- To access slots from a C struct, you can use
CFFI:MEM-REF
generate a plist(cffi:defcstruct display-mode (width :int) (height :int) (format :int) (refresh-rate :int)) (cffi:with-foreign-object (test '(:struct display-mode)) (let ((plist (cffi:mem-ref test '(:struct display-mode)))) (print plist) (print (getf plist 'width))))
- I’ve got a neat OPTIONAL lispy interface which provides an entire fixed timestep game loop
- Everything else is pretty much 1-to-1
- If you’re getting crashes on Mac OS X, put all your code into callback and pass it to al:run-main
- Examples exist if you get lost
Various projects I’ve found using cl-liballegro. Feel free to add items onto the list!
- cl-liballegro-nuklear - Bindings to the nuklear immediate mode GUI library
- Cycle of Evil - A fantasy-themed strategy/simulation with indirect player control
- Mana Break - Indirect colony simulator
- Thoughtbound - Post-modern dungeon crawler in fantasy setting
- Darkness Looming: The Dawn - Old school hack n’ slash
- simple-asteroids - Simple asteroids
- tanks - Tanks
- d2clone-kit - Diablo 2 clone game engine
- cookiecutter-lisp-game - Cookiecutter template for a game
- Gamedev in Lisp part 1, part 2 - Tutorials on ECS game architecture featuring cl-liballegro
The most basic usage is 1-to-1 just uses the bindings “as is” such as in this example.
Names have been changed to use a more lispy convention in which _
is
converted to -
. In most cases function names match like
al_flip_display(display);
becomes (al:flip-display display)
However types, constants, and structures have been shortened for user
convenience. There’s no exact rules for it, but usually any prefix
with ALLEGRO_*
or al_*
is truncated because Common Lisp has
multiple namespaces to handle naming clashes. For the rare edge
cases, check the source code definitions for constants and types.
Another change is that certain constants have been changed to Common
Lisp keywords. Keyboard functions in C use an enum values
corresponding to the key but cl-liballegro uses keywords instead. An
example is ALLEGRO_KEY_K
becoming :K
. CFFI takes care of
translating the value to the keyword and vice-versa. Using keywords
over constants tends to be convenient in practice.
Occasionally dropping down to a level lower using CFFI is necessary. One of these situations is passing a non-opaque data structure by reference.
Consider this block of C:
{
ALLEGRO_EVENT event;
bool running = true;
while (running) process_event(&event);
}
In Common Lisp we will use CFFI to allocate the structure for the corresponding Allegro 5 functions. Remember to free up the memory afterwards!
(defparameter *running-p* t)
(let ((event (cffi:foreign-alloc '(:union al:event)))
(loop while *running-p* do (process-event event))
(cffi:foreign-free event))
At times when something goes wrong the debugger pops up and a new
window is created without the previous one being destroyed. This is
due to how Common Lisp debugger restarts execution. One of the ways
to handle this is wrapping things in an UNWIND-PROTECT
or using the
condition handlers in Common Lisp. Errors should be handled in such a
way that restarts do not re-execute certain s-exps to create a new
display. Errors can also be handled by cleaning up resources.
An optional lisp interface is included with cl-liballegro which provides a full game loop with a fixed timestep and Entity Component System (ECS) implemented on the CLOS. Note that it is provided as is and not optimized. If performance is a concern, it is recommended to implement your own game loop while avoiding multiple dispatch and I will look forward to seeing your AAA game in the future.
- Define system which holds state
;; Creates a 800x600 resizable OpenGL display titled "Simple" ;; Fixed timestep loop runs logic at 1 FPS ;; The remaining time is spent on render ;; ;; The PREVIOUS-KEY slot is user-defined state for this example (defclass window (al:system) ((previous-key :initform "Nothing" :accessor previous-key)) (:default-initargs :title "Simple" :width 800 :height 600 :logic-fps 1 :display-flags '(:windowed :opengl :resizable) :display-options '((:sample-buffers 1 :suggest) (:samples 4 :suggest))))
- Implement Method for Logic Step
(defmethod al:update ((sys window)) (print 'one-logic-frame))
- Implement Method for Render Step
(defmethod al:render ((sys window)) (al:clear-to-color (al:map-rgb 20 150 100)) (al:flip-display))
- Implement Methods(s) for Event Handling
;; The lisp interface runs handlers during the logic step ;; Handlers are defined according to allegro events (defmethod al:key-down-handler ((sys window)) (let ((keyboard (cffi:mem-ref (al:event sys) '(:struct al:keyboard-event)))) (print (getf keyboard 'al::keycode)) (setf (previous-key sys) (getf keyboard 'al::keycode))))
- Run system
(al:run-system (make-instance 'window)))
Running on Mac OS X tends to behave oddly with threads because it
requires GUI related code to run in the main thread (affects programs
outside of Common Lisp too). The Allegro 5 library has a solution
with al_run_main. Define a callback with defcallback and pass it to
AL:RUN-MAIN
.
;; First define a callback
(cffi:defcallback my-main :void ()
;; Code goes in here
(function-with-gui-code))
;; Second execute by passing the callback to AL:RUN-MAIN
(al:run-main 0 (cffi:null-pointer) (cffi:callback my-main))
Common Lisp implementations tend to throw floating point calculation
errors such as FLOATING-POINT-OVERFLOW
and
FLOATING-POINT-INVALID-OPERATION
by default (called traps) to be
explicitly handled rather than ignored. There are situations where
this is valid behaviour but sometimes such errors get thrown despite
valid code being called through the foreign function interface (FFI).
In this case it should be safe to ignore using implementation specific routines or the float-features portability library:
;; SBCL
;; Sets traps globally
(sb-int:set-floating-point-modes :traps (:invalid :inexact :overflow))
;; SBCL
;; Code wrapped in the macro ignores floating point errors in the list
(sb-int:with-float-traps-masked (:invalid :inexact :overflow)
(function-with-floating-point-errors))
;; float-features (portability library)
;; Code wrapped in the macro ignores floating point errors in the list
(float-features:with-float-traps-masked (:divide-by-zero
:invalid
:inexact
:overflow
:underflow)
(function-with-floating-point-errors))
There are path problems in Windows because the Allegro 5 library files
which contain all the functions the CFFI calls upon do not have a
default location unlike Unix environments. When the library is loaded
under Windows, CFFI will look for the library files in the current
folder of the FILE.LISP that evaluates (ql:quickload
"cl-liballegro")
. This means a copy of the library files must be in
the directory of FILE.LISP, not in the cl-liballegro directory unless
the FILE.LISP is in there. SLIME however, likes to change the default
search folder to the one Emacs is in when it starts.
;; Open command prompt in the folder that contains both the DLL and game.lisp > sbcl > (load "game.lisp") ; File contains (ql:quickload "cl-liballegro")
game.lisp contains (ql:quickload :cl-liballegro)
;; Looks for the DLL at /path/to/Desktop/allegro.dll C-x C-f /path/to/Desktop/file9.lisp M-x slime C-x C-f /path/to/Desktop/game/game.lisp C-c C-l
;; Looks for the DLL at /path/to/Desktop/game/allegro.dll C-x C-f /path/to/Desktop/file9.lisp C-x C-f /path/to/Desktop/game/game.lisp M-x slime C-c C-l
;; Looks for the DLL at /whatever/default/emacs/directory/allegro.dll M-x slime C-x C-f /path/to/Desktop/game/game.lisp C-c C-l
There are Gray streams wrapping liballegro file IO APIs:
;; text stream
(with-open-stream (stream (al:make-character-stream "credits.txt"))
(uiop:slurp-stream-lines stream))
;; binary stream
(with-open-stream (stream (al:make-binary-stream "loot.ase"))
(let ((result (make-array (al:stream-size stream)
:element-type '(unsigned-byte 8))))
(read-sequence result stream)
result))
Note: those can be particularly useful when combined with the liballegro PhysicsFS addon, which can help with reading files located within game archives, such as Quake PAK files, zip archives etc.
To mount such an archive as a folder, use the PHYSFS_mount function from
libphysfs
library (usually dynamically linked to liballegro
, except in official
Windows builds, where it is statically linked):
#-win32 (progn
(cffi:define-foreign-library libphysfs
(:darwin (:or "libphysfs.3.0.2.dylib" "libphysfs.1.dylib"))
(:unix (:or "libphysfs.so.3.0.2" "libphysfs.so.1"))
(t (:default "libphysfs")))
(cffi:use-foreign-library libphysfs))
(cffi:defcfun ("PHYSFS_mount" physfs-mount) :int
(new-dir :string) (mount-point :string) (append-to-path :int))
(assert (not (zerop (physfs-mount "archive.zip" (cffi:null-pointer) 1))))
;; now al:make-character-stream and al:make-binary-stream are able to
;; open files from archive.zip
cl-liballegro is organized according to the Allegro 5 Documentation with functions, types, and constants separated.
CFFI is used and its manual recommended to understand more advanced uses though not required for most cases.
Naming conventions has a preference for truncating ALLEGRO
or al
for user convenience since Common Lisp has multiple namespaces for
resolving symbol names. For the rare edge cases, check the types and
constants
Usage of keywords over enums preferred for user convenience.
- ./src/constants/: Allegro 5 constants, enums, and flag definitions
- ./src/ffi-functions/: Allegro 5 function definitions
- ./src/types/: Allegro 5 type definitions
- ./src/interface/: Common Lisp interface definition, optional fixed timestep game loop implemented with CLOS, Gray streams wrapping file APIs.
- ./src/package.lisp: Common Lisp package definition, exports usable symbols
- ./src/library.lisp: CFFI library definition, loads Allegro 5 library files into memory
- ./cl-liballegro.asd: ASDF project definition, specifies source files to be loaded
- [ ] New bindings added for export to package defintion
- [ ] New source files added for loading to the project definition
- [ ] Bump version and add description of changes
FYI these bindings are so stable it can make the repo look dead
Project under zlib license