Inheritance and Polymorphism

Inheritance and polymorphism are powerful object-oriented programming concepts that let you create flexible, reusable code. In game development, inheritance helps you build class hierarchies for game entities (like different enemy types), while polymorphism allows you to treat different objects uniformly (like processing all enemies the same way, even though they behave differently).

In this chapter, you'll learn how to use inheritance to create class hierarchies, override methods for custom behavior, and leverage polymorphism to write flexible, maintainable game code. By the end, you'll be able to design game systems that are easy to extend and modify.

What You'll Learn

  • Inheritance - Creating new classes based on existing ones
  • Base classes and derived classes - Understanding parent-child relationships
  • Method overriding - Customizing behavior in derived classes
  • Polymorphism - Using objects of different types through a common interface
  • Virtual and abstract methods - Controlling how methods can be overridden
  • Practical Unity examples - Real-world game development applications

Prerequisites


Understanding Inheritance

Inheritance allows you to create a new class based on an existing class. The new class (called the derived class or child class) inherits all the fields, properties, and methods from the existing class (called the base class or parent class).

Real-World Analogy:

  • Base Class = Vehicle (has wheels, engine, can move)
  • Derived Classes = Car, Motorcycle, Truck (inherit from Vehicle, add specific features)

In Game Development:

  • Base Class = Enemy (has health, can take damage, can attack)
  • Derived Classes = Goblin, Dragon, Skeleton (inherit from Enemy, add unique behaviors)

Why Inheritance Matters in Games

Code Reusability:

  • Write common code once in the base class
  • Derived classes automatically get all base class functionality
  • Add specific features only where needed

Maintainability:

  • Fix bugs in one place (the base class)
  • Update behavior for all derived classes at once
  • Easier to understand class relationships

Flexibility:

  • Create variations easily (different enemy types)
  • Extend functionality without modifying existing code
  • Build complex systems from simple building blocks

Basic Inheritance Syntax

Inheritance is declared using a colon (:) after the class name, followed by the base class name.

Simple Inheritance Example

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

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

    public virtual void Die()
    {
        Console.WriteLine($"{name} has been defeated!");
    }
}

// Derived class (inherits from Enemy)
class Goblin : Enemy
{
    public int speed;

    // Goblin-specific method
    public void RunAway()
    {
        Console.WriteLine($"{name} runs away!");
    }
}

// Usage
Goblin goblin = new Goblin();
goblin.name = "Goblin Warrior";
goblin.health = 50;
goblin.damage = 10;
goblin.speed = 5;

// Can use base class methods
goblin.TakeDamage(20); // health is now 30
goblin.RunAway(); // Goblin-specific method

Key Points:

  • Goblin inherits all members from Enemy
  • Goblin can add its own members (speed, RunAway())
  • Goblin can use base class methods (TakeDamage(), Die())

Method Overriding

Method overriding allows a derived class to provide its own implementation of a method defined in the base class.

Virtual and Override Keywords

class Enemy
{
    public string name;
    public int health;

    // Virtual method - can be overridden
    public virtual void Attack()
    {
        Console.WriteLine($"{name} attacks!");
    }

    public virtual void Die()
    {
        Console.WriteLine($"{name} has been defeated!");
    }
}

class Goblin : Enemy
{
    // Override base class method
    public override void Attack()
    {
        Console.WriteLine($"{name} swings a rusty sword!");
    }

    public override void Die()
    {
        Console.WriteLine($"{name} drops some gold and runs away!");
    }
}

class Dragon : Enemy
{
    public override void Attack()
    {
        Console.WriteLine($"{name} breathes fire!");
    }

    public override void Die()
    {
        Console.WriteLine($"{name} roars one last time and collapses!");
    }
}

// Usage
Goblin goblin = new Goblin();
goblin.name = "Goblin";
goblin.Attack(); // "Goblin swings a rusty sword!"

Dragon dragon = new Dragon();
dragon.name = "Fire Dragon";
dragon.Attack(); // "Fire Dragon breathes fire!"

Key Points:

  • virtual keyword in base class allows overriding
  • override keyword in derived class replaces base implementation
  • Each derived class can have different behavior

Abstract Classes and Methods

Abstract classes cannot be instantiated directly - they're meant to be inherited from. Abstract methods must be implemented by derived classes.

Abstract Class Example

// Abstract base class
abstract class Enemy
{
    public string name;
    public int health;

    // Abstract method - must be implemented by derived classes
    public abstract void Attack();

