Okay so I didn’t actually make a game in Janet. I played with Janet and Raylib for a few weeks, learned a lot about graphics stuff, but decided to stop working on it before I made anything actually playable. Mostly because, well, it was taking up far too much of my time and I have more important things to work on.

But then I learned about the Autumn Lisp Game Jam: ten days to write a game, in some kind of lisp, starting in about two weeks. Well! Would you look at that. Sounds like a lot of fun. Unfortunately, I already spent my lisp game budget for the year, but now that I know this is a thing perhaps I’ll be around next spring with an entry of my own.

But I did learn some interesting stuff, and I thought I’d write up some of what I did and what I learned, because I thought it might be useful to participants thinking about using Janet or Raylib for their entries – gotchas, lessons learned, sage wisdom from someone who has barely scratched the surface of these libraries.

I mean, that’s sort of a weak pretense. It was a fun project that hit a pretty broad variety of interesting topics, and I thought it would make a good side project diary regardless. The timing of the game jam is the reason I’m writing this up when I am, but the content is definitely not going to be tailored to that specific audience.

Why Janet?

I don’t have a scripting language that I like.

Python has been my go-to “one level above bash” language for basically forever – not because I like Python, but just sort of by default. In fact you might say that I actively dislike Python, and in recent years I’ve been reaching for it less and less, squeezing more and more out of bash, and wishing that I had something better. But what? Ruby? No. Perl? Ha. OCaml? It’s not the most ergonomic for simple string twiddling.

What I really wanted – I thought – was some sort of simple lisp-like. People seem to like Racket, but my outsider impression is that Racket is about as far from “simple” as you can get. I like Clojure in theory, but obviously it’s a non-starter for little scripts, and the last time I used ClojureScript the tooling was so painful that I’m still sort of traumatized. I hear good things about Chicken Scheme, and I still intend to try it out one day, but the whole R5RS/R6RS/R7RS thing is confusing and scary to me, so I keep… not doing that.

Anyway, all of this is to say: I happened to be in the market for a new language.

Then, about a month ago, I saw a blog post on Lobsters called “Why I will stay Janet”. Effusive posts about any technology must be taken with a grain of salt, but I thought it was a nice article and it piqued my curiosity enough to ask: what the heck is Janet?

what the heck is Janet

Well well well. Look what we have here.

A simple dynamically typed interpreted language with garbage collection and Clojure-like syntax? Sounds like exactly what I’m looking for.

I was pleased to see, on the language’s homepage, that there were already some useful libraries for the language – SQLite bindings, an HTTP server, stuff like that. Evidence that it’s not a complete toy language; that someone has actually used it to do something.

And one of those bindings was to something called Raylib, which I had never heard of before.

what the heck is Raylib

I clicked through, and was delighted by what I saw: a simple OpenGL wrapper library with a very simple API – and a wealth of interactive in-browser examples. My curiosity went from piqued to excited. I wanted to check out Raylib; I wanted to see what it’s deal was. I can’t really say why, except to admit that I got into programming by writing little QBASIC games on DOS, and I will occasionally return to my roots and peter around with a game idea for a few weeks before losing interest. Usually on iOS, or in a canvas on the browser. Not since I was in college have I tried to write a game for “the desktop.”

But Raylib looked great, and I just so happened to have been thinking about top-down twin-stick shooters recently. So I decided to check it out.

And since Janet led me to Raylib, why not kill two birds with one stone and give Janet a shot as well?

what the heck is Jaylib

The Janet Raylib bindings are called Jaylib, and the GitHub repo comes with a little example program translated into Janet.

But I couldn’t get it working.

Bad start.

I installed Janet with Nix on macOS, so some of this was my fault. I had never actually depended on a system framework like OpenGL before, and it took me a minute to figure out that I needed to explicitly add that (and CoreAudio, and IOKit, and Cocoa, and…) to my shell.nix file in order to get it building.

$ cat shell.nix
with import <nixpkgs> {};

