DEV Community

Unicorn Developer
Unicorn Developer

Posted on

Game++. Part 1.2: C++, game engines, and architectures

This book offers insights into C++, including algorithms and practices in game development, explores strengths and weaknesses of the language, its established workflows, and hands-on solutions. C++ continues to dominate the game development industry today thanks to its combination of high performance, flexibility, and extensive low-level control capabilities.

1375_chapter_1_pt_2/image1.png

AI

Of all the aspects of game development, AI is my favorite topic. Over the past years, I've spent so much time wrestling with mobs that if I had a dollar for every one stuck in a corner, I'd have bought a house by now. First there were lone enemies, then packs of mobs, then coordinated groups sharing behavior trees (BTs).

Turns out, not every game actually needs "smart" AI. That sounds counterintuitive, but half the time, a couple of simple scripts that have enemies yelping "Ouch!" on a timer and blindly charging into walls are more than enough to satisfy players. AI isn't magic—it's just code. If your engine supports if, switch, coroutines, or timers—congrats: you already have AI support. All the "intelligence" is just a bunch of conditions cleverly disguised as brainpower. But when devs want to go "pro", they usually adopt one or more of the approaches below.

...Behavior Trees (BT)

Basically, think of it as a simple priority list: if you are hungry, eat; if you see an enemy, attack; if you are badly injured, flee. Behavior trees (BTs) express decisions as a hierarchy of composite and leaf nodes; each node returns one of three statuses: Success, Failure, or Running.

Composite nodes—Selector, Sequence, and Parallel—control how child nodes run and implement priorities from the top down. A Selector might order branches such as "health below 20% → flee," "enemy in sight → attack," "hunger above 80% → eat," evaluating them in sequence until one succeeds. Leaf nodes (Action, Condition) perform concrete actions or checks. That structure tends to stay readable—the tree mirrors the intended logic— which is why BTs are widely used in games and work well with visual editors and hot reload, letting designers iterate on AI without recompiling. Still, a BT with 500+ nodes can devolve into spaghetti that nobody can follow, not even whoever wrote it.

...GOAP (Goal-Oriented Action Planning)

GOAP-style AI starts from a question like: "I want to kill the player. What should I do?" It doesn't simply run a fixed script; it assembles a plan from the actions that exist in the world and the current state. Elegant? Yes. But rolling your own planner is no small job. You must handle resources, conflicting effects, action costs, cycles in the search space, and more. GOAP fits heavy, systemic games and is common at large studios, yet it costs serious engineering time and a strong team to implement and debug.

Unlike rigid behavior trees or finite state machines, GOAP is largely declarative: you specify actions with preconditions and effects, and the planner searches for a sequence that transforms the current world state into one that satisfies the goal. For a goal such as "kill the player," it might produce something like: acquire a weapon → close with the player → attack. The planner can infer that attacking needs a weapon, getting a weapon may require reaching an armory, and movement depends on being able to move. That yields more natural, adaptive behavior: the NPC can respond to a changing world and replan when conditions shift.

...FSM (Finite State Machine)

This is the classic setup with states and transitions—idle → walk → attack → flee. A finite state machine (FSM) is a simple formal model: an entity is always in exactly one of a finite number of states, and it moves between them according to rules driven by events or conditions. The pattern maps cleanly to character and enemy behavior. A player might use states such as idle, walk, run, jump, attack, or dead; an AI mob might use patrol, pursue, attack, retreat, or take cover. Each state bundles its own logic: animations, audio, physics tuning, and which actions are allowed. Transitions hinge on explicit triggers: input, collisions, health thresholds, distance to a target, so behavior stays predictable and comparatively easy to debug.

...Utility AI

Look at NPC with a handful of possible actions, each runnable only when its requirements are satisfied. The system repeatedly scores every eligible action by utility and picks the strongest candidate, which yields smooth blending between behaviors: none of the brittle gatekeeping typical of finite state machines, none of the explicit planning overhead of GOAP. Once you give that NPC more than about ten actions, though, and tie each utility curve to dozens of parameters, forecasting what it will do in a given situation becomes almost impossible.