    // Regular method - can be used by all derived classes
    public void TakeDamage(int amount)
    {
        health -= amount;
        if (health <= 0)
        {
            Die();
        }
    }

    // Virtual method - can be overridden
    public virtual void Die()
    {
        Console.WriteLine($"{name} has been defeated!");
    }
}

// Derived class must implement abstract method
class Goblin : Enemy
{
    public override void Attack()
    {
        Console.WriteLine($"{name} swings a sword!");
    }
}

class Dragon : Enemy
{
    public override void Attack()
    {
        Console.WriteLine($"{name} breathes fire!");
    }
}

// Usage
Goblin goblin = new Goblin(); // OK - Goblin is not abstract
// Enemy enemy = new Enemy(); // ERROR - Cannot instantiate abstract class

Key Points:

  • abstract class cannot be instantiated
  • abstract methods must be implemented in derived classes
  • Abstract classes can have both abstract and regular methods

Understanding Polymorphism

Polymorphism allows you to treat objects of different types through a common interface. You can use a base class reference to point to derived class objects.

Polymorphism Example

class Enemy
{
    public string name;
    public int health;

    public virtual void Attack()
    {
        Console.WriteLine($"{name} attacks!");
    }
}

class Goblin : Enemy
{
    public override void Attack()
    {
        Console.WriteLine($"{name} swings a sword!");
    }
}

class Dragon : Enemy
{
    public override void Attack()
    {
        Console.WriteLine($"{name} breathes fire!");
    }
}

// Polymorphism in action
Enemy[] enemies = new Enemy[]
{
    new Goblin { name = "Goblin Warrior", health = 50 },
    new Dragon { name = "Fire Dragon", health = 500 },
    new Goblin { name = "Goblin Archer", health = 30 }
};

// Process all enemies the same way
foreach (Enemy enemy in enemies)
{
    enemy.Attack(); // Calls the appropriate Attack() method for each type
}
// Output:
// Goblin Warrior swings a sword!
// Fire Dragon breathes fire!
// Goblin Archer swings a sword!

Key Points:

  • Base class reference can point to derived class objects
  • Method calls use the derived class implementation
  • Same code works with different types

Access Modifiers and Inheritance

Access modifiers control what derived classes can access from the base class.

Protected Access Modifier

class Enemy
{
    private int health; // Only accessible within Enemy class
    protected int maxHealth; // Accessible in Enemy and derived classes

    public int Health
    {
        get { return health; }
        set { health = value; }
    }
}

class Goblin : Enemy
{
    public void Heal()
    {
        // health = 100; // ERROR - health is private
        maxHealth = 100; // OK - maxHealth is protected
        Health = maxHealth; // OK - Health is public
    }
}

Access Modifiers:

  • private - Only accessible within the same class
  • protected - Accessible in the same class and derived classes
  • public - Accessible everywhere
  • internal - Accessible within the same assembly

Base Keyword

The base keyword allows you to call methods or access members from the base class.

Using Base Keyword

class Enemy
{
    protected string name;
    protected int health;

    public virtual void TakeDamage(int amount)
    {
        health -= amount;
        Console.WriteLine($"{name} takes {amount} damage!");
    }

    public virtual void Die()
    {
        Console.WriteLine($"{name} has been defeated!");
    }
}

class ArmoredEnemy : Enemy
{
    private int armor;

    public ArmoredEnemy()
    {
        armor = 10;
    }

    // Override but also call base implementation
    public override void TakeDamage(int amount)
    {
        int actualDamage = amount - armor;
        if (actualDamage < 0) actualDamage = 0;

        // Call base class method
        base.TakeDamage(actualDamage);

        Console.WriteLine($"Armor absorbed {armor} damage!");
    }

    public override void Die()
    {
        base.Die(); // Call base Die() method
        Console.WriteLine("Armor clatters to the ground!");
    }
}

// Usage
ArmoredEnemy armored = new ArmoredEnemy();
armored.name = "Armored Knight";
armored.health = 100;
armored.TakeDamage(25);
// Output:
// Armored Knight takes 15 damage!
// Armor absorbed 10 damage!

Key Points:

  • base keyword calls base class methods
  • Useful when overriding but still want base behavior
  • Can call base constructor with base()

Constructors and Inheritance

Derived classes can call base class constructors using base().

Constructor Inheritance

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

    // Base class constructor
    public Enemy(string enemyName, int enemyHealth, int enemyDamage)
    {
        name = enemyName;
        health = enemyHealth;
        damage = enemyDamage;
    }
}

