Inheritance
Summary
Inheritance lets a class (the derived or child class) reuse and
extend another class's (the base or parent class) fields and
methods, rather than duplicating them. Goblin and Troll might both
inherit from Enemy, automatically getting health, TakeDamage(), and
Die() without redeclaring any of it, while each adds or changes only
what makes it distinct.
Mental Model
If a class is a blueprint, inheritance is a blueprint that starts from an existing one: "a Goblin house is a House, plus a reinforced door" — every room the base House blueprint specifies still exists in the Goblin variant, and the derived blueprint only needs to describe what's different, not redraw the whole plan from scratch.
Every Goblin object is also an Enemy object — this "is-a"
relationship is exactly what inheritance models, and it's the reason
Enemy-typed code (a variable, a parameter, a list) can hold and operate
on a Goblin without knowing it's specifically a Goblin at all.
Core Explanation
- Base class / derived class.
class Goblin : Enemy(C#) declaresGoblinas inheriting fromEnemy— everypublic/protectedfield and methodEnemydeclares is available onGoblinobjects too, without being rewritten. protectedis an access level betweenpublicandprivate: visible to the class itself and every class that derives from it, but not to unrelated outside code — the standard way a base class exposes something derived classes need without making it fully public.- Overriding. A derived class can redefine a base class method's
behavior (
overridein C#,virtual/overridein C++) —Goblinmight overrideDie()to drop different loot than the baseEnemyimplementation. The base method must be declaredvirtual(or equivalent) to permit this; a non-virtual method cannot be overridden, only hidden, which is a different and usually-unintended thing. - Polymorphism is the direct payoff of overriding: code holding an
Enemyreference that callsenemy.Die()runs whicheverDie()the object's actual runtime type provides —Goblin's override if the object is aGoblin, the base implementation if it's a plainEnemy— decided at runtime based on the object's real type, not the type of the variable pointing at it. - Calling the base implementation. An override can still invoke the
base class's version explicitly (
base.Die()in C#,Super::Die()in Unreal C++) to extend rather than fully replace the inherited behavior — common when the derived class wants to add something in addition to what the base already does, not instead of it. - Single inheritance vs. multiple. C# and C++ classes support inheriting from one base class (C++ technically permits multiple base classes, but it introduces enough ambiguity — the "diamond problem" — that it is generally avoided in practice; Unreal's own class hierarchy is single-inheritance). This is exactly why Interfaces exist: to give a class more than one behavioral contract without needing multiple base classes.
- Inheritance chains. Derivation can go more than one level deep
(
Boss : Goblin : Enemy) — aBossobject is simultaneously aBoss, aGoblin, and anEnemy, inheriting from the whole chain.
Syntax / Examples
C# (Unity)
public class Enemy
{
public int Health = 100;
public virtual void Die() { Debug.Log("Enemy defeated."); }
}
public class Goblin : Enemy
{
public override void Die()
{
base.Die(); // still runs Enemy's shared logic
DropLoot("gold-coin"); // plus Goblin-specific behavior
}
}
// Polymorphism: same call, different behavior per actual object type
Enemy enemy = new Goblin();
enemy.Die(); // runs Goblin's override, even though `enemy` is typed as Enemy
C++ (Unreal)
UCLASS()
class AEnemy : public AActor
{
GENERATED_BODY()
public:
virtual void Die() { UE_LOG(LogTemp, Log, TEXT("Enemy defeated.")); }
};
UCLASS()
class AGoblin : public AEnemy
{
GENERATED_BODY()
public:
virtual void Die() override
{
Super::Die(); // Enemy's shared logic
DropLoot(); // Goblin-specific addition
}
};
Unreal Blueprint
Creating a Blueprint "Child Blueprint Class" of BP_Enemy establishes
inheritance visually — the child inherits every variable and function
from the parent. Overriding a parent function is done by right-clicking
it in the child's function list and choosing "Override"; a
Parent: Function node inside the override calls the base
implementation, mirroring base.Die()/Super::Die().
Practical Example
A list typed as the base class can hold any derived type, and calling a shared method dispatches to each object's own override — the core reason inheritance pays off at scale:
List<Enemy> wave = new List<Enemy> { new Goblin(), new Troll(), new Enemy() };
foreach (Enemy enemy in wave)
{
enemy.Die(); // Goblin's, Troll's, or the base Enemy's Die() — decided per object
}
The loop is written once against Enemy and never needs to change when a
new enemy type is added — as long as the new type also derives from
Enemy and (optionally) overrides Die(), it slots into the exact same
loop with no modification to the loop itself.
Common Mistakes
- Deep, rigid inheritance chains built for hypothetical future reuse — three or four levels deep, where most levels add little — make every change to a base class ripple through everything beneath it. Inheritance should model a genuine, stable "is-a" relationship, not be reached for by default.
- Forgetting
virtualon a base method intended to be overridden — in C++, calling a non-virtual method through a base-class pointer always runs the base implementation regardless of the object's actual type, silently defeating the polymorphism the derived override was supposed to provide. - Overriding without calling the base implementation when the base
behavior was still needed — replacing
Die()entirely instead of extending it can silently drop shared logic every other enemy type still relies on. - Inheriting purely for code reuse without a real "is-a" relationship
— a
PlayerInventoryinheriting fromEnemyInventoryjust to reuse a few fields, when a player is not conceptually a kind of enemy, is a sign the shared logic belongs in a separate reusable class instead (or an interface, see Interfaces), not in an inheritance relationship. - The fragile base class problem — changing a base class's behavior can silently break derived classes that depended on the old behavior, even if they never touch the base class's code directly.
Best Practices
- Use inheritance only for a genuine, stable "is-a" relationship — ask "is a Goblin actually a kind of Enemy" (yes) versus "does Goblin merely want to reuse some of Enemy's code" (reach for composition or an interface instead).
- Mark a base method
virtualdeliberately, only when derived classes are meant to be able to change its behavior. - Call the base implementation (
base./Super::) when overriding, unless the derived behavior is genuinely meant to fully replace it. - Keep inheritance chains shallow; prefer composing smaller, focused classes over deep hierarchies built for anticipated reuse.
Exercises
- Model
GoblinandTrollas classes deriving fromEnemy, each overridingDie()differently, and demonstrate polymorphism with a loop over a list typed asEnemy. - Explain why forgetting
virtualon a base method in C++ silently breaks polymorphism, using a concrete before/after example. - Identify a case where inheritance was used purely for code reuse without a genuine "is-a" relationship, and redesign it using composition instead.
- Write an override that calls its base implementation and explain what would be lost if that call were omitted.
Related Concepts
- Classes and Objects — inheritance relates two classes; the "is-a" relationship it creates is observable on the objects instantiated from them.
- Interfaces — an alternative way to share a behavioral contract across unrelated classes, without inheriting implementation.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules.- C# Language Reference — Inheritance: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance
- Unreal Engine — Class Hierarchy and Inheritance: https://dev.epicgames.com/documentation/en-us/unreal-engine/gameplay-classes-in-unreal-engine