What an Entity Component System actually is
An Entity Component System is a way of organizing game state by how it is used rather than by which object it belongs to. The naive way to write a game is object-oriented: an array of Unit objects, each one carrying every field a unit might need — position, health, animation state, AI state, owning player, current command. When the update loop walks the units to do a position-update pass, it loads the entire struct into cache for each unit, uses three of fifty fields, and evicts the rest. The cache hit rate is terrible, and once you have hundreds of units on screen, cache misses are the whole performance story.
An ECS inverts that layout. Instead of an array of units, you have one array per field — an array of positions, an array of healths, an array of animation states. An entity is just an identity; its data is spread across those arrays. A component is one of those data columns. A system is logic that operates over a query of components — the position-update system walks one tight contiguous array of position data, fits dozens of entities in cache at once, and runs in a fraction of the time the object-oriented version would. People call this data-oriented design now, with components stored as columns in a table rather than rows. The defining move is that state is grouped by use-pattern, so each phase of the update touches only the data it needs.
I did not learn this from a textbook. I learned it in 2003 keeping a real-time strategy game playable on a Pentium III with sixty units fighting on screen, where a cache miss cost roughly a thousand times more than an instruction on data already in cache. We were not consciously building an ECS — I am not sure the term was even codified yet — we were doing struct-of-arrays because the cache architecture of the era would not let us do anything else. That was the primordial form of what I now build deliberately. The full origin story, from 2003 lockstep RTS networking through to MECS, is its own essay.
Why build a custom engine instead of using Unity or Unreal
The obvious question is whether any of this needed to be custom. Unity has DOTS. Bevy has its own ECS. Entitas exists. Several smaller frameworks exist. Why not pick one and ship?
The short answer is a single hard requirement: I needed the engine to run identically on the Unity client and on a .NET backend, with the same component schemas, the same system code, and the same wire format. The simulation logic, the type system, the system ordering, and the network framing all had to be one codebase running in two environments. None of the off-the-shelf options solved that cleanly enough at the start, and the requirement narrowed the search until building it myself was the most defensible option.
The longer answer is that the engine I wanted is opinionated in ways a general-purpose ECS cannot be. Components are unmanaged-only because the messages that carry them are unmanaged-only because the wire format requires it. Systems declare their dependency relationships because the topological sort needs them. The whole framework leans into one specific shape of game — authoritative-server multiplayer at high per-host density, deterministic simulation, viewer-independent state, fast iteration cycles. That shape rules out a lot of other shapes. A general ECS has to support all of them; I only need to support one well, and the cost of not being general is paid back in the parts of the framework I do not have to write. I would build it again.
I should be honest that this is not a recommendation everyone should follow. Building your own engine is the right call only when an existing engine cannot meet a requirement you are unwilling to give up — in my case, one type system spanning client and server with the network baked into the foundation. If you do not have a constraint that sharp, use Unity or Unreal and ship your game. The build-it-yourself decision is earned by a specific need, not by preference.
Why C#, when raw performance argues for C++
C# is not the language you pick if your only criterion is raw throughput. It carries garbage collection, JIT compilation, and a runtime heavier than the C and C++ stacks most performance-critical multiplayer engines run on. I picked it anyway, for three reasons that started partly emotional and became defensible over time. I enjoy writing C#, and after decades that included substantial C++ work I had come to prefer its day-to-day experience — which is the argument that determines whether I am still maintaining this engine five years on. Unity already uses C#, so if MECS was going to run client-side in Unity and server-side in .NET as one codebase, picking the language Unity supports was obvious, and it freed me from ever writing my own renderer. And a .NET backend is materially cheaper and easier to operate at scale than a native one, because most of the cost of running a multiplayer game lives in build pipelines, deployment, debugging, and observability rather than in raw CPU cycles.
The honest counter-argument is the performance gap, and I did not wave it away. I closed it with an unmanaged rewrite in late 2022: components became unmanaged C# structs in explicit memory layouts, and Span<T> went in aggressively across the hot paths. The framework approaches native performance not because C# is fast by default but because C# can be fast if you stay in a narrow lane of the language. I later went further and confronted the question directly — I spent five weeks in early 2024 rebuilding the engine from scratch in pure C as a deliberate dogfood, and came out the other side still preferring C#. The decision was earned by living inside the alternative, not assumed.
The MECS authoring model: tools generate the runtime code
The single biggest friction on an ECS engine is the wiring code. Every new component needs factory code, serialization code, network code, registration code — all of it formulaic, all of it a tax on iteration. The most important ingredient in finding the fun in a game mechanic is the number of times you can iterate on it before you ship, and if the engine costs you iterations, the games on top of it are worse than the runtime numbers say they should be. So from the original thesis I committed to auto-generating that boilerplate.
The model that resulted is this: new components, systems, queries, and messages start as type definitions in an asset database, not as hand-written C# files. A code generator reads those definitions and emits the C# scaffolding that wires everything into the runtime. The asset database is the source of truth; the runtime code is a generated artifact. This decouples type definition from the build cycle and answers the iteration goal directly — you define a type through tooling and the formulaic wiring appears for free.
The tool that edits those databases is worth a note because of how it was designed. It started in April 2024 as a TUI — a text-mode editor that could run inside an SSH session, which mattered because a lot of asset-database editing happens on remote servers where a graphical editor is not an option. It has since grown into a full CLI, a dotnet global tool, and an MCP server that lets agents call its commands directly. The kernel never changed: define types and content in a database, let the generator emit the scaffolding, edit the database through tooling rather than through manually-maintained C# files. That generated-artifact model later turned out to be exactly the property that made the engine legible to AI agents, but that is a thread for another essay.
Components, systems, and the single-writer rule
The primitives are deliberately strict. Components are unmanaged structs with explicit layouts, capped in size, carrying data only — no logic, no strings, no lists, no managed references. The cost is that components cannot hold managed types; the payoff is dense contiguous memory, zero allocation in the hot paths, and aggressive Span<T> use throughout. Behavior lives entirely in systems, which operate over component queries. There is no object-oriented bundling of data and logic — no inheritance hierarchies, no virtual dispatch — because the Dropzone engine I worked on in the mid-2010s taught me that the OO ceremony cost more than it paid for.
Each system declares two things as part of its type definition: the set of component types it reads from, and the set of component types it writes to. The one hard constraint underneath those declarations is that only a single system in the service is allowed to claim write access to any given component type. From those declarations the framework builds a dependency graph, validates that it has no cycles, and produces a startup execution order in which producers always run before consumers. So far this is a standard ECS pattern, and I do not claim it as original.
The non-standard piece — and the one architectural shape I am willing to claim as my own contribution rather than my application of an existing idea — is that the same declarations make systems skippable at runtime. Because every component mutation flows through a delta-broadcast, and because each system has a known set of read inputs, the framework can ask before executing a system whether any of that system's input component types actually changed since the last tick. If none did, the system has no new input and therefore no new output, and it does not run. Most frames have a substantial fraction of systems that meet this condition. The result is a fixed-cost startup-time topological sort paired with a per-frame change-detection pass, and the runtime spends cycles only on systems with real work to do. That change-driven skip, made possible by the single-writer-per-component-type rule, has produced a real per-frame win on every game I have run on the engine, and the same plumbing has been load-bearing since December 2021.
Deterministic, networked simulation
The deepest constraint in the engine is one I have carried since 2003: the simulation must be a pure function of its prior state and its input sequence. This is the determinism discipline that lockstep RTS networking imposes, and it is unforgiving. Every random number comes from a seeded generator the connected clients agree on. The simulation never reads the wall clock and never reads input the network has not delivered yet. If any line violates that purity, networked clients diverge — they go out of sync — and you find out at the worst possible moment with a log that points at five hundred possible culprits.
That discipline forces a second one: not all state is game state. The simulation has to advance at a fixed deterministic cadence on every machine, but rendering wants to run at whatever frame rate the local hardware can sustain and interpolate between simulation samples. The moment you accept that, you cannot have one state — you need at least two, simulation state and view state, and they must never share a mutable reference. In MECS the simulation runs deterministically as a function of input and the view consumes deltas. This is why a MECS game can ship with multiple clients — terminal, graphical, AI-driven — all running against the same authoritative server. The lineage is direct: on Rise of Nations in 2003 we made the C++ compiler enforce that firewall with a three-class pattern where all mutation flowed through a queued command system. The mechanism evolved across twenty years; the principle — sim and view never share mutable references, every change is a command processed at a known later tick — never changed.
One detail people often read as arbitrary is the engine's hard cap on component size. It is not arbitrary. It is the size below which a component is guaranteed to fit inside a single UDP datagram on every commodity network path the engine targets, and therefore the size below which it can never be fragmented in transit. Fragment reassembly on top of UDP is one of the harder things to get right, especially when paired with reliable in-order delivery, and the easiest way to make it bug-free is to make the fragmentation case impossible to reach. That cap was set in 2022 with that property in mind — two years before any real UDP code was written. The whole framework was designed against a wire it would eventually have to meet.
Validating multiplayer in a single process
Here is the part of the architecture I am most glad I got right early. Splitting your binary into a separate client and server is one of the most expensive things you can do to iteration speed — every change touches two executables, every debugging session attaches to two processes, and stale-build mismatches roughly double. So the rule I learned over twenty years is: do not split the binary until the last possible moment. But you can buy most of the architectural integrity of a split binary without paying the iteration cost.
The construction is simple. Run the client-side MECS service in Unity's normal frame update, exactly as it will run in the shipped client. Run a second MECS service — the would-be server — on its own thread inside the same process, with that thread controlling the simulation interval rather than Unity. Connect the two with a pair of ConcurrentQueue<T> instances and treat each queue exactly as if it were a UDP socket: the client writes messages using the same packet framing it would later use on a real socket, the server thread drains and processes them and writes replies to the second queue. Forbid any other communication — no shared state, no static singletons, no direct calls. Anything that needs to cross goes through the queues.
The result is an accurate model of a split-binary client-server architecture without the burden of actually splitting: single-process debugging, one build, Unity hot-reload intact, and the exact message types, serialization paths, and authoritative-server semantics the real transport would inherit, because the queues are the message bus the real transport later replaces one-for-one. To stay honest about how real networking would feel, I added parameterized artificial latency and jitter on both queues so delivery had a distribution. Any timing or ordering assumption that would have broken over a real network broke over the queues first — and got fixed years before any UDP code existed.
From single-process engine to multiplayer platform
The engine crossed from that in-process model to real sockets in late 2023, under a three-month prototyping contract whose bar was a cloud-hosted authoritative server that players could join ad-hoc from anywhere in the United States, demonstrated in front of the customer. That bar was cleared. The transport took two weeks rather than two months precisely because there was nothing left to design — the architecture had been waiting for the wire, and the wire just had to be written. The full account of that crossing, and the backend that grew around the engine, is its own essay.
The first practical change was that MECS started being consumed as a compiled binary rather than embedded Unity source. That sounds like an organizational detail; it was not. It forced the engine's surface area to become a real public API — internal types marked internal, public types designed against consumers who could no longer modify the engine, breaking changes considered rather than just made. The engine had to start behaving like a library.
Then a new requirement reframed everything: the client would not be an installed binary but would run inside the cloud service itself, streamed to the player's device — and that service offered no server-side infrastructure at all. No matchmaker, no account system, no instance manager, no shared-state primitive. Multiplayer was non-negotiable for the game, which meant I was not only building the engine, the game-server simulation, and the streamed client — I was building the entire platform layer underneath all three. The insight I credit to my time inside MECS is that the game-server instances themselves could be modeled as platform services. The engine already had a service abstraction that ran a deterministic frame loop, a message-routing system that crossed a wire, and a serialization model that produced identical bytes for network and disk. The platform was designed from day one to host arbitrary game-server processes the way a SaaS backend hosts arbitrary worker pods.
The raw-UDP backend and its tradeoffs
The transport is a raw UDP socket wrapped in a state-broadcast model. There is no TCP layer underneath and no third-party networking library pulled in — a socket opened in datagram mode, a send loop, a receive loop, and the framing code that turns a list of component deltas into a sequence of packets the other side reassembles. On top of the raw socket sits an application-layer reliability layer: compression of large state broadcasts, fragment reassembly for the rare messages that exceed a single MTU, heartbeating to detect dead connections, and a reliable resend queue with exponential backoff for messages that must arrive in order. The same managed-versus-unmanaged discipline that runs through the engine runs through the transport — the wire format is the in-memory format, so the component bytes that go onto the wire are the component bytes that live in the unmanaged pool.
UDP-plus-application-reliability rather than TCP is a deliberate tradeoff, and it is the right one for low-latency multiplayer. TCP gives you reliable in-order delivery for free, but its head-of-line blocking means one lost packet stalls everything behind it — fatal for a real-time simulation where a late position update is worse than a dropped one. UDP lets you decide per-message whether ordering and reliability are worth the latency cost, so the frequently-updated simulation state can flow as unreliable broadcasts while the handful of must-arrive messages get the resend queue. The cost is that you write the reliability layer yourself, which is real work and a real source of bugs — but it is work I had done variants of since college, having authored the wire protocols for multiplayer titles decades earlier, so the November the transport was built was expedient rather than heroic.
The backend services around the transport sit on two databases, on advice I got from an old colleague who has held the technology stack for some of the largest free-to-play mobile games for a decade. Redis is the shared-state primitive — fast, mature, and at the right level of abstraction for session state, leaderboards, instance directories, and cross-service publish-and-subscribe. Postgres is the durable relational store for analytics, where short-lived session state is the wrong tool. Authentication, a game directory, an assets service, a game-instance manager, and an analytics service all stood up across about three weeks, each one a distinct microservice over those two stores.
The five-layer architecture
The codebase is organized into five layers with strictly downward dependencies. Core holds the unmanaged primitives — the identity and timestamp types, the bit-array family, the unsafe bitwise helpers — and depends on nothing. MECS is the simulation framework itself: the service abstraction, the entity library, the component factory, the message routing, the delta-broadcast model; it depends only on Core. Generate is the code-generation pipeline that reads type definitions and emits the wiring; it depends on MECS and Core. AssetManagement is the database that defines what types exist and what content is loaded, independent of the platform above it. Platform is the server-side services — authentication, analytics, the directory, the instance manager, the persistence layer — and it depends on everything below but on nothing above, because there is nothing above it.
The constraint that holds it together is that dependencies flow strictly downward. Core never imports MECS or Platform. MECS never imports AssetManagement or Platform. The Platform layer can use anything beneath it, but the engine layers do not know the platform layer exists. That is what lets a Unity client consume Core, MECS, Generate, and AssetManagement without dragging in any server-side code, while the server runs the same engine layers plus the platform services with full type-system compatibility between the two sides. This split has stayed essentially unchanged since April 2024.
That property — the simulation and the platform services being one codebase, sharing one type system, one serialization format, one identity model — is the answer to a frustration I had been carrying since 2020. On an earlier educational-MMO project, a SaaS-shaped platform team and a games-shaped game team could not combine cleanly into a shared multiplayer experience, and the gaps between their default assumptions showed up as gaps in what we could deliver to players. In MECS there is no cross-team translation layer because there is no cross-team boundary: a change to a component definition propagates from the database through the generator to both the game-server simulation and the platform service that persists it. The team building the persistence service and the team building the combat system would be reading the same type registry. They are not different stacks; they are different services in the same stack.
Lessons from six years of building an engine
A few things have held up across the whole arc, and they are the ones I would tell anyone considering this kind of work.
Design against the hardest constraint first, even before you can meet it. The component size cap that makes fragment reassembly impossible to hit was set two years before any UDP code was written. The in-process two-thread model validated the authoritative-server architecture for years before a single byte crossed a real network. Designing against the wire from day one is exactly why the transport took two weeks instead of two months. The constraints you bake in early are cheap; the ones you bolt on late are expensive.
Enforce your invariants where the toolchain cannot be talked out of them. The sim/view firewall on Rise of Nations worked because the C++ compiler refused to compile a violation. The single-writer-per-component-type rule in MECS works because it is checked at startup, not documented in a wiki. A rule that lives only in prose is a request; a rule the toolchain enforces is a property of the build. Every hard invariant I have managed to keep, I kept by giving it teeth.
Iteration speed is the dominant variable, and most architecture decisions are really iteration decisions in disguise. Auto-generating the boilerplate, not splitting the binary until forced to, keeping one build that compiles — none of those are about the runtime. They are about how many times a designer can change a mechanic and feel the result before shipping, which is the single biggest determinant of whether a game is fun. An engine that costs you iterations produces worse games than its benchmark numbers promise.
Earn your big decisions; do not assume them. I defended C# for years on paper, then spent five weeks rebuilding the engine in C to find out whether the paper was right. It was — but I only know that because I lived inside the alternative. The decisions I trust most are the ones I tried hardest to disprove.
The engine has been twenty-three years in the making and six years on disk. The two essays this guide draws on tell the longer story in full: the origin, from 2003 lockstep networking to the late-2020 thesis that became MECS, and the crossing from a single-process engine to a UDP-backed multiplayer platform owned outright by the studio. If you are weighing whether to build your own engine, or how to make a deterministic networked simulation hold together, those are the places the reasoning goes all the way down.