Skip to main content

5.8 - Inheritance

Inheritance is one of the four pillars of Object-Oriented Programming (along with encapsulation, abstraction, and polymorphism). It allows you to create new classes that reuse, extend, and modify the behavior defined in other classes.

What is Inheritance?

Inheritance is a mechanism that allows a class to inherit properties and behaviors from another class. The class that inherits is called the derived class (or subclass, child class), and the class being inherited from is called the base class (or superclass, parent class).

Inheritance creates an "is-a" relationship between classes. For example:

  • A Car is a Vehicle
  • A Sword is a Weapon
  • An Enemy is a Character

This relationship allows you to model real-world hierarchies and share common functionality among related classes.

Basic Inheritance Syntax

In C#, you use the colon (:) symbol to indicate inheritance:

// Base class
public class Vehicle
{
public float speed;
public int capacity;

public void Move()
{
Console.WriteLine($"Moving at {speed} mph");
}

public void Stop()
{
Console.WriteLine("Stopping...");
speed = 0;
}
}

// Derived class
public class Car : Vehicle
{
public string model;
public int doors;

public void Honk()
{
Console.WriteLine("Honk! Honk!");
}
}

// Usage
Car myCar = new Car();
myCar.model = "Sedan"; // Car's own property
myCar.speed = 60; // Inherited from Vehicle
myCar.Move(); // Inherited method
myCar.Honk(); // Car's own method

In this example, Car inherits all the public and protected members (fields, properties, methods) from Vehicle. This means a Car object has access to speed, capacity, Move(), and Stop() from the Vehicle class, as well as its own model, doors, and Honk() members.

What Gets Inherited?

When a class inherits from another class, it inherits:

  • All public and protected fields, properties, and methods
  • All internal members if the derived class is in the same assembly

