Abnormal programming

Game++. while (!game(over))

May 6, 202535 min

Back when the trees were tall and game engines were small, there was no choice of whether to write your own — if you didn't have an engine, you basically didn't have a game. Some people bought someone else's engine and enjoyed those lovely imported contraptions, while others built their own, which took months, if not years.

This series of articles was born as notes in the margins of the wonderful book Game Engine Architecture. It's a big, thick book that covers every aspect of building an engine. But it lacks the nuances of practical development. And to see those nuances you need to understand not just the theory — GEA is mostly theory after all — but how a game's code works from the inside. To understand how, and most importantly why, the chosen mechanisms are used inside a game, to spot performance and architecture problems, how to find them and how to fix them — for that you have to understand how game engines work and how they were built.

If memory serves, Carmack said the best way [to make games] is to write your own engine ("The right move is to build your own engine"), to which many will object: it's not that simple at all. But the father of Doom is known not only for his contribution to game engines, but also for being fairly critical about the state of game engines in general, and about the advantages of building your own technology instead of using ready-made solutions.

This philosophy, which he has held throughout his career, came from the idea that your own engine gives developers full control over the technology and the ability to build solutions tailored to the specific needs of their projects.


This is part of the «Game++» series:

But you know, everyone is right in their own way — there's no universal answer, it all depends on the specific situation. Maybe building your own engine isn't as hard as it seems? Why even do it? And what does "build a game engine" even mean?

I've worked with various game engines, built a couple of my own from scratch, and two more before those went into the trash. Three commercial games shipped on the first (third) one, without much success, and in the end it went to a studio in St. Petersburg for a crazy pile of evergreen raccoons, all of 70^2 of them — that was 15 years ago. And I've seen how developers use other engines, how they complain about them, how they talk about their own solutions. I studied the source code of other engines (open and not-so-open). And I think I have a couple of things to say on this topic, though I'll admit my experience may be somewhat biased or one-sided — I always wanted to make strategy games, but for some reason I was mostly offered shooters.

First big question — why?

For me the first two and the last point mattered most. Learning is fun; learning and making a game is fun squared. And I just love programming, doing things the language "supposedly" doesn't allow, digging into algorithms and systems, poking around in the complex details and subtleties of the language. And the heart-to-heart chats at interviews are a bonus on top.

People complain about engines: this doesn't work, that broke, this new thing doesn't support that old thing, and that old thing was requested ten years ago and still hasn't been added. Of course there are reasons for all of this — if you've ever worked at a big company, you'll definitely understand me. But it's always easier to blame someone else than yourself. If you need that feature so badly, just go and write it, no? No, because it's not your engine. The engine is someone else's — but the disappointment is yours. That's expected. Just as it's expected that a feature you need in your own engine will be added when you actually need it.

Second big question — do you even need to?

Here's the thing to understand: the cost of developing your own engine will never pay off; there will be no moment when, as of yesterday, the engine started breaking even. No, even for top studios this is a loss-making line item. Games make money, engines don't. From Unreal's own report, the cost of engine development over just the last four years was more than $160 million — Epic effectively makes a new AAA game every year, we just don't see it.

But once you've written a good foundation, you can reuse it again and again, improving it with each new game. If your goal is to ship a game as fast as possible, just pick a ready-made engine and use the functionality it has. But if you want to grow as a developer, build yourself a base for future projects — not only in gamedev — and you have the time, energy and patience, then go for it; your own engine is perhaps the best application of your knowledge!

So, is it worth making your own engine? At the very least, it's worth thinking for yourself and not relying on the opinion of the author of some random article on Habr.

What is a game engine?

Is SDL/SFML/Allegro a game engine, even though countless indie games have been made with them? Definitely not — these are libraries wrapping platform code, even though they can do everything a basic game engine can. What about Vulkan/DX11? Definitely not — that's a graphics API. What about FMOD/WWISE? Also no — but they let you implement a 2D/3D scene, an audio one, granted.

Now imagine a small project that combines Allegro, DX11 and stbi (a library for loading images of various formats) to draw moving sprites on the screen. Would that be a game engine? Also no, though very close — a game engine appears when there's a game, or games, made with it. Until there are games, it's just a set of libraries glued together somehow through cmake.

Maybe it's just a combination of graphics, animation and user input handling? A game engine without a game is just a set of tools, libraries and other components designed to help create games. If that definition seems vague, almost useless to you — it's not just you.

If you're making a 2D pixel-art platformer, do you really need complex material editors and kilometer-long shaders, seamless geometry and global illumination for it, 3D physics with inverse kinematics and networked multiplayer support? Unreal has all of that and at decent quality, but do you need all of it? And it requires experience, getting used to, and the ability to set it up and use it.

