Static checking in games development

I recently released my first game, Twilight Drive, on Steam. I was the only programmer so with no-one else to check my work, I wanted a programming environment which would help minimise my mistakes. I chose Java, specifically because it meant I could use the checker framework. The checker framework uses Java's annotations to add in customisable checks during compilation. In this post I'll describe a few key features of the checker framework and how it helped me to write 50,000 lines of code with very few runtime errors or bugs.

Nullness checking

Nullness checking is probably one of the more well-known static checks. You annotate your code to say if something can or can't be null: @Nullable/@NonNull respectively. The default in the checker framework is @NonNull for fields and parameters. There is control flow analysis to check that all the annotations are accurate. So for example, this passes:


        // Default is @NonNull for fields so no need to annotate:
        private String message = "";
        public void setMessage(@Nullable String msg)
        {
            if (msg == null)
                msg = "";
            this.message = msg; // line A
        }
    

The flow checker knows that although msg may be null on entry to the method, by the time it reaches line A, msg cannot be null so it's safe to assign to the (always non-null) message field.

The obvious benefit of nullness checking is that null pointer exceptions are virtually eliminated from your code. I can only remember one while writing the game, and that was because of a dodgy suppression of warnings (more on that in the caveats section later). A less obvious benefit to nullness checking is that it can pick up bugs you wouldn't think of as nullness bugs. For example, in this code:


        @Nullable String currentLabel = obj.getLabelOrNull();
        String fallbackLabel = "OK";
        if (currentLabel != null)
            setMessage(currentLabel);
        else
            setMessage(currentLabel); // Copy/paste mistake; meant to be fallbackLabel
    

This code has a bug, as described in the comment, where it uses the wrong variable. This bug will be flagged by the nullness checker, because even though it has no idea I made a copy/paste mistake, the mistake fails the nullness checker (setMessage takes a non-null String and in the else clause I'm passing a possibly-null String). I've had several wrong-variable bugs like this flagged by the nullness checker.

Units of measure

Often your program code has numbers that are in a specific unit. In games development this is very common. For example, here's some fields annotated with their units in my Car class:


        private @degrees float direction;
        private @GameCoord float x;
        private @GameCoord float y;
        private @GameCoord_per_s float curSpeed;
        private final @GameCoord_per_s2 float carAcceleration; 
    

Degrees is built-in to the checker framework. The @GameCoord unit is one I added: it's my notional unit in-game. (Some games think of themselves as using a physical unit like metres, which would be fine too, but I prefer a made-up unit.) @GameCoord_per_s is game coordinates per second (i.e. speed), and @GameCoord_per_s2 is game coordinates per seconds-squared (i.e. acceleration).

The basics of updating the position and speed seem straightforward. Each frame you add the speed to the position, and then the acceleration to the speed:


        x += Math.cos(direction) * curSpeed;
        y += Math.sin(direction) * curSpeed;
        curSpeed += carAcceleration;
    

However, these three lines of code will justifiably produce 5 checker errors. There are three problems with this code:

So the corrected version is:


        // Assume a variable: @s float frameTime
        x += Math.cos(Math.toRadians(direction)) * curSpeed * frameTime;
        y += Math.sin(Math.toRadians(direction)) * curSpeed * frameTime;
        curSpeed += carAcceleration * frameTime;
    

I have told the checker that @GameCoord_per_s multiplied by @s (seconds) gives @GameCoord, and @GameCoord_per_s2 multiplied by @s gives @GameCoord_per_s, so now the units all line up.

I made this mistake of forgetting to multiply by the frame time several times in development. If you have a dynamic frame time, it's something that you may not notice while your frame rate is quite steady, but if it gets variable, suddenly your car is lurching around. However, I never needed to watch out for this while testing because it was checked upfront.

Units are everywhere in game code: time, distance, speed, and so on. They occur in many other applictions, too. Imagine programming a weather app: you'd want to make sure you didn't mix celsius and fahrenheit, mph vs knots, etc. Once you know to look for units, you find them in lots of places: euros vs dollars, pixels vs em, seconds vs milliseconds, and so on.

Asset files

Often in your program code you need to refer to the path of an external file or directory. This is especially true in games where you need to load assets like textures, sound files, levels, and associate them all together. It's often frowned upon to put these in the program code itself, but I don't really have a problem with this, especially as a solo developer where the artists, modellers and programmers are... the same one person. Whether you put "textures/grass.jpg" in your code, or whether you move that to an external file "terrains.txt" and then refer to "terrains.txt" in your code seems to me to be much of muchness. I prefer the first, because it can be more directly statically checked, as I'll explain.

The challenge with asset file paths is theoretically simple: they just need to be correct. The reason this is challenging is that assets tend to change over the course of the project. You start off with "textures/grass.jpg" but then you realise you have multiple grass textures so you rename it "textures/grassA.jpg" -- but you may forget to update all the references. Then later in the project you think grassA is unused and delete it, but you don't realise you missed a reference to it on level 36 until a beta tester complains about it.

With the checker framework I added an annotation @AssetFile that can be added to String variables to mean it points to an asset file (texture, model, sound, etc):


        private final @AssetFile String carModel = "models/car1.g3dj";
    

All assignments to these values are then statically checked at compile-time. During compilation, the checker plugin looks in the assets directory to check if the path "models/car1.g3dj" exists. If it does, fine; if not, you get a compile error. Thus you have compile-time checking of all your asset mentions in the source code.

Caveats

Static checking is not perfect, and you may well have noticed a few potential problems as you read the above. Let's start with the asset files. Just because the file is there at compile-time doesn't completely guarantee it's there at run-time. The file could be deleted or you could not package it correctly with the application. But it's still better to static check it than not check it, in my opinion.

An additional constraint on the @AssetFile annotation is that with a basic checker, you can't dynamically construct paths and have them checked. You can either go without the checker, soup it up, or rejig your code to not dynamically construct paths. I didn't find it too onerous to just avoid constructing paths.

These caveats touch on a general principle: the checker helps a lot, but nothing in life or programming is guaranteed. Just like a unit test, the checker passing doesn't guarantee your code works in all circumstances, but it's a useful automated check that can give you much more confidence.

Sometimes with the checkers you will run up against Gödel's incompleteness theorem: you can have something you know is correct but you can't prove it (or not easily) to the checker framework. For example, here's a utility method I wrote to filter out nulls in a stream:


        public static <T> Stream<@NonNull T> filterOutNulls(Stream<@Nullable T> stream)
        {
            return stream.filter(t -> t != null);
        }
    

It takes a stream of items that may be null, and filters out the nulls, leaving a non-null stream. However, the checker framework's flow control doesn't understand Java's streams enough to know what this method is doing, so it gives an error that the method is returning a stream of @Nullable values. In this case you can make a small rewrite to fix the problem (there's a flatMap()-based solution that the framework would understand), but sometimes the best thing to do is put @SuppressWarnings("nullness") on the method and move on. To give you an idea of how often these are needed, I have 75 annotations between the three checkers described here (thus around 25 per checker) across 50,000 lines, so it's reasonably rare if you properly buy in to using the checkers.

