June 05, 2021

Chuck Yeager's Jet Janitor

my Ludum Dare 48 game

The theme for Ludum Dare 48 was "Deeper and deeper". I created a dogfighting game for the compo—solo, from scratch, in 48 hours—where your goal is to deliver jet planes deeper and deeper into cold storage. It's a mashup of Luftrausers and… shuffleboard.

You can play Chuck Yeager's Jet Janitor at https://jet-janitor.sartak.org or right here! (If you're using Safari some features might not work consistently, so switching browsers might help)


I'm really pleased with how it came out! It's fun to play, has a fresh game mechanic that actually works, and is definitely my kind of quirky. I'm not disappointed about any obvious weak spots. Here's what the game looks like:

Development

  • 2000 lines of pure JavaScript using Phaser
  • 34 pieces of art in Aseprite on iPad with Pencil, using AdamCYounis’s Apollo palette
  • 14 sound effects in Chiptone
  • 4 songs in Abundant
  • 101 todo items checked off in OmniFocus
  • 400,149 keystrokes
  • 3 Twitch streams totalling 31 hours (the vods are 28GB)
  • 10,881 screenshots (13GB) for the time lapse

Time Tracking

I was rigorous about my time tracking. In preparation for this jam, I'd enhanced my progress timer to track particular activities. I had this on-screen the whole time:

Whenever I shifted my attention, I hit a hotkey and that activity's timer would start ticking. (I forgot a few times, but it was easy enough to notice, make a note, and fix after the jam using the Twitch vod.) All this time tracking was carefully logged, so I can show exactly how I spent every single minute of the compo. (Try hovering over the legend!)

Here's a different way to visualize the same data:

Stare at it long enough and you can figure out my game jam strategy. Immediately start coding until the mechanics are all done, pausing only occasionally to think about what I want the game to be. Then, once everything is basically code-complete, add sound and music, then finish up with art. That frontloads my strengths; for me, code is a lot more malleable than pixels. I'd hate to have to throw away art I didn't end up using, but I'm not particularly precious about code or audio.

Art

In fact, my single goal for this jam was to spend at least 6 hours on art. That's one of the reasons I was rigorous about time tracking. In my previous game jam's post-mortem I'd said "The art is just bad. I really need to work on this. … I hope I will be able to report progress [next jam]." This wasn't hyperbole, here's the entire art set from the previous game:

And so in preparation for this jam I invest time on improving my pixel art skills. I practiced a lot, read a couple books, and watched a bunch of videos and Twitch streams. I also captured some pixel artist attention when I tweeted this about using Aseprite on an iPad via Astropad (an extremely slick app that makes the iPad + Pencil a drawing tablet mirroring your computer):

aseprite on an ipad is siiiick. thanks @davidcapello and @astropad!!

I wouldn't say I made huge leaps and bounds, but I can see improvement in my silhouetting and shading. This time, unlike last jam, I felt like the art didn't drag the rest of the game down:

Not to mention the title art—which was a first for me! This was the very last part of the game I added and I ran out of time; I do wish I'd had another thirty minutes to polish up the plane, airfield, etc.

All told, I did end up just barely hitting my goal of 6 hours on art. I look forward to deliberate practice and continued improvement here!

Code

Phaser's "Arcade" physics did much of the heavy lifting for me, applying velocities, accelerations, rotations, collision detections, etc.

I did struggle way too much with mapping between degrees and radians, and having to offset them. Math.sin() uses radians, of course, but player.body.rotation uses degrees and is oriented a different way. Ugh.

I still don't know why I had to initialize afterburners for the turrets since they are stationary… (that comment is really there in the code)

level.turrets.forEach((turret) => {
  // ??
  turret.afterburnerCooldown = -100000;
});

Phaser has a built-in system for having the camera follow an object, but if you change the target you're following (e.g. due to the player selecting a different plane), the camera snaps to that target, rather than panning. It was pretty jarring, so I quickly banged out my own version of camera-follow that always lerps and never snaps. I also took that opportunity to nudge the camera to focus slightly in front of the player. I don't know if anyone consciously noticed that, but it does make a difference. The easiest way to see the effect is when you rotate and fall without thrusting; the camera waggles around helplessly.

Another place I struggled was I wanted to bounce the player away from a wall to avoid letting them take too much damage. By then I was too sleep deprived to figure out that trigonometry.

The shuffleboard mechanics meant occasionally planes would be pushed up out of the goal area. Clearly that would just not do. So, I hacked in some extra gravity which applies to every "finished" plane near the top of the goal. That way it became nearly impossible to knock anything back out of the goal. It did make a big improvement for the game mechanics' sake, but one player picked up on that occasionally feeling unnatural.

I frankly don't understand how drag works.

Design

When the jam started, I immediately started banging out some side-scrolling platformer code. About an hour in, I realized I wanted to make the game about flying rather than jumping. I've always really enjoyed Luftrausers, so I knew I'd use its control scheme.

Initially I tried to make the game a bit too much like Luftrausers; as in, a real dogfighting game with enemy planes coming after you. Designing an AI that could use just thrust and rotation effectively and convincingly, especially in enclosed spaces, turned out to be burning way too much time for a 48-hour jam. (A little bit of preproduction to figure that out would have went a long way!). So, I went to back to the drawing board by throwing my hands up and going to bed. This frustration was the only low point of the jam for me.

Subconsciously working on the game in my sleep helped me finally figure out how to work the theme into the game: have the planer fly the plane deeper and deeper into some kind of goal area. Ditch the enemy AI and have the player cycle through those planes too, and voilà, we get shuffleboard mechanics for free.

Attach the score/win condition to the theme and, blammo, we have a full on Ludum Dare game! Day 2 definitely starting strong!