All you'll need is to handle input, render sprites, maybe even raw PNGs on screen, and play sound. All of that can be written in a couple of days following open guides on YouTube. And those couple of evenings will be spent usefully, and now you've moved on to the main thing the whole thing was started for — making the game.

And if you want to create a AAA hit with top-tier graphics, physics and everything else? Then yes, you'll need all those incredibly complex systems. And a couple of dozen free years of your life to work only on the engine every day and implement all of it — yes, modern game engines have gone hopelessly far from the retro platformer.

What can a game engine do?

The first thing I want to say is that an engine can be small, built specifically for your game. But let's be honest — we're spoiled by the capabilities of big engines and carry these expectations over, expecting the same behavior from everyone else. But if an engine is tailored from the start for one particular game, having few features is also normal. Nevertheless, most game engines will be able to do the following:

Spin the game loop and handle events
  • visual level and UI editors

  • logic updates

  • a scene/level system

  • an entity manager

  • a game state serialization system

  • state rollback (for testing and debugging)

  • a variable inspector

Handle input
  • keyboard, mouse, controller, touch

  • multi-input support (simultaneous controllers, local co-op)

  • remappable keys and control profiles

Artificial intelligence
  • an AI and behavior debugger

  • a built-in navigation system, pathfinding across the map

  • dynamic navmesh updates

  • a behavior system (e.g. behavior trees or FSM)

  • perception (visibility, hearing, aggro)

Handle physics: 2D or 3D
  • joints (connections between objects)

  • raycast (line collision checks)

  • collisions and triggers (colliders)

  • dynamics (mass, forces, impulses)

Load and manage resources
  • packing resources into archives

  • textures, models, sounds, scripts, levels

  • the ability to load formats (via libraries: stb_image, Assimp, FMOD)

  • caching and reusing data

  • support for hot-reload (reloading on the fly, without restarting the game)

  • tracking dependencies (if a shader uses a texture — reload them together)

  • generating builds for different platforms

  • object and component inspectors

  • hot-swapping code or data (hot reload)

  • support for "prefabs" — object templates

  • support for auto-update and patches

Render the final image
  • CPU/GPU/memory profilers

  • skeletal animation

  • blend trees and animation layers,

  • abstractions over low-level APIs (OpenGL, Vulkan, Direct3D, Metal)

  • support for cameras, perspective, lighting, shadows, post-processing

  • the ability to draw both a 2D UI and a 3D scene

  • materials, shaders, LOD, batching, occlusion culling

Support a scripting language or languages
  • writing object behavior in a convenient language (Lua, Python, GDScript)

  • not rebuilding the game on every logic change

  • implementing hot-reload

Audio
  • play media, sounds and effects

  • multi-channel sound (music, effects, speech)

  • sound positioning

  • effects (echo, reverb, filters)

  • mixing (combining sounds into a final stream)

  • volume control by group (music, UI, effects)

  • loading formats (via OpenAL, SDL_Audio, FMOD, Wwise)

User interface
  • buttons,

  • sliders,

  • text,

  • lists, tabs, panels.

Networking
  • unit tests for game systems

  • recording and playback of sessions

  • sockets, UDP/TCP

  • game state synchronization (object states, player actions)

  • replication, interpolation, prediction

  • lobby system, matchmaking, authorization

Having an architecture that ties all these subsystems into a single whole is optional — for now optional — but you have to understand: your particular engine may contain only part of this list, and still remain a game engine. Not every game needs all the features at once — otherwise no indie game would ever see the light of day.

Programming: the nuances

This may sound strange, but classic programming isn't mandatory for implementing game logic. Just look at visual programming systems (Blueprints, VisualScript, ICS), which have existed for decades and let you create complex mechanics without writing the code itself. This is especially popular among designers and artists who want to quickly prototype ideas without diving into the syntax of programming languages.

And yes, designers write code too; visual programming is a full-fledged kind of coding, the words class, if, for just look different. The point, however, isn't the terms but the practical aspects. Implementing visual scripting in an engine requires building specialized tools: node editors, a visual debugger, data-flow visualization systems, optimizers and a lot more. This significantly complicates engine development; for example, in Unreal Engine there's a huge amount of low-level C++ code behind Blueprints, comparable to the renderer or the animation engine.