class Goblin : Enemy
{
    private int speed;

    // Derived class constructor calls base constructor
    public Goblin(string goblinName, int goblinHealth, int goblinDamage, int goblinSpeed)
        : base(goblinName, goblinHealth, goblinDamage)
    {
        speed = goblinSpeed;
    }
}

// Usage
Goblin goblin = new Goblin("Goblin Warrior", 50, 10, 5);
// Base class constructor initializes: name, health, damage
// Derived class constructor initializes: speed

Unity Game Development Examples

Enemy System with Inheritance

using UnityEngine;

// Base enemy class
public abstract class Enemy : MonoBehaviour
{
    protected string enemyName;
    protected int health;
    protected int maxHealth;
    protected int damage;
    protected float moveSpeed;

    protected bool isAlive = true;

    protected virtual void Start()
    {
        maxHealth = health;
    }

    public virtual void TakeDamage(int amount)
    {
        health -= amount;

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

    public abstract void Attack(); // Must be implemented by derived classes

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

    protected virtual void Move()
    {
        // Basic movement - can be overridden
        transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
    }
}

// Melee enemy
public class MeleeEnemy : Enemy
{
    private Transform player;
    private float attackRange = 2f;

    protected override void Start()
    {
        base.Start();
        enemyName = "Melee Fighter";
        health = 100;
        damage = 15;
        moveSpeed = 3f;

        GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
        if (playerObject != null)
        {
            player = playerObject.transform;
        }
    }

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

            if (Vector3.Distance(transform.position, player.position) <= attackRange)
            {
                Attack();
            }
        }
    }

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

    public override void Attack()
    {
        Debug.Log($"{enemyName} swings a sword!");
        // Attack logic here
    }
}

// Ranged enemy
public class RangedEnemy : Enemy
{
    private Transform player;
    private float attackRange = 10f;
    public GameObject projectilePrefab;

    protected override void Start()
    {
        base.Start();
        enemyName = "Archer";
        health = 60;
        damage = 20;
        moveSpeed = 2f;

        GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
        if (playerObject != null)
        {
            player = playerObject.transform;
        }
    }

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

            if (Vector3.Distance(transform.position, player.position) <= attackRange)
            {
                Attack();
            }
        }
    }

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

    public override void Attack()
    {
        Debug.Log($"{enemyName} fires an arrow!");

        if (projectilePrefab != null)
        {
            GameObject projectile = Instantiate(projectilePrefab, transform.position, transform.rotation);
            // Set projectile direction, speed, etc.
        }
    }
}

Item System with Inheritance

using UnityEngine;

// Base item class
public abstract class Item : MonoBehaviour
{
    public string itemName;
    public string description;
    public int value;

    public abstract void Use(Player player); // Must be implemented

    public virtual void PickUp(Player player)
    {
        Debug.Log($"Picked up: {itemName}");
        player.AddToInventory(this);
    }
}

// Consumable item
public class ConsumableItem : Item
{
    public int healingAmount;

    public override void Use(Player player)
    {
        player.Heal(healingAmount);
        Debug.Log($"Used {itemName} - Restored {healingAmount} health!");
        Destroy(gameObject);
    }
}

// Weapon item
public class Weapon : Item
{
    public int attackDamage;
    public float attackSpeed;

    public override void Use(Player player)
    {
        player.EquipWeapon(this);
        Debug.Log($"Equipped {itemName}!");
    }
}

// Quest item
public class QuestItem : Item
{
    public string questId;

    public override void Use(Player player)
    {
        player.CompleteQuestObjective(questId);
        Debug.Log($"Quest item {itemName} used!");
    }
}

Best Practices

Favor Composition Over Inheritance

Sometimes, composition (has-a relationship) is better than inheritance (is-a relationship).

// Good: Composition
class Enemy
{
    private HealthSystem health;
    private MovementSystem movement;
    private AttackSystem attack;

    // Use systems instead of inheriting
}

// Less flexible: Deep inheritance
class Enemy { }
class FlyingEnemy : Enemy { }
class FlyingBossEnemy : FlyingEnemy { } // Gets complex quickly

Use Abstract Classes for Common Behavior

// Good: Abstract class with common behavior
abstract class Enemy
{
    protected int health; // Common to all enemies

    public void TakeDamage(int amount) // Common behavior
    {
        health -= amount;
    }

    public abstract void Attack(); // Each enemy attacks differently
}

Keep Inheritance Hierarchies Shallow

