Programming in Unity · Module 01 · Part 2

The Building Blocks

Types, operators, functions, control flow and memory — the core machinery of C#. Almost everything a program does, it does by combining these eight ideas.

 

Types

Section 06

How C# sorts every value into a category — and checks those categories before your program ever runs.

Gustave Doré engraving of Brother Bruin the bear
Brother BruinGustave Doré

Types

Type

taɪp

1A classification that defines what kind of data a value is, what it can hold, and which operations are legal on it.

e.g.5 is an int, "hello" is a string, true is a bool. You can add two ints — but C# won't let you add an int and a bool.

Types

C# Checks Types Before It Runs

C# is statically typed: it verifies every type at compile time — before the program runs — instead of discovering mismatches mid-execution. Moving the check to the very start buys four things:

  • Mistakes caught early — mixing a string and an int simply won't compile, so a whole class of bugs never ships.
  • Faster execution — types are already settled, so the runtime never stops to ask "what is this?"
  • Leaner memory — knowing each value's size up front lets the runtime lay data out tightly.
  • Smarter tools — your editor knows every type as you type, powering accurate autocomplete and safe refactors.

Types

Two Families of Type

Every type in C# is either a value type (the variable holds the data itself) or a reference type (the variable holds an address pointing to data that lives elsewhere). That single split shapes how data is copied, compared and stored.

flowchart TD
    T["C# type"] --> V["Value type — holds the data"]
    T --> R["Reference type — holds an address"]
    V --> ST["struct"]
    V --> E["enum"]
    R --> C["class"]
    R --> I["interface"]

Types

Copy vs Share — The Core Difference

Assigning a value type copies the data; the two variables are now independent. Assigning a reference type copies the address, so both names point at the same object.

Value type — copied

int a = 10;
int b = a;   // b gets a copy
b = 99;      // a is untouched
// a is still 10

Reference type — shared

int[] x = { 1, 2, 3 };
int[] y = x;   // same array
y[0] = 99;
// x[0] is now 99 too

Types

Why This Distinction Follows You Everywhere

This one idea quietly decides everyday behaviour:

  • Whether changing one variable affects another.
  • What a method can and can't change when you pass data into it.
  • How equality and copying behave.

Keep it in the back of your mind — and we'll make it physical in the Memory section, where you'll see exactly where each family lives.

Types

Simple Types

C# bakes in a handful of simple types for the most basic data — whole numbers, decimals, single characters, true/false — each with a short keyword name. Every keyword is just a friendly alias for a .NET type: int really is System.Int32.

int health = 100;
double speed = 5.5;
char grade = 'A';
bool isAlive = true;

Simple Types

"Primitive" — Use the Word Carefully

People borrow the word "primitive" from languages like Java, but in C# it's not quite right. C# has no formal "primitive" category — its simple types are really structs: full value types with methods of their own.

You can write 5.ToString() or read int.MaxValue; a true primitive couldn't do that. So the accurate term is simple type.

Types

The Four Numeric Types

Picking a number type is a trade between range (how big) and precision (how exact).

int

32-bit whole number, roughly ±2.1 billion. No suffix. Your default for counting — scores, lives, indices.

float

32-bit decimal, ~6–7 digits of precision. Suffix f. Unity's default for positions, speeds, distances.

double

64-bit decimal, ~15–16 digits. The default for any decimal literal. General-purpose maths.

decimal

128-bit, exact decimal, 28–29 digits. Suffix m. Money — no binary rounding error.

Numeric Types

Suffixes Decide the Type

A bare decimal literal is a double; a bare whole number is an int. The suffix tells the compiler you meant something else.

int   lives   = 3;
float gravity = -9.81f;   // f — without it, 9.81 is a double
double pi     = 3.14159;  // double by default
decimal price = 19.99m;   // m — exact, for currency

double pi = 3.14159;  // fine, 3.14159 is already double
float gravity = -9.81; // ERROR — can't assign double to float without losing precision
decimal price = 19.99; // ERROR — can't implicitly convert double to decimal

Integer division truncates (7 / 2 is 3), so promote to a decimal type when you need the fraction: 7 / 2.0 is 3.5.

Types

bool

buːl

1A value type that holds exactly one of two values — true or false.

e.g.A bool named isGameOver is false while the player is alive, and becomes true the moment they lose.

bool

The Foundation of Every Decision

A bool usually comes from a comparison, and it drives every conditional and loop your program runs.

bool isAlive  = true;
bool canEnter = health > 0;   // a comparison produces a bool

if (isAlive)
{
    Console.WriteLine("Keep playing");
}

Types

char

tʃɑːr

1A value type that holds exactly one character, written between single quotes.

e.g.'A', '7', '?' and ' ' (a space) are all valid char values.

char

Single Quotes, and a Number Underneath

A char is the building block strings are made of. Its literal uses single quotes — 'A' — while a string uses double quotes — "A". Don't mix them up.

Under the hood a char is a 16-bit Unicode (UTF-16) code unit — so each character is a number you can do arithmetic on.

char letter = 'A';
int code = letter;   // 65 — the Unicode value of 'A'

Types

enum

ˈiː.nʌm

1A value type that defines a named set of related constants, making code more readable and harder to get wrong.

e.g.enum Direction { North, East, South, West } lets you write Direction.North instead of a bare 0.