Classic code remains the most universal and flexible way to implement logic, but it keeps losing ground. It doesn't depend on the limitations of visual tools and lets you work at the level of system resources. However, the classic approach drags along classic problems, like recompilation, stopping the engine to rebuild and losing time on all these operations, not to mention that training designers in various programming languages like C++ turns into big financial and time costs, while the cost of an error is the same as for a game programmer — an engine crash.

That's why embedded logic uses scripting languages like Lua, Python, JS, Angel — especially if the engine is written in C++. They're much more forgiving of runtime errors and save a ton of time. Even if you develop an engine from scratch, it's easier to build in scripting capability and the use of the language the engine itself is written in from the start — this reduces overhead and simplifies integration.

... and the myths
Building an engine still requires programming; there are no short and easy paths here. Which language to choose? Honestly, it's not critical — the best language is the one you know; an experienced developer will optimize performance-critical spots much better in their own language and avoid beginner problems along the way. Low-level languages like C++ give transparency in resource management, but also require qualitatively more understanding of architecture and development theory.

Main window

Hidden text

Most games need a window to work at all. Even browser games are no exception here: the window can be either the whole web page or a specific <canvas> element inside which the game runs. Creating a window usually happens through platform-dependent mechanisms; these low-level interfaces tend to look bulky and inconvenient, and moreover, for each platform (Windows, Linux, macOS, iOS, Android) you'll have to write separate window-creation code — but it's written once. And then nobody ever comes back to it.

Besides the window, there's usually a graphics API context (OpenGL, Vulkan, DirectX), which is also created through scary platform-dependent calls. But again, you'll have to write them only once, or take a ready-made library that takes all of this on itself (SDL2, glfw, SFML or any of a thousand other libraries).

After creating the window, the next step is to set up the game loop — it's what will handle events, update the game logic and redraw the screen.

The game loop

A few useful links

Here's what unites virtually all games and game engines, although the loop itself may be buried deep inside the engine, replaced by abstractions or callbacks. But ultimately the game loop code looks like this:

int main()
{
    createWindow();
    init();
    while (isRunning())
    {
        handle(InputEvent());
        updateFrame();
        drawFrame();
    }
    shutdown();
    destroyWindow();
}

The game loop keeps the game running; without it the main function would simply finish and the game would close. There are many ways to represent the loop in an engine's API. It can be fully explicit, when the engine user writes while (engine::isRunning()) somewhere in their main function.

It can be hidden inside the engine, for example in some engine::run() function which, in turn, calls some functions provided by the user (callbacks). Whatever you choose, you'll still need a game loop.

Input handling

Basic solutions

User input is one of the basic components of any game, even if all it has to do is tap a hamster.

At the most basic level, any input is handled with platform-dependent APIs like WinAPI, X11, Cocoa, but they're inconvenient to work with: they're not complex, but very verbose and require individual code for each supported platform. But almost all of these APIs provide a way to pull events into a function like pollEvents(), with which you can then sequentially receive events from the user.

When it's time to integrate input into a game engine, different approaches are possible. One of them is to give the user direct access to the event system. In this case the game developer requests events through pollEvent() themselves and decides how to handle them themselves.

The basic structure of event handling usually looks like this: on each iteration of the game loop an event-handling function is called, where all the incoming events are parsed sequentially. Depending on the event type, the corresponding functions are called or certain actions are taken — for example, handling mouse movement, key presses or window closing.

while (engine.isRunning()) {
    Event event;
    while (engine.pollEvent(event)) {
        switch (event.type) {
            case Event::MouseMove:
                application.onMouseMove(event.mouseX, event.mouseY);
                break;
            case Event::KeyPressed:
                application.onKeyPressed(event.keyCode);
                break;
            case Event::WindowClose:
                engine.quit();
                break;
        }
    }

    engine.update(); // Game logic
    engine.render(); // Frame rendering
}

This way gives maximum flexibility, but requires more code from the user and knowledge of the event structure.

Another option is to embed event handling inside the engine and give the user only callback functions. This makes the initial development simpler and faster, but requires a well-thought-out architecture for registering these callbacks.

void onMouseMove(MouseEvent event) {
  application.onMouseMove(event.mouseX, event.mouseY);
}
void onKeyPressed(KbEvent event) {
  application.onKeyPressed(event.keyCode);
}
void onWindowClose(AppCloseEvent ev) {
   engine.quit();
}

int main() {
  engine.subscribe(onMouseMove);
  engine.subscribe(onKeyPressed);
  engine.subscribe(onWindowClose);
  while (engine.isRunning()) {
      Event event;
      updateFrame();

      engine.update(); // Game logic
      engine.render(); // Frame rendering
  }
}

Graphics

SDK

