These past couple months have seen many art updates, font cleanup, stability fixes and performance increases. We’re excited to showcase the highlights below.
At the beginning of 2023, we began coming up with a few designs for the newest member of the
Soldier enemies, the Minotaur. Tony, as always, comes up with several great designs that we then have to decide which fits best with the game, story, etc. Below are a few of the initial designs:
Without going into all of our rationale, we decided to go with the head of the second design and the body of the first design. This yielded the fifth design:
After some further tweaking, we ended up with our final design:
The Minotaur has more HP and strength compared to the Orc and the Reptile. To illustrate this from a gameplay perspective, we decided to make the Minotaur significantly larger. We originally went with 1.5 times larger, but that seemed a bit large comparatively. We also tried 1.25 times larger, but that seemed a bit small. Therefore, at about 1.33 times, the Minotaur stands tall at 42 pixels (our hero is 32 pixels for reference (~5’9″)). We also wanted to showcase the strength with its intimidating walk cycle, as shown below:
December found us finishing up some Reptile animations with the bow and two-handed walk cycle. However, as we were preparing the Minotaur, we had realized we forgot to do idle animations for the reptile. Below are those animations:
We also decided to touch up the cliff walls a bit, as well as add a few palette updates for the cliffs in different biomes shown below:
We finally decided to give an overhaul to the targeting system, both artistically and functionally. Functionally, we added a “pause” concept that would only change the targetable enemies every one second, or when the player made a selection. Even though the computer could calculate accurately which targets were “next”, having them change so rapidly made it really hard for the player to choose. This delay or pause concept really helps in selecting.
Visually, we added three states:
- Light blue bracket — the “next” target if we quickly let go of L-Trigger and press it again
- Yellow bracket — the current target we are locked on to
- Directional arrows — if we press the R-Stick in the direction of the arrow, that enemy will become the “next” target
Below is an illustration of what this looks like:
You may have also noticed in the above screenshots that we moved the HUD elements. We haven’t officially decided if we like this, but moving these elements to the corners of the screen really helps with the vertical screen real-estate.
You might have noticed in the above screenshots that the font is much different than before. We overhauled the font systems and used / touched up some elegant pixel art fonts, as showcased below:
Other Notable Updates
Path Finding Part Two
In our previous post, we explained our problems with the path finding / grid system native to Game Maker and our current solution of using our own grid system. By using our own, faster grid system, we also had to use our own implementation of A*, which essentially made us break even with the performance gains (because Game Maker’s mp_grid_path function is impossibly fast). However, we hinted at our backburner task of rolling out the path finding (A*) on a different thread. Game Maker Studio has the ability to pull in from external resources, and we finally got around to offloading the path finding in C++ as well as on a different thread (spoiler, this gave us extreme performance boosts).
I (Dan) finally had a few consecutive days off after Christmas where I was able to dedicate 100% of my time with no interruptions learning how to make external DLLs that Game Maker Studio could then import and make calls out too. The journey was tough — and honestly, I thought it was going to take longer to figure out than it did. Most of our learnings came from these forum posts / guides:
We ended up going with Visual Studio instead of Eclipse, mainly because we already had Visual Studio installed for Game Maker’s YYC compiler. After about the first day, we had a simple “Hello World” program made where we were able to talk to Game Maker and back to the DLL. Day 1 and 2 also saw us converting our A* from GML to C++. I haven’t touched C++ since my college days, so it took a little bit of time to remember how good ole pointers work. After about Day 2, the A* algorithm was able to compute within a
main program within the DLL (meaning, it was converted successfully).
Day 3 was the trickiest as we had to convert our aforementioned grid system of the previous blog post to buffers. The only way to send large amounts of data to a DLL is to send the memory address (pointer) of the buffer from Game Maker. Therefore, we had to convert our
ds_grids to buffers. Even though this was a pain, buffers are not only faster in Game Maker, but the size in memory was about half, so we inadvertently gained some performance. Day 3 also saw us learning how to use threading properly in C++. Even though native C++ is faster than GML, it still didn’t really make since for us to spend all of these new CPU cycles sending to the DLL if we were still running the A* on the same / main thread as the game. Therefore, we had to figure out how to implement C++ threads.
There were two threading libraries we looked at. The first was pthread, which we ultimately scrapped as it is meant for Unix computers. Though there are ways to make it work on Windows (the eclipse link / we had it kind of working at one point in Visual Studio by importing the
pthread library, but eventually gave up) we finally went with thread, which seems like it is native to C++ now. After a few hours of experimenting, we were able to get threads working within C++.
By the fourth day, we were able to successfully send the grid data to the DLL, create a thread, compute the A* path and send the path data back to Game Maker! When I first got the logs of the path data in Game Maker, I screamed at the top of my lungs and scared my wife! A few minutes later, we were able to convert that data so that our
Soldier enemies could move again! We also were able to figure out ways to cancel computations / threads in the DLL. For example, if an Orc dies while computing its path, we no longer need to compute the path anymore, since its dead.
Seeing the fruits of this labor paid of majorly. We’re even thinking of moving our boid enemies (Globs, Bats, Violets) so that the computation is also threaded / asynchronous, as we eventually want tens of Violets on screen.
If we recall from previous posts, as our hero / camera moves from one location to another, we load in zones that are slightly off screen the direction we are moving and unload zones that are slightly off screen in the opposite direction. If the whole world was computing all at once, our game’s speed would crawl. In the spirit of async, we decided to make our loading / unloading of zones “asynchronous”. We put asynchronous in quotes because, unlike the aforementioned A* which is truly asynchronous, the loading and unloading of zones is spread (vs. done all at once) amongst several frames. For example, if we need to load in 4 zones, and it has taken more than 500ms to do so, we tell that system “hey, you’re taking a long time. Let’s continue loading the next frame”. So, instead of loading 4 zones in 2000ms (remember, a frame should compute and draw in ~16000ms), we load 1 zone each frame for about 500ms. Since these zones are slightly off screen, there is still no need to worry about a jarring “pop in” effect. By offloading this task to several frames of the game, we are able to prevent stutter slow.
Other Stability Increases
We also improved the frame rate of the game by removing a few particle effects that we were planning on replacing anyway. For example, blood was a particle effect that we don’t believe was getting removed properly. Recently, if an enemy dies, its body remains until the player unloads the zone and then comes back to that zone a few minutes later. However, if the player never returns back to the zone, the body and its particle system remained in memory until the game ended. By removing the particle (we’re eventually going to create a system for handling dead bodies / other objects that linger around like this), this in turned freed up memory, which in turn, frees up the CPU.
We also improved the
draw_container routine for the inventory. All other
draw_container calls cached the container that is being drawn, except the inventory screen. This was mainly because refactoring this was going to take a bit of time and was easier to just leave alone. However, keeping the inventory from redrawing the container each frame would create hundreds of structs per frame, which would cause memory to spike. Game Maker’s automatic garbage would eventually clean this. However, the amount of time garbage collection has taken on Game Maker’s part has become quite burdensome for Violet recently. Essentially, there have been stutter slow downs, similar to the aforementioned loading / unloading of zones. However, unlike loading / unloading of zones, we don’t control Game Maker’s automatic garbage collector. Therefore, anything we can do to help avoid unnecessary structs / memory on our part we’ve noticed helps keep Game Maker’s garbage collector more stable.
Weapon Degrading / Capacity / Shards
On Christmas Eve, I (Dan) was playing an old build from 2019. I was mainly doing this just to see how far the game had progressed. While playing, I realized a mechanic that functioned differently than it does now. When a weapon degraded, it only moved down by 1 damage point. Though there was nothing in 2019 indicating that it had degraded, it was interesting to play the game with weapons that slowly degraded vs. now where degrades go down a full letter (therefore, like 20 damage points). We began thinking “what if we degraded by a smaller amount”?
We decided to try this theory and liked this so much better. Now, instead of a
C ranked weapon dropping to a
D, it goes from
D+ and then
D. We feel this really helps with the damage output in mid-battle. However, with degrading only going down by thirds, the amount of weapon shards (which can be used to increase weapon capacity) had not changed, being way more abundant. We decided to decrease this, as well as decrease the amount hits it takes to degrade a weapon. Therefore, the degrading happens more (but the damage output is not cut has bad), which yield more weapon shards (but not too abundant). With these two new systems at play, we also decided to make starting inventory capacity for each type 2. With more weapon shards, it becomes pretty easy to increase weapon capacity. With the primary mechanic of merging degraded weapons together for better weapons, we feel like we’ve made better a really interesting game loop with the weapon system.