It does NOT inherit:

  • Private members (though they still exist in the derived class, they're just not accessible)
  • Constructors (though they can be called using the base keyword)
  • Static members (they belong to the class, not to instances)

Constructors and Inheritance

When you create an instance of a derived class, the base class constructor is called first, followed by the derived class constructor:

public class Vehicle
{
public float speed;

public Vehicle()
{
Console.WriteLine("Vehicle constructor called");
speed = 0;
}

public Vehicle(float initialSpeed)
{
Console.WriteLine($"Vehicle constructor called with speed {initialSpeed}");
speed = initialSpeed;
}
}

public class Car : Vehicle
{
public string model;

public Car() : base() // Explicitly calls the parameterless base constructor
{
Console.WriteLine("Car constructor called");
model = "Unknown";
}

public Car(string carModel) : base() // Calls the parameterless base constructor
{
Console.WriteLine($"Car constructor called with model {carModel}");
model = carModel;
}

public Car(string carModel, float initialSpeed) : base(initialSpeed) // Calls the parameterized base constructor
{
Console.WriteLine($"Car constructor called with model {carModel} and speed {initialSpeed}");
model = carModel;
}
}

// Usage
Car car1 = new Car();
// Output:
// Vehicle constructor called
// Car constructor called

Car car2 = new Car("Tesla");
// Output:
// Vehicle constructor called
// Car constructor called with model Tesla

Car car3 = new Car("Ferrari", 100);
// Output:
// Vehicle constructor called with speed 100
// Car constructor called with model Ferrari and speed 100

Key points about constructors and inheritance:

  • The base class constructor always executes before the derived class constructor
  • You can explicitly call a specific base class constructor using the base keyword
  • If you don't specify which base constructor to call, the parameterless constructor is called by default
  • If the base class doesn't have a parameterless constructor, you must explicitly call one of its constructors

Method Overriding

Method overriding allows a derived class to provide a specific implementation of a method that is already defined in its base class. This is a key aspect of polymorphism (which we'll cover in more detail in the next section).

To override a method in C#:

  1. The base class method must be marked as virtual
  2. The derived class method must use the override keyword
public class Character
{
public string name;
public int health;

public Character(string name, int health)
{
this.name = name;
this.health = health;
}

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

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

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

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

public class Warrior : Character
{
public int strength;

public Warrior(string name, int health, int strength) : base(name, health)
{
this.strength = strength;
}

public override void Attack()
{
int damage = 10 + strength / 2;
Console.WriteLine($"{name} swings a sword for {damage} damage");
}
}

public class Mage : Character
{
public int mana;

public Mage(string name, int health, int mana) : base(name, health)
{
this.mana = mana;
}

public override void Attack()
{
if (mana >= 10)
{
mana -= 10;
Console.WriteLine($"{name} casts a fireball for 30 damage. Mana: {mana}");
}
else
{
Console.WriteLine($"{name} is out of mana and cannot cast spells!");
base.Attack(); // Fall back to the base class implementation
}
}

protected override void Die()
{
Console.WriteLine($"{name} explodes in a burst of magical energy!");
base.Die(); // Call the base implementation as well
}
}

// Usage
Character warrior = new Warrior("Conan", 150, 20);
Character mage = new Mage("Gandalf", 80, 100);

warrior.Attack(); // Output: Conan swings a sword for 20 damage
mage.Attack(); // Output: Gandalf casts a fireball for 30 damage. Mana: 90

warrior.TakeDamage(30); // Output: Conan takes 30 damage. Health: 120
mage.TakeDamage(100); // Output: Gandalf takes 100 damage. Health: -20
// Gandalf explodes in a burst of magical energy!
// Gandalf has been defeated!

Key points about method overriding:

  • The virtual keyword in the base class indicates that a method can be overridden
  • The override keyword in the derived class indicates that a method is overriding a base class method
  • You can call the base class implementation using the base keyword
  • Overridden methods must have the same signature (name, parameters, and return type) as the base method

The base Keyword

The base keyword is used to access members of the base class from within a derived class:

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

public Enemy(string name, int health)
{
this.name = name;
this.health = health;
}

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

public class Boss : Enemy
{
public int shield;

public Boss(string name, int health, int shield) : base(name, health)
{
this.shield = shield;
}

public override void TakeDamage(int amount)
{
// Reduce damage by shield
int reducedAmount = amount - shield;
if (reducedAmount < 1) reducedAmount = 1; // Minimum 1 damage

Console.WriteLine($"{name}'s shield absorbs {amount - reducedAmount} damage");

// Call the base class implementation with the reduced amount
base.TakeDamage(reducedAmount);
}
}

// Usage
Boss dragon = new Boss("Dragon", 500, 10);
dragon.TakeDamage(25);
// Output:
// Dragon's shield absorbs 15 damage
// Dragon takes 10 damage. Health: 490

The base keyword is useful for:

  • Calling base class constructors
  • Calling base class methods from overridden methods
  • Accessing base class members that are hidden by derived class members with the same name

Inheritance and Access Modifiers

Access modifiers play an important role in inheritance:

public class BaseClass
{
public int publicField; // Accessible everywhere
private int privateField; // Accessible only in BaseClass
protected int protectedField; // Accessible in BaseClass and derived classes
internal int internalField; // Accessible in the same assembly

public void PublicMethod() { }
private void PrivateMethod() { }
protected void ProtectedMethod() { }
internal void InternalMethod() { }
}

public class DerivedClass : BaseClass
{
public void AccessTest()
{
publicField = 1; // OK - public is accessible
// privateField = 2; // Error - private is not accessible
protectedField = 3; // OK - protected is accessible in derived classes
internalField = 4; // OK - internal is accessible in the same assembly

PublicMethod(); // OK
// PrivateMethod(); // Error
ProtectedMethod(); // OK
InternalMethod(); // OK
}
}

// Usage
DerivedClass derived = new DerivedClass();
derived.publicField = 10; // OK
// derived.privateField = 20; // Error
// derived.protectedField = 30; // Error - protected is not accessible outside the class hierarchy
derived.internalField = 40; // OK if in the same assembly

Key points about access modifiers and inheritance:

  • public members are accessible everywhere
  • private members are accessible only in the declaring class
  • protected members are accessible in the declaring class and derived classes
  • internal members are accessible in the same assembly
  • protected internal members are accessible in the same assembly or derived classes
  • private protected members (C# 7.2) are accessible only in derived classes in the same assembly

Inheritance vs. Composition

While inheritance is powerful, it's not always the best solution. An alternative approach is composition, where you create relationships between classes by including instances of other classes as members.

// Inheritance approach
public class Vehicle
{
public float speed;
public void Move() { /* ... */ }
}

public class Car : Vehicle
{
public void Honk() { /* ... */ }
}

// Composition approach
public class Engine
{
public int horsepower;
public void Start() { /* ... */ }
}

public class Vehicle
{
private Engine engine;
public float speed;

public Vehicle(Engine engine)
{
this.engine = engine;
}

public void Move()
{
engine.Start();
// Move implementation...
}
}

public class Car
{
private Vehicle vehicle;

public Car(Engine engine)
{
vehicle = new Vehicle(engine);
}

public void Move()
{
vehicle.Move();
}

public void Honk() { /* ... */ }
}

The key difference is that:

  • Inheritance creates an "is-a" relationship (a Car is a Vehicle)
  • Composition creates a "has-a" relationship (a Car has a Vehicle, a Vehicle has an Engine)

When to use inheritance:

  • When there's a clear "is-a" relationship
  • When you want to reuse code from a base class
  • When you want to use polymorphism (treating derived classes as their base type)

When to use composition:

  • When there's a "has-a" relationship
  • When you want more flexibility in changing implementations
  • When you want to avoid the limitations of single inheritance
  • When the relationship between classes might change over time

A common guideline is "favor composition over inheritance" because it often leads to more flexible and maintainable code.

Multiple Inheritance

C# does not support multiple inheritance for classes (inheriting from more than one base class). However, it does support multiple interface implementation, which we'll cover in a later section.

// This is not allowed in C#
public class A { }
public class B { }
public class C : A, B { } // Error: Class cannot have multiple base classes

To achieve similar functionality, you can use interfaces or composition:

// Using interfaces (we'll cover these later)
public interface IA { void MethodA(); }
public interface IB { void MethodB(); }
public class C : IA, IB
{
public void MethodA() { /* ... */ }
public void MethodB() { /* ... */ }
}

// Using composition
public class A { public void MethodA() { /* ... */ } }
public class B { public void MethodB() { /* ... */ } }
public class C
{
private A a = new A();
private B b = new B();

public void MethodA() { a.MethodA(); }
public void MethodB() { b.MethodB(); }
}

Inheritance in Unity

Unity uses inheritance extensively in its architecture. The most common example is the MonoBehaviour class, which all Unity scripts inherit from:

public class PlayerController : MonoBehaviour
{
// Your code here
}

By inheriting from MonoBehaviour, your class gains access to Unity's lifecycle methods (Awake, Start, Update, etc.) and can be attached to GameObjects.

Component Inheritance Hierarchy

Unity's component system is built on inheritance. Here's a simplified view of the hierarchy:

Object
└── Component
├── Behaviour
│ ├── MonoBehaviour (your scripts)
│ └── ...
├── Transform
├── Renderer
│ ├── MeshRenderer
│ ├── SpriteRenderer
│ └── ...
├── Collider
│ ├── BoxCollider
│ ├── SphereCollider
│ └── ...
└── ...

This hierarchy allows you to treat specific components as their base types when needed.

Creating Inheritance Hierarchies in Unity

You can create your own inheritance hierarchies for game elements:

// Base class for all characters
public abstract class Character : MonoBehaviour
{
[SerializeField] protected string characterName;
[SerializeField] protected int maxHealth;
protected int currentHealth;

protected virtual void Awake()
{
currentHealth = maxHealth;
}

public virtual void TakeDamage(int amount)
{
currentHealth -= amount;
Debug.Log($"{characterName} takes {amount} damage. Health: {currentHealth}/{maxHealth}");

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

protected virtual void Die()
{
Debug.Log($"{characterName} has been defeated!");
gameObject.SetActive(false);
}
}

// Player class
public class Player : Character
{
[SerializeField] private int lives = 3;

protected override void Awake()
{
base.Awake();
Debug.Log($"Player {characterName} initialized with {lives} lives");
}

protected override void Die()
{
lives--;

if (lives > 0)
{
Debug.Log($"Player {characterName} lost a life! {lives} remaining.");
currentHealth = maxHealth; // Respawn with full health
}
else
{
Debug.Log($"Game Over! Player {characterName} has no lives left.");
base.Die();
}
}

public void Heal(int amount)
{
currentHealth = Mathf.Min(currentHealth + amount, maxHealth);
Debug.Log($"{characterName} healed for {amount}. Health: {currentHealth}/{maxHealth}");
}
}

// Enemy class
public class Enemy : Character
{
[SerializeField] private int experienceReward;
[SerializeField] private float moveSpeed;

protected override void Awake()
{
base.Awake();
Debug.Log($"Enemy {characterName} initialized with {moveSpeed} move speed");
}

protected override void Die()
{
Debug.Log($"Enemy {characterName} defeated! Player gains {experienceReward} XP.");

// Find player and reward XP
Player player = FindObjectOfType<Player>();
if (player != null)
{
// If we had an XP system, we would reward the player here
}

base.Die();
}

// Enemy-specific method
public void MoveTowardsPlayer()
{
// Implementation...
Debug.Log($"{characterName} moves towards player at speed {moveSpeed}");
}
}

// Boss class - inherits from Enemy
public class Boss : Enemy
{
[SerializeField] private int phaseCount = 3;
private int currentPhase = 1;

protected override void Awake()
{
base.Awake();
Debug.Log($"Boss {characterName} initialized with {phaseCount} phases");
}

public override void TakeDamage(int amount)
{
// Bosses take reduced damage
int reducedAmount = Mathf.Max(1, amount / 2);
base.TakeDamage(reducedAmount);

// Check for phase transition
float healthPercentage = (float)currentHealth / maxHealth;
int newPhase = phaseCount - Mathf.FloorToInt(healthPercentage * phaseCount) + 1;

if (newPhase > currentPhase)
{
TransitionToNewPhase(newPhase);
}
}

private void TransitionToNewPhase(int newPhase)
{
currentPhase = newPhase;
Debug.Log($"Boss {characterName} enters phase {currentPhase}!");

// Implement phase-specific behavior
// ...
}

protected override void Die()
{
Debug.Log($"Boss {characterName} has been defeated! Victory!");

// Maybe trigger a cutscene or level completion
// ...

base.Die();
}
}

This hierarchy allows you to:

  • Share common functionality in the base Character class
  • Specialize behavior in derived classes
  • Override methods to customize behavior
  • Treat all characters uniformly when needed (e.g., applying damage)

Practical Examples

Example 1: Weapon System

// Base weapon class
public abstract class Weapon : MonoBehaviour
{
[SerializeField] protected string weaponName;
[SerializeField] protected int damage;
[SerializeField] protected float attackSpeed;
[SerializeField] protected float range;

protected float lastAttackTime;

public string WeaponName => weaponName;
public int Damage => damage;
public float AttackSpeed => attackSpeed;
public float Range => range;

public virtual bool CanAttack()
{
return Time.time >= lastAttackTime + (1f / attackSpeed);
}

public abstract void Attack();

protected void UpdateAttackTime()
{
lastAttackTime = Time.time;
}

public virtual void Upgrade(int damageBonus, float speedBonus)
{
damage += damageBonus;
attackSpeed += speedBonus;
Debug.Log($"{weaponName} upgraded! Damage: {damage}, Attack Speed: {attackSpeed}");
}
}

// Melee weapon
public class MeleeWeapon : Weapon
{
[SerializeField] private float swingAngle = 90f;
[SerializeField] private LayerMask targetLayers;

public override void Attack()
{
if (!CanAttack()) return;

Debug.Log($"Swinging {weaponName} for {damage} damage");

// Perform a swing attack
Vector3 origin = transform.position;
Vector3 forward = transform.forward;

// Calculate the start and end angles for the swing
float halfAngle = swingAngle / 2f;
Quaternion startRot = Quaternion.Euler(0, -halfAngle, 0) * Quaternion.LookRotation(forward);
Quaternion endRot = Quaternion.Euler(0, halfAngle, 0) * Quaternion.LookRotation(forward);

Vector3 startDir = startRot * Vector3.forward;
Vector3 endDir = endRot * Vector3.forward;

// Check for hits in the swing arc
Collider[] hits = Physics.OverlapSphere(origin, range, targetLayers);

foreach (Collider hit in hits)
{
Vector3 dirToTarget = (hit.transform.position - origin).normalized;

// Check if the target is within the swing angle
if (Vector3.Angle(forward, dirToTarget) <= halfAngle)
{
// Apply damage to the target
IDamageable target = hit.GetComponent<IDamageable>();
if (target != null)
{
target.TakeDamage(damage);
}
}
}

UpdateAttackTime();
}
}

// Ranged weapon
public class RangedWeapon : Weapon
{
[SerializeField] private GameObject projectilePrefab;
[SerializeField] private Transform firePoint;
[SerializeField] private float projectileSpeed = 20f;
[SerializeField] private int ammoCapacity = 10;

private int currentAmmo;

private void Awake()
{
currentAmmo = ammoCapacity;
}

public override bool CanAttack()
{
return base.CanAttack() && currentAmmo > 0;
}

public override void Attack()
{
if (!CanAttack()) return;

Debug.Log($"Firing {weaponName} for {damage} damage");

// Spawn projectile
GameObject projectile = Instantiate(projectilePrefab, firePoint.position, firePoint.rotation);

// Set projectile properties
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
{
projectileComponent.Initialize(damage, range, projectileSpeed);
}

// Reduce ammo
currentAmmo--;

UpdateAttackTime();
}

public void Reload(int ammoAmount)
{
currentAmmo = Mathf.Min(currentAmmo + ammoAmount, ammoCapacity);
Debug.Log($"{weaponName} reloaded. Ammo: {currentAmmo}/{ammoCapacity}");
}

public override void Upgrade(int damageBonus, float speedBonus)
{
base.Upgrade(damageBonus, speedBonus);

// Ranged weapons also get more ammo capacity when upgraded
ammoCapacity += 2;
Debug.Log($"Ammo capacity increased to {ammoCapacity}");
}
}

// Magic weapon
public class MagicWeapon : Weapon
{
[SerializeField] private int manaCost;
[SerializeField] private float areaOfEffect;
[SerializeField] private GameObject spellEffectPrefab;
[SerializeField] private LayerMask targetLayers;

private PlayerMana playerMana; // Reference to the player's mana component

private void Start()
{
// Find the player's mana component
playerMana = FindObjectOfType<PlayerMana>();
}

public override bool CanAttack()
{
return base.CanAttack() && playerMana != null && playerMana.CurrentMana >= manaCost;
}

public override void Attack()
{
if (!CanAttack()) return;

Debug.Log($"Casting {weaponName} for {damage} damage");

// Consume mana
playerMana.UseMana(manaCost);

// Spawn spell effect
if (spellEffectPrefab != null)
{
Instantiate(spellEffectPrefab, transform.position, transform.rotation);
}

// Apply area damage
Collider[] hits = Physics.OverlapSphere(transform.position, areaOfEffect, targetLayers);

foreach (Collider hit in hits)
{
IDamageable target = hit.GetComponent<IDamageable>();
if (target != null)
{
// Calculate damage falloff based on distance
float distance = Vector3.Distance(transform.position, hit.transform.position);
float damageMultiplier = 1f - (distance / areaOfEffect);
int adjustedDamage = Mathf.RoundToInt(damage * damageMultiplier);

target.TakeDamage(adjustedDamage);
}
}

UpdateAttackTime();
}

public override void Upgrade(int damageBonus, float speedBonus)
{
base.Upgrade(damageBonus, speedBonus);

// Magic weapons also get increased area of effect when upgraded
areaOfEffect += 0.5f;
Debug.Log($"Area of effect increased to {areaOfEffect}");
}
}

// Interface for objects that can take damage
public interface IDamageable
{
void TakeDamage(int amount);
}

// Simple projectile class
public class Projectile : MonoBehaviour
{
private int damage;
private float range;
private float speed;

private Vector3 startPosition;

private void Start()
{
startPosition = transform.position;
}

private void Update()
{
// Move forward
transform.Translate(Vector3.forward * speed * Time.deltaTime);

// Check if we've exceeded the range
float distanceTraveled = Vector3.Distance(startPosition, transform.position);
if (distanceTraveled > range)
{
Destroy(gameObject);
}
}

public void Initialize(int damage, float range, float speed)
{
this.damage = damage;
this.range = range;
this.speed = speed;
}

private void OnTriggerEnter(Collider other)
{
IDamageable target = other.GetComponent<IDamageable>();
if (target != null)
{
target.TakeDamage(damage);
}

Destroy(gameObject);
}
}

// Player mana component
public class PlayerMana : MonoBehaviour
{
[SerializeField] private int maxMana = 100;
private int currentMana;

public int CurrentMana => currentMana;
public int MaxMana => maxMana;

private void Awake()
{
currentMana = maxMana;
}

public bool UseMana(int amount)
{
if (currentMana >= amount)
{
currentMana -= amount;
return true;
}

return false;
}

public void RestoreMana(int amount)
{
currentMana = Mathf.Min(currentMana + amount, maxMana);
}
}

// Usage in a player controller
public class PlayerController : MonoBehaviour, IDamageable
{
[SerializeField] private int health = 100;
[SerializeField] private Weapon[] weapons;
private int currentWeaponIndex = 0;

private void Update()
{
// Switch weapons
if (Input.GetKeyDown(KeyCode.Q))
{
SwitchWeapon();
}

// Attack
if (Input.GetMouseButtonDown(0))
{
if (weapons[currentWeaponIndex].CanAttack())
{
weapons[currentWeaponIndex].Attack();
}
else
{
Debug.Log("Cannot attack yet!");
}
}
}

private void SwitchWeapon()
{
// Disable current weapon
weapons[currentWeaponIndex].gameObject.SetActive(false);

// Switch to next weapon
currentWeaponIndex = (currentWeaponIndex + 1) % weapons.Length;

// Enable new current weapon
weapons[currentWeaponIndex].gameObject.SetActive(true);

Debug.Log($"Switched to {weapons[currentWeaponIndex].WeaponName}");
}

public void TakeDamage(int amount)
{
health -= amount;
Debug.Log($"Player takes {amount} damage. Health: {health}");

if (health <= 0)
{
Debug.Log("Player died!");
// Handle death
}
}
}

Example 2: Item System

// Base item class
public abstract class Item : MonoBehaviour
{
[SerializeField] protected string itemName;
[SerializeField] protected string description;
[SerializeField] protected Sprite icon;
[SerializeField] protected int value;
[SerializeField] protected float weight;
[SerializeField] protected ItemRarity rarity;

public string ItemName => itemName;
public string Description => description;
public Sprite Icon => icon;
public int Value => value;
public float Weight => weight;
public ItemRarity Rarity => rarity;

public abstract void Use(PlayerController player);

public virtual string GetTooltip()
{
return $"{itemName}\n{GetRarityColorHex(rarity)}{rarity}</color>\n\n{description}\n\nValue: {value} gold\nWeight: {weight} kg";
}

protected string GetRarityColorHex(ItemRarity rarity)
{
return rarity switch
{
ItemRarity.Common => "<color=#FFFFFF>", // White
ItemRarity.Uncommon => "<color=#00FF00>", // Green
ItemRarity.Rare => "<color=#0000FF>", // Blue
ItemRarity.Epic => "<color=#800080>", // Purple
ItemRarity.Legendary => "<color=#FFA500>", // Orange
_ => "<color=#FFFFFF>"
};
}
}

public enum ItemRarity
{
Common,
Uncommon,
Rare,
Epic,
Legendary
}

// Consumable item
public class ConsumableItem : Item
{
[SerializeField] private int healthRestoration;
[SerializeField] private int manaRestoration;
[SerializeField] private float duration;
[SerializeField] private StatusEffect statusEffect;

public override void Use(PlayerController player)
{
Debug.Log($"Using {itemName}");

// Apply health restoration
if (healthRestoration > 0)
{
player.RestoreHealth(healthRestoration);
Debug.Log($"Restored {healthRestoration} health");
}

// Apply mana restoration
if (manaRestoration > 0)
{
PlayerMana mana = player.GetComponent<PlayerMana>();
if (mana != null)
{
mana.RestoreMana(manaRestoration);
Debug.Log($"Restored {manaRestoration} mana");
}
}

// Apply status effect
if (statusEffect != StatusEffect.None)
{
player.ApplyStatusEffect(statusEffect, duration);
Debug.Log($"Applied {statusEffect} effect for {duration} seconds");
}

// Remove the item after use
player.RemoveItemFromInventory(this);
}

public override string GetTooltip()
{
string baseTooltip = base.GetTooltip();
string effectsTooltip = "";

if (healthRestoration > 0)
{
effectsTooltip += $"\nRestores {healthRestoration} Health";
}

if (manaRestoration > 0)
{
effectsTooltip += $"\nRestores {manaRestoration} Mana";
}

if (statusEffect != StatusEffect.None)
{
effectsTooltip += $"\nGrants {statusEffect} for {duration} seconds";
}

return baseTooltip + effectsTooltip;
}
}

// Equipment item
public class EquipmentItem : Item
{
[SerializeField] private EquipmentSlot slot;
[SerializeField] private int armorBonus;
[SerializeField] private int strengthBonus;
[SerializeField] private int dexterityBonus;
[SerializeField] private int intelligenceBonus;

public EquipmentSlot Slot => slot;

public override void Use(PlayerController player)
{
Debug.Log($"Equipping {itemName}");

// Equip the item
player.EquipItem(this);
}

public void ApplyStats(PlayerStats stats)
{
stats.AddArmorBonus(armorBonus);
stats.AddStrengthBonus(strengthBonus);
stats.AddDexterityBonus(dexterityBonus);
stats.AddIntelligenceBonus(intelligenceBonus);
}

public void RemoveStats(PlayerStats stats)
{
stats.RemoveArmorBonus(armorBonus);
stats.RemoveStrengthBonus(strengthBonus);
stats.RemoveDexterityBonus(dexterityBonus);
stats.RemoveIntelligenceBonus(intelligenceBonus);
}

public override string GetTooltip()
{
string baseTooltip = base.GetTooltip();
string statsTooltip = $"\n\nSlot: {slot}";

if (armorBonus > 0)
{
statsTooltip += $"\nArmor: +{armorBonus}";
}

if (strengthBonus > 0)
{
statsTooltip += $"\nStrength: +{strengthBonus}";
}

if (dexterityBonus > 0)
{
statsTooltip += $"\nDexterity: +{dexterityBonus}";
}

if (intelligenceBonus > 0)
{
statsTooltip += $"\nIntelligence: +{intelligenceBonus}";
}

return baseTooltip + statsTooltip;
}
}

// Quest item
public class QuestItem : Item
{
[SerializeField] private string questId;
[SerializeField] private bool isObjective;

public string QuestId => questId;
public bool IsObjective => isObjective;

public override void Use(PlayerController player)
{
Debug.Log($"Examining {itemName}");

// Show detailed description
UIManager.Instance.ShowItemDescription(this);

// If this is a quest objective, check if it completes a quest
if (isObjective)
{
QuestManager.Instance.CheckQuestProgress(questId, this);
}
}

public override string GetTooltip()
{
string baseTooltip = base.GetTooltip();
return baseTooltip + "\n\n<color=#FFFF00>Quest Item</color>";
}
}

// Enums
public enum EquipmentSlot
{
Head,
Chest,
Legs,
Feet,
Hands,
Weapon,
Shield,
Accessory
}

public enum StatusEffect
{
None,
Strength,
Dexterity,
Intelligence,
Speed,
Resistance,
Regeneration,
Invisibility
}

// Simplified player controller for the example
public class PlayerController : MonoBehaviour
{
private List<Item> inventory = new List<Item>();
private Dictionary<EquipmentSlot, EquipmentItem> equippedItems = new Dictionary<EquipmentSlot, EquipmentItem>();
private int health;
private int maxHealth = 100;
private Dictionary<StatusEffect, float> activeEffects = new Dictionary<StatusEffect, float>();

public void RestoreHealth(int amount)
{
health = Mathf.Min(health + amount, maxHealth);
}

public void RemoveItemFromInventory(Item item)
{
inventory.Remove(item);
Destroy(item.gameObject);
}

public void EquipItem(EquipmentItem item)
{
// Unequip any item in the same slot
if (equippedItems.TryGetValue(item.Slot, out EquipmentItem currentItem))
{
UnequipItem(currentItem);
}

// Equip the new item
equippedItems[item.Slot] = item;

// Apply stats
PlayerStats stats = GetComponent<PlayerStats>();
if (stats != null)
{
item.ApplyStats(stats);
}
}

public void UnequipItem(EquipmentItem item)
{
if (equippedItems.ContainsValue(item))
{
equippedItems.Remove(item.Slot);

// Remove stats
PlayerStats stats = GetComponent<PlayerStats>();
if (stats != null)
{
item.RemoveStats(stats);
}
}
}

public void ApplyStatusEffect(StatusEffect effect, float duration)
{
activeEffects[effect] = Time.time + duration;

// Apply effect...
// This would be implemented based on what each effect does
}

private void Update()
{
// Update status effects
List<StatusEffect> expiredEffects = new List<StatusEffect>();

foreach (var effect in activeEffects)
{
if (Time.time > effect.Value)
{
expiredEffects.Add(effect.Key);
}
}

foreach (StatusEffect effect in expiredEffects)
{
activeEffects.Remove(effect);
Debug.Log($"{effect} effect has expired");
}
}
}

// Simplified player stats
public class PlayerStats : MonoBehaviour
{
[SerializeField] private int baseStrength = 10;
[SerializeField] private int baseDexterity = 10;
[SerializeField] private int baseIntelligence = 10;
[SerializeField] private int baseArmor = 0;

private int strengthBonus = 0;
private int dexterityBonus = 0;
private int intelligenceBonus = 0;
private int armorBonus = 0;

public int Strength => baseStrength + strengthBonus;
public int Dexterity => baseDexterity + dexterityBonus;
public int Intelligence => baseIntelligence + intelligenceBonus;
public int Armor => baseArmor + armorBonus;

public void AddStrengthBonus(int bonus) => strengthBonus += bonus;
public void AddDexterityBonus(int bonus) => dexterityBonus += bonus;
public void AddIntelligenceBonus(int bonus) => intelligenceBonus += bonus;
public void AddArmorBonus(int bonus) => armorBonus += bonus;

public void RemoveStrengthBonus(int bonus) => strengthBonus -= bonus;
public void RemoveDexterityBonus(int bonus) => dexterityBonus -= bonus;
public void RemoveIntelligenceBonus(int bonus) => intelligenceBonus -= bonus;
public void RemoveArmorBonus(int bonus) => armorBonus -= bonus;
}

// Simplified managers
public class UIManager : MonoBehaviour
{
public static UIManager Instance { get; private set; }

private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}

public void ShowItemDescription(Item item)
{
Debug.Log($"Showing description for {item.ItemName}: {item.Description}");
}
}

public class QuestManager : MonoBehaviour
{
public static QuestManager Instance { get; private set; }

private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}

public void CheckQuestProgress(string questId, QuestItem item)
{
Debug.Log($"Checking progress for quest {questId} with item {item.ItemName}");
// Implementation would check if this item completes a quest objective
}
}

Example 3: UI System

// Base UI element
public abstract class UIElement : MonoBehaviour
{
[SerializeField] protected string elementName;
[SerializeField] protected bool startVisible = true;

protected RectTransform rectTransform;
protected CanvasGroup canvasGroup;
protected bool isVisible;

protected virtual void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvasGroup = GetComponent<CanvasGroup>();

if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}

isVisible = startVisible;
UpdateVisibility();
}

public virtual void Show()
{
isVisible = true;
UpdateVisibility();
}

public virtual void Hide()
{
isVisible = false;
UpdateVisibility();
}

public virtual void Toggle()
{
isVisible = !isVisible;
UpdateVisibility();
}

protected virtual void UpdateVisibility()
{
canvasGroup.alpha = isVisible ? 1f : 0f;
canvasGroup.interactable = isVisible;
canvasGroup.blocksRaycasts = isVisible;
}

public virtual void SetPosition(Vector2 position)
{
rectTransform.anchoredPosition = position;
}

public virtual void SetSize(Vector2 size)
{
rectTransform.sizeDelta = size;
}
}

// UI panel
public class UIPanel : UIElement
{
[SerializeField] protected bool closeOnEscape = true;
[SerializeField] protected Button closeButton;

protected override void Awake()
{
base.Awake();

if (closeButton != null)
{
closeButton.onClick.AddListener(Hide);
}
}

protected virtual void Update()
{
if (isVisible && closeOnEscape && Input.GetKeyDown(KeyCode.Escape))
{
Hide();
}
}

public override void Show()
{
base.Show();
Debug.Log($"Panel {elementName} shown");
}

public override void Hide()
{
base.Hide();
Debug.Log($"Panel {elementName} hidden");
}
}

// UI window with dragging
public class UIWindow : UIPanel
{
[SerializeField] protected RectTransform titleBar;
[SerializeField] protected bool isDraggable = true;

private bool isDragging = false;
private Vector2 dragOffset;

protected override void Awake()
{
base.Awake();

if (titleBar != null && isDraggable)
{
// Add event trigger for dragging
EventTrigger trigger = titleBar.gameObject.AddComponent<EventTrigger>();

// Begin drag
EventTrigger.Entry beginDrag = new EventTrigger.Entry();
beginDrag.eventID = EventTriggerType.BeginDrag;
beginDrag.callback.AddListener((data) => OnBeginDrag((PointerEventData)data));
trigger.triggers.Add(beginDrag);

// Drag
EventTrigger.Entry drag = new EventTrigger.Entry();
drag.eventID = EventTriggerType.Drag;
drag.callback.AddListener((data) => OnDrag((PointerEventData)data));
trigger.triggers.Add(drag);

// End drag
EventTrigger.Entry endDrag = new EventTrigger.Entry();
endDrag.eventID = EventTriggerType.EndDrag;
endDrag.callback.AddListener((data) => OnEndDrag((PointerEventData)data));
trigger.triggers.Add(endDrag);
}
}

private void OnBeginDrag(PointerEventData eventData)
{
if (!isDraggable) return;

isDragging = true;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform,
eventData.position,
eventData.pressEventCamera,
out dragOffset
);
}

private void OnDrag(PointerEventData eventData)
{
if (!isDragging) return;

Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform.parent as RectTransform,
eventData.position,
eventData.pressEventCamera,
out localPoint
);

rectTransform.anchoredPosition = localPoint - dragOffset;
}

