911 lines
No EOL
43 KiB
HTML
911 lines
No EOL
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang='en'><head><meta charset='utf-8' /><meta name='pinterest' content='nopin' /><link href='../../../../static/css/style.css' rel='stylesheet' type='text/css' /><link href='../../../../static/css/print.css' rel='stylesheet' type='text/css' media='print' /><title>August 2016 Lisp Game Jam Postmortem / Steve Losh</title></head><body><header><a id='logo' href='https://stevelosh.com/'>Steve Losh</a><nav><a href='../../../index.html'>Blog</a> - <a href='https://stevelosh.com/projects/'>Projects</a> - <a href='https://stevelosh.com/photography/'>Photography</a> - <a href='https://stevelosh.com/links/'>Links</a> - <a href='https://stevelosh.com/rss.xml'>Feed</a></nav></header><hr class='main-separator' /><main id='page-blog-entry'><article><script type='text/javascript' async
|
||
src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML'></script><h1><a href='index.html'>August 2016 Lisp Game Jam Postmortem</a></h1><p class='date'>Posted on August 15th, 2016.</p><p>The <a href="https://itch.io/jam/august-2016-lisp-game-jam">August 2016 Lisp Game Jam</a> just wrapped up at the end of last week.
|
||
I had some free time so I decided to take part, but I did something a bit
|
||
different. Instead of making a new game I ported an existing one (<a href="http://bitbucket.org/sjl/silt/">Silt</a>) to
|
||
Common Lisp.</p>
|
||
|
||
<p>I once read somewhere that when trying to build things and learn programming
|
||
languages you should either build something you know in a language you're
|
||
learning, or build something new in a language you already know, but <em>not</em> try
|
||
to do both at the same time. I've been getting into Common Lisp over the past
|
||
year, so for this game jam I decided to port my <a href="../../../2015/12/ludum-dare-34/index.html">Ludum Dare 34 game</a> from
|
||
Clojure to Common Lisp.</p>
|
||
|
||
<p>The game jam was ten days long. I didn't work on the game every day, but I did
|
||
manage to finish porting it over. I improved and polished a few mechanics along
|
||
the way, learned a lot, and ended up with a nice little library that sprung out
|
||
of the code. I'm happy with the result.</p>
|
||
|
||
<p>The code is <a href="http://bitbucket.org/sjl/silt2/">on Bitbucket</a>. You can play the game over telnet if you
|
||
want to try it out: <code>telnet silt.stevelosh.com</code>. In this post I'm just going to
|
||
jot down a few things I found interesting.</p>
|
||
|
||
<p>Disclaimer: I'm going to simplify some of the code snippets to make them easier
|
||
to read. If you want the full details you can read the actual code.</p>
|
||
|
||
<ol class="table-of-contents"><li><a href="index.html#s1-development">Development</a></li><li><a href="index.html#s2-ncurses-and-cl-charms">ncurses and cl-charms</a></li><li><a href="index.html#s3-using-a-state-machine-as-the-game-loop">Using a State Machine as the Game Loop</a></li><li><a href="index.html#s4-terrain-generation">Terrain Generation</a><ol><li><a href="index.html#s5-tiling-diamond-square">Tiling Diamond Square</a></li></ol></li><li><a href="index.html#s6-entity-aspects-and-systems">Entity, Aspects, and Systems</a><ol><li><a href="index.html#s7-coordinates">Coordinates</a></li><li><a href="index.html#s8-user-interface">User Interface</a><ol><li><a href="index.html#s9-visible">Visible</a></li><li><a href="index.html#s10-flavor">Flavor</a></li><li><a href="index.html#s11-inspectable">Inspectable</a></li></ol></li><li><a href="index.html#s12-food">Food</a></li><li><a href="index.html#s13-creatures-and-mysteries">Creatures and Mysteries</a><ol><li><a href="index.html#s14-energy">Energy</a></li></ol></li></ol></li><li><a href="index.html#s15-random-name-generation">Random Name Generation</a></li><li><a href="index.html#s16-simple-data-structures">Simple Data Structures</a><ol><li><a href="index.html#s17-weightlists">Weightlists</a></li><li><a href="index.html#s18-ticklists">Ticklists</a></li></ol></li><li><a href="index.html#s19-profiling-and-performance">Profiling and Performance</a></li><li><a href="index.html#s20-future-improvements-and-ideas">Future Improvements and Ideas</a></li></ol>
|
||
|
||
<h2 id="s1-development"><a href="index.html#s1-development">Development</a></h2>
|
||
|
||
<p><a href="http://bitbucket.org/sjl/silt2/">Silt 2</a> is written in Common Lisp. It uses <a href="https://github.com/HiTECNOLOGYs/cl-charms">cl-charms</a> (a wrapper around
|
||
<a href="https://en.wikipedia.org/wiki/Ncurses">ncurses</a>) to handle drawing to the terminal, and a few other Common Lisp
|
||
libraries like <a href="https://common-lisp.net/project/iterate/">iterate</a> and <a href="https://github.com/nightfly19/cl-arrows">cl-arrows</a>.</p>
|
||
|
||
<p>I developed it on <a href="http://www.sbcl.org/">SBCL</a> and OS X, and the telnet server is running Debian so
|
||
it works there too. It almost runs in <a href="http://ccl.clozure.com/">ClozureCL</a>, but something
|
||
Unicode-related is broken with ncurses under CCL and I didn't bother debugging
|
||
it.</p>
|
||
|
||
<p>I used <a href="https://github.com/roswell/roswell">Roswell</a> to build a standalone binary for "releases". This binary
|
||
starts up much faster than loading everything from scratch.</p>
|
||
|
||
<p>I use <a href="https://neovim.io/">Neovim</a> and was pleasantly surprised when running ncurses inside
|
||
Neovim's terminal emulator Just Worked (especially since the cl-charms <code>README</code>
|
||
specifically says you <em>can't</em> run it in emacs' terminal!). It was really nice to
|
||
have the actual game running inside my text editor.</p>
|
||
|
||
<h2 id="s2-ncurses-and-cl-charms"><a href="index.html#s2-ncurses-and-cl-charms">ncurses and cl-charms</a></h2>
|
||
|
||
<p><a href="https://github.com/HiTECNOLOGYs/cl-charms">cl-charms</a> is a wrapper around <a href="https://en.wikipedia.org/wiki/Ncurses">ncurses</a> that I used to handle drawing the
|
||
game to the terminal. The original Clojure version used <a href="https://sjl.bitbucket.io/clojure-lanterna/">clojure-lanterna</a>.</p>
|
||
|
||
<p>The game's drawing code is pretty simple, so there's not a whole lot to say
|
||
here. I loop over the screen, drawing the contents of each world coordinate at
|
||
each screen coordinate, and refresh the window.</p>
|
||
|
||
<p>cl-charms mostly worked out great. It's a bit wordy at times (always having to
|
||
pass <code>charms:*standard-window*</code> to everything), but you can wrap it up pretty
|
||
easily. I'd recommend it if you need to do console drawing in Common Lisp.</p>
|
||
|
||
<p>cl-charms has a low-level interface that's just an FFI wrapper around ncurses,
|
||
and a high-level interface that abstracts some of the Cishness away for you.
|
||
I mostly used the high-level interface, but one big thing that's missing is
|
||
support for colors. Working with colors in ncurses is a bit tedious, but this
|
||
is Lisp so I can just abstract away all the boring stuff:</p>
|
||
|
||
<pre><code>(defmacro defcolors (&rest colors)
|
||
`(progn
|
||
,@(iterate (for n :from 0)
|
||
(for (constant nil nil) :in colors)
|
||
(collect `(define-constant ,constant ,n)))
|
||
(defun init-colors ()
|
||
,@(iterate
|
||
(for (constant fg bg) :in colors)
|
||
(collect `(charms/ll:init-pair ,constant ,fg ,bg))))))
|
||
|
||
(defcolors
|
||
(+color-white-black+ charms/ll:COLOR_WHITE charms/ll:COLOR_BLACK)
|
||
(+color-blue-black+ charms/ll:COLOR_BLUE charms/ll:COLOR_BLACK)
|
||
(+color-cyan-black+ charms/ll:COLOR_CYAN charms/ll:COLOR_BLACK)
|
||
(+color-yellow-black+ charms/ll:COLOR_YELLOW charms/ll:COLOR_BLACK)
|
||
(+color-green-black+ charms/ll:COLOR_GREEN charms/ll:COLOR_BLACK)
|
||
(+color-pink-black+ charms/ll:COLOR_MAGENTA charms/ll:COLOR_BLACK)
|
||
|
||
(+color-black-white+ charms/ll:COLOR_BLACK charms/ll:COLOR_WHITE)
|
||
(+color-black-yellow+ charms/ll:COLOR_BLACK charms/ll:COLOR_YELLOW)
|
||
|
||
(+color-white-blue+ charms/ll:COLOR_WHITE charms/ll:COLOR_BLUE)
|
||
|
||
(+color-white-red+ charms/ll:COLOR_WHITE charms/ll:COLOR_RED)
|
||
|
||
(+color-white-green+ charms/ll:COLOR_WHITE charms/ll:COLOR_GREEN))
|
||
|
||
(defmacro with-color (color &body body)
|
||
(once-only (color)
|
||
`(unwind-protect
|
||
(progn
|
||
(charms/ll:attron (charms/ll:color-pair ,color))
|
||
,@body)
|
||
(charms/ll:attroff (charms/ll:color-pair ,color)))))
|
||
</code></pre>
|
||
|
||
<h2 id="s3-using-a-state-machine-as-the-game-loop"><a href="index.html#s3-using-a-state-machine-as-the-game-loop">Using a State Machine as the Game Loop</a></h2>
|
||
|
||
<p>One thing many games have in common is a <a href="https://en.wikipedia.org/wiki/Game_programming#Game_structure">game loop</a>. The original version of
|
||
Silt had one, but for the rewrite I decided to structure the main flow of the
|
||
game as a state machine instead. This worked out really well and I'm glad I did
|
||
it.</p>
|
||
|
||
<p>At first I looked around and tried to find a state machine library for Common
|
||
Lisp, but then I realized I was being ridiculous and could just model a state
|
||
machine with vanilla Lisp functions:</p>
|
||
|
||
<pre><code>(defun state-title ()
|
||
(render-title)
|
||
(press-any-key)
|
||
(state-intro))
|
||
|
||
(defun state-intro ()
|
||
(render-intro)
|
||
(press-any-key)
|
||
(state-generate))
|
||
|
||
(defun state-generate ()
|
||
(render-generate)
|
||
(reset-world)
|
||
(generate-world)
|
||
(state-map))
|
||
|
||
(defun state-map ()
|
||
(charms:enable-non-blocking-mode charms:*standard-window*)
|
||
(state-map-loop))
|
||
|
||
(defun state-map-loop ()
|
||
(case (handle-input-map)
|
||
((:quit) (state-quit))
|
||
((:regen) (state-generate))
|
||
((:help) (state-help))
|
||
(t (progn
|
||
(unless *paused*
|
||
(iterate (repeat *frame-skip*)
|
||
(tick-world)
|
||
(tick-log)))
|
||
(render-map)
|
||
(when *sleep*
|
||
(sleep 0.05))
|
||
(state-map-loop)))))
|
||
|
||
(defun state-help ()
|
||
(render-help)
|
||
(press-any-key)
|
||
(state-map))
|
||
|
||
(defun state-quit ()
|
||
'goodbye)</code></pre>
|
||
|
||
<p>This worked especially well with cl-charms and ncurses because for states like
|
||
the title and help screens there's no point in looping to redraw the screen over
|
||
and over again while waiting for input. I just flipped ncurses into
|
||
block-while-awaiting-input mode and let it free up the CPU while waiting for the
|
||
user to continue.</p>
|
||
|
||
<p>In hindsight I probably should have split out the pause state into a separate
|
||
state, which would have let me use blocking input there too.</p>
|
||
|
||
<p>Using functions for states like this is only possible because SBCL (and CCL)
|
||
perform <a href="https://en.wikipedia.org/wiki/Tail_call">last call optimization</a>, so the stack doesn't get blown by all the
|
||
recursion happening.</p>
|
||
|
||
<h2 id="s4-terrain-generation"><a href="index.html#s4-terrain-generation">Terrain Generation</a></h2>
|
||
|
||
<p>The original Silt was made for Ludum Dare 34 in 72 hours, so I didn't spend too
|
||
much time on terrain. I just created an empty world and scattered some lakes
|
||
around it, which looked like this:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/silt1-terrain.png"><img src="../../../../static/images/blog/2016/08/silt1-terrain.png" alt="Screenshot of terrain in the original game"></a></p>
|
||
|
||
<p>This worked and was quick, but is pretty boring and ugly. In the past few
|
||
months I've learned a lot more about terrain generation, so I fleshed things out
|
||
a bit more for the new port:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/silt2-terrain.png"><img src="../../../../static/images/blog/2016/08/silt2-terrain.png" alt="Screenshot of terrain in the new version"></a></p>
|
||
|
||
<p>Now I've got oceans and mountains for the creatures to explore.</p>
|
||
|
||
<h3 id="s5-tiling-diamond-square"><a href="index.html#s5-tiling-diamond-square">Tiling Diamond Square</a></h3>
|
||
|
||
<p>My initial impulse was to use <a href="https://en.wikipedia.org/wiki/Perlin_noise">Perlin Noise</a> or <a href="https://en.wikipedia.org/wiki/Simplex_noise">Simplex Noise</a> to generate
|
||
the heightmap for the world, but I ran into a problem. I wanted the world to be
|
||
a torus, just like in the original game, so I needed a terrain generation
|
||
algorithm that would generate tileable/wrappable heightmaps.</p>
|
||
|
||
<p>One way to do this is to use higher-dimensional noise to get 2D noise that
|
||
tiles. If you want to get a 2D heightmap that's tileable in one direction, you
|
||
can use 3D noise and take a cylindrical slice of it. To get a heightmap that
|
||
tiles both ways you need to use 4D noise. <a href="http://ronvalstar.nl/creating-tileable-noise-maps">This article</a> gives
|
||
a really nice overview of the process.</p>
|
||
|
||
<p>Unfortunately I couldn't find an implementation of 4D Simplex Noise in Common
|
||
Lisp. <a href="https://github.com/aerique/black-tie">black-tie</a> and <a href="https://github.com/sebity/noise">noise</a> both only offer up to 3D noise, and I don't
|
||
feel confident enough to implement it myself, even after skimming the simplex
|
||
noise paper.</p>
|
||
|
||
<p>So I decided to try a different approach and figure out how to modify <a href="../../06/diamond-square/index.html">Diamond
|
||
Square</a> to tile. The <a href="https://en.wikipedia.org/wiki/Diamond-square_algorithm">Wikipedia article for Diamond Square</a> says:</p>
|
||
|
||
<blockquote>
|
||
<p>Another option [for the diamond step] is to 'wrap around', taking the fourth
|
||
value from the other side of the array. When used with consistent initial
|
||
corner values this method also allows generated fractals to be stitched
|
||
together without discontinuities.</p>
|
||
</blockquote>
|
||
|
||
<p>This sounded great, but after thinking about it for a bit it's obviously not
|
||
correct. If we have a heightmap and do what the article says, it will seem to
|
||
work at first:</p>
|
||
|
||
<pre class="lineart">
|
||
╔══════════════════╗
|
||
┌─┬─┬─┬─┬─┐ ║ ┌─┬─┬─┬─┬─┐ ║
|
||
│5│ │ │ │5│ ║ │5│ │ │ │5│ ║
|
||
├─┼─┼─┼─╱┬╲ ║ ├─┼─┼─┼─╱┬╲ ║
|
||
│ │ │ │╱│││╲ ║ │ │ │ │╱│││╲ ║
|
||
├─┼─┼─╱─┼▼┤ ╲ ║ ├─┼─┼─╱─┼▼┤ ╲ ║
|
||
│ │ │3├─▶◉◀──? ╚═══════│3├─▶◉◀════╝
|
||
├─┼─┼─╲─┼▲┤ ╱ ├─┼─┼─╲─┼▲┤ ╱
|
||
│ │ │ │╲│││╱ │ │ │ │╲│││╱
|
||
├─┼─┼─┼─╲┴╱ ├─┼─┼─┼─╲┴╱
|
||
│5│ │ │ │5│ │5│ │ │ │5│
|
||
└─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┘
|
||
</pre>
|
||
|
||
<p>Wrapping like this will indeed make sure that the averages match up, but there's
|
||
two problems.</p>
|
||
|
||
<p>First: the corners are all the same value, which means that when you put four
|
||
heightmaps next to each other there's an unnatural flat area of four identical
|
||
height values next to each other. This probably wouldn't be noticeable in
|
||
practice, but if you want to do things <em>right</em> it won't be acceptable.</p>
|
||
|
||
<p>But the <em>real</em> problem is the jitter. If the jitter on one side of the map
|
||
happens to be large and positive and the jitter on the other side happens to be
|
||
large and negative, you'll get a jarring "cliff" when you try to tile them:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/bad-tiling-ds.png"><img src="../../../../static/images/blog/2016/08/bad-tiling-ds.png" alt="Example of poorly-tiling diamond square"></a></p>
|
||
|
||
<p>The solution I came up with is to reduce the size of the heightmap by 1.
|
||
Instead of the heightmap being \(2^n + 1\) in each dimension we can make it
|
||
\(2^n\) and adjust the coordinate-wrapping function appropriately.
|
||
Importantly, we <em>don't</em> change the calculation of the radius values as we
|
||
iterate over the array, so this means quite often we'll be "reaching" for that
|
||
final row/column:</p>
|
||
|
||
<pre class="lineart">
|
||
? ?
|
||
┌─╲─┬─┬─╱
|
||
│ │╲│ │╱│
|
||
├─┼─◢─◣─┤
|
||
│ │ │◉│ │
|
||
├─┼─◥─◤─┤
|
||
│ │╱│ │╲│
|
||
├─╱─┼─┼─╲
|
||
│5│ │ │ │?
|
||
└─┴─┴─┴─┘
|
||
</pre>
|
||
|
||
<p>When we try to access that nonexistent coordinate we just wrap around back to
|
||
zero. Notice that we also only need to initialize a single corner cell now.</p>
|
||
|
||
<p>It's a simple change, but the result is <em>much</em> nicer:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/good-tiling-ds.png"><img src="../../../../static/images/blog/2016/08/good-tiling-ds.png" alt="Example of nicely-tiling diamond square"></a></p>
|
||
|
||
<h2 id="s6-entity-aspects-and-systems"><a href="index.html#s6-entity-aspects-and-systems">Entity, Aspects, and Systems</a></h2>
|
||
|
||
<p>Terrain generation is pretty, but the next step in the port was to add some
|
||
plants, creatures, and artifacts. In the original game I just represented
|
||
things in the world as vanilla Clojure maps, but that was getting kind of messy
|
||
and I wanted to try a different approach this time.</p>
|
||
|
||
<p>Recently I read through <a href="http://www.amazon.com/dp/1466560010/?tag=stelos-20">Game Engine Architecture</a> (a <em>fantastic</em> book) and
|
||
made a few games in <a href="https://unity3d.com/">Unity</a>, which together made me want to try using an
|
||
<a href="https://en.wikipedia.org/wiki/Entity_component_system">Entity/Component System</a> this time around. There are a couple of ECS
|
||
libraries out there for Common Lisp like <a href="https://github.com/lispgames/cl-ecs">cl-ecs</a> and <a href="https://github.com/mfiano/ecstasy">ecstasy</a>, but in true
|
||
Lisp fashion I ended up not being quite satisfied with any of them and writing
|
||
Yet Another God Damn Library.</p>
|
||
|
||
<p>It's called <a href="https://sjl.bitbucket.io/beast/overview/">Beast</a>. It's subtly different than the others in that it prefers
|
||
to be a really thin layer over CLOS and uses inheritance instead of composition.
|
||
It uses the word "aspect" instead of "component" to try to overload that word
|
||
a bit less, so it's the "Basic Entity/Aspect/System Toolkit". It ended up being
|
||
about 150 lines of code (not including docstrings), so I managed to avoid going
|
||
down too much of a rabbit hole during the jam.</p>
|
||
|
||
<p>If you want to know all the details, check out its documentation (it has
|
||
<em>actual</em> documentation). But here I'll just talk about a couple of the
|
||
particular bits of Silt that I used it for.</p>
|
||
|
||
<h3 id="s7-coordinates"><a href="index.html#s7-coordinates">Coordinates</a></h3>
|
||
|
||
<p>The first thing I needed was a way to keep track of where things are in the
|
||
world.</p>
|
||
|
||
<p>If the world space were continuous a <a href="https://en.wikipedia.org/wiki/Quadtree">quadtree</a> would have been my first
|
||
choice, but in Silt the world is split into discrete integer coordinates.
|
||
Creatures move directly from \((x, y)\) to \((x+1, y+1)\). I decided to use
|
||
a simple array of lists to represent this:</p>
|
||
|
||
<pre><code>(defparameter *coords-contents*
|
||
(make-array (list +world-size+ +world-size+)
|
||
:initial-element nil))</code></pre>
|
||
|
||
<p>Each value in the array is a list of the entities that are currently there.
|
||
This means looking up what things are at a given coordinate is a single fast
|
||
<code>aref</code>.</p>
|
||
|
||
<p>I tried using a hash table instead of an array at first, thinking that if the
|
||
world were fairly sparse it would be wasteful to allocate an array with a ton
|
||
of <code>nil</code> values in it. But the array method is much faster for looking things
|
||
up (which happens a lot) and memory is cheap, so I decided against the hash
|
||
tables. It worked great in the end.</p>
|
||
|
||
<p>Entities need to know where they are in the world, so I defined a Beast aspect
|
||
for that:</p>
|
||
|
||
<pre><code>(define-aspect coords x y)</code></pre>
|
||
|
||
<p>Then I defined a few functions to handle moving entities into, out of, and
|
||
around the world:</p>
|
||
|
||
<pre><code>(defun coords-insert-entity (e)
|
||
(push e (aref *coords-contents* (coords/x e) (coords/y e))))
|
||
|
||
(defun coords-remove-entity (e)
|
||
(zap% (aref *coords-contents* (coords/x e) (coords/y e))
|
||
#'delete e %))
|
||
|
||
(defun coords-move-entity (e new-x new-y)
|
||
(coords-remove-entity e)
|
||
(setf (coords/x e) (wrap new-x)
|
||
(coords/y e) (wrap new-y))
|
||
(coords-insert-entity e))
|
||
|
||
(defun coords-lookup (x y)
|
||
(aref *coords-contents* (wrap x) (wrap y)))</code></pre>
|
||
|
||
<p>Entities might also like to know what's near them:</p>
|
||
|
||
<pre><code>(defun nearby (entity &optional (radius 1))
|
||
(remove entity
|
||
(iterate
|
||
outer
|
||
(with x = (coords/x entity))
|
||
(with y = (coords/y entity))
|
||
(for dx :from (- radius) :to radius)
|
||
(iterate
|
||
(for dy :from (- radius) :to radius)
|
||
(in outer
|
||
(appending (coords-lookup (+ x dx)
|
||
(+ y dy))))))))</code></pre>
|
||
|
||
<p>This ends up compiling down to a nice tight loop of \((2 * radius + 1)^2\)
|
||
<code>aref</code>s. I only wish iterate had a nicer syntax for looping over nested
|
||
indices like this. I'm sure it's possible to write an iterate driver for it --
|
||
maybe someday I'll try making one.</p>
|
||
|
||
<p>I also needed a way to get entities into the world array when they're created
|
||
and remove them when they die. Beast (well, actually CLOS) makes this trivially
|
||
easy with auxiliary methods:</p>
|
||
|
||
<pre><code>(defmethod entity-created :after ((entity coords))
|
||
(coords-insert-entity entity))
|
||
|
||
(defmethod entity-destroyed :after ((entity coords))
|
||
(coords-remove-entity entity))</code></pre>
|
||
|
||
<h3 id="s8-user-interface"><a href="index.html#s8-user-interface">User Interface</a></h3>
|
||
|
||
<p>Once I had a way of know where things are, the next step was to display them on
|
||
the screen. I broke this into a few separate aspects.</p>
|
||
|
||
<h4 id="s9-visible"><a href="index.html#s9-visible">Visible</a></h4>
|
||
|
||
<p>The <code>visible</code> aspect is for things that are drawn on the screen with
|
||
a particular glyph and color:</p>
|
||
|
||
<pre><code>(define-aspect visible glyph color)
|
||
|
||
;; ...
|
||
|
||
(define-entity tree (coords visible ...))
|
||
|
||
(defun make-tree (x y)
|
||
(create-entity 'tree
|
||
:coords/x x
|
||
:coords/y y
|
||
:visible/glyph "T"
|
||
:visible/color +color-green-black+
|
||
;; ...
|
||
))
|
||
</code></pre>
|
||
|
||
<p>The drawing code can then figure out what to draw for each screen coordinate:</p>
|
||
|
||
<pre><code>(defun draw-map ()
|
||
(iterate
|
||
(for sx :from 0 :below *screen-width*)
|
||
(for wx :from *view-x*)
|
||
(iterate
|
||
(for sy :from 0 :below *screen-height*)
|
||
(for wy :from *view-y*)
|
||
(for entity = (find-if #'visible? (coords-lookup wx wy)))
|
||
(if entity
|
||
(with-color (visible/color entity)
|
||
(write-string-at (visible/glyph entity) sx sy))
|
||
;;; otherwise draw the terrain
|
||
(...)))))</code></pre>
|
||
|
||
<p>Again: my kingdom for a <code>(for-nested ...)</code> iterate driver! But the core is just
|
||
using <code>(find-if #'visible? (coords-lookup wx wy))</code> to find the first visible
|
||
thing and then drawing it:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/aspect-visible.png"><img src="../../../../static/images/blog/2016/08/aspect-visible.png" alt="Screenshot of entities with the visible aspect"></a></p>
|
||
|
||
<p>I used <code>find-if</code> instead of <code>remove-if-not</code> because we can only draw one
|
||
character to a given position in the terminal anyway, so I just pick the first
|
||
thing that happens to be in the list.</p>
|
||
|
||
<h4 id="s10-flavor"><a href="index.html#s10-flavor">Flavor</a></h4>
|
||
|
||
<p>The <code>flavor</code> aspect is for adding <a href="https://en.wikipedia.org/wiki/Flavor_text">flavor text</a> that appears when the user
|
||
puts their cursor over an entity:</p>
|
||
|
||
<pre><code>(define-aspect flavor text)
|
||
|
||
;; ...
|
||
|
||
(define-entity tree (coords visible flavor ...))
|
||
|
||
(defun make-tree (x y)
|
||
(create-entity 'tree
|
||
:coords/x x
|
||
:coords/y y
|
||
:visible/glyph "T"
|
||
:visible/color +color-green-black+
|
||
:flavor/text
|
||
'("A tree sways gently in the wind.")))</code></pre>
|
||
|
||
<p>Then when the user's cursor is at a certain position I can find all the entities
|
||
there and draw the flavor text for any that have the <code>flavor</code> aspect:</p>
|
||
|
||
<pre><code>(defun draw-selected ()
|
||
(write-left
|
||
(iterate
|
||
(for entity :in (multiple-value-call #'coords-lookup
|
||
(screen-to-world *cursor-x* *cursor-y*)))
|
||
(when (typep entity 'flavor)
|
||
(appending (flavor/text entity) :into text)
|
||
|
||
;; ...
|
||
|
||
(collecting "" :into text))
|
||
(finally (return text)))
|
||
1 1 :pad t))</code></pre>
|
||
|
||
<p>Which looks like this:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/aspect-flavor.png"><img src="../../../../static/images/blog/2016/08/aspect-flavor.png" alt="Screenshot of flavor text"></a></p>
|
||
|
||
<p>Of course the flavor text doesn't have to be a constant:</p>
|
||
|
||
<pre><code>(defun make-creature (x y &key
|
||
(color +color-white-black+)
|
||
(glyph "@"))
|
||
(let ((name (random-name)))
|
||
(create-entity 'creature
|
||
:name name
|
||
:coords/x x
|
||
:coords/y y
|
||
:visible/color color
|
||
:visible/glyph glyph
|
||
:flavor/text
|
||
(list (format nil "A creature named ~A is here." name)
|
||
"It likes food."))))</code></pre>
|
||
|
||
<h4 id="s11-inspectable"><a href="index.html#s11-inspectable">Inspectable</a></h4>
|
||
|
||
<p>The last thing I wanted was an easy way to show attributes of entities in the
|
||
main game UI. The original Clojure game just dumped the entire object to the
|
||
screen:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/silt1-inspect.png"><img src="../../../../static/images/blog/2016/08/silt1-inspect.png" alt="Screenshot of creature inspection in the original game"></a></p>
|
||
|
||
<p>But this time I wanted a bit more control. The <code>inspectable</code> aspect has a list
|
||
of things that should be displayed. These can be symbols (which denote CLOS
|
||
slot names) or functions that return <code>(label . text)</code> conses:</p>
|
||
|
||
<pre><code>(define-aspect inspectable
|
||
(slots :initform nil))
|
||
|
||
(defun inspectable-get (entity slot)
|
||
(etypecase slot
|
||
(symbol (cons slot (slot-value entity slot)))
|
||
(function (funcall slot entity))))</code></pre>
|
||
|
||
<p>When creating an entity I can just list out the slots I want to be displayed on
|
||
the screen, or use a little <code>lambda</code> if I want to show something that's not an
|
||
actual slot:</p>
|
||
|
||
<pre><code>(defun make-fruit (x y)
|
||
(create-entity 'fruit
|
||
;; ...
|
||
:inspectable/slots '(edible/energy)))
|
||
|
||
(defun make-creature (x y &key ...)
|
||
(let ((name (random-name)))
|
||
(create-entity 'creature
|
||
;; ...
|
||
:inspectable/slots
|
||
(list 'name
|
||
(lambda (c) (cons 'directions ...))
|
||
'metabolizing/energy
|
||
'metabolizing/insulation
|
||
'aging/birthtick
|
||
'aging/age))))</code></pre>
|
||
|
||
<p>Then I just append some extra text for <code>inspectable</code> entities when drawing
|
||
descriptions of things at the cursor position:</p>
|
||
|
||
<pre><code>(defun draw-selected ()
|
||
(write-left
|
||
(iterate
|
||
(for entity :in (multiple-value-call #'coords-lookup
|
||
(screen-to-world *cursor-x* *cursor-y*)))
|
||
(when (typep entity 'flavor)
|
||
;; ...
|
||
(when (typep entity 'inspectable)
|
||
(appending
|
||
(indent
|
||
(iterate
|
||
(with slots = (mapcar (curry #'inspectable-get entity)
|
||
(inspectable/slots entity)))
|
||
(with width = (apply #'max
|
||
(mapcar (compose #'length #'symbol-name #'car)
|
||
slots)))
|
||
(for (label . contents) :in slots)
|
||
(collect
|
||
(let ((*print-pretty* nil))
|
||
(format nil "~vA ~A" width label contents)))))
|
||
:into text))
|
||
|
||
(collecting "" :into text))
|
||
(finally (return text)))
|
||
1 1 :pad t))</code></pre>
|
||
|
||
<p>This is pretty ugly because I wanted to justify and indent things nicely, but
|
||
the result looks much nicer than the original game:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/silt2-inspect.png"><img src="../../../../static/images/blog/2016/08/silt2-inspect.png" alt="Screenshot of creature inspection in the new version"></a></p>
|
||
|
||
<h3 id="s12-food"><a href="index.html#s12-food">Food</a></h3>
|
||
|
||
<p>Seeing the world is nice, but we also want the things in it to actually <em>do</em>
|
||
something. The world revolves heavily around food and energy, so I defined
|
||
a few aspects to handle things:</p>
|
||
|
||
<pre><code>(define-aspect edible
|
||
energy
|
||
original-energy)
|
||
|
||
(define-aspect decomposing
|
||
rate
|
||
(remaining :initform 1.0))
|
||
|
||
(define-aspect fruiting
|
||
chance)
|
||
|
||
(defmethod initialize-instance :after ((e edible) &key)
|
||
(setf (edible/original-energy e)
|
||
(edible/energy e)))</code></pre>
|
||
|
||
<p>I do wish there was a slightly less wordy way to default the value of one slot
|
||
to another one, but oh well.</p>
|
||
|
||
<p>Then I just added the aspects to the appropriate entities:</p>
|
||
|
||
<pre><code>(define-entity tree (coords visible fruiting flavor))
|
||
(define-entity fruit (coords visible edible flavor decomposing inspectable))
|
||
(define-entity algae (coords visible edible decomposing))
|
||
(define-entity grass (coords visible edible decomposing))
|
||
(define-entity corpse (coords visible flavor decomposing))</code></pre>
|
||
|
||
<p>Trees can grow fruit, so they have the <code>fruiting</code> aspect. The <code>grow-fruit</code>
|
||
Beast system handles growing some each tick:</p>
|
||
|
||
<pre><code>(define-system grow-fruit ((entity fruiting coords))
|
||
(when (randomp (fruiting/chance entity))
|
||
(make-fruit (wrap (random-around (coords/x entity) 2))
|
||
(wrap (random-around (coords/y entity) 2)))))</code></pre>
|
||
|
||
<p>Fruit is <code>edible</code>, but also decomposes over time. It's got <code>flavor</code> and
|
||
<code>inspectable</code> aspects so you can see how much energy is left.</p>
|
||
|
||
<p>I added algae and grass as secondary food sources to spread out the food supply
|
||
a bit more and make the creatures a bit less dependent on the trees. I didn't
|
||
give these flavor text to avoid cluttering up the UI too much.</p>
|
||
|
||
<p>I considered making corpses edible too, but figured that might be a bit too
|
||
gruesome. So corpses decompose, but the critters aren't cannibals.</p>
|
||
|
||
<p>I made a couple of Beast systems to handle the process of decomposing things
|
||
every game tick:</p>
|
||
|
||
<pre><code>(define-system rot ((entity decomposing))
|
||
(when (minusp (decf (decomposing/remaining entity)
|
||
(decomposing/rate entity)))
|
||
(destroy-entity entity)))
|
||
|
||
(define-system rot-food ((entity decomposing edible))
|
||
(setf (edible/energy entity)
|
||
(lerp 0.0 (edible/original-energy entity)
|
||
(decomposing/remaining entity))))</code></pre>
|
||
|
||
<p><code>rot</code> runs on everything with the <code>decomposing</code> aspect. It ticks along the
|
||
progress of an entity's decomposition, and destroys it once it's finished.</p>
|
||
|
||
<p><code>rot-food</code> runs on every entity that's both <code>decomposing</code> <em>and</em> <code>edible</code>. It
|
||
reduces the energy value of the food over time, because rotten food is less
|
||
healthy. I'm pretty happy with how easy Beast makes this kind of thing.</p>
|
||
|
||
<h3 id="s13-creatures-and-mysteries"><a href="index.html#s13-creatures-and-mysteries">Creatures and Mysteries</a></h3>
|
||
|
||
<p>The final pieces of the world are the creatures and artifacts.</p>
|
||
|
||
<h4 id="s14-energy"><a href="index.html#s14-energy">Energy</a></h4>
|
||
|
||
<p>Creatures need food (energy) to survive. I modeled this with a <code>metabolizing</code>
|
||
aspect and <code>consume-energy</code> system:</p>
|
||
|
||
<pre><code>(define-aspect metabolizing insulation energy)
|
||
|
||
(defmethod starve ((entity entity))
|
||
(destroy-entity entity))
|
||
|
||
(defmethod calculate-energy-cost ((entity metabolizing))
|
||
(let* ((insulation (metabolizing/insulation entity))
|
||
(base-cost 1.0)
|
||
(temperature-cost (max 0 (* 0.2 (- (abs *temperature*) insulation))))
|
||
(insulation-cost (* 0.1 insulation)))
|
||
(+ base-cost temperature-cost insulation-cost)))
|
||
|
||
(define-system consume-energy ((entity metabolizing))
|
||
(when (minusp (decf (metabolizing/energy entity)
|
||
(calculate-energy-cost entity)))
|
||
(starve entity)))</code></pre>
|
||
|
||
<p>I made <code>starve</code> and <code>calculate-energy-cost</code> generic functions because I thought
|
||
I might eventually have different metabolizing things in the world and might
|
||
want to override them. I didn't end up doing this in the end (creatures are the
|
||
only things that burn energy) so these could have been normal functions.</p>
|
||
|
||
<p>The energy mechanic works similarly to the original game:</p>
|
||
|
||
<ul>
|
||
<li>Creatures spend a bit of energy each tick to stay alive.</li>
|
||
<li>When you make the temperature hotter or colder, it costs additional energy per
|
||
tick for the creatures to live.</li>
|
||
<li>Creatures sometimes gain/lose insulation during reproduction, which mitigates
|
||
the energy cost of the temperature difference.</li>
|
||
<li>Insulation itself costs a little bit of energy every tick.</li>
|
||
</ul>
|
||
|
||
<p>The effect is that if you change the temperature gradually over time, the
|
||
population will evolve higher insulation values (because the children with more
|
||
insulation are more likely to survive longer). If you then set the temperature
|
||
back to zero (the ideal) the population will eventually evolve to shed the
|
||
insulation, because it costs a little bit of energy and doesn't provide any
|
||
benefit when the world is pleasant. Natural selection is fun.</p>
|
||
|
||
<p>The last piece of the puzzle is letting things actually take action. Creatures
|
||
and some artifacts need to take an action on every tick, while other artifacts
|
||
only do things occasionally. A pair of aspects and systems handles the
|
||
bookkeeping here:</p>
|
||
|
||
<pre><code>(define-aspect sentient function)
|
||
(define-aspect periodic
|
||
function
|
||
(counter :initform 1)
|
||
next
|
||
min
|
||
max)
|
||
|
||
(define-system sentient-act ((entity sentient))
|
||
(funcall (sentient/function entity) entity))
|
||
|
||
(define-system periodic-tick ((entity periodic))
|
||
(when (zerop (setf (periodic/counter entity)
|
||
(mod (1+ (periodic/counter entity))
|
||
(periodic/next entity))))
|
||
(setf (periodic/next entity)
|
||
(random-range (periodic/min entity)
|
||
(periodic/max entity)))
|
||
(funcall (periodic/function entity) entity)))</code></pre>
|
||
|
||
<p>I'm not going to go over all the actual AI and actions, you can take a look at
|
||
the code if you're curious.</p>
|
||
|
||
<h2 id="s15-random-name-generation"><a href="index.html#s15-random-name-generation">Random Name Generation</a></h2>
|
||
|
||
<p>I wanted to add a more personal connection to the creatures this time around, so
|
||
I decided they should have names. I used a really simple form of
|
||
<a href="http://www.roguebasin.com/index.php?title=Syllable-based_name_generation">syllable-based name generation</a> to give each creature its own random
|
||
name:</p>
|
||
|
||
<pre><code>(defparameter *name-syllables*
|
||
(-> "syllables.txt"
|
||
slurp
|
||
read-from-string
|
||
(coerce 'vector)))
|
||
|
||
(defun random-name ()
|
||
(format nil "~:(~{~A~}~)"
|
||
(iterate (repeat (random-range 1 5))
|
||
(collect (random-elt *name-syllables*)))))</code></pre>
|
||
|
||
<p>To get a random name I just smash together one to four random syllables. To
|
||
make a list of syllables I grabbed some Icelandic text and made a pair of really
|
||
janky shell and Python scripts to print out every 3/4/5-letter chunk of every
|
||
word, sort them by frequency, and take the top 500.</p>
|
||
|
||
<p>They're not <em>really</em> syllables but they're okay for just a couple of lines of
|
||
code and a few minutes work:</p>
|
||
|
||
<p><a href="../../../../static/images/blog/2016/08/silt-names.png"><img src="../../../../static/images/blog/2016/08/silt-names.png" alt="Screenshot of creature names"></a></p>
|
||
|
||
<h2 id="s16-simple-data-structures"><a href="index.html#s16-simple-data-structures">Simple Data Structures</a></h2>
|
||
|
||
<p>As I coded things up I wound up with a handy pair of data structures I might use
|
||
again for other things in the future.</p>
|
||
|
||
<h3 id="s17-weightlists"><a href="index.html#s17-weightlists">Weightlists</a></h3>
|
||
|
||
<p>Each game tick a creature needs to decide which direction to walk. At the start
|
||
of the game they just pick a random direction, but as they reproduce their
|
||
children can mutate to prefer certain directions over others.</p>
|
||
|
||
<p>The natural selection of Silt turns out to prefer creatures that wander around
|
||
to those that stay in place. Fruit takes time to grow, so it's more effective
|
||
to travel around and gather it than to sit in place waiting for it to regrow.</p>
|
||
|
||
<p>The original game just used a Clojure vector of weights and directions to
|
||
represent how much a creature prefers each direction. That worked, but the
|
||
weights are only ever set/changed when a creature is born, and a random element
|
||
is chosen every turn. It's more efficient in the long run if we precompute
|
||
a few things up front, so I made a little "weightlist" API:</p>
|
||
|
||
<pre><code>(defstruct (weightlist (:constructor %make-weightlist))
|
||
weights sums items total)
|
||
|
||
(defun make-weightlist (items weights)
|
||
"Make a weightlist of the given items and weights."
|
||
(%make-weightlist
|
||
:items items
|
||
:weights weights
|
||
:sums (prefix-sums weights)
|
||
:total (apply #'+ weights)))
|
||
|
||
(defun weightlist-random (weightlist)
|
||
"Return a random item from the weightlist, taking the weights into account."
|
||
(iterate
|
||
(with n = (random (weightlist-total weightlist)))
|
||
(for item :in (weightlist-items weightlist))
|
||
(for weight :in (weightlist-sums weightlist))
|
||
(finding item :such-that (< n weight))))</code></pre>
|
||
|
||
<p>This is pretty straightforward. Note that the weights can be integers or floats
|
||
(or some of each!) and things will Just Work, because Common Lisp's <code>random</code> can
|
||
take either. Weights of zero are fine too, as long as at least one element has
|
||
a nonzero weight.</p>
|
||
|
||
<h3 id="s18-ticklists"><a href="index.html#s18-ticklists">Ticklists</a></h3>
|
||
|
||
<p>In a couple of places I needed some kind of list where items in it expire over
|
||
time. For example:</p>
|
||
|
||
<ul>
|
||
<li>The Fountain artifact only lets creatures drink from it once every thousand
|
||
ticks, so I needed a way to keep track of the entities that had drank
|
||
recently.</li>
|
||
<li>The game log at the bottom of the screen contains messages that should be
|
||
shown for a certain number of ticks, then disappear.</li>
|
||
</ul>
|
||
|
||
<p>I made a simple little thing I called a "ticklist" to handle these:</p>
|
||
|
||
<pre><code>(defun make-ticklist ()
|
||
nil)
|
||
|
||
(defmacro ticklist-push (ticklist value lifespan)
|
||
`(push (cons ,lifespan ,value) ,ticklist))
|
||
|
||
(defun ticklist-tick (ticklist)
|
||
(flet ((decrement (entry)
|
||
(decf (car entry)))
|
||
(dead (entry)
|
||
(minusp (car entry))))
|
||
(->> ticklist
|
||
(mapc #'decrement)
|
||
(remove-if #'dead))))
|
||
|
||
(defun ticklist-contents (ticklist)
|
||
(mapcar #'cdr ticklist))</code></pre>
|
||
|
||
<p>Internally a ticklist is just a list of <code>(remaining-ticks . thing)</code> conses, but
|
||
the rest of my code doesn't have to care about that:</p>
|
||
|
||
<pre><code>(defun fountain-act (f)
|
||
(with-slots (recent) f
|
||
(zapf recent #'ticklist-tick)
|
||
(iterate
|
||
(with already-drank = (ticklist-contents recent))
|
||
(for creature :in (remove-if-not #'creature? (nearby f)))
|
||
(unless (member creature already-drank)
|
||
(creature-mutate-appearance creature)
|
||
(ticklist-push recent creature 1000)
|
||
(log-message "~A drinks from the fountain and... changes."
|
||
(creature-name creature))))))
|
||
|
||
(defun log-message (s &rest args)
|
||
(ticklist-push *game-log* (apply #'format nil s args) 200))
|
||
|
||
(defun state-map-loop ()
|
||
;; ...
|
||
(unless *paused*
|
||
(iterate (repeat *frame-skip*)
|
||
(tick-world)
|
||
(zapf *game-log* #'ticklist-tick))))</code></pre>
|
||
|
||
<h2 id="s19-profiling-and-performance"><a href="index.html#s19-profiling-and-performance">Profiling and Performance</a></h2>
|
||
|
||
<p>When something starts slowing things down it's helpful to be able to turn on
|
||
profiling and see what's going on. SBCL has a nice statistical profiler, so
|
||
I made a couple of functions to flip it on and off as needed:</p>
|
||
|
||
<pre><code>#+sbcl
|
||
(defun dump-profile ()
|
||
(with-open-file (*standard-output* "silt.prof"
|
||
:direction :output
|
||
:if-exists :supersede)
|
||
(sb-sprof:report :type :graph
|
||
:sort-by :cumulative-samples
|
||
:sort-order :ascending)
|
||
(sb-sprof:report :type :flat
|
||
:min-percent 0.5)))
|
||
|
||
#+sbcl
|
||
(defun start-profiling ()
|
||
(sb-sprof::reset)
|
||
(sb-sprof::profile-call-counts "SILT")
|
||
(sb-sprof::start-profiling :max-samples 50000
|
||
; :mode :cpu
|
||
:mode :time
|
||
:sample-interval 0.01
|
||
:threads :all))
|
||
|
||
#+sbcl
|
||
(defun stop-profiling ()
|
||
(sb-sprof::stop-profiling)
|
||
(dump-profile))</code></pre>
|
||
|
||
<p>When I wanted to check performance I could just evaluate <code>(start-profiling)</code>
|
||
over NREPL and let the game continue to run, then <code>(stop-profiling)</code> a little
|
||
bit later and look at the results. It came in handy once or twice when tracking
|
||
down some slowness.</p>
|
||
|
||
<h2 id="s20-future-improvements-and-ideas"><a href="index.html#s20-future-improvements-and-ideas">Future Improvements and Ideas</a></h2>
|
||
|
||
<p>This game jam was quite a bit of fun! I'm happy with the results and feel like
|
||
I've learned a lot along the way.</p>
|
||
|
||
<p>I'm done with the game and don't plan on updating it any more, but I'll scribble
|
||
down a few extra ideas for things that could be improved here, just to get them
|
||
out of my head:</p>
|
||
|
||
<ul>
|
||
<li>Figure out the Unicode issues with cl-charms and CCL.</li>
|
||
<li>Contribute to cl-charms to add some higher-level tools for working with color.</li>
|
||
<li>Contribute to one of the Common Lisp noise libraries to implement the 4D
|
||
variant of Simplex Noise.</li>
|
||
<li>Flesh out the name generation into something much nicer and more polished.</li>
|
||
<li>Implement different costs for moving over different terrain.</li>
|
||
<li>Add health, fighting, and carnivores.</li>
|
||
<li>Add more mysterious artifacts to the world.</li>
|
||
<li>Flesh out the vegetation model to let trees grow and die, algae spread, etc.</li>
|
||
<li>Improve the visuals. <a href="https://sites.google.com/site/broguegame/">Brogue</a> proves you can do far more than you might
|
||
think with just Unicode characters.</li>
|
||
<li>Model senses like vision, providing the creatures with more information but
|
||
with an energy cost.</li>
|
||
<li>Give creatures "brains" by generating and mutating actual Lisp code. This
|
||
would let the creatures learn strategies over time, though I'm not sure how
|
||
feasible it would be.</li>
|
||
<li>Improve performance by profiling much more and fixing the hottest parts of the
|
||
code.</li>
|
||
<li>Add saving and loading of the world.</li>
|
||
<li>Add the ability to seed the RNG and make everything deterministic, so people
|
||
can share interesting seeds. Doing this for the terrain generation at least
|
||
should be pretty easy, the game as a whole might be slightly trickier.</li>
|
||
<li>Improve the UI a bit more, maybe using ncurses' support for windows layered on
|
||
top of other windows.</li>
|
||
</ul>
|
||
</article></main><hr class='main-separator' /><footer><nav><a href='https://github.com/sjl/'>GitHub</a> ・ <a href='https://twitter.com/stevelosh/'>Twitter</a> ・ <a href='https://instagram.com/thirtytwobirds/'>Instagram</a> ・ <a href='https://hg.stevelosh.com/.plan/'>.plan</a></nav></footer></body></html> |