1A fundamental style or approach to structuring programs and reasoning about them.
e.g.Procedural, object-oriented and functional are three of the most influential paradigms.
Programming in Unity · Module 02
Building on the fundamentals — scope and memory, object-oriented programming, structs and generics, collections, inheritance and exception handling. The second pass through C#, into the features you will lean on most in Unity.
Lorenzo Morini · Lead Unity Developer
Test slides · June 2026
Section 01
Memory
While your program runs, all its data lives in memory (RAM) — picture an enormous wall of numbered lockers, each holding a small piece of data. The CPU fetches and stores values by those numbers, called addresses.
Variables are really just friendly names for locations in that memory. Your code never juggles raw addresses — C# handles that for you. But it splits memory into two regions that behave very differently: the stack and the heap.
Memory
Fast, organised, automatic. It grows and shrinks in strict last-in, first-out order: each method call pushes a stack frame holding its locals and parameters, and returning pops it straight off.
Allocation is just moving a pointer, so it's very fast — but it's limited in size and tied to method calls.
A larger, flexible pool for data whose size or lifetime isn't fixed — every object you create with new. Allocation is more involved, and the memory lives as long as something still references it.
It's looked after by the garbage collector, which we'll meet in a moment.
Memory
This is where the value/reference split from the fundamentals becomes concrete:
So copying a value type duplicates the data, while copying a reference type shares it — and "value types live on the stack" is a handy simplification, not an absolute rule.
Memory
flowchart LR
A([new object]) --> B[Lives on the heap]
B --> C{Still reachable?}
C -->|Yes| B
C -->|No| D[Marked as garbage]
D --> E[("Memory reclaimed")]
Memory
In some languages you must manually free every piece of memory you allocate, and forgetting causes leaks and crashes. C# is managed: the garbage collector (GC) periodically finds heap objects that nothing references any more and reclaims them automatically. An object becomes eligible the moment it's unreachable.
This frees you from manual cleanup, but it isn't free: collections take time, so in games we avoid creating needless garbage in hot code paths.
Memory
You allocate and free memory by hand. Maximum control and speed, but mistakes cause leaks, dangling pointers and crashes.
The runtime manages the heap and the GC frees memory for you. Far safer and faster to write, at the cost of some control and the occasional GC pause.
This trade — safety and productivity over manual control — is central to why C# fits games and apps so well.
Section 02
Introduction to OOP
A paradigm is a style of organising and thinking about code. Languages tend to favour one or more. Understanding the main paradigms explains why C# is shaped the way it is.
1A fundamental style or approach to structuring programs and reasoning about them.
e.g.Procedural, object-oriented and functional are three of the most influential paradigms.
Programming Paradigms
Procedural programming organises code as a sequence of steps and functions that act on data. It's exactly the style we've used so far: do this, then that, calling functions along the way.
Simple and direct, but it can get unwieldy as a program grows and data and behaviour drift apart.
Programming Paradigms
Object-oriented programming bundles data and the behaviour that acts on it into objects. Instead of functions floating beside loose data, each object owns its data and the methods that operate on it.
This models real-world "things" — a Player, an Enemy, an Inventory — very naturally.
Programming Paradigms
Functional programming treats computation as the evaluation of functions, favouring immutable data and avoiding hidden state changes. It leads to predictable, easily testable code.
C# borrows many functional ideas — lambdas, LINQ, immutability — even though it isn't a purely functional language.
Programming Paradigms
C# is multi-paradigm. It's object-oriented at its core, fully supports procedural code (the top-level statements you've been writing), and embraces functional techniques too.
You can pick the right style for each problem — but OOP is the backbone we'll build on next.
Introduction to OOP
Object-oriented programming structures a program as a set of cooperating objects, each combining state (data) with behaviour (methods). It's the dominant way large C# and Unity projects are built.
What Is OOP
Each pillar gets its own treatment later in the course.
What Is OOP
As programs grow, loose functions and shared data become hard to manage. OOP tames that complexity by:
What Is OOP
C# gives you the full OOP toolkit: class and struct to define types, access modifiers for encapsulation, inheritance via :, virtual/override for polymorphism, and abstract classes and interfaces for abstraction.
The next sections introduce each of these in turn.
Section 03
Classes and Objects
This is the cornerstone of OOP. A class is a blueprint that defines what a kind of thing has and does; an object is a concrete instance built from that blueprint.
Classes and Objects
A class defines a new type by describing its data (fields) and behaviour (methods). It's a template — defining a class doesn't create any actual data yet.
class Player
{
public string Name;
public int Health;
public void TakeDamage(int amount)
{
Health -= amount;
}
}Classes and Objects
An object is a real instance of a class, with its own copy of the data, living in memory. From one Player class you can make many independent players.
Player hero = new Player();
hero.Name = "Aria";
hero.Health = 100;
hero.TakeDamage(30); // hero.Health is now 701A class is the blueprint; an object is a specific thing built from it. One class, many objects.
e.g."House blueprint" is the class; the three identical houses built from it are three objects, each with its own address and paint colour.
Classes and Objects
Creating an object from a class is called instantiation, done with the new keyword. new allocates the object on the heap and hands you a reference to it.
Player p1 = new Player(); // one instance
Player p2 = new Player(); // a separate, independent instance
Each new produces a distinct object with its own field values.
Classes and Objects
Remember how the compiler wrapped your top-level code in a class with a Main method? Now it makes sense: there is no code outside a class in C#. Your program was always a class all along.
class Program
{
static void Main(string[] args)
{
// your top-level code lived here
}
}
The training wheels are off — everything is objects and classes.
Classes and Objects
An object's data is stored in fields, but we usually expose it through properties — a controlled gateway that looks like a field but can run code behind the scenes.
Fields and Properties
A field is a variable that belongs to an object — its raw stored data. Each object has its own copy.
class Player
{
public string name; // a field
public int health; // a field
}
Exposing fields directly is convenient but gives you no control over how they're read or changed.
Fields and Properties
A property looks like a field from the outside but is backed by get and set accessors — methods that run when the value is read or written. This lets you validate or react to changes.
class Player
{
private int health;
public int Health
{
get { return health; }
set { health = value < 0 ? 0 : value; } // never below 0
}
}Fields and Properties
When you don't need custom logic, an auto-property gives you the property syntax with a hidden backing field generated for you — concise and the everyday default.
class Player
{
public string Name { get; set; }
public int Health { get; private set; } // read-only from outside
}Fields and Properties
The get accessor returns the value; the set accessor receives it through the implicit value keyword. You can omit one to control access, or run any logic inside.
public string Name
{
get => name;
set => name = value.Trim(); // tidy input on the way in
}Fields and Properties
Raw storage. Direct and fast, but no control — anyone can read or write any value.
A controlled gateway. Same usage syntax, but it can validate, compute, restrict access, or react to changes.
Rule of thumb: keep fields private, and expose data through public properties.
Classes and Objects
A constructor is special code that runs when an object is created, used to set it up — typically giving its fields their initial values.
Constructors
A constructor is a method named after the class, with no return type. It runs once, when you new the object, guaranteeing it starts life in a valid state.
class Player
{
public string Name;
public Player() // the constructor
{
Name = "Unnamed";
}
}Constructors
If you write no constructor at all, C# supplies a hidden default constructor that takes no arguments and leaves fields at their default values. The moment you write any constructor of your own, that freebie disappears.
Player p = new Player(); // works via the default constructorConstructors
A constructor can take parameters so callers supply starting values, making objects impossible to create in a half-built state.
class Player
{
public string Name;
public int Health;
public Player(string name, int health)
{
Name = name;
Health = health;
}
}
Player hero = new Player("Aria", 100);Constructors
One constructor can call another with : this(...), avoiding duplicated setup code. This is constructor chaining.
public Player() : this("Unnamed", 100)
{
// delegates to the constructor below
}
public Player(string name, int health)
{
Name = name;
Health = health;
}Classes and Objects
Access modifiers control who can see and use a member. They're how C# enforces encapsulation — deciding what's part of a type's public face and what's hidden inside.
Access Modifiers
public members are visible to everyone — any code anywhere can read or call them. This is the deliberate, public-facing surface of a type.
public string Name;
public void Attack() { /* ... */ }Access Modifiers
private members are visible only inside the same class. This is the default for class members, and the right choice for internal details you don't want others touching.
private int health;
private void Recalculate() { /* ... */ }Access Modifiers
protected members are visible inside the class and any class that inherits from it, but not to outside code. It's the bridge between a base class and its descendants.
protected int experience;
We'll use this once we reach inheritance.
Access Modifiers
internal members are visible anywhere within the same assembly (project/compiled unit) but not to other assemblies. Good for code that's public across your project yet not part of its external API.
internal class LevelLoader { /* ... */ }Access Modifiers
Access modifiers turn encapsulation from an idea into a rule the compiler enforces. By keeping fields private and exposing a small public surface, you:
Classes and Objects
Most members belong to an object. Static members belong to the class itself — shared by all instances, and usable without creating any object at all.
Static Members
static ties a member to the type rather than to any one instance. There's exactly one copy, no matter how many objects exist — or even if none do.
Console.WriteLine(...); // WriteLine is static — no Console object needed
int big = int.MaxValue; // MaxValue is a static field on intStatic Members
A static field is shared state across all instances; a static method is called on the class, not an object. Static methods can't touch instance data, since there's no specific instance.
class Enemy
{
public static int Count; // shared by all enemies
public Enemy() { Count++; }
public static void ResetCount() => Count = 0;
}Static Members
A class marked static can't be instantiated and holds only static members. It's a tidy home for utility functions that don't need any object state.
static class MathHelper
{
public static int Square(int n) => n * n;
}
int result = MathHelper.Square(5);Static Members
Belongs to a specific object. Each object has its own copy; accessed through a variable like hero.Health.
Belongs to the class. One shared copy; accessed through the type like Enemy.Count.
Ask: "is this about one object, or about the whole type?" That answers whether it should be static.
Classes and Objects
A method is a function that lives inside a class. Everything you learned about functions — parameters, return types, ref/out — applies; the difference is that a method belongs to a type and can act on its object's data.
Methods
The standalone functions we faked with top-level statements are, properly, methods on a class. Inside a method, you can read and change the object's own fields directly.
class Player
{
public int Health;
public void Heal(int amount) // a method
{
Health += amount; // acts on this object's field
}
}Methods
Overloading lets several methods share a name as long as their parameters differ. The compiler picks the right one from the arguments you pass.
void Log(string message) { /* ... */ }
void Log(int code) { /* ... */ }
void Log(string message, int code) { /* ... */ }
Log("Saved"); // calls the first
Log(404); // calls the second
Log("Error", 500); // calls the third
Section 04
Structs
A struct defines a custom type much like a class — with fields, properties and methods — but it's a value type. That single difference changes how it's copied, stored and used.
Structs
You declare a struct with the struct keyword. It's ideal for small, self-contained bundles of data, like a point or a colour.
struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
}
Point p = new Point(3, 4);Structs
Copied on assignment; each variable is independent. Usually lives on the stack. No inheritance.
Shared by reference; copies point to the same object. Lives on the heap. Supports inheritance.
Point a = new Point(1, 1);
Point b = a; // b is a full copy
b.X = 99; // a.X is still 1
Structs
Reach for a struct when the data is:
Unity's Vector3 and Color are structs for exactly these reasons. When in doubt, use a class.
Structs
null.
Section 05
Generics
Generics let you write a class or method with a type placeholder, filled in when it's used. You get code reuse and full type safety — no casting, no losing track of types.
Generics
The placeholder — written <T> by convention — stands in for a real type chosen by the caller. The same code then works for any type, while the compiler still checks every use.
List<int> numbers = new List<int>(); // T is int
List<string> names = new List<string>(); // T is string
You've already used generics every time you wrote List<...>.
Generics
A generic class declares one or more type parameters in angle brackets, then uses them inside as if they were real types.
class Box<T>
{
private T contents;
public void Put(T item) => contents = item;
public T Get() => contents;
}
Box<int> intBox = new Box<int>();
intBox.Put(42);
int value = intBox.Get();Generics
A single method can be generic even if its class isn't. The type parameter goes right after the method name.
void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y); // T inferred as intGenerics
Sometimes a generic type must meet a requirement — be a class, have a constructor, or implement an interface. The where clause adds those constraints, unlocking what you can do with T.
T CreateAndReturn<T>() where T : new()
{
return new T(); // allowed because T must have a constructor
}
void Print<T>(T item) where T : IComparable { /* ... */ }
Section 06
Collections
Collections are types built to store and manage groups of objects. Unlike arrays, most can grow and shrink, and they come with rich methods for adding, finding and removing items.
Collections Overview
Real programs rarely know how many items they'll hold up front — an inventory grows, enemies spawn and die. Collections handle that dynamism for you, so you don't manually resize and copy arrays.
They live in the System.Collections.Generic namespace and are all generic.
Collections Overview
Fixed size, chosen at creation. Slightly faster and lighter. Best when the count is known and stable.
Resizes automatically. Rich helper methods. Best when items are added and removed over time.
An array is the simple foundation; collections add convenience and flexibility on top.
Collections
A List is the workhorse collection: an ordered, resizable sequence of same-typed items. If you reach for one collection by default, it's this one.
List
A List<T> is like an array that can grow and shrink. T is the element type, fixed when you declare it, so it stays fully type-safe.
List<int> scores = new List<int>();
List<string> names = new List<string>();List
Create an empty list, or initialize it with starting values using a collection initializer.
List<int> empty = new List<int>();
List<string> party = new List<string> { "Aria", "Ben", "Cleo" };List
Lists handle resizing for you as you add and remove.
List<string> party = new List<string>();
party.Add("Aria"); // append
party.Insert(0, "Ben"); // at a position
party.Remove("Aria"); // by value
party.RemoveAt(0); // by index
Console.WriteLine(party.Count); // how manyList
Walk a list with foreach when you want each item, or a for loop when you need the index.
foreach (string name in party)
{
Console.WriteLine(name);
}
for (int i = 0; i < party.Count; i++)
{
Console.WriteLine($"{i}: {party[i]}");
}List
Add / AddRange — append items.Remove / RemoveAt / Clear — take items out.Contains / IndexOf — search.Count — current number of items.Sort / Reverse — reorder in place.Collections
A Dictionary stores data as key → value pairs, letting you look up a value instantly by its key instead of scanning through positions.
Dictionary
A Dictionary<TKey, TValue> maps unique keys to values. Think of a real dictionary: look up a word (key) to get its definition (value).
Dictionary<string, int> ages = new Dictionary<string, int>();Dictionary
Each entry pairs a key with a value. Keys must be unique; values needn't be. The key is how you find the value again.
// key: player name value: their score
Dictionary<string, int> scores = new Dictionary<string, int>();
scores["Aria"] = 1500; // "Aria" is the key, 1500 the valueDictionary
Start empty or seed it with pairs using an initializer.
Dictionary<string, int> scores = new Dictionary<string, int>
{
["Aria"] = 1500,
["Ben"] = 1200
};Dictionary
Use the key like an index to read or write. TryGetValue safely handles missing keys.
scores["Cleo"] = 900; // add or update
scores.Remove("Ben"); // remove by key
int aria = scores["Aria"]; // read (throws if missing)
if (scores.TryGetValue("Cleo", out int c))
{
Console.WriteLine(c); // safe lookup
}Dictionary
Loop with foreach; each item is a KeyValuePair with .Key and .Value.
foreach (KeyValuePair<string, int> entry in scores)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}Collections
List and Dictionary cover most needs, but C# offers specialised collections whose shape enforces a particular access pattern.
Other Collections
A Queue is first-in, first-out (FIFO) — like a line at a checkout. You Enqueue at the back and Dequeue from the front.
Queue<string> tasks = new Queue<string>();
tasks.Enqueue("load");
tasks.Enqueue("render");
string next = tasks.Dequeue(); // "load"Other Collections
A Stack is last-in, first-out (LIFO) — like a stack of plates. You Push on top and Pop from the top. Great for undo history.
Stack<string> history = new Stack<string>();
history.Push("move1");
history.Push("move2");
string undo = history.Pop(); // "move2"Other Collections
A HashSet stores unique values with no order, and checks membership extremely fast. Adding a duplicate simply does nothing.
HashSet<string> visited = new HashSet<string>();
visited.Add("room1");
visited.Add("room1"); // ignored — already present
bool seen = visited.Contains("room1"); // trueOther Collections
Process items in arrival order — job queues, message handling, breadth-first traversal.
Process the most recent first — undo/redo, backtracking, parsing.
Track membership and uniqueness — "have I seen this?", removing duplicates.
Pick the collection whose rules match your access pattern, and the code reads its own intent.
Section 07
Inheritance & Polymorphism
Inheritance lets one class build on another, reusing its data and behaviour and adding or changing what it needs. It models "is-a" relationships: a Dog is an Animal.
Inheritance
Inheritance creates a new class from an existing one. The new class gets everything the original has, then extends it — avoiding duplicated code and capturing shared structure once.
class Animal
{
public string Name;
public void Eat() => Console.WriteLine($"{Name} eats");
}
class Dog : Animal
{
public void Bark() => Console.WriteLine("Woof");
}1The base (parent) class is inherited from; the derived (child) class inherits and extends it.
e.g.Animal is the base class; Dog is a derived class that gains Name and Eat() while adding Bark().
Inheritance
You declare inheritance with a colon: class Derived : Base. A class can inherit from exactly one base class in C#.
class Enemy { /* ... */ }
class Boss : Enemy { /* Boss is an Enemy, plus more */ }Inheritance
A derived class inherits the base's public, protected and internal members. It does not inherit:
private members (they exist but aren't accessible directly).The derived class can then add new members or override existing behaviour.
Inheritance
base refers to the parent class. Use it to call the base constructor, or to invoke base behaviour you're extending rather than fully replacing.
class Boss : Enemy
{
public Boss(string name) : base(name) // call Enemy's constructor
{
}
public override void Attack()
{
base.Attack(); // do the normal attack...
Console.WriteLine("...then a special move!");
}
}Inheritance & Polymorphism
By default a derived class can't change an inherited method. virtual and override are the opt-in mechanism that lets it provide its own version — the basis of polymorphism.
Virtual and Override
Marking a base method virtual declares "derived classes may replace this." It still provides a default implementation that's used unless someone overrides it.
class Animal
{
public virtual void Speak() => Console.WriteLine("...");
}Virtual and Override
A derived class uses override to supply its own version of a virtual method. Calls then run the derived version, even through a base-typed variable.
class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof");
}
Animal a = new Dog();
a.Speak(); // "Woof" — the Dog version runsVirtual and Override
Together they let you write code against a general type and have each specific type behave correctly. A list of Animal can hold dogs and cats, and Speak() does the right thing for each — no if-chains on type.
Virtual and Override
sealed is the opposite of leaving things open. On a class it prevents further inheritance; on an overridden method it stops further overriding down the chain.
sealed class FinalBoss : Enemy { /* no one can inherit from this */ }
public sealed override void Attack() { /* can't be overridden again */ }Inheritance & Polymorphism
An abstract class is a base class that can't be instantiated on its own — it exists to be inherited from. It captures what a family of types shares while leaving some details for each child to fill in.
Abstract Classes
Marking a class abstract says "this is an incomplete blueprint — only concrete subclasses can be created." You can't write new Shape() if Shape is abstract.
abstract class Shape
{
public string Name;
}
// new Shape(); // error — abstract
Shape s = new Circle(); // fine — Circle is concreteAbstract Classes
An abstract method has no body — just a signature. It forces every concrete subclass to provide an implementation, guaranteeing the behaviour exists.
abstract class Shape
{
public abstract double Area(); // no body — must be overridden
}
class Circle : Shape
{
public double Radius;
public override double Area() => 3.14159 * Radius * Radius;
}Abstract Classes
Use an abstract class when types share real structure and code, but one member can't be meaningfully defined at the base level.
Shape, Enemy, Weapon.Inheritance & Polymorphism
An interface is a pure contract: a list of members a type promises to provide, with no implementation and no data. It says what a type can do, never how.
Interfaces
Declared with interface (named with a leading I by convention), it lists method and property signatures only. Any type that implements it must supply all of them.
interface IDamageable
{
void TakeDamage(int amount);
int Health { get; }
}Interfaces
Implement an interface with the same colon syntax as inheritance, then provide every member it declares.
class Player : IDamageable
{
public int Health { get; private set; } = 100;
public void TakeDamage(int amount) => Health -= amount;
}Interfaces
A pure contract — no fields, (traditionally) no implementation. A type can implement many. Answers "what can it do?"
A partial base — can hold fields and shared code. A type can inherit only one. Answers "what is it?"
Rule of thumb: use an interface for a capability, an abstract class for a shared identity.
Interfaces
A class inherits from only one base class, but it can implement many interfaces — mixing in several capabilities at once.
class Player : Character, IDamageable, IMovable, ISaveable
{
// must implement the members of all three interfaces
}
This is how C# gets the flexibility of multiple inheritance without its pitfalls.
Inheritance & Polymorphism
Polymorphism — "many forms" — lets one piece of code work with objects of many types through a shared base or interface, each responding in its own way.
Polymorphism
It means a single name or type can take many forms. A variable typed as Animal can hold a Dog or a Cat, and calling Speak() runs the version that fits the actual object.
Polymorphism
With virtual/override, C# decides at run time which method to call based on the object's real type — not the variable's declared type. This is runtime polymorphism.
List<Animal> zoo = new List<Animal> { new Dog(), new Cat() };
foreach (Animal a in zoo)
{
a.Speak(); // each speaks correctly — Woof, then Meow
}Polymorphism
IDrawable, whatever its concrete type.TakeDamage on anything IDamageable.ISaveable object the same way.Polymorphism is what lets game systems treat wildly different objects uniformly.
Section 08
Exception Handling
Even correct code meets bad input, missing files and broken connections. Exceptions are C#'s way of signalling and handling these runtime problems without crashing the whole program.
Exception Handling
An exception is an object representing an error that occurs while running. When something goes wrong, code throws an exception; unless it's caught, it stops the program.
int[] a = { 1, 2, 3 };
Console.WriteLine(a[10]); // throws IndexOutOfRangeExceptionException Handling
Wrap risky code in try, handle failures in catch, and put cleanup that must always run in finally.
try
{
int result = 10 / divisor;
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Can't divide by zero");
}
finally
{
Console.WriteLine("This always runs");
}Exception Handling
NullReferenceException — using something that's null.IndexOutOfRangeException — an array index past the end.DivideByZeroException — integer division by zero.FormatException — parsing text that isn't the expected format.ArgumentException — a method got an invalid argument.Exception Handling
You can define your own exception types for errors specific to your program, by inheriting from Exception. They make failures self-describing and catchable on their own.
class OutOfManaException : Exception
{
public OutOfManaException(string message) : base(message) { }
}
throw new OutOfManaException("Not enough mana to cast");Exception Handling
Sometimes you catch an exception to log or react, then want it to keep propagating. Use a bare throw; to re-throw while preserving the original error and its stack trace.
try
{
LoadSave();
}
catch (Exception ex)
{
Log(ex);
throw; // re-throw; don't write "throw ex;" — it loses the trace
}Exception Handling
flowchart TD
A[System.Exception] --> B[SystemException]
A --> C[Your custom exceptions]
B --> D[NullReferenceException]
B --> E[IndexOutOfRangeException]
B --> F[ArgumentException]
F --> G[ArgumentNullException]
Section 09
Week 1 Recap
Week 1 Recap
None of these topics stands alone. Types describe your data; variables hold it; functions and control flow act on it; classes bundle data with behaviour; the value/reference distinction explains how it all lives in memory; collections and exceptions are built from the very same ideas.
You now have a complete mental model of how a C# program is built and how it runs.
Week 1 Recap
With the language under your belt, we step into Unity. Everything you've learned carries over directly:
MonoBehaviour.Vector3 and friends are structs; game objects are managed by reference.Same language, new stage. See you in Unity.