Teardown

The Asset Store Frankenstein

Unity · C# · Claude/Cursor

A survival-crafting game for PC: you land on an island, gather wood and stone, craft tools, and build a shelter before the night arrives on its timer. Third person, Unity 2022 LTS, one developer working alone for six months with an AI assistant in Cursor. The game was assembled rather than built. The inventory, the combat, the dialogue, the quests and the interface all arrived as Asset Store plugins, and the code the developer wrote was mostly the code that connected them. By the fifth month it was a real game, with a day-night loop and a tech tree and a save that worked. It had also stopped being a project its own developer could safely change.

The deliberate setup

The approach was consistent from the first week. When the game needed a system, the developer asked the AI which plugin to buy, and the AI answered from training data in which those plugins are well represented. The inventory came from one publisher, the combat from a second, the dialogue from a third, the quests from a fourth, and the interface sat on a UI framework from a fifth. The assistant never wrote an inventory system. It recommended one, waited for the import to finish, and then wrote the code that let the combat system read from it.

None of those decisions was silly on its own. Each plugin had reviews, a publisher answering forum posts, and a store page listing the Unity versions it supported. Each solved the problem it was bought for. What nobody designed was the space between them: which plugin owns the player’s items, which one owns a quest transition, what happens when two of them want the same GameObject. The AI could not design that space either, because the seams between five closed-source packages are not in any file it can read. It stitched the plugins together faster than a human could have, which is exactly what made the project feel healthy for as long as it did.

Then a routine update to the inventory plugin broke the combat system.

The item list both plugins owned

The inventory plugin kept the player’s items in its own store, with its own save format. The combat plugin expected to find the same items in a component it owned, with its own fields. The glue between them was a small class the AI had written: when the inventory changed, it copied the current contents into the combat component, and the combat component became the source the combat code actually read.

The inventory plugin’s next release changed one behaviour at the edge of its API. Its lookup method had previously returned null when an item was absent; the new version returned an empty item with a quantity of zero, because the publisher had added item variants and needed a non-null result to attach them to. The glue checked for null, found an empty item instead, and copied it. The player could craft a stone axe and the combat system would still treat them as unarmed, because the axe had never reached the component the combat code read.

Nothing flagged this. The glue compiled against the new release, the store page said the update was compatible with the project’s Unity version, and the publisher’s own demo scene worked, because the demo used the new API directly and had no glue in it. The failure lived in the contract between two plugins, which is the one place neither publisher tests. The change from null to an empty item was announced in exactly one place: the release notes. The compiler will not mention it, the store page will not, and the demo scene cannot, because the demo has no glue.

The API call from the previous release

The fix went back to the AI, and this is where the project’s second incident came from. The assistant rewrote the glue against the combat plugin’s documentation as it remembered it: a method signature from the previous release. The plugin had kept that method for compatibility, but the current release had changed what the second argument meant, and the deprecated form silently ignored the flag the glue passed. The call compiled, the quest it started ran, and the on-screen objective never appeared, because the flag that suppressed the popup was the one being ignored.

The invisibility here is version drift. The AI is fluent in the plugin’s older documentation because that documentation is well represented in its training data, and the installed release differs from what it remembers in ways that do not break the build. The developer saw a compile that passed and an assistant that explained the code confidently, and had no reason to open the installed assembly and check the signature against the call. That check is the whole fix: the version that matters is the one in the Package Manager, not the one in the assistant’s memory.

The dialogue and quest event loop

The dialogue plugin fired events when a line finished. The quest plugin listened, because dialogue choices advance quests. The quest plugin fired events when an objective completed. The dialogue plugin listened, because completed quests unlock new dialogue. The AI had wired each one to the other, and most of the time that worked.

The edge case was a quest that could complete on a dialogue choice. The choice finished the line, which fired the dialogue event, which completed the objective, which fired the quest event, which opened the next line, which finished, which fired the dialogue event again, and so on until the call stack ran out. In the editor it surfaced as a stack overflow in the console after a freeze. On the first few playthroughs nobody hit the combination, and when it did happen it looked like a random hang rather than a wiring diagram problem.

The AI’s fix was a re-entrancy flag: a bool that made the handlers return early when they were already inside the loop. That stopped the freeze and preserved the bug. The double transition still happened, the quest state was now inconsistent in a way the game could not show, and the guard became a permanent piece of ritual in code that had no reason to contain it. The structural fix was to give one plugin ownership of quest transitions and have the other one listen. Two plugins that both think they drive a state change do not need a referee; they need one of them to stop driving.

The demo scenes in the build

Each plugin shipped with its own world attached. The inventory plugin’s package contained an example island with its demo prefabs. The dialogue plugin’s package contained sample characters and a sample conversation. One of them dragged in an entire post-processing stack that only its demo scenes used. The shipping build contained all of it, because nothing in the process ever asked whether the game itself referenced any of it.