Enums

Declaration and Use

Declare the set of names, then use them by name. The code reads like English, and the compiler refuses any value outside the set.

enum Direction { North, East, South, West }

Direction facing = Direction.North;

if (facing == Direction.North)
{
    Console.WriteLine("Heading north");
}

Enums

Customising an Enum

Each member is really a number — by default an int starting at 0. You can set the values, change the underlying type, or mark it [Flags] to combine members like switches (give each a distinct power of two).

Chosen values & type

enum Difficulty : byte
{
    Easy   = 1,
    Normal = 2,
    Hard   = 4
}

Combinable flags

[Flags]
enum Toppings
{
    None   = 0,
    Cheese = 1,
    Bacon  = 2
}
var o = Toppings.Cheese | Toppings.Bacon;

Types

string — Text as a Reference Type

A string is a sequence of characters — player names, dialogue, anything textual. Unlike the numeric and char types it's a reference type: the variable holds a reference to the text, which lives elsewhere.

Yet it behaves like a simple value, because strings are immutable — once created they never change. You can share one freely without fear someone will alter it underneath you. (Full treatment comes in its own section later.)

string

Creating and Combining Strings

Write strings in double quotes. Join them with +, or — better — use $ interpolation to drop values straight in.

string name = "Aria";
string greeting = "Hello, " + name;
string line = $"{name} scored {1500} points";

That's enough to get going. Immutability, memory cost and StringBuilder get a section of their own.

Types

Arrays — A Fixed Row of Same-Typed Values

An array holds a fixed-size, ordered collection of values that all share one type — a row of numbered boxes. It's a reference type, so the variable points to the block of elements; copying the variable shares the data.

int[] scores = new int[3];   // three slots, all 0
scores[0] = 100;

string[] names = { "Aria", "Ben", "Cleo" };
Console.WriteLine(names[1]);   // Ben — indexing starts at 0

Recognise the type[] syntax and the zero-based indexing for now; arrays get a dedicated section later.

 

Operators

Section 07

The symbols that combine, compare and transform your values.

Gustave Doré engraving of a fierce charge — forces combining and acting on each other
They Fiercely Charged ForwardGustave Doré

Operators

What an Operator Is

An operator is a symbol that performs an action on one or more values — its operands. You've already met = and +. The common ones fall into a few families.

Operators

Arithmetic & Assignment

Arithmetic

int sum  = 5 + 3;   // 8</div>
      <div class="column"><h3>Assignment</h3>
<pre><code>int x = 10;</div>
    </div>
    int diff = 5 - 3;   // 2
int prod = 5 * 3;   // 15
int quot = 7 / 2;   // 3  (truncates)
int rem  = 7 % 2;   // 1  (remainder)
x += 5; // x = x + 5 -> 15 x -= 3; // -> 12 x *= 2; // -> 24 x++; // increment -> 25

% (modulo) gives the remainder of a division — perfect for "every Nth" logic and wrapping values. The compound forms (+=, *=, …) change a variable based on its own current value.

Operators

Comparison & Logical

Comparison — produces a bool

5 == 5   // equal           true</div>
      <div class="column"><h3>Logical — combine bools</h3>
<pre><code>hasKey && isUnlocked  // both</div>
    </div>
    5 != 3   // not equal       true
5 > 3    // greater         true
3 <= 2   // less or equal   false
isSafe || hasShield // either !isFriendly // flip

Comparisons are the source of nearly every condition you write. && and || short-circuit — they stop the moment the answer is certain, which also lets you guard against errors (obj != null && obj.IsReady).

Operators

Type Conversion

Often you need a value as a different type — an int used as a double, or text turned into a number. C# is strict about when this happens automatically and when you must ask for it.

Type Conversion

Implicit Widening vs Explicit Casting

Implicit — no data lost

A smaller type widens into a larger one automatically.

int whole = 7;
double precise = whole;  // 7 -> 7.0

Explicit — risk acknowledged

When a conversion might lose data, you must cast with (type).

double d = 9.8;
int n = (int)d;          // 9 — truncated

Casting between number types truncates; it doesn't round.

Type Conversion

Converting Text and Numbers

To move between text and numbers, use library methods rather than casts:

int n = int.Parse("42");               // string -> int (throws on bad input)
bool ok = int.TryParse("42", out n);   // safe: reports failure, never crashes
string s = 42.ToString();              // int -> string
double d = Convert.ToDouble("3.14");

Prefer TryParse for anything a user typed — it returns false instead of throwing.

Operators

Two Axes of Typing

Languages differ along two independent axes, and together they decide how many mistakes the compiler can catch for you:

  • Strong vs weak — a strongly typed language enforces types and won't silently treat text as a number; a weakly typed one lets types bend, which is flexible but error-prone.
  • Static vs dynamicstatic typing checks at compile time; dynamic typing checks while the program runs.

Two Axes of Typing

C# Is Strong and Static

Every variable has a known type at compile time, and illegal mixes are rejected before the program ever runs.

int x = 5;
x = "hello";   // compile error — caught immediately

A weakly typed language might have quietly allowed that and misbehaved much later. The trade is a little more typing up front, in exchange for fewer runtime surprises and far better tooling. For games, catching a type bug at compile time beats finding it during a live demo.

 

Functions

Section 08