private void OnEndDrag(PointerEventData eventData)
{
isDragging = false;
}

public void SetDraggable(bool draggable)
{
isDraggable = draggable;
}
}

// Inventory window
public class InventoryWindow : UIWindow
{
[SerializeField] private Transform itemContainer;
[SerializeField] private GameObject itemSlotPrefab;
[SerializeField] private int columns = 5;
[SerializeField] private int rows = 4;

private List<ItemSlot> itemSlots = new List<ItemSlot>();

protected override void Awake()
{
base.Awake();

// Create item slots
CreateItemSlots();
}

private void CreateItemSlots()
{
// Clear existing slots
foreach (Transform child in itemContainer)
{
Destroy(child.gameObject);
}

itemSlots.Clear();

// Create new slots
for (int i = 0; i < rows * columns; i++)
{
GameObject slotObject = Instantiate(itemSlotPrefab, itemContainer);
ItemSlot slot = slotObject.GetComponent<ItemSlot>();

if (slot != null)
{
slot.Initialize(i);
itemSlots.Add(slot);
}
}

Debug.Log($"Created {itemSlots.Count} inventory slots");
}

public void AddItem(Item item)
{
// Find first empty slot
ItemSlot emptySlot = itemSlots.Find(slot => !slot.HasItem);

if (emptySlot != null)
{
emptySlot.SetItem(item);
Debug.Log($"Added {item.ItemName} to inventory");
}
else
{
Debug.Log("Inventory is full!");
}
}

public void RemoveItem(Item item)
{
// Find slot containing the item
ItemSlot slot = itemSlots.Find(s => s.CurrentItem == item);

if (slot != null)
{
slot.ClearItem();
Debug.Log($"Removed {item.ItemName} from inventory");
}
}
}

