Classes and Objects: Object-Oriented Programming

Object-oriented programming (OOP) is a programming paradigm that organizes code around objects rather than functions. In C# and Unity, almost everything is an object - your player character, enemies, weapons, UI elements, and even the game itself. Understanding classes and objects is essential for writing clean, maintainable game code.

In this chapter, you'll learn how to create classes, instantiate objects, and use OOP principles to organize your game code. By the end, you'll be able to structure your game projects using professional object-oriented design patterns.

What You'll Learn

  • Classes and objects - Understanding the relationship between blueprints and instances
  • Class syntax - How to declare and define classes in C#
  • Object instantiation - Creating instances of classes
  • Fields and properties - Storing data in objects
  • Methods in classes - Defining behavior for objects
  • Constructors - Initializing objects when they're created
  • Practical Unity examples - Real-world game development applications

Prerequisites

  • Completed Functions and Methods in C#
  • Understanding of variables, data types, and methods
  • Basic familiarity with Unity (optional, but helpful)

Understanding Classes and Objects

Think of a class as a blueprint and an object as a house built from that blueprint. You can use one blueprint to build many houses, and each house is independent - changing one doesn't affect the others.

Real-World Analogy:

  • Class = Cookie cutter (the template)
  • Object = Individual cookie (the instance created from the template)

In Game Development:

  • Class = Enemy class (defines what an enemy is)
  • Object = goblin1, goblin2, goblin3 (individual enemy instances)

Why OOP Matters in Games

Code Organization:

  • Group related data and behavior together
  • Create reusable components
  • Model real-world game entities naturally

Maintainability:

  • Fix bugs in one place (the class)
  • Update behavior for all instances at once
  • Easier to understand and modify code

Scalability:

  • Add new features without breaking existing code
  • Create variations easily (different enemy types)
  • Build complex systems from simple building blocks

Creating Your First Class

A class is declared using the class keyword followed by the class name. By convention, class names use PascalCase (first letter of each word capitalized).

Basic Class Syntax

class Player
{
    // Fields (data)
    public string name;
    public int health;
    public int level;

    // Methods (behavior)
    public void TakeDamage(int damage)
    {
        health -= damage;
    }

    public void LevelUp()
    {
        level++;
        health = 100; // Restore health on level up
    }
}

Key Components:

  • class Player - Class declaration
  • Fields - Variables that store data (name, health, level)
  • Methods - Functions that define behavior (TakeDamage, LevelUp)
  • public - Access modifier (makes fields/methods accessible from outside)

Creating Objects (Instantiation)

Once you have a class, you can create objects (instances) from it using the new keyword.

// Create a new Player object
Player player1 = new Player();
player1.name = "Hero";
player1.health = 100;
player1.level = 1;

// Create another Player object
Player player2 = new Player();
player2.name = "Warrior";
player2.health = 120;
player2.level = 5;

Important Points:

  • Each object is independent
  • player1 and player2 have separate data
  • Changing player1.health doesn't affect player2.health

Fields and Properties

Fields store data in your objects. Properties provide controlled access to fields with additional logic.

Using Fields

class Enemy
{
    public string name;
    public int health;
    public float speed;
    public bool isAlive;
}

// Usage
Enemy goblin = new Enemy();
goblin.name = "Goblin";
goblin.health = 50;
goblin.speed = 3.5f;
goblin.isAlive = true;

Using Properties

Properties allow you to add validation and logic when getting or setting values.

class Player
{
    private int health; // Private field (not accessible from outside)

    // Property with validation
    public int Health
    {
        get { return health; }
        set 
        { 
            // Ensure health never goes below 0 or above 100
            if (value < 0)
                health = 0;
            else if (value > 100)
                health = 100;
            else
                health = value;
        }
    }
}

// Usage
Player player = new Player();
player.Health = 150; // Automatically clamped to 100
player.Health = -10; // Automatically clamped to 0

Property Benefits:

  • Control how data is accessed
  • Add validation and error checking
  • Maintain data integrity
  • Hide implementation details

Methods in Classes

Methods define what objects can do. They can access the object's fields and modify them.

Instance Methods

class Weapon
{
    public string name;
    public int damage;
    public int durability;

    // Method that uses the object's data
    public void Attack(Enemy target)
    {
        target.health -= damage;
        durability--; // Weapon wears down

        if (durability <= 0)
        {
            Console.WriteLine($"{name} broke!");
        }
    }

