Linux Game Jam 2019 Experiences

We decided to participate once again in the annual Linux Game Jam and made a small terminal-based platformer game. So, I figured I'd write a little about how our game jam went, the technical decisions and problems we faced and how the final product came out.

Introductions

We've participated in all of the Linux Game Jams hosted by The Linux Gamer, aka Gardiner, since 2017. Our little Jam team basically consists of a few Finns (Tumocs, Tuubi and me) and we occasionally bring in a Dutch guy (Ysblokje).

Our Linux Game Jam history has been as follows:

Brainstorming

We like to do something interesting on the game jam and we tend to come up with pretty wild and silly ideas when we are planning our game jams. With ENDUSER in particular we set fairly strict limitations on ourselves from which we molded a quite strange game world, and I feel we succeeded with that project quite well.

We wanted to do a similar thing this year and during our brainstorming the idea of an ASCII-based platformer game came up. We're also quite fond of exploring different technologies and learning new things, instead of just getting stuck in our ways, so writing a game using ncurses/pdcurses seemed like an interesting direction to take the game. We had also planned some interesting gameplay mechanics and our original idea involved an evil EMACS gradually eating the ASCII characters of the ASCIILAND, but in the end due to Tumocs having to drop out of the team and due to deadline constraints we had to keep our game rather simple.

On the technical side, we settled on using C and ncurses/pdcurses with SDL2 for sound and gamepad input. We picked C as our programming language, since everyone in the team had some experience with C, although none of us had used it particularly recently or in large quantities. For me this was the first real project I'd written in C.

Execution

In the end we decided that we'd just start implementing various platformer mechanics and entities and then coming up with levels that utilized them. It didn't take very long until we had an @ sign jumping around, and due to the ASCII style, implementing levels was quite easy and they could be trivially loaded from text files.

A static game world is no fun though, and we began work on some dynamic entities. At first I was going to just copy-paste the player's physics code when necessary and replace the input/update functionality with code specific to that entity. This clearly would have been laborious and would have resulted in a lot of repeated code, so instead I wrote a more generic entity system. I defined the Entity in the following way:

struct Entity {
    struct Level* level;
    int width;
    char hidden;
    char disabled;
    char sprite;
    char state;
    double x, y, x_vel, y_vel;
    void* internal_data;
    void (*processFunc)(struct Entity* entity, double delta);
};

The interesting parts are the processFunc function pointer and the internal_data pointer, which would be specific to each type of entity. This allowed us to use a form of semi-OOP, where the Entity data structure would contain the common information for what to draw and how to process the physics of the entity (when needed) but the entity would be responsible for updating itself through it's own update function, which the processFunc would point to.

In our update loop we then had to only do this:

for (int i = 0; i < MAX_ENTITIES; i++) {
    if (level->enemies[i] != NULL) {
        if (!level->enemies[i]->disabled) {
            level->enemies[i]->processFunc(level->enemies[i], delta);
        }
    }
}

The internal_data pointer contains a reference to entity-specific data, since the Entity data structure doesn't always have enough data to represent all the necessary information for an entity's behaviour. So, we could always define a new data structure for an entity and initialize the internal_data pointer so that any extra information could be accessed through it. It's almost like inheritance.

I am quite proud of this entity system and it turned out to be very powerful and we could represent arbitrarily complex entities with very few changes having to be made outside of the entity definitions.

Now that we were able to load static map data and had an entity system that was extensible and powerful, we only had to tie these things together, such that we could define which entities we wanted our maps to have. At first we considered adding proper scripting support, but it turned out that embedding a scripting language was slightly beyond the time we could afford, so we wrote what I call the “level pre-processor”.

Essentially, when we loaded a map from a folder, we had two files: map.txt and metadata.txt. The map.txt simply contained the static level geometry, whereas the metadata.txt contained various instructions for the pre-processor that told the engine where to place monsters, falling spikes, player starting location and goals. The level pre-processor only gets read and parsed once, which meant that we couldn't implement any easy scripting there and instead any complicated interactions would need to be created by implementing relevant entities and initializing them with proper values. An example of such an interaction was our Button entity, which only replaces a block at a given X,Y coordinate with empty space. On the technical side it's a very boring button, but in practice this allowed us to implement opening doors and paths, which is 90% of what buttons in games do.

Technical problems and issues

It wasn't 100% smooth sailing throughout the game jam, of course. In addition to having to reduce the scope of the game due to one of our team members being too busy with their studies, we had some troubles with the tech we decided to use for this project.

ncurses problems

The most immediate problem we noticed was related to how ncurses handles keyboard input. The curses library is old and was primarily built for applications that wait on user input. This wouldn't be an issue if our game was turn-based but since we were writing a real-time platformer game, the game world needs to carry on even if the player is indecisive.

By default the getch() function used to read a key from the keyboard will block, meaning the game would stop entirely. However, this can be remedied by setting a timeout on the key press such that it almost functions as if you were just scanning the keyboard. This still does not get us usable platformer controls, however. When getch() reads the keyboard, it does not actually scan the keyboard, it just waits for a keyboard event. The important difference here is that the program needs to wait for a keyboard event to be sent, rather than asking the keyboard what keys are currently pressed.