Debugging gets ugly fast—"why did it break into a dance instead of attacking?"—because you must chase every utility term and how they combine. Tuning weights per enemy type or scenario also fights the fact that the architecture keeps chasing locally best moves. That is why production setups often pair Utility AI with an FSM for coarse phases, or restrict scoring to explicit contexts so choices stay interpretable.

...exotic ones: HTN, ML, Neural Nets, RL

HTN (Hierarchical Task Network) extends the GOAP idea with hierarchical task decomposition: complex goals are broken into subtasks through a system of methods and primitive actions. It produces structured plans and supports long-term strategic planning for NPCs defined in that way.

Reinforcement learning (RL) and neural networks promise even more dramatic results: agents that learn strong policies through interaction with the environment, adapt to the player's style, and exhibit emergent behavior.

The results speak for themselves: AlphaStar in StarCraft II, OpenAI Five in Dota 2, agents that outperform humans in complex strategy games.

Those successes create an illusion that the technology is ready for broad use, yet training an agent for even a relatively simple game can take millions of rollouts—weeks or months of continuous computation on powerful GPU clusters. A single training run can cost hundreds of thousands of dollars. Moreover, any gameplay change or new mechanic often means restarting training from scratch.

On top of that, trained models can be hard to reason about. Debugging this kind of "black box" is difficult even for the ML engineers who built it. That is why large studios still tend to reserve ML for targeted uses—procedural generation, player analytics, and similar—while relying on classical methods for core gameplay AI, where predictable behavior and controlled costs matter most.

...pathfinding

The A* (A-star) algorithm and its variants are the workhorses of game AI: they search a discrete navigation graph efficiently by combining the cost-so-far from the start with a heuristic estimate of the remaining cost to the goal. Practical tweaks turn that elegant textbook algorithm into something that can absorb hundreds of path requests per frame.

The harder problems are usually building and updating the graph, and turning graph paths into motion in the real level. A* outputs a polyline of nodes, but the character still has to move along it with a body, inertia, clearance, and dynamic clutter in mind. Familiar failure modes include corner snagging on coarse geometry, jams in tight choke points, trouble routing around movers (other NPCs, the player, physics props), and fragile vertical routing (ladders, ledges, jumps). Even so, A* (plus navmesh or grid tooling around it) remains the backbone of most shipped navigation, while every new map still means tuning geometry and chasing edge cases.

... navmesh

Finally, the bedrock of in-game navigation: a navigation mesh (navmesh) turns messy 3D level geometry into a discrete walkable surface made of polygons and the links between them. Each polygon is (usually) a walkable region; shared edges or portals are where the agent can move from one patch to the next.

A good navmesh trades fidelity against cost. Too much detail spawns unnecessary complexity and slows search; too little detail skews paths and causes characters to hug corners or snag. Today's pipelines lean on automatic bakes. Recast & Detour—the open-source stack behind many Unity and Unreal workflows—voxelizes or samples the level and outputs a mesh using agent parameters (radius, height, max step, max slope, and so on). Commercial tools and in-house bakes at big studios add more specialized rules and are tuned to specific engines and production needs.

No generator, however sophisticated, truly understands a level. It does not know a nook is meant for stealth, that a footbridge is scripted to break, or that a ladder needs a bespoke climb. Common bake failures include links through paper-thin walls or open windows, bogus connections between platforms at different heights, doors/elevators/destructibles treated as static, and broken islands in dense spaces.

So modern workflow is automation plus hand pass: designers place jump links, cover hints, corner fixes, and mark areas with special costs, speed scales, or unit-class rules. Without that manual nav polish, the fanciest decision-making stack is like a wheelbarrow with a wheel missing—it can decide what to do, but it still cannot reliably carry it out in the real geometry.

...AI isn't about "smart enemies"