    public void Repair()
    {
        durability = 100;
        Console.WriteLine($"{name} repaired!");
    }
}

// Usage
Weapon sword = new Weapon();
sword.name = "Iron Sword";
sword.damage = 25;
sword.durability = 100;

Enemy enemy = new Enemy();
enemy.health = 50;

sword.Attack(enemy); // enemy.health is now 25

Constructors

Constructors are special methods that run when an object is created. They initialize the object's fields.

Basic Constructor

class Player
{
    public string name;
    public int health;
    public int level;

    // Constructor - same name as class, no return type
    public Player(string playerName, int startingHealth)
    {
        name = playerName;
        health = startingHealth;
        level = 1;
    }
}

// Usage - constructor is called automatically
Player player = new Player("Hero", 100);
// player.name = "Hero"
// player.health = 100
// player.level = 1

Constructor Overloading

You can have multiple constructors with different parameters.

class Enemy
{
    public string name;
    public int health;
    public int damage;

    // Constructor 1: Full initialization
    public Enemy(string enemyName, int enemyHealth, int enemyDamage)
    {
        name = enemyName;
        health = enemyHealth;
        damage = enemyDamage;
    }

    // Constructor 2: Default damage
    public Enemy(string enemyName, int enemyHealth)
    {
        name = enemyName;
        health = enemyHealth;
        damage = 10; // Default damage
    }

    // Constructor 3: Default enemy
    public Enemy()
    {
        name = "Generic Enemy";
        health = 50;
        damage = 5;
    }
}

// Usage
Enemy boss = new Enemy("Dragon", 500, 50);
Enemy goblin = new Enemy("Goblin", 30); // Uses default damage
Enemy generic = new Enemy(); // Uses all defaults

Unity Game Development Examples

Player Controller Class

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // Fields
    public float moveSpeed = 5.0f;
    public float jumpForce = 10.0f;
    private Rigidbody2D rb;
    private bool isGrounded;

    // Constructor-like initialization (Unity uses Start instead)
    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        isGrounded = true;
    }

    // Methods
    void Update()
    {
        HandleMovement();
        HandleJump();
    }

    void HandleMovement()
    {
        float horizontal = Input.GetAxis("Horizontal");
        rb.velocity = new Vector2(horizontal * moveSpeed, rb.velocity.y);
    }

    void HandleJump()
    {
        if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
        {
            rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
            isGrounded = false;
        }
    }

    void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isGrounded = true;
        }
    }
}

Enemy Class

using UnityEngine;

public class Enemy : MonoBehaviour
{
    // Fields
    public string enemyName = "Enemy";
    public int health = 100;
    public int damage = 10;
    public float moveSpeed = 2.0f;

    private Transform player;
    private bool isAlive = true;

    void Start()
    {
        // Find the player
        GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
        if (playerObject != null)
        {
            player = playerObject.transform;
        }
    }

    void Update()
    {
        if (isAlive && player != null)
        {
            MoveTowardsPlayer();
        }
    }

    void MoveTowardsPlayer()
    {
        Vector2 direction = (player.position - transform.position).normalized;
        transform.Translate(direction * moveSpeed * Time.deltaTime);
    }

    public void TakeDamage(int damageAmount)
    {
        health -= damageAmount;

        if (health <= 0)
        {
            Die();
        }
    }

    void Die()
    {
        isAlive = false;
        Debug.Log($"{enemyName} has been defeated!");
        Destroy(gameObject);
    }
}

Item Class

using UnityEngine;

public class Item : MonoBehaviour
{
    // Fields
    public string itemName;
    public string description;
    public int value;
    public bool isConsumable;

    // Constructor (called when object is created)
    public Item(string name, string desc, int itemValue, bool consumable)
    {
        itemName = name;
        description = desc;
        value = itemValue;
        isConsumable = consumable;
    }

    // Method to use the item
    public virtual void Use(Player player)
    {
        Debug.Log($"Using {itemName}: {description}");

        if (isConsumable)
        {
            // Item is consumed after use
            Destroy(gameObject);
        }
    }
}

// Usage in another script
public class Inventory : MonoBehaviour
{
    void PickUpItem(Item item)
    {
        Debug.Log($"Picked up: {item.itemName}");
        // Add to inventory logic here
    }
}