let frameworks =
  with darwin.apple_sdk.frameworks; [
    AudioToolbox
    Carbon
    Cocoa
    CoreAudio
    Foundation
    IOKit
    Kernel
    OpenGL
  ];
in
mkShell {
  nativeBuildInputs = [ janet darwin.libobjc ] ++ frameworks;
}

But even with that sorted out, Jaylib still wouldn’t build. Something was wrong with the include path; it wasn’t finding GLFW.

This was sort of hard to debug. The jpm tool – the Janet package manager that was actually trying to build Jaylib – didn’t seem to support any kind of --verbose output.1 I could see error messages from clang, but I couldn’t see the clang invocation that actually caused them. I didn’t know if this was some Nix issue or not, but looking around the project.janet file I realized a discrepancy, and long story short it ought to work now.

That patch is pretty trivial, but it was tricky to actually test it. To explain why, we have to talk a little bit about how jpm works.

a little bit about how jpm works

jpm is… a throwback to a simpler time.

Remember when you used to want to install language dependencies, and there was just some system-wide directory that you threw all the dependencies in? Like, if you had a project that depended on left-pad-1.2, you had to globally install left-pad-1.2? And if you had another project that depended on left-pad-1.3 then, well, shoot, I guess it’s time to buy another computer?

So that’s how jpm works by default. It also eschews any sort of versioning, and works purely on, like, git repos. Another thing that– well, whatever. It doesn’t matter. Nix can paper over that, and jpm allows you to use a per-project directory instead of a global path by setting the JANET_PATH environment variable. Which is nice and simple, and combined with shell.nix it’s very easy to make a little virtualenv-like Janet thing. jpm makes it very easy to have per-project dependencies, so I shouldn’t really complain about the default global behavior.

Anyway. You declare your dependencies by writing a little project.janet file that looks something like this:

(declare-project
  :name "game"
  :description "some kind of game"
  :dependencies [
    "https://github.com/janet-lang/jaylib.git"
  ])

(declare-executable
  :name "game"
  :description "some kind of game"
  :entry "src/main.janet")

The :dependencies key being the relevant bit there. When you run jpm deps to install your dependencies, it installs them into the directory at JANET_PATH. Simple as that.

Except… :dependencies, apparently, has to be a list of URLs. To git repositories. Served over HTTP. If you want to make some changes to a library, say to fix the build parameters, you have to… push it to some git repository somewhere in order to be able to install it. You can’t just list a local file path as a dependency.

Kind of crazy town, but sure okay whatever. Maybe there is some way to do that, but I could not figure it out. Instead I figured out a hacky-looking way around it: throw away jpm deps, and use jpm install instead.

jpm install will install the current project into the JANET_PATH. Where jpm deps “pulls” dependencies, jpm install like… pushes them. Into a directory full of Janet dependencies.

So in order to test my changes, I had to go into my local Jaylib directory, and run, basically:

~/src/jaylib ➜ JANET_PATH="$PWD/../game/jpm" jpm install

To like… push the library into the other project’s jpm/ directory (the name I used for my “local libraries” – think the node_modules/ equivalent).

So that’s… gross and weird and kind of the opposite of how I think of dependencies, but here we are. I had to do that a lot. It was weird and gross the whole time.

Writing a Nix thing to manage installing Janet dependencies would make this feel better, but I haven’t done that yet. Oh well.

we built Jaylib

We built Jaylib! Hurray. Look at us. And in the process we learned how to make changes to Jaylib, which is something we’re going to be doing a lot over the coming weeks.

Plus the example code worked. We look good; we feel good. Time to actually do… anything.

I started by making a little move-a-circle-with-the-arrow-keys demo. I knew I wanted my game to use a scaled up “pixel art” vibe, because that’s what my formative gaming memories look like. In the modern era, this means rendering the screen to a frame buffer and then drawing that to the screen scaled up.

Jaylib couldn’t actually do that, though, when I started this project. But someone had already opened a PR to add the function, so I pulled that into my fork and went on my merry way.

And then I wrote some Janet.

