Venture-Out Postmortem - Creating a DOS Game in 48 Hours
So the Ludum Dare 44 weekend is over, and having had a few days to recover from post-LD fatigue, I present to you my postmortem!
Also, if you'd like to poke around some hasty jam-code, or the base framework, the source is here: https://github.com/64Mega/ld44-venture-out
Jam-code is mostly confined to the game directory, and almost all of the code in src is from the framework. Pardon the mess! It's a living project.
About the game
I began the game as an "engine-ready test": I've been working on a DOS framework/engine for the better part of two years now, and having finally gotten to the point I could make a game on it, I decided to do the insane thing and do a 48-hour jam on a completely untested engine.
The final theme-voting round is what convinced me to join up: The "Limited Memory" theme was pretty much spot-on if it happened to survive, but I was fine with the majority of the remaining themes. As soon as the theme was released, I started to brainstorm.
Eventually I settled on mixing two old favorites: Legend of Zelda and Arkanoid. The 'twist' and theme hook would be that your extra lives are also the currency you need to win the game!
I think I managed about 30% of my initial design: That hastily typed up document contained the outline for a game that would include two major world areas, a boss, switch puzzles and multiple "upgrades" to the paddle and ball, purchasable from the shop.
Naturally I had to cut things down as much as possible to fit within my development capabilities: I decided to have two chests with "special" objects in them, and if you could obtain both of them, you win.
As I mentioned in the previous section, I've been working on creating a DOS Game Dev Framework for around 2 years, alongside either a book or series of blog-posts showing people how to follow in my footsteps.
The current iteration of the framework is, perhaps, a month old: I've rewritten it from scratch several times over the last two years as I've broadened my scope and learned to do things I thought I couldn't before.
The first iteration of the engine used Mode 13h, a very easy-to-use graphics mode for VGA-compatibles, and I had absolutely no intention of working sound or music into the game.
Fast-forward, and the framework is compiled for 32-bit Protected mode (Meaning I have access to as much RAM as a machine could have back in the day), and utilizes something known as Mode X. I also managed to cram sound and music into the thing, thanks in part to a treasure-trove of old text files known as the PC Game Programmer's Encyclopaedia (Link), as well as the creators of the Reality AdLib Tracker (Link), who kindly included player code that I ported to 32-bit compatible assembly.
For sound I have to credit the creator of SMIX (Link), an old free-with-conditions sound library from the early 90s. It's functional, but the code is incredibly unstable: I had an explosion of errors towards the end of the jam-weekend when I linked it into my project, and it had side-effects galore that I spent about two hours stamping out. I fully intend to create a from-scratch properly Open-Source SoundBlaster library in the near-future.
The majority of the code is written in C, though a large portion of it is written in Assembly (NASM). I'm using OpenWatcom as my compiler, and for graphics I'm using Aseprite along with a custom PCX converter that takes a PCX and stores it in a plane-oriented format that is easy to draw to the screen.
The Development Process
I woke up at the late hour of 8AM on Saturday morning to begin work on my game. I live in the UTC+2 timezone, so the jam was already five hours in. I had decided the night before to get a good night's sleep and not attempt to wake up at an unusual time. I needed to save my energy for the sprint ahead.
I took a few hours of "design time": A cup of tea and a pad of paper. By 10AM I had the gist of things and had prototyped a few sprites in Aseprite. I then put a solid hour into creating a mockup that then became the source of my sprites and tiles:
From the start I had the idea of enemies roaming the areas in the game, and they are sort-of in the game: The sprites are loaded, but never used.
I decided on Day 1 that my first goal would be to get a paddle and ball moving on the screen. I did a few sprites, loaded them, then attached keyboard input. All was well.
I then decided to start loading a background, and that's when disaster struck: A strange "ghosting" effect was happening at a specific coordinate of the screen, and any sprite drawn to that region would have some of its columns drawn into the bottom-right of the screen for seemingly no reason!
This was a minor bug in my assembly routines, and at first I thought I'd have to just ignore it: Digging into 800+ lines of assembly isn't exactly time-efficient, and I couldn't guarantee that I'd find the bug. But after taking a short lunch break, I slowly read through the sprite-blitter and found the bug.
For those interested, this is what it was:
; Add starting sprite offset to buffer pointer: mov edi, dword [bufptr] add di, ax ; <- The bug ; The fixed code: mov edi, dword [bufptr] add edi, eax
This problem was truncation: The location on the screen where the pixels were appearing correspond with the 64K upper-bound of a 16-bit integer, and the ax/di registers are 16-bit in length: Yet I was working with 32-bit memory spaces!
Just this tiny swap (And a check to make sure I wasn't using the upper values of eax for anything important), and all was well.
I spent the rest of the day implementing a basic entity system and hard-coding two test areas into memory (Those two test areas became the final areas: I ran out of time to make more).
At the end of Day 1 I had a working ball and paddle as well as breakable bushes: A decent start, given the architectural implementation I had to deal with (E.G: Entity system, how to check for collisions between two objects and apply logic to it, etc).
Since I'd ironed out all of the major architecture bugs, I spent most of Day 2 working on features: The rocks, the bombs, the "inner" area (The cave), the shop, ball-drops from bushes, etc. By the time I was done with this modest feature list and the art for them, it was 1AM: Two hours before submission time!
So I decided to wrap it up and try to get music and sound working before packaging things up for distribution and testing.
I tackled sound first: I first generated the sounds using BFXR, then converted them in Audacity to RAW WAV (8-bit PCM unsigned) format, which I then passed through to a tool that came with SMIX for converting these WAVs to RAW format, and then yet-another-tool for turning those RAW files into a soundbank. This took longer than I'd have liked, and by the time I was ready to do music, I had 45 minutes left!
I created a one-pattern loop quickly in RAD Tracker, got it working, and then set about testing the distribution.
I finished the submission process a few minutes before 3:00AM - the submission hour technically begins at 3:00AM, but I was so dead-tired by the end of things that I had to upload or I'd collapse!
So, what went wrong?
Surprisingly, not as much as I was expecting! The engine was expected to give trouble as it was untested: I wasn't even sure I'd hit an acceptable performance level with what I was planning, but the engine turned out to be surprisingly flexible in that regard.
Time, as per the norm with Ludum Dare, was the biggest issue. Time management is key, and usually I'd be able to dedicate specific blocks of time to the implementation of a feature. Again, because the engine was an "unknown" in the equation, I could only wing it and hope for the best. If I decide to enter next time with the same engine, I'll have the experience of this game to build on, as well as more helper-code. This should let me allocate time in a better way for future jams.
I did have a big panic moment with the Sound library, SMIX. It was written originally for Turbo Pascal, and the creator did a really quick-and-dirty C implementation. The biggest issue it presented was that it would clash if you included the header file in more than one file at a time: This was due to the way the struct type was declared in the header and again in the C unit. Lacking time, I worked around this by including the header once in the main file, and providing a sort of flag-based event system for sounds to play (Which is actually a good way of going about things - but my implementation of an event system is hardly good given the time I had to work with).
It also causes an unfortunate exception with DOSBox: If you run the game in DOSBox, close it, then check the stdout.txt file in the directory you ran it from, you'll notice a bunch of exceptions:
Illegal read from 1010101, CS:IP 160: 17f524
This only appears if the sound library is initialized and a soundbank is read into memory, and this usually happens when uninitialized pointers are read. I didn't have the time to do any in-depth debugging, but as mentioned earlier, I'm not intending to use SMIX going ahead, as I'm wanting to create my own solution to digital sound anyway.
I also almost nearly didn't include the Game Over screen! In my frantic last-minute implementation of it, I was resetting all game data back to initial state. To do this, I was calling a function that initialized the global data structure that held pointers to certain functions, a pointer to the resource file and a few global flags.
What I had forgotten was that the pointers were set to 0 (NULL) on calling this function! So after the Game Over screen faded out, it called the initialization function, and handed over to the Logo screens: These screens now had no data to work with, since the data pack was now pointing to invalid memory! This resulted in an eerily slow fade to white, which then ducked back to black very quickly and began again.
At 2AM, I was almost too sleepy to catch this, but fortunately my wits won out eventually and I quickly fixed the issue!
What went right
The project could have been a nightmare to work with: I was using my own untested code to create an overambitious game in 48 hours. It went smoother than I was expecting: during the development of the initial framework, I've managed to crash both the emulated CPU and DOS, and beyond that was able to crash DOSBox itself once or twice.
I didn't have a single error along those lines throughout the project, and even in cases where I was forgetting to initialize pointer, I was just getting amusing palette corruptions and visual glitches!
The development process was pretty straightforward, and I found that most of the functions I had prepared beforehand were enough to get me through the basics: I could load, draw and move sprites of any size (So long as they're sized in multiples of 4!), and had keyboard input.
The implementation of the entity system was also fairly smooth, though I went for a bit of a brute-force approach. Instead of using fixed-length arrays of structs of different types to hold entity data, I would now rather go back and include an extra field in the struct that holds the index of the next 'living' entity in that array: This would prevent having to loop through 64 of each entity type every frame, multiple times, and would save a lot of time (This method is basically a fixed-length linked-list implementation, and is pretty easy to implement, if not very memory efficient).
What I intend to do next
So the game is "done", but I'm fully intending to do a few development passes on it after the rating/judging period is over!
First up is a few engine changes: I'd like to implement the faster entity system, and fix a few oversights in resource loading (The ball has to do a string comparison for each new instance created, since it gets a fresh pointer from the PAK file instead of reusing one).
Then come the gameplay fixes! I've had a few suggestions from commenters I'd like to work on, as well as some obvious problems: The ball only moves at 45 degree angles, the paddle feels a bit "floaty", and some of the elements I originally planned weren't implemented (Enemies, a boss, another 'zone', switches, an artifact or two).
I'd call that the "version 0.5". After that, I'd like to see if I can create a full-length game on top of what I already have, likely still targeting DOS (That was my original goal, setting out to create the framework two years ago).
For now, I'm going to play and rate games, and invite you to play mine if you haven't already! You can follow this devlog for any future updates with regard to the game too.
Thanks for reading!
Leave a comment
Log in with itch.io to leave a comment.