For the last few years we’ve been working on a train management game, Iron Roads, which we released to Early Access today. Somewhat atypically for a game releasing in 2025, Iron Roads is written in pure C, not C++, pure C99. As a choice it has had its ups and downs, which I wanted to share in this post.
The TLDR for why I chose C is that I wanted easy portability and simplicity, but most importantly I wanted clarity over what my code was doing. I wanted to know where it was allocating memory, where performance problems were likely to arise, and I was willing to pay a considerable price to work in a language that doesn’t obscure those details at all.
Clearly any technical decision should be a function of the game being written.
Iron roads is a 2D train simulation game. Our focus when developing has been the gameplay arising from optimising a complex network of tracks with a large number of trains. Additionally, we’ve always wanted this to be a game that is portable to all platforms, and accessible to a wide range of players. Technically, this means:
In a technical sense Iron Roads has followed a winding path to its final form, starting from a series of partial prototypes. In total there were four in three languages: Go, Haskell and Rust.
Haskell was the most interesting of these experiments, and worth a blog post of its own. Personally, I wish it had worked out because it was a lot of fun to work on. Unfortunately the requirements of a high frequency game loop seemed to be too much at odds with idiomatic Haskell code to work out for me. Before long every function I wrote seemed to be over the IO monad, and it stopped feeling like Haskell code at all.
Go and Rust were more viable choices, and it is not hard to see myself having written Iron Roads in either of them. Unfortunately they share with Haskell the difficulty of porting the language runtime to a console, and no ports seem to be publicly available. As mentioned above, the NDAs mean that very few languages have first class support for console platforms, and Go, Rust and Haskell are not among those that do.
There are cases where a language has been ported by some energetic individual. To get access you would need to persuade that individual you have also signed the NDA. Often you get access, but the upstream project likely wouldn’t, so there is no guarantee that breaking changes won’t appear upstream at any time, leaving the NDAd fork behind.
C/C++ are provided by all platform owners, so they are safe choices, and C# has enough corporate momentum behind it to more or less guarantee a port, but beyond that the only languages I felt confident I could rely on being available were those that embed in C, like Lua, or those that compile to it, like Nim (aka the prototype I regret not writing!).
I should come clean that saying I wrote Iron Roads in C is a lie. Iron Roads is written in C and Lua.
Lua, for those who are not familiar with it, is a scripting language designed to be easily embeddable in other applications, and is commonly used in games for that purpose. I wrote the lower level code that runs every video or simulation tick in C (approx 40k sloc), but a lot of higher level game logic, including all the level specific code, is in Lua (approx 8k sloc).
This structure seems almost an inevitable outcome for a game written in C. Greenspun’s tenth rule states that any sufficiently complex C program contains a partial implementation of Common Lisp, but in the gamedev world it would be more accurate to say that any sufficiently complex game in C/C++ contains Lua, and Iron Roads is no exception.
An immediate question for someone working in C is why they are not working in C++?
Given that I always knew I’d be writing the “content” in Lua, my priority for the C code was to build a highly efficient and reliable engine for routing trains, ticking their positions, and rendering it all. The routing engine especially is a non-trivial piece of logic, and from the start I have been worried about its efficiency.
When profiling previous games I wrote in C++ to improve their performance I would often find STL containers as the root of a performance issue, only to have to rewrite those segments of code to an optimised pure C equivalent.
There is nothing wrong with those containers, or their implementation, but they hide the dynamic memory allocation and de-allocation they carry out for you. It is their greatest feature, however it becomes easy to write code that looks like it should be efficient, but is not. I personally found working in C to help with this problem!
I was also tired of the compilation speed of C++. I’ve never worked with a C++ codebase that is quick to compile.
When writing a game, especially when covering the roles of both designer and programmer, a lot of my time is spent in a loop of playing the game, finding something to change, changing it, and playing again. I strongly believe that speeding up this iteration loop improves my process enough to have an observable effect on the quality of the game I am writing.
Generalisations are never accurate, but I find that idiomatic C++ tends to take longer to compile than the equivalent idiomatic C code. This was a huge vote in favour of C for me for Iron Roads.
Finally, C’s struct literals are incredible. I have no explanation for why they never made it to C++.
The real success though, is that for me choosing C was part of a broader pattern of choosing the most straightforward tech possible to build Iron Roads. Possibly I just can't be trusted to write C++, but this has been great for readability and performance of the codebase, and great for keeping me focussed on the game I am writing rather than the technology I am using to write it.
In truth I’m not sold on writing games in C, and I don’t think I’d repeat the process. I’m happy with the outcome both codewise, and designwise, but the process of getting there was needlessly difficult.
I don’t see much point in returning to C++, but I am very interested in experimenting with modern, higher level languages that compile to C. I think the first prototype of my next game will be written in Nim!