Game AI is mostly about selling the illusion of intelligence. Players need reactions, pressure, and the occasional surprise. Behind the curtain it is still if statements, switches, and a pile of navmesh—exactly the kind of machinery they should not have to think about. "Good" AI is not the same as genuinely smart AI: the job is to entertain and stave off boredom, not to pass a Turing test. A slow-witted opponent is fine if it behaves the way the player expects. A predictable opponent is fine, too: readable patterns make mastery feel earned. A hand-authored script is fine if it is reliable, fun to play against, and does not break the world.

In short, the goal is not to be smart; the goal is to seem smart. If the player believes they are facing something clever, you have done the important part of the work.

Scripts and configs

Embedded gameplay logic often lives in scripting layers: Lua, Python, JavaScript, AngelScript, and similar, especially when the core engine is built in C++. Those languages tend to fail more gracefully at runtime and can save a lot of iteration time. Even if you are writing an engine from scratch, it pays to plan for scripting early and to keep the host language's strengths and limits in mind: that reduces glue cost and keeps integration straightforward.

Building an engine still means writing plenty of code; there is no real shortcut. Which language should you pick? Frankly, what you already know well matters more than the label on the tin. A seasoned developer will squeeze much more out of performance-sensitive paths in a familiar language and will sidestep the usual traps more reliably. Low-level options such as C++ give you clear control over resources, but they expect solid architecture judgment and a firm grasp of how systems fail and scale.

...it's not about performance or FPS

Script layers and data-driven configs exist mainly so designers can iterate quickly and so less engineering-heavy teammates can contribute safely. High-level languages—Lua, Python, a bespoke DSL, or similar—give you a flexible sandbox for gameplay prototyping: tune balance, try new mechanics, and adjust AI behavior without kicking off a full native rebuild or pulling an engineer into every tweak.

With a low-level-only workflow, even small changes often mean compile, link, and deploy—minutes to hours, depending on the codebase. Interpreted scripts plus JSON/XML-style configuration draw a clean boundary between the stable C++ core and the volatile content layer. You pay a modest runtime tax (parse time, interpretation) but you buy shorter iteration loops, easier A/B experiments, and content patches that do not require shipping new binaries.

...it's about convenience and development speed

Scripting enables a fundamentally different development rhythm: the write–test–fix loop collapses to seconds because languages are interpreted and many setups support hot reload. You can nudge an enemy's detection logic or aggression tuning and see the outcome immediately in-game. Modern engines add live inspectors, debug consoles, and visual debugging hooks - exactly what you want when design asks for constant pivots. Standing up several AI behavior variants in one evening becomes realistic, so teams can experiment without treating every idea like a production milestone. Compared with the C++ pipeline, where each tweak might cost thirty seconds to minutes of rebuild time, a script-forward workflow is simply easier to stay "in the zone" with.

...surprisingly, it's also about safety

Sandboxed scripting is mostly about isolation: each script runs inside a controlled environment with a deliberately narrow API. In Lua, for instance, you can spin up separate VMs (lua_State) with their own global tables and expose only the bindings you intend—blocking raw filesystem access, sockets, process spawning, and similar primitives unless you explicitly whitelist them. Typical gameplay-facing surfaces might look like SpawnActor(), GetPlayerHealth(), or PlaySound().

You can also cap risk operationally: maximum CPU time per script tick, memory budgets, instruction budgets, call counts, recursion depth (whatever your VM exposes) and hard-stop the script when a limit trips. When script code misbehaves, the interpreter can usually trap the error, log context, and let the native core keep running. Native plugins and dynamically loaded modules are different: they generally execute in-process with the engine, so a stray pointer or buffer overrun can still take the whole process down.

...it's about overall project reconfigurability

Hard-coded literals in C++ become architectural friction: every tweak to an AI tunable forces a full native iteration: preprocess, compile, link, ship bits and before anyone can feel the change in-game. That pain shows up fastest when you are chasing numbers such as damage modifiers, nav penalties, aggression thresholds, or weapon timings. The straightforward fix is to push anything gameplay-facing into external data (JSON/XML/INI), spreadsheets, custom DAG formats, or database rows, so designers can rebalance without touching source. Many engines layer reflection-style property systems on top so values hydrate from disk and can be edited live in inspectors.

