< All Topics

Learning CPP

ue architechture and code quality

To succeed as a Tech Lead in Unreal Engine, you must treat the project structure as a battle against Dependency Hell and Compilation Time.

Here is the deep dive into the specific architectural patterns you must enforce to keep a project healthy.


1. The “C++ Sandwich” Pattern (Balancing BP & C++)

The biggest mistake teams make is either “Pure C++” (too slow to iterate) or “Pure Blueprints” (unmaintainable spaghetti).

The Rule: C++ is for Systems. Blueprints are for Content/glue.

  • The Base Class Strategy:
    Never let a designer create a Blueprint that inherits directly from AActor or ACharacter.
    • Wrong: BP_Goblin inherits from ACharacter.
    • Right: BP_Goblin inherits from ACreatureBase (C++), which inherits from ACharacter.
  • The “Hooks” Architecture:
    You write the logic in C++, but you leave “hooks” for designers to add juice (particles, sounds) without touching code.
    • BlueprintCallable: C++ does the math, BP calls the function.
    • BlueprintImplementableEvent: C++ says “I took damage,” BP decides to play a sound.
    • BlueprintNativeEvent: C++ has a default logic, but BP can override and extend it (using Parent: Call Function).

Tech Lead Check: If you see math (vector addition, dot products) in a Blueprint, reject the Pull Request. That belongs in C++.


2. The War on “Hard References” (Memory Management)

This is the specific knowledge that separates Seniors from Leads.

The Problem:
If your BP_Player has a variable UPROPERTY() TSubclassOf<AWeapon> RocketLauncher;, the engine will load the Rocket Launcher into memory the moment the Player is loaded.
If the Rocket Launcher references an Explosion, and the Explosion references a Sound… loading the Player effectively loads the entire game.

The Solution: Soft References (TSoftObjectPtr)
You must enforce the use of Soft Pointers for assets that are not immediately needed.

  • Bad (Hard Reference):UPROPERTY(EditAnywhere) UTexture2D* ProfileImage; // Loads image immediately when class loads
  • Good (Soft Reference):UPROPERTY(EditAnywhere) TSoftObjectPtr<UTexture2D> ProfileImage; // Only stores a string path ("Game/Art/...")
  • The Workflow:
    When the UI opens, you call AssetManager::GetStreamableManager().RequestAsyncLoad(). This loads the texture only when needed.

Tech Lead Check: Periodically use the Reference Viewer (Right-click asset -> Reference Viewer). If your Main Menu references your Final Boss, you have failed this architecture rule.


3. Interface-Based Decoupling (Avoiding Casts)

In Blueprints, developers love to use Cast To BP_Player.
This is architectural suicide.
If a BP_Door casts to BP_Player, the Door now “knows” about the Player. You cannot load the Door without loading the Player.

The Solution: UInterface
Create a C++ Interface IInteractable.

  1. The Door implements IInteractable.
  2. The Player sends a generic message: “Interact with the object in front of me.”
  3. The Player does not need to know it is a door. The Door does not need to know who opened it.

// C++ Interface class IInteractable { GENERATED_BODY() public: UFUNCTION(BlueprintNativeEvent, Category = "Interaction") void OnInteract(AActor* Instigator); };

Tech Lead Check: Search the project for Cast To. If you see casting used for generic gameplay mechanics, force a refactor to Interfaces.


4. Data-Driven Design (UPrimaryDataAsset)

Don’t let designers put stats (Health = 100, Damage = 50) inside the BP_Enemy defaults. This buries data inside logic files.

The Solution: Data Assets
Create a C++ class inheriting from UPrimaryDataAsset.

UCLASS(BlueprintType) class UEnemyData : public UPrimaryDataAsset { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly) float MaxHealth; UPROPERTY(EditDefaultsOnly) TSoftObjectPtr<USkeletalMesh> Mesh; };

  • Why?
    1. Designers can mass-edit 50 enemies in seconds without opening 50 Blueprints.
    2. You can reference the Data (lightweight) without loading the Mesh (heavy).
    3. Your BP_Enemy just has one variable: EnemyDataAsset. On BeginPlay, it reads the data and sets itself up.

5. Module-Based Architecture (Compile Times)

By default, UE5 puts everything in one module (YourGame). As the project grows, changing one line of code takes 5 minutes to compile.

The Solution: Split the Code
Split your code into distinct modules in the .uproject and Source folder:

  1. YourGame_Core: The heavy math, basic types. (Dependencies: Engine)
  2. YourGame_Network: Networking logic. (Dependencies: Core)
  3. YourGame_UI: Widgets and HUD. (Dependencies: Core)
  4. YourGame_Weapons: Specific gun logic. (Dependencies: Core)

Why?
If you change a file in YourGame_UI, the compiler only recompiles that module. It doesn’t touch the Networking or Core code. This saves hours of dev time per week.


6. Subsystems (The Singleton Replacement)

Stop putting logic in GameInstance or GameMode. Those classes become “God Objects” with 5,000 lines of code.

The Solution: GameInstanceSubsystem
UE5 has auto-managed lifetimes called Subsystems.

  • Instead of writing ScoreManager inside GameInstance, create UScoreSubsystem : public UGameInstanceSubsystem.
  • The engine automatically creates it when the game starts and destroys it when the game ends.
  • Access: GetGameInstance()->GetSubsystem<UScoreSubsystem>().

This keeps your code clean, separated, and modular without needing to manually manage “Manager” actors in the scene.


Summary Checklist for an Architectural Audit

  1. Reference Viewer: Are we using Soft Pointers for assets not needed in Frame 1?
  2. Interfaces: Are we using Interfaces to stop Actors from casting to each other?
  3. Data Assets: Are stats stored in DataAssets, not inside Actor Blueprints?
  4. Base Classes: Do all Blueprints inherit from a custom C++ Base Class?
  5. Subsystems: Are global systems (Score, Inventory, SaveSystem) separated into Subsystems?