Ludum Dare 33: Post Mortem

Shark Swimulator Title Screen

Another Ludum Dare has come and gone. Colby and I decided to participate again, this time taking a more active approach than four months ago when we decided to enter on a whim. We spent about a week setting up a code base we felt comfortable with and even came up with a few ideas for each potential theme to help us get the ball rolling right away. We also too an active role in voting and were very happy with the theme for Ludum Dare 33: You are the Monster.

To prepare our code, we first decided on the libraries we would be using so we could create template project that was ready to go. Firstly, we have recently switched paradigms and have moved away from class-based object-oriented programming (OOP) in favour of data-driven entity-component-systems (ECS). We have significantly improved the readability and sensibility of our code with this switch and I plan to do a write up about it at some point. Knowing that we'd be using ECS, our first library of choice was the truthfully named tiny-ecs. We've been using tiny-ecs for a little while now and it's just magical. It works great, is super fast, and very flexible. We were intent on making a 3D game so naturally we included LÖVE3D, CPML, IQE, and our new animation library anim9. I'll note here that we had a serious bug in our IQM library that has since been fixed, but was not ready for Ludum Dare. Our IQM library is about 40x faster than the IQE library and provides extra data that we had to improvise during the jam. More on that later.

We recently met some folks on the #love IRC channel that had created some really nifty tools that we decided to test out during the jam, and buy was it worth it. Talkback is a very similar library to HUMP.Signal but with the ability to return data from the signal. While we didn't leverage that particular feature during the jam, we felt it was worth moving to Talkback anyway. We also liked the cute API. Another terrific library we were introduced to was Tactile, a really handy input library. To put it shortly, it made integrating keyboard, mouse, and game pad controls simple and reliable. We will be using this one for more projects in the future. Finally we were casually discussing the frustration with chaining callbacks during events such as setting up timers to fade a rectangle out, hold, then fade back in during a screen transition. A great fellow in #love popped into the conversation and showed us his freshly minted knife.convoke library that uses coroutines to break and continue through a function. Simply brilliant. It turns horrible code such as this:

local fade = { opacity=255 }  
timer.tween(0.75, fade, {opacity=0}, "linear", function()  
   timer.add(4, function()
      timer.tween(0.75, fade, {opacity=255}, "linear", function()
         Scene.switch("scenes.title")
      end)
   end)
end)  

to this:

local fade = { opacity=255 }  
convoke(function(continue, wait)  
   timer.tween(0.75, fade, {opacity=0}, "linear", continue())
   wait()
   timer.add(4, continue())
   wait()
   timer.tween(0.75, fade, {opacity=255}, "linear", continue())
   wait()
   Scene.switch("scenes.title")
end)()  

Isn't that so much easier to read and comprehend?! Definitely a keeper. Finally, we had such great feedback from our last Ludum Dare entry due to the voice acting that we wanted to incorporate that into our new game. Only this time with a bit of a twist: we wanted localizations! The woman who did the vocals for our last game is bilingual in both English and Deutsch so we wanted her to translate the lines I wrote to Deutsch and then voice both audio tracks. We wrote a pretty simple internationalization (i18n) library that handles loading and fetching locale data and ended up putting it to good use. More on that later.

A heavy customization that Colby made to LOVE's boot sequence was to implement hot reloading of the game. By pressing F5, we could load the game without needing to restart it, and we start the game in any state we desired. This ended up saving us a lot of time as we could quickly reload the play scene without needing to use any game menus. With our template set up, we wanted to create some logos for a splash screen.

The logos were pretty simple. For the Excessive logo, Colby grabbed a common manga font and drew a cute little heart on it. As I said, it was simple. The LOVE3D logo was a bit more challenging. We wanted the logo to look natural so I started with the LOVE logo, opened it up in Inkscape, and drew the "3D" using Bezier curves and adjusted them until the new glyphs looked like they came from the same font. It is worth noting here that the LOVE logo was custom designed and there is no font file that I could have used to whip up a logo. With everything ready to go, it was time to start writing our game!

Day One

The basis of our game is that you are a shark and you are trying to rescue divers from shark cages. We wanted our monster to not necessarily be bad, just misunderstood (and also misunderstanding). To start, Colby whipped up a shark model with a simple swim animation. He also whipped up a very simple water world using an inverted cylinder and basic Perlin noise for the sea floor. While he was blending models, I set up the render system so that models could be drawn to the screen. With a player model, a world to play in, and a render system to spit out pixels, I got to work setting up our input and movement systems. In the mean time, Colby built what ended up being a really swanky particle system that really brought the world to life. We gave the shark air bubbles and the world random debris particles. This really helped with sense of scale, sense of location, and sense of motion.

While I continued to tweak the input and camera controls, Colby built a simple notification system that we leveraged to display language information and other such data. It ended up working really well and looking nice, too.

Finally, I spent my last waking hour writing a basic script for the game that we would use for localization and vocals. After 12 hours, we had a shark swimming around an ocean, a script, and a game plan for the next day.

Day Two

After a short but welcome slumber, we got straight back to work and started adding game play mechanics. We decided that since we were forced to fallback to our IQE loader instead of our IQM loader, we would use very coarse spherical collisions. IQM comes with per-frame bounding boxes already calculated whereas IQE does not and we didn't want to spend any precious time building them. Spheres are extremely simple to use and are good enough for a game jam. Spherical collisions consist of a simple vector distance check to see if the distance between the centres of spheres A and B is less than the radii of spheres A and B. If so, the spheres are colliding. While this works great, our objects are not even remotely spherical in shape so what we did was create several small spheres and place them along the objects' bodies to more closely represent their approximate shapes. As I said, good enough for a jam.

