Endless Attack REMIX Development Journal #3
09 Oct 2022
Welcome to (*checks notes*) the third development journal? Weird, usually my projects don't make it this far before being scrapped...
Despite VCF coming and going without a demo prepared, development over the last few months was pretty active. There were a few weeks where I had to pause any work in order to focus on university, but other than that I've been taking any opportunity I can to at least mess with things.
Say hello to Stonecutter!
At VCF, I went in with one goal: get a system solely to test REMIX on. I was looking to get a high-end 486 system of some kind, so I could get a good idea of how REMIX would run on actual, period-correct hardware. As I started going down the merchant hall, one system stood out to me: A Packard Bell Pack-Mate 3500CD. There were 3 specs listed on a sticky note that caught my eye:
- Pentium 75
- 16MB RAM
- Aztech sound card w/ OPL3
I immediately went for it, and before leaving the show, I picked up a CRT that matched. So, here it is: the dedicated REMIX testing system!
The Pentium 75 is a little faster than I would have liked, but it's still a really good indicator of how the game will run. The 16MB of RAM is split between 2 DIMMs, so I can drop down to 8MB if need be (since that's what I'm aiming for at the moment). The OPL3 sound card was a huge plus, as that's the lowest common denominator I'm writing the music for. It's cool getting to hear my music through an actual YMF262 chip.
Time to time
I suppose I should talk about this here since the next couple of sections mention it, but the timer system I mentioned I had trouble designing in the last journal ended up being much more simple than I expected. Instead of using identifiers for each timer, I just use variables called "timer counters" to keep track of time. The reference to a counter is passed into the timer functions, and is then compared to the target. If the counter hasn't reached the target, the function returns false. If it has, the function returns true and, in the case of periodic timers, the target is subtracted from the counter. In both cases, the counter is incremented by the time it took for the last frame to be computed, rendered, and displayed. That's really all there was to it!
There are now actual, killable enemies that spawn in the game! So far, it's just the first enemy type in the game (Spikeys), but constructs are in place for more types, how fast they should spawn per wave, and how many points they give the player.
Enemies can also take more than one hit to kill.
Something I'm still trying to figure out is how to give each enemy their own, unique behaviors while still being able to lump them all under a single "enemy" struct. The way enemies are currently handled involves an "enemy type" field in the enemy struct, so when the enemy update function is called, the type is used to determine which "behavior" function to also invoke. Issues arise, however, when the idea of these behaviors needing additional fields than what's provided in the struct is brought up. For example, what if an enemy shoots a bullet multiple times? It needs at least an additional timer and a counter variable to tell when to stop. These can't just be added to the enemy struct, because all enemies would end up having these fields as well. And as more and more enemy types are implemented, the enemy struct could become huge!
Another idea I had was just defining a small chunk of memory in the enemy struct for each behavior to interpret differently, but if you open up a thesaurus, go to the word "fun", and look at the antonyms, you'll find "defining a small chunk of memory in the enemy struct for each behavior to interpret differently" in there. Weird!
This is something that inheritance in object-oriented languages is really good for. Unfortunately, I made the decision to write the game in C, which- surprise!- isn't really object-oriented, so I have to live with it. And cry. A lot.
oh, bullets have health too by the way
That's not how it's going to be described in-game, but there's a "piercing" system for player bullets, which determines how many enemies a bullet can collide with before disappearing. Since I was in the middle of implementing enemy death logic, I decided to implement this as well. You can see it in action below:
Player bullets have no piercing.
Player bullets have 1 level of piercing.
There are some specifics regarding how bullet damage will work with piercing, but I still have to figure out which route I want to take with that. The game is somehow already getting pretty deep mechanic-wise!
How to make enemies
Sure, adding in enemies seems like a super simple task. In most game prototypes, you'll usually have basic enemies implemented within at least the first week, though this depends on the genre (for instance, RPGs might choose to focus on overworld first, and that's fine). Since I practically had to write a game engine before doing anything interesting, there was a solid year before I could even put the first enemy in the game (granted, most of that year was spent not working on the game...). It could have been sooner, but there were two important things I wanted to tackle before doing so: animated sprites and hit detection.
Animated sprites work using the "spritebank" system I mentioned way back in the first journal. Turns out I didn't really need to implement a dynamic array for it (though I did to some extent). Spritebanks are essentially a collection of RLE sprites, formed from each of the individual sprites in a sprite sheet. It's really more so meant to be used with sprites of equal dimensions, though in the future I could extend it to spritesheets with mixed sprite sizes as well; I digress.
Animated sprites are then given a pointer to a spritebank (multiple sprites can use the same spritebank, saving memory), and the current sprite to be displayed is simply a pointer to an entry in that spritebank. Animation gets controlled using an array of tuples, where each tuple is a frame: one value is how long the frame will be displayed before moving to the next, and the other is the index of the frame in the spritebank. The final tuple determines how the animation will loop; it can either go back to any frame in the animation, or simply freeze at the last frame.
Animated sprites also have the ability to flicker at a specified interval, which is a common effect that was used on systems without transparency support (Allegro has support for transparency, but I'd rather have the game perform well on older systems!). I'll most likely use the effect sparingly, and at slow intervals.
With the animated sprite system out of the way, I replaced the single-sprite system that game objects had, making it so every game object uses an animated sprite. For objects that only need a single sprite and no animation, it's pretty easy to make an animated sprite with no animation control array. As an added bonus, animated sprites were designed with the ability to use them outside of game objects in mind, in instances where hit detection or movement isn't necessary.
From the start, I knew that hit detection would be pretty costly CPU-wise. So, I decided that the engine would only support 3 collision shapes: rectangles, circles, and lines. It's enough for a game like REMIX, and in the case where more complex shapes are needed, I can just add additional shapes to a game object (spoilers!).
Any two shapes can be checked for collision using what I like to call a "mux function", which checks the types of the two shapes, and returns the result of the appropriate collision function (e.g. rect-rect, rect-circle, line-circle, etc.)
Each game object can then have collision shapes added to them one at a time, with support for positioning at an offset from the object. Collisions between two objects can also be checked; this just checks if any of the shapes between the two are colliding. There's... really not much else I can say about this. Much like animated sprites, collision shapes can be used outside of game objects, which makes them useful as regions (or sensors? I don't know what engines other than Stencyl call them).
Retracing my footsteps
In the previous journal, I talked about how the engine is able to determine the framerate of the game in order to compensate for dropped frames. As a refresher, this relied on measuring the time it takes in-between each vsync, and doing some math to get the maximum possible framerate from that. Then, by measuring the time taken to compute + render a frame of the game and comparing it to the minimum frame time, the speed to run the game at could be determined. This worked for the most part, but issues started to appear that I hadn't considered.
When I got REMIX running on Stonecutter, at first the game ran pretty smooth. However, after a few test runs, something happened to throw my whole frame-timing system into question: the game stuttered HARD, from startup to exit. The framerate graph switched wildly between 70 and 45FPS constantly, despite the game running at a solid 70 in the previous run. When the game was restarted, it was back to a solid 70 again, as if nothing had happened.
This stuttering behavior would continue to show up here and there every several tests of the game on Stonecutter. So, I headed back to the drawing board. How should I make frame timing more accurate? Ah, how about changing how the minimum frame time is measured? I had noticed that, with each run of the game, the time measured was never really the same; so what about doing multiple time measurements, taking the smallest value, and calling that the "true" minimum frame time?
Er... let's just say I'm not an expert with display timings, because calling Allegro's vsync() multiple times in a row with only a few instructions in-between led to the game simply freezing during the time measurements. Ok, new plan: don't change anything! "Surely I can fix this one later," I thought to myself.
And I did, about a month later. While perusing the Allegro timer system API, I stumbled across a peculiar variable I had previously overlooked:
retrace_count. Essentially, this is an integer that is incremented by 1 at a fixed rate (70 times per second), regardless of how "busy" the game logic is. So... I had written much of that frame time measurement system for nothing. Cool. After a week or so of determining how difficult it would be to replace the time measurements with a simple check of
retrace_count, I went for it.
And I'm glad I did! The game became noticeably more stable; apparently the "stutter patterns" I mentioned in the previous journal weren't a side effect of DOSBox: it was just my frame time measurement system being inaccurate. Similarly, the more severe stuttering issues on real hardware went away as well.
Unfortunately, there are some caveats to using
retrace_count, most notably the fact that the game will never be able to run any faster than 70FPS on modern platforms. There's a slight chance I could be wrong about this one though, but I'm pretty sure
retrace_count still increments 70 times per second regardless of the display's refresh rate. And since the game speed is now tied to the difference in
retrace_count between frames... yeah, you get the picture.
I've only tested the Linux port between the last journal and now. I honed the build process for it down to the point where switching the build target is a simple
make clean and
make TARGET=... away, so I'm now able to test the port quite often in conjunction with the DOS version.
The use of
retrace_count earlier and some small modifications to how timers are calculated led to the game running at a playable speed again (instead of 2000FPS- did I forget to mention that last time?), though things continue to be capped at 30FPS for reasons I still can't discern. I'm grasping at straws at this point, so if you've written stuff with Allegro 4 and might know why this is the case, please don't hesitate to get in contact!
Other than stability fixes, I played around with getting sound to play under the Linux port. It doesn't seem like MIDI playback works at all (even with timidity running as an ALSA MIDI server), which is a shame. On the flipside, I got streamed OGG playback to work, so that's now on the table! What I didn't get working was any sort of sound working under ALSA, which is concerning since that's the only "still relevant" sound system for Linux that's supported by Allegro 4. What I had to do instead was install alsa-oss and tell the game to use OSS as the sound system; then it worked fine. Not sure how I'm going to deal with this later on down the road, but at least it works under this very specific configuration.
During the couple of weeks where I had to pause development, I was still able to do a few things related to the game that weren't as stress-inducing. For instance, the game now has a website!
It's not live yet; I'm still waiting until more content is added so I can make a proper gameplay reel and add some screenshots. But other than that, it is ready to go.
I also started writing the game's design document, something I had neglected doing for quite a while! So far I have most of the specifics of the game's rules written down, which should definitely help now that gameplay features are beginning to be written into the game.
How about smaller things related to the game/engine code? There's a new input system, which provides a way to check if a control was pressed, or if it was released. Prior to this, the only way to get input state was to read from an array that just said if a control was being held down or not, so I had to keep adding in additional "stops" to menu-related input code in order to prevent them from being unusable. Not any more!
I also ported over the logging system ("Tail") that I made for Stencyl and my previous engine attempt, Origami. It can write to a log file and stdout! Wow!!
There are a few other small things I implemented into the game/engine over the past few months, which I didn't really feel the need to talk about in-depth here: the shop display reference from the last journal getting rendered in-game, nineslice window scaling, asynchronous fades, a frame stepper for debugging purposes, and probably more that I'm forgetting...
As you can see, a lot of the engine side of things has been completed (or at least near-final), and at this point it's mostly up to pure game development. Sounds like the demo is closer than ever, right?
Unfortunately, the cracks are beginning to show in how I picked out my courses for this semester. I mentioned at the beginning of the journal that I've had to ignore development at times in order to focus on these courses; they're difficult. I could complain for another paragraph about how the constant attention shift between schoolwork and personal projects is tearing me apart mentally, but I really shouldn't. Point is, I have no idea when the demo will be ready. I have the entirety of December free to work on the game, and the hope is that I'll have it ready some time early next year, but the next 2 months could end up with barely any extra work done on the game. Other people seem to be able to juggle university and a job just fine, but I simply can't. It really sucks!
Until next time, then!
Thanks for reading to the end! I noticed this journal didn't really have much to look at, so let's fix that.
Testing the animated sprite system with one of the boss animations.