The core philosophy, the hybrid pattern, when to refactor, and Blueprint Interfaces for decoupled systems.
The single most important principle for scalable Unreal development:
This isn't a preference — it's an architectural boundary. Blurring it creates projects that are hard to maintain, hard to iterate on, and hard to hand off to other developers or designers.
| Belongs in C++ | Belongs in Blueprint |
|---|---|
| Damage calculation logic | Damage values and curves per weapon |
| Movement component rules | Walk speed, jump height per character |
| Interaction interface definition | What each interactable actually does |
| Health component with die/respawn | Death VFX, sound, respawn location |
| AI behavior tree structure | Specific patrol routes, aggro ranges |
If you follow this rule consistently, designers can tune and configure gameplay without touching C++, and engineers can change rules without breaking Blueprint graphs.
The most powerful Unreal workflow is the hybrid pattern: define a base class in C++, then derive Blueprint subclasses for actual in-game use.
// C++ — defines the rules
UCLASS()
class AWeapon : public AActor
{
GENERATED_BODY()
public:
// Exposed to Blueprint — designer fills in the value
UPROPERTY(EditDefaultsOnly, Category="Stats")
float BaseDamage = 25.f;
// Callable from Blueprint — C++ defines behavior
UFUNCTION(BlueprintCallable)
void Fire();
};
From this single C++ base class, you can derive:
BP_Rifle — sets BaseDamage to 30, configures a rifle mesh and soundBP_Shotgun — sets BaseDamage to 15, adds pellet spread logic in BlueprintBP_Sniper — sets BaseDamage to 100, adds scope zoom behaviorEach variant is a Blueprint subclass that only specifies what makes it different. The shared firing logic, damage application, and lifetime management all live once in C++.
| Specifier | Meaning |
|---|---|
EditDefaultsOnly | Editable in the Blueprint Class Defaults — the most common for per-type tuning |
EditInstanceOnly | Editable per placed instance in the level |
EditAnywhere | Editable in both Class Defaults and instances |
BlueprintReadWrite | Blueprint can read and write the value at runtime |
BlueprintReadOnly | Blueprint can read but not write — good for exposing state |
There are two clear signals that tell you the balance has shifted and it's time to move work between layers:
Copy/paste in Blueprints is the same code smell as copy/paste in C++. If the same graph appears in BP_Door and BP_Elevator, extract the shared logic into a C++ base class or a Blueprint Function Library, then call it from both.
Blueprint Interfaces are the most scalable way to communicate between Blueprints without creating hard dependencies. An interface defines a contract — a function that any implementing class promises to respond to — without specifying how.
Define an interface with a single function: Interact. Any Blueprint that implements BPI_Interactable promises to respond to that call.
// Player interaction logic — no knowledge of what it's hitting
FHitResult Hit;
if (LineTrace(Hit))
{
// Call Interact on whatever was hit — door, switch, NPC, pickup
IInteractable::Execute_Interact(Hit.GetActor(), this);
}
The player's interaction code does not know — and does not care — whether the hit actor is a door, a light switch, an NPC, or a pickup. Each implements Interact differently:
BP_Door — opens or closesBP_Switch — toggles a connected lightBP_NPC — starts a dialogueBP_Pickup — adds to inventory and destroys itself| Cast To | Interface | |
|---|---|---|
| Couples caller to | Specific class | A contract only |
| Adding new types | Requires modifying caller | Zero caller changes |
| Best for | Known relationships | Open-ended systems |
Use a Cast when you know exactly what type you're dealing with and need class-specific functionality. Use an Interface when you want a system to work with any object that satisfies a contract, regardless of what it actually is.