Quantcast
Channel: Shawn M Moore
Viewing all articles
Browse latest Browse all 36

The Whirlwind of Game Design (a Ludum Dare postmortem)

$
0
0

Over the weekend of December 1st, I participated in my first game jam: Ludum Dare 43. I chose the Compo track, so I made a complete game from scratch, solo, in 48 hours. The theme of the jam was "sacrifices must be made", which I incorporated by creating a side-scrolling beat-'em-up game where your weapon is your partner.

You can play Pigheaded Pirate at https://pigheaded-pirate.com or right here!

click to play
Pigheaded Pirate

Preparation

I've made games before, but the lastcouple have been for iOS using SpriteKit. I intentionally chose to make an in-browser game to satisfy a few necessities: a lightning quick edit-build-test feedback loop (because compiling and deploying to a phone still takes tens of seconds which is like forever), the widest possible audience (not everyone has an iPhone, or even uses Windows, etc, but 100% of participants do have a web browser), and being able to own the release process rather than dealing with app review.

JavaScript has grown up to become quite a nice language—destructuring bind is such a nice quality of life feature—but choosing it left me with a problem: I've never made a game with JS before. So a few days before the jam began, I googled "JavaScript game engine" and ended up picking the first result. As you do. That engine, Phaser, provides all the primitives I knew I'd need: sprites, animations, sound effects, particle systems, a physics engine, text rendering, etc. The one feature that seems to be iffy is shader support, but that was acceptable because I had only 48 hours to work with. Writing shaders takes me a long while because my usual debugging repertoire doesn't transfer. And also also, like, a small part of my monkey brain is stuck in 2003 when even something as innocent as a trailing comma could wreck some shit. So directly programming the GPU on the web seems like landing on Mars by comparison.

To exercise all those features and to get a feel for Phaser itself, I put together a dumb prototype that turned out to be more fun than it rightly deserves. I blame that on physics engines being remarkably effective.

click to play
the prototype

Anyway. I wanted a great edit-build-test iteration cycle. I believe strongly that quick (ideally instant) feedback is a disproportionately important part of any software engineering endeavor. But it's especially true for making games. So I wanted code linting with automatic fixups, type checking results right within Vim, automatic reload on changes, and so on. I'm a web developer so this is definitely the sort of thing I can set up myself from scratch with webpack, babel, prettier, flow, etc in like five seconds. Actually no I just used create-react-app which offers a great developer experience out of the box.

I did have to eject out of create-react-app immediately due to Phaser refusing to handle automatically-inlined data: images, seemingly only out of principle; the solution is to update Webpack config. There was plenty of other setup friction too. The Flow type checker handles some types of static assets like png and json perfectly well, but errors out on other types like wav and mp3 until you add a shim for each content type. The fun doesn't stop there though, because yarn build would throw Unknown browser query ‘dead’ errors until I found and upgraded all the nested copies of css-loader throughout my dependency tree.

Web development, am I right? Luckily I ironed all that out before the jam started!

I streamed my entire Ludum Dare game dev live on Twitch. To give viewers (and honestly myself) a sense of the progress I whipped up a clock script which sat in the corner at all times to track real and effective dev times. Effective time meaning that clock got paused for breaks… the realtime clock sadly never stops ticking.

Although I used create-react-app, I didn't use any React for the game itself. But since I had it, I used it for the "frame" of the game, meaning everything else that is on the page. I made novel use of the distinction between development and production builds. In dev mode, I had a prominent link to the game that anyone who caught the Twitch stream could visit to try the game in its current state. I didn't end up needing it, but I also had a solid plan for using React to build debug controls (sliders, textboxes) that would feed into the game logic to drive down that feedback cycle further. In production mode, the page instead explains that it's a Ludum Dare game, and of course minifies assets and whatnot.

The Jam

Here's a timelapse of the entire game development from scratch to shipped. I also included some gameplay videos throughout to show the game at each milestone, which I hadn't seen before:

Back of the envelope math suggested that, to capture the fifteen thousand screenshots for that timelapse, I was gonna need more disk space than my laptop had available. So I wrote them directly to an external hard drive. But I always feel anxiety about wearing down that spinning rust. By the way, in macOS, programmatically taking a screenshot of a second monitor can be done by providing multiple output filenames to /usr/sbin/screencapture. The --help output doesn't make that super clear.