Although… a bunch of players raised eyebrows at the whole conceit of the game, one even demanding to know the lore underlying the mechanics. But I'm gonna vigorously wave the "48 hours" flag here. It's meant to be silly.

There was also some debate about what it is exactly that you're dropping the planes into. The in-game text merely hints at "deep cold storage". Is it water, or some kind of a tractor beam, or stasis goo? According to canon (as in, the identifier that I happened to use in the code), it is… jelly.

The name of the game is a play on a few late 80s, early 90s games: Chuck Yeager's Advanced Flight Trainer and Chuck Yeager's Air Combat. I don't know how many players picked up on that, but it sure amused me. Especially the absurd incongruity of a figure like Mr. Yeager with… "jet janitor". The first iteration of the name was simply "Trash Pilot". But later I remembered my wife and I joking about being "Excel janitors" at work. So there you go. Also, I didn't think to check this until after the jam, but sure enough, Wikipedia has an image of Chuck Yeager's signature, and I completely whiffed on it.

Audio

I used my old friends ChipTone and Abundant for sound effects and music respectively.

The sound effects weren't anything special. They're forgettable, but, as always, even mediocre sounds add a lot to the game feel. So "forgettable" is probably pretty good for a game jam, to be honest. I did my typical thing of morphing out a few variations for the sounds that play a lot (like firing missiles). I spent a long time trying to get the cash register chime to sound anything but extremely annoying; I think the one player who mentioned it ended up liking it.

I'm very happy with how the music came out. Every jam I get a bit better at all my tools and Abundant is no exception. This time I was able to limit the instruments to just drum and bass, which made for some very thematic 90s dogfighting game music. The intro cutscene matched up surprisingly well with the music, entirely out of coincidence. (Which is great, because if you listen to the whole thing in the timelapse below, the song falls apart soon after the intro cuts it short). The main level music slaps. Great work, Abundant. You and I have come a long way since "Did you use a compilation of 'game over' songs?"

Timelapse

Here's a timelapse of the whole development process:

Players

Some of my favorite quotes from players. I'm really pleased with the recurring themes of charming, satisfying, and addictive.

  • "Incredibly charming. The music bops, and the game is actually designed pretty well."
  • "I liked the audio! Good art, it felt personal."
  • "The art was nice and charming, and the concept + backstory was great."
  • "Man it was satisfying diving into the blue stuff. The abject chaos was pretty fun. It’s nice to play a game like this, the world needs more."
  • "The effects really help sell this game. Makes things really satisfying."
  • "The game is addicting."
  • "Fantastic game!"
  • "This was fun! I enjoyed the music and the mood. The game just felt nice to play."

Most people I watched stream the game got pleasantly surprised when they re-entered a finished level and discovered the planes they'd already dropped were still around. I did have to add that intentionally, but I certainly didn't expect it would get much reaction. I suppose it's a strong enough betrayal of the common trope of clean-slate levels. Maybe there's something more here? (Or maybe not - maybe only other game devs have this expectation…)

One player consistently fired missiles as a way to boost backwards. I don't even think your speed is capped when you do that. I didn't think of this tactic at all - afterburners were for this, after all - but I'm totally gonna lie and claim it was intentional. :)

Results

Here's how Chuck Yeager's Jet Janitor scored in the compo:

Overall 144th (3.86 / 5)
Fun 63rd (4.02 / 5)
Innovation 125th (3.88 / 5)
Theme 496th (3.5 / 5)
Graphics 354th (3.58 / 5)
Audio 184th (3.64 / 5)
Humor 72nd (3.739 / 5)
Mood 356th (3.4 / 5)

Overall, I certainly can't complain about these scores. In absolute terms, this is easily my strongest game at 3.7 stars average of all categories (Pigheaded Pirate: 2.92, Jumpcoins: 3.47, Boids: 3.51). I broke top 100 in Fun and Humor (which is exactly where I did with Boids).

That said I expected a slightly stronger performance. I'd really like to break top 100 in most categories and I thought I had a good shot at that here. From player feedback some concrete issues that held me back were:

  1. A game-breaking bug for monitors with refresh rates faster than 60Hz. The game plays at warp speed. I don't have a fast monitor so I never picked up on this, and I still have yet to track down where the bug us. Most folks assume I made the beginner mistake of assuming 60Hz but that's not the case - the code uses velocity and acceleration only, and other techniques which eliminate the impact of frame time. In fact, it works fine on slower computers that can't hit 60 FPS (which I do test). My best guess is that it's a bug in how I rewire Phaser's game loop to support hot reload and looped live editing.
  2. There's still a few rough edges around the game feel. The planes' hitboxes are too large (I'd never scaled them down from the size of the spritesheet), the screenshake is way too aggressive, etc. More testing would have let me nail these down.
  3. One astute player pointed out that there's no real consequence to choosing different ships. I don't think this held me back, but it would have been good to improve upon! In a real game this might come out as different stats per plane, or upgrades. But in the jam, I think just spreading the planes throughout the level would have helped a lot.
  4. A few players stumbled upon the "perfect" mechanic, which usually made a good impression. However they were quickly deflated when they flew what they thought was perfectly but didn't get credit for it. In reality, each plane is physics objects even before you fly it, and it can be hit by stray bullets or even plane-plane collisions. This problem is merely a communication issue, but it's definitely an important one. The most obvious way to address that would be to have the plane emit a lot of smoke if it's not at perfect health, so at least the player always knows where they stand.

I'm still surprised I did so poorly in Theme ("Deeper and deeper"). I was getting a little bit bored of all the mining and submarine style games in the jam. Maybe my connection was a little too subtle, but like… 496th still feels a bit high. Oh well! The two compo games that scored highest in Theme (self reflection and GA GAME ME) were absolutely well-deserved.