Teardown

The Frozen Project

Unity · C# · Claude/Cursor

A roguelite dungeon crawler: procedural rooms, permadeath, a skill tree that carries between runs. The kind of game a small team builds over eight months with Cursor and Claude, because the alternative is hiring three more people they cannot afford. Two developers, one part-time artist, and an AI writing most of the code. The game builds, it runs, and it has not been updated in three months.

The deliberate setup

There was no deliberate setup.

The team used AI from the first prompt. No assembly definitions, no data/visual split, no service layer. Every feature request went into a new MonoBehaviour, and every MonoBehaviour had an Update method because that is what MonoBehaviours do. The architecture was whatever the AI reached for in that session, and the AI reached for something different every session.

The developer who wrote the first prompt was not a senior engineer. They were a game designer who learned to prompt AI well enough to make things that compiled. The code worked, the game ran, and the team shipped builds to testers every fortnight. For six months that was enough.

How individual AI patterns accumulate until the project freezes Six layers of weight stacked on a figure at the bottom. Each layer is labelled with a pattern: Scene/Script Disconnect, Input System Chaos, MonoBehaviour God Classes, Asset Store Frankenstein, Update Loop Sprawl, Fake State Machines. The figure at the base is labelled 'The developer'. The stack grows until movement is impossible. The developer Fake State Machines Update Loop Sprawl Asset Store Frankenstein MonoBehaviour God Classes Input System Chaos Scene / Script Disconnect Each feature adds another layer
No single pattern broke the project. The weight of all of them together did.

The handover

There was no single breaking point. The team just stopped being able to make changes without breaking things they had not meant to touch.

The Unity update

A routine LTS update broke three Asset Store plugins. The combat plugin, the inventory plugin, and the quest system all depended on internal APIs that had changed between versions. The team rolled back the update and never tried again. The project has been on Unity 2022.3.17 ever since, and every new package they want to add has to be checked against that version first.

The AI that recommended those plugins could not have known they would break. It recommended each one in isolation, for the feature being built that session. Nobody was keeping a dependency map, because nobody knew they needed one.

One feature, four bug fixes

The team wanted to add a new enemy type: a shielded variant that takes damage only from behind. The AI generated the enemy class, the shield logic, and the hit-detection override in about twenty minutes. It compiled on the first try.

Then the shield logic conflicted with the damage-number system, which was handled by a different plugin. Then the hit-detection override broke the combo counter, which lived in the combat plugin’s event chain. Then the new enemy’s spawn timing clashed with the wave manager, which was a hand-written MonoBehaviour that had grown to seven hundred lines. Then the AI’s fix for the wave manager introduced a null reference in the room-transition system.

Four bugs, three systems, one new enemy type. Each fix the AI generated broke something else, because the AI could see the file it was editing but not the three other files that depended on it.

Save data with no version field

The save system was JSON serialisation written by the AI in the first month. It wrote the entire game state as a flat object (player stats, inventory, skill tree progress, room history), all in one file with no version field and no migration path.

When the team shipped a content update that added a new skill-tree node, every existing save file failed to deserialise. The JSON parser threw on the unknown field and returned null. Players who had been testing for two months lost their progress.

The AI’s fix was to make the new field optional. That worked for the next update. The one after that would need another optional field, and the one after that would need a migration path that the flat structure could not support. The team knew this and did not know how to fix it, because fixing it meant rewriting the save system, and rewriting anything meant touching code nobody understood.

The settings menu, and combat

A producer asked for a settings menu: volume sliders, a brightness toggle, control remapping. The UI team built the mockup. The AI generated the menu code, wired it to the existing audio and input systems, and the menu appeared.

It also broke combat. The settings menu’s input handling subscribed to the same events as the combat system, and the AI’s event-unsubscribe logic in the menu’s OnDestroy never fired because the menu was a DontDestroyOnLoad object. Every time the player opened settings and closed them, the combat input handler stopped working until the next scene load.

