# Making a game in C for the Global Game Jam: Inside Job At the beginning of this year I participated [once more]("ggj_25.md.html") in the global game jam. In this post I'll talk about how my team and I designed a local multiplayer investigation game, how I implemented a record-replay mechanic, the strange platform bugs we hit along the way, and how we still managed to make a game we're proud of, using my engine written in C. ![]("res/ggj_26_title.png") ## Brainstorm This year I teamed up with 3 other people: Sandro, a [game designer from the swiss scene]("https://www.sandrodallaglio.ch/"), Camille, a [2d animator](https://camillebovey.com) (incidentally my girlfriend!), and David [a composer and sound effect designer]("https://www.instagram.com/david_dg_music/") (incidentally my cousin!). I was handling the programming side, using my C game engine as usual. At 17:00, the theme was revealed: **Mask**. We started brainstorming right away. In my experience, this phase of a game jam is rarely easy, but often quite exciting. We all suggested ideas, bounced ideas off one another, spotted problems, proposed solutions... We also shared our own preferences in terms of games, bringing up various references as we brainstormed. ![the team brainstorming]("res/ggj_26_brainstorm.png" width=90%) It can be quite hard to know when to stop. Often a couple of ideas stand out, but it's tricky to get a sense of whether a design will actually hold water once implemented. The best way to know is to prototype, but the time constraint makes this impracticable. On the other hand, we want to start working on an idea everybody is onboard with and that has a good chance of working. Pretty early we had the thematic idea of the bal masqué. We figured we could take inspiration from cluedo, and have a house with multiples rooms. "Mask" could have the double meaning of masking information to some players. The game could be multiplayer, with one imposter that has a secret goal (e.g. kill someone?). But depending on in which room the various players are standing at various points, they would see different information and would have to piece it together (with the imposter potentially lying to throw off their opponents). We explored this idea a bit, and came up with a bunch of design ideas, but in the end we had one problem: all of these ideas felt more like board games than digital games. Although we all like board games, we wanted to go for and idea that made sense as a digital game and couldn't be done purely physically. At some point in the brainstorm, I showed a prototype or a survival game I had been working on. In it, you play a pixelized astronaut that explores an abandoned space station, riddled with monsters. A particularity of that prototype is that there is a visibility system, where parts of the world that are not visible to the character are darkened, like a shadow. This immediately resonated with Sandro, who could see our murder mystery idea working in this context. ![my survival game prototype, used as a basis for our jam game]("res/ggj_26_survival.png" width=90%) Here's the design we came up with: The game plays with two players, locally on the same machine. __Phase 1:__ One player plays as the inspector: they covers their eyes and "open their ears". The other person plays as the thief: they takes the controller and move a corrupted guard around. They have a limited amount of time to steal as many art exhibits as possible, preferably without getting spotted by other guards. While the thief moves around, they make noise, potentially providing clues to the inspector. They walk on different kind of floors, and have to break the glass of art exhibit displays. When the time runs out, they have to blend in and position themselves as to pretend to be one of the guards. __Phase 2:__ Now the inspector takes the controller. They must investigate, try to retrace the steps of the thief and arrest them. On top of the auditory clues they got in phase 1, they can "interrogate" any guard which would then tell them what they saw. We would realize that as a "replay" of what the thief did, form the point of view of the interrogated guard. If the inspector interrogates the thief (who is also a guard), they would say they saw nothing. With enough information gathered, the thief might be able to tell that one specific guard is lying and arrest them! ## "Proof of concept" prototype At this point everybody was mostly convinced, and we figured we could quickly put together a prototype using my space survival game as a basis, so we just got started. Sandro made a level in the simple level editor that I had made for the survival game, I slapped a countdown display in the top right, and we were already playtesting to validate our design. Instead of implementing the snapshot feature right away (which would have been quite time consuming), we had a third person assist the playtest and take literal screenshots every time the thief entered the line of sight of a guard. I thought that was a nice way to quickly test out our design with the tools at hand! ![Our cobbled-together prototype]("res/ggj_26_proto.webp" width=60%) ## The technical difficulties begin I had just finished moving the code from my space game to the jam project I had setup beforehand, and it was time for Sandro to start iterating on the level and game design. Unfortunately, to my surprise, the game wouldn't launch on Sandro's windows laptop, crashing at launch. I tried to remain calm, and started investigating. From the stacktrace, it seemed that a texture couldn't be created by sokol. Since I couldn't reproduce locally on my mac (yep, I'm sure you can see where this is going...), I added some logging, made another build and had Sandro run it. The function `ID3D11Device::CreateTexture2D` was returning the error code for `INVALID_ARG` when creating a texture destined to be rendered into, for the lighting effect. I started to scrutinize the code around the texture creation, logged more values to try to understand the problem, checked every argument — hoping I would see something obviously wrong like corrupted memory. But nothing jumped to my eyes. What was troubling was that it I worked perfectly on my mac, with the Metal api. Even more so, I remembered the space game prototype running just as well on windows, with DirectX 11. So I had to somehow have introduced a mistake when moving the code over to the jam project. I was quite intrigued, and wanted to investigate more, but going back and forth between my computer and Sandro's was starting to get frustrating for both of us. Then I remembered that I had parallels installed and could run the game on windows, with the Visual Studio debugger attached. I crossed my fingers for the bug to reproduce in that environment. And it did! Sadly, even with that setup, I didn't manage to get more info. All the debugger gave me is the same INVALID_ARG error code. At that point, wanting to move on to prototyping the game, I gave up. But after the jam, with a clear head and less time pressure, I was able to pinpoint the issue. All I had to do was to run the binary through Visual Studio with the D3D11 validation layer enabled, and look at the Output pane (as opposed to stdout like I had been doing). > `D3D11 ERROR: ID3D11Device::CreateTexture2D: D3D11_BIND_RENDER_TARGET and D3D11_BIND_DEPTH_STENCIL can't both be used together. [ STATE_CREATION ERROR #99: CREATETEXTURE2D_INVALIDBINDFLAGS]` > Whoops... I was indeed telling sokol to create a texture that could be bound as a render target and a depth stencil. It wasn't an issue on Metal and webGL, but failed on D3D11. ```c sg_image_desc img_desc = { .usage = { .color_attachment = true }, .width = 256, .height = 256, .pixel_format = OFFSCREEN_PIXEL_FORMAT, .sample_count = OFFSCREEN_SAMPLE_COUNT, .label = "color-image" }; sg_image color_img = sg_make_image(&img_desc); img_desc.pixel_format = SG_PIXELFORMAT_DEPTH; img_desc.usage.depth_stencil_attachment = true; // <---- my mistake here, not initializing the entire usage struct and leaving .color_attachment to true. img_desc.label = "depth-image"; sg_image depth_img = sg_make_image(&img_desc); ``` Since then I contributed a [small PR](https://github.com/floooh/sokol/pull/1475) to sokol so it asserts if you try to do this, hopefully that saves someone some time in the future. ## ...and continue Since I couldn't solve that bug quickly in the heat of the jam, Sandro said he would go to his house and back to get his mac laptop. This was a windows issue after all. When he was back he booted the game without problems, as expected. Unfortunately, and to my great surprise, his mouse position in the editor started lagging widely after a few seconds. The framerate of the game was fine, but his apparent mouse position was delayed by an unpredictable and sometimes huge amount of time. At this point I was quite embarrassed. I couldn't reproduce the issue on my own mac at all. I was running a version of macos that was a few major version behind, whereas Sandro had updated to the latest, so I figured this was the problem. Desperate to unblock Sandro, and not wanting to waste time fighting obscure platform bugs, we took David's car and went to my office to get a mac mini with a macos version old enough to not exhibit the bug, and setup Sandro to work on it. Problem sidestepped at last! After the jam I figured it must have been this problem introduced in macOS 26 (Taohe): https://github.com/floooh/sokol/issues/1344 The issue mentions high polling-frequency mice, and Sandro had a mouse that looked like it could be high frequency (I should check with him). I should also check whether the mitigations discussed in the github issue actually fixed the our problem in practice. ## The game in the making Sandro made a nice concept in the form of a flowchart with screen mockups. It was a good way to make the design more concrete and answer some questions preemptively. ![Flowchart and screen mockups]("res/ggj_26_flow.jpg" width=100%) He also designed multiple levels, which we playtested at a few points during the jam. We kept the best 3 for the jam submission. David made some sounds effects for the guards, some footstep sounds on different floors, and two music tracks. One nice touch with the music that plays when the inspector is investigating is that the track gets more intense near the end, and stops right at the moment when the timer runs out. Camille drew a new (high definition) tileset to replace the one from my space game. Then she worked on visual designs for the two character and made their animations. ![Camille animating]("res/ggj_26_camille_animating.jpg.webp" width=50%)
The finished cop and thief animations.
Finally, she made the screens Sandro had conceived into proper illustrations. ## Record & Replay One of the core features of the game design is that when interrogating a guard, you see a "recording" of what that guard has seen.
The replay mechanic in action.
When I sat down to implement this, my first thought was to leverage the replay feature built-in my engine. This feature allows to rewind time and show the game as it was N frames ago, then replay what happened through recorded inputs. After thinking about it a bit, it became clear that this wouldn't work out of the box: - By default the replay system targets the whole game state. In the case of our game, we'd still need some state to persist independent of the replays, such as the round timer. - During the replay, we want to show the point of view of the guard instead of the player (the thief). - The engine's replay system replays playing inputs. That works fine to replay the entire game, but in our case it might be awkward: what if they open the pause menu for example? Maybe if I changed the architecture and API of the engine's replay system, I could make it useful to implement game mechanics. Providing the basic bricks to record pieces of state, along with specific inputs needed to replay how they change over time should be doable. What I ended up doing was much simpler. Every frame where a guard sees the player, they take a "snapshot" of the whole entity array. The entity array happens to contains every piece of state that needs to be replayed: the player position and facing direction, the art exhibits and their "stolen" state (intact or stolen), the state of other guards (_in alert_ or not)... Then, in the investigation phase, when the inspector interrogates a guard, I just replay these snapshotted frames one by one. I achieved that by separating the simulation of the game logic and the drawing of the scene. Conceptually, drawing the scene becomes a "pure function" that only takes the entities snapshot and which entity should be used as a camera target as arguments. ```c void game_tick(void) { // Simulate the game: player movement, interaction with guards etc } void draw_entities(int viewer_entity, entity_t* entities, int entities_count) { // Draw the scene using the passed-in entities as the only state, with // viewer_entity as the target for the "camera" } ``` Now all I have to do to replay a particular recording is to set `game_state->current_recording` to a recording index, and the game will draw that recording frame instead the "current" game state. These recordings could be stored on the entity that recorded them, but since I didn't need these recordings to be themselves recorded by other entities, I choose to put them all in an array in the game state. Each entity which has a recoding then stores an index into that array. An advantage to having implemented the game's replay feature separately from the engine replay feature is that I could then replay the replay. E.g. use the engine feature replay part of the game where the game was itself showing recorded scene. It actually came in handy post-jam to debug a visual issue with the animation in replays. Down the line, I think I'd like to expose enough of the machinery of the engine replay system so it can be used independently by games and tools, on the data of their choice. That way, more complex replays as game mechanics could be easily archivable. Note that by recording all frames and simply playing them back in order, I make the assumption that the fps doesn't change drastically, which proved not to be a problem since I use smoothed frame times and always render at the screen refresh rate. For a proper game release, a more robust solution could be to record at a fixed rate and interpolate between snapshots. Clearly not needed for a game jam though! ## The result ![The team at the end of the jam]("res/ggj_26_team_1.jpg" width=50%) In the end we had a functional game that we were proud of, and we were glad to see that people were having fun playing it. I'm glad that we made a two players game, as I find the social aspect of one player having to playfully deceive the other quite interesting. Don't hesitate to try it with your friend / partner / kid / coworker / passerby ! ## Takeaways During this jam, several engine bugs slowed us considerably. This was a humbling experience: I realise that these kind of problems would probably not have happened if we had used an off-the-shelf engine like Unity or Godot. There were points in the jam where these bugs were completely blocking team members from working, which felt was quite stressful. Thankfully, in this jam, we were able to either solve or work around every problem, and the end result was — imo — really good, so it eclipsed the doubts in my mind. But if we hadn't managed to make a good game, I would probably have felt quite bad at the end. All in all I think I was quite unlucky to hit these bugs all at once, and that on average the experience of using my engine is much smoother (e.g. in 2025, it went much smoother in terms of engine bugs). Also, these bugs are now fixed, and I now understand some things better and would be able to debug similar problems more efficiently. ## Appendix: Stencil out A gamedev friend, [Eike](https://bsky.app/profile/zet23t.bsky.social), was up to try my engine for that jam. He started earlier in the week, which allowed me to help him set up and fix some platform-specific bugs he stumbled upon. I'm grateful that he gave the engine a go despite knowing there would be problems. Even if his game was pretty self-contained, by the end of the jam I had a list of things to improve. I think the biggest thing that's missing right now — besides many fixes all around — are some good samples that demonstrate how to use the various APIs and features of the engine. I've got to work on that soon. The game he ended up making is arguably more of a toy: > An experimental spray-can like painting app where you use stencils to create paintings. > > The project was the result of working on the Global Gamejam 2026. The topic was: Mask. > > I originally intended to make this a puzzle game but got hang up on the spray can mechanic as I found it too much fun to just paint stuff. ![Eike's game: Stencil Out]("res/ggj_26_stencil_out.jpg" width=80%) Try Eike's game here!

Comment on this post on Bluesky