[UE5] Dividing Work Between Blueprint and C++: What Goes Where

Created: 2025-12-12Last updated: 2026-07-20

An illustrated guide to splitting work between Blueprint and C++ in UE. Covers the C++ parent / BP child pattern, a quick reference for what goes where, why you should not put everything in C++ or everything in Blueprint, and a hands-on split of an enemy character class.

Once a project grows a little, you start hesitating every time you add a feature. Should this enemy's HP logic live in C++, or is Blueprint fine? Without a rule, the answer becomes whatever felt right that day, and later you cannot find anything.

UE has a standard shape for this split: write the parent class in C++ and create its child in Blueprint. This article covers that shape, the criteria for deciding what goes where, and a walkthrough of actually splitting an enemy character class.

A C++ foundation block on the left and Blueprint node cards on the right, with a single enemy assembled from both

What You'll Learn

  • The basic shape is C++ parent, Blueprint child
  • The deciding question is "does this change often, or never?"
  • Why not to put everything in C++: your iteration slows down
  • Why not to put everything in Blueprint: no readable diffs, and references snowball
  • Hands-on: split an enemy character class across C++ and BP

Sponsored

The Basic Shape: C++ Parent, Blueprint Child

UE classes connect through inheritance, and a class you write in C++ can serve directly as a Blueprint parent. That is where the whole division of labor comes from.

A hierarchy diagram with three child BPs, BP_Slime, BP_Skeleton, and BP_Boss, branching from the C++ class AEnemyBase. The C++ side is labeled HP calculation, the BP side meshes and numbers
LayerResponsibilityExamples
C++ (parent)The skeleton that never changesHP subtraction, death checks, state type definitions, communication plumbing
Blueprint (child)Everything that keeps changingMax HP values, which mesh to use, hit effects, death SFX

The benefit shows up in how fixes propagate. To change the HP formula you touch one C++ parent, and it reaches all 20 child BPs. To change the slime's HP from 50 to 60 you touch one number in BP_Slime, and no build happens.

Creating a child Blueprint from a C++ class is covered in Your First Step from Blueprint to C++. This article is the next question: what belongs in the parent.


The Deciding Question: "Does This Change Often?"

Splitting by "C++ is faster" usually goes wrong. The axis that actually works is a single one: how often will this code change from here on?

A balance scale with "things that never change = C++" on the left pan and "things that change daily = Blueprint" on the right pan
  • Things that rarely change go to C++. If it barely changes, you almost never pay the build-wait cost
  • Things that change daily go to Blueprint. Numbers, assets, and presentation get swapped constantly as the design evolves

There is a secondary axis too: do you need many copies of the same thing? If 20 enemies share the same logic, that logic is easier to distribute from a C++ parent. If a gimmick only appears in one level, leaving it in Blueprint is fine.

As for speed, you only need to think about it once you have measured a real bottleneck. See the stat command article for how to measure.


Quick Reference for What Goes Where

Here are the cases that cause the most hesitation. The best answer shifts with project size, but for solo work through small teams, this layout will not steer you wrong.

ItemWhere it goesWhy
Struct / Enum type definitionsC++Blueprint versions tend to corrupt existing data when fields change
Base classes (Character, GameMode, Component)C++The foundation handed to children. Changes reach everything
Subsystems (save, settings, progression)C++Persistent systems whose specs settle down early
Per-frame loops and bulk processing of many actorsC++One of the few places where execution speed actually differs
Stat values (HP, speed, cooldown)Blueprint / Data AssetYou do not want a build between every tuning pass
Which mesh, animation, or material to useBlueprintAssigning asset references in the editor is faster
VFX, SFX, camera shakes and other presentationBlueprintThe highest swap frequency of anything
UMG widget layout and animationBlueprintDesigners touch the visuals directly
Level-specific gimmicks (a door used in one stage)BlueprintThe cost of C++ does not pay off for one-offs

Note: Stat values can either live in the child BP's defaults or be extracted into a Data Asset. With three to five enemy types, child BPs are plenty. Past twenty types, moving to Data Assets makes them much easier to manage.

Sponsored

Why Not Put Everything in C++

Leaning too hard on C++ slows development down for reasons that have nothing to do with code quality.

A contrast diagram: on the all-C++ side a figure waits watching an hourglass, while on the BP side a figure moves a slider immediately
  • Every tweak means waiting for a build: changing an enemy's HP from 50 to 60 becomes edit code, build, restart the editor. Even at tens of seconds each, doing it dozens of times a day adds up
  • Touching the header means closing the editor: adding a single variable cannot go through Live Coding. Hitting that mid-experiment breaks your train of thought
  • Some things are simply faster in the editor: Timelines, curves, UMG layouts, Animation Blueprint states. These are adjusted by eye, so writing them in code buys you nothing
  • Non-programmers lose access: on a team, if numbers and presentation are not exposed on the BP side, every tuning request lands on a programmer