Hot reload for scripts usually boils down to watching files and re-importing modules: the tooling notices that a script changed, reloads that unit inside the running interpreter, and keeps the native host process alive. That workflow matters when you are iterating on AI rules, event handlers, or broader mechanics and want feedback without a reboot cycle.

...it'so a door for the community

User-generated content needs a low floor for contributors: easy tooling and modest prerequisites. Shipping a mod-facing C++ surface usually means an SDK with headers, build docs, ABI promises, and matrix-testing across compiler versions and platforms: Visual Studio generations, GCC, Clang, and whatever else your players use. Scripting sidesteps much of that: the interpreter already ships inside the game, so modders mainly need an editor, your dialect docs, and enough syntax to stay out of trouble.

A modular script API also gives you a narrow contract: gameplay-facing calls stay stable while native internals churn. Native plugins are different—engine upgrades often force rebuilds, toolchain bumps, and subtle breakage even when nothing "meaningful" changed on paper. Script packs tend to keep working across releases as long as the exposed bindings and semantics stay compatible.

...you can't please everyone

Lua has become the de facto gold standard for embedded gameplay scripting: small runtime, fast enough for tight loops, straightforward C/C++ binding, and oceans of examples and reference material. Most titles drive it from a single gameplay thread; when teams need parallelism, they typically spin up separate Lua states (or isolate work carefully), rather than treating one VM as freely multithreaded.

Python's ecosystem is deep and the syntax is approachable, but shipping it inside an engine is often painful: interpreter startup, packaging, the GIL, and embedding details can consume engineering time you would rather spend on gameplay. That friction pushes many projects toward Lua for in-engine scripting.

JavaScript, through embeddable engines (V8, QuickJS, etc.), can deliver strong throughput and reasonable memory use—modern JIT/AOT JS stacks are mature, and for some teams they are a credible alternative to the usual Lua route.

Smaller embeddables—AngelScript, Wren, Squirrel, ChaiScript, and friends—fill niches. They trade different ergonomics and feature sets, and could have been stronger contenders if Lua had not become the default reference point; today they are usually judged against that baseline.

Networking

It is no secret that everyone dreams about it: persistent worlds, co-op, PvP, MMO-scale ambitions, or even simple player chat. Then you crack open Wireshark and your dreams narrow to one thing—getting any of it to actually run.

Here is the good news first: your game may not need networking at all. If you are building something offline—a puzzle game, a platformer, or "SimTower but with toads"—you can happily ignore the whole topic. If "multiplayer" suddenly sounds tempting, though, tighten your seatbelt. You are stepping onto a road where everything matters, and there is no universal architecture that fits every title.

Modern languages such as C#, Rust, or Go ship tolerable ergonomics for sockets—async IO, nice coroutines, that sort of thing. In C++ (you have my sympathies) you will probably reach for a battle-tested third-party stack such as RakNet, ENet, or similar. None of them feel "brand new," but they have shipped in countless titles.

Sockets are only the handshake at the door. Establishing a connection is roughly one percent of the job; the interesting problems start immediately afterward. What exactly is a "networking engine," anyway? Good question—I asked myself the same thing while hacking together my first co-op prototype. I never got a crisp definition, but I collected a long list of features you suddenly care about, which I will walk through next.

...state synchronization

This is the bedrock of multiplayer, and also one of the hardest parts of network programming. Every participant runs their own local simulation of the world, yet those simulations must stay compatible enough that play feels coherent. Perfect lockstep is not achievable in principle: latency means that by the time news of one player's action reaches everyone else, the situation may already have moved on.

So, teams pick strategies that match the genre. Turn-based games can pause until acknowledgements arrive. Real-time titles usually trade purity for responsiveness—either constrain pacing to the worst connection, or predict locally and reconcile mistakes afterward. Another common pattern is an authoritative server: simple conceptually, tough on high-latency clients who constantly receive corrections.