// Character window
public class CharacterWindow : UIWindow
{
[SerializeField] private Text characterNameText;
[SerializeField] private Text levelText;
[SerializeField] private Text healthText;
[SerializeField] private Text manaText;
[SerializeField] private Text strengthText;
[SerializeField] private Text dexterityText;
[SerializeField] private Text intelligenceText;
[SerializeField] private Text armorText;

private PlayerStats playerStats;

protected override void Awake()
{
base.Awake();

// Find player stats
playerStats = FindObjectOfType<PlayerStats>();
}

public override void Show()
{
base.Show();

// Update stats display
UpdateStatsDisplay();
}

private void UpdateStatsDisplay()
{
if (playerStats == null)
{
Debug.LogWarning("PlayerStats not found!");
return;
}

// Update text fields
characterNameText.text = "Player Name"; // Would come from player data
levelText.text = $"Level: 10"; // Would come from player data
healthText.text = $"Health: 100/100"; // Would come from player data
manaText.text = $"Mana: 50/50"; // Would come from player data

strengthText.text = $"Strength: {playerStats.Strength}";
dexterityText.text = $"Dexterity: {playerStats.Dexterity}";
intelligenceText.text = $"Intelligence: {playerStats.Intelligence}";
armorText.text = $"Armor: {playerStats.Armor}";
}
}

