#StandWithUkraine

Russian Aggression Must Stop


Linux Game Jam 2022: Retrospective

2022/05/01

Tags: linux gaming programming game dev

Another year, another Linux Game Jam (apart from the years it didn't happen, shush). Naturally, our crack team of developers (thanks once again, Tuubi and Ysblokje, and also Gurnu who was responsible for creating music for us) decided to take on the challenge of creating a whole new Linux game within the ~10 day allotment.

The fruit of our labour over the jam was BEAT NOID, an Arkanoid-like with tracker music integration and gameplay elements that reacted to the music: /img/lgj2022/beat-noid.png

You can find BEAT NOID on my Itch.io page at: https://samsai.itch.io/beat-noid

In this post I'll go over some of our experiences, the process, what went right and what went wrong.

Coming up with the idea

We came up with the general gist of the thing we wanted to work on already before the game jam and settled on it after having a look at the optional prompts for the game jam.

We often draw inspiration from retro aesthetics and technologies (on the past two Linux Game Jams we went with very low-tech ASCII for display), and this time we landed on tracker/mod music as an element we wanted to explore. So, we decided to use Arkanoid/Breakout as our base gameplay foundation and make it somehow reactive to a mod track playing on the background. We debated ideas of revealing notes by destroying blocks or getting bonus points by hitting blocks that were "playing".

Ultimately we had to scale back many of our ideas, as is customary for game jams. But the background concept carried on through the game jam and major changes in direction were not necessary.

Implementation technology

The technology we've used in game jams has changed every single time. In the first Linux Game Jam back in 2017 we went with Godot Engine. In 2018 we made a simple text adventure in Python. In 2019 we made an ncurses-based platformer game with ASCII art.

We considered using ASCII technology, perhaps on top of C++ for this year, but eventually settled on using Rust, much to my delight. We also considered whether we should pull off another ASCII game, but decided to instead take another technological leap and go for low-res, but not really pixel-art, graphics instead.

Since I had most experience in the Rust game development ecosystem, the responsibility for picking the libraries fell on me. I've used SDL2's Rust bindings in the past and also ggez.rs, so I debated using those for this project. However, the SDL2 bindings, as great as they are otherwise, leave us with an external dependency on SDL2 and the API requires a bit more lifetime trickery than I wanted to deal with, especially with two relative newcomers to Rust. I also decided to pass on ggez.rs after finding it somewhat lacking in another game dev project.

So, the next big options were Bevy Engine and Macroquad. I've found Bevy Engine a very interesting project for a while, but learning to wield a whole game engine while trying to make a game in 10 days seemed a risky choice, especially since the project is still in fairly early state and documentation is a bit sparse.

Macroquad on the other hand seemed like an excellent option. It's API is designed specifically to avoid difficulties with the borrow checker and provides simple, but comprehensive functionality for building 2D games. Its ability to also target Linux, Windows and web with ease was also quite appealing.

Although I originally found ECS to be an overkill for a simple project like this one, Ysblokje wanted to pull in an ECS dependency. Historically the popular options for ECS have been Specs and Legion, but after the Amethyst Foundation went quiet, both of these projects seem to have gone unmaintained. The ECS from the Bevy Engine project is available as a separate module and seems quite similar to Legion. However, there was also hecs, which is a minimalist ECS library and one that I haven't tried before. After a quick browse through the documentation, I figured it would work for us, since our game was unlikely to need complex schedulers and simplicity of API would serve us better.

For music and audio we went with mod_player for decoding mod files and Kira for handling playback of audio, since Macroquad does not grant us ability to feed information from mod_player to the audio buffers.

Get jammin'

With an idea and most of the tech stack figured out early, we hit the road running and were able to hack together a basic clone of Arkanoid mechanics by around the second day of the jam. Tuubi was able to create the initial art assets and incorporate them into the game very quickly as well.

During the week days I had to balance worklife and late meetings with the jam, so my contributions ended up coming in bursts of gameplay mechanic implementations and an otherwise passive consultation role for when Rust-specific architectural decisions needed to be made and concurrent constructs built.

Luckily between my sporadic activity in the evenings, Tuubi and Ysblokje were able to work on audio functionality and Gurnu could focus on creating music. Towards the end of the week, the music was ready and the foundations for the music-gameplay interactions had been laid out. By the end of Friday we had a small but workable game. There was a 24 hour extension given, which we spent improving small things here and there and coming up with more levels for the game, ending up with 11 levels total.

Observations from the game jam

Our tech stack was very good for jamming/prototyping

When people talk about Rust, usually rapid prototyping isn't something that comes up. However, I think in our case the tech stack we settled on was a real boon to productivity.

Although Rust issues did pop up among our two relative newcomers and some fights were had with the borrow checker, these were not too numerous and my experience with Rust helped us navigate through these problems relatively easily. However, I would also argue that in these cases the Rust compiler prevented us from making a few issues that would have cropped up as difficult-to-debug bugs. Helped by Rust's strong type system, every time the game compiled, we basically never encountered severe bugs, except for minor, non-fatal issues.

Although I thought about ECS not being necessary, I think going with an ECS helped us even with this game. It helped us build the game piece-by-piece by focusing on the data needed to represent a particular concept and then building isolated functions to execute the necessary changes on them. And although Arkanoid is not a very complex game, we eventually started adding concepts like particle emitters and variations on blocks that benefited from the versatility.

Macroquad was also a real hero in this story. The API it exposes is so simple, that hacking together a game with it was a real pleasure and I really want to use it for other projects as well. Unlike with the SDL2 bindings, which require you to occasionally consider lifetimes for things like textures, Macroquad hides lifetimes just about completely and the only architectural limitation is that it requires the use of async functions. Inside async-colored functions we were able to do just about what we pleased without having to worry about architecting our game to match what Macroquad expects, although carrying data between the async boundary was somewhat tricky.

Games consist of different states, account for it early

Potentially the biggest architectural failing of our project was that we let the game stay in a single-state mode for far too long and only accounted for state transitions late in the project.

A game, even a simple one, has multiple states that it can be in, where different gameplay elements and reactions to input happen. You have your start screens, main menus, options menus, variations on the state where gameplay takes place and victory and game over screens. You need the correct parts of your game to react properly to being in a particular state and you need the game to smoothly transition from one state to the next.

During most of our jam, the game just consisted of a loop where we ran all the gameplay related systems and essentially all state lived in top-level variable declarations.

When it became time to represent the game overs and start screens, I essentially just copied the state automata pattern I've used previously:

  pub struct StateManager {
      states: Vec<Box<dyn State>>
  }

  pub enum StateTransition {
      NewState(Box<dyn State>),
      ReplaceState(Box<dyn State>),
      PopState
  }

  pub trait State {
      /// Initialize the state, called when the state gets added to the StateManager
      fn init(&mut self, world: &mut World, context: &mut Context);

      /// Update the world and the state based on given input, optionally modifying the state automata
      fn update(&mut self, world: &mut World, context: &mut Context, delta: f32) -> Option<StateTransition>;

      /// Draw the state based on the state of the world
      fn draw(&mut self, world: &World, context: &Context);
  }

However, now the problem became that a lot of state lived in the top-level declarations and come concurrent systems were also dependent on this data. This made moving that data into the Context object very annoying certain variables were referenced all over the place.

For example, our game in its current state needs to initialize the first level twice, first to initialize the audio system with the music and then a second time to load the entities into the ECS world for gameplay and drawing.

Had we created the StateManager abstraction earlier, we would have accounted for this early and not after creating tight couplings to top-level variable declarations living in main.

Balancing work and jamming can be tricky, but doable

I ended up taking maybe a bit more stress from the jam in terms of work-life balance than I probably should have. This was the first time I had a deadline on a project that I was doing concurrently to regular work and it definitely sapped a bit of my energy.

However, thinking back, I don't think I was at any point really short on time. Putting even an hour into the game jam project every evening, if done efficiently, keeps the project moving towards the goal. My use of time wasn't necessarily very efficient and I think that's partially because of stress. Next time I'll need to remind myself that it's a.) not that serious and b.) more efficient to work without stressing too much.

Conclusions

I am very proud of the game we managed to make for Linux Game Jam 2022 and happy to have participated once again. We will probably also update the game after the jam rating period is over and implement a few missing features, such as sorting out a web build. The jam also introduced me to Macroquad and hecs, both of which I hope to use in the future.

It's also very cool to see the high turnout for the jam. I haven't yet tried out the other submissions, but I plan to go through at least some of the more interesting ones over the next week.

By the way, you can find all of the submissions here: https://itch.io/jam/linux-game-jam-2022/entries

As always, thanks to the people that collaborated with me to make BEAT NOID, thanks to Cheeseness for organizing the Linux Game Jam again and congratulations to all that participated in the jam, even if you didn't manage to submit anything this time around.

>> Home