A surreal racing experience for GMTK Jam 2025 | InsomnaKart

I made a Jak X-inspired racing game for GMTK Jam 2025

Here we go again

As part of my effort to participate in more game jams this year, I entered GMTK Game Jam 2025 immediately following Kenney Jam 2025. Unlike Kenney Jam, going in, I didn’t have a strong desire to make any particular genre; however, I did know that despite being a four day jam, I wouldn’t be able to work more than about two days due to other obligations.

Planning

When “loop” was announced as the theme, I started with a mind-map and widdled that down to just two ideas:

  • A racing game where you’re trying to hit obstacles on the track (heavily inspired by the rush hour mode of Jak X: Combat Racing)
  • An RTS with a tech tree where time resets when you die but you keep all your materials allowing you to advance more each run

Having just finished my Riverworld modding tools, I really wanted to make an RTS but decided the content and AI would be too much for me in 48 hours. The racing game seemed easier since it didn’t necessarily require AI.

Being a fan of Jak X, and the intense sense of speed it gives, I decided to mimic its controls for my game. To that end, I planned for a boost mechanic which I would encourage players to use by multiplying the score of a hit obstacle with each lap and a power-slide mechanic which would be required to navigate hairpin turns in the track. I also planned for a weapon power-up which would give the player a larger hitbox on demand; however, this was ultimately cut for time.

Final gameplay

Development

Architecture

Like Maximum Voltage, I heavily leveraged a component-based architecture for my game; however, I decided to try a different approach to components that I had seen in a YouTube video. With my previous games, each component was its own class which often led to duplicate code for components that reacted with an area in the environment. For example, Maximum Voltage’s battery pickup and intel pickup components both had logic to kill an attached health component on activation. Based on this, I decided to apply a behavior-based pattern to interactable components in this game.

I created a base InteractionComponent which would detect a collision with an area and pass the area to a custom behavior resource’s interact_with method. The base component had all the logic to disable itself on use but deferred to the custom behavior resource for what would actually happen when it was triggered. I used this system to implement checkpoints, boost pickups, and the out-of-bounds respawn. In theory, this seemed like a good idea but in practice it made the code more complex for little benefit.

The main problem with this pattern is there’s no good way to connect the custom behavior resource to other components of the parent object. For example, the out of bounds component ideally needs a reference to the player it is attached to so it can trigger a respawn but it isn’t easy to pass a reference to the player node into a resource since that resource then becomes scene-local and can only use node path references. For similar reasons, the parent object can’t easily connect to events from the behavior if the behavior was created through the UI. It’s not impossible to do with this pattern but is messier than I’m personally comfortable with.

I think this new pattern has merit in systems where the base component has references to everything the behavior could need (ex where the behavior describes a type of projectile, spell, etc) but I don’t think this pattern works as well when each behavior has dependencies on different things.

Player controllers

Movement

The player movement is largely based on the KidsCanCode Arcade Style Car but I did make a few changes:

  • Tweaked various base physics settings
  • When reversing, the steering direction is inverted
  • The maximum speed is limited using the _integrate_forces function
  • The turn speed, angular damping, and amount you can accelerate and turn is modified when power sliding
  • The maximum speed is increased when boosting and the acceleration is instantly max’d out

This worked relatively well but I did find it very difficult to tune given the number of parameters available. It also has an annoying quirk that the player will slowly slide off sloped surfaces.

Camera

Even while boosting, the player never actually moves that fast. Instead, the sense of speed comes from a few camera tricks I learned from playing Jak X during the jam.

Since the game is running in Godot’s compatibility mode, effects like camera blur aren’t available. Normally, I’d mess with the camera’s FOV to give the impression of speed but I noticed Jak X has the same limitations but a different solution. Instead, at a certain speed threshold it drops the camera lower to the track. Because the track is textured, getting closer to it makes it look like it’s moving by quicker. This effect was very easy to implement and I find to be less jarring than altering the FOV.

The next trick is having the camera pan left/right from a point in front of the car/bed while turning. I defined a curve which, based on how fast you’re going, determines how far in the opposite direction the camera will pan while turning. Since the camera pans from the front of the car, it gives the effect that you’re drifting more than you actually are and gives a sense of weight to the car.