Why Not Put Everything in Blueprint

Push through with Blueprint alone and a different set of problems shows up once the project grows.

  • Git cannot show diffs: Blueprints are binary assets. You cannot review what changed since yesterday, and if two people edit the same BP, one person's work has to be thrown away
  • References snowball: when a BP directly references another BP, loading one pulls the other into memory with it. Stacked up, that comes back as startup load time
  • No sweeping edits: when you want to rename a function, neither text search nor find-and-replace is available. You open every referencing asset and fix them one by one
  • Struct changes corrupt data: adding a field or changing a type on a Blueprint-authored Struct can reset the existing values that used it. Keeping type definitions in C++ avoids that accident
Left: BPs referencing each other directly, so opening one loads the whole chain. Right: dependencies reduced by going through a C++ parent

Note: You can also shrink reference bloat from the BP side. Putting a Blueprint Interface or an Event Dispatcher between BPs instead of a direct reference thins out the dependency lines. It is worth reviewing that before moving anything to C++.

Sponsored

Hands-On: Splitting an Enemy Character Class

Let's build one enemy, split across C++ and Blueprint. A trash mob in a top-down ARPG, a grunt in a side-scrolling action game, an attacking unit in a tower defense: "it has HP, it takes damage, it dies at zero" is the same in every genre.

The Finished Result

A sequence showing a slime hit three times, its HP bar dropping 50 to 30 to 10 to 0 before it falls. Labeled C++ calculates at the top, BP presents at the bottom

The C++ class AEnemyBase handles everything up to "take damage, reduce HP, die at zero." The child BP only holds the max HP value and the hit and death presentation.

Setup

Give AEnemyBase (parent class: Character) the following members.

MemberTypeInitial valueSpecifiersWhere it lives
MaxHealthfloat100.0fEditDefaultsOnly, BlueprintReadOnlyDeclared in C++, value set in BP
CurrentHealthfloat0.0fVisibleAnywhere, BlueprintReadOnlyC++ only
bIsDeadboolfalseVisibleAnywhere, BlueprintReadOnlyC++ only
TakeDamage(...)virtual float override(existing AActor function)C++ only
GetHealthPercent()const function returning floatBlueprintPureImplemented in C++, used in BP
OnDamagedEffect(float)function (one float param)BlueprintImplementableEventDeclared in C++, implemented in BP
OnDeathEffect()function (no params)BlueprintImplementableEventDeclared in C++, implemented in BP

The key detail is that MaxHealth uses EditDefaultsOnly. That means it can be edited in the child BP's class defaults but not per instance placed in a level, which prevents the accident of two identical slimes having different HP depending on where they were dropped. For a full read on specifiers, see the first-step article.

C++ Side: The Skeleton That Never Changes

// EnemyBase.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "EnemyBase.generated.h"

UCLASS()
class MYPROJECT_API AEnemyBase : public ACharacter
{
    GENERATED_BODY()

public:
    AEnemyBase();

    // Override the existing AActor function. This is the entry point for Apply Damage
    virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
                             AController* EventInstigator, AActor* DamageCauser) override;

    // For wiring to an HP bar. Becomes a green node with no execution pins
    UFUNCTION(BlueprintPure, Category = "Enemy|Status")
    float GetHealthPercent() const;

protected:
    virtual void BeginPlay() override;

    // Set in the child BP's class defaults. Not editable per instance
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Enemy|Status")
    float MaxHealth = 100.0f;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy|Status")
    float CurrentHealth = 0.0f;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy|Status")
    bool bIsDead = false;

    // Presentation is implemented on the Blueprint side
    UFUNCTION(BlueprintImplementableEvent, Category = "Enemy|Effects")
    void OnDamagedEffect(float DamageAmount);

    UFUNCTION(BlueprintImplementableEvent, Category = "Enemy|Effects")
    void OnDeathEffect();

    void Die();
};
// EnemyBase.cpp
#include "EnemyBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"

AEnemyBase::AEnemyBase()
{
    PrimaryActorTick.bCanEverTick = false;
}

void AEnemyBase::BeginPlay()
{
    Super::BeginPlay();

    // Use the MaxHealth set in the child BP as the starting HP
    CurrentHealth = MaxHealth;
}

float AEnemyBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
                             AController* EventInstigator, AActor* DamageCauser)
{
    const float ActualDamage =
        Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

    // Do nothing if already dead or the damage is zero or less
    if (bIsDead || ActualDamage <= 0.0f)
    {
        return 0.0f;
    }

    CurrentHealth = FMath::Clamp(CurrentHealth - ActualDamage, 0.0f, MaxHealth);

    UE_LOG(LogTemp, Log, TEXT("%s took %.1f damage. HP: %.1f / %.1f"),
           *GetName(), ActualDamage, CurrentHealth, MaxHealth);

    // Leave the hit presentation to the BP side
    OnDamagedEffect(ActualDamage);

    if (CurrentHealth <= 0.0f)
    {
        Die();
    }

    return ActualDamage;
}

