Dev Diary #0: Why we write our own Rust game engine
In short: MKA Engine is our own simulation engine for grand strategy games, written in Rust. Ready-made engines solve graphics and scenes well; what we need is a core that computes millions of units every game day, fast and with the same result on every computer. We will use Godot for the visuals and write the simulation ourselves.
Welcome to the first MKA Engine developer diary. Diary zero is about the two questions that come before everything else: why we did not use a ready-made game engine and are writing our own, and why we are writing it in Rust. You will find the basis for every claim next to it or in the sources at the end.
What ready-made engines solve, and what they do not
Most game developers today start with a ready-made engine such as Unity, Unreal or Godot, and for most games that is the right decision. These engines give you graphics, audio, physics, scene editing tools and platform support out of the box.
The weight of a grand strategy game lies elsewhere. There is a map on screen, but behind it runs a simulation where thousands of provinces, hundreds of countries and millions of population units are recalculated every game day. General-purpose engines are built around scenes, graphics and physics; a data simulation at this scale has to be written by the game itself. Paradox, the pioneer of the genre, also makes its games with its own Clausewitz engine (Wikipedia, Paradox Development Studio (opens in a new tab)).
| Need | General-purpose engine | MKA Engine |
|---|---|---|
| Focus | Graphics, physics, scenes | Large-scale data simulation |
| Multiplayer | The server replicates object state to players (Unreal networking (opens in a new tab)) | Lockstep: only player commands travel |
| Same result on every machine | Not guaranteed with floating point (Fiedler (opens in a new tab)) | The law of the engine, checked by tests |
| Modding | Depends on the game | The game itself is a mod |
Lockstep multiplayer is not a new idea: Age of Empires kept hundreds of units in sync over slow modem connections this way, by sending only commands (1500 Archers on a 28.8 (opens in a new tab)). The condition of the method is that every machine produces the exact same result, bit for bit, from the same commands; the whole design of MKA Engine is built on that condition.
This does not mean we threw ready-made engines away. We will use Godot for the game's front end: map rendering, interface and audio. The simulation itself runs in MKA Engine.
Why Rust?
When choosing the engine's language we looked at three things: speed, safety and identical behaviour on every platform. For years the language of this genre was C++. Rust compiles to machine code at the same level as C++ and catches most memory errors at compile time.
1. Speed without a garbage collector
Rust compiles directly to machine code and uses no runtime or garbage collector (rust-lang.org (opens in a new tab)). The pauses a garbage collector inserts in the middle of the game are unacceptable for an engine that wants to compute every game day in a few milliseconds. We decide when memory is allocated and when it is released; the "zero allocations in the day loop" measurement you will see below is the result.
2. Memory safety
In large software written in C and C++, most serious bugs come from memory errors: accessing a deleted object, writing past the end of an array, or two threads changing the same data at the same time. Microsoft says about 70% of the vulnerabilities it assigns a CVE each year are memory safety issues (Microsoft Security Response Center, 2019 (opens in a new tab)); the Chromium project reports the same for about 70% of its high-severity security bugs (Chromium, memory safety (opens in a new tab)). Rust catches most of these classes of bugs in the compiler, before the program ever runs. That is a big safeguard when we split the simulation across several threads.
3. The same result on every machine
In multiplayer, every player's computer computes the same world on its own, and only commands travel over the network. For that to work, the same commands must give the exact same result, bit for bit, on every machine. Floating-point numbers can produce tiny differences depending on the compiler, the build settings and the processor (Fiedler, Floating Point Determinism (opens in a new tab)). That is why the engine core does not use them at all; every calculation uses fixed-point numbers.
Rust helps in two ways here. First, we can enforce rules with types: a fixed-point number is its own type, so it cannot be mixed up with a floating-point number by accident. Second, we can easily compile the same code for different targets. We run the core tests on three targets: x86_64 (our development computer), aarch64 (ARM processors, emulated inside Docker) and WebAssembly (with wasmtime). Because the golden tests compare every output with files stored in the repository, passing on all three targets means all three produce the same results.
4. A safe sandbox for mods
Mods compile to WebAssembly and run in a sandbox isolated by wasmtime inside the engine (wasmtime security (opens in a new tab)). Rust is one of the languages that supports WebAssembly as a first-class target (Rust and WebAssembly (opens in a new tab)). A mod in the sandbox can only call the functions the engine opens to it; it cannot reach the player's files on its own.
5. Tooling
Cargo, Rust's package manager and build tool, brings building, testing, documentation and dependency management into one place (The Cargo Book (opens in a new tab)). The engine's core packages depend on no external library, only on each other. External libraries live only in the edge layer, behind our own interface; their licences, sources and security advisories are checked with cargo-deny at the close of every development phase (cargo-deny (opens in a new tab)).
In numbers: what does "fast" mean?
Our goal is to compute a game day in a few milliseconds even in the most crowded late game. We measure this at every phase with a test world we call the "scale world": 30,000 locations, 250 countries and 2 million population units. Our target in this world is to compute a game day in at most 5 ms on 8 threads and at most 10 ms on a single thread. We measure every phase against this target.
How did we set the target?
We chose these numbers by looking at three things:
- The scale of similar games: We studied the world size and structure of today's games in the genre: how many locations, countries and population units they carry, and which calculations run every day and which run less often. That is why we built the scale world close to the most crowded late game of this genre.
- Our test machine: We take the measurements on our development computer with an AMD Ryzen 7 5700G (8 cores, 16 threads). It is not a high-end desktop processor; results may differ on players' computers.
- Room for the screen: A 5 ms day means the simulation can compute at most 200 game days per second (1000 ÷ 5; a calculation, not a measurement). As long as the game advances below that speed, the simulation uses only part of each second; the rest is left for map rendering, the interface and AI. The single-thread target keeps the game smooth on computers with fewer cores too.
How many milliseconds is a game day? (ms / day)
| Label | Before | After |
|---|---|---|
| 1 thread | 10.32 | 8.48 |
| 8 threads | 2.70 | 2.06 |
- Before
- After
| Measurement | Target | Before | After | Gain | Against target |
|---|---|---|---|---|---|
| 1 thread | 10 ms | 10.32 ms | 8.48 ms | 17.8% | 15.2% below target |
| 8 threads | 5 ms | 2.70 ms | 2.06 ms | 23.7% | 58.8% below target |
In the latest performance work, a day on 8 threads went from 2.70 ms to 2.06 ms, and on a single thread from 10.32 ms to 8.48 ms. Before this work we were 3.2% above the target on a single thread; now both measurements are below the target. The gain came mainly from these changes: reusing intermediate results in the market calculation, faster fixed-point division, splitting work across threads in smaller pieces, and starting long jobs first. One important thing did not change: the state digest taken at the end of every day stayed identical, bit for bit, and memory allocations in the day loop stayed at zero.
- 21 min 35 s total time for 600 years
- 0 memory allocations in the day loop
- 62 MB peak memory use
- 0 crashes and number overflows
Long runs matter too: in the same scale world we ran 600 game years, about 219,000 game days, in one go. The run finished without errors; the day loop made no memory allocations and peak memory use stayed at 62 MB. Because we manage allocation ourselves in Rust, we can set a goal like "zero allocations" and measure it.
This measurement has two limits. First, the test world has a fixed size: the number of population units and countries does not change during the run, so it does not test a world that grows over time; that scenario will be measured separately. Second, the computer was also busy with other builds during the run, so day times fluctuated within the run, and its times do not replace the speed measurement above.
These numbers come from the engine's own runner. We have not compared against other engines under the same conditions, so we do not put another engine's numbers next to them.
The layers of the engine
You can think of MKA Engine as a three-layer structure:
- Core: Numbers, randomness, how data is held in memory, the scheduler and saving. Clocks, files and the network cannot enter here.
- Mod interface: The single door through which the game and mods talk to the engine.
- Game: Tables, rules, numbers and content. The game itself is a mod that lives in this layer.
What does it cost?
Writing your own engine is not a cheap decision. A ready-made engine would have reached a first playable version faster. We write, test and maintain every piece ourselves, from numbers to the scheduler, from saving to networking, from the mod interface to developer tools; we also build the link between the front end and the simulation ourselves. We pay this price knowingly, because in this genre changing the foundation later is close to impossible.
Frequently asked questions
Is MKA Engine a game?
No. MKA Engine is an engine that computes the simulation of grand strategy games. The front end that draws the picture and the game itself are separate projects; the engine is in development today.
Why not Unity, Unreal or Godot?
Those engines are strong in graphics, sound and scene tools. The heavy part of a grand strategy game is computing a world of millions of rows every day and producing the same result on every computer in multiplayer. We write that core ourselves; for the visuals, interface and sound we will use Godot.
Why Rust?
Rust catches memory errors at compile time, has no garbage collector and so no sudden pauses, and the compiler prevents data races in parallel code. The details are in the Rust section of this diary.
How fast is the engine?
In the scale world one game day took 8.48 ms on one thread and 2.06 ms on 8 threads. These are simple measurements taken with the headless runner; drawing is not included.
Sources and measurement records
External sources:
- Microsoft Security Response Center, A proactive approach to more secure code (opens in a new tab) (2019)
- Chromium, Memory safety (opens in a new tab)
- Glenn Fiedler, Floating Point Determinism (opens in a new tab)
- Paul Bettner, Mark Terrano, 1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond (opens in a new tab)
- Epic Games, Networking overview for Unreal Engine (opens in a new tab)
- Wikipedia, Paradox Development Studio (opens in a new tab)
- rust-lang.org (opens in a new tab), Rust and WebAssembly (opens in a new tab), The Cargo Book (opens in a new tab)
- wasmtime security (opens in a new tab), cargo-deny (opens in a new tab)
Our internal measurement records (the engine repository is closed for now, so we cannot link them):
- Speed measurement: Phase 7b performance work, 14 October 2026. Scale world, late game, 1 and 8 threads, AMD Ryzen 7 5700G; after every change the state digests and the allocation count in the day loop were compared.
- 600-year run: 14 October 2026,
runner --years 600 --threads 8; time and peak memory measured with/usr/bin/time -v. - Cross-target check: The core packages give the same results on x86_64, aarch64 and wasm32; the recorded results are identical bit for bit on all three targets.
In the next diary we will look at the engine's most basic building block: fixed-point numbers. We will explain in detail why we do not use floating-point numbers, how the numbers are stored in memory, and how operations such as multiplication, division and square root give the same result on every machine.
If you have questions or ideas about this diary, write to us from the contact page. For an overview of the engine see the MKA Engine page, and for all diaries the Developer Diaries list.