Dave's guide to Common Lisp game development

Table of Contents

Overview

Use the N and P keys to flip to the Next and Previous pages, or click the links in the header. Press B to go Back or "?" for help. This document can also be browsed as a single large web page without Javascript.

Welcome to my work-in-progress Common Lisp game development guide. In this document I use my free Lisp game engine Xelf to develop several simple 2-D games and show how each part works, in order to help you make your own games and share them with friends. In various chapters we will discuss using sprites, simple physics, and simple AI techniques supported by tools from Xelf. General principles of game design are discussed, and a concrete example is given in the form of a completely source-documented multiplayer Lisp game called Squareball. Procedural content generation is also discussed.

Knowledge from this guide can be applied directly to developing with Xelf, or can be transposed by the reader for other Lisp game development tools such as CL-SDL2, Cepl, Clinch, or Sketch. Links to such tools are provided, and we also give pointers to our friends in the Scheme world.

Procedures for building distributable game apps for Windows, Mac, and Linux will be discussed. A future version of this guide will also cover 3-D game development and building Android apps.

About this document

Teaching Common Lisp itself is outside the scope of this document. The wikipedia page for Common Lisp has a reasonable capsule explanation of the syntax, with links to further resources. There's also Zach Beane's "Where to get help with Common Lisp".

I also do not cover specifics of OpenGL programming in Lisp. An excellent resource for this is Bart Botta's OpenGL tutorial series.

The example games use Xelf, but attempts will be made to explain what is going on underneath so that readers will understand the techniques being used at a high level. For those who wish to delve deeper, source code links are provided in each case. Please see also the Xelf documentation reference and the Xelf documented program source code page.

Handling Lisp in your text editor

If you visit the wikipedia page for Common Lisp and looked at the code examples, you probably noticed all the parentheses. The heavy use of parentheses marks Lisp off from other languages and makes it feel unfamiliar. But once you are able to read and write the basics, it all falls together naturally. The unusual syntax of Lisp is actually the key to many of its coolest features.

Matching up parentheses and indenting things properly is best handled automatically by a text editor with support for editing Lisp code, such as Vim, which can be used with Slimv — the Superior Lisp Interaction Mode for Vim. I myself use the GNU Emacs text editor along with Slime.

Using one of these options—Emacs with Slime, or Vim with Slimv—is probably the best way to develop and test Lisp code made with Xelf. With Slime or Slimv you can edit Lisp code efficiently, send commands to the underlying Lisp environment that Xelf is running in, or redefine methods and functions in order to alter object behaviors while the system is running.

Both Emacs and Vim are highly customizable development environments, not just text editors; in fact, I have developed and tested Xelf (all ~10,000 lines of code) entirely with GNU Emacs as my IDE.

Furthermore, Emacs and Vim are Free software, will run on basically any platform, are of very high quality, and have large, friendly user communities.

That being said, you can edit Lisp code in basically any text editor, and it's quite possible that the text editor you already use has a plugin or script available for editing Lisp code and matching those parentheses. If you're unsure about Vim and Emacs, try looking around to see if you can find Lisp support for your existing editor.

The instructions below assume Slime is being used.

(Optional) Install Draft ANSI Common Lisp Standard

This will make it easy to dynamically look up definitions of Common Lisp symbols and read the documentation as hypertext within Emacs.

First you will need to download and extract the files formatted for GNU Emacs. ftp://ftp.ma.utexas.edu/pub/gcl/gcl-info+texi.tgz

Then add the following to your Emacs initialization file:

