🏆 Lesson 12.1: Capstone — A Complete C++ Gameplay Feature
This is it — the moment every module has led to. You'll build a complete collectible system: collectibles in the world that a player gathers, tracked by a subsystem, shown on a live HUD, saved to disk, and fully networked. It uses every skill in the course, woven into one shippable feature. No new syntax — just mastery, assembled.
🎯 Learning Objectives
By the end of this capstone, you will be able to:
- Architect a full gameplay feature spanning data, actors, UI, persistence, and networking
- Build a designer-extensible C++ base class with a clean interop surface
- Route state through a subsystem and drive UI with events
- Make the whole feature save/load and replicate correctly
- See how the course's patterns compose into professional Unreal C++
Estimated Time: 90 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
The Blueprint of the Feature
We're building a collectible system — think coins, gems, or "collect the orbs." A player walks over a collectible; it's added to their collection; the HUD updates; the total persists across sessions; and it all works in multiplayer. Here's the architecture:
type, value, mesh, sound"] --> A["ACollectibleBase (C++, Abstract)
overlap → collect → destroy"] A -->|"server grants"| Sub["UCollectionSubsystem
counts per type + OnCollectionChanged"] Sub -->|"event"| HUD["Collection HUD widget"] Sub -->|"gather/apply"| Save["USaveGame slot"] A -->|"BP subclass"| BP["BP_GoldCoin, BP_Gem..."]
📖 Plan before code — the three lenses
We plan the feature through three lenses you've internalized: the interop boundary (what designers configure vs what C++ enforces, Module 6), the data flow (events, not polling, Modules 5/9), and the network categories (authoritative / replicated / cosmetic, Module 10). Nail those three and the code writes itself.
Step 1: The Data Layer
Start with data (Modules 2, 6, 7). A UENUM for the kind, a DataAsset for each collectible's config — so designers add new collectible types with no code.
// CollectibleTypes.h
UENUM(BlueprintType)
enum class ECollectibleType : uint8 // uint8 for BlueprintType (Lesson 6.3)
{
Coin UMETA(DisplayName = "Coin"),
Gem UMETA(DisplayName = "Gem"),
Key UMETA(DisplayName = "Key")
};
// A rich config object referenced directly (Lesson 7.1 DataAsset).
UCLASS(BlueprintType)
class MYPROJECT_API UCollectibleData : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Collectible")
ECollectibleType Type = ECollectibleType::Coin;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Collectible")
int32 Value = 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Collectible")
FText DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Collectible")
TObjectPtr<UStaticMesh> Mesh;
};
✅ Data first, always
Leading with the data layer means the rest of the system reads from configuration rather than hardcoded values. A designer makes DA_GoldCoin (value 10) and DA_RubyGem (value 50) as assets — the C++ never changes. This is the data-driven mindset from Module 7, and it's why the feature scales to any number of collectible types.
Step 2: The Collectible Actor
The C++ base (Modules 3, 5, 6, 10) — a networked collectible with the overlap-grant-destroy mechanic, an interop surface, and a cosmetic hook for designers.
// CollectibleBase.h
UCLASS(Abstract, Blueprintable) // designer-extensible base (Lesson 6.4)
class MYPROJECT_API ACollectibleBase : public AActor
{
GENERATED_BODY()
public:
ACollectibleBase();
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Collectible")
TObjectPtr<USphereComponent> Collision;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Collectible")
TObjectPtr<UStaticMeshComponent> Mesh;
// Designers assign a DataAsset per Blueprint subclass (Step 1).
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Collectible")
TObjectPtr<UCollectibleData> Data;
// Replicated so late/lagging clients see it as taken (Lesson 10.2).
UPROPERTY(ReplicatedUsing = OnRep_Collected)
bool bCollected = false;
UFUNCTION()
void OnRep_Collected();
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* C, AActor* Other, UPrimitiveComponent* OC,
int32 Idx, bool bSweep, const FHitResult& Hit);
// Cosmetic hook designers implement in Blueprint (Lesson 6.1).
UFUNCTION(BlueprintImplementableEvent, Category = "Collectible")
void OnCollectedFX();
// Cosmetic broadcast to all machines (Lesson 10.3).
UFUNCTION(NetMulticast, Unreliable)
void MulticastCollectedFX();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>&) const override;
};
// CollectibleBase.cpp
#include "Net/UnrealNetwork.h"
ACollectibleBase::ACollectibleBase()
{
PrimaryActorTick.bCanEverTick = false; // event-driven, no Tick (Lesson 11.2)
bReplicates = true; // networked (Lesson 10.2)
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
RootComponent = Collision;
Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(RootComponent);
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void ACollectibleBase::BeginPlay()
{
Super::BeginPlay();
if (Data && Data->Mesh) { Mesh->SetStaticMesh(Data->Mesh); } // data-driven visuals
Collision->OnComponentBeginOverlap.AddDynamic(this, &ACollectibleBase::OnBeginOverlap);
}
void ACollectibleBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
Super::GetLifetimeReplicatedProps(Out);
DOREPLIFETIME(ACollectibleBase, bCollected);
}
void ACollectibleBase::OnBeginOverlap(UPrimitiveComponent*, AActor* Other,
UPrimitiveComponent*, int32, bool, const FHitResult&)
{
// Server decides (Lesson 10.1); ignore on clients & if already taken.
if (!HasAuthority() || bCollected || !IsValid(Other) || !Data) { return; }
// Grant to the collector's subsystem (Step 3), if it's a player.
if (APawn* Pawn = Cast<APawn>(Other))
{
if (UGameInstance* GI = GetGameInstance())
{
if (UCollectionSubsystem* Coll = GI->GetSubsystem<UCollectionSubsystem>())
{
Coll->AddCollectible(Data->Type, Data->Value); // authoritative grant
}
}
}
bCollected = true; // replicates (Lesson 10.2)
MulticastCollectedFX(); // cosmetic for everyone (Lesson 10.3)
SetLifeSpan(0.1f); // let the multicast land, then destroy (Lesson 10.4)
}
void ACollectibleBase::OnRep_Collected()
{
if (bCollected) { Mesh->SetVisibility(false); } // hide instantly on clients
}
void ACollectibleBase::MulticastCollectedFX_Implementation()
{
OnCollectedFX(); // run the designer's Blueprint VFX/SFX on every machine
}
✅ Count the modules in one actor
Components & construction (3.2), collision (5.1), Cast (2.2), authority (10.1), replication + RepNotify (10.2), multicast (10.3), the destroy-timing trick (10.4), a BlueprintImplementableEvent cosmetic hook (6.1), data-driven visuals (7.1), no-Tick performance (11.2), and the Abstract, Blueprintable base pattern (6.4). Every one is a habit now — you're composing, not recalling.
Step 3: The Collection Subsystem
The tally lives in a GameInstance subsystem (Module 7) so it persists across levels and is reachable from anywhere. It broadcasts an event (Module 5) whenever the collection changes.
// CollectionSubsystem.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FOnCollectionChanged, ECollectibleType, Type, int32, NewTotal); // Lesson 5.4
UCLASS()
class MYPROJECT_API UCollectionSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Collection")
void AddCollectible(ECollectibleType Type, int32 Amount);
UFUNCTION(BlueprintPure, Category = "Collection")
int32 GetCount(ECollectibleType Type) const;
// Persistence (Lesson 7.3):
UFUNCTION(BlueprintCallable, Category = "Collection")
void SaveTo(const FString& Slot);
UFUNCTION(BlueprintCallable, Category = "Collection")
void LoadFrom(const FString& Slot);
UPROPERTY(BlueprintAssignable, Category = "Collection")
FOnCollectionChanged OnCollectionChanged; // the HUD subscribes to this
private:
UPROPERTY()
TMap<ECollectibleType, int32> Totals; // TMap (Lesson 2.4)
};
// CollectionSubsystem.cpp
void UCollectionSubsystem::AddCollectible(ECollectibleType Type, int32 Amount)
{
int32& Total = Totals.FindOrAdd(Type);
Total += Amount;
OnCollectionChanged.Broadcast(Type, Total); // notify listeners (HUD)
}
int32 UCollectionSubsystem::GetCount(ECollectibleType Type) const
{
const int32* Found = Totals.Find(Type);
return Found ? *Found : 0;
}
📖 The subsystem is the hub
Everything routes through here: the collectible grants to it, the HUD listens to it, the save reads/writes it. Because it's a GameInstance subsystem, the collection survives level transitions (Lesson 7.4), and any system can reach it via GetSubsystem. A clean central service like this — not scattered globals — is what a well-architected feature looks like.
Step 4: HUD, Save & Network
Now the three finishing systems, each a few lines because the groundwork is done.
The HUD listens (Module 9)
void UCollectionHUD::NativeConstruct()
{
Super::NativeConstruct();
if (UGameInstance* GI = GetGameInstance())
{
if (UCollectionSubsystem* Coll = GI->GetSubsystem<UCollectionSubsystem>())
{
Coll->OnCollectionChanged.AddDynamic(this, &UCollectionHUD::HandleChanged); // 5.4/9.2
HandleChanged(ECollectibleType::Coin, Coll->GetCount(ECollectibleType::Coin)); // prime
}
}
}
void UCollectionHUD::HandleChanged(ECollectibleType Type, int32 NewTotal)
{
if (CoinText && Type == ECollectibleType::Coin) // BindWidget (Lesson 9.1)
{
CoinText->SetText(FText::AsNumber(NewTotal));
}
}
The save persists (Module 7)
// A USaveGame holds the collection as plain, serializable data (Lesson 7.3).
void UCollectionSubsystem::SaveTo(const FString& Slot)
{
UCollectionSave* Save = Cast<UCollectionSave>(
UGameplayStatics::CreateSaveGameObject(UCollectionSave::StaticClass()));
Save->Totals = Totals; // copy the map into the save
UGameplayStatics::SaveGameToSlot(Save, Slot, 0);
}
void UCollectionSubsystem::LoadFrom(const FString& Slot)
{
if (!UGameplayStatics::DoesSaveGameExist(Slot, 0)) { return; }
if (UCollectionSave* Save = Cast<UCollectionSave>(
UGameplayStatics::LoadGameFromSlot(Slot, 0)))
{
Totals = Save->Totals;
// Re-broadcast so the HUD reflects the loaded values:
for (const TPair<ECollectibleType, int32>& P : Totals)
{
OnCollectionChanged.Broadcast(P.Key, P.Value);
}
}
}
✅ Networking came almost for free
Notice we barely wrote network code in this step — the collectible actor (Step 2) already handled authority, replication, and multicast. The grant happens on the server; in a full multiplayer build you'd house per-player collections on the PlayerState (Lesson 3.1/10.4) and replicate them, reusing the exact same RepNotify → broadcast → HUD chain. The architecture anticipated multiplayer, so scaling to it is incremental, not a rewrite.
Step 5: The Designer's Half
Here's the proof the interop boundary is right — a designer builds real content with zero C++:
- Create DataAssets:
DA_GoldCoin(Coin, value 10),DA_Ruby(Gem, value 50). - Create Blueprint subclasses of
ACollectibleBase:BP_GoldCoin,BP_Ruby— assign each its DataAsset. - Implement
OnCollectedFXin each Blueprint: spawn a sparkle, play a chime — tuned live, no compile. - Place them around the level, or spawn them from a
TSubclassOf<ACollectibleBase>spawner (Lesson 3.4). - Design the Collection HUD widget layout and reparent it to
UCollectionHUD.
📖 This is the whole thesis of the course
The programmer owns the mechanics, invariants, networking, and persistence — the things that must be correct. The designer owns the content, feel, and placement — the things that must be iterated. Neither can break the other's work. That division, powered by C++/Blueprint interop, is what makes Unreal teams productive, and it's what you now know how to build. It's the promise from Lesson 1.1, delivered.
Every Module, One Feature
Look back at what this single feature touched — the whole course, composed:
| Module | What it contributed to the capstone |
|---|---|
| 1–2 Foundations | Classes, reflection, types (TMap, FText), memory (TObjectPtr, GC-safe refs) |
| 3 Gameplay framework | The collectible actor, components, spawning, communication |
| 4 Input & control | The player pawn that walks into collectibles |
| 5 Interaction & events | Collision overlap, the OnCollectionChanged delegate |
| 6 Interop | The Abstract, Blueprintable base, exposed data, cosmetic events |
| 7 Data & persistence | The DataAsset, the subsystem hub, the SaveGame |
| 8 Build & plugins | Could ship as a reusable "CollectibleSystem" plugin |
| 9 UI | The event-driven collection HUD |
| 10 Networking | Authority, replication, RepNotify, multicast FX |
| 11 Performance | No-Tick, event-driven, cache-friendly design |
✅ You didn't learn 44 tricks — you learned one way of thinking
The capstone needed no new syntax because the course was never about syntax. It was about a way of building: C++ for correct mechanics, Blueprint for content; events over polling; the loosest coupling that works; data-driven design; authoritative networking; measured performance. Those principles composed into this feature, and they'll compose into whatever you build next.
Course Conclusion
🎓 You've completed C++ for Unreal Engine
From "why C++ at all?" to a networked, data-driven, persistent gameplay feature — you've crossed the full distance. You can now:
- Write idiomatic Unreal C++ that works with the engine's systems, not against them
- Design clean C++/Blueprint boundaries that empower designers
- Build data-driven, persistent, networked gameplay
- Structure, extend, and profile a real project
Most importantly, you have the judgment to choose the right tool — and that's what separates someone who writes Unreal C++ from someone who engineers with it.
🚀 Where to Go Next
- Build something. A small complete game teaches more than any lesson. Pick a scope you can finish.
- Read engine source. You now have the vocabulary — step into
ACharacter,UGameplayStatics, the movement component, and learn from Epic's own code. - Go deeper on a specialization. The Gameplay Ability System (GAS), advanced networking & prediction, editor tooling, or Niagara/rendering in C++ are natural next frontiers.
- Package a plugin. Turn a system you love — maybe this collectible system — into a reusable plugin (Module 8) for your next project.
📚 Additional Resources
- Programming with C++ in Unreal Engine (Epic)
- Epic Developer Community — Learning
- Unreal Engine C++ API Reference
🎉 Congratulations — you did it!
Twelve modules, forty-four lessons, one complete feature. You're an Unreal C++ programmer now. Go build something remarkable.