C# for Unity¶
What's in here
- The C# features Unity code uses constantly, with comparisons to Java, Python, and C++
- The Unity-specific surprises: its strange version of
null, serialization limits, and coroutines - What Unity's C# version can't do yet
How to Use This Page¶
This is NOT a from-scratch C# course. It's for people who can already program in something and want to read and write Unity code without getting ambushed by the differences. Thus, I recommend skimming the headings, reading what's new to you, and coming back when a snippet in another guide (or someone else's code) uses something you haven't seen.
Where it helps, sections have tabs comparing C# to Java, Python, and C++ as well.
Unity uses C# 9
Microsoft's docs often show newer C# than Unity supports. If a snippet gives you a strange syntax error, check the C# 9 note in the style guide first.
Value Types and Reference Types¶
C# has two kinds of types:
- Classes are reference types. A variable holds a reference to an object. Assign it to another variable and both point at the same object.
- Structs are value types. A variable holds the value itself. Assign it and you get a copy.
Unity's math types are structs: Vector3, Quaternion, Color. Thus, you can't do this:
// Error CS1612: Cannot modify the return value of 'Transform.position'
// because it is not a variable.
transform.position.x = 5f;
transform.position is a property that hands you a copy of the position. Changing .x on that copy would do nothing, so the compiler stops you. The fix is to copy, change, and assign back:
Java has no user-defined value types. Everything except the primitives is a reference. In C#, a struct behaves like a primitive: copied on assignment, compared by value, and never null (unless you make it nullable, below).
In Python, every variable is a reference, so b = a never copies anything. C# structs don't do this; Vector3 b = a; makes an independent copy, and changing b leaves a alone.
Note that in C++, struct and class only differ in default access. In C#, they're totally different: struct is a value type (copied, usually on the stack or inline), and class is a reference type (always on the heap, garbage collected). There's no choosing per-variable like T versus T*.
Properties¶
A property looks like a field from the outside, but runs code when you read or write it.
public class Health : MonoBehaviour
{
// Auto-property: anyone can read it, only this class can set it.
public int Current { get; private set; }
// Computed property: no storage, calculated every time.
public bool IsDead => Current <= 0;
}
The style guide has the conventions I recommend for them.
Properties replace getX()/setX() pairs. health.Current reads like a field but calls the getter, and { get; private set; } is a public getter with a private setter in one line.
Same idea as @property, with the getter and setter declared together, and an auto-property generates the storage for you.
There's no direct equivalent. Think of a pair of inline accessor methods with field syntax at the call site. Note that properties can't be passed by ref, which is part of why the transform.position.x error above exists.
Access Modifiers¶
| Modifier | Who can see it |
|---|---|
public |
Everyone |
private |
Only this class. The default for class members if you don't write anything. |
protected |
This class and classes that inherit from it |
internal |
Anything in the same assembly (for us, usually the whole game, this isn't super relevant) |
Best practice is to default to private and open things up only when something outside actually needs them.
Namespaces and using¶
Namespaces group related types and stop names from colliding. using at the top of a file lets you skip the namespace prefix.
using System.Collections.Generic;
using UnityEngine;
namespace Combat
{
public class DamageCalculator
{
// ...
}
}
Unity's C# 9 only supports the block form above, not the newer namespace Combat; one-liner.
Namespaces are like packages, and using is like import, except that C# namespaces don't have to match your folder structure.
using is roughly from module import *, scoped to one file. There's no per-name import; you get the whole namespace.
Namespaces work almost exactly like C++ namespaces, and using UnityEngine; is like using namespace (but it's normal and fine here, not a code smell).
Delegates, Action, Func, and Lambdas¶
A delegate is a variable that holds a method. You'll mostly use the two built-in delegate types:
Action,Action<T>,Action<T1, T2>: a method that returns nothing and takes the specified types (T) as parametersFunc<TResult>,Func<T, TResult>: a method that returns something (the last type is the return type)
Action onLanded = () => Debug.Log("Landed!");
Func<int, int> doubleIt = x => x * 2;
onLanded();
int result = doubleIt(21);
The => syntax is a lambda: a small inline function.
Like functional interfaces (Runnable, Function<T, R>) and Java lambdas, but you don't need an interface: Action and Func cover almost everything.
Functions are first-class in Python, and delegates are how C# does the same thing with types attached. Lambdas can have full bodies with braces, unlike Python's one-expression lambda.
Like std::function combined with lambdas, with automatic capture by reference of local variables (be careful with that inside loops).
Events¶
An event is a delegate with a lock on it: outside code can only subscribe (+=) and unsubscribe (-=), and only the class that owns the event can raise it.
public class Boss : MonoBehaviour
{
public event Action Defeated;
private void Die()
{
Defeated?.Invoke();
}
}
// Somewhere else:
boss.Defeated += OpenExitDoor;
boss.Defeated -= OpenExitDoor;
Every += needs a matching -=, usually in OnEnable and OnDisable. The OOP Toolkit explains in more detail why that's important.
Generics¶
Generics let one class or method work with any type, while keeping full type checking.
List<Enemy> enemies = new List<Enemy>();
Dictionary<string, int> scores = new Dictionary<string, int>();
T GetOrAdd<T>(GameObject target) where T : Component
{
if (!target.TryGetComponent(out T component))
{
component = target.AddComponent<T>();
}
return component;
}
where T : Component is a constraint: it promises the compiler that T will be some kind of component, which is what makes calling AddComponent<T>() legal.
Similar syntax, but C# generics aren't erased: List<int> really stores ints (no boxing), and you can use typeof(T) at runtime. Look up reification for more info!
Like type hints (list[Enemy]), except the compiler actually enforces them.
Like templates, but checked once when you write the generic code (using constraints) instead of per instantiation. Much friendlier error messages, and less power than templates.
Attributes¶
The things in square brackets are attributes: metadata attached to code, which tools (like Unity) read.
[SerializeField], [Tooltip], [Header], and [CreateAssetMenu] are the ones you'll see most. They don't change what your code does when it runs; they change how Unity treats it.
Exactly like annotations (@Override, @Deprecated).
They look a bit like decorators, but they never wrap or change the function. They're just labels something else reads.
Similar in spirit to [[nodiscard]]-style attributes, but far more common, and readable at runtime through reflection.
Interfaces and Abstract Classes¶
Both describe what something can do.
public interface IDamageable
{
void TakeDamage(int amount);
}
public abstract class Weapon : MonoBehaviour
{
[SerializeField] protected int damage = 10;
public abstract void Fire();
public virtual void Reload() { }
protected void PlayFireSound()
{
// Shared code every weapon gets for free.
}
}
- An interface is a pure contract: method signatures, no fields (you can provide default implementations, however). A class can implement as many as it wants.
- An abstract class can also share real code and fields, but a class can only inherit from one. Virtual fields can also provide a default body.
So interfaces are for "can do X" (damageable, interactable, flammable), and abstract classes are for "is a kind of X that shares the same interior." The OOP Toolkit covers when to use each.
Null (and Unity's Weird Null)¶
Reference types can be null, and using a null reference throws a NullReferenceException, the single most common error in Unity. C# gives you some shortcuts for dealing with null:
// Null-conditional: only calls Invoke if Died isn't null.
Died?.Invoke();
// Null-coalescing: use the right side if the left side is null.
string label = customName ?? "Unnamed";
There's a catch in Unity. When you Destroy a GameObject or component, the C# object doesn't actually disappear. Unity destroys the real object on its side, but your variable still points at a C# shell. To make that less confusing, Unity overrides == so that a destroyed object compares equal to null:
Destroy(enemy);
// Later, on a following frame:
if (enemy == null)
{
// true, even though the C# variable isn't technically null
}
The catch is that ?. and ?? can't be overridden, so they skip Unity's check and see the shell as "not null." Read Unity's docs for more.
So for anything that's a Unity object (GameObjects, components, ScriptableObjects):
// Works correctly with destroyed objects:
if (target != null)
{
target.TakeDamage(10);
}
// Also works: Unity objects convert to false when destroyed or missing.
if (target)
{
target.TakeDamage(10);
}
// Looks fine, but lets a destroyed target through:
target?.TakeDamage(10);
?. is still perfectly fine on plain C# things, like events, lists, and your own non-Unity classes.
Three flavors of 'it's null'
NullReferenceException: a plain C# reference wasnullwhen you used it.MissingReferenceException: you used a Unity object that's been destroyed. Usually a sign that something kept a reference to an object after it was gone (like an event that was never unsubscribed).UnassignedReferenceException: a serialized field was left empty in the Inspector. The fix is usually dragging the right object into that field.
Nullable Value Types¶
Value types like int can't normally be null. Add a ? and they can:
int? bestTime = null;
if (bestTime.HasValue)
{
Debug.Log($"Best: {bestTime.Value}");
}
int shown = bestTime ?? 0;
Handy for "no value yet," like a best time before the first run.
var¶
var lets the compiler figure out a variable's type from the right-hand side. It's still fully typed; you just didn't write the type. The style guide rule is to use it only when the type is obvious from the right side, like var enemies = new List<Enemy>();.
Strings¶
// Interpolation: the easy way to build strings.
string message = $"{playerName} took {damage} damage";
// Verbatim strings: backslashes are literal.
string path = @"C:\Games\Save.json";
Strings are immutable; every change makes a new string. That's fine almost everywhere, but building a big string in a loop is faster with System.Text.StringBuilder.
Collections¶
| Type | Use it for | Unity serializes it? |
|---|---|---|
T[] (array) |
Fixed-size lists | Yes |
List<T> |
Lists that grow and shrink | Yes |
Dictionary<TKey, TValue> |
Looking things up by key | No |
HashSet<T> |
"Is this in the set?" checks | No |
Note that Dictionary field won't show up in the Inspector, and Unity doesn't save it. A common workaround is a serialized List of key/value pairs that you turn into a dictionary in Awake. Alternatively, if you use Odin Inspector (which the SIGGD games will be using), you can serialize special types of dictionaries.
foreach loops over any of them:
Don't add or remove items from a collection while you're foreach-ing over it. That throws an exception. Loop over a copy, or collect the changes and apply them after.
A quick note on LINQ
using System.Linq; gives you methods like Where, Select, OrderBy, and First that make collection code short and readable. Most of them allocate memory as they go, though, so keep them out of code that runs every frame. These are super helpful though!
Exceptions¶
When code throws an exception in Unity, the error shows up in the Console, the rest of that method call is skipped, and the game keeps running. Next frame, Update runs again like nothing happened. That's forgiving, but it also means you have to be more intentional. Ensure that your code doesn't throw unnecessary errors.
Only catch exceptions you can actually do something about. Wrapping code in a try/catch that swallows everything tends to hide bugs that could be fixed.
Coroutines and Async¶
Games often need something to happen over time: wait two seconds, fade out, then respawn. There are two main ways to write that.
Coroutines are Unity's built-in way. A method returns IEnumerator, and yield return pauses it:
private IEnumerator RespawnAfterDelay(float seconds)
{
yield return new WaitForSeconds(seconds);
Respawn();
}
// Start it:
StartCoroutine(RespawnAfterDelay(2f));
A coroutine belongs to the MonoBehaviour that started it. It stops if that GameObject is deactivated or the script is destroyed, but not if you just disable the script with enabled = false.
async/await is C#'s general version of the same idea. The game project uses a library called UniTask, which makes async code work nicely with Unity's frame loop. You'll see it in the real codebase, but coroutines are completely fine for most people. I personally prefer UniTask, but it can be complicated to learn!
Next Steps¶
- How Unity Thinks: the Unity side of the mental model, if you haven't done it yet.
- C# Style Guide: how we write all of the above in the game.
- The OOP Toolkit: putting interfaces, events, and generics to work.
Further Reading¶
- Microsoft's C# documentation: the official language reference (remember it shows newer versions than Unity's C# 9).
- Unity Manual: Script serialization rules: exactly what Unity can and can't save.
- Unity Manual: Coroutines: everything about writing and stopping coroutines.
- Unity Scripting API: Object: the official word on Unity's null behavior.