Screenshot of Impaler

Writing a 3D Game in C (in 2022)

Many indie game developers will reach for Unity, Unreal, or Godot when starting a new project. These battle-proven game engines are likely the right choice. After years of using Unity, I wanted to do something different. I decided to push myself and write a game from scratch. It took longer than expected, but I learned a ton, which renewed my interest in game development.

Motivations to write Impaler in C:

  • wanted to do something new - was tired of building in Unity
  • saw an opportunity to learn more about the internals of game engines
  • performance - wanted the game to run flawlessly on the Steam Deck
  • wanted the game to look and feel unique

Thoughts on Proprietary Engines

Games built on proprietary engines are increasingly rare today. It is estimated that fewer than 20% of games launched in 2021 were using in-house tech. Overall, this is a good thing. This means developers are focused on creating a good game experience vs implementing low-level systems. The majority of projects should be approached this way. That being said, I’m always excited to play a game that was built from scratch - I enjoy seeing what tradeoffs & innovations developers make. Games built on proprietary engines are often more unique.

Screenshot of Factorio Factorio (proprietary engine) supports a staggering number of units, buildings, and systems that interact in real-time.


Screenshot of Journey Journey (proprietary engine) has a distinct visual style and is considered one of the most unique games of all time.

There are examples of games that should not have been built on in-house tech too. Cyberpunk 2077 was delayed multiple times, plagued with performance issues, and shipped with game-breaking bugs. The developer has recently switched to Unreal - this suggests the engine may have contributed to Cyberpunk’s challenges. There are risks in building on a bespoke engine.

I learned hard lessons building Impaler too. When I pitched Impaler to publishers, I shipped a preview build with a sinister bug. The bug made the game run in slow motion, and it appeared to have extremely poor performance. In reality, it turned out to be an issue with the game clock and handling of laptops with power-saving features. Naturally, it did not reveal itself on my machine. This almost killed my chances of securing a publishing partner. “writing it yourself” comes with risks.


Background on Unity

Unity is a mature game engine with indie-friendly licensing and a massive community. It uses a modern language (C#) and has everything you need to build a game. Anything not provided out-of-the-box can be found on the asset store or GitHub. The available tutorials, free add-ons, and open-source projects are incredible. On top of that, you can export your games to run a wide range of platforms. All-in-all, it is a complete solution.

I was introduced to Unity in 2013 when I was a mobile game developer. It was an attractive option for making cross-platform games and it did this very well. Later, in 2017 I used Unity to make VR games. I was the “optimization person” on many of these projects, which allowed me to learn a lot about it.

Screenshot of Cuphead Cuphead (Unity) makes you feel like you are inside a 1920s cartoon.


Screenshot of Elderborn Elderborn (Unity) is an action game with massive environments, significant polish, and stellar performance.


Why I Avoided It

Unity is also associated with titles that “feel” like Unity games. I attribute the “Unity feel” phenomenon to the overuse of common components and starter projects. The previous examples do not suffer from this. The developers took great care to ensure the visuals, physics, and camera implementations were tailored to the game they were making. Unfortunately, others feel like you are playing a reskinned tutorial.

Despite the strengths of Unity, I disliked using it, and my interest in game development diminished. The aspects of game programming that I enjoyed the most (graphics and physics) were not a big part of the job. Instead, I glued pre-built components together, performed frustrating upgrades, and refactored code to avoid garbage collection. I found the Unity way of doing things at odds with the way I like to write software.

My past challenges with Unity development:

  • required a lot of mouse work and clicking
  • made heavy use of binary files - a challenge for collaboration and version control
  • the “nice” C# features such as LINQ would generate garbage
  • I didn’t like ECS, prefabs, or the approach to instantiating game objects
  • it felt very object-oriented vs. data-oriented (with state and logic mixed)
  • keeping a portfolio of games current with the latest Unity version was exhausting

Motivations for Not Using an Engine

I was making a game in a crowded genre and knew many other titles were made in Unity. Building on my own (very simple) engine could help me capture the retro feel and aesthetic I was after.

My goals & circumstances:

  • wanted every ounce of performance for the Steam Deck
  • could afford the extra time investment
  • the scope of the game was not overly ambitious
  • wanted full control of the look and feel
  • saw a plausible path to reusing the tech for other projects
  • wanted to avoid engine licensing fees

Knowing I was writing a game from scratch, I had to choose an environment and framework. I chose MinGW, GLFW, and OpenGL because I was familiar with them. I then stumbled across Awesome C on GitHub and found libraries that convinced me writing a game in C was feasible.


Why C and not C++?

C++ is the obvious choice, but the syntax and endless features overwhelm me. I’m not a fan of OOP for game development - class-heavy C++ was not the right move. Despite being 50 years old, I found the simplicity of C attractive. The game is almost entirely structs, arrays, and functions. I like data-oriented design, and C somewhat encourages a separation of data and logic. Overall, it matched my programming preferences better.

C structs became my favorite language feature:

  • deep cloning of data with simple assignment a = b;
  • deep equality checks bool are_equal = memcmp(&a, &b, sizeof(some_struct_t));
  • immutable data with const const some_struct_t foo = {};
  • designated initializers some_struct_t a = { .foo = true, .bar = 123 };
  • simple preallocation of game state static game_obj_t object_pool[MAX_GAME_OBJS] = {};

Caveats:

  • cloning and equality checks assume structs contain only primitive types (not pointers)
  • memcmp is not guaranteed to produce accurate results due to struct padding nuances (while it “works” for my application, it is discouraged)
  • there are a lot of sharp corners with C - be careful

What I Learned

Screenshot of Impaler's development timeline

It is a slow burn - especially for a weekend project

C is time-consuming to write compared to other languages - there is no doubt about that. The basics like string & array manipulation can’t always be done with elegant one-liners. However, some of the lost time is rewarded to you in other ways. C is a typed language with good static analysis, making refactoring predictable and low-risk. There are limited ways to approach problems, but this consistency is a feature. It is a slow and steady process; with the right code foundation, a developer can be productive in C.

The good:

  • fantastic open source C libraries are available
  • memory management isn’t painful when you preallocate
  • compile times are short, and binaries are small (~4MB)
  • builds are simple and fast - can push a new build to Steam in 15s (pure code change)
  • the code is portable if you avoid OS-specific APIs (I develop on a Mac & test on a PC)
  • the performance is great

The bad:

  • debugging user issues in production is hard (Unity crash reporting vs log files)
  • scarce community, support, and tutorials
  • AV software may think your game .exe is a virus
  • creating good menu interfaces is challenging
  • less confidence the game will work on a wide range of hardware
  • SDKs often have C++ interfaces that require a custom wrapper to call from vanilla C

Impaler could have been made in <50% of the time if implemented in Unity. From that perspective, C was the wrong choice. However, the qualities that give the game character would not exist if developed outside the constrained environment of C. It forced me to create a short polished experience at an appropriately small scale. The slower development pace gave me time to think, and the inherent challenge of the project motivated me to complete it. I am happy with the end product and that is what matters most.


Closing Thoughts & Helpful Resources

Off-the-shelf game engines are the right choice for most projects - especially ones that must be profitable and ship quickly. If you have the luxury of writing a game from scratch, it will expose you to more and make you a better developer. At the very least, you will gain an appreciation for what commercial engines provide you. I plan to continue and write more games in C - I’m enjoying it.

I compiled a list of resources that were helpful while developing the game. You can find it here on GitHub. I hope to share more about the internals and architecture of the game in a future post.