Naming and reusing a block of logic — the first big step toward organised code.

Gustave Doré engraving of a parrot urging her on — a named helper called to act
The Parrot Urged Her OnGustave Doré

Functions

Function

ˈfʌŋk.ʃən

1A named, reusable block of code that takes optional inputs, performs a task, and optionally returns a result.

e.g.A Square function takes a number and returns it multiplied by itself — write it once, call it anywhere.

Functions

A Useful Pretence, For Now

Strictly, C# has no free-floating functions — every function belongs to a type and is called a method (we meet those properly in the OOP section).

But top-level statements let us declare local functions and call them as if they stood alone. It's a convenient pretence, so we can learn the idea before learning classes.

Functions

Parameters and Arguments

Parameters are the named inputs in a function's definition. Arguments are the actual values you pass when you call it.

void Greet(string name)   // 'name' is a parameter
{
    Console.WriteLine($"Hello, {name}");
}

Greet("Aria");   // "Aria" is the argument

Functions

Returning a Value — or Not

A function's return type declares what it hands back; return sends that value and ends the function. A function that acts but hands nothing back has the return type void.

Returns a value

int Square(int n)
{
    return n * n;
}
int r = Square(5);   // 25

void — does, returns nothing

void PrintScore(int score)
{
    Console.WriteLine($"Score: {score}");
}
PrintScore(1500);

Functions

Named and Optional Arguments

Pass arguments by name to make a call self-documenting and order-free. Give a parameter a default value and callers may omit it — optional parameters must come after all required ones.

void Attack(int damage, bool critical = false) { /* ... */ }

Attack(10);                       // critical defaults to false
Attack(10, critical: true);       // named — clear at the call site
Attack(damage: 25);               // order wouldn't matter

Functions

The params Keyword

params lets a function accept any number of arguments of a type, gathered into an array — great when the count isn't fixed.

int Sum(params int[] numbers)
{
    int total = 0;
    foreach (int n in numbers) total += n;
    return total;
}

Sum(1, 2, 3);          // 6
Sum(10, 20, 30, 40);   // 100

Functions

ref and out Parameters

Normally value-type arguments are copied. ref passes a variable by reference, so the function can change the caller's variable. out is similar, used to hand back extra results.

void Double(ref int n) { n *= 2; }

int x = 5;
Double(ref x);   // x is now 10

bool ok = int.TryParse("42", out int parsed);   // out returns the parsed value

Functions

Expression-Bodied Functions

When a function is a single expression, => gives a compact one-line form — no braces, no return.

int Square(int n) => n * n;
string Greet(string name) => $"Hello, {name}";

Identical behaviour to the block form, just tidier for short functions.

Functions

Recursion

flowchart TD
  A["Factorial(4)"] --> B["4 × Factorial(3)"]
  B --> C["3 × Factorial(2)"]
  C --> D["2 × Factorial(1)"]
  D --> E["1 — base case"]
  E -->|unwind| F["= 24"]

A recursive function calls itself to solve a smaller version of the same problem. Every recursion needs a base case that stops it — or it loops forever.

int Factorial(int n)</div>
    </div>
    {
    if (n <= 1) return 1;        // base case
    return n * Factorial(n - 1); // recursive step
}

Factorial(4);   // 24

 

Scope and Lifetime

Section 09

Where a name can be seen, and how long its value lives.

Gustave Doré engraving of a labyrinth — bounded regions where a name is visible
In Labyrinth of Many a RoundGustave Doré

Scope and Lifetime

Two Linked Ideas

Scope is where a name is visible; lifetime is how long its value exists in memory. The two move together — a variable usually lives exactly as long as the block it's declared in is running.

Outside its region, the compiler doesn't even know the name exists. That's what stops unrelated parts of a program from stepping on each other's names.

Scope and Lifetime

Local and Block Scope

Local to a function

void Play()</div>
      <div class="column"><h3>Local to a block</h3>
