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 challenge myself by writing a game from scratch in C. It took longer than expected, but I learned a ton and the experience renewed my interest in game development.

TLDR;

  • was tired of building in Unity
  • wanted to learn more about game engines
  • had a humbly-scoped project with the right set of circumstances

Thoughts on Proprietary Engines

Games built on proprietary engines are increasingly rare today. It is estimated that fewer than 20% of titles 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. And, in my opinion, games built on proprietary engines are often more interesting.

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 one of the most unique games I’ve played.


Proprietary Tech Comes with Risks

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 also learned some hard lessons while building Impaler. When I pitched Impaler to publishers, I shared a preview build with a serious bug. The bug made the game run in slow motion, and it appeared to have extremely poor optimization. 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 finding a publisher. “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 a polished action game with huge environments and good performance on the Steam Deck.


Why I Avoided It

Unity is sometimes associated with titles that “feel” like Unity games. I attribute the “Unity feel” phenomenon to the overuse of common components and starter projects. Cuphead and Elderborn shown above 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 experience with Unity

  • 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 components, 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 most competing titles were made in Unity. Building on my own (very simple) engine would allow full control over the look and feel (to capture the nostalgia for games like Doom). It was also a challenge that would teach me what it takes to make a game from scratch.

My goals & circumstances

  • wanted full control of the look and feel
  • the scope of the game was not overly ambitious
  • could afford the extra time investment
  • wanted extra performance to run on the Steam Deck @ 60hz
  • saw a plausible path to reusing the code in future projects

Why C and not C++?

C++ is the obvious choice, but the syntax and endless features overwhelm me. I’m not a fan of object orient programming for game development so 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 matches my programming preferences better.

Having settled on C, I then had to choose a development environment and graphics framework. I went with MinGW + GLFW + OpenGL because I was already familiar with them. I then stumbled across Awesome C on GitHub which further convinced me writing a game in C was doable.

C structs quickly became my favorite feature

  • deep cloning of data with simple assignment a = b;
  • deep equality checks bool are_equal = memcmp(&a, &b, sizeof(Entity));
  • immutable data with const const Entity foo = {};
  • designated initializers Entity a = { .foo = true, .bar = 123 };
  • static preallocation of state static Entity entity_pool[MAX_ENTITIES] = {};
  • can hold a function pointer when you really need it

NOTE:

  • the memcmp trick only works under certain scenarios so be careful
  • cloning and equality checks assume structs contain only primitive types
  • what worked for this project may not work for another

What I Learned

C is time-consuming to write compared to other languages - there is no doubt about that. The basics like string & array manipulation can’t 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 state is statically allocated
  • compile times are short and binaries are compact
  • 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 user interfaces is challenging
  • less confidence the game will work on a wide range of hardware
  • SDKs with C++ interfaces require a custom wrapper to use from vanilla C


Impaler could have been developed in less than 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 ample time to think and plan my approaches. I am happy with the end product and enjoyed the process.


Closing Thoughts & 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 found the endeavor rewarding and hope to write more games in C.

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.