// Item slot for inventory
public class ItemSlot : MonoBehaviour
{
[SerializeField] private Image backgroundImage;
[SerializeField] private Image itemImage;
[SerializeField] private Text quantityText;

private int slotIndex;
private Item currentItem;

public Item CurrentItem => currentItem;
public bool HasItem => currentItem != null;

public void Initialize(int index)
{
slotIndex = index;
ClearItem();
}

public void SetItem(Item item)
{
currentItem = item;

if (item != null)
{
itemImage.sprite = item.Icon;
itemImage.enabled = true;

// If the item is stackable, show quantity
StackableItem stackable = item as StackableItem;
if (stackable != null)
{
quantityText.text = stackable.Quantity.ToString();
quantityText.enabled = true;
}
else
{
quantityText.enabled = false;
}
}
}

public void ClearItem()
{
currentItem = null;
itemImage.sprite = null;
itemImage.enabled = false;
quantityText.enabled = false;
}

public void OnClick()
{
if (currentItem != null)
{
// Show tooltip or use item
UIManager.Instance.ShowItemTooltip(currentItem);
}
}
}

// Stackable item (additional derived class for the item system)
public class StackableItem : ConsumableItem
{
[SerializeField] private int maxStackSize = 99;
private int quantity = 1;

public int Quantity
{
get => quantity;
set => quantity = Mathf.Clamp(value, 0, maxStackSize);
}

public int MaxStackSize => maxStackSize;

public override void Use(PlayerController player)
{
base.Use(player);

// Reduce quantity instead of removing the item
quantity--;

// If quantity reaches 0, remove the item
if (quantity <= 0)
{
player.RemoveItemFromInventory(this);
}
}

public override string GetTooltip()
{
string baseTooltip = base.GetTooltip();
return baseTooltip + $"\n\nQuantity: {quantity}";
}

public bool CanStack(StackableItem other)
{
// Check if items are the same type and can be stacked
return other.ItemName == ItemName && quantity < maxStackSize;
}

public int AddToStack(int amount)
{
int spaceAvailable = maxStackSize - quantity;
int amountToAdd = Mathf.Min(amount, spaceAvailable);

quantity += amountToAdd;

// Return the amount that couldn't be added
return amount - amountToAdd;
}
}

