(If you understand how BepInEx works and what it does, you can skip this and go to the next section.)
Modding Unity games is pretty standardized in 2025. In general, you have to create your mod code in C#, build it into a DLL file, then convince the game to execute your code.
There are two ways of doing this. The first is if the game has a mod loader built in, you just have to create and install mods based on their instructions. Games that work like this include Rimworld and Oxygen Not Included, and games that work like this generally have Steam Workshop support or some other standard way of distributing mods.
The other way is that if the game doesn't natively support mods, you can use BepInEx. BepInEx is a modloader, similar to Fabric for Minecraft. BepInEx works by placing a winhttp.dll file in your game folder, which does all the same things as the winhttp.dll file in your System32 folder, but it also tells it to load the code in the BepInEx DLL. From there, BepInEx can detect and execute modded code for you, and provide utilities like loggers, dependency management, and configuration files.
With BepInEx (or your game's mod loader), you can now load some code and run it when the game starts. The problem is getting your custom code to run at the specific time you want it to run. This is the main tool in your toolbox to make anything beyond the most simple of mods. For example, in my Coroner mod for the game Lethal Company, I have to run some code whenever the player gets killed by an enemy.
This is where Harmony comes in. Harmony is a tool which lets you patch any method, modifying the functionality of existing code while maintaining compatiblity with other patches. Some games like Oxygen Not Included have Harmony built-in, other games have Harmony available on their mod workshops (it's the most downloaded mod for Rimworld for a reason!). And of course, BepInEx includes Harmony automatically*.
Let's walk through some code that I used to handle recording when a player gets killed by a Bracken.
First, in my BepInEx plugin's Plugin.Awake() method, I call the following code:
// Apply Harmony patches
Harmony harmony = new Harmony(PLUGIN_GUID);
harmony.PatchAll();This method looks for any class in your mod that has a [HarmonyPatch] annotation and registers it as a Harmony patch. You can also call harmony.Patch() manually, but I find the annotation technique to be way more readable.
Now, if we create a new class and add the required annotation onto it, we can patch an existing function to add new functionality. Note that patch classes don't have to be in the same file as the BepInEx plugin! They can be anywhere you want, and the PatchAll() function will automatically detect them! I recommend splitting up patches into different files to keep things organized.
Let's take a look at this patch, and dissect it a bit.
[HarmonyPatch(typeof(FlowermanAI), nameof(FlowermanAI.killAnimation))]
class FlowermanAIKillAnimationPatch
{
public static void Postfix(FlowermanAI __instance)
{
try
{
LogDebug("Accessing state after Bracken snapping neck...");
if (__instance.inSpecialAnimationWithPlayer == null)
{
LogWarning("Could not access player after snapping neck!");
return;
}
LogDebug("Player killed by Bracken! Setting special cause of death...");
AdvancedDeathTracker.SetCauseOfDeath(__instance.inSpecialAnimationWithPlayer, AdvancedCauseOfDeath.Enemy_Bracken);
}
catch (System.Exception e)
{
LogError("Error in FlowermanAIKillAnimationPatch.Postfix: " + e);
LogError(e.StackTrace);
}
}
}Here, we define a class FlowermanAIKillAnimationPatch. The name doesn't matter here.
We have the [HarmonyPatch] annotation at the top, and it takes some arguments. The first is the type of the existing class we want to modify, and the second is the name of the method we want to modify. If the method you want to patch is public, you can use the nameof() method and the compiler will make sure the variable exists, but if the method is private you will have to use a string (don't worry, you can still apply your patch to these private methods just fine). There are other arguments for when you need to be more specific; you can read more about the Harmony annotations here.
In this example, the annotation informs the game we want to modify the FlowermanAI.killAnimation() method. Earlier, I used the ILSpy tool to determine that this is the method in Lethal Company's code which kills the player when they are attacked by a Bracken.
Within the class, we have to include one or more methods. The names of these methods, the arguments, and even the names of the arguments are important, so pay attention.
We have a static method with no return value (it has to be static) called Postfix. The game will call this method any time the targeted method is called, after the targeted method is finished running.
The arguments of the Postfix method should be the arguments of the original method; this lets you access those values in your patch (note the names and types must be EXACTLY the same for this to work!). You can also include many different types of injected values, whose functionality is determined by the name of the variable (usually starting with two underscores):
- An argument named
__instancewill include the object the method was called on, if it was a static method. In the above example code, this is the Bracken that killed the player. - If the method had a return value, you can add an argument named
__resultto access that value. If you define the argument by reference (for exampleref string __result), you can modify it and change what value the method returns! Very useful. - If the instance you're working with has private fields, you can add an argument with three underscores. So you can fetch
FlowermanAI.secretValuewith an argument named___secretValue. - There are many other injected values you can read more about here.
Once we have access to the Bracken, and know it just killed the player, we can now call our custom code using that info. We can access any information about the Bracken we want, modify it, or call other methods elsewhere in our code or the game's code.
One important note, I wrap the whole thing in a try/catch block to catch any exceptions and log them. Uncaught exceptions in a Harmony patch are dangerous, and they will pretty much always cause a crash, so either be very careful with writing safe code, or add exception handling code around everything.
You can read more about Postfix patches here.
Sometimes, you want to create a Prefix() method rather than a Postfix() method. You do this if:
- You want your code to run BEFORE the target method, rather than after.
- You want to modify the arguments of the target method before it runs.
- You want to skip the method entirely, or completely replace its output.
- You want to remember some values that can be recalled in the
Postfix()method later.
A Prefix() method can either return void, or a bool; if you set the return value to a boolean, you can return true to skip the original method and use whatever return value you specify.
You can also define a __state value
I wrote this simplified example code to demonstrate:
class TargetClass {
public string ExampleMethod(string firstArg) {
// Do some stuff.
return "Hello, world!"
}
}
[HarmonyPatch(typeof(TargetClass), nameof(ExampleMethod))]
class MyPatch {
public static bool Prefix(TargetClass __instance, ref string firstArg, ref string __result, ref string __state) {
// We can do a lot of stuff in a Prefix method.
if (shouldSkip) {
// We can set __result to change the result of the method entirely!
// Note that if you do this in the Prefix, your custom return value will get ignored,
// unless you skip the method entirely by passing
__result = "Foobar";
return true;
}
// Return false if you don't want to skip the method itself.
return false;
}
public static void Postfix(TargetClass __instance, string firstArg)
}If you need to:
- Call some code in the MIDDLE of a method.
- Alter the functionality of a method in detail, without just copying the whole thing and skipping the original.
- Do something that you CAN'T do with a Prefix method or Postfix method. Please use those if possible.
Transpiler patches are powerful. Transpiler patches are error-prone. Transpiler patches are flexible. Transpiler patches can do things you never thought possible. Transpiler patches can produce errors nobody has ever seen in their life. Transpiler patches are torture. Transpiler patches are rewarding. Transpiler patches are complicated. Transpiler patches are complicated. Transpiler patches are complicated. Transpiler patches are complicated. Transpiler patches are complicated. Transpiler patches are complicated. Transpiler patches are complicated. I'm not insane you're insane.
Transpiler patches require a separate guide, contact me if you want me to write one.
*BepInEx actually uses HarmonyX, a fork of Harmony that includes a bunch of fixes to improve compatibility between mods and stuff.