I also set up a super legitimate keylogger just for kicks. Some interesting takeaways from that:

I supposedly typed a million keystrokes that weekend, which is a sustained, I dunno, 80 wpm? I think a lot of that must have been mashing on the arrow keys to play. Shift and other control characters were also counted as separate keystrokes.

My most commonly typed letters were z (due to being the primary action in the game), then j and k (because vim), then w (also vim… I use word-based movement commands a lot). All these were an order of magnitude more than other letters.

I don't think fatigue had a big effect on how much typing I did. The hours immediately preceding and following the one night of sleep look the same. What clearly had a much larger effect was how large the existing codebase was. In the early hours of the jam, I started with a blank slate, so there was much more thinking and planning than typing. Towards the end, I spent most of the time producing artwork. So hours 20-30 had the most typing as I had a large codebase in which I could spend lots of keystrokes on the details.

Todo Tracking

I can't stand forgetting anything. So I'm very aggressive about moving thoughts from my head into durable storage. While I'm working on a project I leave lots of notes to myself in a todo list:

I do this for all of my programming, whether for work or not. It's a stupidly high-leveraged way to ensure I leave no loose ends. You can see here it's not limited to just bugs or features: I have notes of edge cases to test, design questions to chew on, low-level implementation notes. Also a list of sound and graphical assets to create. Basically, any time I have a useful thought, I take the five seconds to capture it, then continue. I do this when I start a project too, by just breaking it down into smaller components. The concept of "next action" is hugely useful.

In this way I made several hundred tasks throughout the weekend. And I'm perfectly satisfied that my primary constraint was time rather than my personal memory. Every bug that I noticed either got fixed, or deprioritized as a non-showstopper. All the polish and feature tradeoffs were decided intentionally rather than accidentally. I knew exactly how much art I had to bumble through. And so on. Most importantly, being supported by a crammed-full todo list gives the freedom to fully focus on each specific task in turn. I cannot overstate how important and powerful this is. By the way this blog post is now an ad for Getting Things Done and OmniFocus.

A fringe benefit of this approach is when it comes time to review the project, you'll already have a detailed list of all the work you did!

Some ideas that had to get cut for lack of time: music (which is fully due to my complete lack of trying ever in my life), any sort of particle effect, enemy attacks, health recharge items, two bosses you fight simultaneously, a timer to make sure you keep moving, etc. Kinda glad I pared it down though, and to that end, I should have even cut the third level shorter.

The most glaring bug I had to leave in is poor handling of walking into walls, especially jumping into walls. You can get weirdly "stuck" against a wall as you try to walk into it. Since there weren't too many walls in the game, and the bug lasts only until the player decides to stop walking into the wall, I decided it's not a big deal. That turned out to be the right tradeoff because no one has flagged it. I'll dig into the joys of physics engines in a bit!

One idea I didn't have during the jam but should have is when the pirate respawns, apply an impulse to push all enemies away. This will help avoid them crowding you and potentially causing a death loop. The reason I should have had this idea is because Streets of Rage (the most direct inspiration for Pigheaded Pirate) does something very similar!

Another good idea which Marc suggested would be to have more skill involved which increases damage to enemies or decreases damage to the pighead. I'm not sure what that would look like though. Also, it's worth pointing out that you could theoretically go through the game without using the pighead at all, but who would play that way? The player is almost certainly complicit in the "sacrifices must be made" theme. (Unfortunately I don't have analytics to show whether anyone played so virtuously!)

Programming and Game Design

As an engineer, this was the easy part for me.

I ended up with about 2500 lines of code. Here's how it grew over the weekend, where each dot is a commit, the x-axis is time, and y-axis is lines of code:

All the advice I've seen for game jams suggest finishing the core mechanics as quickly as possible—like within a few hours—to leave the rest of the time for art, polish, balance, and any other connective tissue (in this game's case: victory condition and advancing between levels, but also generally any non-gameplay features like a start menu, save game system, etc). That also optimizes for getting important feedback about whether the game has promise and is fun. From the timelapse you can see that I sorta managed to follow that advice: picking up the partner, throwing them at enemies, scrolling the level, enemies spawning in waves, etc were all there by hour 10.