void AEnemyBase::Die()
{
    bIsDead = true;

    // Stop collision and movement once it goes down
    GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    GetCharacterMovement()->DisableMovement();

    // Leave the death presentation to the BP side
    OnDeathEffect();
}

float AEnemyBase::GetHealthPercent() const
{
    return (MaxHealth > 0.0f) ? (CurrentHealth / MaxHealth) : 0.0f;
}

Blueprint Side: Just Values and Presentation

Right-click EnemyBase, create BP_Slime, and set the following.

SettingValue
Max Health (class defaults)50.0
MeshSlime skeletal mesh
Anim ClassABP_Slime
Event On Damaged EffectHit SFX + white material flash
Event On Death EffectDissolve VFX + Destroy Actor after 2 seconds
BP_Slime (child class)
Event On Damaged Effect (receives Damage Amount)
  → Play Sound at Location (Sound: SFX_SlimeHit)
  → Set Scalar Parameter Value on Materials (Parameter: "HitFlash", Value: 1.0)
  → Delay (0.1) → Set Scalar Parameter Value on Materials (Value: 0.0)

Event On Death Effect
  → Spawn System at Location (System: NS_SlimeBurst)
  → Play Anim Montage (Montage: AM_SlimeDeath)
  → Delay (2.0) → Destroy Actor

Make BP_Skeleton (Max Health = 120.0) the same way and your second enemy is done without adding a single line to C++.

Verify

Call Apply Damage (Damage = 20.0, Damaged Actor = the enemy) from the player's attack and hit BP_Slime three times.

  • The first hit drops HP from 50 to 30, and the Output Log shows BP_Slime_C_0 took 20.0 damage. HP: 30.0 / 50.0
  • The second hit takes it from 30 to 10, with the hit SFX and white flash playing both times
  • The third hit stops HP at 0, not -10 (the Clamp is doing its job), the death VFX plays, and it disappears after 2 seconds
  • If you wired an HP bar to GetHealthPercent, it moves 1.0 → 0.6 → 0.2 → 0.0

If HP never drops, check that the attacker is calling Apply Damage. TakeDamage is UE's damage entry point, so it is reached through Apply Damage rather than called directly (how to send and receive Apply Damage is covered in Health and Damage in UE5). If the presentation never plays, check that the Event On Damaged Effect node is actually placed in the child BP's event graph (see the debugging article).

Two things to take away.

  • C++ holds "the things that must happen in this order": reduce HP, clamp at zero, set the death flag, disable collision. That order is identical for every enemy, so letting child BPs write it produces one ordering bug per enemy type
  • All you hand to BP is "when to call": OnDamagedEffect has no idea what it does. Swapping the presentation triggers no C++ build, and fixing the HP math leaves the presentation untouched. This split follows the same thinking as Event Dispatchers
Sponsored

Bonus: Good to Know for Later

  • You do not have to start in C++: prototyping in Blueprint and lifting the parent class into C++ once the shape settles is a perfectly good order. Existing BPs can be re-pointed at a C++ parent with File > Reparent Blueprint
  • BlueprintNativeEvent is the middle ground: BlueprintImplementableEvent can only be implemented in BP, but BlueprintNativeEvent lets you write a default implementation in C++ that only the child BPs that need to can override. Good for "usually the same, occasionally different" logic
  • Split categories with Parent|Child: writing Category = "Enemy|Status" produces nested headings in the Details panel. It starts paying off once you pass about ten C++ variables
  • Do not count on Blueprint Nativization: the feature that auto-converted BPs to C++ was removed in UE5. Where speed matters, write the C++ by hand from the start
  • When in doubt, put it in Blueprint: moving something to C++ later is possible, but moving C++ back to BP is a chore. If you cannot decide, get it working in BP first and see how it behaves

Summary

  • The basic shape is C++ parent, Blueprint child. Fixes flow from C++ to everything; tuning happens per BP
  • The deciding axis is not speed but "does this change often?" Unchanging things to C++, daily changes to BP
  • Type definitions (Structs and Enums), base classes, and Subsystems go in C++; numbers, assets, and presentation go in Blueprint
  • All-C++ costs you iteration speed; all-Blueprint traps you in diffs and references
  • The core of the hands-on is that C++ owns the order and BP owns the look, so neither blocks the other's builds and swaps

Of the features you are building right now, which ones have you changed the value of three or more times this week? Those belong on the Blueprint side.