With collisions working, we wanted to improve our movement by using basic physics (F=ma). We wanted to simulate collision physics without actually reading up on it by transposing some velocity of the player object into whatever it rammed into. We ended up with a bug in our code that caused our force scalar to be completely wrong, but we had to roll with it by approximating the values we wanted using eye judgement. This ended up working but caused my collision system to break.

The first collision system registered a collision with any collider sphere on any object and its collisions with another object. This was a problem because it could end up registering six or 12 or more collisions at once instead of, well, one. I adjusted the code to register per-object collisions as well as per-collider collisions and only act on object collisions. This cut down the registered collisions to two per collision. Better, but still not what we want. Since the game is simple and very player-centric, I decided to only register player collisions. This brought us down to the one collision we wanted. However, there was a serious bug in the collision code that had me pulling my hair out on and off for hours (I had to leave the problem to work on other things several times due to time restraints) until an off remark by Colby led me to the answer. What was happening was that each collision sphere was overwriting the previous sphere's collision (or non-collision) so only the last collision sphere was actually working. This was a quick remedy and suddenly our collisions were working very well. We could now slam into other objects and it would slow us down and bump them in the direction we hit them.

While I was jumping back and forth between collision and other problems, Colby created a diver model with a swim animation and spent a lot of time prettying up the world using nicer looking particles and various shader effects. His time was well spent as the game became quite beautiful in a short period of time and we've received a lot of feedback on just how pretty the game is. When taking a break from collision, I built level files that moved a lot of code out of our scene and allowed us to make several different levels. I also spent some time working with Nadja to get our English track finalized.

When we were spent for the day, we linked some screenshots of our game in #love and other places and people started pouring in asking if they could translate our game to their native languages. We were very surprised and happy with this, obliging anyone who wanted to translate for us.

Day Three

I woke up to a very happy surprise on the third day. Several people came through with their translations and our localization idea exploded beyond my wild imagination. Our game jam entry ended up being translated to eight different languages (English, français, Deutsch, italiano, português, Eλληνικά, polski, and Русский) and even a joke language (PHP CEO). I received the Deutsch vocals and even PHP CEO vocals during this day and that really made me happy.

With all these locale files, I decided to prioritize our i18n support and got to work hooking up even listeners so that text could be drawn and audio could be played during various events such as a level starting, the player hitting something too hard, winning or losing the level, etc. I was really surprised at just how simple the i18n library was to use and it worked flawlessly right off the bat. Suddenly our whole game came to life as Nadja and EntranceJew's voices rang out, subtitles displaying below. It was great.

With the event listeners in place, setting up the win and lose conditions of the game were simple. I had the diver hooked up to a simple rope and when the player slammed into the rope, it would break and the diver would swim to the surface of the ocean. If the diver reached the surface before the timer ended, you won and the next level would load. If he didn't make it in time or if you accidentally ate him, you lost and a menu would pop up asking if you wanted to restart the level or exit. Colby added a blood explosion particle to the player's corpse to give a better sense of urgency to the player. Colby also added a pause to the lose menu so that you could spin your camera around and look at all the particles frozen in time. It was a really cool effect but we needed to make adjustments to accommodate.

What we should have done from the beginning, but didn't, was separate the camera input and movement from the player input and movement. We finally decoupled these systems which gave us more flexibility such as being able to spin the camera while the player input is locked.

Colby finally pumped out a few cage models in various states of repair and a built in diver that allowed us to really pull the game together. The level would spawn with a game at 100% health and as you rammed it, it would change its model until it finally broke, spawning a separate diver entity that would then try to swim away. With this, we finally had our game. You could play, win, lose, it was a game!

I added actual options to our options menu (change language and volume) which were saved to disk as a JSON string. Loading the preferences (or setting defaults) into game made it feel a bit more complete since we hoped people would be happy to set the game to their native language and then have that information kept instead of always reverting to English.

Burnt out from coding, I decided to try and make a few sound effects. I found a straw and filled a mixing bowl with water and blew bubbles for a while... It was kind of relaxing. The bubbles sounded fine but I wanted them to sound more oceanic instead of... mixing bowl. I grabbed a favourable section of the audio clip and cut it from the rest, slowed it by 50% and added fade in and fade out. It ended up sounding pretty swell. We used that for the diver's breathing.

Another sound effect I made was the bone crunch when you accidentally kill the diver. I took a piece of celery and split it apart in front of my mic several times to create various sounds. I then compiled several of the sounds together and used a sound profile to reduce the noise, ending up with a loud, somewhat realistic crunching sound.

The final act of the third day was to create a logo for the game. Earlier I had joked that if we didn't create game mechanics then we could at least say we built a shark simulator--swimulator, if you will--and the joke was well received. Shark Swimulator became the name of the game and we grabbed the generic "simulator" font (Helvetica Ultra Compressed Italic), added a couple bubbles and an offset shadow, and voila! a really great looking logo. Too good, honestly.

Day Four

On the final day we spent our last five hours finalizing the three levels in the game, added a boat and rope to the scenes, adding a win screen, and creating a special credits screen if the player finished the game. We ended up declaring our game to be finished about an hour before submission period which gave us time to tweak a couple things here and there.

After we submitted our game, we took an hour break and after submission period ended we decided to keep our live stream running and started playing other Ludum Dare entries. We ended up playing 60+ games over nine hours with a joyful audience of about 20~ people.

Conclusion

We had a blast making this game, playing other games, and just partaking in the experience of Ludum Dare yet again. We plan to take this game further by fixing bugs, cleaning up the code, and adding more "game". If we end up with something we deem very fun, we might even try to sell it. ;D

You can play our game or download the source (MIT license). We'd be happy to hear any feedback! <3