We already have a window. We can even react to the mouse and keyboard somehow. But… we can't show that reaction yet. Time to deal with graphics. Honestly, 3D always came hard to me; I always envied my buddy @megai2, who effortlessly writes renderers for anything, from a PlayStation to mobiles.

Most window-creation libraries, such as SDL2, already include 2D graphics support — you can draw right into the window, with textures, or by manually filling pixels with your favorite colors. Primitive, but honest.

Let's say the engine is tailored for 2D games with tiles and sprites. Then you'll most likely have to implement something like drawSprite(image, x, y). Possibly with support for all sorts of niceties: scaling, rotation, color tinting, desaturation, alpha blending. Internally this can be implemented manually — by directly copying pixels with some calculations and blitting. Or through another library like cairo, which internally turns out to be a pretty decent renderer, capable of, if not everything, then a great deal. Or through OpenGL/DX/Vulkan, if your soul craves hardcore and speed.

Or you can choose not to specialize at all and just hand the engine user universal wrappers, the way most libraries like SDL/SFML/Allegro do — let them figure it out themselves. In my implementation for Pharaoh there's a wrapper over SDL2 (which can use native GAPIs), but there's no full-fledged renderer in the usual sense. For each engine I wrote my own rendering system from scratch. On one hand, it was painful. On the other, you can experiment with graphics. However the rendering system is built, sooner or later you'll need to render text. For the UI, debug info and so on. You can, of course, draw the letters manually with pixels, but it's better to take a proper font library — for example, FreeType. For masochists and Unicode lovers — harfbuzz. Just don't write your own font parser — that path leads to pain.

Assets

Resources

Resources are everything that brings the future game to life: sprites, configs, scripts, textures, sound files, 3D models, levels, AI and other pieces of content, without which your whole great rendering and physics system is just an expensive doodad on GitHub.

How to store and load them? Well… there are, as usual, a million ways.

You can, for example, compile all resources into one big binary blob and bake it inside the executable, the way many games did and still do. Android and iOS did this at first. The advantages? Everything is right there, everything is fast — no "loading", no file paths — you just grab an array of bytes and work with it as a resource. Sometimes this saved various projects, genuinely saved them, and let you pass certification for Sony or the stores — and so what if the binary is 700 megabytes, that's all the cat's fault and the junior programmers', who wrote a pile of templates.

But of course there are downsides too. You won't be able to change a single asset without recompiling the whole game. Even if you just want to change the color of one pixel in a texture — wait for a build. And it also makes modding practically impossible. How will players insert their custom hats if all the content is sealed into the .exe?

You can store resources as ordinary files in an assets/ folder next to the executable. Then the engine simply loads the needed file when the level starts, if we didn't worry about performance ahead of time. This method is ideal for development: fast, transparent, you can go into the folder at any moment, replace a texture and see the changes in the game. And it's a real gift for modders: take it, change it, add to it — everything is open.

You can go further and pack all the assets into an archive. It can be an ordinary ZIP (and then zlib will come in handy), or a homemade format tailored to the engine's specifics. Big commercial engines, by the way, usually go exactly this route. Archives are convenient to load, convenient to cache, and nobody pokes around in them with their hands… except those who really want to. This is where VFS (Virtual File System) or archives come to the rescue, understanding what lies where and how to get it out.

However the assets end up being stored, they still need to be turned somehow from raw bytes into meaningful data. Here you'll need format libraries, stb_image — to turn a PNG into an array of pixels. Or various JSON ones — to pull entity information out of JSON-like configs, if you haven't become disillusioned with JSON as a config format yet. At some point the team comes to understand that a purely declarative config format doesn't cover all cases, and then VMs (Virtual Machines) for js/lua/squirrel/angelscript come into the engine — they're supposedly scripting languages, but are used for parsing configs — or a homemade format is invented, which in the end also arrives at a VM-style look.

And you can also — I'd say you should, as the second step after configs — add support for hot reloading of assets, so you don't restart the game a thousand times a day. That is, you change a picture, a sound or a config — and hop, it's all already picked up right there in the game. Magic? No, just one more reason why the resource manager grows into a monster that the rest of the engine suffers from.

Audio

Sound&Fx

Sound output is, as you've probably guessed, hell on platform-dependent APIs. Ugly, unintuitive, capricious; the Xbox in particular loves to spit out exceptions if you write too much into the buffer, and… well, if you put in less than you allocated memory for the buffer, it'll also spit out. I'm not a real welder, i.e. not an audio programmer, but even a small dose of adventures was enough to understand — if you just want to play a sound, get ready to suffer. Or… you can take a library that hides all this nightmare behind a normal interface. For example, OpenAL or SDL2. Lately I've been using the latter, because it's very forgiving of errors, even if you do something completely wrong.