<pre><code>if (isAlive)</div>
    </div>
    {
    int score = 0;   // local to Play
    score += 10;
}   // score is gone here
{ int bonus = 50; // only inside { } score += bonus; } // bonus not accessible here

A block is any region between { } — a loop body, an if, or a bare pair of braces. Each function call gets its own fresh copy of its locals.

Scope and Lifetime

Shadowing

Shadowing is when an inner scope declares a name that already exists in an outer one, hiding it for that region. C# forbids it for locals to prevent confusion.

int value = 10;
{
    int value = 20;   // error — already declared in an enclosing scope
}

It can still happen between a field and a local — one more reason clear names matter.

 

Conditionals

Section 10

Letting a program choose between paths based on what's true.

Gustave Doré engraving of a forking road — choosing one branch over another
The Road to PerditionGustave Doré

Conditionals

Making Decisions

A conditional runs code only when a condition is true. It's the difference between a script that always does the same thing and one that reacts — the heart of every behaviour you'll write.

Conditionals

if / else if / else

flowchart TD
  A([score]) --> B{"score >= 90 ?"}
  B -->|yes| C["Gold"]
  B -->|no| D{"score >= 50 ?"}
  D -->|yes| E["Silver"]
  D -->|no| F["Try again"]

if runs its block when the condition is true. else if offers another test; else catches everything left. Only the first matching branch runs.

if (score >= 90)</div>
    </div>
    {
    Console.WriteLine("Gold");
}
else if (score >= 50)
{
    Console.WriteLine("Silver");
}
else
{
    Console.WriteLine("Try again");
}

Conditionals

The Ternary Operator

For a simple either/or that produces a value, the ternary operator ?: is a compact inline if/else. Read it as "condition ? value-if-true : value-if-false".

int health = 30;
string status = health > 0 ? "Alive" : "Dead";   // "Alive"

Perfect for short assignments. For anything with real branching, reach back for a full if.

Conditionals

Nested Conditionals

An if can live inside another, checking a second condition only once the first holds. Useful — but deep nesting hurts readability, so keep it shallow.

if (isLoggedIn)
{
    if (hasPermission)
    {
        OpenEditor();
    }
}

Conditionals

switch — Statement and Expression

When you compare one value against many fixed options, switch is cleaner than a long if/else if chain. The modern expression form returns a value directly.

switch statement

switch (direction)
{
    case "N": Move(0, 1);  break;
    case "S": Move(0, -1); break;
    default:  Stay();      break;
}

switch expression

string label = score switch
{
    >= 90 => "Gold",
    >= 50 => "Silver",
    _     => "Try again"
};

The expression form uses => arms and _ as the catch-all — concise for mapping one value to another.

 

Arrays

Section 11

Working with fixed-size collections of same-typed values, in depth.

Gustave Doré engraving of ranked towers — an ordered, fixed row of slots
Knocked Down Towers and FortressesGustave Doré

Arrays

Declaration and Initialization

You met arrays earlier; now the details. Size an array up front and fill it later, or initialize it with values immediately. Its length is fixed once created — it can't grow.

int[] a = new int[5];             // five zeros
int[] b = new int[] { 1, 2, 3 };  // sized from the values
int[] c = { 10, 20, 30 };         // shorthand for the same

Arrays

Accessing Elements

Reach an element by its index, counting from 0. The last index is Length - 1; going out of range throws at run time.

string[] names = { "Aria", "Ben", "Cleo" };
Console.WriteLine(names[0]);    // Aria
names[2] = "Cara";              // overwrite Cleo
Console.WriteLine(names.Length); // 3
// names[3] -> IndexOutOfRangeException

Arrays

Multi-Dimensional Arrays

Arrays can have more than one dimension — handy for grids, boards and tile maps. A 2-D array uses [,] and two indices: row and column.

int[,] grid = new int[2, 3];   // 2 rows, 3 columns
grid[0, 0] = 5;
grid[1, 2] = 9;

int[,] board = { { 1, 2 }, { 3, 4 } };

 

Loops

Section 12

Repeating work without repeating yourself.

Gustave Doré engraving of Munchausen pulling on his own braid — a self-referential repeating act
Pulled on My Own BraidGustave Doré

Loops

Doing It Again

A loop runs a block of code repeatedly — to process every item in a collection, count to a number, or keep a game running frame after frame. C# offers four, each suited to a different question.

Loops

for — a known count

flowchart TD
  A([Start]) --> B["i = 0"]
  B --> C{"i < 5 ?"}
  C -->|yes| D["print i"]
  D --> E["i++"]
  E --> C
  C -->|no| F([Done])

The for loop is ideal when you know how many times to repeat. It bundles three parts: a start value, a continue condition, and a step.

for (int i = 0; i < 5; i++)</div>
    </div>
    {
    Console.WriteLine(i);
}

Start at 0, keep going while i < 5, add 1 each pass.

Loops

while — an unknown count

flowchart TD
  A([Start]) --> B{"countdown > 0 ?"}
  B -->|yes| C["print countdown"]
  C --> D["countdown--"]
  D --> B
  B -->|no| E([Done])

A while loop repeats as long as a condition stays true, checking it before each pass. Use it when you don't know the count in advance.

int countdown = 3;</div>
    </div>
    while (countdown > 0)
{
    Console.WriteLine(countdown);
    countdown--;
}

Make sure something changes inside, or it never ends.

Loops

do-while — at least once

flowchart TD
  A([Start]) --> B["read input"]
  B --> C{"input != quit ?"}
  C -->|yes| B
  C -->|no| D([Done])

A do-while loop checks its condition after the body, so the body always runs at least once. Perfect for menus and input prompts.

string input;</div>
    </div>
    do
{
    input = Console.ReadLine();
}
while (input != "quit");

Loops

foreach — every item

flowchart TD
  A([Start]) --> B{"more items ?"}
  B -->|yes| C["name = next item"]
  C --> D["print name"]
  D --> B
  B -->|no| E([Done])

The foreach loop walks every item in a collection, with no index to manage. It's the cleanest way to read each element in turn.

string[] names = { "Aria", "Ben", "Cleo" };</div>
    </div>
    foreach (string name in names)
{
    Console.WriteLine(name);
}

Use it when you want each item but not its position.

Loops

Steering a Loop from Inside

Sometimes you need to control a loop mid-flight — leaving early, or skipping a single pass. Two keywords do it: break stops the loop entirely; continue skips to the next iteration.

Loop Control

break vs continue

break — stop the loop

for (int i = 0; i < 10; i++)</div>
      <div class="column"><h3>continue — skip this pass</h3>
<pre><code>for (int i = 0; i < 6; i++)</div>
    </div>
    {
    if (i == 5) break;
    Console.WriteLine(i);  // 0..4
}
{ if (i % 2 != 0) continue; Console.WriteLine(i); // 0,2,4 }

break leaves the loop the moment you've found what you need; continue ignores items that don't qualify and moves on.

Loop Control

Nested Loops

flowchart TD
  A([Start]) --> B{"row < 3 ?"}
  B -->|no| H([Done])
  B -->|yes| C["col = 0"]
  C --> D{"col < 3 ?"}
  D -->|yes| E["print row,col"]
  E --> F["col++"]
  F --> D
  D -->|no| G["row++"]
  G --> B

A loop inside another is a nested loop — the inner one runs fully for each pass of the outer. Essential for grids and tables. A break only exits the loop it's in.

for (int row = 0; row < 3; row++)</div>
    </div>
    {
    for (int col = 0; col < 3; col++)
    {
        Console.Write($"({row},{col}) ");
    }
    Console.WriteLine();
}

 

Memory

Section 13

A first look at where your data lives while a program runs.

Gustave Doré engraving of corpse candles — faint lights marking where things linger
Corpse CandlesGustave Doré

Memory

What Memory Is

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.

Memory

The Stack and the Heap

The Stack

Fast, orderly, automatic. Holds local variables and tracks which function is running. Grows and shrinks neatly as functions are called and return.

The Heap

A larger, flexible pool for data whose size or lifetime isn't fixed — the objects that reference types point to. Tended by the garbage collector.

This is the overview; we'll revisit both in depth once you've met classes.

Memory

Value and Reference Types, Made Physical

flowchart LR
  subgraph Stack
    v["hero — holds an address"]
  end
  subgraph Heap
    o["the Player object"]
  end
  v --> o

Here the value/reference split becomes concrete:

  • Value types typically sit right on the stack, holding their data inline.
  • Reference types live on the heap; the stack variable just holds a reference to them.

That's exactly why copying a value type duplicates the data, while copying a reference type shares it.

Memory

C# Manages Memory for You

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 automatically reclaims heap objects you're no longer using.

You focus on logic; the runtime handles cleanup. We'll see how the GC actually works in the later deep-dive section.

 

Introduction to OOP

Section 14

Organising a program around the "things" in your problem.

Gustave Doré engraving of a courting scene — objects relating to one another
CourtingGustave Doré

Introduction to OOP

Programming Paradigm

1A fundamental style for structuring programs and reasoning about them.

e.g.Procedural, object-oriented and functional are three of the most influential.

Programming Paradigms

Three Ways to Organise Code

Procedural

A sequence of steps and functions acting on loose data. Exactly what we've written so far — direct, but it drifts apart as it grows.

Object-Oriented

Bundles data and the behaviour acting on it into objects. Models real things — a Player, an Enemy — naturally.

Functional

Computation as evaluating functions, favouring immutable data and no hidden state. Predictable and testable.

C# is multi-paradigm: object-oriented at its core, fully procedural (your top-level statements), and happy to borrow functional ideas — lambdas, LINQ, immutability.

Introduction to OOP

What OOP Is — and Why

Object-oriented programming structures a program as a set of cooperating objects, each combining state (data) with behaviour (methods). It exists to tame the complexity that sinks large procedural programs:

  • Group related data and behaviour so they evolve together.
  • Hide internals, so changes stay local and safe.
  • Encourage reuse through inheritance and shared contracts.

Introduction to OOP

The Four Pillars

Encapsulation

Bundle data with the code that uses it, and hide the internals.

Inheritance

Build new types on existing ones, reusing what they offer.

Polymorphism

Treat different types through one interface, each behaving its own way.

Abstraction

Expose the essentials, hide needless detail.

Each pillar gets its own treatment in the sections ahead. C# implements them with class/struct, access modifiers, : for inheritance, virtual/override, and abstract/interface.

 

Classes and Objects

Section 15

The blueprint and the thing built from it — the heart of OOP.

Gustave Doré engraving of a giant — one grand blueprint made concrete
A GiantGustave Doré

Classes and Objects

Class vs Object

1A 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 houses built from it are three objects, each with its own address and paint colour.

Classes and Objects

Blueprint and Instance

The class — a template

class Player</div>
      <div class="column"><h3>The object — a real instance</h3>
<pre><code>Player hero = new Player();</div>
    </div>
    {
    public string Name;
    public int Health;

    public void TakeDamage(int amount)
    {
        Health -= amount;
    }
}
hero.Name = "Aria"; hero.Health = 100; hero.TakeDamage(30); // Health -> 70

Defining a class creates no data. new instantiates it — allocating one object on the heap and handing you a reference. Each new is independent.

Classes and Objects

The Top-Level Statements Reveal

Remember the compiler wrapping your 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.

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

Fields and Properties

An object's raw data lives in fields, but we usually expose it through properties — a gateway that looks like a field but can run code on read or write.

Field — raw storage

public int health;   // anyone can set
                     // any value, no control

Property — controlled gateway

private int health;
public int Health
{
    get => health;
    set => health = value < 0 ? 0 : value;
}

Rule of thumb: keep fields private, expose data through public properties.

Fields and Properties

Auto-Properties

When you need no custom logic, an auto-property gives the property syntax with a hidden backing field generated for you — the everyday default. The set receives the new value through the implicit value keyword; omit or restrict it to control access.

class Player
{
    public string Name { get; set; }
    public int Health { get; private set; }   // read-only from outside
}

Classes and Objects

Constructors

A constructor runs once, when you new an object, to set it up — guaranteeing it starts life valid. It's named after the class and has no return type. If you write none, C# supplies a hidden default constructor; the moment you write your own, that freebie disappears.

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

Constructor Chaining

One constructor can call another with : this(...), avoiding duplicated setup. This is 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 — Encapsulation Enforced

Access modifiers decide who can see each member, turning encapsulation from an idea into a rule the compiler enforces.

public

Visible to everyone — the deliberate, public-facing surface of a type.

private

Visible only inside the same class. The default, and right for internal details.

protected

Visible in the class and any class that inherits it. The bridge to descendants.

internal

Visible anywhere in the same assembly, but not to other projects.

Keep fields private and expose a small public surface: objects stay valid, internals stay free to change, and the type is easy to use correctly.

Classes and Objects

Static Members

Most members belong to an object. Static members belong to the class itself — one shared copy, usable with no object at all (Console.WriteLine, int.MaxValue).

class Enemy
{
    public static int Count;          // shared by all enemies
    public Enemy() { Count++; }

    public static void ResetCount() => Count = 0;
}

Ask: "is this about one object, or the whole type?" That answers whether it should be static. A class marked static can't be instantiated and holds only static members — a tidy home for stateless utilities.

Classes and Objects

Methods on a Class

A method is a function that lives inside a class — everything you learned about functions applies, plus it can act on its object's own data. Several methods can share a name through overloading, as long as their parameters differ; the compiler picks the right one.

class Player
{
    public int Health;
    public void Heal(int amount) => Health += amount;  // acts on this object
}

void Log(string message)            { /* ... */ }
void Log(int code)                  { /* ... */ }
void Log(string message, int code)  { /* ... */ }   // overloads

 

Structs

Section 16

Lightweight value types that look like classes but behave differently.

Gustave Doré engraving of a large toad — a small, self-contained creature of its own kind
Large ToadGustave Doré

Structs

Struct vs Class

struct — value type

Copied on assignment; each variable independent. Usually on the stack. No inheritance.

struct Point</div>
      <div class="column"><h3>class — reference type</h3>
<p>Shared by reference; copies point to the same object. Lives on the heap. Supports inheritance.</p>
<pre><code>Point a = new Point(1, 1);</div>
    </div>
    {
    public int X, Y;
    public Point(int x, int y)
    { X = x; Y = y; }
}
Point b = a; // a full copy b.X = 99; // a.X is still 1

A struct defines a custom type just like a class — fields, properties, methods — but that one difference (value vs reference) changes how it copies, stores and behaves.

Structs

When to Use — and Its Limits

Reach for a struct when data is…

  • Small — a handful of fields.
  • Logically one value — a coordinate, a colour.
  • Short-lived or immutable — copied cheaply, not shared.

Mind the limits

  • No inheritance.
  • Large structs are costly to copy.
  • Easy to mutate a copy by mistake.
  • Uninitialised fields are all-zeros, never null.

Unity's Vector3 and Color are structs for exactly these reasons. When in doubt, use a class.

 

Generics

Section 17

Writing code once that works safely for many types.

Gustave Doré engraving of Baron Munchausen
Baron MunchausenGustave Doré

Generics

One Placeholder, Many Types

Generics let you write a class or method with a type placeholder — written <T> by convention — filled in when it's used. You get reuse and full type safety: no casting, no losing track of types. You've used them every time you wrote List<...>.

List<int> numbers = new List<int>();      // T is int
List<string> names = new List<string>();  // T is string

Generics

Generic Classes and Methods

A generic class

class Box<T></div>
      <div class="column"><h3>A generic method</h3>
<pre><code>void Swap<T>(ref T a, ref T b)</div>
    </div>
    {
    private T contents;
    public void Put(T item) => contents = item;
    public T Get() => contents;
}

Box<int> b = new Box<int>();
b.Put(42);
{ T temp = a; a = b; b = temp; } int x = 1, y = 2; Swap(ref x, ref y); // T inferred as int

A method can be generic even when its class isn't — the type parameter goes right after the method name, and is often inferred from the arguments.

Generics

Type Constraints

Sometimes T must meet a requirement — be a class, have a constructor, 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: T must have a constructor
}

void Print<T>(T item) where T : IComparable { /* ... */ }

 

Memory in Depth

Section 18

Back to the stack, the heap and the garbage collector — now that we have objects.

Gustave Doré engraving of Roland's descent — going deeper into the layers below
Roland's DescentGustave Doré

Memory in Depth

The Stack and the Heap, Closely

The Stack

Grows and shrinks in strict last-in, first-out order. Each call pushes a frame of locals and parameters; returning pops it off, instantly freeing them. Blazing fast — allocation is just moving a pointer — but limited and tied to calls.

The Heap

A larger, flexible pool for data whose lifetime isn't tied to one call — every object made with new. Allocation is more involved, and memory stays alive as long as something references it.

Reference-type variables on the stack simply hold the address of their object on the heap.

Memory in Depth

Where Each Value Actually Lives

  • A local value type lives in the stack frame and dies with it.
  • A reference type lives on the heap; the stack holds only a reference to it.
  • A value type inside a class object lives on the heap too — wherever its owner lives.

So "value types are on the stack" is a useful simplification, not an absolute rule.

Memory in Depth

The Garbage Collection Cycle

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 Depth

Managed vs Manual

C / C++

You allocate and free by hand. Maximum control and speed — but mistakes cause leaks, dangling pointers and crashes.

C#

The runtime manages the heap; the GC frees memory automatically when an object becomes unreachable. Far safer and faster to write, at the cost of some control and occasional GC pauses.

That trade — safety and productivity over manual control — is central to why C# suits games and apps. The catch: collections take time, so in hot code paths we avoid creating needless garbage.

 

Collections

Section 19

Flexible, resizable containers for groups of data.

Gustave Doré engraving of a crowded tournament — a large gathered group
Deplorable Death of SavoisyGustave Doré

Collections

Why Collections Exist

Array

Fixed size, chosen at creation. Slightly faster and lighter. Best when the count is known and stable.

Collection

Resizes automatically, with rich helper methods. Best when items come and go over time.

Real programs rarely know their item count up front — inventories grow, enemies spawn and die. Collections (in System.Collections.Generic, all generic) handle that dynamism so you never manually resize and copy arrays.

Collections

List — the Workhorse

A List<T> is an ordered, resizable sequence of same-typed items — like an array that can grow and shrink, fully type-safe. If you reach for one collection by default, it's this.

List<string> party = new List<string> { "Aria", "Ben" };
party.Add("Cleo");          // append
party.Insert(0, "Zed");     // at a position
party.Remove("Aria");       // by value
party.RemoveAt(0);          // by index
Console.WriteLine(party.Count);

List

Iterating and Common Methods

Walk it

foreach (string name in party)
    Console.WriteLine(name);

for (int i = 0; i < party.Count; i++)
    Console.WriteLine($"{i}: {party[i]}");

Handy methods

  • Add / AddRange — append.
  • Remove / Clear — take out.
  • Contains / IndexOf — search.
  • Sort / Reverse — reorder.

Use foreach when you want each item, a for loop when you need the index.

Collections

Dictionary — Look Up by Key

A Dictionary<TKey, TValue> stores key → value pairs, so you fetch a value instantly by its key instead of scanning positions. Keys are unique; values needn't be.

Dictionary<string, int> scores = new Dictionary<string, int>
{
    ["Aria"] = 1500,
    ["Ben"]  = 1200
};

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

Iterating a 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

Queue, Stack, HashSet

Queue — FIFO

First in, first out, like a checkout line. Enqueue at the back, Dequeue from the front.

var q = new Queue<string>();</div>
      <div class="column"><h3>Stack — LIFO</h3>
<p>Last in, first out, like a stack of plates. <code>Push</code> on top, <code>Pop</code> from the top. Great for undo.</p>
<pre><code>var s = new Stack<string>();</div>
      <div class="column"><h3>HashSet — unique</h3>
<p>Unordered, unique values, with very fast membership checks. Adding a duplicate does nothing.</p>
<pre><code>var seen = new HashSet<string>();</div>
    </div>
    q.Enqueue("load");
q.Dequeue();  // "load"
s.Push("move1"); s.Pop(); // "move1" seen.Add("room1"); seen.Contains("room1"); // true

Pick the collection whose rules match your access pattern, and the code reads its own intent.

 

Inheritance and Polymorphism

Section 20

Building types on top of types, and letting them behave in their own way.

Gustave Doré engraving of an elder lady — an ancestor passing traits down the line
Older LadyGustave Doré

Inheritance

Base & Derived Class

1The base (parent) class is inherited from; the derived (child) class inherits and extends it.

e.g.Animal is the base; Dog is a derived class that gains Name and Eat() while adding Bark().

Inheritance

Building on What Exists

Inheritance lets one class build on another, reusing its data and behaviour and adding or changing what it needs. It models "is-a": a Dog is an Animal. Declare it with a colon — a class inherits exactly one base.

class Animal
{
    public string Name;
    public void Eat() => Console.WriteLine($"{Name} eats");
}

class Dog : Animal          // Dog is an Animal, plus more
{
    public void Bark() => Console.WriteLine("Woof");
}

A derived class inherits public, protected and internal members — but not private ones or constructors (though it can call them).

Inheritance

The base Keyword

base refers to the parent — use it to call the base constructor, or to invoke 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();                           // the normal attack...
        Console.WriteLine("...then a special move!");
    }
}