The other drawback to static checking is that it does increase your compile times. One possible mitigation is to only run the checkers in an automated build when you push your changes, but in the end I decided the trade-off was worth running them all the time. An additional mitigation is to split your project into multiple modules, for several reasons: to get parallel compilation, to reduce unnecessary compilation, and to control which modules need which checkers. For example, my geometry utility module doesn't deal with asset files, so it doesn't need the asset file checker to run.

Comparison to other languages

Some of the checks listed here are available in other programming languages, either implicitly or through external tools. For example, many functional programming languages (e.g. Haskell) don't have null pointers to begin with. Nullness checkers exist for many other imperative languages. What I like about the checker framework in specific is that there's a lot of checkers built-in, including some rarer ones (for example, Swift and F# are the only languages I know of with compile-time units of measure support), but also that you can define your own, with very customisable reasoning. Haskell's type system is very powerful, for example, but it can't implement some of these checks without very invasive Template Haskell additions to the code. The power of Java's annotations is that they're simple to add to the code, and then all the logic goes in an external checker. My asset file checker is just over 100 lines of code, for example.

Summary

Like all software engineering techniques, static checking is no silver bullet. But it's a useful tool to minimise errors. It's really a souped-up static type-checking with similar advantages and disadvantages. You cut out a lot more errors at compile-time, but at the cost of having to tell the compiler more about your types and having it spend time doing the checks. I like this way of programming: I often find that having to specify these types clarifies my own thinking even before the checkers examine the code. I don't know how common or rare static checking is in games development but I found it a great benefit. Games can be difficult to test automatically, so it's good to use as many other ways of eliminating bugs as possible.

Finally, if you liked this article, why not check out the free demo on Steam.