This means that only one key can be read at a time and continuous key presses are only detected at the rate the keyboard repeat events are sent. This is not acceptable, since the player may need to move and jump at the same time and the player character's movement speed shouldn't be tied to how fast you can mash the buttons or how fast the keyboard will relay input events to the game.

The patchwork solution I came up for this problem was to keep moving the player in the same direction until the button was pressed again or the direction needed to change. This is by no means a perfect solution and it takes some getting used to before you will get the hang of it, but it does allow the player to execute jumps while running. We eventually added in SDL2 controller input, which I was somewhat surprised to find out worked nicely even in a TTY environment. It makes sense when you think about it, but considering SDL keyboard input is tied to the window SDL manages, it was a bit of an epiphany.

C problems

The most common problems we had were linked to the inherent nature of the C language. I have nothing against C and the people writing in C, but there are definitely good reasons why languages like C++ and Rust exist. We had a number of small memory leaks, which weren't really an issue in a game that only processes a few thousand kilobytes of data per level of gameplay, but I did spend a while hunting them down simply for my own sanity.

The more destructive problems were the crashes, which arrived and disappeared mysteriously. On more than one occasion we reverted patches after one of these crash bugs came up, only to find out the patches were not at all related to the actual bug.

One particular case of these mystery bugs came up when we added moving platforms to the game. We'd had the game working perfectly fine, but suddenly on some levels the game would suddenly start running extremely slowly, rendering would be partially garbled and then the game would run into a segmentation fault in a piece of code that hadn't been modified at all. We were utterly confused and began to get quite desperate.

After a lengthy debugging session, I traced the problem down to an attribute we had added to our Entities: width. The width attribute was used by the moving platforms to allow us to make platforms of various sizes, but it was not of much use for other entities. Because other entities didn't make use of it, in one entity we forgot to set this attribute, which lead to the game attempting to render a button 100 000 tiles wide, which lead to the render loop taking extremely long to execute. This had the side-effect of causing our time delta value to grow rapidly, which meant that when moving entities would next update themselves, they would attempt to “catch up” and would simply fly out of the level, causing a read outside of allowed memory, resulting in the segmentation fault.

We ran into this problem repeatedly, until eventually I decided to initialize the Entity struct to all zeros upon allocation to avoid these kinds of problems related to uninitialized memory.

Packaging problems

Our game packaging wasn't really problem-free either. Our team uses mainly Arch Linux, which means that we cannot just compile the game and package it, since compatibility is a one-way street: you can run stuff built against older stuff on newer operating systems but you cannot run stuff built against new stuff on older operating systems.

I attempted to solve this problem by spinning up a Debian virtual machine, but Debian's old build tools were causing unexpected problems due to GCC bugs (if statement wouldn't be properly triggered without curly braces) and various other similar problems, so I passed the task of packaging over to Tuubi, who is a Linux Mint user. We had to fight for a while to get the proper libraries packaged in along with our binaries, first going with too many library dependencies (causing Arch to break due to an old libgcc.so) to too few dependencies (libsdl2.so depended on fluidsynth) but eventually through trial and error we got a decent mix of libraries together to get the game running on both Arch and Mint.

A hint to people who may want to start shipping games to Linux: the “ldd” command is your friend for figuring out what dependencies are needed by your software and you can tell your game to load them from a specified directory with the LD_LIBRARY_PATH environment variable. You're welcome.

Outcome

Download the game on Itch.io: https://samsai.itch.io/cursed-platformer

Browse the source code on Gitlab: https://gitlab.com/Samsai/linux-game-jam-2019-game

Writing the game was a fun experience and despite the problems it caused us, writing it in C was exciting and many things were learned, particularly about how to design object-like systems without formal classes and about memory management and initializing your memory before using it. My frequent trips down into GDB and Valgrind have also been an excellent learning opportunity, and I feel way more confident with those tools now than before the game jam. I may make more use of them in my other projects now as well.

Our game also seems to have been fairly well received, and in the peer ratings we were rated as 3rd in the Visual Flair category and 8th in the Overall Fun Factor category (out of 33 entries). I have yet to hear the opinions from all of the official judges but I don't expect their opinions to be drastically different from the existing ratings.

I am particularly proud of our technical execution and I would rate our code quality rather good, despite us having relatively little experience with C. I'm sure that there are still bugs lurking in there, but at the moment our game seems to be in a relatively stable state. The portability of our code is also currently being tested by Ysblokje, who intends to port our game onto his Z80-based home computer from the 1980s.

Finally, big thanks to the Linux Game Jam organizers once again for making this event happen. Particular praise goes out to Cheeseness for actively helping devs throughout the jam on the Linux Game Jam Discord chat and streaming through the submissions on Twitch for over 9 hours.

We'll see what we'll cook up for 2020!