Post Mortem: Eternal Echoes




Overview
This game was for the Metroidvania Month 28 game jam. I got started about a week and a half late. First, because I was working full time on a commercial game with a deadline of May 28th, and second because I entered the GameDev.tv 2025 Game Jam (I wanted a free class). That jam started the day after Metroidvania, and ended on May 26th. So I had one day on the 15th, and then started really working on May 29th. That gave me 19 days.
I made the decision before the jam started that I was going to use all my art assets from Craftpix. I've taken two GameDev.tv courses for Godot that used these resources, which means I'm familiar with them. (I highly recommend their courses, especially the Blender ones.) One of the things I learned from my last game jam is to stop experimenting with new things during game jams. It eats a ton of time. Secondly, the frustration of preparing for the GameDev.tv jam was mainly centered around the limitation that you had to use only FREE resources. It was very frustrating.
I also wanted to use all the sound effects and music I'd bought from Ovani Sound and ELV Games in particular. They're awesome and I highly recommend both. (Ovani has bundles on Humble periodically, and every few months if you're on their mailing list they put out a huge new bundle of music and sound for $20. ELV has a Music pack for like $35 that they add 5 new songs to to almost every Friday.)
The other thing I wanted to use was a Godot framework I created for game jams back in Nov '24. I had just finished a jam, and someone had a Godot game template for game jams. It was awesome because it contained various settings, splash screens, and credits. I downloaded it, but I found it difficult to use. Partially it was the way it was coded, which was clever, but hard to decipher. Partially because there was no documentation on how to actually integrate it into a game.
So, I started making my own game template. I made it open source in case anyone else could use it, and put it up on GitHub. (Links below.) Two jams ago, I cannibalized pieces, most notably the Splash screen section. In the previous jam (GameDev.tv 2025 Game Jam) I used the menu system, splash and credits, but really hadn't even figured out how to hook up the menu to start the actual game. This jam I was actually going to integrate it.
Every game jam before, my goal has been to put out the shittiest game that works. This jam, I actually wanted to go for the win. So I wanted a polished, playable game. That goal caused me a lot of setbacks. I learned a lot about what I had, and didn't have.
Initial Game Design
Before the jam started, I had settled on a title and theme of Lil' Reaper. I had been rewatching the show Dead Like Me, and Craftpix had a like 6 reaper characters. I thought it would be a neat theme if you played a reaper and and were bullied by the more experienced reapers. In an older game jam I'd coded an NPC ghost that chased the PC and just said random stuff to him. So I thought perhaps this time I would make it so you escorted ghosts to various places to cross over (Dead Like Me reference), and each time you did, you got a new power.

Character Visuals
On the first day, I knew that I was going to want to use a number of characters. Each character had 17 animations, each animation had 6 to 24 separate sprites. I wanted to be able to import an entire character just by pointing to a folder containing the images, and then delete the animations I didn't need later so that the game wouldn't get so big I couldn't upload it as a web game (A problem I had with two previous 3D games in jams.) Happily, each animation was stored in its own folder, which meant I wanted the game to just use the folders that were there. If I deleted a folder, the game would know it couldn't use that animation.
I created a custom version of Godot's AnimatedSprite2D and called it ChibiAnimatedSprite2D. I made it so that in the editor you could click a button, select the folder, and the code would auto-populate all the animations. This is where I made my first mistake.
I wanted to test it out quickly, and I made a test Character class (based on a CharacterBody2D) that I planned to inherit for my Player and Enemy classes later. But I quickly hacked together an if-else chain state machine just to test it. I never fully got that machine out of my code. I re-vamped it for the Enemy but it stuck around for the Player and was a mess to deal with the rest of the project because I didn't have time to go back and fix it.
Game Template
Day Two started something like 12 days after Day One. I really wanted a polished-looking game, and I was determined to actually incorporate the game template system I had created. I spent the first few days actually revamping these systems. I come from a professional developer background, and I didn't want my code to be a mess at the end I had to refactor. (This happened anyway as I ran out of time.)
I had repositories for every part of my system:
- Disk - Save/Load functionality for games and settings (two different systems),
- Display - Changing screen resolution, full screen, multi-monitor support.
- Sound - Audio buses, volume levels, sound effect players, music player, songs, albums, sound effects, and SFX projects.
- Controller - Handles all player input (Input was already taken), including gamepads (XBox, PS, Nintendo), Mouse, and Keyboard. Manages multiple inputs and UI textures for all inputs.
I updated them each time I made a change and then re-imported it into my project. This lasted a few days until I realized I was eating up a lot of time on this. I started making changes in the game and stopped porting back. This was necessary for time, but it means I have a LOT of back-porting to do.
Player
I implemented every animation I planned to use for the player with controls and got the player jumping around on a level. I was showing someone how easy it was for me to swap out animations, and I swapped in a Knight for the Reaper. As I went to bed that night, I started thinking that my reaper game design was too ambitious. I thought perhaps a more straightforward fantasy themed game might be easier. No following ghosts for one. So, I switched gears on the theme.
Music
I love collecting game music. I find it inspires me to want to develop different genres of games. I also really like to support people making art. So I buy what I can. I then spend a lot of time organizing my music and listening to SFX to have an idea of what I may want to use in the future. As of writing this, I have 4,953 songs collected. I store them sorted into folders and then import them all into WinAmp. Then I listen to it, rate it and in the Genre field, I store keywords that I can later use to search for pulling music for a game.
During the game jam, I listened to music I hadn't categorized while I was developing/building, and I would categorize as I went. This screenshot is from today where I'm doing the same as I write this article.
While I was working on the Metroidvania Month 28 jam, I was playing and voting on games for the GameDev.tv jam. In one of the games, there was a really impressive end-credits song. I looked and he had used an AI website called Suno. I thought I'd try it out because it was free. I typed in something along the lines of, "Song about heartbreak and loss in the style of an anime JRPG videogame." It created a song with lyrics and the title Eternal Echoes. It nailed what I wanted, though unfortunately the quality on the high end wasn't great. I spent a whole day on a rabbit hole of trying to fix that before giving up. In the process I ended up with some remixes. I created a Soundtrack Playlist for anyone who wants to listen to the songs I made for the game.
I honestly had pretty mixed feelings about using AI music. I believe it's important for people to get paid for their work, and I have ethical concerns about how AI LLMs are trained. However I also realize that AI-generated content isn't going away, and I believe that as an indie developer with a team of one, I cannot do everything. Since this is a game jam and I'm not making money on this game, I decided that passing up on using a song that I really liked was something I felt ok about doing.
This song led directly to me coming up for the story that I ended up using.
Cutscenes
I really, really wanted cutscenes in my game. I've spent time designing a cutscene system on the whiteboard in my room, but never got around to developing one. So I prioritized this before gameplay - because I knew that once the end of the jam came, I wouldn't have a ton of time to spend on bells and whistles. And honestly, this was just that.
I had a lot of fun designing the initial cutscenes. I even initially used AI to create voiceovers. I ultimately recorded those myself. (See the Sound section below.) I ended up spending a lot of time on this. Again, I started with a prototype that was embedded in the first level just to get going. This ended up being a mistake that cost me time at the end because I had to pull it out because it was playing every time the player entered the level.
Once I got to the first line of dialogue, I stopped. I realized I didn't have a story, and I had no gameplay!. I had no levels, and I had no enemies. I did not have a game! This was the lesson I had learned from my previous game jam, where I literally had no game loop. I decided it was time to get organized.
Game Design Take 2
I took my game design notebook outside. I sat at a table, and I started by writing bios for these two main characters. I game them names and stories. I gave them parents, careers, and hopes and dreams. I decided that I wanted a twist ending. In an homage to Metroid, I wanted the knight to actually be the woman, but I wanted you to think it was the guy. I also wanted to head-fake the player into thinking that the woman was the necromancer, when it fact it was the man. This was a complete waste of time. I ended up wasting a day on echoing sound effects in the helmet to mask the knight's efforts to sound masculine. I also got to put very little of the story into the game - so it didn't matter. You never even get to see the bad guy. Plot twists are not a good idea in a game jam game. The plot must be streamlined
Still, this process did pay off, because I also outlined for each level what powers the player got, how they defeated obstacles, what monsters they encountered, and the boss. Then I actually drew out the level design on paper. This was the BEST use of my time the entire jam. I should have done it before I started coding.
Process
I have a stack of colored 3x5 index cards on my desk. I pulled out various colors and started writing out task lists on them.
- Red - Bugs.
- Pink - Things I could do after the jam.
- Green - Things I needed to get get done to have a working game and make my level design a reality.
- Orange - Stretch goals. (Things to do if I had extra time left.)
Armed with this list I started making a level and enemies. If some idea came up, I'd add it to the card. Unfortunately, I didn't start this process at the beginning. I forgot that I needed to tell people they could throw the dagger at the lock on the mine door to open it, and the first people who tried my game couldn't play it. I was so used to that feature, I forgot to document it. In the future, I plan to write in-game instructions as I go.
State Machine(s)
I've been working on StateMachine and State classes for a while now. There are a number way of doing them, but I wanted to use Composition. What that means is I can add a StateMachine node to an object, and then I can add and remove State nodes to change the behaviors of individual objects. I wanted the code to be generic enough that I could use it for multiple things. For example, the states that my game can be in, or the states that the player can be in, and the states of enemies.
So I created a system where a StateMachine assumes whatever object it is attached to is the parent object. It uses this information when logging to output what it's attached to. It also knows what States are attached to it. If a State is added or removed, even at runtime, the StateMachine goes through the process of activating or deactivating it. This can solve race conditions where a State has to be initialized, but that can't be done until the StateMachine exists. (In Godot, objects attached to parent objects are created first.)
I had intended to implement all this inside the Game Template, and I did, but only inside the game. (It still has to be back-ported.) The states themselves can define what happens when they are activated and deactivated, as well as what happens when they are entered and exited. I ended up revising the StateMachine and derived classes about 5 times during the last week of development. I assumed since I had implemented this before in C#, that doing it in GDScript wouldn't be too bad. I even had some prototypes so I was able to copy/paste most of the starting code. This was my first large-scale implementation of the system with modifications I'd made since I last used it in C#, and I underestimated the amount of thought, design (including whiteboarding), and implementing it would take. I fell into the "It'll be easy" trap.
GameStateMachine
The game itself needed to manage a number of states. (See the picture above.) It took a while, but I implemented a Loading screen. This allowed the game to boot up faster. The levels themselves didn't load until the game was started. In the future, I may implement another loading tier to happen while the splash screens are playing. However, I made it so you can skip them after you watch them once (a recommendation from the previous game jam), and this was a complication I didn't need to debug during the jam.
I didn't want States to be dependent upon one another for information, so I had the Loading state utilize a singleton Game object to send a signal when the game was loading. Then the Gameplay state just monitored that Game object for the signal. When that happened, it started checking the progress of the loading, and when it was complete, loaded the game level itself.
CharacterStateMachine
Then it was time for the hard part - handling all the states of the enemies. As mentioned earlier, I had already done a giant crude if-else monstrosity that I hated to get all the player animations. The plan was to move them to States in a StateMachine. The problem was that I was running out of time, and the Player worked. Having many years of development and project management experience outside of games, I knew that I was better off leaving the Player alone. This meant the underlying Character class had to be abandoned too. So I copied the code from Character to Character2D - both of which inherit from CharacterBody2D - a base Godot class.
So this is when my pre-planning got me in trouble. See all those blue State2Ds and StateMachine2Ds. I made those because when I made the little icons for the nodes, I also made blue (2D) and red (3D) versions in case I needed them later.
I didn't.
But I thought I did. See what I did next was overthink it. In my previous game where I had first done this (in C#), I had one two CollisionShape3Ds around my enemy. One to decide when to chase/return to patrolling, and one to determine when to attack. I wanted my new state machine to be more encapsulated and decided that they needed to own each CollisionShape2D underneath them. The problem with this, was State was derived from Node which has no positional data. So I developed State2D inherited from Node2D so it could store positional data for their CollisionShape2Ds. Since the StateMachine was above the states in the tree, I also needed a StateMachine2D. So now my code is diverging in a way as a developer I really didn't like. But at this point I have a week and a half left and no enemies to fight!
(The reason, by the way, that positional data was so important, was that I had created the Character base class in such a way that I could change the scaling of the whole character at the root, and everything beneath would follow. I had plans for giant-sized enemies, and tiny enemies, powers that shrank and enlarged the player (Shrink Slide and Colossal Kick). In the end, the only use I made of it was to make the goblins slightly smaller than the player.)
So, I implemented the state and had a pull-only state philosophy. I got this from the Kanban product management/software development system. Briefly, the idea is that a worker cannot be pushed a task, they can only pull from a prioritized list. And they cannot work on anything new until someone upstream takes the task away from them. So, I implemented this.
It worked mostly well, but there were a few snafus. At first, the goblins couldn't get hurt if they were in their throwing animation. I fixed that by adding a queue. Hurt and Death states were allowed to queue if they triggered. This resulted in a comedic pause when the goblins were hurt or died because they were almost always mid-animation. I also had layers of CollisionShape2Ds obscuring the goblins and everything around them in the editor. Turning them off and on for visibility required editing like 5 nodes. I then implemented the zombies and their chase/attack combo. I was having so many timing issues.
Two days later, I pulled the State2D/StateMachine2D architecture. You see, I only wanted states to enter when they were doing their thing. I was turning process() functions on and off to help with that, but I was still having issues. I also didn't really like the queuing system. It felt like a hack I hadn't needed before. I realized in reading an article somewhere, that StateMachines are in fact part of the object they are manipulating, even though - strictly speaking - they are added as part of OOP Composition. So it was ok that they knew about things outside them - namely the CollisionShape2Ds doing collision detection. With those out from underneath the states, I didn't need them to inherit from Node2D.
I also realized I didn't need to use a queuing system, because if I created the states correctly, the Hurt and Death states would appropriately interrupt other states and stop them from occurring. (Before the goblin's throw animation was getting interrupted, but the throwing was still happening.) I also decide I needed to push two states - Idle and Falling. So I implemented a way to do that. Then I realized the StateMachine should be the only one pushing states, and it could effectively do that to just Idle and Falling. And since it was doing that, I didn't need a way to push other states. (I couldn't let those two figure it out independently, because there was some if-elsing and other logic at the time, but I think I know now how to get the EnemyStateMachine to be generic again.)
Finally, after three days, I had a system that was what I originally envisioned and was working. I learned not to radically change system designs near the end of a jam. If I'd stuck with what I started with, I wouldn't have lost 2 DAYS.
Sound
I learned a LOT about sound design during this game jam. About 9 days before the end of the jam, on a Friday evening, a friend came over after work. He's a DJ and musician. He lent me a quality recording mic, and we recorded me saying all the cutscene dialogue. I had originally used AI because I didn't have a mic (at all) and with some tweaking, I was able to get almost what I wanted out of the AI. Good enough for free at least. With me recording it, I got the exact performance I wanted.
He also taught me a lot about sound effects and sound design - something I knew nothing about. If you've played the game, you'll notice that the character's efforts (sound effects for jumping, throwing, hurt, dying, etc.) all sound echo-y as if inside a helmet. I spent half a day trying to get that to work, and then my friend and I spent part of that recording evening fixing it. He also helped me do sound levels so the music wouldn't overpower the voiceover or game. I learned a little more in Audacity than just how to covert WAV files to Ogg Vorbis. I didn't get a chance to implement everything I learned in this game, but for the future I know what I would do differently.
One of the time sucks was me putting in an Easter Egg no one would catch because I didn't finish the game. So it was just amusing for me. In Metroid, no one knew Samus Aran was female until people started beating the game in under 5 hours. So, I decided in a head-fake that you would think you were playing a male protagonist, and that the bad guy (never created) would appear to be the female love interest from the beginning of the game, shrouded in mystery. The end was going to reveal that Amelia was under the helmet the whole time, and so all the efforts came from Ovani Sound's Your Voice Actor: Hannah Weeks Voice Over Pack. It was a day of effort wasted in the jam because no one ever knew.
HUD
The HUD took much longer to get working than I planned. In fact, it took probably two days of time. It was my first time utilizing a feature I'd put into Dragonforge Controller months before. It had a function, that if you passed it the name of a mapped action (like "jump"), it passes back a texture that matches not only the action, but the last input device used by the player. It recognizes and has all the buttons/etc for keyboard, mouse, XBox, Playstation, and Nintendo gamepads. (though I have yet to test the Nintendo one - full disclosure.) I'm really proud of it, and it works! If you're playing with your controller and bump your mouse, all the HUD icons on screen immediately change - and vice - versa. It was a lot of piddling to get them looking good, and I had to add a new signal that went out whenever the input type changed so that the HUD only checked for new textures when that happened.
Then, I exported the game...
Export Nightmare
Exporting in Godot is easy and simple for every OS except iOS. However when I exported my game this time, all the characters except the blacksmith were gone from my game! They were there - I could tell - just invisible. It turned out that something I had done in the clever importing didn't translate to exporting to Windows. I ended up having to mark every character in the game as Editable in the editor for them to work. I've since discovered the actual bug has to do with the fact that Windows doesn't handle capital letters in filepaths well. I knew this, and I'm very careful to name everything with snake_case. However - and here's the kicker - Craftpix uses uppercase letters in their filenames and I didn't even think about it.
The next problem was none of the keyboard keys came through. My XBox and Playstation controllers worked fine - I could even hot-swap them mid-game. But move that mouse or press a key and everything was blank. A frantic Godot forums blog post helped me track down the issue. In this case it was that apparently the Godot editor is case-insensitive, but some of the action key names I was being passed by the system had uppercase letters in them. But Windows was case-sensitive and needed the names to match the lowercase filenames. Fixing that solved my problem.
Just to top it all off - after I got everything exported and submitted 5 minutes before the jam ended - it turned out the last version for the web ONLY contained a png and nothing else. I was able to quickly fix it and re-upload it with Butler.
Butler
I would not have submitted a game without using Butler. If you haven't used it, look at how easy it makes pushing builds to itch.io. It only uploads a diff, which makes a HUGE difference for me. My games for jams are always 150+ MB in size, and they take forever to upload. My internet connection sucks - especially on the wekeends when most game jams end. I have missed submitting games to 2 jams because my internet just failed me on the last day. Butler not only works with bad connections by just waiting, it also only uploads a diff to your last upload. That means I can upload a full build a week (or a few days) before the end of the jam, and any subsequent build I upload will only upload the changes and be SUPER FAST. This allowed me to do like 5 revisions on the last day.
Using Butler also allowed me to catch my export bugs early, because I just exported something to get it up there.
Submission Page
The page for the game I made a week before the end of the jam. I knew I was going to be fixing bugs and adding features until the last minute (literally it turned out.) So I got the background and text up. What I forgot was screenshots and a cover photo. I made a cover picture in the last 30 minutes of the jam. The screenshots were literally in the last five minutes, after I submitted the game. What I learned: Do the submission page early - and the screenshots/cover photo - just in case.
Testing
I did a lot of testing. I knew what was going to work, and what was going to suck. I spent half a day just tweaking the jumping and movement speeds and velocities to get something that felt good. I knew the zombies were a little too aggro. I tweaked the goblins so you didn't jump into their throwing knives every time. I made the boss as good as I could in the time I had (and revision 2 was much better), but lack of time really showed me he wasn't going to be as cool as I wanted.
One thing I forgot through all the testing was that I didn't tell the player to "pick" the lock by throwing a dagger at them. I also didn't take into consideration that the jump/throw to get through those doors, while an homage to shooting Metroid doors - would have been less annoying if I'd made the lock throwing level.
Conclusion
Overall, I was pretty pleased with the result. It is my most playable game for a game jam to date. I was underwhelmed with the amount of content I completed. I was kinda fiddly about pillars holding up most of the ledges in the background - and that was time I could've spent other places. I spent a lot of time doing things that amused me, or would've been cute if I'd finished the game (I had three levels planned) but I didn't get there - so that was days of accumulated wasted time.
The biggest thing I learned though is that a month-long game jam on top of work deadlines and another game jam was way too much pressure. I killed myself, and spent this whole week recuperating, just so I can start another game jam tonight. Pacing myself is important, and I should only plan on completing a third of what I actually plan.
Get Eternal Echoes
Eternal Echoes
A fantasy Metroidvania that tells a story of lost love.
Status | Released |
Author | Dragonforge Development |
Genre | Platformer, Action, Role Playing |
Tags | Action-Adventure, Action RPG, Metroidvania |
Leave a comment
Log in with itch.io to leave a comment.