User Manual

Contents

Overview

trivial-gamekit is a very simple toolkit for making games in Common Lisp. Its goal is to be as simple as possible while allowing a user to make a complete game s/he can distribute.

It manages system resources for you by creating and initializing a window, abstracting away keyboard and mouse input and any other system-dependent configuration. trivial-gamekit also spawns a default game loop for you allowing to hook into it for drawing and other per frame activities via certain methods described below.

It strives to fully support Common Lisp’s interactive development approach where you change your application on the fly and allowed to gracefully recover from programming errors via restarts without crashing or rerunning an application.

Defining a game

gamekit’s central idea is a game object you define via defgame macro. It contains all the state gamekit needs to run your game. It is instantiated by #'start which also bootstraps the whole application and opens a window, runs your game logic, rendering, etc. Only single game object can be run at a time, so subsequently calling #'start for the same or different game objects is disallowed - you need to stop game execution first. To stop a running game you would need to call #'stop.

You can put game initialization code into #'post-initialize. Specifically, it is probably a best place to bind input via #'bind-cursor or #'bind-button as described below, but you are free to do any other preparations that require fully initialized game instance. If you need to release any unmanaged resources (e.g. foreign memory you allocated during a game or in #'post-initialize) to avoid leaks you can override #'pre-destroy function which is called after last game loop iteration.

#'start also invokes a default game loop. To hook into it you can use #'act and #'draw methods. #'act is used for any activity you need to do per frame except drawing where #'draw should be used exclusively for drawing/rendering.

Math

For moving objects around, defining their place on a canvas and giving them a color we need a bit of a math applied. gamekit points and positions are represented as two-dimensional vectors created with #'vec2. Colors consist of four elements: red, green, blue and alpha, so they are represented by four-dimensional vecors created via #'vec4.

Some vector operations are exported: #'mult for element-wise multiplication, #'add for addition, #'subt for subtraction and #'div for element-wise division.

Element accessor methods for setting or getting values out of vectors are also exported: #'x to access first element, #'y for a second, #'z - third and #'w to access fourth.

Locating resources

Resources are very important for games. Textures, models, sounds, fonts, animations, tiles - you name it. gamekit has several routines prepared for you to ease the handling of those.

First of all, we need to tell gamekit where to find resources. gamekit resources are associated with Common Lisp packages. Any package can have associated filesystem directory to find resources in. Call #'register-resource-package as a toplevel form to bind a directory to a package. Absolute paths required here. Below I’ll try to explain why we need this package-directory association.

Before diving deeper, lets see how resources are defined. You tell gamekit what resources to load and use via #'define-image, #'define-sound or #'define-font macros. First argument to them is a symbol that would become an identificator for the resource and the second argument is a relative path to the resource.

Now, when gamekit knows absolute path to root and relative paths to resouces, it can locate and prepare them to use in a game. To find absolute path to a certain resource gamekit looks into its id and extracts the package (resource identificators must be symbols), then searches for registered package-directory association and extracts absolute path for the package from there and finally gamekit combines absolute path of a package with resource’s relative path and can locate it exactly.

This seemingly confusing mechanism was implemented for several games to coexist in one lisp image. Unless you use keywords or common packages like :cl-user, resources are going to be uniquely identified and each game will load its own resources correctly. Another usage scenario is an asdf system with just a resources and their definitions for sharing between fellow game developers.

Resources are used by functions that requires them to operate, like #'draw-image, #'make-font, #'play-sound and others.

When defgame option :prepare-resources is set to nil, you would need to request resources manually by calling #'prepare-resources with resource ids you need. Preparation can take a while, so #'prepare-resources asynchonously requests resources and returns immediately for you to be notified later with #'notice-resources. This is useful, when you don’t want to wait until all assets are loaded and start rendering some loading or start screen right away and continue with game after all resources are ready.

Drawing

gamekit provides simple to use but versatile drawing API. If you know how HTML5 canvas API is organized you will find gamekit’s one quite similar. Several functions with names starting with draw- exists to help you draw primitives like lines, rectangles, ellipses, arcs, polygons or even bezier curves. Images can be drawn via #'draw-image and text with customizable font can be put onto canvas via #'draw-text.