The fix was three lines in the right place. Finding the right place took a week, because the input handling was spread across four scripts, two plugins, and a ScriptableObject that the AI had generated as a singleton and that nobody knew existed.

Onboarding the new developer

The team hired a third developer. They were senior enough to read the codebase and understand what they were looking at. After a week they gave notice.

The code had no README, no architecture documentation, no comments that explained why anything was the way it was. The AI had written comments, hundreds of them, and every one described what the code did, line by line. // increment counter by 1 on counter++. The new developer could see what every function did and could not understand why any of it existed. They spent the week tracing event chains through plugins they had never seen, trying to work out which MonoBehaviour was supposed to own which piece of state, and finding that the answer was usually “all of them.”

The team was back to two people, and the codebase was no easier to understand than it had been before.

The milestone

A publisher milestone was six weeks out. The deliverable was a new biome, three enemy types, and a boss fight. The team had built all of it in a feature branch. The branch had been open for five weeks.

Every time they tried to merge it, something broke. The new biome’s room-generation logic conflicted with the wave manager. The boss fight’s state machine, written by the AI as a switch statement on a string enum, added a new case that the existing transition logic did not handle. The three enemy types each introduced their own edge cases in the combat plugin’s damage pipeline.

The milestone shipped three weeks late, without the boss fight, and the publisher’s QA found seventeen bugs in the first week. The team spent the next month fixing things the merge had broken, and by the time they were done, the next milestone was already overdue.

What the project still lacks

No test coverage on any critical path. No save-system versioning or migration. No architecture: all game logic lives in MonoBehaviours, with no assembly definitions and no separation between data, logic, and presentation. Three Asset Store plugins at stale versions, one of them abandoned by its developer. No CI pipeline, no automated builds, no test runs. No Profiler data has ever been captured on a target device. No architecture documentation, no onboarding guide, no README that explains how the project is structured. The only documentation is the AI’s line-by-line comments, which tell you everything except what you need to know.

The lesson

Every individual AI-generated feature was reasonable in isolation. The inventory plugin works. The combat plugin works. The save system saves and loads. The problem is not that any one thing is broken. The problem is that nobody designed how they fit together, and the AI cannot design that because it cannot see the runtime interactions between systems it did not write in the same session.

The frozen project is not caused by bad code. It is caused by accumulated code that nobody understands well enough to change safely. The fear is not irrational. It is the correct response to a codebase with no tests, no architecture, and no single person who holds the whole thing in their head. The developer who stops touching it is not giving up. They are making the only rational decision available to them.

What we would do differently

If this codebase were audited for a client, the report would flag these findings, in order:

CriticalNo test coverage on any critical path. Every change is unverified
CriticalNo save-system versioning or migration. Player data corrupts on every content update
HighNo architecture: all game logic in MonoBehaviours, no assembly definitions, no data/visual split
HighStale dependencies, one abandoned plugin blocking Unity LTS updates
HighNo CI pipeline. No automated builds or test runs
MediumNo Profiler data captured. Performance unknown on target devices
MediumNo architecture documentation or onboarding guide. New developers cannot get oriented
MediumMixed input systems: at least two of Unity's three input systems are active simultaneously

The remediation roadmap phases into three stages. Stabilise: add tests on the critical paths (save/load, combat, room transitions), fix the save system with versioned data and a migration path, and replace or fork the abandoned plugin to unblock the Unity update. Structure: introduce assembly definitions to enforce layer boundaries, extract game logic from MonoBehaviours into pure C# classes, and separate data from presentation. Sustain: set up a CI pipeline with automated builds and test runs, run a profiling pass on target devices, and write architecture documentation that explains why things are the way they are: the kind of documentation the AI never wrote.

Recognise these failure patterns?

An audit will tell you exactly which ones are in your codebase, and what to do about them.

Book a discovery call

[email protected] · replies within 1 business day