Well, actually, no. The first thing I did was make sure I could distribute programs written in Janet. In case this ever winds up on itch.io or something, I want to make sure I don’t have to ask anyone to install Janet.

And it’s actually really easy! jpm build will produce a statically-linked executable with all the dependencies – which is to say, Raylib – compiled in:

$ otool -L build/game
build/game:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.60.2)
    /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 22.0.0)
    /System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo (compatibility version 1.2.0, current version 1.5.0)
    /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
    /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)

Neat! So no one needs to, like, install Janet in order to run this. Honestly it’s mystifying to me that there are interpreted languages that don’t do this, but good luck getting, like, a Ruby script that you can share with someone else.

Janet sort of feels like it was meant to be embedded in a larger program – think Lua – so it makes it very easy to start up a Janet runtime from C. And that’s exactly how the “binary build” works – it just writes out a trivial C file that contains the precompiled Janet bytecode and starts up the Janet interpreter. It “embeds itself” in this simple wrapper progam. It’s neat and elegant and everything should work that way.

Anyway, that gave me some more confidence that Janet was actually a usable thing and not just someone’s hobby project that wouldn’t stand up to any kind of actual use. Not that my little game is “actually using” Janet, but you know what I mean.

And with that newfound confidence, I started actually writing some code.

and i have some opinions

I really like Janet as a language. It’s very tastefully designed – I like a lot of the choices the author has made. It’s been many years since I wrote any Clojure, so I don’t remember how many of these choices are inherited from there and how many are Janet originals, but either way I like the end result.

I like that you can introduce bindings with classic let expressions or imperative assignment statements.

I like that it distinguishes between bindings (think JavaScript’s const) and variables (JavaScript’s let). I like the names it uses for these: (def x 1) and (var x 1).

I like that reassigning a variable is an explicitly different operation than declaring one – (set x 2). I have seen so many dynamic languages conflate these two operations, leading to horrible nonlocal bugs when your variable declaration magically changes to a reassignment because your set of imported names changed or whatever.

I like that it has true and false and booleans are their own type. I don’t like that it still has the concept of “truthiness” – you can still use nil as a “falsy” value – but at least it’s a step in the right direction.

I like that nil is not the empty list. (Although… more on that later.)

I like its string literal syntax: you can write normal double-quoted strings, but Janet also supports Markdown-style backtick quoting, using however many backticks you need. For example:

(print ``markdown uses `backticks` to express code blocks``)