Checkpoints

Based on how I’ve heard other racing games work, I implemented a checkpoint system to determine whether the player legitimately completed a lap. Along the track are Area3Ds which you must pass through in sequence to activate the next. A checkpoint will not register if it is not active and will not become active unless the previous checkpoint was passed. Passing the last checkpoint triggers a signal that a lap was completed and starts the sequence over.

The same checkpoints are also used to trigger the enemy spawners located along the track. This was meant to help pace the enemy spawns but didn’t work so well in practice.

This system was incredibly simple to implement and would have served to help with player respawns if I had time to finish that system (as-is, the player always respawns at the start of the lap). I definitely want to explore this system more in future games!

Each rectangle is a checkpoint and the spheres are enemy spawners

Design pivot

Saturday afternoon, I had all the gameplay features implemented and asked my friend James to help with beta testing. Right away, I realized there was a problem when I had to explain that your goal was to hit the memories rather than dodge them. There was no obvious connection to the theme to guide the player to that objective nor did I have time to make a cutscene.

As James played, he noted that he was having difficulty aiming for the memories. At the same time, I was playing and noticed I was gravitating towards dodging memories to get higher lap multipliers rather than trying to hit them. At that moment, I realized the core concept was flawed and that it would be more fun and better aligned with the narrative to have the player dodge the memories.

Thankfully, the component architecture made it easy to move scoring to the boost pickups and within a couple of minutes I had a new build which was much more fun to play. Unfortunately, I think I was still too hung-up on the scoring system to recognize that I should have made other gameplay changes but I’ll save that discussion for the post-mortem section.

Time allocation

Unlike Kenney Jam 2025, I didn’t bother putting together a schedule since I wasn’t sure exactly how much time I’d have to work on it; however, I did track my time throughout the jam for those curious.

Expand devlog
  • Wednesday
    • 12:15: Project setup
    • 12:25: Theme planning
    • 13:48: Break
    • 19:03: Planning
    • 19:11: Player controller
    • 20:04: Learning how to import/retarget animations
    • 20:50: Sleep
  • Thursday
    • 21:00: Camera controller
    • 23:02: Finding music
  • Friday
    • 00:12: Checkpoint component
    • 02:00: Interaction component
    • 02:30: Player controller
    • 03:30: Sleep
    • 19:10: Game events
    • 20:00: Break
    • 23:00: Pushable component
    • 23:20: Health/death components
    • 23:30: Score component
    • 23:36: Game timer
  • Saturday
    • 00:00: Enemy animation
    • 00:14: Break
    • 01:20: High score system
    • 02:00: Sleep
    • 10:00: Boost & game feel
    • 10:56: Boost pickup
    • 11:45: Lunch
    • 12:00: Boost pickups
    • 12:31: Track
    • 13:08: Break
    • 13:45: Enemy AI
    • 13:50: Break
    • 15:10: Enemy spawner
    • 15:30: Game feel
    • 17:53: Beta testing
    • 19:53: Polish visuals
    • 21:48: Break
    • 23:00: Menus
  • Sunday
    • 03:00: Audio
    • 03:59: Visual effects
    • 05:00: Bug fix
    • 05:15: Break
    • 06:09: Visual effects
    • 08:00: Polish
    • 09:39: Jam page
    • 11:28: Done
Expand time summary
TaskDetailsMinutes
PlanningIdeation83
PlanningSchedule8
Planning Total91
Project SetupGodot setup10
Project SetupAnimation import workflow46
Project Setup Total56
Code CodeHealth/death components10
Code CodeGame events50
Code CodeHigh score system40
Core Code Total100
MechanicsCamera controller170
MechanicsCheckpoint component108
MechanicsInteraction component30
MechanicsPlayer controller113
MechanicsPushable component20
MechanicsScore component6
MechanicsGame timer24
MechanicsBoost pickup136
MechanicsEnemy AI5
MechanicsEnemy spawner20
Mechanics Total632
Game Loop BookendsMenus240
Game Loop Bookends Total240
Level DesignLevel design37
Level Design Total37
Beta TestingBeta testing120
Beta Testing Total120
Bug FixesBug fix15
Bug Fixes Total15
BalanceGame feel143
Balance Total143
AudioAudio59
AudioFinding music22
Audio Total81
PolishGeneral polish99
PolishVisual polish301
Polish Total400
Jam SubmissionItch page & Submission109
Jam Submission Total109
BreakBreak819
BreakSleep2870
Break Total3689
Total5713
Dev Total2024 (35% of allotted time)

