lgame is a pygame-inspired library for making simple games in Common Lisp using cl-sdl2.
This is very very pre-alpha quality and as such I don't really recommend it to others yet. Still, perhaps it will be useful alongside lgame-examples as a reference or study? You can at least checkout the screenshots in there.
If you do end up using lgame anyway, it'd be nice to know so I can try and avoid breaking your code with future changes, or at least provide a fix-up patch!
But seriously, don't use this. It's fun to try and make games in an old-fashioned style, that is, trying to do everything on a single thread, and it can be a useful platform for learning a few things or just spinning up a quick UI window, but every time I do something with this library I have to resist the urge to throw it all away and start over with something new. Something that's multi-threaded and something that takes care of garbage memory, even the foreign-allocated stuff, automatically.
Compared to the 4-line example shown by trivial-gamekit, lgame takes a bit more effort. But for the full control it may be worth it. Assuming you have loaded lgame already (see next Usage section) you can copy this to a file and load it, or paste it directly into your REPL:
(defpackage #:lgame.example.hello
(:use #:cl))
(in-package :lgame.example.hello)
(defun main ()
(lgame:init)
(lgame.display:create-centered-window "Hello Comparison" 800 600)
(lgame.display:create-renderer)
(let* ((font (lgame.font:load-font (lgame.font:get-default-font) 15))
(txt (lgame.font:render-text font "Hello, lgame!" 0 0 0))
(txt-rect (lgame.rect:get-texture-rect txt)))
(lgame.rect:move-rect txt-rect 240 (- 600 240 (sdl2:rect-height txt-rect))) ; gamekit's origin is bottom-left, we are top-left following SDL
(lgame.time:clock-start)
(unwind-protect
(loop while (lgame.time:clock-running?) do
(livesupport:continuable
(game-tick txt txt-rect)))
(sdl2:free-rect txt-rect)
(sdl2:destroy-texture txt)
(lgame:quit))))
(defun game-tick (txt txt-rect)
(lgame.event:do-event (event)
(when (= (lgame.event:event-type event) lgame::+sdl-quit+)
(lgame.time:clock-stop)))
(lgame.render:set-draw-color 255 255 255)
(lgame.render:clear)
(lgame.render:blit txt txt-rect)
(lgame.render:present)
(livesupport:update-repl-link)
(lgame.time:clock-tick 60))
(main)
If you are using SLIME, the use of the livesupport calls should prevent your main REPL loop from being taken over. This is great for interactive development, and makes making a single-game-thread sort of small game not too terrible.
lgame isn't meant for big complex games, you really want a full-fledged engine for those.
If you're lost on how to structure a game, a general pattern you can use (I picked it up long ago from Andy Harris) is acronymed as IDEA/ALTER. IDEA is the overall structure and ALTER is the structure of the game loop.
- Initialize -- Init lgame and anything else that should be setup at the start
- Display setup -- window and size, renderer, window title, etc.
- Entities -- make all the game entities that will be used (player, background, enemies, HUD, etc.)
- Action -- create and run the game loop, every iteration should result in a new frame to display
The game loop itself can be broken down with ALTER:
- Assign important values -- anything you need for the loop itself, like a clock
- Loop -- begin the game loop running until the game is quit, often convenient to have the body call a game-tick function
- Time -- manage time to run at a desired frame rate. I prefer this at the end of the loop, though
- Events -- handle user events and use them to update entities, which themselves may produce further events like AI logic or collisions. You can also think of this section as a logic and physics processing step
- Render -- when all the game state is finalized, render out the frame
You can see this pattern in the example above.
Avoiding spaghetti code can be difficult as the size of the game increases, though, so consider specific architecture beforehand to better assist in each step. Possibly even switching to an ECS framework instead. (Or as another example, if you have thousands of things bouncing around, instead of checking every object against every other object for collisions, you probably want to have some sort of collision manager / service / singleton that every object registers itself to and updates if needed, and then the collision manager itself can resolve collisions in a more efficient manner.)
lgame is not in Quicklisp, so you should first clone it to
~/quicklisp/local-projects/ or put a symlink there so that your local quicklisp
and ASDF can find it. Try to (ql:quickload "lgame")
and verify all its dependencies are
loaded. In particular, you'll need to first install some libraries to your OS if
you haven't done so already:
- SDL2
- SDL2-image
- SDL2-mixer
- SDL2-ttf
The SDL libraries should be "fully loaded", i.e. if you're on gentoo, you'll need to make sure libsdl2 includes things like haptic support. This is because by default lgame tries to initialize SDL with "everything".
After verifying it loads, you can try running some of the examples in
lgame-examples to see that it
works. They're written to be runnable standalone with sbcl --script
or except
for taste.lisp
to be runnable by
loading the file and executing (main)
. Some examples will take over the REPL
thread, but some make use of
livesupport for interactive
development.
After that, happy hacking! aliens.lisp
is probably the most full-featured
example at the moment for you to consider referencing, but my goal is that by
looking at a random pygame game or a using-SDL2-with-C/C++ tutorial or the
official SDL docs you should be able to gather how
to accomplish the same things using lgame and not have to hunt around too deeply
through underdocumented Lisp code. I've provided a Library
Organization section here in the readme covering the
namespaces of lgame which can help orient you further.
lgame code itself is licensed under the Unlicense and as such is in the public
domain. You can use any part of this as-is or snip it into your own code
without attribution. Assets under assets/
are also in the public domain,
unless they are accompanied by their own individual licenses, such as the
default included font.
lgame examples are also under the Unlicense/public domain, unless a particular example indicates otherwise.
If you want to build and distribute binaries using lgame, it is important to
adhere to the license requirements of all dependencies, such as SDL and various
Lisp systems used by lgame. lgame.util:display-licenses
can help find this
information.
If you do end up using this or wanting to use this despite the warnings and
limitations, and find some issues or want to contribute back some fixes or
features, feel free to open up an issue and/or pull request and I'll try to look
into it but no promises. For a pull request in particular, I'm happy, like many
projects, to assume implicit agreement with the 'license' choice if nothing
special is said, but it'll help if you explicitly include somewhere a statement
acknowledging that you're putting such a contribution into the public domain,
see unlicense.org if you want something to copy. If
you disagree with putting stuff into the public domain, I'm open to accepting a
contribution in the form of an additional dependency to lgame.asd
pointing to
your extension. Also happy if you want to put yourself in a new authors.md file.
The goal of lgame is not to duplicate pygame's API entirely, or even to wrap SDL2 and friends as thoroughly and carefully, but to provide something close enough which facilitates making the sorts of programs found on pygame.org about as easily but in Common Lisp. To further that end, lgame is not against eventually becoming a sort of "kitchen sink" of features that can be useful for a lot of games. For instance, some A* pathfinding code is included. Basically as I make my own games or game concepts, if I find myself needing something in more than one of them, it's likely to end up in lgame, or at least an example.
A lot of code so far is very optimistic and doesn't bother with all the error
checks recommended by SDL or in many cases done for you by cl-sdl's sdl2:
package wrapper functions. I'll try to improve such things over time but it's
not a high priority.
Similarly I haven't gotten anything to the point where I want to build and distribute binaries beyond my own machines. I develop with gentoo linux, so there may be platform specific bugs with Mac or Windows that I haven't run into. I also don't plan on ever explicitly testing and supporting Mac for either development or game binaries, so only Windows will realistically receive my attention for faults. If you want to contribute Mac fixes though, I'll accept them.
lgame is currently "single-threaded preferred". That is, like most pygame games, the style of development should be that one thread is responsible for initializing everything, loading assets (and unloading them later), and running the game loop. This keeps things on a relatively 'happy' path with respect to threading issues, especially around foreign memory and GL stuff, but it's obviously not modern. I'd like to experiment sometime with a more multi-threading aware architecture, including one that automatically and safely manages foreign memory, but this will likely end up changing a lot of interfaces and may be better to do without SDL under the hood in the first place.
Despite having some useful wrappers, it's not nearly complete enough a wrapper over SDL2 as is Pygame. Since SDL is a C library, if you were using C or C++ you would have to worry about low level memory management yourself. Pygame entirely eliminates that, mostly because it itself is implemented as mostly C code to expose a nicer interface to Python.
cl-sdl2 does not handle the memory management for you in the general case. What's worse, the documentation can be actively misleading!
(In 2022, https://github.com/lispgames/cl-sdl2/commit/2d761165f01f03f2a8012be683828895ee6821c9 made the following remarks obsolete; I've left it here for now as a historical note.)
--begin outdated remarks--
This commentary isn't meant to denigrate anyone who has contributed to the project, I'm very
glad that cl-sdl2 exists, and arguably I could help things more with a pull
request rather than this note, but consider
rect.lisp.
Its first function, make-point
, has lying documentation that it will be garbage
collected as needed. It is lying because, if you look at
cl-autowrap's plus-c:c-let
function
that make-point
is using, the let takes an optional :free t
statement that
by default is nil
. Of course it must be nil
here for make-point
, because
if it simply allocated and freed, it would have nothing valid to return. The
conclusion is that if you use sdl2:make-point
, or other functions like
sdl2:make-rect
, you are responsible for calling sdl2:free-point
and
sdl2:free-rect
later on when you are done with them. The GC will not clear up
this foreign memory for you.
Now if you look at the commit history of that file, you'll find a big commit about disabling finalizers. What are those? They are a concept in the trivial-garbage library. In theory, with careful usage, they allow you to create a GC-trackable object wrapper, and when the GC decides to garbage collect that wrapper, you can have it call a custom hook that can clean up the underlying foreign resource unknown to the GC. cl-sdl2-ttf, which lgame currently [ed: no longer] relies on to provide bindings for SDL2-TTF for font rendering, does still use finalizers, possibly correctly, and even on underlying SDL objects like surfaces where my preference is to just explicitly free them.
I asked mfiano about that commit once, here's what he said:
(09:45:21 AM) mfiano: In the past cl-sdl2 had finalizers, but they caused many bugs, like double-frees. I removed that the first chance I got when I took over the project.
(09:45:37 AM) mfiano: Bugs mostly because it's own codebase was using them wrong.
(09:46:19 AM) mfiano: Not to mention, you never know when a finalizer is going to run with the GC, and they have to be finalized on the same thread they were constructed on. It was a serious bug trap
This seems reasonable to me. So again, just a little bit of doc string cleanup
would go a long way to making it clear that people are responsible, unless they
are able to use a lexical with-...
macro like with-rects
which you can see
does does have a call to free-rect
. Similarly if they just used the previously
mentioned c-let
with :free t
. However even that might not be desirable,
see the Library Organization section on the lgame.event package
for some notes on how I try to have macros that enable stack allocation.
--end outdated remarks--
At the time of this writing, lgame expects the game to run in a single thread, but I want to make it multi-threaded eventually, or at least to not fall apart if someone else wants to incorporate it in a multi-threaded game. The elephant in the room is that bad stuff can happen on various platforms when you don't work with memory interacting with the GPU (think: textures, GL calls) from within the same thread. It's a commonly discovered problem for C++ developers too. This despite that the concept of "render thread" is legacy/technical debt now and that modern engines use a multi-threaded job-scheduling architecture with nothing called the render thread. One day I'll learn more about what's actually going on but for the curious see cl-sdl2's source itself for various Mac workarounds, or this 2012 interview with Carmack:
on windows, OpenGL can only safely draw to a window that was created by the same thread. We created the window on the launch thread, but then did all the rendering on a separate render thread. It would be nice if doing this just failed with a clear error, but instead it works on some systems and randomly fails on others for no apparent reason.
The Doom 4 codebase now jumps through hoops to create the game window from the render thread and pump messages on it, but the better solution, which I have implemented in another project under development, is to leave the rendering on the launch thread, and run the game logic in the spawned thread.
Anyway this whole digression should make it clear that cl-sdl2 is not very high level, and that's sufficient for why I won't just use it directly for a game and would create wrappers if I didn't have lgame, and why even if cl-sdl2 provides a convenient wrapper for something, I might use it, but I also might not and just prefer using the raw underlying FFI stuff. On top of that, again, is that it doesn't fully wrap SDL in Lispy ways, so you'll need to get underneath it to the actual FFI for some things anyway. You have to do this so often, you almost might as well treat cl-sdl2 as just handling the bare minimum of using cl-autowrap yourself on the header files. If it has something higher level you can safely and intuitively use, great!
If there's a corresponding direct way of doing something pygame provides in one or maybe two function calls to something provided by cl-sdl2 or an SDL2 FFI call, then I don't want to wrap it with an lgame namespaced function. Though for convenience lgame does :use all the functions and symbols in the FFI. See the project organization section.
The core viewpoint is that "how can I do X?" should be answerable by looking up how it's done in regular SDL2 or sometimes pygame (many examples available online) and then just translating it directly (including manual memory management!), but possibly searching lgame for existing usage and discovering a convenient wrapper. (For example, a single function call to handle loading an image with sdl2-image into a surface, converting that to a texture, and freeing the surface.)
As I work on my own small game ideas, I may want to generalize and include
something into lgame that has no equivalent in pygame, for example A*
pathfinding. I currently have no precommitments to minimalism. Another more
trivial example is (lgame.loader:get-texture ...)
which 1) keeps a centralized
copy of the SDL_Texture
making it easy to unload (and free) them all later,
and 2) caches them so that a common 'mistake' I've found (and probably made) in
pygame games of loading the images in a sprite object's constructor carries no
penalty. Basically I'm a fan of having program-wide managers or services
("Services Architecture") for various things even inside libraries, that other
objects talk to, subscribe to, or pluck stuff from, instead of having every
program having to create functionally identical management systems for so many
things on their own.
This approach can in the future maybe be used to provide safe finalizers on wrapped objects -- e.g. the thing being wrapped keeps track of what thread created the foreign memory in addition to the foreign memory pointer itself, and so when the GC decides in its thread that it's time to deallocate it, the routine actually sends that request to the responsible thread.
lgame makes use of implicit state, I'm currently undecided if this is a good
idea and might change things later. As one example, to control a game's frame
rate you can call (lgame.time:clock-start)
before the game loop, and
(lgame.time:clock-tick 60)
at the end of each frame. In contrast pygame has
you make a pygame.time.Clock()
object and tick that each frame. My theory is
that most games don't need more than one of these, so lgame can just keep the
state for you. Similar idea with lgame:*screen*
and lgame:*renderer*
which
are exported and intended to be passed to appropriate sdl2 functions if I didn't
bother making a wrapper for them that uses them implicitly.
Currently foreign memory must be manually managed. lgame does manage or provide helpers for managing a few things (and I'll need to better document them) but generally if you do something like making a texture or long-lived SDL_Rect you're responsible for freeing it. I may investigate using finalizers so that things can be garbage collected, or perhaps make it configurable, but that's a longer term plan.
lgame is divided into several packages to mimic (in a non-hierarchical way)
pygame's structure and to hold to a general design principle that each service
should get its own package/namespace. I try to document things in the Lisp code
itself, so if you're comfortable jumping in consult packages.lisp
and then
each lisp file implementing the package you're interested in. Currently I make
extensive use of cl-annot and so generally
speaking you'll need to look file-by-file (or rely on your editor's symbol
auto-completion) to see what symbols are actually exported to each package.
This package manages global shared state among the various lgame modules that
upstream games can also access and make use of. For example, many SDL functions
require an SDL_Renderer argument, this is exposed with
lgame.state:*renderer*
.
This state is use'd and re-exported into the "top-level" lgame package for convenience.
The default top-level package fulfills two purposes. The first is to provide the
entry point function (lgame:init)
that should be called by any game before
using other lgame features, and the exit point function (lgame:quit)
that
should be called when a game closes. (Importantly, this quit will not kill your
Lisp image, so you can restart a game if you need to, it just cleans up any
lgame state.)
The second purpose is to use and re-export lgame.state
's state symbols, and to
use (but not export) all symbols in the sdl2-ffi.functions
and sdl2-ffi
packages (that is, the underlying SDL functions and enums). The exported state
symbols are:
*screen*
-- aka the SDL_Window created for you by(lgame.display:create-window ...)
that wrapsSDL_CreateWindow(...)
.*screen-rect*
-- a convenient SDL_Rect with fields x=0, y=0, width=windowWidth, height=windowHeight. Note that if you uselgame.display:set-logical-size
the*screen-rect*
will be updated to have the logical width and height instead of the actual window's.*renderer*
-- the SDL_Renderer created for you by(lgame.display:create-renderer)
, parented to the window.- ...possible misc singletons -- exported for internal convenience like
*texture-loader*
but shouldn't be interfaced with directly (uselgame.loader
)
These symbols may in the future turn into functions instead, so consider wrapping them in inline getters over using them directly...
The benefit of having the FFI stuff available in this package is that if you read some random C/C++ code and see some functions you'd like to call, it's easy. e.g.:
SDL_Texture texture = SDL_CreateTextureFromSurface(main_renderer, source_surf);
turns to:
(setf texture (lgame::sdl-create-texture-from-surface lgame:*renderer* source-surface))
You can of course explicitly use sdl2-ffi.functions:sdl-create-texture-from-surface
, the symbols refer to the same thing.
Or even use sdl2:create-texture-from-surface
, which wraps the sdl- function
but includes a null pointer check.
Similarly using things like either lgame::+sdl-windowpos-centered+
vs. sdl2-ffi:+sdl-windowpos-centered+
is up to you.
Exports functions for setting up your game window.
lgame.display:create-window
-- creates an SDL_Window and binds it tolgame:*screen*
, also returns it if you need a local copy.lgame.display:create-centered-window
-- convenience version for having the window centered on the desktop.lgame.display:create-renderer
-- creates an SDL_Renderer and binds it tolgame:*renderer*
, also returns it if you need a local copy.lgame.display:set-logical-size
-- can make use of SDL2's logical screen size feature. If you wanted to make a retro 640x480 game, you might set the window size to that and go into full screen and hope your monitor has a mode for that (with black bars if the aspect ratio isn't the same). With SDL2, you don't have to do that anymore. Now you can make your window size something like 1920x1080, but set the logical size to 640x480. Your render code stays the same as if you had a small window (drawing in a 640x480 zone) but SDL will automatically upscale things for you and add black bars (or whatever bg you want) to preserve the aspect ratio. If you go into fullscreen, the monitor's display mode also doesn't need to change.lgame.display:screenshot-png
-- lets you save a screenshot of your rendered frame. Note that it uses the rendered display size, not the logical display size, and a 4k image will take quite some time since it's doing a slow pixel copy.window-pixel-format
-- default window pixel format, useful when creating your own textures
An example usage:
(lgame:init)
(lgame.display:create-centered-window "Hello" 1920 1080)
(lgame.display:create-renderer)
; game loop here, you're setup with a 1920x1080 screen ready to render stuff on
(lgame:quit)
Utils and wrappers around SDL2 events, particularly handling the event loop
somewhat nice and intuitively with a lgame.event:do-event
macro and to reference such
event data with ref
or a specialized function on top of ref
.
Here is some more explanation in the context of code:
;; Example do-event usage for a few event types and properties:
(lgame.event:do-event (event)
(when (or
(= (lgame.event:event-type event) lgame::+sdl-quit+)
(and (= (lgame.event:event-type event) lgame::+sdl-keydown+)
(= (lgame.event:key-scancode event) lgame::+sdl-scancode-escape+)))
; quit
(setf *running?* nil))
(when (= (lgame.event:event-type event) lgame::+sdl-mousebuttondown+)
; ...
)
(when (= (lgame.event:event-type event) lgame::+sdl-mousebuttonup+)
; ...
)
(when (and (= (lgame.event:event-type event) lgame::+sdl-keyup+)
(= (lgame.event:key-scancode event) lgame::+sdl-scancode-f+))
; f to toggle full screen without switching the monitor's display mode
(if *full-screen*
(progn (lgame::sdl-set-window-fullscreen lgame:*screen* 0) (setf *full-screen* nil))
(progn (lgame::sdl-set-window-fullscreen lgame:*screen* lgame::+sdl-window-fullscreen-desktop+) (setf *full-screen* t)))))
Support functions are lgame.event:event-type
and lgame.event:key-scancode
. They both build on a
general ref
macro. Taken from event.lisp
:
(defmacro ref (event &rest fields)
`(plus-c:c-ref ,event sdl2-ffi:sdl-event ,@fields))
(defun event-type (event)
(ref event :type))
(defun key-scancode (event)
(ref event :key :keysym :scancode))
plus-c:c-ref
lets you poke at nested struct fields. So you could write
key-scancode
as (plus-c:c-ref event sdl2-ffi:sdl-event :key :keysym :scancode)
which is basically equivalent to C's event.key.keysym.scancode
.
Knowing what fields exist requires looking at the SDL Documentation and knowing your type. For key codes, you start with the SDL_Event page and descend to the SDL_KeyboardEvent then the SDL_Keysym and lastly the SDL_Scancode (an enum).
do-event
's definition just loops over sdl-poll-event
:
(defmacro do-event ((event) &body loop-body)
"Helper macro to iterate through SDL's event list until it is empty,
binding each SDL_Event to event."
`(with-event (,event)
(loop until (zerop (sdl2-ffi.functions:sdl-poll-event ,event))
do
,@loop-body)))
All this I think is easier to understand than the sdl2:with-event-loop
macro.
Of important note here is my with-event
macro. It binds foreign SDL_Event
structs (and similar macros bind other things like SDL_Rect) in a more
efficient way than using cl-sdl2's macros. This is done by using cffi directly
with a constantly known type size, enabling allocation on the stack. See the
macro doc:
(defmacro with-event ((event) &body body)
"Helper macro to enable sdl event allocation on the stack.
Verify with macroexpand this:
(with-event (event)
(print event))
against:
(sdl2:with-sdl-event (event)
(print event))
or even:
(plus-c:c-let ((event sdl2-ffi:sdl-event :free t))
(print event))
"
(let ((size (autowrap:foreign-type-size (autowrap:find-type 'sdl2-ffi:sdl-event))))
`(cffi:with-foreign-pointer (,event ,size)
,@body)))
lgame.mouse:get-mouse-pos
-- simple wrapper to get the mouse position as an (x y) list.
Some functions to handle time-related tasks, notably enforcing a max FPS framerate.
Example usage:
(lgame.time:clock-start)
;; now start the game loop
(loop while (lgame.time:clock-running?) do
(lgame.event:do-event (event)
(when should-quit? (lgame.time:clock-stop))
...)
; render...
(lgame.time:clock-tick 60))
Note some examples still make use of their own *running?*
variable. Do what
you like.
clock-tick
should be called at the end of the frame, because it takes the duration of
the frame into account for how long it should sleep for. Sleeping is done with lgame::sdl-delay
.
If you don't pass anything to clock-tick
, the framerate will not be capped. In
any case, you can also use the return value of clock-tick
which is the time in
milliseconds for the frame to complete (independent of whether sleeping
occurred).
Unfortunately since the clock resolution is only in milliseconds, this isn't really a good value to use as a "dt" variable to pass down to update functions for frame-rate-independent physics. You should read Gaffer On Games - Fix Your Timestep! anyway.
If you intentionally delay a frame with (sleep (/ 1 30))
, or dropped to around
30 FPS due to an intensive scene, you would at least see a useful number like 33
or 34. Keeping statistics of this number can help you notice when you don't have
consistent frame times (e.g. a garbage collection might have kicked off).
Package for working with SDL_Rects. You can create them, update them, and do
useful things like checking for collisions or scaling. There's also a handy
rect-coord
function to query or set things about the rect without having to
touch the underlying x,y,w,h fields for everything. e.g.:
(lgame.rect:with-rect (r 0 0 5 5)
; 5x5 rect at x,y coordinate 0,0
(lgame.rect:rect-coord r :center) ; query its center position, i.e. (2,2)
(lgame.rect:rect-coord r :topright) ; query its top-right position, i.e. (5,0)
(setf (lgame.rect:rect-coord r :topright) '(20 40)) ; moves top-right position to coord (20,40)
(lgame.rect:rect-coord r :center) ; now rect center is at (17,42). Its left axis is at 15, its right at 20, i.e. it's still a 5x5 rect.
(lgame.rect:outside-screen? r) ; sees if this rect lgame.rect:collide-rect? with lgame.state:*screen-rect*
)
Note that since SDL Rects must use integers for their values, setters
may automatically truncate
their inputs (the same as casting to int in C),
and destructive moves use round
.
Similar set of classes and mixins to work with game sprites compared to
pygame.sprite. Check out
chimp.lisp
for a simple example or aliens.lisp
for a bigger example. The
main idea is that you give each sprite its own class inheriting from the
appropriate lgame.sprite base class, set the sprite's image and rect slots in a
constructor, and implement the specializing method update
. If you need
something more complicated than "blitting" the sprite's image slot to the
location specified by its rect slot, you can also implement the method draw
.
You should write your game loop to call update/draw for all sprites every frame.
There's also a group
class to create sprite groups and thus only have to call
(from the main loop) update/draw on the group itself.
For now, lgame just provides a way to load and store TTF files
(it also includes a default liberally licensed font if you
don't want to download one or specify a system font) and a single
lgame.font:render-text
function that functionally does the same as sdl2-ttf:render-text-solid
but without using finalizers. Other ttf functions are exposed in the
lgame-sdl2-ttf.ffi
package.
Thus you will need to clean up the font texture yourself after you're done with
it. See chimp.lisp
for a static example which basically goes like this:
; pre-game loop setup
(let* ((font (lgame.font:load-font (lgame.font:get-default-font) 20))
(banner-txt (lgame.font:render-text font "Your message here" 10 10 10))
(banner-txt-rect (lgame.rect:get-texture-rect banner-txt)))
; ...
(unwind-protect (game-loop)
(sdl2:free-rect banner-txt-rect)
(sdl2:destroy-texture banner-txt)
(lgame:quit)))
; inside game loop:
(lgame.render:blit banner-txt banner-txt-rect)
;; note: could also write:
;(sdl2:render-copy lgame:*renderer* banner-txt :dest-rect banner-txt-rect)
;; or:
;;(lgame::sdl-render-copy lgame:*renderer* banner-txt nil banner-txt-rect)
If you want to rely on system fonts, lgame uses
font-discovery and so can accept
font family names via load-font
or explicitly get paths via find-font-path
.
Some wrappers like the just shown blit
that let you avoid having to specify
the *renderer*
all the time.
lgame.render:blit
- wraps sdl-render-copy for a texture and only dest-rectlgame.render:clear
- wraps sdl-render-clearlgame.render:present
- wraps sdl-render-presentlgame.render:set-draw-color
- wraps sdl-set-render-draw-color, allows passing in a 3 or 4 length rgb/rgba list, or providing the rgba arguments explicitly.lgame.render:with-draw-color*
- macro to temporarily set and then restore a render draw colorlgame.render:with-render-target*
- macro to temporarily set and then restore a render target to something besides the whole screen texture
Additional functions building off of sdl2's draw/render primitives. Currently
render-fill-circle
and render-rounded-filled-rect
.
Lets you use a singleton to load and cache textures, with them automatically
being destroyed on lgame:quit
. Example:
; in setup, after (lgame:init)
(lgame.loader:create-texture-loader (directory-namestring *load-truename*))
; later on in game:
(let ((image (lgame.loader:get-texture :ball)))
(setf (lgame.sprite:.image self) image)
(setf (lgame.sprite:.rect self) (lgame.rect:get-texture-rect image)))
That will try to load "ball.png" from the directory that the lisp file resides
in. If get-texture
is called with the same data as before, it will return the
texture from cache.
get-texture
maps keyword names to lowercase png files, but you can also give
it an explicit filename string like "my ball.jpg" instead. If you need to use a
different context directory than the one specified at creation time, you can
pass in a :dir
keyword argument. If you need to set a color-key for alpha
transparency, you can pass in a :color-key
argument as a list of 0-255 (r g b).
Incomplete port of an old A* implementation. Check out subdirectories of examples/maze/ for usage until I get around with re-creating a playground and adding in all the extra functionality the original supported. I also want to benchmark it for speed and accuracy against some game maps before I declare it reliable (especially because it's using an odd extra map array for data that should probably be stored with the primary map array cells instead).
Utility functions that don't fit in other packages, and may indeed be better split off into separate libraries. Currently just a function for printing licensing information that may be useful when shipping a game binary.
There is no lgame.sound, but that may change. For now, using sdl2-mixer directly as some examples do is easy enough.
The two GL examples show that using OpenGL is a lot of work, even when using SDL to do the initial window setup. If I start experimenting with more 3D games and using raw GL calls, some useful things may end up in something like a lgame.gl package.
If lgame ever does become a true 'kitchen sink' it might become better to think of it as an engine, but I'd rather at that point just split things off into an engine.