Skip to main content

πŸ›οΈ Lesson 6.4: The C++ Base / Blueprint Subclass Pattern

This is the pattern the entire course has been building toward β€” the one that dominates professional Unreal projects. Write the mechanics once in a C++ base class; create many Blueprint subclasses for the content and tuning. We'll assemble a complete weapon system to see every interop tool click into place.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain the division of labor between a C++ base and Blueprint subclasses
  • Design a clean interop surface: what to expose and how
  • Mark a class Blueprintable / Abstract appropriately
  • Build a full AWeaponBase that a designer extends into many weapons
  • State the guidelines that keep the C++/Blueprint boundary healthy

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

The Division of Labor

The pattern from Lesson 1.1, now with the vocabulary to build it. A single C++ base class holds the mechanics; each Blueprint subclass supplies the content and tuning.

graph TD Base["AWeaponBase (C++)
fire logic, ammo, reload, damage, cooldown timer"] Base --> R["BP_Rifle
mesh, stats, VFX, sounds"] Base --> S["BP_Shotgun
mesh, spread, pellets, VFX"] Base --> P["BP_Pistol
mesh, stats, VFX"] Base --> L["BP_Launcher
mesh, projectile, VFX"]
Figure 1: One C++ base, four Blueprint weapons. Programmers own the mechanics; designers own the arsenal. Adding a fifth weapon is a new Blueprint asset β€” no C++ change.
C++ base owns (mechanics)Blueprint subclass owns (content)
Fire logic, hit/trace, ammo mathWhich mesh, materials, animations
Reload flow, cooldown timersFire rate, damage, magazine size (tuning)
Damage applicationMuzzle flash, sounds, camera shake
State machine & invariantsFeel and polish, iterated without compiling

Blueprintable & Abstract

Two UCLASS specifiers set up the base for subclassing:

  • Blueprintable β€” allows designers to create Blueprint subclasses of this C++ class. Without it, "Create Blueprint Class based on…" won't offer your class.
  • Abstract β€” marks the base as not-directly-usable: you can't place a raw AWeaponBase in a level, only its concrete subclasses. This prevents designers from accidentally using the un-configured base.
// A base meant to be subclassed in Blueprint, never used directly.
UCLASS(Abstract, Blueprintable)
class MYPROJECT_API AWeaponBase : public AActor
{
    GENERATED_BODY()
    // ...
};

βœ… Abstract = "you must subclass me"

Abstract communicates intent and prevents mistakes: a bare AWeaponBase has no mesh and no stats, so placing one would be a bug. Marking it Abstract makes the engine enforce "always use a configured subclass." It's the C++ equivalent of a pure interface you must fill in.

Building AWeaponBase

Here's the base, using every interop tool from this module. Header:

// WeaponBase.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "WeaponStats.h"     // FWeaponStats + EWeaponType (Lesson 6.3)
#include "WeaponBase.generated.h"

class USkeletalMeshComponent;

UCLASS(Abstract, Blueprintable)
class MYPROJECT_API AWeaponBase : public AActor
{
    GENERATED_BODY()

public:
    AWeaponBase();

protected:
    virtual void BeginPlay() override;

    // ---- Content: designers set these on the Blueprint subclass ----
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
    TObjectPtr<USkeletalMeshComponent> Mesh;

    // Tuning bundled as a struct (Lesson 6.3), edited on the BP default.
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon")
    FWeaponStats Stats;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon")
    EWeaponType Type = EWeaponType::Rifle;

    // ---- Runtime state: C++ owns it, BP reads it ----
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "State")
    int32 CurrentAmmo = 0;

public:
    // ---- Mechanics: BP can trigger, C++ implements (Lesson 6.1) ----
    UFUNCTION(BlueprintCallable, Category = "Weapon")
    void Fire();

    UFUNCTION(BlueprintCallable, Category = "Weapon")
    void Reload();

    UFUNCTION(BlueprintPure, Category = "Weapon")
    bool CanFire() const;

    // ---- Cosmetic hooks: C++ calls, designers implement in BP ----
    UFUNCTION(BlueprintImplementableEvent, Category = "Weapon")
    void OnFired();              // muzzle flash, sound, recoil

    UFUNCTION(BlueprintImplementableEvent, Category = "Weapon")
    void OnReloadStarted();      // reload animation, sound

    // ---- Event others can subscribe to (Lesson 5.4) ----
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAmmoChanged, int32, NewAmmo);
    UPROPERTY(BlueprintAssignable, Category = "Weapon")
    FOnAmmoChanged OnAmmoChanged;

private:
    FTimerHandle ReloadTimer;    // cooldown/timer (Lesson 5.3)
};

Implementation β€” the mechanics, in C++:

// WeaponBase.cpp
#include "WeaponBase.h"
#include "Components/SkeletalMeshComponent.h"

AWeaponBase::AWeaponBase()
{
    PrimaryActorTick.bCanEverTick = false;
    Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
    RootComponent = Mesh;
}

void AWeaponBase::BeginPlay()
{
    Super::BeginPlay();
    CurrentAmmo = Stats.MagazineSize;   // start full, using the tuned struct
    OnAmmoChanged.Broadcast(CurrentAmmo);
}

bool AWeaponBase::CanFire() const
{
    return CurrentAmmo > 0;
}

