It’s been a bit longer between posts than we were hoping for. I (Dan) usually write these posts on the weekends. But here in development land and in the real world, it has been pretty busy. A couple of quick stories before we proceed:
- Two weekends ago, my computer started micro stuttering the game weirdly. At first I thought it might have been something to do with my graphics card. However, after some testing, it was concluded that my hard drive was starting to go bad. More on this story later.
- We were able to show the game to some familiar faces who hadn’t seen the game in a couple years, as well as new faces who were seeing the game for the first time. The feedback was amazing as, for the first time, we were able to see a lot of the game mechanics for others actually come together and work as intended. This was really exciting as it reinvigorated us and reinforced that we have something good here!
The good news in the extra time between posts means there is a bit more to show!
Horse
The title of this post is two-fold. First, there were several concepts and functionalities that we experimented, or, “horsed around” with. Second, of course, the horse!
The prototype horse has been in the game for a couple of years now. It’s hidden away in a barn and we usually don’t tell players about it and let them discover it on their own. For those players that do discover the horse, they absolutely love it (even in its terrible, prototype state). We decided to have Tony create a walk and gallop animation for us to test ideas and concepts out with. Below are those animations:
The animations feel very good in the game. However, one of the reasons we haven’t gone “full force” into the horse is of course the additions of all the new different kinds of animations we would need. We had an idea to potentially save time by cropping out the bottom half of our hero’s animations and replace them with a sitting / riding animation. However, this idea in practice is probably not going to end up working. We’re still “horsing around” with ways to make animations go quickly as we know everyone loves the horse. However, we still have a lot of other art / animations to get done before our Kickstarter goal and reworking current animations just to ride a horse may not be time best spent.
Reptile Updates
We also have a few more animations to share about our good friend (rather, enemy), the Reptile, that we introduced in our previous post:
Sound Effects
It’s been long overdue, but at the beginning of July, Noah Flack began replacing many of the old, crusty placeholder sound effects with some new, polished sound effects. We started down the road of creating sound effects from scratch, but it seemed more feasible to use a library of sounds and update / alter them according to our needs. The library we are using in their licensing agreement has a restriction of “redistributing” these sound effects elsewhere. Though this post isn’t intended to be a place where we are redistributing sounds (as well as many of these are heavily modified and would be unrecognizable from the original source), we felt it would be safer not to showcase any of those sounds from that library. However, we do have a couple of sound effects that we created from scratch. Here are a few below:
Knight Ghost Hit
Knight Ghost Death
Raining Outside Perspective
Raining Inside Perspective
UI / UX Updates
There was a reason why we wanted to tell the stories at the beginning of the post that we did, and here is the first reason. While watching folks play the game, we noticed a few reoccurring behaviors that needed to be addressed:
1. Players had a hard time seeing how much damage they were taking in the “heart bar” in the top left of the screen. Though Violet’s iframes are intentionally small, we still found a way to communicate how much damage the player was taking by making an animation of the heart breaking. The more damage / combo damage that happens, the longer the delay for the start heart animation to playback.
2. In the same vain, players had a hard time seeing how much health they recovered when eating a recovery item. Now granted, we still don’t have an eating animation (which we plan to add). However, adding a heart animation that shows the player how much health they’ve recovered should help with this problem. The delay of the start of the animation works similarly to taking damage, in that the more health / combo health recovered will delay the start of the animation.
3. We’ve also observed many folks in the colder regions wonder why they began taking damage. After the initial cold damage popup (which players may forget or ignore), there was no way of knowing why the player was taking damage other than seeing the temperature gauge down in the bottom right. However, before these updates, the needle was very small and hard to see. We’ve addressed this with a few things:
- Made the needle bigger as well as change colors the closer it gets to the cold / warm zones
- When taking cold damage, use a blue color instead of red
- When taking heat damage, use a red color (like before)
- When taking any other damage, use a violet color instead of red
- When taking cold or heat damage, make the gauge turn colors and have a cold / heat animation playback
- In cold / warm zones, shake the temperature gauge a little to grab the players attention
Doorframes
We ended up reworking doorframes a bit. We noticed when going through a doorframe with a weapon that the weapon was drawn in the wrong layer. This is because all we were doing to sell the illusion that our hero was going through a doorframe was turning the visible
property on
and off
at the right times. This was a bit hacky and we went back and redid the way this works so that doorframes / doorways are on the appropriate layer now:
Other Notable Updates
Path Finding
At the crux of our Soldier
AI is Game Maker’s path finding algorithm. In hindsight, Game Maker’s mp_grid_path function is impossibly fast (seriously, if anyone knows how this is implemented, hit us up) — which is unfortunate because our problem wasn’t with this function. Game Maker has two requirements for creating paths:
- Grids, which is essentially “the world” made up of walls and not walls.
- The aforementioned
mp_grid_path
function, which takes a grid and generates a path, most likely using an extremely optimized version of the A* algorithm.
The problem we were encountering was the time it takes to create grids. Though paths are extremely fast to compute, grids are on the slower side (perhaps Game Maker pre-optimizes the grids, which is why paths are so fast? We’re still not sure). We’ll explain why this is a problem soon, but we really need a solution from Game Maker described in this Reddit post.
The way Game Maker Studio encourages developers to use these functions is to create a “world grid” one time and be done. For the average developer using Game Maker’s mp_grid_*
functions, this would be more than adequate. However, in our case, our world is huge and each enemy has a slightly unique grid. We’ve still been able to make great use of these functions though by doing a few workarounds.
To get around these problems, each Soldier
enemy would create their grid based on the “chunk” of the world they resided in, as well as a few trade secrets. In the above link to the Reddit post, since there wasn’t a simple mp_grid_copy
function, we were manually creating grids every time we needed them (smarter enemies would make a grid every 30 frames for example). This was costly on the CPU. We had several tricks to make this less intensive, such as offsetting each enemy to create their grid, essentially preventing multiple grids from being created on every frame. But this trick can only go so far.
One other problem we were wanting to solve for is the built in path finding deals with points, where our enemies and their hitboxes are bigger than one pixel (essentially wanting clearance-based path finding). It wasn’t a big deal until recently when we discovered our Centaur enemy, with its big hitboxes, kept running into the wall on a tight turn, and not able to clear it. With this problem and all the other aforementioned problems, we decided to roll out our own grid and pathing implementation.
We knew we needed to update the path finding algorithm itself, but the first thing we focused on was creating fast grids. Since we were able to roll out whatever solution we could build, we wanted to solve three things with our implementation of grids:
- Have a “master” grid that all enemy / etc. grids would inherit from.
- Only update enemy grids with necessary updates (i.e. only update when needed, don’t update everything every
n
frames). - It needs to be fast.
It took many nights and weekends to solve for this, but the solution we came up with for grids is ten times as fast as Game Maker’s grids!
We even solved a bonus problem introduced by our layerDepth
system: instead of one grid per enemy, it was however many layerDepth
s there were per enemy. We ended up solving this problem with our friend, binary! Our grids represented a wall as 1
and a walkable tile as 0
. We could express walls in layerDepth
0
as simply 1
(0001
). In layerDepth
1
, we would express walls as 2
(0010
). But, what if we had a wall in both layerDepth
0
and 1
? We could express walls as 3
(0011
). When we needed to know if a cell in a particular layerDepth
had a wall, we could simply pull out the bits of a number and determine whether a wall was there or not.
Our last task was to actually implement our own variation of A* and clearance based path finding. Rolling this out in pure Game Maker has actually been more of a hindrance than a help. With the performance gains we’ve obtained from the grids, we’ve essentially lost that and then some with our implementation of A*. This isn’t because our implementation is bad — it’s actually really clever all things considering. It’s simply that GML is a language above C++, and the further we get from writing code in native 1
‘s and 0
‘s, the slower the code becomes.
We did implement one trick with our A* which essentially only computes an enemies path for 100ms for a given frame. If it takes longer, we store the computations its made and move on until the next frame, where we continue from where we left off. It’s essentially fake threading and is a good compromise for now. At the very least, we “break even” with our net performance gains / losses from all of this.
Where do we go from here? Well, one of the backburner tasks we’ve been wanting to implement is rolling out path finding on a different thread. Game Maker Studio has the ability to pull in from external resources, and this is the next logical step for us. Offloading the path finding in C++, as well as on a different thread is going to give us extreme performance boosts — especially with our Soldier
AI being so integral to the game behind-the-scenes.
TileCompressedData
Another update we had to do was in regards to our build scripts. Game Maker 2022.8 introduced TileCompressedData
to the IDE, which increases build times by tenfold. Before, Game Maker Studio did not compress tile data for rooms, which made the file size for larger rooms with many tiles in the megabytes. When doing builds, Game Maker would do comparison with these rather large room files, taking additional time for each build. So what’s the problem — this seems like a great thing?! Well, our build scripts were built with the uncompressed tile data, so we had to account for that. This was a weekend project, but we were able to reverse engineer how Game Maker Studio stores its TileCompressedData
. That code is available here in our GMS-Tasks helper project.
Recorder / Playback Input
I bet you’re still wondering about the juicy details of what happened to my computer’s hard drive and what caused the micro stuttering? Well, if we recall back in December of 2020 we introduced a dev feature called “Playback Input” which lets us record all of the inputs we make and if something crashes, we can “playback” those inputs exactly to get the exact crash. Well the way we were handling this was less than optimal. The reason for this is not because we are incompetent, but at the time, Game Maker did not have a means of handling crashes. For example, if the game crashed, there was no way to “save” our input recorder since there was no way of handling unexpected crashes. Our hacky solution around this was to write out one file every frame of the game the input the player made. That way if the game crashed, we were able to “glue” those files together to create one big file.
The problem is writing 60 files every second while the game plays for the past two years has been taxing on my hard drive. Out-of-the blue one fine Saturday morning, our game finally had enough and began running terribly slow. I didn’t realize at the time that the problem was the Input Recorder. My brother-in-law was in town and as I was explaining the issue, he happened to noticed the SSD in task manager was maxing out at 100%. Once I remembered we were writing those files every frame, I turned the setting off and the game played normally again. After testing on my laptop, confirming no one else was having issues, and reinstalling Windows, that’s when I knew I was beginning to see a hardware failure.
Good news in all of this is I decided to buy the AMD Ryzen 5700x processor and the Corsair MP600 1TB M.2 drive. The processor cuts build times IN HALF and I’ve seen about 20% increase in build times with the faster read times on the M.2 drive. The other good news in all of this is unbeknownst to me, Game Maker Studio added the exception_unhandled_handler which lets us handle unexpected crashes gracefully. This lets us, for example, save out our input recorder, as one, sane normal file! All of that to say, we needed to update our Recorder Input to handle writing just one file and our Playback Input to read in this one file. Hopefully we won’t have to worry about future drive issues now 😛