Fast-moving interactions are where it gets ugly—mutual kills, two players grabbing the same pickup, racing inputs on a contested objective. Some arbiter (often time-stamped server adjudication plus latency-aware rewinds) has to decide what "happened first," even though each player experienced different delays. Get that wrong and players see constant rewind-snaps—"rubberbanding"—as the simulation reconciles against what actually occurred.

...input prediction and rollback netcode

Rollback netcode keeps fast games playable even when latency is high. The idea is straightforward: do not stall the simulation waiting for remote input—advance locally using predictions. When real inputs finally arrive, compare them with what you assumed; if they disagree, rewind to a saved checkpoint and replay forward with the corrected inputs.

Shippable rollback stacks almost always need fast snapshotting or equivalent bookkeeping: enough history to restore an earlier timestep and resimulate deterministically. Practically, that means storing periodic world snapshots or compact state deltas—positions, velocities, animation phases, RNG seeds, anything that affects simulation outcomes—plus the input stream needed to replay.

When a mismatch fires, you load the snapshot at the divergence point and fast-forward to "now," applying the true inputs along the way. Done well, players barely perceive the correction—the game keeps feeling crisp even around 100–150 ms RTT, where naive lockstep feels awful.

The catch is engineering cost: the gameplay simulation must be deterministic and replay-safe, which often forces broad refactors across gameplay code, physics hooks, and fixed-point or controlled-float policies. Saving "the entire world every frame" is the textbook mental model; production engines usually optimize that snapshot strategy, but the obligation remains the same —you must be able to reconstruct the past quickly enough to hide latency.

...interpolation, extrapolation, smoothing

Networked games constantly deal with messy delivery: packets show up at uneven intervals, drop entirely, or land out of order. If you simply apply each update as it arrives, motion looks choppy and often unplayable. Interpolation smooths that out by blending between known samples, if you know a player was at point A at time T1 and at point B at time T2, you can render a continuous motion between those keys instead of snapping.

Extrapolation pushes further: you guess where something will be next from its current velocity and heading so you can keep moving it while you wait for fresh data. The trade-off is accuracy— the longer you predict ahead, the more likely the object suddenly stops or turns, and your guess diverges from reality.

...secure communication protocol

Network protocol security begins with never trusting the wire blindly. Every inbound message should be validated: correct framing, sane ranges, and consistency with what that peer is allowed to do right now. If you apply client-supplied state without checks, you invite classic exploits: arbitrary teleports, inventory stuffing, stat inflation, and worse.

Authentication and authorization harden the perimeter: only legitimate clients should join a session, and each peer should only act within its role. Sessions normally start with identity proof—tokens, signatures, or platform-issued tickets—and critical operations should re-check permissions and freshness. Rotate credentials on a sensible schedule, enforce validation on high-risk actions, and treat session keys as a second line of defense so a stolen long-lived password does not instantly mean game over.

