Post-mortem: How We Shipped After Everything Went Wrongtl;drWhat went wrong?
- Our last-choice theme was chosen
- We ran into underlying issues with the framework
- My teammate had to drop from the competition after the second day
- Drastically reducing scope
- Repurposing what we had already built to fit a different and much simpler gameplay loop
- Reduce the story to fit new gameplay
- Planning inheritance hierarchies
- Pixel art
- You need more slack in your plan than you think
- All parts of a gamejam plan should be incrementally implementable
- A well thought-out framework can't completely prevent things from going off-plan
- Contributing improvements to arcade
- Learning about graphics
- Maybe some more games?
Disclaimer: this is written from memory and some shallow log review rather than an intensive review of git and chat history1. Our last-choice theme got picked
We were eager and hopeful before the theme was announced. Aquila, our top choice, got us excited about scrolling shoot-em-ups with space ships and fighter jets. We discussed how perfect arcade is for implementing games from that genre. The Raiden series and Ikaruga are commonly praised standout examples. We were also somewhat excited about other themes like Bridging the Gap and Soul to Soul.
Then, to our dismay, Cops was announced as the theme. Making any kind of shooter game now seemed both unoriginal and possibly tactless in light of recent events around the world.
How did we deal with it?
We brainstormed ways to make a cop-themed game with no violence
We had been discussing different stealth mechanics when I started riffing on recent news about the 2021 GameStop stock rally. If you're unfamiliar with the story:
- Casual investors started buying GameStop stock after reading posts describing a profitable loophole in stock purchasing contracts.
- The original poster, who goes by Roaring Kitty on twitter, testified before US congress about the issue and faced a lawsuit over securities fraud
- The readers who bought stock started calling themselves "apes", using the slogan "Apes Together Strong", and donating profits to gorilla conservation
Koko the Gorilla, a gorilla that once kept cats as pets and was reportedly able to use sign language, combined with the aforementioned topics to give us our first iteration of the game's name: Cat Burglar. We were fairly sure we wanted to make a stealth game with an ape protagonist. But what would an ape steal? A meme that became popular early in 2021 gave us our answer: three orangutans at a table, one asking "where banana" without a question mark at the end .
We were now going to make a game about an ape working with a cat to steal bananas protected by cops and redistribute them to fellow apes in need. Since we wanted to do a stealth game, a gorilla seemed like the funniest option for an ape player character since they are the largest living apes.
How would we make that into fun and non-violent gameplay? We decided that the player could use the cat and gorilla to interact with items in a police station to distract cops and sneak through a building. Objects like light switches, donuts, coffee machines, and office furniture were all now tools for stealth and distraction. We'd talked about incremental gameplay as a guiding principle. Slowly adding new interactable items to the map as time allowed after establishing a core gameplay framework seemed like a decent way to achieve that.
Things were going well!
We were excited. I made some filler cop assets that ended up being used in our final game. I began writing a stateful animated sprite class to play the animations and let us quickly prototype NPCs and environment objects. GelamiSelami started working with Tiled to help us build levels and made an excellent animated gorilla sprite that is nearly identical to the one used in the game. The frames for it are still in the assets folder if you'd like to look at them. Here's the backwards / "moonwalking" version they first posted:
GelamiSelami then combined our prior sprite work with a basic platformer physics system imported from arcade and an excellent custom camera object. I made a stateful gorilla class that could walk and animated appropriately in response to movement. I then began making alternate skin options for cops after writing a sketch of initial story revolving around the gorilla trampling the garden outside their police station during an escape. The two cop sprites were now named Randy and James as references to characters in popular media they resembled (Randy Marsh from South Park and James Doakes from Dexter). They still have their names reflected in their asset folder names in the final version of the game.
2. Performance Issues with Text
We wanted to display time remaining, but we ran into an issue we'd previously seen mentioned in chat channels for game jams: as of arcade 2.5.6, frequent update attempts on the text value of a UI label will slow your game to a crawl.
The issue stems from the fact that as of the time of writing, UI labels re-render their textures entirely each time the text value is updated, and do so with the CPU rather than opengl. Not just once, but three times for each one call of text update. The three textures correspond to three different display states a label can have as part of a button. This design choice made sense when the UI label was written because they were assumed to be infrequently updated rather than a score or time displays.
Then users like us immediately grasped for the first solution that seemed apparent for displaying scores and times. This is counter-intuitive to say the least.How did we address it?
We decided to cut dynamically updating text such as timers out of the game. Only static notifications would get shown, and I considered using speech and thought bubbles with icons in them instead of text for interaction with cops.3. My Teammate had to drop after the 30thWhat went wrong?
GelamiSelami, through no fault of their own, unexpectedly had to drop from the competition after the second day. There was now no time to implement a single character version of the original design, let alone a second cat character to swap between.
I had to figure out a way to turn our pieces of a game into something I could finish on my own in the time remaining. This was harder than expected. I wasn't familiar with the same parts of the arcade framework GelamiSalami was, and I had to things to do during the day, leaving me limited time to work on the game.
On top of the technical concerns, I also realized that our story and art no longer made sense. How are you a cat burglar if you aren't stealing anything? Gorillas don't wear pants and they run on their knuckles, so there's no way for them to actively carry anything and run, climb, or do acrobatics effectively at the same time.How did I deal with it?
I pivoted hard toward a drastically simpler design. Stealth wasn't an option, so I would make a runner game.
A New Enemy & An Old Friend
The new goal of the game was to avoid touching enemies, with bananas as a thrown weapon to halt police and down drones. I added flying police drones as a second category of enemy. I'm grateful to my past self for writing the stateful sprite back-end and derived Actor class. It saved our project because it worked as intended by turning adding new enemies into a matter of artwork and passing config for loading sprites. To drive the point home, it took at least 10 times longer to finish the art than to add the new NPC class and have it spawn.
I was thinking about how to manage multi-level terrain and how I'd depict have bananas hitting drones out of the air. Then I checked how much time I had left in the week and I made a decision that is now clearly good in retrospect.
I abandoned all plans for bananas, complex drone behavior, terrain, and scripted intro scenes. I didn't bother removing the assets. There are unused drone, gorilla, and cop assets in the asset folder. I now have a new and very personal appreciation for the unfinished game assets and entities often discovered buried in shipped with game titles. Sometimes, there's not enough time to take things out without risk of breaking something.
This game was turning into a poor clone of the dinosaur game in Google Chrome. I made enemies delete themselves after they moved too far to the left. I tore out the camera, level loading, and even player movement. All that was left was a static viewport and a simple physics system that allowed the player to jump up and down.
Puprose and A Splash of Color
I also remembered the second issue: Why was the gorilla running? What were they stealing? The story about GameStop provided the answer. The gorilla would bust the cat out of jail after the cops framed him for illegal stock trading. The cat would ride on the gorilla's back.
Now I had to add a cat. Watching GelamiSalami's spriting process helped me understand how to color my sprites much better. Remembering the color theory I had read about earlier in the week, I decided to make the cat a golden-orange ginger to contrast with the sky blue of the cop shirts. I then animated the head and tail bobbing to give the cat some life. Giving the cat color also helped the formerly monochrome player sprite stand out against a monochrome background. The gorilla now had only one animation used in the game, and it looked like this:
Add a High Score and...Oh, right. We can't do dynamic text. That means no high scores, which means no infinite runner. How do I make this a finished game with a sense of progress?
Once again, the reusable components I built earlier came to my rescue. I hastily implemented a randomized enemy spawner out of lerps, a timer object, and a few builtins. Enemy spawn rate slowly increases as the win condition timer nears completion:
self.time_till_next.remaining = random.uniform( self.min_enemy_gap_sec, lerp( self.max_enemy_gap_sec, self.min_enemy_gap_sec, self.global_time_elapsed.completion ) )
I hastily tested the win state by temporarily setting a much shorter time-to-win, and verified that I could reach a win screen. I set a final time limit for the player to survive, and added text display in-game to reflect the player winning or losing. The game was now ugly but technically fully playable.
Those Labels Again
The same text rendering behavior that annoyed me earlier hurt me again by making it impossible to unblur text in the time I had left. Oh well. The viewport was locked to upscale and I decided I didn't want to try to upscale all the sprites as a hacky fix. Instead, I focused on the install instructions and getting someone to help test them. The game is uploaded and shipped, even if I'm unsatisfied with it.
This challenge helped me apply and refine my class hierarchy design skills, even if it was in a very limited fashion. The classes in this game are somewhat ugly, but they are effective as zero-thought tools for rapidly adding enemy types. The timer and stateful animated sprite classes I wrote ended up doing the brunt of the work of my work in this game, both before and after the pivot to a runner.
I am also grateful to GelamiSalami for showing me some of their pixel art process. I believe that I got slightly but visibly better at pixel art during this challenge by studying their techniques. Compare the cop, done first, to cat on the back of the gorilla below:
Although the cat is still imperfect and rushed, it reflects form and lighting better than the cop sprite and in a smaller amount of pixels. The cop looks flat and blocky in comparison to the cat, and I believe the better understanding of hue shifting I developed during this game jam is part of that.
These will be completely unsurprising to anyone with experience in game jams.1. You need more slack in your plan than you think
As is often the case for many teams during game jams, we didn't have enough time to do everything we wanted. We had some smaller but unambitious ideas for games earlier that we probably should have used rather than trying to build a side-scrolling stealth game. A larger team might also have made stealth gameplay more feasible, so we hurt ourselves by not recognizing our lack of room to shift work between team members.2. All parts of your plan should be incrementally implementable
Ironically, we talked about this this principle before the competition started. I think we got too optimistic about what back-end we could implement in the time we had before we started adding incremental additions. If we had stuck with a simpler core gameplay concept like a runner to begin with, we would probably have had time to add things like throwing bananas. Going forward, I think I'll try to stick to very simple core gameplay loops. However, it seems to be hardest to keep gameplay concepts simple when you're brainstorming around the theme rather than core mechanics. I think other teams that built gameplay first and wrapped the theme around it better have a better sense of the game jam metagame, and I should follow suit.3. A well thought-out framework can't completely prevent things from going off-plan
This is more of a supporting point to the previous two than a stand-alone. A good class or gameplay design can't protect you from things like hardware problems or other reasons people may have to stop work on a project. It also won't prevent you from making unfeasible game design choices. The only way to reliably deal with those sorts of issues is to stick to the first two principles.Next Steps
After the submission period ended, I started contributing to arcade in small ways to familiarize myself with the codebase before working on bigger ones. I've submitting bug reports, and made small quality of life pull requests, and helped debug issues with some unruly hardware. My main goal for arcade is to provide a solution for displaying rapidly updated numerical or text data so people don't have to worry about performance hits from trying to display scores in a way that cleanly fits into the UI paradigm pushed.
I've also started trying to understand the graphics primitives behind arcade. This might take a while as OpenGL is complicated, but I have learned enough to at least follow discussion and explanations from experienced maintainers.
Maybe I'll write some more games eventually as well. I will try to keep it small in initial scope with room for polish as practice for future game jams.
I'm looking forward to the next PyWeek!