Access Modifiers

Access modifiers control where your class members can be accessed from.

Public

public class Player
{
    public string name; // Accessible from anywhere
    public int health;  // Accessible from anywhere
}

Private

public class Player
{
    private int experience; // Only accessible within this class
    private void CalculateLevel() // Only accessible within this class
    {
        // Internal logic
    }
}

Protected

public class Player
{
    protected int health; // Accessible in this class and derived classes
}

public class Warrior : Player
{
    void Heal()
    {
        health = 100; // Can access protected member
    }
}

Best Practices

Naming Conventions

// Good: Clear, descriptive names
class PlayerController { }
class EnemyAI { }
class WeaponSystem { }

// Bad: Vague or unclear names
class PC { }
class E { }
class WS { }

Single Responsibility Principle

Each class should have one clear purpose.

// Good: Focused classes
class PlayerMovement { } // Handles movement only
class PlayerCombat { }    // Handles combat only
class PlayerInventory { } // Handles inventory only

// Bad: One class doing everything
class Player { } // Handles movement, combat, inventory, UI, etc.

Encapsulation

Hide internal details and expose only what's necessary.

// Good: Controlled access
class Player
{
    private int health; // Hidden

    public int Health // Controlled access
    {
        get { return health; }
        set { health = Mathf.Clamp(value, 0, 100); }
    }
}

// Bad: Everything exposed
class Player
{
    public int health; // Anyone can modify directly
}

Common Mistakes to Avoid

Mistake 1: Forgetting to Use new

// Wrong: This creates a reference, not an object
Player player;
player.name = "Hero"; // Error: Object reference not set

// Correct: Create the object first
Player player = new Player();
player.name = "Hero";

Mistake 2: Modifying Class Instead of Object

// Wrong: Trying to modify the class
Player.name = "Hero"; // Error: Cannot access instance member

// Correct: Modify the object instance
Player player = new Player();
player.name = "Hero";

Mistake 3: Not Initializing Fields

// Wrong: Using uninitialized fields
class Enemy
{
    public int health; // Not initialized
}

Enemy enemy = new Enemy();
enemy.health += 10; // health is 0, so result is 10 (might be unexpected)

// Correct: Initialize in constructor
class Enemy
{
    public int health;

    public Enemy()
    {
        health = 50; // Initialize to default value
    }
}

Practical Exercise

Create a GameCharacter class with the following requirements:

  1. Fields:

    • name (string)
    • health (int)
    • maxHealth (int)
    • level (int)
  2. Methods:

    • TakeDamage(int damage) - Reduces health
    • Heal(int amount) - Increases health (but not above maxHealth)
    • LevelUp() - Increases level and restores health to max
  3. Constructor:

    • Takes name and maxHealth as parameters
    • Initializes health to maxHealth and level to 1

Solution:

class GameCharacter
{
    public string name;
    public int health;
    public int maxHealth;
    public int level;

    public GameCharacter(string characterName, int characterMaxHealth)
    {
        name = characterName;
        maxHealth = characterMaxHealth;
        health = maxHealth;
        level = 1;
    }

    public void TakeDamage(int damage)
    {
        health -= damage;
        if (health < 0)
            health = 0;
    }

    public void Heal(int amount)
    {
        health += amount;
        if (health > maxHealth)
            health = maxHealth;
    }

    public void LevelUp()
    {
        level++;
        health = maxHealth;
    }
}

Next Steps

Now that you understand classes and objects, you're ready to explore more advanced OOP concepts:

  • Inheritance - Creating new classes based on existing ones
  • Polymorphism - Using objects of different types through a common interface
  • Encapsulation - Protecting data and controlling access
  • Abstraction - Hiding complex implementation details

Move on to Inheritance and Polymorphism to learn how to create class hierarchies and build more complex game systems.

Summary

  • Classes are blueprints that define what objects are
  • Objects are instances created from classes
  • Fields store data in objects
  • Properties provide controlled access to fields
  • Methods define what objects can do
  • Constructors initialize objects when they're created
  • Access modifiers control where class members can be accessed from
  • OOP helps organize code, improve maintainability, and model game entities naturally

Understanding classes and objects is fundamental to C# programming and Unity game development. Practice creating your own classes and objects to solidify these concepts before moving on to more advanced topics.