C# Basics Crash Course
This chapter is for readers who have never learned C# (or have zero programming experience). If you're already comfortable with C#, feel free to skip ahead and start from Getting Started.
C# (pronounced "C Sharp") is an object-oriented programming language developed by Microsoft, used mainly on the .NET platform and in the Unity game engine. ADOFAI itself is a Unity game written in C#, so all Mod code is C# too.
Don't be intimidated by the words "programming language" — 80% of Mod development is modifying a template; only 20% requires writing your own code. The goal of this chapter is to help you read template code, know what to change, and understand why it's written that way.
We'll learn in the following order, with each section tied to a real ADOFAI Mod scenario:
- Basic program structure
- Variables and types
- Operators and expressions
- Control flow
- Methods (functions)
- Classes and objects
- Collections
- The
statickeyword - Generics
- Inheritance and interfaces
- Attributes
- Reflection
nulland nullable types- Exception handling with
try/catch - Delegates and events
- Naming conventions and coding style
1. Basic Program Structure
A C# program consists of three core concepts: namespaces, classes, and methods. You can think of them as: folder → file → file contents.
using System; // Import the system library so we can use Console, Math, etc.
namespace MyFirstMod // Namespace: groups code together, like a folder
{
public static class Main // Class: a container for code
{
public static void SayHello() // Method: an executable block of code
{
Console.WriteLine("Hello"); // Print a line of text to the console
}
}
}Line-by-line explanation:
using System;— tells the compiler "I want to use the System library". Without this line,Consolecan't be found. Think of it as installing a toolbox:usingtakes the tools out and keeps them within reach.namespace MyFirstMod— puts the code under the "folder"MyFirstMod. A namespace can contain multiple classes.class Main— a class is a container for code; all methods are written inside a class.void SayHello()— a method, a unit of work that can be executed.voidmeans "returns no value".Console.WriteLine("Hello")— calls a system method that prints a line of text to the console.
About "Projects"
In real development, code spans more than one file. A project contains multiple .cs files, managed by a .csproj file:
<!-- MyFirstMod.csproj — the project file -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net481</TargetFramework> <!-- Target framework: .NET Framework 4.8.1 -->
</PropertyGroup>
</Project>TargetFramework decides which libraries your Mod can use — ADOFAI runs on .NET Framework 4.8.1, so Mods must target the same version. The template project already configures this for you.
The Entry Method
When UMM starts a Mod, it calls a method at a specific path: Namespace.ClassName.MethodName.
// UMM finds and calls this method
namespace MyFirstMod
{
public static class Main
{
// Path: MyFirstMod.Main.Load — UMM finds the entry by this name
public static void Load() { /* ... */ }
}
}Remember this "Namespace.ClassName.MethodName" structure — every later chapter uses it.
A More Concise Syntax (C# 10+)
If you're using a C# 10 or newer compiler, the namespace can be written on a single line without braces:
using System;
namespace MyFirstMod; // File-scoped namespace: the whole file belongs to it
public static class Main
{
public static void SayHello()
{
Console.WriteLine("Hello");
}
}Both forms are completely equivalent; the second just saves one level of indentation. Note: with file-scoped namespaces, a file can only declare one namespace, and it must come after all using statements.
The templates in this series enable the latest C# syntax (
<LangVersion>latest</LangVersion>), so both forms work. You only need to be able to read both.
2. Variables and Types
A variable is like a named box that holds data. Each box has a type — which determines what it can hold and what you can do with it.
Basic Types
int count = 10; // Integer (no decimal point)
float speed = 1.5f; // Decimal (note the trailing f)
double precision = 3.14159; // Higher-precision decimal (no suffix needed)
bool enabled = true; // Boolean: only true or false
string name = "ADOFAI"; // String (text)
char letter = 'A'; // A single character (single quotes)float and double are both decimals; the difference is precision: float has about 7 significant digits, double about 15. For speeds, coordinates and the like in Mods, float is enough.
var — Let the Compiler Infer the Type
var count = 10; // the compiler knows this is int
var name = "ADOFAI"; // the compiler knows this is string
var speed = 1.5f; // the compiler knows this is floatvar doesn't mean "untyped" — the compiler automatically infers the type from the value on the right. More convenient to write, but the result is exactly the same.
When to use
var? For local variables (inside methods)varis recommended for brevity; for class fields, explicit types read better.
const — Values That Never Change
const string ModName = "MyFirstMod"; // Constant: cannot be changed once defined
const int MaxRetry = 3;
// ModName = "Other"; // Compile error! Constants cannot be reassignedA const value is fixed at compile time and cannot change at runtime. Good for Mod names, version numbers, and other values that never change.
Type Conversion
Sometimes you need to convert between types:
// Implicit conversion: smaller type automatically becomes larger (no data loss)
int score = 100;
double exact = score; // int → double, automatic
// Explicit conversion: larger to smaller (may lose data, must be manual)
double pi = 3.14;
int approx = (int)pi; // result is 3, decimal part truncated (not rounded!)
// Safe conversion with the Convert class (recommended)
string input = "42";
int number = Convert.ToInt32(input); // string → int
float value = Convert.ToSingle("1.5"); // string → floatHarmony patches often need to cast
object-typed parameters to concrete types (e.g.(float)args[0]) — that's an explicit conversion.
String Operations
Strings are everywhere in Mods (reading configs, building logs, formatting output):
string a = "A Dance of";
string b = "Fire and Ice";
// Concatenation
string full = a + " " + b; // "A Dance of Fire and Ice"
string full2 = $"{a} {b}"; // same, using string interpolation
// Common methods
full.Length; // string length
full.Contains("Fire"); // does it contain a substring → true
full.StartsWith("A"); // does it start with text → true
full.Substring(2, 4); // take 4 characters starting at index 2 → "Danc"
full.Replace("Ice", "Snow"); // replace → "A Dance of Fire and Snow"
full.Split(' '); // split by space into an array → ["A", "Dance", "of", "Fire", "and", "Ice"]
string.Join(", ", full.Split(' ')); // join array with commas → "A, Dance, of, Fire, and, Ice"String interpolation
$"{name} is playing"is used constantly in Mod logging:Logger.Log($"Current speed: {speed}")embeds variable values into text.
3. Operators and Expressions
Operators are symbols that operate on variables. C# provides a rich set, introduced by category.
Arithmetic Operators
int a = 10, b = 3;
a + b; // add → 13
a - b; // subtract → 7
a * b; // multiply → 30
a / b; // divide → 3 (integer division, decimals truncated!)
a % b; // modulo (remainder) → 1 (10 = 3×3 + 1)The integer division trap:
10 / 3is3, not3.33. If you want a decimal result, at least one operand must be floating-point:10.0 / 3→3.333.... This pitfall is easy to hit when computing precision or speed values.
Comparison Operators
Comparisons produce a bool (true or false):
10 > 3; // greater than → true
10 < 3; // less than → false
10 >= 10; // greater than or equal → true
10 <= 3; // less than or equal → false
10 == 10; // equal → true (note: == not =)
10 != 3; // not equal → true
=vs==:=assigns (x = 5),==compares (x == 5). Mixing them up is one of the most common beginner bugs.
Logical Operators
Combine multiple conditions:
bool isPlaying = true;
bool isPaused = false;
// && (AND): both must be true
isPlaying && !isPaused; // true && true → true
// || (OR): at least one true
isPlaying || isPaused; // true || false → true
// ! (NOT): invert
!isPaused; // invert false → truePractical usage (checking conditions in a Mod):
// Both conditions must hold
if (health > 0 && isAlive) { /* ... */ }
// Either condition is enough
if (speed > 10f || isBoosting) { /* ... */ }
// Execute when the condition does NOT hold
if (!isModEnabled) { /* ... */ }The Ternary Operator
A shorthand if/else for simple two-way choices:
// Syntax: condition ? valueA : valueB
string status = isAlive ? "alive" : "dead";
// Equivalent to:
string status;
if (isAlive) status = "alive";
else status = "dead";Often used in Mods to provide defaults:
_handler ??= new MelonHandler();(a similar idea — the null-coalescing operator is covered in section 13).
4. Control Flow
Programs execute line by line from top to bottom by default. Control flow lets you decide "which path to take under which conditions" and "how many times to repeat".
if / else — Conditional Branching
int health = 75;
if (health > 100)
{
// runs when health is greater than 100
Logger.Log("Full health");
}
else if (health > 50)
{
// runs when health is between 51 and 100
Logger.Log("In good shape");
}
else
{
// runs when health <= 50
Logger.Log("Low health!");
}Only the first matching branch executes. If health is 75, only the second branch runs.
Almost every Prefix patch uses
if— for example, checking whether the Mod is enabled or whether a parameter is in range:
public static void Prefix(ref float speed)
{
if (MyMod.Settings.SpeedMultiplier > 0) // a speed multiplier is configured
{
speed *= MyMod.Settings.SpeedMultiplier;
}
}switch — Multi-Way Branching
When checking against several fixed values, switch is clearer than if/else:
string difficulty = "Hard";
switch (difficulty)
{
case "Easy":
Logger.Log("Easy mode");
break;
case "Normal":
Logger.Log("Normal mode");
break;
case "Hard":
Logger.Log("Hard mode");
break;
default:
Logger.Log("Unknown difficulty");
break;
}Every case must end with break (or return), otherwise the compiler errors. default is the fallback when nothing matches.
for — Loop a Fixed Number of Times
// Count from 0 to 9 (10 iterations)
for (int i = 0; i < 10; i++)
{
Logger.Log($"Iteration {i}");
}The for parentheses contain three parts: initialization (int i = 0); condition (i < 10, keep looping while true); update (i++, executed after each iteration).
ADOFAI scenario: iterating over all tiles in a level to modify them in bulk:
// Suppose tiles is an array holding every tile's data
for (int i = 0; i < tiles.Count; i++)
{
tiles[i].speed = 1.5f; // set every tile's speed to 1.5
}foreach — Iterating a Collection
When you don't need the index, foreach is cleaner than for:
var mods = new List<string> { "Iridium", "Iris", "Sarcary" };
foreach (var modName in mods)
{
Logger.Log($"Loaded: {modName}");
}
foreachappears very frequently in Mods — iterating all objects in a scene, walking config lists, and so on.
while — Conditional Loop
Loops as long as the condition is true:
int retry = 0;
while (retry < 3)
{
// try to load the resource
retry++;
}Beware infinite loops: if the condition never becomes
false, the program hangs.whileis used less often thanfor/foreachin Mods, but occasionally for waiting on some state.
break and continue
break— exit the entire loop immediatelycontinue— skip this iteration, move to the next
foreach (var tile in tiles)
{
if (tile.isMidspin) continue; // skip midspins
if (tile.isEnd) break; // stop at the end tile
tile.speed = 1.5f;
}5. Methods (Functions)
A method (also called a function) is a named, reusable block of code. It takes input (parameters), processes it, and returns a result.
Basic Form
// Returns int, takes two int parameters
public static int Add(int a, int b)
{
return a + b; // return sends the value back to the caller
}
// Call it
int result = Add(1, 2); // result = 3Methods Without a Return Value
public static void LogMessage(string text)
{
Console.WriteLine(text);
// no return (void means nothing to return)
}
// Call it
LogMessage("Mod loaded");The ref Parameter
ref lets a method modify the variable that was passed in, instead of working on a copy:
public static void Double(ref int x)
{
x = x * 2; // modifies the outer variable itself
}
int v = 5;
Double(ref v); // after the call, v is 10 (not a return value — v itself changed)Why is ref everywhere in Mods? Harmony's Postfix patches need to modify a method's return value, and they use ref for exactly that:
// ref __result: modify the original method's return value
public static void Postfix(ref float __result)
{
__result *= 2f; // directly change the game's computed result
}
refis one of Harmony's core mechanisms (see Magic Parameters).
The out Parameter
out is similar to ref, but signals "this parameter is an output of the method":
// out means number is produced by TryParse
bool success = int.TryParse("42", out int number);
// success = true, number = 42
bool fail = int.TryParse("abc", out int notNumber);
// fail = false, notNumber = 0 (default value on failure)TryParse parses strings safely — on failure it returns false instead of throwing. Mods often use it to read configs:
if (float.TryParse(configText, out float speed))
{
// parse succeeded, use speed
}
else
{
// parse failed, fall back to default
speed = 1.0f;
}Method Overloading
Methods with the same name can have different parameter lists; the compiler picks based on what you pass:
public static void Log(string message)
{
Console.WriteLine(message);
}
public static void Log(string message, string tag) // one extra parameter
{
Console.WriteLine($"[{tag}] {message}");
}
// Calls
Log("hello"); // calls the first
Log("hello", "MyMod"); // calls the secondOptional and Named Parameters
public static void CreatePlayer(string name, float speed = 1f)
{
// speed defaults to 1f and can be omitted at the call site
}
CreatePlayer("Alice"); // speed uses the default 1f
CreatePlayer("Bob", 2.5f); // speed uses 2.5f
CreatePlayer(name: "Charlie", speed: 3f); // named arguments: order doesn't matterparams — Variable Number of Arguments
public static void LogAll(params string[] messages)
{
foreach (var msg in messages)
{
Console.WriteLine(msg);
}
}
LogAll("message 1", "message 2", "message 3"); // pass as many as you like6. Classes and Objects
A class is a blueprint for objects; an object is an instance created from that blueprint. Analogy: the class is "the car's design drawing", the object is "the specific car built from it".
Fields and Methods
public class Player
{
// Fields (variables inside a class)
public string name;
public float speed = 1f; // can have default values
public int health = 100;
// Methods (functions inside a class)
public void Play()
{
Console.WriteLine($"{name} starts playing, speed {speed}");
}
public void TakeDamage(int amount)
{
health -= amount;
Console.WriteLine($"{name} takes {amount} damage, {health} left");
}
}Creating Objects
// The new keyword creates an instance
var player = new Player();
player.name = "Alice";
player.speed = 2f;
player.Play(); // output: Alice starts playing, speed 2
player.TakeDamage(30); // output: Alice takes 30 damage, 70 leftEach new Player() is an independent object; they don't affect each other:
var p1 = new Player();
var p2 = new Player();
p1.name = "Alice";
p2.name = "Bob";
p1.TakeDamage(10); // only Alice loses healthShorthand for Creating Instances (C# 9+)
With a C# 9 or newer compiler, the type after new can be inferred from the declaration on the left:
var player = new Player(); // common: type on the right
Player player2 = new(); // C# 9 shorthand: type on the left, new() infers itThe type inside
new()equals the declared type of the variable. It also works for fields and arguments (settings.Sensitivity = new()). The templates support both forms — being able to read them is enough.
Constructors
A constructor runs automatically when new creates an object, for initialization:
public class Player
{
public string name;
public float speed;
// Constructor: same name as the class, no return type
public Player(string name, float speed = 1f)
{
this.name = name; // this means "the current object"
this.speed = speed;
}
}
// Pass initial values directly when creating
var player = new Player("Alice", 2f);The this keyword refers to "this current object". When a method parameter has the same name as a field, this disambiguates: this.name (field) vs name (parameter).
Access Modifiers
Access modifiers control "who can access what":
public class Player
{
public string name; // public: anyone can access
private int health = 100; // private: only inside this class
protected float speed; // protected: this class and subclasses
}| Modifier | Who can access | Use for |
|---|---|---|
public | Anyone | The exposed interface |
private | This class only | Internal details you don't want changed externally |
protected | This class + subclasses | For inheriting classes |
In Mod templates,
publicis most common (Harmony needs to reach patch methods), whileprivateis for internal state you don't want to expose.
Properties
Properties look like fields but can add access control and logic:
public class Settings
{
public bool EnableFeature { get; set; } = true; // read/write, default true
public float SpeedMultiplier { get; set; } = 1f;
// Read-only property (get only, no set)
public string ModName { get; } = "MyMod";
// Property with logic
private float _volume = 50f;
public float Volume
{
get => _volume; // reading returns the internal value
set => _volume = Math.Clamp(value, 0f, 100f); // writing clamps to 0–100
}
}The { get; set; } accessors — get controls reading, set controls assigning. UMM's settings classes are written exactly like this (see Settings System).
7. Collections
A single variable holds one value. A collection holds a group of values — one of the most used data structures in Mod development.
Arrays
Arrays are the most basic collection; their length is fixed after creation:
// Declare and initialize
int[] scores = { 95, 87, 72, 100, 88 };
// Or like this
int[] scores2 = new int[5]; // length-5 array, all elements default to 0
// Access by index (starts at 0)
scores[0]; // → 95 (first element)
scores[4]; // → 88 (last element)
scores.Length; // → 5 (array length)List<T> — Dynamic List
A List can grow and shrink at any time, far more flexible than an array:
var names = new List<string>(); // create an empty string list
// Add elements
names.Add("Alice");
names.Add("Bob");
names.Add("Charlie");
// Remove elements
names.Remove("Bob"); // remove by value
names.RemoveAt(0); // remove by index
// Common operations
names.Count; // number of elements → 2
names.Contains("Alice"); // contains? → true
names[0]; // access by index → "Charlie"
names.Clear(); // remove all elementsMods often use
Listto collect game objects — e.g. iterate all tiles in the scene, add matching ones to a list, then modify them together.
Dictionary<TKey, TValue> — Key-Value Pairs
A Dictionary looks up a value by a key, like looking up a definition by headword in a dictionary:
var config = new Dictionary<string, float>
{
["SpeedMultiplier"] = 1.5f,
["Volume"] = 80f,
["Threshold"] = 0.1f
};
// Read
config["SpeedMultiplier"]; // → 1.5f
// Modify
config["SpeedMultiplier"] = 2.0f;
// Safe read (doesn't throw if the key is missing)
if (config.TryGetValue("SpeedMultiplier", out float speed))
{
Logger.Log($"Speed multiplier: {speed}");
}
// Check existence
config.ContainsKey("Volume"); // → true
// Iterate
foreach (var pair in config)
{
Logger.Log($"Key: {pair.Key}, Value: {pair.Value}");
}
Dictionaryis used a lot for reading Mod configs and storing tile event data. The template's settings system is key-value based underneath.
HashSet<T> — A Set Without Duplicates
A HashSet guarantees no duplicates — great for deduplication and fast membership checks:
var tags = new HashSet<string> { "mod", "visual", "effect" };
tags.Add("mod"); // already present, not added again
tags.Count; // → 3
tags.Contains("visual"); // → true, faster than List8. The static Keyword
static means "belongs to the class itself" — usable without creating an instance. Analogy: static is like "the company-wide bulletin board" — everyone sees the same one; nobody needs their own copy.
public static class MathHelper
{
// static method: call directly through the class name
public static int Square(int x) => x * x;
public static float Clamp(float value, float min, float max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
}
// Calling: no need for new MathHelper()
MathHelper.Square(3); // → 9
MathHelper.Clamp(15f, 0f, 10f); // → 10static also works on fields — all instances share the same value:
public class GameInfo
{
public static int TotalMods = 0; // shared across all objects
public GameInfo()
{
TotalMods++; // increment the counter for every new object
}
}UMM's
Mainclass and Harmony patch classes are allstatic.Loadis a static method, so UMM can call it withoutnew Main(). A Mod needs exactly oneMain, hencestatic.
9. Generics
Generics use angle brackets <> to mean "a template that works with any type". You write the code once; it handles int, string, float... anything.
// T in List<T> is a placeholder
var numbers = new List<int>(); // T = int
numbers.Add(1);
var names = new List<string>(); // T = string
names.Add("Alice");
// Dictionary<TKey, TValue> has two generic parameters
var map = new Dictionary<string, int>(); // TKey=string, TValue=int
map["score"] = 100;You can define generic methods yourself:
// T can be any type
public static T GetDefault<T>()
{
return default(T); // default returns the type's default value (0 for numbers, null for references)
}
GetDefault<int>(); // → 0
GetDefault<string>(); // → null
GetDefault<bool>(); // → falseHarmony's Transpiler returns
IEnumerable<CodeInstruction>— "a sequence ofCodeInstructionitems", whereCodeInstructionis the IL instruction type andIEnumerablemeans "iterable one by one" (see Transpiler Introduction).
10. Inheritance and Interfaces
Inheritance
A class can inherit another class, gaining all its fields and methods, and can extend or override them:
// Base (parent) class
public class ModBase
{
public string Name { get; set; }
public bool IsEnabled { get; set; }
public virtual void OnToggle(bool value) // virtual: subclasses may override
{
IsEnabled = value;
}
}
// Derived (child) class
public class SpeedMod : ModBase // colon means inheritance
{
public float SpeedMultiplier { get; set; } = 1f;
// override: replace the parent's method
public override void OnToggle(bool value)
{
base.OnToggle(value); // call the parent's logic first
if (value) SpeedMultiplier = 1f; // the subclass's own logic
}
}- Methods marked
virtualcan beoverride-en by subclasses base.OnToggle()calls the parent's original implementation- The subclass automatically has the parent's
NameandIsEnabledproperties
In the ADOFAI Mod templates, loader adapters inherit different base classes:
BaseUnityPlugin(BepInEx),MelonMod(MelonLoader). TheIHandlerinterface is next.
Interfaces
An interface defines "what capabilities must exist" without caring "how they're implemented":
// Interface: the contract
public interface IHandler
{
void Log(string message); // log a message
string ModPath { get; } // where the Mod lives
bool IsEnabled { get; set; } // enabled or not
}
// Implementing the interface: UmmHandler satisfies the contract its own way
public class UmmHandler : IHandler
{
private UnityModManager.ModEntry _entry;
public UmmHandler(UnityModManager.ModEntry entry)
{
_entry = entry;
}
public void Log(string message)
{
_entry.Logger.Log(message); // use UMM's logging system
}
public string ModPath => _entry.Path;
public bool IsEnabled { get; set; }
}
// Implementing the interface: MelonHandler satisfies the contract its own way
public class MelonHandler : IHandler
{
public void Log(string message)
{
MelonLogger.Msg(message); // use MelonLoader's logging system
}
public string ModPath => Environment.CurrentDirectory;
public bool IsEnabled { get; set; }
}A class can implement multiple interfaces but inherit from only one base class.
The heart of the multi-loader template is the
IHandlerinterface: core code only knowsIHandlerand is unaware of which loader it's running on. See Advanced: Multi-Loader Template.
11. Attributes
Attributes use [square brackets] to tag code; frameworks read these tags to decide what to do. Think of them as sticky notes attached to your code.
[HarmonyPatch(typeof(Player), nameof(Player.Play))] // tells Harmony: this is a patch targeting Player.Play
public static class Play_Prefix
{
public static void Prefix() { /* ... */ }
}Common built-in attributes:
[Obsolete("Use NewMethod instead")] // marked deprecated; the compiler warns
public void OldMethod() { /* ... */ }
[Serializable] // marks the type as serializable (savable to file)
public class SaveData { /* ... */ }Defining your own attribute is simple too (though Mods usually just consume framework-provided ones):
// Define the attribute
[AttributeUsage(AttributeTargets.Class)]
public class ModInfoAttribute : Attribute
{
public string Name { get; }
public ModInfoAttribute(string name) { Name = name; }
}
// Use the attribute
[ModInfo("My First Mod")]
public class MyMod { /* ... */ }You don't need to write attributes — you just need to use them: copy [HarmonyPatch(...)] from the template (see HarmonyPatch In Depth).
12. Reflection
Reflection means "using code to inspect code" — at runtime, reading information about classes, methods and fields, and even invoking them dynamically.
// typeof: get type information
Type playerType = typeof(Player);
// GetType: get the type from an instance
var obj = new Player();
Type type = obj.GetType();
// Get method information
var method = AccessTools.Method(typeof(Player), nameof(Player.Play));
// AccessTools is Harmony's reflection helper, more convenient than raw reflection
// Invoke a method dynamically
method.Invoke(obj, null); // calls obj's Play methodDon't worry about the details — just know: the game's method and class names exist as strings, and Harmony uses reflection to find them and inject patches. You won't hand-write reflection in Mod development; Harmony's
AccessToolshandles it (see Locating Target Methods).
13. null and Nullable Types
null means "nothing" — the variable doesn't point to any object.
string name = null; // name points to no string
// Accessing members of null crashes!
// name.Length; // → NullReferenceException (the classic error)? — Nullable Types
Value types (int, float, bool) can't be null by default. Adding ? allows it:
int? maybeNull = null; // nullable int
float? speed = null; // nullable float
// Check before use
if (speed.HasValue)
{
float value = speed.Value; // extract the actual value
}The most common nullable types in Mods are
Harmony?andUnityModManager.ModEntry?— they may benull(before the Mod loads).
Safe Operators
C# offers several idioms to avoid NullReferenceException:
// ?. — null-conditional: if the left side is null, the whole expression is null (no crash)
string? upper = name?.ToUpper(); // if name is null, upper is null too
// ?? — null-coalescing: if the left side is null, use the right side
string result = name ?? "default name"; // uses "default name" when name is null
// is not null — pattern matching
if (name is not null)
{
Logger.Log(name.Length);
}
// Chained ?. (common with Unity)
player?.GetComponent<Rigidbody>()?.AddForce(Vector3.up);
// if any link is null it safely yields null, no crashHunting
NullReferenceExceptionis every Mod developer's daily routine. 90% of the time it's a missing null check. See FAQ.
14. Exception Handling with try/catch
Runtime errors (division by zero, missing files, wrong types...) throw exceptions. Unhandled, they crash the program. try/catch lets you handle errors gracefully.
Basic Form
try
{
int.Parse("not a number"); // this line throws a FormatException
}
catch (Exception ex)
{
Logger.Log($"Error: {ex.Message}");
}If code in try fails, execution jumps straight to the catch block — no crash; your error-handling logic runs instead.
Catching Specific Exceptions
try
{
var content = File.ReadAllText("config.txt");
int value = int.Parse(content);
}
catch (FileNotFoundException)
{
Logger.Log("Config file missing; using defaults");
}
catch (FormatException)
{
Logger.Log("Config file has invalid format");
}
catch (Exception ex) // fallback: catches everything else
{
Logger.Log($"Unknown error: {ex.Message}");
}Put specific exceptions before the general
Exception. The compiler errors if the order is reversed.
finally — Always Runs
FileStream? file = null;
try
{
file = File.OpenRead("data.json");
// read the file...
}
catch (Exception ex)
{
Logger.Log($"Read failed: {ex.Message}");
}
finally
{
file?.Dispose(); // runs whether it succeeded or failed (releases the resource)
}throw — Throwing Your Own Exceptions
public static void SetSpeed(float speed)
{
if (speed < 0)
{
throw new ArgumentException("Speed cannot be negative");
}
// ...
}Harmony's Finalizer patch is essentially this idea — catch exceptions thrown by the target method so the game doesn't crash.
15. Delegates and Events
Delegates
A delegate treats a "method" as a variable — you can assign methods to variables, pass them as arguments, store them in lists.
// Define a delegate type (describes "what kind of method can be assigned")
public delegate bool ToggleHandler(bool value);
// Create a delegate variable and assign
ToggleHandler handler = OnToggle;
bool result = handler(true); // equivalent to calling OnToggle(true)Action and Func — Built-in Delegates
C# provides two general-purpose delegate types so you don't define your own:
// Action: methods with no return value
Action<string> logger = (msg) => Logger.Log(msg);
logger("Mod loaded");
// Func: methods with a return value (the last type parameter is the return type)
Func<int, int, int> add = (a, b) => a + b;
int result = add(1, 2); // → 3
ActionandFuncappear constantly in Mod templates —modEntry.OnToggleis anAction<bool>-style delegate.
Lambda Expressions
=> (read "goes to") marks Lambda syntax — a shorthand for anonymous methods:
// Fully written
Action<string> log = (string msg) => { Logger.Log(msg); };
// Types can be omitted (compiler infers)
Action<string> log2 = (msg) => { Logger.Log(msg); };
// Parentheses optional with a single parameter
Action<string> log3 = msg => Logger.Log(msg);
// Multi-line lambda
Func<int, bool> isPositive = n =>
{
if (n > 0) return true;
return false;
};Events
Events are the "safe version" of delegates — outsiders can only subscribe/unsubscribe, not trigger:
public class EventBus
{
// Define events
public event Action<string> OnModLoaded;
public event Action<string> OnModUnloaded;
// Trigger (only inside the class)
public void Load(string modName)
{
OnModLoaded?.Invoke(modName); // ?.Invoke: skipped if no subscribers
}
}
// Usage
var bus = new EventBus();
bus.OnModLoaded += (name) => Logger.Log($"{name} loaded"); // subscribe
bus.OnModUnloaded += (name) => Logger.Log($"{name} unloaded");
bus.Load("MyMod"); // triggers → logs "MyMod loaded"+=subscribes to an event-=unsubscribes- Outside code cannot invoke
bus.OnModLoaded("test")directly; only the class can trigger it
UMM's
modEntry.OnToggle = OnTogglehands our method to UMM to call at the right time — that's delegation in essence (see Minimal Example).
16. Naming Conventions and Coding Style
The C# community shares naming conventions. Following them makes your code easier to read and consistent with other Mods.
Naming Rules
| Context | Convention | Examples |
|---|---|---|
| Class, method, property, namespace names | PascalCase (capitalize each word) | PlayerSpeed, OnToggle, Main |
| Local variables, parameters | camelCase (lowercase first word) | speedMultiplier, modEntry |
| Constants | PascalCase | MaxRetry, DefaultSpeed |
| Private fields | camelCase or _camelCase | _handler, speed |
| Interfaces | I + PascalCase | IHandler, ISettingsProvider |
// Good naming
public class PlayerController
{
private float _moveSpeed = 5f;
public int MaxHealth { get; set; }
public void ApplyDamage(int damageAmount) { /* ... */ }
}
// Bad naming
public class playercontroller
{
private float ms;
public int mh { get; set; }
public void DoStuff(int d) { /* ... */ }
}Comments
/// <summary>
/// Calculates the ADOFAI speed multiplier.
/// </summary>
/// <param name="baseSpeed">The original speed</param>
/// <param name="multiplier">The multiplier</param>
/// <returns>The computed speed</returns>
public static float CalculateSpeed(float baseSpeed, float multiplier)
{
return baseSpeed * multiplier;
}/// is an XML doc comment; the IDE shows it in hover tips. Regular comments use // (single line) or /* ... */ (multi-line).
Mod code doesn't need a comment on every line, but key logic (why it's done this way, what side effects exist) should be commented. The template projects set a good example.
17. The Mindset You Need
If you could follow all 16 sections above — congratulations, you have enough foundation for this series. The rest is repeated practice:
- Read code: look up unfamiliar syntax instead of being scared; C# error messages are usually clear
- Copy templates: 80% of Mod development is modifying a template, not writing from scratch
- Read errors: paste exceptions from
Player.loginto a search engine — answers usually exist - Use your IDE: Visual Studio / Rider completion, hover tips and quick-fixes save you from memorizing everything
- Debug with breakpoints: set a breakpoint on a key line; the program pauses there at runtime so you can inspect variables step by step
What You Learned
- C# program structure:
namespace/class/method, whatusingdoes, the.csprojproject file - Variables and types:
int/float/double/bool/string/char,var,const, conversions, string operations - Operators: arithmetic, comparison, logical, ternary
- Control flow:
if/else,switch,for,foreach,while,break/continue - Methods:
ref,out, overloading, optional parameters,params - Classes and objects: fields, methods, constructors,
this, access modifiers, properties - Collections: arrays,
List<T>,Dictionary<K,V>,HashSet<T> static, generics, inheritance and interfaces (whereIHandlercomes from)- Attributes and reflection
null,?.,??, nullable typestry/catch/finally, exception types- Delegates,
Action/Func, Lambdas, events - Naming conventions and coding style
- Every section maps to concrete usage in later chapters
Next Step
Ready? Jump into real Mod development → Getting Started