(require 'info-look) 
(add-to-list 'Info-directory-list (file-name-as-directory "/home/dto/gcl-info/")) 
(setq Info-default-directory-list (cons "/usr/local/info/"  Info-default-directory-list)) 
(info-lookup-add-help 
 :mode 'lisp-mode 
 :regexp "[^][()'\" \t\n]+" 
 :ignore-case t 
 :doc-spec '(("(gcl.info)Symbol Index" nil nil nil))) 

(add-to-list 'load-path (expand-file-name "~/gcl-info/")) 
(require 'get-gcl-info) 

2D sprites with Xelf

For a general overview of Xelf setup instructions, please see Getting Started with Xelf. You should set up the Plong example to be loaded by Quicklisp, run it, and then read through the embedded source code here.

The particulars of Xelf's sprite implementation are documented in Bart Botta's OpenGL tutorial series, and the code is originally derived from them.

Defining a loadable system: the file PLONG.ASD

We must create a small .ASD file with your project's name and source file.

 (asdf:defsystem #:plong
  :depends-on (:xelf)
  :components ((:file "plong")))

Common Lisp packages

Then we must define a Common Lisp "package" for our game's code to inhabit.

  (defpackage :plong 
    (:use :cl :xelf) 
    (:export plong))

Then we declare what package the source file is in.

(in-package :plong)

Configuring your space

Here we define an arbitrary measurement unit used throughout, and set up some variables to hold the height and width of the game world.

 (defparameter *unit* 16)
 (defun units (n) (* *unit* n))
 (defparameter *width* 640)
 (defparameter *height* 480)

Defining Xelf game objects

Now it's time to define some game objects. Xelf game objects are called "nodes", and they can interact in two dimensions by being grouped into "buffers" of different kinds. Naturally there are base classes called NODE and BUFFER. These classes define the basic behaviors of the game engine. Nodes are endowed with such properties as an (X Y) position, width, height, an image to be displayed, and so on. The default node behaviors also hook all game objects into buffer features, such as collision detection, pathfinding, and serialization.

To define nodes of your own, use DEFCLASS and give NODE as a superclass. You can override the default values of NODE slots, as well as add your own.

 (defclass ball (node)
   ((height :initform (units 1))
    (width :initform (units 1))
    (color :initform "white")
    (speed :initform 6)
    (heading :initform (direction-heading :downright))))

The generic function UPDATE is called on each object once during each game loop.

 (defmethod update ((ball ball))
   (with-slots (heading speed) ball
     (move ball heading speed)))

Now we need walls around the game world in order to contain the ball.

 (defclass wall (node)
   ((color :initform "gray50")))

Handling collisions

We want the ball to bounce off of the walls. The COLLIDE method is called for every frame on all pairs of objects whose bounding boxes collide during that frame.

 (defmethod collide ((ball ball) (wall wall))
   (with-slots (heading speed x y) ball
     ;; back away from wall
     (move ball (opposite-heading heading) speed)
     ;; point toward player. (The function PADDLE is defined later.)
     (aim ball (heading-between ball (paddle)))
     ;; sometimes choose another direction to prevent getting stuck
     (percent-of-time 10 (incf heading (radian-angle 90)))))

Making noise

The ball should emit a retro beep when colliding with any node. We use DEFRESOURCE to let Xelf know about the sound file.

 (defresource "bip.wav" :volume 20)

 (defmethod collide :after ((ball ball) (node node))
   (play-sample "bip.wav"))

Destructible colored bricks

Now it's time to bash some bricks! First we define the dimensions of a brick and create a class.

 (defparameter *brick-width* (units 2))
 (defparameter *brick-height* (units 1.2))

 (defclass brick (node)
   ((color :initform "gray60")
    (height :initform *brick-height*)
    (width :initform *brick-width*)))

Here's how we can add color to bricks when they're being created.

 (defmethod initialize-instance :after ((brick brick) &key color)
   (when color
     (setf (slot-value brick 'color) color)))

Finally, the ball should bounce off the bricks and break them. See also DESTROY and RADIAN-ANGLE.

 (defmethod collide ((ball ball) (brick brick))
   (with-slots (heading) ball
     (destroy brick)
     (incf heading (radian-angle 90))))

Referring to global objects

Now we define some useful shorthand functions to refer to the ball and paddle.

 (defun ball () (slot-value (current-buffer) 'ball))
 (defun paddle () (slot-value (current-buffer) 'paddle))

(We'll set up the CURRENT-BUFFER later so that its SLOT-VALUEs are indeed referring to the right objects.)

Controlling the player

The player controls a rectangular paddle which can move left or right within the buffer.

 (defclass paddle (node)
   ((direction :initform nil)
    (height :initform (units 1))
    (width :initform (units 8))
    (color :initform "white")))

 (defparameter *paddle-speed* 3)

Now we define some handy functions to check whether the player is pressing left or right on the keyboard. Numeric keypad is also supported—it's a good idea to check both when using arrows to control your game.

 (defun holding-left-arrow ()
   (or (keyboard-down-p :kp4)
       (keyboard-down-p :left)))

 (defun holding-right-arrow ()
   (or (keyboard-down-p :kp6)
       (keyboard-down-p :right)))

 (defun find-joystick-direction ()
   (let ((heading (when (left-analog-stick-pressed-p)
                   (left-analog-stick-heading))))
     (when heading 
       (if (and (> heading (/ pi 2))
               (< heading (* 3 (/ pi 2))))
          :left 
          :right))))

 (defun find-direction ()
   (or (when (plusp (number-of-joysticks))
        (find-joystick-direction))
       (cond ((holding-left-arrow) :left)
            ((holding-right-arrow) :right))))

See also:

In the paddle's UPDATE method, we read the inputs and move the paddle accordingly.

 (defmethod update ((paddle paddle))
   (with-slots (direction) paddle
     (setf direction (find-direction))
     (when direction
       (move paddle (direction-heading direction) *paddle-speed*))))

Keeping the paddle in the playfield

The paddle should bounce back from the walls, too.

 (defmethod collide ((paddle paddle) (wall wall))
   (with-slots (direction) paddle
     (setf direction (opposite-direction direction))
     (move paddle (direction-heading direction) (* *paddle-speed* 2))))

See also:

The "english" is the directional force applied to the ball because of the player's moving the paddle to the left or right at the moment of collision.

 (defmethod english ((paddle paddle))
   (with-slots (direction) paddle
     (case direction
       (:left (direction-heading :upleft))
       (:right (direction-heading :upright))
       (otherwise (+ (slot-value (ball) 'heading)
                    (radian-angle 90))))))

In the BALL,PADDLE collision method, the english is applied and the ball is bounced away.

 (defmethod collide ((ball ball) (paddle paddle))
   (with-slots (heading speed) ball
     (setf heading (english paddle))
     (move ball heading speed)))

See also MOVE.

Building the game-world out of objects

Now that we have all the pieces of our game world, it's time to put them all together in a buffer. First we have a function to make a wall of a specified height, width, and position.

 (defun make-wall (x y width height)
   (let ((wall (make-instance 'wall)))
     (resize wall width height)
     (move-to wall x y)
     wall))

See also MOVE-TO, RESIZE.

This function MAKE-BORDER returns a buffer with four walls.

 (defun make-border (x y width height)
   (let ((left x)
        (top y)
        (right (+ x width))
        (bottom (+ y height)))
     (with-new-buffer
       ;; top wall
       (insert (make-wall left top (- right left) (units 1)))
       ;; bottom wall
       (insert (make-wall left bottom (- right left (units -1)) (units 1)))
       ;; left wall
       (insert (make-wall left top (units 1) (- bottom top)))
       ;; right wall
       (insert (make-wall right top (units 1) (- bottom top (units -1))))
       ;; send it all back
       (current-buffer))))

See also INSERT and CURRENT-BUFFER.

Now it's time for pretty rows of colored bricks.

 (defparameter *row-colors* 
   '("dark orchid" "medium orchid" "orchid" "dark orange" "orange" "gold"))

 (defun row-color (row)
   (nth (mod row (length *row-colors*))
        *row-colors*))

 (defun make-puzzle ()
   (with-new-buffer
     (dotimes (row 6)
       (dotimes (column 17)
        (add-node (current-buffer)
                  (make-instance 'brick :color (row-color row))
                  (+ 50 (* column *brick-width*))
                  (+ 50 (* row *brick-height*)))))))

See also ADD-NODE.

You can see that MAKE-PUZZLE also returns a new buffer. We'll put together these component buffers into the final game board below with a function called PASTE.

But first, we need a Buffer subclass for the game board.

 (defclass plong (buffer)
   ((paddle :initform (make-instance 'paddle))
    (ball :initform (make-instance 'ball))
    (background-color :initform "black")
    (width :initform *width*)
    (height :initform *height*)))

After initializing a new Plong buffer, we set things up so that pressing Control-R causes the game to reset.

 (defmethod initialize-instance :after ((plong plong) &key)
   (bind-event plong '(:r :control) 'start-game))

See also BIND-EVENT.

Putting it all together

The START-GAME function builds the game board by inserting the ball and paddle objects, then pasting in the bricks and border.

 (defmethod start-game ((plong plong))
   (with-slots (ball paddle) plong
     (with-buffer plong
       (insert ball)
       (insert paddle)
       (move-to ball 80 280)
       (move-to paddle 110 400)
       (paste-from plong (make-border 0 0 (- *width* (units 1)) (- *height* (units 1))))
       (paste-from plong (make-puzzle)))))

Now we define the main entry point for the game, the function PLONG. We set up our variables and then invoke WITH-SESSION to start Xelf going.

 (defun plong ()
   ;; Configure the screen dimensions
   (setf *screen-height* *height*)
   (setf *screen-width* *width*)
   ;; Allow resizing of window and scaling
   (setf *resizable* t)
   (setf *scale-output-to-window* t)
   (with-session
     (open-project :plong)
     ;; this indexes everything defined with DEFRESOURCE
     (index-pending-resources) 
     (let ((plong (make-instance 'plong)))
       ;; start the buffer running
       (switch-to-buffer plong)
       (start-game plong))))

See also:

Hack the example!

Try working with the example and making your own additions and modifications.

  • The paddle is slow—try speeding it up.
  • The game is incomplete, because the ball doesn't disappear when it hits the bottom of the screen. Try making a separate wall subclass for the bottom wall, and add a COLLIDE :AFTER method to destroy the ball when it hits that wall.
  • The ball physics are totally wrong. Try substituting your own collision rules.

Make something interesting and have fun.

Squareball: Simple physics and AI

Please see the squareball page for a completely source-documented advanced application of Xelf.

General game design

Please see the game design page.

Implementing Xelf

A literate source code documentation project is well underway. What better way to learn about writing games in Common Lisp, than to browse and read the documented source code of an entire Lisp game engine? You can visit the under-construction page Xelf: eXtensible Emacs-Like Facility.

TODO Localization

font issues

character encoding of source files

character encoding at runtime

cl-gettext

TODO Networking

TODO Serialization

TODO Procedural generation

This section covers basic concepts, including generative grammars and Xelf's buffer combinators.

TODO Tile maps and smooth scrolling

TODO Distributing games to players

Please see my notes on building and cross-compilation. The build script will work on both Linux and Windows, and you can even use Wine to cross-compile self-contained Win32 downloadables from within Linux!

A Mac OSX version of these build scripts is in the works. You can check out the current work-in-progress document to learn more.

Building the binary executable

SBCL makes it easy

ECL does it in C

Packaging assets and licenses/source code

Linux, Mac, Windows

Android

End-user Install/Uninstall

Mac App packaging

Free Software tools

AppImage

TODO Live coding

Creating, modifying, and inspecting live objects

Debugging and tracing

Redefining methods and variables

TODO 3D worlds and advanced physics with Clinch

Author: David O'Toole

Created: 2017-04-12 Wed 06:32

Validate