DEV Community

Cover image for How a Broken Button Forced Me to Rebuild My Entire Architecture
Mario Mignemi
Mario Mignemi

Posted on

How a Broken Button Forced Me to Rebuild My Entire Architecture

A few weeks ago, I hit a deadlock that took me an embarrassingly long time to fix: I couldn't have a shuttle without a button, and I couldn't have a button without a shuttle.

Here's why. My four-year-old button system required a live GameObject reference to be valid: no reference, and the button would self-destroy. Fine for a button that operates on a ship already sitting in the scene. Not fine for a boarding shuttle, which doesn't exist until a button spawns it. So the button needed the shuttle to exist to avoid self-destruction, and the shuttle needed a button to exist to spawn. Neither one could go first.

The bug wasn't in the shuttle. It was in a button that was four years old.

I'm building The Weight of One: The Jovarko Incident, a tactical space-combat game where you command a capital ship and its subsystems. The shuttle in question is a small craft you launch from your own hangar to either forcibly dock an enemy ship from the outside, or peacefully move into the hangar of a neutral or allied ship. Simple concept. It broke everything I had.

This post isn't really about the shuttle, though. It's about the module structure I ended up building because of it, and whether drawing hard boundaries — before writing a single line of code — was worth the friction it's currently causing me.


Every button in my prototype came with a spaceship stapled to it

My original UI code, written during early prototyping, made buttons stateful. A ButtonModel held a direct reference to the ship it operated on, and if that reference wasn't there, another script would destroy the button on the spot:


    // Pseudo-Code
    public class ButtonModel : MonoBehaviour
    {
        public Transform linkedShip;
    }

    public class SelfDestructOnDestroyedObject : MonoBehaviour
    {

        private ButtonModel model;

        ...

        private IEnumerator OnLostReferenceDestroySelf()
        {
            ...
                    Destroy(gameObject);
            ...


Enter fullscreen mode Exit fullscreen mode

That's fine when the ship already exists in the scene before the button does, which has been the case so far. However, for a shuttle, it's exactly backwards: the button is what's supposed to trigger the creation of the gameobject. The classic Mexican standoff.

I don't blame past-me for this. It was prototype code from 2021-2022, and it did its job for years, until it didn't in 2026. But I'm not the type to bolt on a special case just for shuttles and move on. If I have the feeling that the design is wrong, I want to know why and fix it.


Making the button stop caring whose ship it was

The fix felt obvious once I named the actual problem: the button shouldn't hold a reference at all. It should announce an intent and let something else figure out the target.

Stateless buttons break the deadlock cleanly. The catch: something still needs to track which button maps to which ship. There’s no getting around it. That something is basically a lookup — conceptually a Map<Button, Spaceship> — and I didn't want a bare dictionary sitting inside a MonoBehaviour pretending to be a service, and six months later nobody remembers what it's for.


Down the ScriptableObject rabbit hole

I had used ScriptableObjects for exactly one thing before this story: configuration data. Stat blocks, balance numbers, that kind of thing. Looking into how larger studios structure decoupled systems, I found a much bigger use of SOs: as runtime variables, event channels, and typed runtime sets that replace both generic EventBuses, which I've been using so far, and direct references.

It solved my mapping problem cleanly.

What it didn't solve was a bigger question: full rewrite, or hybrid architecture? I have no problem resetting and rebuilding systems, that's genuinely part of why I enjoy programming. However, a full architectural rewrite of a project that's somewhere between a prototype and an actual demo is a different bet than restructuring one feature. So I picked a feature that didn't exist at all — a flexible mission system — and decided to build it entirely with a ScriptableObject architecture in a separate project, as a real test.


Drawing five boxes before writing any mission code

Before touching the mission system, I made one decision on purpose: assembly definitions, not just folder conventions, would decide where files were allowed to live. In fact, almost everything I came across about SO architecture — articles, videos, you name it — flagged a common problem: every variable, event channel, and runtime set is its own small asset, and that adds up fast.

A feature that used to be two files in the old EventBus system could easily turn into five or six files. Left unorganized, that adds up into a mess fast, so I wanted the boundaries locked in from day one.

Five main modules, five assemblies:

  • Core: Generic, game-agnostic primitives — base SO wrappers, generic interfaces, extensions
  • UI: Draws to Canvas/Screen, captures interface input, raises intent events
  • Data: SO Variables, Event Channels, Runtime Sets, and config schemas — no Update() loop
  • Gameplay: Moment-to-moment entity behavior like movement, combat, mission triggers.
  • Systems: Global, cross-scene infrastructure, like save/load, scene transitions, campaign progress.

Dependency graph:
Dependency Graph


The one rule that makes the other five make sense

None of this is exotic on its own. Assembly definitions weren't new to me, but I’ve never actually reached for them. Game development isn't my day job, so they just weren't on my radar until now.

This is the part that maps directly back to the shuttle deadlock. The old ButtonModel was a compile-time UI-to-Gameplay dependency wearing a MonoBehaviour costume. The new rule makes that specific mistake impossible to write by accident. The assembly definition simply won't allow it.


Where the shuttle actually landed

Here's the part I want other people to take away from this. Understanding ScriptableObject architecture hasn't stopped me from placing files where I think they belong, only to have the assembly definition reject them.

At first, this was annoying. Then I noticed a pattern: every one of those errors was telling me one of two things. Either the file is genuinely misplaced, or the code design underneath it is wrong: future tech debt I hadn't noticed yet, because nothing was preventing me from writing it that way.


Is it paying off?

Honestly, provisionally, yes! But I'm not going to pretend the trade-off is free: having a ScriptableObject architecture and five modules means more files, which adds real ceremony for a solo dev before any feature ships. There's no dodging that.

I picked the mission system specifically because it was new territory: a contained way to test the architecture before betting the rest of the game on it. I'm treating that as a deliberate validation step. I want to see it hold up across a few more features before I call it the standard.

On the plus side, what I didn't expect was how much the friction itself became useful information. Every misplaced file is a small, cheap lesson about my own design, delivered immediately instead of six months later as an unmaintainable spaghetti mess.


If you're staring down a similar dilemma, the one thing I want you to take from this “happy accident” is: don't draw module boundaries as folders, draw them as dependency "permissions". Enforce them with assembly definitions, and let the compiler be blunt with you.

This whole approach is a sharp departure from the generic EventBus pattern I used to lean on, which is a big enough topic to deserve its own post. For now: I'll keep building the mission system and report back on whether five assemblies still feel right once the feature list grows.

Developers unite! Let me know how you've drawn your own lines.


Useful Links:
The Weight of One - Official Channel
The Weight of One - Itch.io Page
My Personal Channel

You can find my Dev Vlogs in all channels!

Top comments (0)