u/Dry-Huckleberry8284

Human-Friendly Game Architecture

Human-Friendly Game Architecture

Before we begin, let me tell you who this article is for:

  • You've written some game code
  • You've felt your code was a mess but didn't know how to fix it

It's not for:

  • People with no programming experience
  • People who haven't written much code
  • People who only let AI write code and suffer debugging it

I know many people don't write code themselves anymore. But I'm still writing this because:

  • Even if you just feed it to your AI, maybe it'll generate less chaotic code.
  • I genuinely struggled with this for a long time.

If you're "All in Claude" — this article's target audience is your Claude. Stuff it into its system prompt.

Let's get started.

Data Management

Games have a lot of data. I'm talking about static data. Where to put it is a real question. Here's the answer.

> Newbie tip:
> Static data = data that never changes. e.g. "Pikachu's base HP is 35", "Fireball deals 25 damage". The player's current HP changes, so that's not static data.

If you have very little data: hardcode it.

Yes, just hardcode it. Don't go building a data-driven pipeline for three characters' stats just because you read two blog posts about architecture.

Keep it simple — the smaller your tech stack, the easier it is to maintain. A constant object, imported and used directly.

If you have lots of data: write it in Excel, then export to a common format.

I know there are libraries to read Excel directly, but it's always a bad idea. I genuinely can't find a single benefit to reading .xlsx files directly. They're larger, their structure is less predictable, and they're not git-friendly.

Just have your code agent write an export script that outputs YAML or JSON, with type checking and foreign key validation.

If that sounds like too much work, don't worry — someone's already done it:

An Excel export tool

Sure, adopting a new tool means learning overhead, but you can always ask your code agent.

My recommended export format is YAML, because YAML can be handwritten. When you have data that just can't be expressed in a spreadsheet, write it by hand without any special workaround.

Don't chase perfect data-driven design.

This is the #1 dumb trap for indie developers, bar none.

Someone always has an epiphany — "data-driven is the way!" — and starts cramming all game logic into JSON files:

  • Behavior trees in data
  • Script callbacks in data
  • Conditional branches in data

Eventually, the data files become... a language. An awkward, untyped, Turing-incomplete language that only you can read.

Congratulations, you've invented a new language — and it's worse than every existing one.

The truth is: you need a real programming language. One with a type system, a debugger, an LSP, and a million similar questions answered on Stack Overflow. Why would you trade all that for JSON?

Besides, you're already using a programming language to make your game.

Large, structured, spreadsheet-friendly data — enemy stats, item info, skill data — naturally fits in tables. Spreadsheets genuinely work, and using Excel is great for your mental health.

For complex, branching, heterogeneous data — skill effects are the classic example. Fireball is "damage + burn", Blink is "teleport + invincibility frames", Thorns Shield is "reflect damage while active". Their logic structures are completely different. Forcing them into the same table abstraction is a disaster. In these cases, write code. Use a string tag in the data to identify the effect type and dispatch with a simple switch.

DI

I use DI, but only as a service locator, and almost exclusively with singletons.

> Newbie tip:
> DI (Dependency Injection) sounds scary, but it just means I don't new things myself — they're passed in from outside.
> You don't need a DI library; simply passing dependencies as function parameters is already DI.
> But many beginners, trying to avoid singletons, pass AudioManager from the main menu through five layers of functions to the battle scene — which is worse than a singleton.

I know "singleton" sounds unhealthy. But games genuinely have a bunch of singletons: resource manager, audio manager, save manager. They're naturally globally unique — the entire game lifecycle needs exactly one instance. Manage them with a DI container.

class CombatManager {
    val mobTemplates: DataTable<MobTemplate> by inject()
}

Inject anywhere, anytime. No long constructor parameter lists. No threading AudioManager through five intermediate layers.

Sounds good, but you might think — why not just:

object Global {
    var mobTemplates: DataTable<MobTemplate>
}

Fair point. Here's my reasoning:

  • If your language supports nullable types, you lose null safety.
  • You lose some advanced DI container features, like keyed injection or factories.

Of course, don't abuse it. I'm not putting an enemy's health controller in DI. That's going too far.

State Management

Model your game as a state machine.

I said aggressively model it as a state machine.

But before you go search for "game state machine framework" — you don't need an ultimate library with visual editing, transition guards, and hierarchical substates. You just need mutually exclusive states expressed through the type system:

type PlayerState =
    | { kind: "Idle"; mood: "Joy" | "Sad" }
    | { kind: "Moving"; direction: Vector2; speed: number }
    | { kind: "Attacking"; comboStep: number }