Best Practices for Inheritance

  1. Use Inheritance for "Is-A" Relationships: Only use inheritance when there's a clear "is-a" relationship between classes.

  2. Keep the Inheritance Hierarchy Shallow: Deep inheritance hierarchies can become difficult to understand and maintain. Try to keep it to 3-4 levels at most.

  3. Favor Composition Over Inheritance: When in doubt, prefer composition (has-a) over inheritance (is-a).

  4. Design for Inheritance or Prohibit It: Either design your class carefully for inheritance (with virtual methods, etc.) or mark it as sealed to prevent inheritance.

  5. Use Abstract Base Classes for Common Functionality: Abstract base classes are great for sharing common functionality among related classes.

  6. Override ToString() for Debugging: Override the ToString() method in your classes to provide useful debugging information.

  7. Be Careful with Constructors: Ensure base class constructors are called appropriately from derived classes.

  8. Don't Override Non-Virtual Methods: Only override methods that are marked as virtual in the base class.

  9. Use the base Keyword When Appropriate: Call the base class implementation when overriding methods if you want to extend rather than replace the behavior.

  10. Consider Interfaces for Multiple Inheritance: When you need functionality from multiple sources, use interfaces instead of trying to simulate multiple inheritance.

Conclusion

Inheritance is a powerful feature of Object-Oriented Programming that allows you to create hierarchical relationships between classes. By using inheritance effectively, you can:

  • Reuse code across related classes
  • Model "is-a" relationships
  • Create specialized versions of more general classes
  • Implement polymorphic behavior

In Unity, inheritance is used extensively, from the core MonoBehaviour class to your own game-specific hierarchies. Understanding how to use inheritance properly will help you create more organized, maintainable, and extensible code for your games.

In the next section, we'll explore polymorphism in more detail, which builds on the inheritance concepts we've covered here.

Practice Exercise

Exercise: Design a simple enemy system for a game with the following requirements:

  1. Create a base Enemy class with:

    • Properties for name, health, damage, and movement speed
    • Methods for taking damage, attacking, and moving
    • A virtual method for dying
  2. Create at least three derived enemy types (e.g., Goblin, Orc, Dragon) that:

    • Override the attack method with unique behavior
    • Override the die method with unique behavior
    • Add at least one unique property or method to each
  3. Create a simple EnemySpawner class that can spawn different types of enemies.

  4. Demonstrate polymorphism by creating an array or list of the base Enemy type and filling it with different derived enemy types.

Think about how inheritance helps you organize your code and reduce duplication while still allowing for specialized behavior in each enemy type.