void AWeaponBase::Fire()
{
    if (!CanFire()) { return; }         // enforce the invariant in C++

    --CurrentAmmo;                       // ammo math: mechanics
    // ...trace or spawn projectile using Stats.Damage (Modules 3-5)...

    OnAmmoChanged.Broadcast(CurrentAmmo);
    OnFired();                            // let the Blueprint play VFX/SFX
}

void AWeaponBase::Reload()
{
    OnReloadStarted();                    // designer's reload animation/sound
    // After a delay, refill β€” timer instead of Tick (Lesson 5.3):
    GetWorldTimerManager().SetTimer(ReloadTimer, [this]()
    {
        CurrentAmmo = Stats.MagazineSize;
        OnAmmoChanged.Broadcast(CurrentAmmo);
    }, 2.0f, false);
}

βœ… Count the modules in this one class

Components (M3), timers (5.3), delegates (5.4), BlueprintCallable/Pure/ImplementableEvent (6.1), exposed properties (6.2), and a BlueprintType struct + enum (6.3) β€” all working together. This is what "the course keeps building on itself" was for: the finale isn't new syntax, it's composition mastery.

The Blueprint Subclass

Now the designer's half β€” no C++ required. In the editor: right-click AWeaponBase in the C++ Classes folder β†’ Create Blueprint class based on WeaponBase β†’ name it BP_Rifle. Then they:

  1. Set the Mesh's skeletal mesh to the rifle model.
  2. Fill in Stats (Damage 30, FireRate 8, MagazineSize 30) on the default.
  3. Set Type to Assault Rifle from the dropdown.
  4. Implement OnFired in the Event Graph: spawn muzzle flash, play sound, add recoil.
  5. Implement OnReloadStarted: play the reload montage.

πŸ“– What the designer never touches

Ammo counting, the fire invariant (CanFire), the reload timer, damage application β€” all live safely in C++, identical across every weapon. A designer can't accidentally break "you can't fire with zero ammo" because that logic isn't in Blueprint. They get total freedom over content and zero ability to break mechanics. That's the pattern's superpower.

βœ… Ten weapons, one class

BP_Shotgun, BP_Pistol, BP_Sniper… each is a Blueprint asset over the same AWeaponBase, differing only in data and cosmetic events. Balancing the arsenal is editing numbers in Blueprint defaults β€” fast iteration, no recompiles, no programmer bottleneck. This is why studios structure code this way.

Boundary Guidelines

Principles that keep the C++/Blueprint boundary healthy as a project grows:

Do βœ…Avoid ❌
Keep invariants & hot logic in C++Putting core rules in Blueprint where they can be broken
Expose cosmetic hooks as ImplementableEventHardcoding VFX/SFX in C++ (kills designer iteration)
Bundle tuning in BlueprintType structsDozens of loose exposed floats
Guard state: read-only + validated settersBlanket BlueprintReadWrite on everything
Mark reusable bases Abstract, BlueprintableLetting designers place un-configured bases
πŸ’‘ The guiding question: for each piece of a feature, ask "should a designer be able to change this without a programmer, and without being able to break the rules?" If yes β†’ expose it (property or cosmetic event). If it's a rule that must always hold β†’ keep it in C++. Draw the boundary there, and your C++ empowers designers instead of fighting them.

Hands-on Exercise & Quiz

πŸ‹οΈ Exercise: Design an APickupBase

Objective: Apply the pattern to the pickup you've built across the course.

  1. Sketch APickupBase (Abstract, Blueprintable) with: a collision component + mesh (C++), an FPickupEffect struct exposed on the BP default (6.3), the overlap-grant-destroy mechanic in C++ (5.1), and a BlueprintImplementableEvent OnCollected() for the pickup VFX/SFX.
  2. List what BP_HealthPickup, BP_AmmoPickup, and BP_CoinPickup each set β€” with no C++ changes.
  3. Which part must stay in C++ so a designer can't break it?
βœ… Answer sketch
  • C++: the sphere overlap handler that checks capability, applies PickupEffect, calls OnCollected(), and Destroy()s.
  • Each BP subclass sets: the mesh, the FPickupEffect (kind + amount), and implements OnCollected (particle + sound).
  • The overlap-grant-destroy flow stays in C++ so no pickup can, say, grant twice or fail to destroy β€” the invariant is protected.

🎯 Quick Quiz

Question 1: In the pattern, what does the C++ base class own?

Question 2: Which specifiers set up a base to be subclassed in Blueprint but never placed directly?

Question 3: A designer wants custom muzzle-flash VFX per weapon. You should expose:

Summary

πŸŽ‰ Key Takeaways

  • The dominant Unreal pattern: C++ base class of mechanics + Blueprint subclasses of content/tuning.
  • Mark reusable bases UCLASS(Abstract, Blueprintable) β€” subclassable in Blueprint, never placed raw.
  • Keep invariants, hot logic, and state in C++; expose tuning (properties/structs) and cosmetics (ImplementableEvent) to Blueprint.
  • A single well-designed base yields many Blueprint variants β€” new content is a new asset, no recompile, no programmer bottleneck.
  • Draw the boundary by asking: "should a designer change this freely, without being able to break the rules?"

πŸ“š Additional Resources

πŸš€ What's Next?

Module 6 β€” the heart of the course β€” is complete. You can now make C++ and Blueprint one seamless workflow. Module 7 takes the data-driven idea further: DataTables, DataAssets, config, saving/loading, and Subsystems β€” driving whole games from data.

πŸŽ‰ Module 6 complete β€” the keystone is set!

C++ and Blueprint now work as one. Let's make your game data-driven.