Box art for Impaler Gold

Two Years on Steam with a C-Based Game

I made the unusual choice to write a game in C. It was a slow and challenging process - but one that was also rewarding. After several years of part-time development, Impaler was released on Steam in late 2022. At release, the game appeared stable and passed quality assurance with relatively few bugs. Still, I was uncertain about how things would go. Would it crash? Would there be surprises? Fast forward two years, and I can now answer this. Ultimately, the things I was most worried about were not an issue. Instead, the challenges came from unexpected areas.


Screenshot of Impaler Steam page

Impaler (2022)

Screenshot of Impaler Gold Steam page

Impaler Gold (2024)


Post-launch Recap

Impaler is a smaller game that launched on Steam at $2.99 USD. It was well received, but it needed more content to warrant a price increase. Thankfully, most of my energy was put into expanding the game versus dealing with technical issues post-launch. You can see the focus on content in the changelog below.

Update Description Theme
U1 Quality of life, balance Balance
U2 Difficulty modes Content
U3 New stages Content
U3.5 Leaderboards and GOG Launch Content
U4 New monsters Content
U4.5 Physics and gravity overhaul “Feel”
U5 New weapons and art upgrades Content
U5.5 New achievements and quality of life Content
U5.6 Graphics upgrades and decals Technical
U5.7 New monsters Content
U6 Impaler Gold release and new game mode Content
U7 Leaderboards and achievements Content


Did It Crash?

My biggest concern, unexplainable crashing, was not an issue. I owe the “not crashing” to several decisions I made early on - much relating to memory management and my efforts to avoid it. Here are the three main rules I followed.

Rule #1 - Statically allocate game state

Game state and entities are allocated at compile time. Instead of calling malloc to instantiate something, it lives in static structures-of-arrays or arrays-of-structures. This increases the executable size and puts a cap on the max number of entities. However, that was an acceptable tradeoff for this project. There were other benefits as well.

  • the memory footprint of the game state is known
  • the data has a simple lifecycle
  • malloc() and free() are not repeated 100s of times
  • easier to avoid leaks and some memory-related crashes


Rule #2 - Avoid using NULL as a parameter

This one isn’t fancy but I made a strong effort to not use NULL as a parameter. I couldn’t avoid it in all cases, however, the reduction was a quality of life win to avoid redundant null checks.

  • if you never pass NULL, you don’t need to check for NULL
  • instead of NULL, pass a reference to an empty struct {} when absolutely needed
  • replace if (thing != NULL) with something like if (thing->active)
  • pairs well with rule #1


Rule #3 - Limit code paths that modify game state

When the state of an entity or system needs to change, it can only occur in an update loop or event handler. This restriction made debugging far easier. Essentially:

  • entities are allowed to modify their own state in update loops
  • entities can read an immutable copy of the game state
  • when one entity interacts with another, it is done through an event system
  • emitted events are processed at the end of each update loop



The Surprising Challenges

While the game was stable, there were technical issues related to graphics drivers and laptops. These were infrequent and had a clear cause and solution. In contrast, gameplay and “feel” issues were less straightforward to address.

1. Driver settings

On a few occasions, players reported seeing white seams along the floors and walls. The cause of the issue turned out to be a setting forced on in the driver control panel. The game’s 2D assets are packed into a texture atlas (one that doesn’t enforce powers of two alignment). As a result, the mip mapping did not behave nicely when anisotropic filtering was forced on. It took some back and forth with the player to diagnose this, but it was addressed by simply reverting the setting. The lesson here is that you can tell OpenGL what to do, but the graphics driver has the final say.

2. Laptops with multiple GPUs

The most frustrating technical issue was related to laptops that had both an integrated AND a discrete GPU. Players reported that their framerates fluctuated drastically while playing the game. This was happening when the laptop switched between the integrated and discrete GPU (which was happening during gameplay). The solution was to configure Windows to only use the discrete GPU when playing the game. Again, some problems live outside of the codebase.

3. “Floatiness”

There was feedback describing the game physics as feeling floaty - especially when the player was airborne. I initially investigated this as a bug but the math showed to be correct. That meant it was not a physics issue but one of perception. Internet research suggested that many games don’t use the standard 9.8 m/s^2 gravity constant. Instead, gravity is implemented in a way that makes the gameplay and movement enjoyable. In Super Mario, gravity is doubled when you are falling making stomp attacks more effective. It turns out that this trick works well in Impaler too. The Mario gravity logic paired with gravity increased to 15 m/s^2 largely fixed the “floatiness” phenomenon. It was only a few lines of code but it took a long time to arrive at a solution.

Plot of different gravity curves

Comparison between "correct" gravity and what is actually fun


Wrapping Up

I was able to ship something stable by avoiding the pitfalls of memory management and keeping things simple. The most difficult issue, something gameplay related, was solved by borrowing a trick from Super Mario. Overall, I found that gameplay and “feel” issues can be more challenging to solve than technical problems. When in doubt, borrow ideas from other games.