I intend to be even more aggressive in finishing game mechanics next time. Like, hour 5. Polish is that important. First impressions really make or break the game for a player. I appreciate this even more now having played a couple dozen other Ludum Dare entrants: the quality of the artwork on the game's thumbnail alone strongly attracts me to or repels me from even trying that game.

For this game, I chose to use a physics engine (matter.js) because it's a great tool to make everything way more fun. This was especially important because this game in particular is all about collisions, and making those look and feel good was vital. As I describe later, partway through development I did something that screwed up the collisions, and it sucked the fun out of the game. So I have pretty strong evidence that, if I'd opted to not use a physics engine, the game would have been much worse off.

In writing this postmortem, I tried to tease apart implementation notes from design decisions, but they're really intertwined. So here's a smattering of notes on how the game works:

  • In some projects, like action games, it can be difficult to use the debugger to poke and prod at state while the program is running. So, in addition to the common game technique of localizing all state centrally, I also assigned a window.state variable, but only for dev (rather than production) builds. Then at any point I could inspect, watch, and modify anything in window.state, live, using the browser console, which tightened the feedback loop.
  • I used Flow for JS typechecking, to try to cut down on time fixing bugs. But I didn’t want any friction for using one-off state variables, so I typed the entire game state variable as any. That effectively turned off typechecking for the whole game. Oops. However, it turned out to not a big deal because the game was small enough to fit in my head. Types really show their worth once a project hits a much larger critical mass.
  • The level is surrounded on all four sides side by thick, invisible walls. This helps the physics engine keep all the characters inside the level. They're invisible because camera lerp would otherwise cause them to be shown sometimes, which broke whatever suspension of disbelief this game was able to cobble together.
  • True to the beat-em-up genre, you can never go back; this is implemented by continuously repositioning the "left wall" to match the camera's left-hand edge.
  • Enemies are separated into waves. But, importantly, each wave of enemies is spawned all at once, and they immediately all start moving toward the pirate. If enemies instead spawned individually, a clever player might realize they can inch along and then wait for each single enemy to approach, and slowly dispatch them one by one. That would mean the optimal strategy is the least fun one, which is pretty terrible game design.
  • You can throw the pighead offscreen, but only to the right, and only up to some short distance (about one game screen's width). This was intentional, but I'm not sure whether was a great idea. Perhaps it subtly encourages a bit of caution?
  • There are checkpoints you cannot advance beyond until all the enemies in the current wave are dead; this is implemented by both locking the camera and moving the "right wall" to match the checkpoint's position. But moving the wall naïvely might cause enemies (or the pighead) to fall outside the game's boundaries. So the right wall moves instantaneously to each subsequent checkpoint, and enemies are only ever spawned to the left of it.
  • Even with all that careful wall moving, there's still a way that the pighead could get out of bounds: by squishing it between the camera's left edge and some level geometry like the boxes on level 1 or trees on level 3. But since the invisible walls have a large width, it's far more likely that the pighead will instead clip through level geometry, which is preferable. But, on the off-chance the pighead does break outside the level boundary, there's a check for its position. If they're ever outside the level, the game immediately respawns them.
  • Similarly, an enemy somehow ending up outside the level boundary would be even more problematic, because you need to defeat all enemies to advance. Any inaccessible enemy would stop the game in its tracks. So a similar check kills any enemy that falls out of bounds. This sort of issue did impose subtle limitations on where level geometry could be placed relative to checkpoints, to avoid potential for squishing.
  • Damage is calculated by the two colliding objects' relative velocity and mass. So walking into an enemy does way less damage than a high-speed pighead. This was implemented by tracking each object's velocity, as vended by the physics engine, and doing a bit of linear algebra when a collision occurs.
  • I used a state machine for all the possible states of carrying the pighead: calm, pull, hold, and throw. This turned out to be really valuable. The state machine made it easy to carefully handle the transitions between states and catch all sorts of edge cases, like what if you die or finish the level while holding the pighead. I don't think there were any bugs here. The state machine also helped when it came time to implement the two-step "tutorial".
  • That pull state is pretty subtle. I imagine most players' mental model was: if the pirate's sprite is touching the pighead's, then you can pick them up. But that's not quite right, as you can be about a character's width away and pressing pickup will pull the pighead toward you. And once you do colide, whatever velocity the pighead has will push you back, too! So, z works more like a tractor beam. I tried to show this in the tutorial, but it was still pretty subtle. If I were to continue working on the game I'd add some sort of vortex effect any time you press z to guide the player to the right mental model. If you're too far away and press z the pighead does wiggle though.
  • The state machine was also useful for managing the pighead's physics body. When you're holding the pighead, its physics body should be disabled (otherwise it'll be treated like any other collision and cause both to push away from each other). The only problem with this approach is when you throw, I couldn't figure out how to re-enable the disabled physics body. So as a hack the pighead invisibly respawns in the same spot the moment you throw it!
game.sound.play('throw-sidekick');

// recreate a new sidekick because re-adding to// physics seems unsupportedsidekick = level.sidekick = replaceSidekick(sidekick);
  • The end cutscene alone is about 500 lines of code, almost exclusively just orchestrating animations. Which on the surface seems fine because I certainly didn't have time to build a high-level scripting engine, and it works hard to sell the theme. But…that 500 lines accounts for 20% of the entire codebase. I should have spent only a small amount of that effort on the last minute experience and the rest on the first minute experience.
  • I am allergic to reusing code by copy and paste, but that quickly dissipated as Sunday afternoon rolled around. The treasure chest's pulsing glow effect was particularly egregious here, as it was one of the last bits of code I worked on and I couldn't spend time to thread it through the code properly.
  • I'm pretty quick with Vim, so I just used it as my level editor. Especially useful was visual block mode, which is a bit hard to explain, but it lets you edit arbitrary subregions of text in a file. This was useful for moving level elements around in bulk, moving columns around as easily as other editors can move rows around.
  • In the level map, enemies are various uppercase letters, and level geometry tiles are lowercase letters. There are a few other special characters like # for the treasure, | for a checkpoint, @ for the pirate's starting position, and $ for the partner's. This worked out pretty well, as the level definition gave an (admittedly janky) preview of what the level would look like in the live game.
  • The seahorses are too damn hard. Many players flagged that. It's because they move 50% faster than other characters, which makes an outsized difference. I ran out of time before I could balance the game.

Game Feel

A definition of "game feel" I like is providing the maximum amount of output for the minimum amount of player input. Animations, particle systems, sound effects, screen shake, haptic feedback, and so on all work together to make the player feel a sense of direct connection with the game world. It helps sell the simulation. Screen shake and explosions and pew-pew sounds all have little to no impact on the game mechanics, but they all contribute to making the player feel mighty. And that makes for good fun.

Game feel isn't limited to such universal, generic features either. In my game, when you throw the pighead, the pirate gets pushed back a bit. Pighead recoil. When you're carrying the pighead, you run slower and can't jump as high. If the pighead's HP is sufficiently low, it'll try to inch away from you. (I should have made this easily-missed effect more prominent because it's kind of cute but also pretty horrible.)

This sort of design sense transfers to non-game apps too. Of course, games take these to eleven, but even boring productivity apps can have animations and other effects, graphical or otherwise, to help the user intuitively understand what effect their actions have. This is why recent iOS versions have a built-in physics engine built into the standard UI toolkit. Sell the simulation. A little bit of bounce goes a long way.

The level transitions got a disproportionate amount of positive feedback (one player even commented that the "level transitions are very nice compared to overall graphics"😅). But it was pretty easy to create that effect because I'd already had the level divided into 32x32 tiles. Each block was even a separate physics body, which I'm sure destroyed the framerate on older machines. Given more time I'd coalesce larger rectangles into fewer physics bodies.

Notice how that transition looks like the ship breaks apart, and then the next level you're underwater with a sunken ship in the background. Similarly the underwater level ends at the top of the screen, and then the subsequent level starts with a beach. I don't know if anyone consciously noticed these subtle links, but, they were a great unintentional bit of consistency.

A few people noticed that your partner's sprite gets progressively unhappier as they take damage. One player even noticed that the pighead gets stuck in an "unhappy" state. They thought that must have been a bug. But it was intentional. Each level, the pighead gets sicker and sicker of your crap, culminating in the end cutscene. I should have made all of this more obvious with better, less subtle art. Sometimes it pays to be heavy-handed!

I really didn't want to have the player read through a bunch of instructions before playing. Quite a few Ludum Dare games I played involve several pages of story with the controls described somewhere in there, but that's not how I roll. That's why my game's "tutorial" is two lines of text that quickly teach you the single novel mechanic. And, because the board game Go has warped my brain, anything that serves only a single purpose must be treated with suspicion, and so the second tutorial line starts with "If you must… press Z to throw me" (emphasis added) to reinforce the game jam's theme.

Bugs

There were four significant bugs I had to solve. And most of those, and my other toil throughout the jam, was due to physics. Now, I do truly enjoy physics engines, both as a player and as a designer. They add a lot of depth, character, potential for skill, and novel design possibilities to a game. Pigheaded Pirate would be way less fun without penguins getting knocked back, rolling into each other, and the pighead bouncing off realistically. Yeah, that's right, "realistically".

With a physics engine, you simply set up with some bodies and occasionally apply impulses, then each frame it dutifully spits out positions, velocities, rotations, collisions, and so on. That's a remarkably outsized result for how little work you have to do. But, sometimes, what it spits out is just off. And there's little recourse for the designer when that happens. For one, the physics engine has to be run in realtime, so it can only do approximations at best, and sometimes those approximations can be noticeably inaccurate. Two, it is hard to figure out the right set of inputs to mesh together to produce results that feel good, since the two are at very different levels of abstraction (hi Seena!). Third, you probably need a much more mature math background than I have to understand, much less tweak or even fix, the code for the engine itself. So a physics engine is pretty much the platonic ideal of a black box, for better or worse. (To be honest, largely for the better!)

But in future jams, I would probably shy away from any physics-engine-shaped game. It's because you have to burn a lot of time (which is totally at odds with game jams) trying to bend the physics engine to your will, but they are remarkably non-bendy. Here are some examples of that.

Jumping

Using a physics engine for a game with platforming elements is an exercise in frustration. Platforming games really don't use anything resembling proper physics. In the old 2D games, Mario doesn't rotate backwards when he gets hit with a Koopa shell, or have any friction when falling down hugging a wall. Even jumping doesn't use anything resembling real-world physics, because gravity suddenly triples at the top of his arc. But, importantly, that unrealistic trajectory makes Mario much more fun to play, and that consideration must come before all others.

When using a physics engine, even something that should be simple, like detecting whether a character is currently touching the ground, is surprisingly tricky. The usual solution seems to be to add a special non-interacting physics body directly under the character, and check whether it's colliding with anything. And I did just that. But I did it incorrectly, which caused some aggravating bugs where the character would arbitrarily not be able to jump.

The physics engine Pigheaded Pirate uses gives you callbacks when collisions begin and end. So in each one of those callbacks, I check if one of the bodies is that special under-player physics body. If so, I toggle the "touching ground" flag accordingly. Then when the player presses the up arrow, check that flag to determine whether to allow the jump. And that initially worked quite well. But then something completely unexpected broke it: switching to a tile-based map.

I'll give you a moment to ponder why that might have happened.

Each tile is its own physics body. So as soon as the first pixel touches a new tile, the special under-player body kicks off a "begin collision" event with that new tile. And as soon as the last pixel stops touching a previously-touched tile, there's an "end collision" event.

The problem is that you can stop touching an old tile before you begin touching a new tile, so the "collision end" callback will set the "touching ground" flag to false. And then you seemingly-arbitrarily can't jump. Move a bit more until you happen to start colliding with a new tile, then you can jump again. Aggravating!

The solution was to instead listen to the "collision active" callback which gets called every frame. At the beginning of each frame, set the "touching ground" flag to false. Then if any active collision involves the under-player body, set that flag to true. This has proven to be pretty solid. It also easily supports jumping off other objects like enemies.

Tangentially, for a while, there was also a bug where if you tried to pick up the pighead while you happened to be standing on top of it, it was far enough from the pirate sprite's center that it would pull closer to you, but never close enough to pick up. But that added an impulse upward which of course affects you too. This meant you could fly. Oops.

The solution to that was to reuse the special non-interacting physics body collisions to also check whether the pirate is touching the pighead. If so, then z immediately picks up.

As mentioned above I never quite solved the sticky wall problem. The solution seems to be to introduce two more special non-interacting physics bodies, one on each side of the character. If one of them collides with anything, you nudge the character away in the opposite direction. But I never got that quite working.

Density

The game started out with each character being just a rectangle. I'd intended to come back and do the art later, once I figured out what the setting would be. But when I eventually loaded art into the game, the gameplay was completely thrown off. The pighead would rocket off at the speed of sound, jumping would easily hit the top of the level, moving left and right would fling you to the side of the screen.

This is because the physics engine was calculating a mass for each character, taking into account transparent pixels. And so by loading in new artwork, which wasn't rectangular, I inadvertently changed the mass of each object. So all of the impulses and other physics constants I'd tuned were now all very wrong.

To solve this I first tried hardcoding the mass of each character to what the physics engine had previously calculated, but that never seemed to work quite right. I imagine because the physics engine infers a lot more than just mass. So to solve it definitively, I reverted back from pixel-perfect physics bodies to plain rectangles. The artwork is still there, but it's completely ignored for physics calculations. And for lack of time I ended up not turning near-pixel-perfect physics bodies back on, even though the engine supports it perfectly well. Oh well.

Physics engines being a stubborn black box can be really frustrating!

Suddenly Not Fun

At some point late in the weekend I realized the game had mysteriously become not very fun. I couldn't put my finger on what it was, but something was definitely missing.

Eventually I realized it was related to the physics. Throwing the green box was fun, but throwing the pighead wasn't. I was pretty sure this was more fallout from the previous issue, because I had just applied artwork and had just discovered density/mass issues. This must've been yet another symptom of that problem.

So I reverted back to rectangular physics bodies and continued on to other work, thinking I'd solved my problem. But I was wrong. It turns out the cause was completely different, and my problem was still there. Bad of me to conflate these separate issues together. I suppose I was just ready to blame the physics yet again.

Later on, while reviewing some earlier videos I'd tweeted, I saw the missing element immediately: when getting smashed by the pighead, the enemies weren't rotating at all like they'd used to. It's like they were now on ice skates, just getting pushed back linearly. Way less fun.

Luckily, that reminded me of the angular velocity fix I'd put in place due to (again) tile-based maps.

This is what the debug build of the game looked like. Each pink box outlines a physics body. You can see the special non-interacting physics bodies on each side of the player. Since the pighead is being held, its physics body is temporarily turned off.

Notice that each tile in the floor has its own physics body. And they are, in fact, perfectly tiled: there's no gap or overlap between tiles, and each tile is at the same level. So the physics engine ought to be able to treat it as one continuous flat surface. But because it relies on approximations, that doesn't quite work out so nicely. Sometimes enemies would get stuck on the seams between tiles. And also, because I'm applying an impulse to the enemy to make it move, it's pretty easy for them to just tip over.

Again, using a proper physics engine for a platformer is not advised.

To try to address these issues, I had put in a cap on how much angular velocity an enemy could have. This would help keep them upright.

But capping angular velocity also makes it so they don't fall backwards when hit with the pighead, or another enemy. To make the game way more fun again, literally all I had to do was delete this line of code:

enemy.setAngularVelocity(0.0005);

Even though enemies fall over more easily now, sacrifices must be made in order to make the core game mechanic fun.

Parallax scrolling

(Ugh, even just reading that header brings back a little anguish.)

Each level has a background image. My intent was that each background image should scroll separately from the camera in such a way as to handle any combination of possible background and level widths. So at the beginning of the level, the left edge of the background would show, and by the end of the level, the right edge. This would let me change the size of any level arbitrarily based on the needs of the game design, and the size of any background image arbitrarily based on my ability to produce artwork. Seemed ideal, what could possibly go wrong?

After some attempts to get this formula right I quickly got frustrated and wired up a window.f = function (bgWidth, levelWidth, cameraX) { … }; then, every frame, background.x = window.f(…); so I could iterate on the formula right within the JS debugger by redefining window.f. But even that turned out to be ineffective because I still needed to scroll the screen (ie play the game) to see whether I got the formula right.

So I took a few combinations of backgrounds and levels, manually positioned the background pixel-perfectly and inspected its .x, then wrote a test suite for this formula. That let me basically brute force the solution rather than having to think.

All told, I burned three full hours on getting this goddamn background.x = … formula right. Turns out algebra is the first skill I lose while sleep deprived.

Here are various iterations of the formula that I really did try. See if you can spot which ones are subtly broken.

If I ever get to use time travel, the second thing I'll do is give myself the formula:

constprogress = camera.x / (level.width - screen.width);
background.x = progress * (level.width - background.width);

Theme and Setting

Soon after the "sacrifices must be made" theme was announced, kicking off the game jam, I quickly figured out what genre of game to make (beat-em-up) and how to apply the theme (throw your partner at enemies to sacrifice them). I've played a lot of Streets of Rage 2 in my day, so I was excited to make a beat-em-up. If you look carefully in the timelapse you can see me watching Streets of Rage videos to get amped up (that music! looking forward to Streets of Rage 4 too! also wait omg have you played God Hand yet??)

The more interesting question is why the pigs, pirates, and penguins setting? At about 30 hours in, the code was complete. So I ran out of work to structuredly-procrastinate on the setting, and so I put down the computer for a bit to start really brainstorming what to replace those solid-color rectangle character sprites with.

I'd already come up with some pretty specific criteria that I felt important to hit:

  • The setting had to be somewhat novel. Specifically not vigilantes beating up gangs, which is what most beat-em-up games use.
  • The main character needed sufficient motivation to sacrifice, and that motivation needed to serve as the literal goal at the end of each level. This is the constraint that I started brainstorming from. And it came to me: pirates are all about booty.
  • The partner had to be substantial to make the jam's "sacrifices must be made" theme work. If you're just throwing, say, a parrot around, that is not really a plausible sacrifice. That also feels like it would have been a bit too Angry Birds. I also definitely did not want the partner to be a mermaid or anything else that presents as a woman.

I got stuck here though. The pirate setting is novel enough (and using treasure as the goal offers fringe benefits: it's easy for the player to understand, and for me to make the art for). But where did the pigs come from? Well, the constraints suggest that the pirate must be obstinate about lust of treasure over love of their partner. The thesaurus provided a very evocative synonym of obstinate: pigheaded. That trivially supplied the name of the game, though not before I spent a while trying to work "corsair" into a title. The partner also being a pig was pretty straightforward after that.

Mashing together pigs, pirates, and penguins also satisfied another constraint that I had come up with before the jam even started: the game must have elements of what I naïvely call normalized absurdity. Which is essentially the notion that players will gladly let you take them to absolutely ridiculous places, so long as you, the designer, do so rigorously. In other words, instead of breaking the fourth wall, double down on the absurdity. That's why the pighead yells both "squeal!!"and"avast!!". Because pig pirates, duh. Absurdity seems to be a growing trend in game design. Rocket League—soccer but with rocket cars—is a good example. Crypt of the NecroDancer, too, is exactly what it says on the tin, and they go all-in on that idea. For me, the prototypical example of normalized absurdity is the game Last Man Sitting:

I think Pigheaded Pirate offered a mediocre glimpse at normalized absurdity. Quite a few people commented positively on the setting being silly and fun though, which suggests to me the idea has legs. If I had more time, sounds like an actual squeal when you throw the pighead, better art (though it still tickles me to no end that the pirate pig has a thick beard), and so on would have worked together to further sell the absurdity.

This all certainly says a lot about my twisted sense of humor.

The specific enemies and settings I chose did not come until a bit later. My initial ambitions of fighting off swole shark bros were stymied by my shoddy art skills. On level two, the rectangle characters would tip over and slide down the ramps headfirst which suggested a penguin to me. I was pretty happy with that because with a penguin I could also get away with not creating a walking animation. Seahorse too. The crab would have been pretty easy to animate, but I ran out of time for that.

I needed to choose level designs that would be easy to produce assets for. Level one you are on a ship, level two you've shipwrecked and sunk to the briny deep, level three you're climbing onshore a volcanic, tropical island. I doubt many players thought about that progression, which is fine, but it probably is important that there is some kind of progression.

Why the eclipse, volcano, and sandworm as the background for level three? By then I was looney from sleep deprivation.

Shipping

Releasing the game was stress-free because throughout the jam I had already shipped a couple dozen releases. To do so I'd set up this pre-push hook in git that built a production bundle and rsync'd it over as static files:

cd~/devel/ld43yarnbuildrsync-avzbuild/ ld43.sartak.org:/var/www/ld43.sartak.org

Since I was using eslint and babel, I also had no concerns about unexpected browser compatibility issues, even using such advanced features as destructuring bind and trailing commas.

As soon as I figured out the name of the game, I registered the domain for it and had it ready before the jam ended, which I thought was pretty nifty.

It was also pretty straightforward to embed the game directly onto this page, which was one of the reasons why I picked JS to begin with.

Reception

My plan for this Ludum Dare was to credibly simulate all parts of the game development lifecycle, even before and after the 48 hours. I am satisfied that I was able to achieve everything I set out to do…except create music for the game. I can't be surprised by that though, because I've literally never created music before. That's something I plan to work on over the next year (as well as pixel art). In fact, even as an engineer, I think polished art is the single most important aspect of a game, as it's what gives the lasting first impression. In future games, I will aim for simpler but cleaner art styles. I'm sure I could've made rectangle character sprites work by adding just a few important details. Also on that note, I invested a lot of time in the last minute experience of the endgame cutscene, but I should have spent it instead on the first minute experience to keep more players in the funnel.

Not monetizing the game was intentional, but given my day job, I'm sure I could figure that out.

I was surprised at how hungry streamers are for novel games, and so Ludum Dare is a surprisingly good way to get some Twitch attention. I reached out to a few game streamers (make sure to use the specific method that each streamer requests!) and got largelypositivefeedback.

I also did my own stream of livecoding the whole event. I probably won't stream game jam development again since I ended up having only a few viewers throughout, which was not worth taxing my CPU, network, screen real estate, attention, etc. My laptop held up pretty well with all that and driving a 4K display and playing a 60 FPS game. Not perfectly though, as Vim segfaulted several times, and occasionally lost syntax coloring, which were alarming.

Ludum Dare is a very popular event. The compo track I participated in had 764 other game submissions! Here's how I did:

Overall356th (3.111 / 5)
Fun283rd (3.148 / 5)
Innovation340th (2.963 / 5)
Theme354th (3.130 / 5)
Graphics394th (2.741 / 5)
Audio359th (2.389 / 5)
Humor127th (3.250 / 5)
Mood399th (2.640 / 5)

While I was optimistic I would make it into the top 100, I didn't come close anywhere except Humor. That's normalized absurdity for you! In any case, I have a lot to improve on for next time. I think better graphics and audio will be a rising tide that improves the rest of the scores too.

And some of them are really good. I'm super blown away by Total Party Kill which is a puzzle platformer which just gets everything, especially the theme, right:

And it deservedly won the compo!

Here are some of the other games I enjoyed as well:

I've come to appreciate that making games is an important part of my self-actualization. There are a lot of skills involved. Some of them I think I've gotten pretty good at: programming, game design, project management. Some of them I definitely need to work on: art, sound, polish. All those inputs feed into each other until a game pops out, which is judged pretty much on a single criterion: does the player want to continue playing? If I were to start a software company, evenespecially a non-game one, I'd look to hire indie game devs, since we're all singularly focused on user experience.

And with that… I didn't have a unifying message when I started writing this article. It took seven thousand words but I finally figured out what I wanted to say. However I'll save those proto-thoughts about how game design and UX design relate for another day.

Anyway, I'm quite happy with how the theme of sacrifices must be made, the absurd setting of pigs pirates and sea creatures, the beat-em-up genre, and the alliterative title of the game all tied together nicely. I think the game mechanics worked out well, both despite and because of the physics engine. I learned a lot about my limits and what skills to work on for future games. But, in a single weekend, I made a complete game from scratch, and it turned out to be one I can be proud of.

Overall, the whole Ludum Dare experience is very highly recommended! I can't wait for the next one.


Viewing all articles
Browse latest Browse all 36

Trending Articles