These libraries usually expect from the engine a so-called audio callback — a function the system will call (usually from a separate thread) dozens or even hundreds of times per second. Each call — you have to return a bit of audio data — everything else is your concern. You want to — twiddle your thumbs in silence. You want to — play a single MP3 in the background. But most likely you'll need "a bit more": music, effects, voice, all in different threads, with different volumes, with smooth fades, without clicks, without jitter, with reverb effects… well, you get the idea.

For all of this to work as it should, you need a mixer. Not a human, of course (though sometimes that wouldn't hurt), but an audio mixer — a library that will combine the many sound streams into one, carefully control their volume, filter out sharp peaks, add effects and keep the sound from turning into mush if everything plays at once. Especially if a battle suddenly starts in your game with 15 sounds of different units, fanfares and tense music.

A good engine usually implements its own audio mixer. It can be big and complex, with mixdown and reverb, effects and post-polling, or simple with four channels and background music, but the essence is the same — the mixer works with abstract sound streams, lets you combine them however you like, and on output produces one final stream that's already fed to the audio output. You should probably try writing your own at least once, to understand how it works. Or you can look for a library that already does all of this — mixes, balances, compresses. There are probably hundreds of them… but honestly, I didn't really look. In the end I wrote my own on top of SDL2. At least now I know how every clicking piece of crap in the headphones works.

Physics

Good solutions

Honestly… most games don't need physics at all. Real physics, that is — with forces, impulses, friction and all that academic baggage. Most need something simpler: animations, logic, scripts — the things that create the impression of physics while having almost nothing to do with it.

Take Civ VI, for instance — one of the most complex strategy games of our time. But physics isn't needed there. At all. Or various city-builders — banal, but houses don't fall, roads don't crack, and nobody computes the falling vector of a water tower. Simply because it's not needed. Why complicate things? Yet both Civ and Cities: Skylines for some reason drag in a dependency on PhysX, and possibly don't just drag it in but actually use it somehow.

A platformer or a top-down 2D RPG — all the "physics" is usually limited to moving the character and simple collisions with walls, enemies and crates. This is written in a couple of evenings, unless, of course, you get stuck on every line, as I did trying to figure it out. The main thing is that it'll all be under your control: want the hero to slide like on ice — sure. Want the enemies to bounce absurdly like rubber ducks — go ahead.

And if you have too many collisions and everything starts lagging — you can optimize. Spatial hashing, quadtrees, and a range limit from the main character. But… if you're still stubborn and know for sure that you'll need real physics — take a ready-made library: Box2D for 2D or Bullet for 3D. Don't reinvent the wheel; there will be plenty of those in a homemade engine anyway.

And when you need to understand how such engines work under the hood — it's better to watch a video on YouTube from the author of Box2D. He explains in great detail how physics with impulse-based constraints works and generally gives a rare look from the inside.

Once I thought physics was about mass, velocity and formulas. Now I know that real physics is when at 3 a.m. you're trying to figure out why the character gets stuck in a staircase whose collision mesh didn't load.

Scripts and Configs

Configs
Scripts

Sounds trendy and convenient, it seems. In reality — one more language, one more layer of debugging, one more set of debug tools and one more reason why at 3 a.m. you have to dig through the log with prints.

What is it, anyway? Essentially, it's a part of the game logic written not in the engine's language (e.g. C++), but in another, lighter, often interpreted one. Lua, Python, GDScript, Angelscript, JavaScript — pick any flavor.

... it's not about performance or FPS
It's about the number of features a designer manages to crank out in an evening. In a low-level language like C++ every line is like a climb up a mountain. Yes, powerful, precise, but hard, and you have to recompile the game.

... it's about convenience and development speed
In scripts it's like riding a bicycle: not scary, fast, fun and convenient. In Lua, for instance, you can knock out a new enemy behavior pattern in about fifteen minutes and immediately see the result. No compilation, no restart. That's priceless, especially when a game designer is rushing you, or you yourself — right before a demo to the bosses.

... surprisingly, it's about safety
Scripts usually run in an isolated environment. That means if a modder wrote something weird — they'll break only their own mission, not the whole game. You can restrict scripts' access: give them the ability to work only with the objects you need. Set limits, interpret exceptions, and in the worst case — just don't run anything suspicious. You won't get this kind of flexibility in C++. There, any crooked plugin is a potential crash.

... it's about the reconfigurability of the project as a whole
If you've ever had to rebuild a project because of changing a single AI parameter, congratulations — you didn't think about configurability; hardcode is rarely a good thing. Hot-reload is the ability to change code during the game. Added a condition — hit "save" — and it already works, even if the game is running, even on the third mission. Compilers still aren't fully capable of this (well, almost incapable), but script interpreters — do it all in the best possible way, but see point 1.

... it's a door for the community
A door into the engine. No matter the size of the engine or the game — if you want the community to add new missions, enemies, dialogues, mini-games and even broken economic simulations — you need to give them a simple and safe way to do it. Lua, Python — these are familiar languages, written by schoolkids, students, enthusiasts. C++? Don't even think about it. Nobody will deal with a build server and compiler versions. And you probably won't want to read bug reports afterwards from a person who "just replaced a variable in a header file".

... it's — you can't please everyone

... a plus is two minuses, sometimes more
Debugging — pain. You'll have to write the debugger yourself, or rather assemble it from rakes and tears.
Performance (see point 1) - scripts are slower. Sometimes much. Sometimes — so much that rewriting the logic back into C++ begins.
The bridge between languages. That's the very layer where bindings and wrappers are written. Where types start to get confused. Where strings become char*, std::string, const char*, LuaStringView and all of it at once.
Unpredictability. A script can be written by anyone. And write anything. Including an infinite loop or enemy.hp = -9000.

... more often — a salvation. sometimes — a headache.
Sometimes — a catalyst for new ideas and chaos. They don't suit everyone, not always, and not everywhere. But if you do everything right — you'll be able to edit the game's logic during the game, without recompilation, without restarts, without extra hours of waiting.

Networking

Not much here

Sometimes everyone dreams about this. Synchronous worlds, co-op, PvP, MMO, or at least a chat between players. And then you open Wireshark and start dreaming about only one thing — that all of this just works.

Let's start with the good news: maybe your game doesn't need it at all. If you're making an offline game, a puzzle, a platformer, some "SimTower with frogs" — you can forget about networking. But if it occurred to you to "make multiplayer" — get ready. You're now stepping onto a path of pain where everything depends on the details. No universal architecture exists.

In modern languages (C#, Rust, Go) there will already be decent abstractions for working with sockets. Async calls, convenient coroutines and all that. In C++ (my condolences) — you'll most likely have to use something third-party like RakNet/Enet, not super modern, but reliable libraries proven by time and a ton of projects. But let's be honest: sockets are only the beginning. Connecting to the server is 1% of the work. After that the real fun begins. What even is a "network engine"? Good question. I asked myself the same when I was writing my first co-op prototype. Never did find the answer:

Sounds hard? It is. And you also need:

A while ago, on the hype wave of the second Counter-Strike, I wrote a couple of multiplayer prototypes. With friends. For fun, on Irrlicht. They almost worked. Until the first frame, when one friend's instance started moving without real events, and the other — hung in a wall at that time. That's when I understood: making reliable, responsive and flexible multiplayer is a separate profession. If you want to do it alone — you'll need not only knowledge, but also something to fend off the men in white coats with.

Artificial intelligence (AI)

Basic solutions

My favorite topic, because besides optimizations, for the last seven years I've been doing exactly monster AI in various guises, swarm behavior and group BTs. You may not believe it, but not every game needs strong AI, if it needs AI at all. And those that do most often get by with a couple of lousy scripts that say "Oops!" on a timer and run into a wall.

AI is just code. If you can write code in the engine — congratulations, you already have AI support. All game "intelligence" is just a pile of ifs and switches pretending the NPC is "thinking" something. Everything else developers built on top of these two building blocks:

Behavior Trees (BT) — a hierarchy of tasks where the NPC "thinks" about what to do. Reads well, easy to debug, but often turns into endless logic duplication and hellish nesting.

Goal-Oriented Action Planning (GOAP) — the AI itself chooses how to reach a goal. Cool, smart. But writing a planner — pain.

Finite State Machines (FSM) — states and transitions between them. Simple, effective, but quickly grows into spaghetti FSM.

Utility AI — everything on numbers and weights. A kind of FSM subspecies where there are no pre-written transitions between states. Flexible, even too much, and therefore unpredictable.

HTN, BT+ML, Neural Nets, Reinforcement Learning — sounds like magic. Works mostly in papers.

Pathfinding — the A* algorithm or its variants, plus a bit of magic so they don't get stuck in a corner.

Navmeshes — you can build them by hand or use ready-made tools (Recast, NavPower, etc).

Decision system — in any game it all eventually comes down to: "see the player — attack", "don't see — look for the player", "low HP — run away".

Some time ago I wrote a library for Behavior Trees. Working, simple, with convenient debugging, an editor and all the rest. Maybe it's worth open-sourcing, but the support contract with the customer hasn't ended yet — it turned out to be a cool thing, like UE blueprints but with a sane debugger and replays.

But the more it was modernized and refined, the more often I started noticing how designers, instead of building beautiful trees and following "divide and encapsulate" patterns, just shove all sets of behaviors into one long DoEverythingAndRepeat sequence. And you know — it worked pretty well.

AI is first and foremost about how to entertain the player and not let them get bored, not about intelligence. Let the NPC be dumb — the main thing is that it behaves the way the player wants. Let it be predictable, but that makes it easier to memorize patterns, and tactics only benefit from it.

Let it not be a real "AI", but just a competent script — at least it works stably, entertains the player and acts dumb. AI is not about intelligence. It's about the illusion of intelligence. And if you manage to make the player believe that someone smart is playing against them — then you did everything right.

UI

A few links

You can make a game without physics, without AI, even without audio. But without a UI — alas, practically impossible; we live in a world where games without a UI are perceived as a low-effort cash grab. And somewhere you do need to write "New Game" and "Exit", right?

...it's hard

On paper everything looks simple: big deal, you need to draw a couple of buttons, texts, maybe a slider or a checkbox. Except these objects suddenly acquire:

And now you're no longer making a game, you're writing a UI framework. Which, by the way, may not even be used anywhere besides this particular game. Why can't you just take something ready-made? You can. But:

And even if you take something normal, okay Scaleform, you're not as bad as it seemed at first — you'll still have to wire it into your engine, connect it with the input system, events, shaders, texts and so on and so on. Of course, a custom UI library was written, on an SVG renderer, with declarative constructors, for different GAPIs, but it didn't go further than a prototype and a couple of editors; in the end in one case we went to Scaleform, and in another to ImGui.

Not imgui

UI is a complex, structured, multi-level, platform-dependent and very underrated part of any engine. But you'd better walk this path at least once, minimally, but well. Don't try to outdo Unity UI, Unreal Slate or HTML. Make a couple of controls: a button, text, maybe a checkbox — and have them not crash.

Architecture

Reflection
Basic solutions

By a certain point we already have everything separately: graphics, sound, assets, physics, UI, maybe even AI and scripts. But one important thing is still left aside: how do you connect all of this into something that works? There are several working approaches. This topic was covered in more detail in the previous article, so for now I'll just summarize.

No architecture is also architecture

Many indie developers, tired of the complexities of modern frameworks, turn to the simple library approach in development. Its essence lies in creating separate, independent subsystems, each responsible for specific functionality. You create a gfx subsystem for drawing graphics, audio for playing sounds, assets for loading data from disk and input for handling user input. Each subsystem functions in isolation, having no idea about the other parts of the program. You as the developer decide which functions to call and in what order to do it. The whole process is under your direct control. By the way, this approach was popular in the late nineties — early 2000s.

This method has a number of advantages: it's simple to implement, has transparency of operation and is great for small projects. But success heavily depends on the developer's personal discipline; there are no automatic scene managers or other helper structures. Over time it becomes hard to keep the whole scheme of calls and their cause-and-effect relationships in your head.

But at first everything works easily and beautifully, and for many that's enough. Later you may of course run into bugs like the UI not being notified about asset updates, or background music not starting after loading a new level. Solving these problems is the inevitable price for the absence of connections between the various parts of the program. And these connections are, in essence, the application's architecture, the very thing we were initially trying to get away from.

GodObject is also architecture

Big game engines like Unity, Unreal, Godot, CryEngine, Dagor and others often choose an approach based on a single base class. Usually it's a class named Object, Entity, GameObject, Actor or similar, which becomes the ancestor of all game entities.

Such a base class usually contains virtual methods for the main game functions.

class GameObject {
public:
    virtual void update(float dt);
    virtual void render();
    virtual void onEvent(const Event&);
};

All game elements you create — UI buttons, enemies, NPCs — inherit from this single class. Each of these elements automatically gets the ability to update in the game loop, draw on the screen and react to various events. Convenient? Very. Out of the box it provides a uniform interface for all game objects. The centralized structure makes it easy to manage game entities and lets you implement a global object manager.

The main problem is the excessive use of inheritance — all classes descend from one parent, which contradicts the "composition over inheritance" principle. Difficulties arise when you need to implement multiple behaviors for objects — the system becomes fragile and hard to extend.

After a couple of months your GameObject turns into a giant dumpster with 300 lines of virtual methods. Attempts to fix this through components only make it worse. In the end you start to think...

ECS — trendy! But...

An architectural model that has gained great popularity in recent years among indie developers and enthusiasts. The core idea is the radical separation of data and logic inside the game system — everything is built around three key concepts. The Entity is just an identifier containing no logic or data on its own. The Component is a container for a certain type of data, such as an object's position, health pool or sound parameters. The System is a functional block that processes components according to certain rules.

Separately they won't work, but together they let you eliminate the traditional object hierarchy, replacing it with batch processing of data. For example, the physics system can simultaneously access all Transform and Velocity components to update the coordinates of all moving objects in one operation.

Among the advantages we get high performance thanks to efficient use of the CPU cache, a clear separation of responsibilities between different parts of the system, and good scalability.

Among the downsides — we plunge fully into using a complex system that often requires writing a significant amount of boilerplate code. Without specialized editors ECS code is practically unreadable, and without visual debuggers it's also impossible to debug.

For small-scale projects this will be like firing a tank at flies. Attempts to mass-sell this to indie developers have been stalling for the sixth year now, both in Unity and in Unreal.

Or maybe just leave everything as it is?

Yes and no. Architecture isn't a goal, it's a tool. If you're making a hobby project over the weekend — there's no need to build the perfect framework, because you'll end up with a framework, and once you start writing a framework, game development fades into the background. But it's also important that architecture is like a game's skeleton. Nobody sees it, but if it's crooked, everything will ache, bend and break at every sneeze. And when it gets really inconvenient — don't be afraid to throw it all away and start over. Don't be afraid to throw away what's broken, everyone does it (the rule of three systems).

Besides, GEA is still relevant and nothing better has been invented yet; if you suddenly get stuck, reread the corresponding chapter in this book.

Tools

Speaking of tools, the helper instruments for working with the engine — as you might have already guessed — not every engine or game needs them. As tools, for example, simple Python scripts can be used.

Most engines provide full-fledged visual editors out of the box, which look like a hybrid of an IDE (development environment) and Blender, with the ability to place objects in the scene, edit materials and shaders, set up physics and interactions, preview animations and the interface, and test everything in real time.

But creating a full-fledged editor is a task comparable in complexity to developing the engine itself. For a small engine, building a Unity-like editor isn't needed and isn't worth it — it would be inefficient and eat up a ton of time. Nevertheless, you shouldn't neglect simple tools that take a couple of evenings to write but significantly make life easier:

It's important to remember: time spent on a tool should save you more time in the future. Don't create a visual editor for the sake of the editor itself. But if you have to manually edit 200 lines of JSON every time and it's inconvenient — a simple tool or script will save a ton of time.

Conclusion

Here we discussed all sorts of interesting features of game (game engine) development — containers, strings, allocators, architecture and so on. And I hope that after all of this the thought "should I make my own engine" doesn't seem too suicidal. Yes, but let's not forget the obvious: 90% of games don't need any of this.

To start understanding how games are built you don't need an Unreal-level engine. So as not to become yet another Unreal "kinda" C++ programmer who can't tell an array from a vector — you have to make your first (or fifth) game without an engine, make a primitive version that opens a window, draws a couple of sprites and packs the project into a zip. Let it be a game-engine. Simple, crappy, but your own. And, more importantly, made with an understanding of the tools used. Without Unity, without Godot, without other convenient crutches. Let it be crooked and slow — that doesn't matter.

But it will let you tinker with architecture, design patterns and figure out resource optimization. This path will take time, but in the end you'll get not just a game, but an understanding of the "inner kitchen". The importance of such an approach is hard to overestimate for your future career. This is where professional growth is laid down and begins. Real growth, without the wrappers. A person who came into development knowing Unreal will only be able to work in it; a person who knows how it's built from the inside can do Unreal, and Unity, and Godot.

Of course, this isn't the only option: if Unreal Engine, Godot or other ready-made solutions seem more convenient to you — why not? It all depends on ambition, patience and willingness to dig deeper. But even if you eventually settle on someone else's engine, the experience of building your own will become a powerful boost for professional growth, teaching you to see, behind the technical essence, the magic of games.

The choice — as always — is yours.

One more thing

And I'm also still looking for the very best studio. The big M is too big, and too clumsy when it comes to decisions. If anyone's interested, there's a LinkedIn link in the profile.

P.S.

This is the last article of the cycle with theory and descriptions of interesting cases. But as a bonus there will be one more with all sorts of examples of perf drops in real cases from game projects (with names given where there was permission), that one will be without theory — just cases of freezes and how they were fixed. Stay tuned!

← All articles