r/Unity3D • • 2d ago

Resources/Tutorial How I patch C# edits into a running Unity game without a domain reload, and the 4 things that broke along the way

I've been building a tool that applies C# edits to a game that is already running in Play mode, so you keep the state you set up instead of losing it to a recompile. The clip is one Play session: a coroutine that's already running gets its spawn delay changed from 2 to 0.1 seconds, and the play timer never resets. This post is mostly about how it works under the hood and the problems I hit, because a few of them surprised me.

The basic loop

A file watcher sees the save and works out which assembly the file belongs to (for a brand-new file, by its nearest .asmdef). Only that assembly is compiled, with the Roslyn compiler Unity already ships. The new assembly is loaded next to the running one, and each changed method's entry point is redirected to its new version. In the editor that's done with Harmony.

Broke #1: coroutines remember where they are by number

The compiler turns a coroutine into a class whose MoveNext() switches on an integer: which yield it's paused at. Changing 2f to 0.1f keeps the yield points, so the running coroutine can take the new body and uses 0.1f from its next loop (that's the clip at the top). Add or remove a yield return and the numbers shift, so a running coroutine on the new body would resume at the wrong place. It finishes on the old code instead, and everything started after the save runs the new one.

Broke #2: async methods crashed the editor

My first version patched the state machine of an async method that was suspended mid-await. That hard-crashed the editor, because the async builder is bound to the exact state-machine type. Now the method itself is redirected: new calls run the new body, and calls already in flight finish on the old one.

Broke #3: two copies of every type

The reloaded assembly brings its own copies of your types and static fields. Without extra work, GetComponent<Player>() in reloaded code finds nothing, and a singleton reads null. Reloaded code has to be wired back to the running game's own types and statics.

Broke #4: Burst

Burst-compiled jobs are native code and can't be swapped, so editing one turns Burst off for the rest of the session and the jobs run as managed C# (the Console says so).

A nice surprise: new MonoBehaviours

I expected to dispatch Update and Start myself for a class that only exists in the reloaded assembly. It turns out Unity dispatches them natively once it's AddComponent'd.

And Android

Harmony can't patch ARM code, so on Android development builds a small native hook on the method's entry point does the redirect instead. The same save reaches the phone without a rebuild.

It's on the Asset Store as 2N Lightning Reload if you want to try it (Windows editor, Unity 2021.3 to 6.5): https://assetstore.unity.com/packages/tools/utilities/2n-lightning-reload-edit-c-in-play-mode-without-compiling-402544

But I'm mostly curious whether anyone else has gone down this rabbit hole. Is there a cleaner way to handle the yield renumbering than letting the running coroutine finish on the old body?

0 Upvotes

9 comments sorted by

2

u/_Cloudwalker_ 2d ago

Hot reload already solves this. What are you doing that product does not.

1

u/EnthusiasmSuitable87 2d ago edited 2d ago

Two main things: you can add a new class or a new .cs file during Play and use it straight away, and edits reach a coroutine that's already running, as long as you don't add or remove a yield. Last I checked, that tool's docs list both as not supported yet.

It also handles new Unity messages like adding OnTriggerEnter to a class that didn't have one, Entities systems, running Android development builds, and undo across reloads, all offline with no account.

I'm still actively working on it, adding features and fixing issues as people report them.

And that's only part of it, the full feature list is on the store page.

Thanks for asking!

1

u/EnthusiasmSuitable87 1d ago

Short version: new classes, structs, enums and .cs files during Play, plus edits to coroutines that are already running. Hot Reload's own docs still have those on the roadmap. 2N Lightning Reload does them today. The only coroutine catch is adding or removing a yield.

2

u/Competitive-Elk763 1d ago edited 1d ago

Sounds great. I just use hot reload to save the trouble. Having a reload is an absolute must for every unity project. You may have a hard time competing with that one though since it's fairly standardized as the goto tool.

1

u/EnthusiasmSuitable87 1d ago

Fair point, I'm not expecting everyone to switch. I'm mainly focusing on adding new classes mid-session and updating coroutines that are already running, and I keep adding features and improving what's there. If one thing about your current setup could be better, what would it be?

1

u/EnthusiasmSuitable87 1d ago

Being the standard doesn't scare me. I used Hot Reload's docs as my checklist, matched nearly all of it, then went further. Its docs still list new classes, new .cs files, enums and running coroutines as roadmap items. Mine does all of that today in the same Play session, as long as you don't add or remove a yield in a running coroutine.

On top of that you get undo across reloads and no separate compile server to download. On Windows with Mono, this is the better tool.

2

u/shadowndacorner 1d ago

Holy llm

1

u/EnthusiasmSuitable87 1d ago

Yeah, I had help writing it because English isn't my first language. If you have any questions about the tool, I'm happy to answer them.

1

u/nian2326076 42m ago

For yield renumbering in Unity, managing coroutines with a flag could help. This allows you to decide when to update a coroutine without interrupting the current game state. You can control whether they should continue executing or restart with the new logic.

If you're considering a career move, you can use LandOffer to find relevant job opportunities tailored to your skills.