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
- Completed Classes and Objects: Object-Oriented Programming
- Understanding of classes, objects, and methods
- Basic familiarity with Unity (optional, but helpful)
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:
Goblininherits all members fromEnemyGoblincan add its own members (speed,RunAway())Goblincan 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:
virtualkeyword in base class allows overridingoverridekeyword 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:
abstractclass cannot be instantiatedabstractmethods 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 classprotected- Accessible in the same class and derived classespublic- Accessible everywhereinternal- 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:
basekeyword 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:
-
Base Class:
Character- Fields:
name,health,maxHealth,level - Methods:
TakeDamage(),Heal(),LevelUp() - Abstract method:
Attack()
- Fields:
-
Derived Classes:
Warrior- High health, melee attacksMage- Low health, magic attacksRogue- Medium health, quick attacks
-
Requirements:
- Each class must implement
Attack()differently - Use polymorphism to process all characters uniformly
- Override
LevelUp()to give class-specific bonuses
- Each class must implement
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.