Inheritance & Polymorphism

virtual and override

virtual — "you may replace this"

class Animal</div>
      <div class="column"><h3>override — "here's my version"</h3>
<pre><code>class Dog : Animal</div>
    </div>
    {
    public virtual void Speak()
        => Console.WriteLine("...");
}
{ public override void Speak() => Console.WriteLine("Woof"); }

By default a derived class can't change an inherited method; virtual/override is the opt-in. Calls then run the derived version even through a base-typed variable — the basis of polymorphism. sealed does the opposite: it stops further inheritance or overriding.

Animal a = new Dog();
a.Speak();   // "Woof" — the Dog version runs

Inheritance & Polymorphism

Abstract Classes and Interfaces

abstract class — a partial base

Can't be instantiated; exists to be inherited. Holds shared fields and code, plus abstract methods (signature only) every subclass must implement.

abstract class Shape</div>
      <div class="column"><h3>interface — a pure contract</h3>
<p>No fields, (traditionally) no implementation — just members a type promises to provide. A class can implement <strong>many</strong>.</p>
<pre><code>interface IDamageable</div>
    </div>
    {
    public abstract double Area();   // no body
}
class Circle : Shape
{
    public double Radius;
    public override double Area()
        => 3.14159 * Radius * Radius;
}
{ void TakeDamage(int amount); int Health { get; } } class Player : Character, IDamageable, IMovable { /* implements all members */ }