Tellingly, I managed to spend 5 fewer hours working on this game than my submission for Kenney Jam 2025 despite having double the time limit.

Post-mortem

Results

Of the 9,643 final entries and 197k ratings, I received 25 ratings and the following ranks in each category:

CategoryRankRating
Narrative#15023.00 / 5.00
Enjoyment#36672.96 / 5.00
Creativity#38753.28 / 5.00
Audio#44612.64 / 5.00
Artwork#71592.16 / 5.00

I actually didn’t think my narrative was all that clear this jam so I’m incredibly shocked that I ranked in the top 16% for that category! Despite not ranking that high in enjoyment or creativity, I am also really happy to see that my ratings weren’t too bad.

The most common feedback I got was that people liked the humor/narrative and thought the game was fun; however, many felt the steering and braking weren’t responsive enough and that it was too hard/unfair to dodge the memories. Based on the rankings, I can also see that audio and visuals were not well received which isn’t surprising given I didn’t leave myself enough time at the end to polish them.

You can play IsomnaKart on Itch.io but I recommend some of my favorite submissions instead:

Observations

Being the first time I’ve taken place in the GMTK jam, I thought I’d take some notes about how it differs from the other jams:

  • Nearly every category winner was download-only - my previous experience has been that downloadable games don’t do as well
  • Only half of the category winners were made by teams
  • The level of polish for winning games was nearly the same as commercial releases
  • Most people were not, or at least did not appear to be, using pre-made assets
  • Most games were much longer than 3-5 minutes - some even taking half an hour or more to complete

Pros and cons

Pros

  • Recognized the original concept wasn’t engaging and pivoted to what felt fun
  • Slide/boosting around corners and off edges is extremely satisfying
  • Love how the brain pickups look
  • The Godot Road Generator plugin worked flawlessly
  • Audio levels were correct this time
  • I think having a gif on the jam page helped explain the gameplay

Cons

  • Game was severely underpolished even by 48 hour jam standards
  • The intro cutscene is awkward and jank
  • Connection from the theme to the gameplay isn’t great
  • Impossible to dodge without holding boost
  • The way I intended the game to be played isn’t obvious to everyone (ie holding boost all the time)
  • Random spawns meant not everyone had a good first experience and made the skill ceiling low as scoring high is purely random
  • There isn’t much score variation between runs making it feel like you can’t improve
  • Using an in-editor thumbnail for the submission page just doesn’t look that good
  • I spent way too much time taking breaks

What I would do different

I believe I was right to pivot the game idea when I did but in hindsight I don’t think I pivoted enough.

When the game was based on hitting the memories, the heavier steering, random enemies, and score-based gameplay made more sense since you wanted to hit as many as possible. When changing the game to avoid memories, I should have also made the steering more responsive and the enemy spawns more predictable so dodging was skill-based.

I also think it was a mistake to stick to score-based gameplay. It would have fit better with the narrative to have the game be a time-trial where you’re trying to complete x laps as fast as possible. I could have also changed the timer to count up from midnight to the morning to represent your character struggling to fall asleep.

Artistically, I really want to make games that look and feel like they were released for the PS2 which is why I used one of the PS2’s resolutions for this project; however, the pre-made assets I was using just don’t look as good at that resolution without more advanced visual effects like tilt-shift and dynamic lighting. This is something I already knew but neglected for this jam. Until I can make my own PS2-style assets, I should probably stick to more retro-inspired theming since it works better with the simpler assets I have access to.

With the jam page, I want to get away from using in-editor screenshots for the thumbnail. It just doesn’t look as polished as what others are producing and I beleive it’s limiting how many people choose to play my games.

All that said, I’ve got other projects I want to spend my time on so I won’t be updating InsomnaKart but I will try to remember this for the next jam.