Tagged union, sum type — call it what you want. When you miss a branch in switch (state.kind), the compiler immediately warns you. This exhaustiveness check is the best weapon against state bugs, more reliable than any state machine library, with zero runtime overhead and zero learning cost.

isJumping && isSliding both true? That's a compile error with this approach. Mysterious freezes, animation glitches, unresponsive inputs — many of them just vanish.

UI too. Main menu, settings, in-game HUD, pause menu — all managed by a single UIState union type. No screen stack needed.

Functional Programming

You don't need to become a functional programming zealot. Just write pure functions when you can. If you have zero FP background:

Pure function: same input always produces the same output; doesn't modify or depend on external state.

The most textbook example in games is damage calculation. Input attacker stats, defender stats, skill params — output damage value and a list of side effects. This function shouldn't read a global RNG (pass RNG as a parameter instead), and it shouldn't directly modify the target's HP. It just computes the result. Applying damage, playing effects, showing numbers — that's the caller's job.

Immutable data structures? Yes, absolutely good — but don't chase 100% immutability, or it becomes a headache.

Abstraction

Let me be real: "over-abstraction" is not as scary as you think.

"You only use it once, no need to abstract" — this phrase is overused. The purpose of abstraction isn't "extensibility" — it's semantics. It's about seeing movementController.update(delta) and instantly knowing what it does, instead of staring at thirty lines of spaghetti mixing input handling, cursor checks, and animation switching, then spending five minutes reverse-engineering the intent.

Code you think "I'll only ever use this once" today will likely be revisited in three months when requirements change. Future you will thank past you for spending an extra ten minutes giving that logic a proper name.

Of course, I'm not advocating abstracting character movement into Strategy Pattern + Factory + Adapter just to support a swimming feature that might never exist. That is crazy. Reasonable, intentional abstraction and abstraction-for-its-own-sake are two completely different things.

Localization

Use gettext. That's it, this section is done.

If you want an explanation:

The common problem with every other approach: what you see in source code is a cold key string like "menu.start_game" instead of actual readable text.

Want to know what the button actually says? Go dig through the string table.

Want to add a new text? Create a key in the table, reference it, handle key conflicts — the development experience is painfully fragmented.

Yeah, the real issue is: you have to manually maintain a giant dictionary.

gettext is the only truly human-friendly localization solution.

How gettext works: write the original text directly in source code: _("Start Game"). Tools extract it automatically into .po files. Translators see the original text and fill in translations. Code stays readable, no ID mapping, no key conflicts. You can even write comments for translators right in the code:

// TRANSLATORS: Main menu start button, keep it short
_("Start Game")

This comment is automatically extracted into the .po file by the gettext toolchain. Translators see it instantly.

Plural forms, variable interpolation — gettext supports them natively. No manual string concatenation, no embarrassing "1 apples". This toolchain has been battle-tested for thirty years, with mature implementations in nearly every language.

Debug

Build yourself a DebugMenu, or you're just torturing yourself.

You will always need to cheat at runtime: god mode, add gold, skip levels... If every parameter tweak requires changing code, recompiling, and running to a specific scene, you're wasting your life.

ImGui has bindings in almost every language, and the API is dead simple:

ImGui::SliderFloat("Move Speed", &moveSpeed, 1.0f, 20.0f);

No XML layouts, no event system, no worrying about UI-to-game-state synchronization — because everything is redrawn every frame. Hide it behind a hotkey or a compile flag in release builds. It won't pollute your production code.

Also, you'll want shortcuts for quick testing. Use command-line arguments for profiling: --no-steam, --quickStart, --debug. For example, --quickStart can skip the start menu. Parsing these is trivial — they're just a string array.

Task Runner

> Newbie tip:
> Build scripts automate repetitive daily tasks, like: Excel to YAML conversion, packaging the game into an .exe, uploading to Steam/Itch.

Write build scripts in your own programming language. Don't learn another DSL.

Game development is full of automation tasks: packaging, deployment, localization extraction... The traditional approach is to bring in Make, or trendy stuff like Just, then spend two days figuring out their plugin ecosystems. My advice: don't.

Write these scripts in whatever language your project already uses. I use Bun + TypeScript:

async function performBuild(isRelease = false) {
    const isWin = isWindows()
    const task = isRelease
        ? "desktopApp:createReleaseDistributable"
        : "desktopApp:createDistributable"
    const subDir = isRelease ? "main-release" : "main"
    const buildFolder = isWin ? "build-win" : "build-linux"

    const gradleGenPath = `desktopApp/${buildFolder}/compose/binaries/${subDir}/app/GoodIdleGame`
    const platformTag = isWin ? "windows-x64" : "linux-x64"
    const finalOutputPath = `${CONFIG.paths.outputBase}/${platformTag}/${isRelease ? "release" : "debug"}`

    log.step(`Running Gradle task: ${task}`)
    await $`./gradlew ${task}`.throws(true)

    log.info(`Preparing directory: ${finalOutputPath}`)
    await rm(finalOutputPath, { recursive: true, force: true })
    await mkdir(finalOutputPath, { recursive: true })

    log.info(`Moving artifacts...`)
    await cp(gradleGenPath, finalOutputPath, { recursive: true })

    const libs = isWin
        ? [
              `${CONFIG.paths.libs.win}/steam_api64.dll`,
              `${CONFIG.paths.libs.win}/steamworks4j64.dll`,
          ]
        : [
              `${CONFIG.paths.libs.linux}/libsteam_api.so`,
              `${CONFIG.paths.libs.linux}/libsteamworks4j.so`,
          ]

    for (const lib of libs) {
        const fileName = basename(lib)
        const dest = join(finalOutputPath, fileName)

        await cp(lib, dest).catch(() => log.error(`Missing lib: ${lib}`))
    }

    if (!isRelease) {
        await cp(
            `${CONFIG.paths.libs.base}/steam_appid.txt`,
            join(finalOutputPath, "steam_appid.txt"),
        )
    }

    return finalOutputPath
}

The main benefit is you're really using a programming language. You can easily write real logic. Imagine implementing the flow above in Just. Or imagine the pain of releasing without any task runner.

Or a simpler scenario: you want the built artifact named [game_name]_[version].zip — you can trivially pull the version from your game's codebase.


It's 2026. You might not write much code yourself anymore. But architectural decisions are still yours — because the quality of AI-generated code largely depends on how clear the context you give it. These principles work well for AI too.

Originally published on my blog: blog

u/Dry-Huckleberry8284 — 3 hours ago

The Web is the best platform for 2D games, and why, and why it’s likely the best for 3D too

I recently made a game, primarily using Svelte js + Pixi js.
First, let me explain the background I had while developing this game:

  • I have game development experience, mainly with Godot + C#
  • I released a game on Steam, but it was made with Compose Multiplatform (if you don’t know it, Compose is basically a UI library for Kotlin)
  • I had zero web experience and only knew a little TypeScript
  • My new game uses Svelte js + Pixi js

Why I chose this tech stack

Godot is great software, but—every game engine, note, every single one—lacks a truly good UI solution.

My first game was an Idle game that was basically all UI. I didn’t need complex physics simulations, particle systems, animation systems, or any of those “general game development” solutions.

When I made that game, I ended up picking Compose Multiplatform instead of web technologies, simply because I didn’t really like frontend; the ecosystem is chaotic, and HTML and CSS are genuinely painful to write.

Compose offers a top-tier UI development experience. It’s reactive (just like Svelte, you directly modify variables without needing any advanced special syntax—interestingly, Compose also heavily depends on the Kotlin compiler; specifically, Compose code literally lives inside the Kotlin compiler’s source code, so Compose isn’t a very pure library. However, the Kotlin compiler’s role isn’t quite the same as Svelte’s. I won’t dive into details here). It has a layout model that is better than CSS, an officially recommended best practice, and everything feels comfortable.

That is, until I needed to make a new game that required some things closer to a traditional game engine, like sprites. At that point, sticking with Compose became unwise. After some research, I ultimately chose Svelte + Pixi js.

(PS. Before this, I had already made a small demo using Solid js.)

Ultimately, it comes down to one thing: I want a genuinely good UI solution.

Svelte is that ultimate solution

To be honest—and I love telling the truth—Svelte is the most comfortable UI framework in the web world. I know that sounds a bit radical, but when I entered this field, I tried:

  • Solid js
  • Vue
  • React

All of them have their own shortcomings:

  • Solid js is nice, and JSX felt familiar because of my Compose background, but really, syntax like setStore/setXXX just feels incredibly annoying, super annoying. Once your objects start getting complex, you’re forced to bring in libraries like mutative, otherwise it’s genuinely painful.

  • Vue feels like it was designed for true “web developers.” SFC is awful (and this is different from Svelte—in Vue you can’t write local snippets and have to put them in a separate file). Template directives like v-for aren’t great either, kind of tricky to write. And it offers two APIs simultaneously. This might seem picky, but I really dislike it; it just leads to everyone’s code looking different.

  • React is basically a worse Solid js. (I know, I should actually say Solid js is a better React, sorry!)