As a result of the string syntax, quasiquote is written ~ instead of `. This looks better to me, although it loses the visual pun with '. Plus ' and , are still present, and ugly as ever. Oh well.

I like that it uses familiar names for basic operations. Transforming a list is map, not mapcar or select or project or collect. Filtering a list is filter, not select or remove-if-not or find-all or whatever else is out there. It does call foldreduce,” unfortunately, but I guess you can’t have everything.

I like Janet’s splicing – think Python’s splat (*) or JavaScript’s spread (...) or maybe lisp’s unquote-splice (,@), except that you can use it anywhere. Janet uses ; for splicing, and I like that – it’s almost a visual pun of , and ; doing these spiritually related operations. I find it pleasing.

Splicing is very useful with Jaylib because sometimes Raylib functions expect points – Raylib calls these Vector2s – and sometimes they expect separate x and y arguments:

void DrawCircle(int centerX, int centerY, float radius, Color color);
void DrawCircleV(Vector2 center, float radius, Color color);
void DrawCircleLines(int centerX, int centerY, float radius, Color color);

Many functions have these V equivalents that take vectors, but not all of them – there is no DrawCircleLinesV in Raylib, for example. And since Jaylib pretty faithfully wraps the Raylib API, there’s no draw-circle-lines-v in Jaylib either. But that’s okay! If you want to draw the outline of a circle, you can just:

(draw-circle-lines ;center radius color)

This makes Raylib’s inconsistent API less troublesome. (And that’s a very simple example – there are some functions that take Rectangle structs and others that take x, y, width, height – which would be pretty annoying without ; to paper over it.)

These are all surface-level things: things that don’t really matter, but that make Janet look palatable and feel nice in the hand. Also note that there are plenty of things that I dislike, but I’ll get into those in future blog posts. My overall impression is very positive, and I had fun writing code in Janet – which is really the most important thing for a silly side project like this.

writing what code

Okay, so let’s talk about the actual game.

I had planned on making a top-down procedural stealth shootery game – think, like, Splinter Cell, but top-down 2D. The actual game idea is a little more complicated, but since I didn’t actually implement it that’s a good enough starting point to understand the components.

“Stealth” means line of sight – viewcones and such; you know the drill. And line of sight means raycasting, probably.

So that was the first thing I did. I wrote a little 2D raycaster thing. It was fun! I even took a video when I first got something that sort of worked:

It’s super ugly, but, you know, it’s debug stuff. White on yellow is never a great choice. Also note the goofy “crosshair” which changes shape as the camera moves around – I now know that you need to align drawing to integer pixel values or you can get some weird behavior like that.

But you can see the rays; you can see that they’re being cast to corners. You can also see that not all the rays are being cast yet.

I found a great article about 2D FOV and was basing my implementation of that description. It’s a very simple algorithm, but as someone who hadn’t thought about vectors in over a decade it did take me a minute to get comfortable with the math. I think I took that video as I was getting comfortable with the separating axis theorem, because you can see that I don’t yet have “vision past corners” implemented.

That comes soon, though, along with some less painful debug output:

The colors indicate the “order” of rays, sorted clockwise around the viewer, in a slightly more readable way than the numbers did in the first video.

It’s important that the points are ordered correctly so that we can construct a triangle fan to “fill in” the region in view, and that was my way of checking that I did it right.

But hark: I did not do it right. There is a bug here – but we’ll get to that in the next post.

Doing all this required adding a few more functions to Jaylib – triangle fan drawing and HSV conversions. Nothing tricky: all functions that are available in Raylib, but didn’t yet have Janet bindings.

And can I just say: it’s very pleasant to add Janet bindings! The whole process was very easy. I did screw up the HSV stuff initially, thinking that colors ranged from 0 to 255 as I am used to (they’re 0 to 1 in OpenGL and Raylib), but the actual Janet-related bits were trivial. Thanks, Janet bindings.

Putting it altogether, my final FOV looked like this:

And that was the end of day one. I was pretty pleased with how it turned out, and Raylib’s API was exactly what I wanted. I could draw lines, I could draw shapes – what more could someone need in their game?

what more

It’s fun looking back on these early experiments. Everything was new and exciting. This is where it started. Here’s where it ended, a few weeks later:

So… we’ll get to that. We’ll learn about rendering soft shadows with signed distance fields, and we’ll replace this goofy field of view raycasting prototype with realtime raymarching on the GPU. We’ll add dynamic lights and shadows; we’ll learn about surfacing and normal maps and a bunch of other stuff – transparency, depth buffers, you name it. We will, reluctantly, realize that our top-down 2D game might actually be an isometric 3D game, and see what it takes to make that leap. We’ll experiment with postprocessing effects to give it that lo-fi 64-color palette you see above.

Stay tuned!


  1. It turns out jpm actually does support --verbose output, it’s just very hidden and confusing and I couldn’t figure it out in my first ever encounter with the tool. See, unlike every other tool I’ve ever used, you don’t run:

    $ jpm deps --verbose
    error: <function deps> called with 1 argument, expected 0
      in main [/nix/store/wssxm55ydhzypn78n91b8r5j6ggv76fn-janet-1.16.1/bin/jpm] on line 1440, column 9
      in _thunk [/nix/store/wssxm55ydhzypn78n91b8r5j6ggv76fn-janet-1.16.1/bin/jpm] on line -1, column -1
      in cli-main [boot.janet] on line 3559, column 39
    

    No no no, if you want verbose output, you actually have to run:

    $ jpm --verbose deps
    

    I didn’t figure this out until I read the source for jpm, which is, well, you know. There are rough edges here. ↩︎