Rule of thumb: an interface for a capability ("what can it do?"), an abstract class for a shared identity ("what is it?"). Multiple interfaces give C# the flexibility of multiple inheritance without its pitfalls.

Inheritance & Polymorphism

Polymorphism in Action

Polymorphism — "many forms" — lets one piece of code work with many types through a shared base or interface, each responding its own way. With virtual/override, C# decides at run time which method to call from the object's real type, not the variable's declared type.

List<Animal> zoo = new List<Animal> { new Dog(), new Cat() };
foreach (Animal a in zoo)
{
    a.Speak();   // Woof, then Meow — each correct, no type checks
}

It's what lets a render loop draw every IDrawable, a damage system hit anything IDamageable, a save system serialise every ISaveable — treating wildly different objects uniformly.

 

String in Depth

Section 21

Revisiting strings now that reference types and memory make sense.

Gustave Doré engraving of a wide flowing river — a long, continuous sequence
The River Was So WideGustave Doré

String in Depth

Immutable, and Why It Helps

We met string early as "text". Its real nature: a reference type whose data lives on the heap — yet immutable, so once created the characters never change. Operations that seem to modify a string actually return a new one.

string s = "hello";
s.ToUpper();      // returns "HELLO" but s is unchanged
s = s.ToUpper();  // reassign to keep the result