Svelte is great; it solves all these problems:

  • You can have snippets inside SFCs
  • $state lets you directly mutate plain JavaScript objects while keeping reactivity
  • It’s wonderful

I believe Svelte will become the new de facto standard.

Pixi js is exactly the game engine I was looking for

Yes, Pixi js is just a rendering library, but isn’t that even better! It removes unnecessary abstractions for you, yet keeps useful ones like the Scene Graph! And it’s very fast, more than enough for making games! 99% of 2D games won’t hit a rendering bottleneck. If you do, don’t blame Pixi js—it just means your code is too messy.

All game engines share this problem: too many poorly designed abstractions, because they’re general-purpose game engines after all.

I’m totally fine with pulling in other libraries as I need them.

For instance, in my new game, I use Tone js as my audio library because I want a great reactive audio experience, and Tone js is incredibly comfortable for that.

You might think, “Ahhh, without the high-level abstractions of a game engine, how will we write game logic?? You’re lying blah blah blah.” The answer is simple: bun add bitecs.

Pixi js’s own UI solution is also terrible—writing a UI library is hard—but we already have Svelte!

The only downside is that since I’m using Svelte, my UI must be drawn in the DOM, which means it can’t benefit from post-processing on the Canvas.

No big deal, I don’t need that feature right now. I’ve also seen some in-progress Canvas HTML libraries, so the future looks promising.

UI

Time to talk about the elephant in the room—UI.

To be honest, the developer experience for UI in the web world isn’t exactly superb—

  • HTML wasn’t designed for describing UI. If you don’t care about accessibility and search engines (which happens to be the case for my game-making scenario), you can ignore all semantic tags and just use div.
  • CSS is especially awful. It has a chaotic layout model, and vanilla CSS is basically unusable.

Yes, we have Tailwind CSS, which significantly alleviates the problem of CSS itself being terrible, but it doesn’t completely solve it—you still have to think about CSS’s messy layout model...

But either way, the UI experience still surpasses 100% of game engines, simply because it lets you describe UI with text and provides reactivity.

Oh, and also very importantly, we have a ton of component libraries. (daisyUI, absolutely amazing!)

The best distribution

I barely need to elaborate here. You just log into Vercel, push your repository, and you can share a link!

Local distribution is also easy: you can use Tauri and even write native code in Rust.

The best DX

No other field’s developer experience compares to the web world, I’m serious.

On my Compose journey, I had to painfully wrestle with Gradle. In the end, I even wrote my tasks.ts in TypeScript because Gradle is truly baffling, and it’s super slow, extremely slow, incredibly slow. The Kotlin compiler itself is also very slow.

Vite is fast, Bun is fast, hot reload is fast—change one line of code and you instantly see the result. That is pure fantasy in Compose.

TypeScript is top-tier

TypeScript is wonderfully designed. It has everything I need; I think I don’t need to say much more. Compared to Kotlin, though, it’s still a little behind. I think the only downsides are the lack of an enum class and a relatively small standard library.

Speaking of enum class, I feel most people underestimate its use cases. Specifically, Kotlin’s enum class is a set of constants with associated data, which is incredibly handy. Consider:

enum class Rarity(val label: string, val color: Color) {
    Epic("Epic Equipment", RED),
    Common("Common Equipment", BLACK),
}

In UI you can do:

Rarity.entries.forEach { Text(it.label, color = it.color) }

In TypeScript you have to:

const Rarity = [
    { label: "Epic Equipment", color: RED }
    { label: "Common Equipment", color: BLACK }
] as const

{#each Rarity as rarity}
    <p>{rarity.label}</p>
{/each}

And you lose the ability to do things like:

const epic = Rarity.Epic; // ???

Also, the standard library is small, so I ended up implementing many commonly used utility functions myself (like mapOrPut and similar).

The NPM ecosystem is top-tier

I have to say, you really rarely need to reinvent the wheel—NPM basically has every library you need!

What about 3D?

We have Babylon js. I haven’t used it deeply yet, but it is indeed a real game engine.

Summary

I think more game developers should try to break free from all-in-one game engines. You certainly don’t use every single feature of an engine, right? Plus, experimenting with new technology is inherently fun.

If you want to see an actual product: https://store.steampowered.com/app/4646350

u/Dry-Huckleberry8284 — 11 days ago

Hello! I'm developing an active incremental game about network expansion. It's in the early stages, but I'm struggling to make the loop feel "fun" or engaging. I’m committed to the project and would love to hear your thoughts, ideas, or critiques.

If you have a moment to playtest it, that would be amazing. Thanks!

GameLink

u/Dry-Huckleberry8284 — 24 days ago