A Postmortem of RedShift Protoype
First of all, thanks to all who played my game! RedShift Prototype placed 1st Overall in Bullet Hell Jam 2021, and have received 18 insightful comments, thanks to you. Was it fun for you? I’m deeply grateful to you.
During the jam, some have asked me to do a writeup to document the process, and how the rewind mechanic was implemented. In this post, I hope to expose some of that.
Making the game
I have always loved bullet hell shooters, and made numerous attempts at making one in the distant past, but those never came to any fruition. The first “bullet hell” I ever made public is Designated Victim back in Ludum Dare 44, where it placed 18th Overall and 10th in Fun. Satisfied with the results of my first LD, I went on to experiment with other genres in a few more game jams that followed.
Until I saw Bullet Hell Jam in my inbox, I didn’t really have a recent plan to build a bullet hell again. As a result, I had to implement any systems from scratch. This did work to my advantage, however, since I was conversely able to architecture the code freely without the bounds of legacy code. This ended up to be somewhat beneficial to the rewind mechanic, as I will later explain.
When the theme, “10 seconds”, came out, I didn’t have any good ideas for it, and throughout the first day, I considered dropping out. It didn’t help that “X seconds” is one of the most common cliches in Ludum Dare slaughter rounds, one of which I have just finished before the jam. Eventually, however, I thought of something. A series of games that shared the initials with what my game was titled with. That’s what inspired me to make the mechanic of time reversal.
Turns out that even with some experience, the basic systems still took quite some time to code. I only finished all of them on day 3, by which time I still had zero content. I had to crunch quite a bit in the 4 following days, rushing out music, sounds, shaders and sprites, despite cutting a lot of content, even including the final boss that would have justified my game’s namesake (which is why it’s called RedShift Prototype, instead of just RedShift).
In the end, a lot of the game isn’t quite satisfactory, and I regret being unable to fully present to you what I had in mind. I never expected it to place #1, but to my surprise, people seemingly liked it despite its copious failings. Again, before I move on to more technical stuff, I’d like to thank you for reading this far.
Behind the rewind
While it might seem magical if you don’t know what to look for, the rewind mechanic is actually very trivial to implement. In a sense, I didn’t even implement it. Rather, I allowed it to happen.
If that didn’t mean anything to you, then consider an emulator of an arcade machine, or a classic console. Emulators often have this save-state feature, which allowed you to return completely to any early point in time: audio, graphics, game state, everything. Does this sound familiar to you? Yes.
RSP simply makes 60 save-states every second. That’s it.
Now, unlike emulators, modern game engines don’t usually support save-states, for a good reason. In an emulator, a save-state is just a snapshot of the RAM and registers of the virtual hardware. Making and restoring one is as simple as memcpy
. That’s easy, fast, and bug-proof. Modern engines have much, much more going on internally, making such instant save-states infeasible to generally implement. That’s why I had to allow it to happen myself.
What I did is:
- Consolidating the game state into a central SSOT. This makes it much easier to reason about history, than if the data is scattered in individual objects.
- Programming the old way, being very stingy with dynamic allocation. This allows me to
memcpy
most of the state, speeding the save-state process up drastically, enough for it to happen in 60fps, even on low-end machines. - Writing the game logic in the system programming language Rust, instead of GDScript, for easy control of said low-level aspects.
With the help of high-level features in Rust, the Godot Rust bindings (full disclosure: I maintain it), and a heavily modified version of hecs, it was fairly easy to code the game considering all the constraints. Once the rewind itself is working, the rest of the game is made normally, since the mechanic works outside the game logic. I could easily use the intuitive model of state and mutation in enemy / player behavior, without fear that rewinds might somehow become screwed up.
While time reversal itself is not a new idea, it was a lot of fun implementing it.
What went right
- Godot is a good engine and Rust is a good language. Those are what made the game possible in the first place.
- The full-screen rewind meter was a success. I’ve always wondered about how the extra space on a modern wide screen monitor could be used in a traditional vertical scroller, and I’m glad I spent some time to make this one look right.
- Crabs.
What went wrong
- Time management. Contents take a long, long time to make. Many things in the game are far from satisfactory in quality:
- The background animation is just a grid in perspective. This was originally intended for a tutorial, but ended up being using on the main game because I didn’t have time to make a real one.
- The music isn’t finished. It isn’t arranged properly. It isn’t mixed properly. Some of the placeholder instruments made it to the end. It even has some stock drum loops from the DAW in it. Oh well.
- Some of the sound effects are just made with sfxr. Bleh.
- The player doesn’t have an engine trail.
- There isn’t a focus shot pattern. It’s just the same as the normal one.
- The mid-boss isn’t animated.
- There is no boss. What?
- The game ends very abruptly with no stage end animation and no ending.
- The title screen is… the title screen. I’ll leave it at that.
- I didn’t ensure that it’s possible to achieve a full chain. I did design the stage with that (somewhat) in mind, but I didn’t have the time to check.
- Having worked with
hecs
for some while, I’m increasingly feeling that it’s being outgrown by my needs. I managed to patch it enough to fit the requirements this time, but eventually I might want do a hard fork of it or even reinvent my own wheels. Who knows? - There is no global or local leaderboard.
- Replays are recorded internally but not saved, nor can they be loaded anyway.
- The difficulty progression is not ideal. I didn’t have time to fully balance it out.
- I didn’t have time to add a tutorial, and while the controls and hit-chain mechanics are rather standard, many details went unexplained. This led to a very low “Easy to get into” rank at #74, the lowest of all categories.
- The theme isn’t that well-integrated into the game, in my opinion, despite the game ranking a decent #13 in the category. In my original idea, there would have also been a 10-second countdown boss at the end, which might have improved the result or not. I’m not sure how this theme can be implemented well, at all. Indeed, I didn’t really see anything that wowed me in this aspect among the games I played this jam.
- The rewind mechanic is not very useful for survival, compared to a traditional life stock system. While I added the score-keeping effect for scoring, it doesn’t really help players who struggle to complete the stage. I might need to think this over if I’m to take the project further.
- Many raters have noted that hit-rewinds can result in chain deaths. While I did anticipate this, and have added a release curve that delayed and then slowed updates for a short while after a rewind ends, it seems that the effect alone is not enough to prevent the issue. Hit-rewinds have the strong penalty of rewinding your score and removing your combo, so it might have been useful to add some extra survival benefits to them.
- Right after the jam, I participated in Ludum Dare 48 that immediately followed, so I didn’t have as much time to play and rate Bullet Hell Jam entries as others had, and it took a long time for me to get back to those who rated and commented on my game. Apparently, this caused me to place higher than I deserved, according to some people in the community. If my placing made you upset, my apologizes to you. Please don’t give up.
Wrapping up
Despite previous experience in game jams short and long, time management is still one of my weakest points. I really need to learn how to plan my time better, not just for jams, but also for longer projects. And while personally I’m not very satisfied with the game, I’m very glad that people likes it nevertheless.
Thank you for reading to the end.
Get RedShift Prototype
RedShift Prototype
Set correct what that went wrong.
Status | Prototype |
Author | thyrotoxicosis |
Genre | Action |
Tags | 2D, Bullet Hell, Pixel Art, Shoot 'Em Up |
Comments
Log in with itch.io to leave a comment.
Amazing post-mortem. The rewind mechanic was super creative and joining all of the game state into a SSOT kinda reminds me of the bullet hell pattern system I've implemented, which all the bullet data belongs to the shooter and not on individual bullets, so its easy to keep track on them, as such when the enemy gets killed and then all bullets are queued for destruction. I could see doing an implementation of a rewind for my game as well, but unfortunately not all the game logic is in a SSOT, which just wouldn't work.
I'm actually really impressed by the good amount of technical information in this postmortem, there are many things I didn't even know before. (I guess this is the problem of a Unity programmer that has all of these fancy tools but no deep understanding of low level programming...)
Good work, and you even participated into Ludum Dare afterwards! One thing that I've noticed that most jam games (myself included) unfortunately fall into, are balancing issues at release. You can do the most beautiful or well designed game, but in the end... There's always something that could be made easier, or more challenging. Or maybe you haven't noticed a detail that could potentially scare away some players, it's really tough. You managed to pull off really well surprisingly, and most things you've noted weren't even noticeable, audio seems really just fine to me (which by the way, the rewind effect just sounds amazing).
Looking forward for a more polished version of this game, really wish for an interesting boss!
Thank you! Yes, this would have been a lot more hairy had all the data not been in a single place. I had the freedom of starting from scratch, but integrating it into an existing project could mean a lot of kludges or even, in the worst case, rewriting everything, if your previous architecture was particularly incompatible (e.g. lots of global state, interconnecting references everywhere, etc.).
The balancing problem is very real. Truly, it's a very delicate process that requires a lot of time, which is in short supply during jams. Rewind was the main gimmick of my game, so I decided to spend some time to make it look and sound just right. The rewind SE is produced using a turntable plugin and some samples I had lying around. I'm glad you liked it!
If I do make a more polished update later, I'll be sure to post here!
was discussing with a friend whether you'd have been updating all the entities in reverse, then me wondering how you'd deal with entities being created and destroyed in that process, but to think it was just plain save states... suppose it makes sense with the relatively low rewind time.
interesting to know all the stuff you didn't manage to implement or had to rush, final product looks very polished so you wouldn't expect the original idea to be so different. looking forward to at the very least the boss update, it sounds like what would've been the most important part of the game.
cute girl btw
There is a way to extend the rewind limit with this method, too, that I didn't have time to implement: packing older chunks of state into the form of base state + replay, and unpacking them on-demand on a second thread. This way, the rewind period can easily be extended to the entire stage and more, all while requiring less memory footprint than the current 10s (actually 20s internally) implementation, at the cost of some CPU time.
I decided that it isn't really worth it to implement for the jam, though. I think that was a good decision.
I'm not sure if I'll be making a update to RSP this soon after the jam, especially with my LD48 game in need of an update too, but I might come back to it, some day, depending on the interest.
Thanks!