Canvas transformation operations are supported too. You can scale, translate, rotate a canvas with #'scale-canvas, #'translate-canvas and #'rotate-canvas accordingly. If you need to keep canvas state for nested drawing operations you will appreciate existence of #'with-pushed-canvas macro, that keeps canvas state to the dynamic extent of its body. This means that upon returning from the macro canvas transformation state will be returned to its original state before the macro and all transformation operations inside its body would be canceled out.

All drawing operations should be performed inside #'draw.

Unlike many other drawing APIs gamekit uses bottom left corner as an origin (0,0) with y-axis pointing upwards which is more convenient mathematically.

All origins or positions are represented by two-dimensional (x,y) vectors created via #'vec2. Colors are passed around as four-dimensional vectors made with #'vec4 consisting of red, green, blue and alpha channels each within 0.0-1.0 range.

Playing an audio

Audio can substantially boost game’s atmosphere, and gamekit is ready to serve you well in this regard too. #'play-sound will help with getting sounds to reach your users ears. On the other hand, #'stop-sound can be used to stop this process.

Listening to input

There’s no game without some sort of interaction with a player. gamekit allows you to grab keyboard and mouse input to listen for player actions. You can pass a callback to #'bind-button which will be called upon pressing or releasing keyboard/mouse buttons. Callback passed to #'bind-cursor is going to be invoked when user moves a mouse. Callbacks provided are not stacked together, meaning if you try to bind multiple callbacks to the same button only last callback is actually going to be invoked. Same goes for cursor input.

Building a distributable

Sharing a Common Lisp game in a binary form amongst users was always a bit of a pain. But fear no more! gamekit includes a mechanism for delivering your game packaged using only a single function - #'deliver. It will build an executable, pack all required resources, copy needed foreign libraries used by trivial-gamekit and compress all that into a shippable archive.

To gain access to this function you need additionally load :trivial-gamekit/distribution system, and then you would be able to find it in gamekit.distribution package.

(ql:quickload :trivial-gamekit/distribution)

For building these packages for Windows, GNU/Linux and macOS with just a single push to a github or gitlab respository check out Advanced Features section.

Symbol Index

macro defgame (name (&rest classes) &body ((&rest slots) &rest opts))

Defines a game class that can be passed to #'start to run a game. name is the name of a class generated. classes are names of superclasses, slots - standard class slots and opts are class options. So, pretty much standard class definition except it does configure a class in certain ways specifically for gamekit use and allows passing additional options in opts apart from standard :documentation, :default-initargs and so others.

Additional options that can be passed in opts are:

  • :viewport-width - width of the window and canvas
  • :viewport-height - height of the window and canvas
  • :viewport-title - title of the window
  • :prepare-resources - boolean value that indicates whether gamekit should load resources automatically on startup or if not, user prefers to load them dynamically on request. Defaults to t.

Example:

(gamekit:defgame example ()
  ;; some game related state
  ((world :initform (make-instance 'world))
   (game-state))
  ;; options
  (:viewport-width 800)
  (:viewport-height 600)
  (:viewport-title "EXAMPLE")
  (:prepare-resources nil))

function start (classname &key (log-level info) (opengl-version '(3 3)) blocking)

Bootsraps a game allocating a window and other system resources. Instantiates game object defined with defgame which can be obtained via #'gamekit. Cannot be called twice - #'stop should be called first before running start again.

Example:

(gamekit:start 'example)

function stop ()

Stops currently running game releasing acquired resources.

Example:

(gamekit:stop)

function gamekit ()

Returns instance of a running game or nil if no game is started yet.

Example:

(gamekit:gamekit)

generic post-initialize (system)

This function is called after game instance is fully initialized, right before main game loop starts its execution. Put initialization code for your application into method of this function. For example, it would be logical to bind input via #'bind-cursor or #'bind-button here.

Example:

(defmethod gamekit:post-initialize ((this example))
  (init-game)
  (bind-input))

generic pre-destroy (system)

This function is called just before shutting down a game instance for you to free all acquired resources and do any other clean up procedures.

Example:

(defmethod gamekit:pre-destroy ((this example))
  (release-foreign-memory)
  (stop-threads))

generic act (system)

Called every game loop iteration for user to add any per-frame behavior to the game. NOTE: all drawing operations should be performed in #'draw method.

Example:

(defmethod gamekit:act ((this example))
  (report-fps))

generic draw (system)

Called every game loop iteration for frame rendering. All drawing operations should be performed in this method.

Example:

(defmethod gamekit:draw ((this example))
  (gamekit:draw-text "Hello, Gamedev!" (gamekit:vec2 10 10)))

function vec2 (&optional (x 0.0) (y 0.0))

Makes a two-dimensional vector.

Example:

(gamekit:vec2 0 0)

function vec3 (&optional (x 0.0) (y 0.0) (z 0.0))

Makes a three-dimensional vector.

Example:

(gamekit:vec3 1 1 2)

function vec4 (&optional (x 0.0) (y 0.0) (z 0.0) (w 0.0))

Makes a four-dimensional vector.

Example:

(gamekit:vec4 1 1 2 3)

function mult (arg0 &rest args)

Element-wise multiplication. Accepts both vectors and scalars.

Example:

(gamekit:mult 2 (gamekit:vec2 1 1) 0.5)

function add (arg0 &rest args)

Element-wise addition. Accepts both vectors and scalars.

Example:

(gamekit:add 1 (gamekit:vec2 1 1) -1)

function subt (arg0 &rest args)

Element-wise subtraction. Accepts both vectors and scalars.

Example:

(gamekit:subt 1 (gamekit:vec2 1 1) (gamekit:vec2 -1 -1))

function div (arg0 &rest args)

Element-wise division. Accepts both vectors and scalars.

Example:

(gamekit:div (gamekit:vec2 1 1) 2 (gamekit:vec2 0.5 0.5))

function x (vec)

Reads first element of a vector.

Example:

(gamekit:x (gamekit:vec2 1 1))

function (setf x) (value vec)

Stores first element of a vector.

Example:

(setf (gamekit:x (gamekit:vec2 1 1)) 0)

function y (vec)

Reads second element of a vector.

Example:

(gamekit:y (gamekit:vec2 1 1))

function (setf y) (value vec)

Stores second element of a vector.

Example:

(setf (gamekit:y (gamekit:vec2 1 1)) 0)

function z (vec)

Reads third element of a vector.

Example:

(gamekit:z (gamekit:vec4 1 1 2 3))

function (setf z) (value vec)

Stores third element of a vector.

Example:

(setf (gamekit:z (gamekit:vec4 1 1 2 3)) 0)

function w (vec)

Reads fourth element of a vector.

Example:

(gamekit:w (gamekit:vec4 1 1 2 3))

function (setf w) (value vec)

Stores fourth element of a vector.

Example:

(setf (gamekit:w (gamekit:vec4 1 1 2 3)) 0)

function register-resource-package (package-name path)

Associates resource package with filesystem path. For proper resource handling it is recommended to put it as a top-level form, so resources could be located at load-time.

First argument, a package name, must be a valid Common Lisp package name that could be used to locate package via #’find-package. Second argument is a filesystem path to a directory where resources can be found.

Example:

(gamekit:register-resource-package :example-package
                                   "/home/gamdev/example-game/assets/")

macro define-image (name path)

Registers image resource by name that can be used by #'draw-image later. Second argument is a valid path to the resource. Only .png images are supported at this moment.

Name must be a symbol. Package of that symbol and its associated path (via #'register-resource-package) will be used to locate the resource, if relative path is given as an argument to this macro.

Example:

(gamekit:define-image 'example-package::logo "images/logo.png")

macro define-sound (name path)

Registers sound resource by name that can be used by #'play-sound later. Second argument is a valid path to the resource. Formats supported: .wav, .ogg (Vorbis), .flac, .aiff.

Name must be a symbol. Package of that symbol and its associated path (via #'register-resource-package) will be used to locate the resource, if relative path is given as an argument to this macro.

Example:

(gamekit:define-sound 'example-package::blop "sounds/blop.ogg")

macro define-font (name path)

Registers font resource by name that can be passed to #'make-font later. Second argument is a valid path to the resource. Only .ttf format is supported at this moment.

Name must be a symbol. Package of that symbol and its associated path (via #'register-resource-package) will be used to locate the resource, if relative path is given as an argument to this macro.

Example:

(gamekit:define-font 'example-package::noto-sans "fonts/NotoSans-Regular.ttf")

function make-font (font-id size)

Makes a font instance that can be later passed to #'draw-text to customize text looks. font-id must be a valid resource name previously registered with define-font. Second argument is a font size in pixels.

Example:

(gamekit:make-font 'example-package::noto-sans 32)

function prepare-resources (&rest resource-names)

Loads and prepares resources for later usage asynchronously. resource-names should be symbols used previously registered with define-* macros.

This function returns immediately. When resources are ready for use #'notice-resources will be called with names that were passed to this function.

gamekit by default will try to load and prepare all registered resources on startup which might take a substantial time, but then you don’t need to call #’prepare-resources yourself. If you prefer load resources on demand and have a faster startup time, pass nil to :prepare-resources option of a defgame macro which will disable startup resource autoloading.

Example:

(gamekit:prepare-resources 'example-package::noto-sans
                           'example-package::blop
                           'example-package::logo)

generic notice-resources (game &rest resource-names)

Called when resource names earlier requested with #'prepare-resources which indicates those resources are ready to be used.

Override this generic function to know when resources are ready.

Example:

(defmethod gamekit:notice-resources ((this example) &rest resource-names)
  (declare (ignore resource-names))
  (gamekit:play-sound 'example-package::blop)
  (show-start-screen))

function draw-line (origin end paint &key (thickness 1.0) (canvas *canvas*))

Draws a line starting from coordinates passed as first argument to coordinates in second parameter. Third parameter is a color to draw a line with. :thickness is a scalar floating point value controlling pixel-width of a line.

Example:

(gamekit:draw-line (gamekit:vec2 8 5) (gamekit:vec2 32 11)
                   (gamekit:vec4 1 0.5 0 1)
                   :thickness 1.5)

function draw-curve (origin end ctrl0 ctrl1 paint &key (thickness 1.0) (canvas *canvas*))

Draws a bezier curve from coordinates passed as first argument to coordinates in second parameter with two control points in third and fourth parameters accordingly. Fifth argument is a curve’s color. :thickness is a scalar floating point value controlling pixel-width of a curve.

Example:

(gamekit:draw-line (gamekit:vec2 8 5) (gamekit:vec2 32 11)
                   (gamekit:vec2 0 5) (gamekit:vec2 32 0)
                   (gamekit:vec4 1 0.5 0 1)
                   :thickness 1.5)

function draw-rect (origin w h &key (fill-paint nil) (stroke-paint nil) (thickness 1.0) (rounding 0.0) (canvas *canvas*))

Draws a rectangle with origin passed in first argument, width and height - second and third arguments accordingly. :fill-paint key is a color to fill insides of a rectangle with. If you pass color to :stroke-paint, edges of the rectangle are going to be struck with it. :thickness controls pixel width of struck edges. Use :rounding in pixels to round rectangle corners.

Example:

(gamekit:draw-rect (gamekit:vec2 0 0) 314 271
                   :fill-paint (gamekit:vec4 1 0.75 0.5 0.5)
                   :stroke-paint (gamekit:vec4 0 0 0 1)
                   :rounding 5.0)

function draw-circle (center radius &key (fill-paint nil) (stroke-paint nil) (thickness 1.0) (canvas *canvas*))

Draws a circle with center in first argument and radius in second argument. Provide color with :fill-paint paramater to fill the inner area of the circle with. If :stroke-paint color is provided, circle’s border is going to be struck with it. :thickness controls pixel width of struck border.

Example:

(gamekit:draw-circle (gamekit:vec2 100 500) 3/4
                     :fill-paint (gamekit:vec4 1 1 1 1)
                     :stroke-paint (gamekit:vec4 0 0 0 1)
                     :thickness 3)

function draw-ellipse (center x-radius y-radius &key (fill-paint nil) (stroke-paint nil) (thickness 1.0) (canvas *canvas*))

Draws an ellipse with center provided in first argument, x and y radii as second and thrid arguments accordingly. Pass a color as :fill-paint paramater to fill the inner area of the ellipse with. If :stroke-paint color is provided, ellipse’s border will be struck with it. :thickness controls pixel width of struck border.

Example:

(gamekit:draw-ellipse (gamekit:vec2 128 128) 16 32
                      :fill-paint (gamekit:vec4 0 0 0 1)
                      :stroke-paint (gamekit:vec4 1 1 1 1)
                      :thickness 1.1)

function draw-arc (center radius a0 a1 &key (fill-paint nil) (stroke-paint nil) (thickness 1.0) (canvas *canvas*))

Draws an arc from a0 to a1 angles (in radians) with center passed in first argument and radius in second. If provided, color in :fill-paint will be used to fill the area under an arc confined between a circle’s curve and a line connecting angle points. :fill-paint and :stroke-paint colors are, if provided, used to fill insides and stroke arc’s border correspondingly.

Example:

(gamekit:draw-arc (gamekit:vec2 256 256) 128
                  (/ pi 4) (* (/ pi 2) 1.5)
                  :fill-paint (gamekit:vec4 0.25 0.5 0.75 1)
                  :stroke-paint (gamekit:vec4 0.75 0.5 0.25 1)
                  :thickness 2.0)

function draw-polygon (vertices &key (fill-paint nil) (stroke-paint nil) (thickness 1.0) (canvas *canvas*))

Draws a polygon connecting list of vertices provided in first argument. :fill-paint is a color to fill insides of a polygon and :stroke-paint color is used to stroke polygon edges. :thickness controls pixel-width of a stroke.

Example:

(gamekit:draw-polygon (list (gamekit:vec2 10 10) (gamekit:vec2 20 20)
                            (gamekit:vec2 30 20) (gamekit:vec2 20 10))
                      :fill-paint (gamekit:vec4 0.25 0.5 0.75 1)
                      :stroke-paint (gamekit:vec4 0.75 0.5 0.25 1)
                      :thickness 3.0)

function draw-polyline (points paint &key (thickness 1.0) (canvas *canvas*))

Draws a polyline connecting list of vertices provided in first argument. Second argument is a color to stroke a line with. :thickness controls pixel width of a line.

Example:

(gamekit:draw-polyline (list (gamekit:vec2 10 10) (gamekit:vec2 20 20)
                             (gamekit:vec2 30 20) (gamekit:vec2 20 10))
                       (gamekit:vec4 0.75 0.5 0.25 1)
                       :thickness 3.0)

function draw-image (position image-id &key origin width height)

Draws an image at coordinates specified in first argument. Second argument is image-id used in #'define-image earlier. Optional :origin key is a point within image to start drawing from, if you want to render only a part of image. :width and :height keys tell width and height of a subimage to draw. They are optional and could be skipped to draw a subimage with full height and width available.

Example:

(gamekit:draw-image (gamekit:vec2 314 271) 'example-package::logo
                    :origin (gamekit:vec2 0 0)
                    :width 320
                    :height 240)

function draw-text (string origin &key (fill-color *black*) (font *font*))

Draws text on the canvas starting at coordinates passed as second argument. Use :fill-color key parameter to change text’s color. To change a font, pass object created with #'make-font via :font parameter.

Example:

(gamekit:draw-text "Hello, Gamekit!" (gamekit:vec2 11 23)
                   :fill-color (gamekit:vec4 0 0 0 1)
                   :font (gamekit:make-font 'example-package::noto-sans 32))

function translate-canvas (x y &key (canvas *canvas*))

Moves drawing origin to the specified position making the latter a new origin. All following draw operations will be affected by this change unless wrapped with with-pushed-canvas macro.

Example:

(gamekit:translate-canvas 100 500)

function rotate-canvas (angle &key (canvas *canvas*))

Rotates current canvas for specified number of radians. All following drawing operations will be affected by this change unless wrapped with with-pushed-canvas macro.

Example:

(gamekit:rotate-canvas (/ pi 2))

function scale-canvas (x y &key (canvas *canvas*))

Scales current canvas by x and y axes accordingly. All following drawing operations will be affected by this change unless wrapped with with-pushed-canvas macro.

Example:

(gamekit:scale-canvas 0.5 1.5)

macro with-pushed-canvas ((&optional canvas) &body body)

Saves current canvas transformations (translations, rotations, scales) before entering its body and restores upon exist from the body. All transformation operations inside this macro don’t affect outer canvas outside of a body of this macro.

Example:

(gamekit:translate-canvas 400 300)
(gamekit:with-pushed-canvas ()
  (gamekit:rotate-canvas (/ pi 4)))

function play-sound (sound-id &key looped-p)

Plays a sound defined earlier with define-sound. Pass t to :looped-p key to play sound in a loop.

Example:

(gamekit:play-sound 'example-package::blop
                    :looped-p t)

function stop-sound (sound-id)

Stops a playing sound by provided sound id.

Example:

(gamekit:stop-sound 'example-package::blop)

function bind-button (key state action)

Binds action to specified key state. When key state changes to the one specified, action callback is invoked with no arguments. #'bind-button function should be called when there’s active game exists started earlier with #'start. state can be either :pressed, :released or :repeating.

Actions are not stacked together and would be overwritten for the same key and state.

Supported keys:

  :space :apostrophe :comma :minus :period :slash
  :0 :1 :2 :3 :4 :5 :6 :7 :8 :9
  :semicolon :equal
  :a :b :c :d :e :f :g :h :i :j :k :l :m
  :n :o :p :q :r :s :t :u :v :w :x :y :z
  :left-bracket :backslash :right-bracket
  :grave-accent :world-1 :world-2
  :escape :enter :tab :backspace :insert :delete
  :right :left :down :up
  :page-up :page-down :home :end
  :caps-lock :scroll-lock :num-lock :print-screen :pause
  :f1 :f2 :f3 :f4 :f5 :f6 :f7 :f8 :f9 :f10 :f11 :f12
  :f13 :f14 :f15 :f16 :f17 :f18 :f19 :f20 :f21 :f22 :f23 :f24 :f25
  :keypad-0 :keypad-1 :keypad-2 :keypad-3 :keypad-4
  :keypad-5 :keypad-6 :keypad-7 :keypad-8 :keypad-9
  :keypad-decimal :keypad-divide :keypad-multiply
  :keypad-subtract :keypad-add :keypad-enter :keypad-equal
  :left-shift :left-control :left-alt :left-super
  :right-shift :right-control :right-alt :right-super
  :menu

  :mouse-left :mouse-right :mouse-middle

Example

(gamekit:bind-button :enter :pressed
                     (lambda ()
                       (start-game-for *player*)))

function bind-cursor (action)

Binds action callback to a cursor movement event. Everytime user moves a cursor callback will be called with x and y of cursor coordinates within the same coordinate system canvas is defined in: bottom left corner as (0,0) origin and y-axis pointing upwards.

Actions doesn’t stack together and would be overwritten each time #'bind-cursor is called.

Example:

(gamekit:bind-cursor (lambda (x y)
                       (shoot-to x y)))

function deliver (system-name game-class &key build-directory (zip *zip*) (lisp *lisp*))

Builds an executable, serializes resources and packs required foreign libraries into a .zip archive for distribution. system-name is a name of asdf system of your application and game-class is a game class defined with defgame (the one that could be passed to #'start to start your game). By default, it builds all artifacts into build/ directory relative to system-name system path, but you can pass any other path to :build-directory key argument to put target files into it instead.

This routine uses zip and lisp (‘sbcl’ Steel Bank Common Lisp is the default) to build a distributable package on various platforms. If those executables are not on your system’s PATH, you would need to provide absolute paths to them via :zip and :lisp key arguments accordingly.

You can load this function into an image via :trivial-gamekit/distribution system.

Example:

(ql:quickload :trivial-gamekit/distribution)
(gamekit.distribution:deliver :example-asdf-system 'example-package::example
                              :build-directory "/tmp/example-game/"
                              :zip "/usr/bin/zip"
                              :lisp "/usr/bin/sbcl")