Immutability is why a string feels like a value: you can share one freely, certain no one will alter it underneath you.

String in Depth

The Cost of Concatenation

The problem

Because every change makes a new string, building text with + in a loop allocates a throwaway string each pass — extra work for the GC.

string result = "";</div>
      <div class="column"><h3>The fix — StringBuilder</h3>
<p>A mutable buffer you append to, producing the final string just once.</p>
<pre><code>var sb = new StringBuilder();</div>
    </div>
    for (int i = 0; i < 1000; i++)
{
    result += i;   // wasteful
}
for (int i = 0; i < 1000; i++) sb.Append(i); string result = sb.ToString();

Common methods — each returning a new string or a value: Length, ToUpper/ToLower, Trim, Substring, Replace, Split, Contains, IndexOf.

 

Exception Handling

Section 22

Dealing with the things that go wrong while a program runs.

Gustave Doré engraving of Satan in torment — something gone badly wrong, caught mid-fall
Satan Knew PainGustave Doré

Exception Handling

When Things Go Wrong

Even correct code meets bad input, missing files and broken connections. An exception is an object representing a runtime error: code throws one, and unless it's caught, it stops the program.

int[] a = { 1, 2, 3 };
Console.WriteLine(a[10]);   // throws IndexOutOfRangeException

Exception Handling