The developer did ask, in the end. The answer came back as a build four times the size the game’s own content justified, and an install that took noticeably longer than it should have on a machine that was not slow. The AI could not help with the stripping: it could not tell which of the thousands of files the game actually referenced, and the plugins’ own importers expected their demo folders to be present. Nobody had documented what each package needed at runtime, because the packages arrived with demos attached and the demos looked like part of the product. Plugin bloat of this kind shows up in every project assembled from packages: content you did not ask for, kept because removing it is a research project of its own.

The Unity version standoff

The UI framework’s publisher released a version that required Unity 6. The combat plugin’s publisher had not shipped a Unity 6-compatible release, and its store page still listed the project’s version of Unity 2022 LTS. The framework’s new release also contained the fix for a layout bug the game hit daily, which is why the developer wanted it at all.

The project now had no upgrade path. Moving to Unity 6 would break combat. Staying on 2022 LTS meant the framework stayed on its old release with the layout bug, because the framework’s publisher had stopped shipping fixes for the old engine version. Upgrades are supposed to be a menu action with a loading bar. The constraint that made this one impossible was not in the project at all: it was two publishers’ release schedules, which the developer discovered one at a time, each discovery costing a day of trying. That is the point where a project stops being able to move.

The save add-on nobody maintained

The save system was a plugin too, and it was the one the project could least afford to lose. It serialised everything: the inventory, the quest states, the world, the player. Its publisher had stopped updating it eighteen months earlier. The store page still worked, the forum posts went unanswered, and the plugin had never been tested against Unity 6, because it predated it.

The developer could not replace it, because replacing a save system means writing a migration for every save the game has ever produced, and the game was in testing with real saves. The developer could not keep it and upgrade, because the upgrade the framework needed would strand the save system on an engine version with no path forward. The project was frozen by the oldest decision in it: a plugin bought in week one, on the strength of reviews that were already two years old, doing a job that nothing else in the project could do.

That was the state when I was asked to look at it. The game worked. Nobody dared change any of it, and the developer had stopped opening the project on days when a plugin update notification was waiting.

What the project still lacks

No single source of truth for game state: five plugins, five data models, and sync code between them that grew with every feature. No automated tests of the seams between plugins, because testing them means building scenes that exercise combinations the plugins never promised to support. No CI, so nothing ever rebuilt the project from scratch except the developer’s machine, and no build report was ever read. No architecture documentation, because the architecture was a list of packages in the Package Manager window and a folder of glue classes whose job was to hide the gaps. A build four times the size it needed. An abandoned plugin at the centre of the save path. And a Unity version chosen by the intersection of five vendors’ roadmaps rather than by the game.

The lesson

Every purchase in that project was defensible in isolation. The inventory plugin works, the combat plugin works, the dialogue plugin works. The failure was in the wiring between packages that were never designed to be wired together, and the developer had no way to see wiring: nothing in the editor shows it, the compiler never mentions it, and the AI answered every question it was asked, which was always about the next plugin, never about the four already in the project.

Buying the architecture from five vendors means buying five roadmaps. The project’s update schedule, its engine version and its ceiling all follow the lowest of them, usually a plugin bought early and given the least thought. The Asset Store is a fine place to buy a system. It is a strange place to store your architecture.

What we would do differently

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

CriticalAn abandoned save plugin owns all persistence. Eighteen months without updates, never tested against Unity 6, and no migration path out of it
CriticalNo single source of truth for game state. Five plugins, five data models, five event systems, and glue code copying between them
HighA circular event chain between the dialogue and quest plugins. It froze the game once and the re-entrancy guard that hides it is still there
HighPlugin version conflicts. The UI framework needs Unity 6; the combat plugin does not support it, so the project has no valid upgrade path
HighGlue code that duplicates logic across plugins solving the same problem slightly differently, so every plugin update can break two copies of a fix
HighA build four times the size the game's own content justifies, carrying demo scenes and sample assets from every plugin
MediumAn API call written against the previous release's documentation. It compiles, and the deprecated overload silently ignores one of its arguments
MediumNo tests on any plugin interaction. The only regression suite is the developer's memory of what used to work
MediumNo architecture documentation. The dependency graph exists only in the Package Manager window, with no record of which plugin does what or why

The remediation roadmap phases into three stages.

  • Stabilise: take persistence back from the abandoned plugin. A small save layer owned by the project, with a versioned format, is the one change that unblocks everything else. Break the dialogue and quest event loop by giving quests sole ownership of transitions, and remove the demo content from the build.
  • Structure: define one game-state model and put a service interface in front of every plugin, so the inventory and combat plugins become swappable implementations behind code the project owns. Add assembly definitions so the boundaries are enforced at compile time rather than by convention.
  • Sustain: an integration scene that exercises the seams between plugins, run whenever a plugin updates. CI with a build-size check and a written register of plugins: which version is installed, which Unity version it is pinned to, and why.

The save system comes first. Fixing the glue and stripping the demos while the abandoned save plugin still owns persistence buys the project a cleaner build with the same ceiling: it will still decide, on its own schedule, when the project can next move. The plugin that was bought first and thought about least is the one the whole recovery has to start from.

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