...encryption (you don't get paranoid for nothing)

Encryption in games is not paranoia, it is basic hygiene. Even casual titles move data that should stay private: progression, virtual currency, handles tied to accounts. Modern capture tools are so approachable that unencrypted traffic is trivially observable. TLS is the usual baseline for protecting bytes on the wire, but cryptography is not free: handshakes cost round trips, and ciphered payloads add CPU to margins that can sting in latency-sensitive modes where tens of milliseconds matter. That is why many realtime stacks blend approaches, such as lighter session crypto for steady-state updates, selective encryption for the highest-risk fields, or tuned stacks built for games and while still refusing to ship gameplay plaintext that trivially enables spoofing and griefing.

...object replication

In a multiplayer game, the same logical object often exists in several places at once: on the server, on each client, and sometimes in more than one representation on the same machine (for example, authoritative versus predicted). The server copy typically owns the facts that matter for outcomes (health, transforms, inventory counts), while what you render locally may be interpolated, extrapolated, or rolled back.

A replication layer must choose what to sync, how often, and to whom. Private facts (full inventory loadouts, quest flags, hidden traps) should narrowcast to the owning player; public signals (rough pose, equipped silhouette, team tint) can broadcast to everyone who needs them for rendering or fairness. Delta compression and other bandwidth tricks shrink payloads, but each trick adds edge cases you must reason through when state diverges.

...logging and debugging

Logging in networked games is not merely an ops checkbox. For contentious bugs you often want an audit trail for player inputs, meaningful state transitions, and sometimes even packet-level breadcrumbs when you can afford it.

The catch is volume: a busy title can spill gigabytes per hour if you log naively. Production setups therefore rate-limit, sample, struct-tag, and compress, and they aggressively drop chattier channels in hot paths.

Structured logs (timestamps, build ids, match ids, player ids, entity ids) are what let you rebuild a story after the fact. When a report says "my unit turned into a different type," you need a thread you can follow: spawn time, command stream, authority changes, template swaps, replication apply order. Without that kind of chain, the bug becomes guesswork.

... dealing with cheaters

Fighting cheaters is a slow arms race, and the house does not win every round. Every mitigation invites a countermeasure; every anti-cheat update trains tooling to adapt faster. The structural issue is simple: part of your game executes on hardware the attacker owns. Anything you ship to the client can be inspected, patched, replayed, or spoofed at some level of effort.

Server authority is still your backbone: validate outcomes and sanity-check inputs so blatant lies die server-side. Pure perfection would mean simulating everything centrally, which quickly collides with responsiveness and cost, so teams settle on layered checks on the moments that matter.

Client-side anti-cheat scans memory and modules for known signatures and anomalies. It can help, but it walks straight into trust and privacy debates: kernel drivers and elevated monitors feel invasive, players hesitate to install them, and false positives annoy legitimate setups from capture tools to security suites.

UI

You can ship a game with no physics, no AI, even no sound. Shipping without UI is almost impossible. These days a completely UI-less title reads as intentional avant-garde indiesperimentation. Besides, you still need somewhere to put New Game and Quit.

On paper it sounds trivial: draw a few buttons, drop in labels, maybe a slider. Then reality arrives. Interactive widgets accumulate states. A button is not one bitmap; it is idle, hovered, pressed, focused, disabled, and sometimes half a dozen custom variants. Same story for fields, toggles, lists.

Hierarchy piles on complexity. Modern screens are trees: a button lives inside a row inside a panel inside a sheet that might not exist until a tab opens. Each layer needs layout, clipping, focus routing, and visibility rules that do not fight each other.

Coordinate spaces make UI math fiddly. Meshes live in one world space; UI fragments flip between pixel screenspace, scaled logical units, and parent-relative boxes. Hit testing, anchoring, DPI scaling, and safe areas all sit on top of that stack.

Input parity is another whole axis. Desktop expects mouse precision and keyboard traversal; mobile expects fat fingers and gestures; consoles expect focus rings and analog navigation. One layout rarely fits all without deliberate affordances.

Polish is where hobby UI dies and production UI begins: transitions, micro-motion, feedback on press, disabled styling that still reads clearly. None of that is mere ornament; it is what separates "dead controls" from something that feels alive.

And then you notice the trap: you set out to ship a game, but you are halfway to authoring a mini framework that may never leave this codebase. Even when you embed a mature stack (Scaleform is better than its reputation, for what it is worth), you still owe integration work: input translation, event bubbling, font atlases, shader passes, batching, localization hooks.

So yes, UI is structured, layered, platform-sensitive work that teams routinely underestimate. It is still worth doing properly at least once, even in a small scope. Just do not expect weekend one to reproduce Unity UI, Unreal Slate, or the entire HTML ecosystem.

Tools

Not every engine or game needs a heavyweight toolchain around it, and that should not surprise anyone. Sometimes a handful of Python glue scripts is enough. Many commercial stacks ship editors that feel like an IDE crossed with a DCC app: place entities, tweak materials and shaders, tune physics, scrub animations, poke UI, and scrub changes live.

Building an editor that polished is almost as expensive as building the runtime itself. For a small custom engine, chasing Unity-parity chrome is usually a bad ROI: months disappear into docking layouts and undo stacks you never needed.

Still, do not ignore small tools. A few evenings of scripting for bake helpers, batch exporters, or sanity validators routinely buys back weeks of manual slog later.

...level converters

Custom binary schemas exist because you often want load paths that map cleanly onto engine structs and arrays with minimal translation cost.

Text interchange formats such as JSON or XML pay for human readability at runtime: lexing, parsing, string-to-number conversion, heap churn. A well-designed packed blob can mmap or fread straight into layouts you already use in simulation, which matters when a level streams thousands of transforms, material handles, and tagging metadata.

Binary pipelines also give you room for embedded compression, baked LOD chains, precooked nav data, hash tables, whatever preprocessing saves milliseconds on the critical path.

Conversion tooling ties into normal asset workflows: watch folders, rebuild packs when sources change, split authoring formats from shipping blobs so artists keep familiar exporters while engineers evolve on-disk layouts or platform-specific variants without forcing daily disruption.

...asset validation

Asset validation is one of the few things that cheaply pays for build integrity: it blocks bad data before it poisons a milestone. Validators walk reference graphs, because every serious asset points at other assets through GUIDs, source paths, or custom handles, and a missing link is a late-night crash you do not want in QA.

They also enforce content rules: materials must resolve textures, mesh chunks must bind valid skeletons, animation clips must line up with rig definitions, shader permutations must exist for the platforms you actually ship.

Hook those checks into CI and the content bake so failures surface with filenames, not mystery repros. On big teams, that is how you keep an artist's innocent rename from silently deleting someone else's dependency three departments away.

Time invested in tooling should pay rent: if a workflow still means hand-editing two hundred lines of JSON, a small exporter or inspector script will usually earn its keep faster than another meeting about process. You still should not build a glittering editor "because engines have editors," but targeted automation is rarely wasted effort.

Architecture

Eventually the parts exist: rendering, audio, data, physics, UI, maybe AI and scripts. The missing question is how you wire it all into something that runs as one product. A few established patterns keep showing up in production codebases.

...no architecture is also architecture

After fighting overbuilt frameworks, many small teams fall back to a deliberately dumb architecture: a handful of independent libraries, each owning one slice of the problem. You wrap rendering behind a gfx module, audio behind audio, streaming behind assets, polling behind input. Modules stay loosely coupled on purpose; nothing magically knows about anything else unless you wire the calls.

That was the default posture of late-nineties and early-aughts codebases. It is quick to stand up, easy to reason about in small scopes, and leaves execution order entirely visible.

Discipline becomes the hidden dependency: without scene graphs or forced pipelines, nothing stops you from forgetting a step.

Scale hurts first in your head: the call graph sprawls, initialization order turns fragile, and regressions arrive as subtle sequencing bugs. UI stops refreshing after a content reload, music fails to cue on level transition, physics wakes before assets finish streaming. Each fix is manual glue, because the missing piece was never "library quality," it was composition.

Ironically, the wiring you tried to postpone is the architecture. Evading structure does not delete it; it just relocates the burden into ad hoc calls sprinkled everywhere.

...godobject is also architecture

Large game engines like Unity, Unreal, Godot, CryEngine, Dagor, and others often adopt an approach based on a single base class. Typically named Object, Entity, GameObject, Actor, or similar, this class becomes the ancestor of all game entities. This base class usually contains virtual methods for core game functions:

struct GameObject {
  virtual void update(float dt);
  virtual void render();
  virtual void onEvent(const Event&);
};
Enter fullscreen mode Exit fullscreen mode

Everything you author, from HUD widgets to hostile NPCs, subclasses one mega-type. Each instance magically inherits tick callbacks, draw hooks, and event plumbing. Convenient? Extremely. You get a uniform surface area "for free," plus a natural place to hang a central registry.

The downside is inheritance sprawl: one mammoth base class fights composition, mixing orthogonal concerns into the same vtable. Want multiple reusable behaviors? You bolt more flags and overrides onto GameObject. Within a few milestones that base type balloons into hundreds of lines of virtual hooks.

Bolting "components" onto the same hierarchy often compounds the mess instead of curing it. Eventually you regret the shape of the tree, yet countless shipped titles prove you can still finish under this pattern if scope stays modest.

...ECS is good! But...

This architectural model has exploded in popularity among indie developers and enthusiasts in recent years. The core idea is a radical separation of data and logic, built around three key concepts. An entity is just an identifier, with no logic or data of its own. A component is a container for a specific type of data, such as position, health, or sound parameters. A system is a functional block that processes components according to specific rules. None of them works alone, but together they replace traditional object hierarchies with batch-oriented data processing.

For instance, a physics system can walk all entities that have both Transform and Velocity and update every moving object's coordinates in one structured pass. Benefits include strong performance from cache-friendly layouts, clearer separation of responsibilities between subsystems, and good scalability.

Drawbacks include jumping straight into a fairly complex architecture that often needs a lot of boilerplate and patterns. Without specialized editors, ECS-heavy codebases can be hard to read, and without visual debugging tools, diagnosing issues gets painful. For smaller projects, it can feel like using a sledgehammer to crack a nut.

Game engine

Maybe you should leave everything exactly as it is? Yes and no. The engine, and the scaffolding around it, is a means to an end. If you are hacking on a weekend hobby project, do not chase the perfect framework, or you may ship nothing but scaffolding while the actual game keeps slipping. Still, architecture matters the way a skeleton matters: nobody admires your ribs in screenshots, but if the bones are crooked, everything built on top aches, bends, and snaps under light stress. When the structure becomes genuinely painful, do not be precious about it. Throw it out and rebuild. Everyone tosses bad foundations sometimes (that is the spirit behind the "three systems" rule). Classics such as Jason Gregory's Game Engine Architecture remain relevant because nobody has handed us a strictly better substitute.

To learn how games actually tick, you do not need an Unreal-scale codebase on day one. If you want to avoid becoming yet another résumé line that reads "Unreal C++" yet hides shallow familiarity with how containers or allocators behave, try shipping your first or second project with minimal middleware. Make something deliberately crude: a window, a handful of sprites, a tiny resource loader that stuffs textures into a .zip. Call it a toy engine if you like: simple, buggy, full of reinvented wheels, but yours, and built while knowing what each lever does. Skip Unity, skip Godot, skip the polished shortcuts that quietly vault you past ninety percent of what is happening under the hood.

You will fight architecture problems, patterns, and asset budgets head-on. It costs calendar time, but you earn more than a demo reel clip: you earn intuition about internals that is hard to fake in interviews. That is where serious growth starts, without borrowing momentum from black boxes. Someone who only knows editor wiring may stay boxed into one toolchain; someone who understands memory layouts, threading contracts, and asset pipelines can adapt across engines.

That said, if Godot, Unreal, or another turnkey stack matches your patience and goals, use them. Ambition and pace matter more than purity contests. Even if you eventually standardize on third-party tech, the scars from rolling your own stack become leverage: you stop treating engines like magic and start reading them as engineered systems.

As always, the choice is yours.

Author: Sergei Kushnirenko

Sergei has over 20-year experience in coding and game development. He graduated from ITMO National Research University and began his career developing software for naval simulators, navigation systems, and network solutions. For the past fifteen years, Sergei has specialized in game development: at Electronic Arts, he worked on optimizing The Sims and SimCity BuildIt, and at Gaijin Entertainment, Sergei headed up the porting of games to the Nintendo Switch and Apple TV platforms. Sergei actively participates in open-source projects, including the ImSpinner library and the Pharaoh (1999) game restoration project.

All parts

Game++. Part 1.1: C++, game engines, and architectures

Top comments (0)