try / catch / finally

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

Common Exceptions

  • 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

Custom and Re-thrown Exceptions

Your own exception type

Inherit from Exception for errors specific to your program — self-describing and catchable on their own.

class OutOfManaException : Exception</div>
      <div class="column"><h3>Re-throwing</h3>
<p>Catch to log or react, then let it keep propagating with a bare <code>throw;</code> — preserving the original stack trace.</p>
<pre><code>try { LoadSave(); }</div>
    </div>
    {
    public OutOfManaException(string m)
        : base(m) { }
}

throw new OutOfManaException("Not enough mana");
catch (Exception ex) { Log(ex); throw; // not "throw ex;" — that loses the trace }

Exception Handling

The Exception Hierarchy

flowchart TD
    A[System.Exception] --> B[SystemException]
    A --> C[Your custom exceptions]
    B --> D[NullReferenceException]
    B --> E[IndexOutOfRangeException]
    B --> F[ArgumentException]
    F --> G[ArgumentNullException]

 

Week 1 Recap

Section 23

Looking back at the language — and ahead to the engine.

Gustave Doré engraving of an assault — pressing onward toward the next stage
The AssaultGustave Doré

Week 1 Recap

Everything We Covered

The language core

  • Foundations — programs, languages, how C# runs.
  • Types — value vs reference, the simple types, enums, strings, arrays.
  • Operators & conversion, and strong static typing.
  • Functions — parameters, returns, recursion.
  • Control flow — conditionals and loops.

Structure & memory

  • Scope, lifetime and the stack/heap.
  • OOP — classes, structs, the four pillars.
  • Inheritance & polymorphism, interfaces.
  • Generics and collections.
  • Exceptions for when things go wrong.

Week 1 Recap

Next — Into Unity

You now have the whole C# language under your belt: the types, the control flow, the objects and the memory model. That's the foundation everything in Unity is built on.

From here, the same C# you've written all week becomes game behaviour — scripts attached to objects in a scene, reacting frame by frame. Next module: the Unity engine itself.