// Good: Shallow hierarchy
class Enemy { }
class MeleeEnemy : Enemy { }
class RangedEnemy : Enemy { }

// Avoid: Deep hierarchy
class Enemy { }
class FlyingEnemy : Enemy { }
class FlyingBossEnemy : FlyingEnemy { }
class FlyingBossEnemyElite : FlyingBossEnemy { } // Too deep!

Common Mistakes to Avoid

Mistake 1: Overusing Inheritance

Wrong:

class Player { }
class Warrior : Player { }
class Mage : Player { }
class Archer : Player { }
// Too many classes for simple variations

Better:

class Player
{
    public PlayerClass playerClass; // Use enum or composition
    // Handle differences with if/switch or strategy pattern
}

Mistake 2: Not Using Virtual/Override Correctly

Wrong:

class Enemy
{
    public void Attack() // Not virtual
    {
        Console.WriteLine("Attack!");
    }
}

class Goblin : Enemy
{
    public void Attack() // Hides base method, doesn't override
    {
        Console.WriteLine("Sword swing!");
    }
}

Enemy enemy = new Goblin();
enemy.Attack(); // Calls Enemy.Attack(), not Goblin.Attack()!

Correct:

class Enemy
{
    public virtual void Attack() // Virtual
    {
        Console.WriteLine("Attack!");
    }
}

class Goblin : Enemy
{
    public override void Attack() // Override
    {
        Console.WriteLine("Sword swing!");
    }
}

Enemy enemy = new Goblin();
enemy.Attack(); // Calls Goblin.Attack() correctly!

Mistake 3: Forgetting Base Constructor Calls

Wrong:

class Enemy
{
    protected string name;
    public Enemy(string n) { name = n; }
}

class Goblin : Enemy
{
    public Goblin() { } // ERROR - base constructor requires parameter
}

Correct:

class Goblin : Enemy
{
    public Goblin() : base("Goblin") { } // Call base constructor
}

Practical Exercise

Create a class hierarchy for game characters:

  1. Base Class: Character

    • Fields: name, health, maxHealth, level
    • Methods: TakeDamage(), Heal(), LevelUp()
    • Abstract method: Attack()
  2. Derived Classes:

    • Warrior - High health, melee attacks
    • Mage - Low health, magic attacks
    • Rogue - Medium health, quick attacks
  3. Requirements:

    • Each class must implement Attack() differently
    • Use polymorphism to process all characters uniformly
    • Override LevelUp() to give class-specific bonuses

Solution:

abstract class Character
{
    protected string name;
    protected int health;
    protected int maxHealth;
    protected int level;

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

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

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

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

    public abstract void Attack();
}

class Warrior : Character
{
    public Warrior(string name) : base(name, 150) { }

    public override void Attack()
    {
        Console.WriteLine($"{name} swings a mighty sword!");
    }

    public override void LevelUp()
    {
        base.LevelUp();
        maxHealth += 20; // Warriors get extra health
        health = maxHealth;
    }
}

class Mage : Character
{
    public Mage(string name) : base(name, 80) { }

    public override void Attack()
    {
        Console.WriteLine($"{name} casts a fireball!");
    }

    public override void LevelUp()
    {
        base.LevelUp();
        // Mages get special abilities on level up
        Console.WriteLine($"{name} learned a new spell!");
    }
}

class Rogue : Character
{
    public Rogue(string name) : base(name, 100) { }

    public override void Attack()
    {
        Console.WriteLine($"{name} strikes from the shadows!");
    }
}

Next Steps

Now that you understand inheritance and polymorphism, you're ready to explore more advanced C# features:

  • Collections - Arrays, Lists, and Dictionaries for managing groups of objects
  • Error Handling - Try-Catch blocks and exception handling
  • File I/O - Saving and loading game data
  • Advanced C# Features - LINQ, async/await, and more

Move on to Collections: Arrays, Lists, and Dictionaries to learn how to manage groups of objects efficiently.

Summary

  • Inheritance allows you to create new classes based on existing ones
  • Derived classes inherit all members from base classes
  • Method overriding lets derived classes customize behavior
  • Polymorphism allows treating different types uniformly
  • Abstract classes cannot be instantiated and define contracts
  • Virtual methods can be overridden, abstract methods must be implemented
  • Base keyword calls base class methods and constructors
  • Inheritance helps create flexible, maintainable game systems

Understanding inheritance and polymorphism is essential for building complex game systems. These concepts allow you to create hierarchies of game entities, reuse code effectively, and write flexible systems that are easy to extend and modify.