[UE5] Dividing Work Between Blueprint and C++: Share the HP Math, Vary How Each Enemy Falls

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

Where to split Blueprint and C++, thought through with enemy HP and effects. Shared HP math in C++, max HP and death effects in child Blueprints, verified with Apply Damage. Also diagrams judgment beyond change frequency, plus references and diffs.

You want to change the slime's HP from 50 to 70. You want it to linger a moment after falling rather than vanishing instantly. Meanwhile, you want the bug where HP goes negative fixed for every enemy at once.

Even within one enemy feature, the things you want to change are separate. Splitting the HP math everyone shares from the numbers and effects you tune per kind makes it easier to see where to make a fix.

Combining C++'s shared logic with Blueprint's tuning to build one enemy

In this article we put HP management in a C++ parent class and vary max HP and how they fall in child Blueprints. It assumes you can add a C++ class and build. For first-time environment setup and how to read the macros, start from First steps into C++.

What You'll Learn

  • Thinking of shared logic and per-kind settings separately
  • Calling a child Blueprint's effect event from C++
  • Sending 20 damage and confirming HP drops 50 → 30 → 10 → 0
  • Tuning enemy health and time-to-vanish without changing C++

Sponsored

The basic shape: different enemies from the same parent

A parent class is a plan holding shared logic and variables. A child class inherits that construction and uses it. Here we make the C++ EnemyBase the parent and create child Blueprints BP_Slime and BP_Golem.

Inheriting EnemyBase's HP math, BP_Slime sets HP 50 and BP_Golem sets HP 120 with their own visuals
Where it livesIts responsibility here
The C++ parentReduce HP on damage. Never go below 0. Do not run twice once already dead
The child BlueprintChoose max HP and appearance. Show the hit, and build the post-death motion

To fix the HP formula, you change one place in the parent C++. To make just slimes tougher, you change BP_Slime's max HP. Shared fixes reach the parent; per-kind tuning reaches the child.

This is not a form of sharing only C++ can do. A Blueprint parent or an Actor Component can reuse the same logic too. Here we try the shape of tuning a C++ foundation from Blueprint.

What you judge is "what do you want to change"

"Put frequently changing things in Blueprint" is a useful starting point. But change frequency alone does not decide everything. Start by separating do you want to change a value, or change a rule of the logic?

Comparing the change of tuning max HP from 50 to 70 against fixing the HP math shared by every enemy

"Set the slime's max HP to 70" is a settings change. "Make sure no enemy's HP goes below 0" is a rule change. Both are about HP, but you can separate the place you touch each time.

Where you declare and where you tune are different

Declaring a variable in C++ does not require hard-coding its value there too.

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Enemy|Status")
float MaxHealth = 100.0f;

This says "provide a MaxHealth variable in C++ whose initial value can be changed in the child Blueprint's Class Defaults." Set it to 50 in BP_Slime and enemies made from that child start at 50. Tuning numbers alone can be tried without a C++ build.

If you are unsure where to write logic, also consider the following.

  • Who tunes it. Logic you want to try while looking at the editor is easier to handle in Blueprint.
  • Whether the feature you need is available as a node. If you need an unexposed API or an external library, C++ becomes a candidate.
  • What is taking time. If performance is the reason, measure the time before choosing the target.

For example, if a heavy calculation is the problem, you can move just that calculation to C++. There is no need to move the enemy's visuals and UI along with it.

A guide for when placement is unclear

What you want to buildExample first placementThe judgment point
Max HP, movement speed, mesh usedChild Blueprint, Data AssetDo you want to tune it without opening code?
Hit and death sounds and visualsBlueprintDo you swap it while looking at it?
Shared rules such as HP mathA shared parent or ComponentIs Blueprint enough, or is there an advantage to C++?
Structs and Enums used from bothDefine the type in C++Do you need the same type on both sides?
Enemy AI or level gimmicks in prototypeBlueprintTry the behavior first and split once shared parts appear
A loop measured and found heavyConsider moving to C++Does that work need to happen every time at all?

Structs and Enums used only in Blueprint are fine created in Blueprint. A "type" is the shape of data: what fields an item has, what states an enemy can be in. If you plan to use the same shape in C++ too, defining it in C++ and exposing it to Blueprint is easier to handle.

There is also no rule that numbers must move once you have a certain number of enemy kinds. When you want to swap whole sets of visuals and numbers, consider a Data Asset.

Things you want to tune while looking go in Blueprint; types used from both sides and heavy loops are C++ candidates
Sponsored

What moving to C++ does not solve on its own

Blueprint also has search and diff

Blueprint has Find References to locate where something is used and the UE Diff Tool to compare before and after. You can check "how does this differ from yesterday's logic."

But a Blueprint .uasset is binary, so it does not suit a workflow of reading line-level diffs and auto-merging in Git the way ordinary code does. Seeing a diff and combining two people's changes into one are different things. If two people touch the same BP, you need an agreement on collaborative work such as dividing who edits what.

How much gets loaded also depends on how you hold references

A hard reference is a connection where one asset directly needs another. Load the source and the target loads with it.

If the UI depends on the BP_Boss type — which owns large visual assets — just to read HP, that dependency affects you even when only using the UI. You can consider putting the HP-reading entry point on a lightweight parent and having the UI use that parent's type.

The difference between the UI depending on the heavy child BP's type and using only the shared parent's type

The arrows in the diagram are asset and type dependencies. On the right, the boss's visuals still load when the boss actually appears. The idea is to reduce the connection where "the type the UI needs also demands the boss's visuals."

In C++ too, hard-referencing meshes from a parent class increases loading. Nor does inserting an Interface erase other remaining references. Confirm the actual connections in the Reference Viewer; designs that load only when needed are covered in soft references and async loading.

C++ Structs still need data verification after changes

Changing an existing type or field name may require carrying over assets and saves that use it. C++ makes changes easier to follow as text, but it does not automatically protect old values. Try large changes on a copy and confirm you can read previous data back.

Hands-On: separating HP management from the falling effect

What we build first

For practice, we build one stationary enemy. Its appearance is the engine's standard Sphere standing in for a slime. Pressing H sends 20 damage at a time; three hits flatten it and it vanishes after 2 seconds. We confirm HP values in the Output Log.

Sending 20 damage at a time drops HP from 50 to 30, 10, 0, and the flattened enemy vanishes after 2 seconds

We move between C++ and Blueprint in this order.

  1. A test Blueprint calls Apply Damage.
  2. The enemy's C++ receives the damage and updates HP.
  3. C++ tells the child Blueprint "you were hit" and "you fell."
  4. The child Blueprint changes its appearance in response.
Sending damage from Blueprint, then reaching the child Blueprint's effect events after C++ updates HP

BlueprintCallable in First steps into C++ meant "BP can call a C++ function." What we use here, BlueprintImplementableEvent, means the body of logic C++ calls is built in BP. C++ decides when to call; the child decides what to show there.

Prepare the C++ parent

Into the CppFirstStep project from First steps into C++, add a C++ class with Actor as parent named EnemyBase. Since we use no walking or animation here, choose Actor rather than Character.

Once the automatic build finishes, save and close UE and replace the two generated files with the following. If trying this in an existing project, change CPPFIRSTSTEP_API to the API macro name generated for that project.

EnemyBase.h

#pragma once

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

UCLASS(Blueprintable)
class CPPFIRSTSTEP_API AEnemyBase : public AActor
{
    GENERATED_BODY()

public:
    AEnemyBase();

    virtual float TakeDamage(float DamageAmount, const FDamageEvent& DamageEvent,
        AController* EventInstigator, AActor* DamageCauser) override;

    UFUNCTION(BlueprintPure, Category = "Enemy|Status")
    float GetHealthPercent() const;

protected:
    virtual void BeginPlay() override;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Enemy|Status",
        meta = (ClampMin = "1.0"))
    float MaxHealth = 100.0f;

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

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

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

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

EnemyBase.cpp

#include "EnemyBase.h"
#include "Components/SceneComponent.h"

AEnemyBase::AEnemyBase()
{
    PrimaryActorTick.bCanEverTick = false;
    SetRootComponent(CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot")));
}

void AEnemyBase::BeginPlay()
{
    MaxHealth = FMath::Max(1.0f, MaxHealth);
    CurrentHealth = MaxHealth;
    bIsDead = false;

    Super::BeginPlay();
    UE_LOG(LogTemp, Log, TEXT("%s HP: %.0f / %.0f"),
        *GetName(), CurrentHealth, MaxHealth);
}

float AEnemyBase::TakeDamage(float DamageAmount, const FDamageEvent& DamageEvent,
    AController* EventInstigator, AActor* DamageCauser)
{
    if (bIsDead || !CanBeDamaged() || DamageAmount <= 0.0f)
    {
        return 0.0f;
    }

    const float AcceptedDamage =
        Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
    if (AcceptedDamage <= 0.0f)
    {
        return 0.0f;
    }

    const float PreviousHealth = CurrentHealth;
    CurrentHealth = FMath::Clamp(CurrentHealth - AcceptedDamage, 0.0f, MaxHealth);

    // Settle the dead state before calling the effects
    bIsDead = (CurrentHealth <= 0.0f);
    if (bIsDead)
    {
        SetCanBeDamaged(false);
        SetActorEnableCollision(false);
    }

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

    OnEnemyHitVisual();
    if (bIsDead)
    {
        OnEnemyDefeatedVisual();
    }

    return PreviousHealth - CurrentHealth;
}

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

After saving, choose Development Editor / Win64 in Visual Studio and build the game's CppFirstStep project. On success, reopen the .uproject.

What the code protects

BeginPlay puts the MaxHealth set on the child BP into CurrentHealth. The 0 on CurrentHealth in the header is its pre-play value; at Play start it becomes the max HP such as 50. Max HP is normalized to at least 1, and this example does not place enemies starting at 0 HP.

TakeDamage is the existing function through which an Actor receives damage. Overriding means rewriting a function from the parent for your own class. Here we also call the parent's logic with Super::TakeDamage, then reduce our own HP afterwards.

The argument list is long because it receives not only the damage amount but attack information, the Controller that ordered the attack, and the Actor that directly landed it. In this exercise we use only the damage amount for now.

The order: bail out if already dead, update HP and settle the dead state, then call the child Blueprint's effects

Clamp keeps a value within a specified range. Take 20 damage at 10 remaining HP and, kept within 0 to max HP, the result is 0 rather than -10. The return value is the HP actually lost, which is 10 on that final hit.

bIsDead marks that death already happened. We settle "it fell" before the BP's effects. Press H again during the 2 seconds before it vanishes and the HP update and death effect do not repeat.

Max HP is changed in the child BP, but the place that reduces current HP stays in this C++. Adding another subtraction in BP's Event AnyDamage would cause the same attack to subtract twice. Use the dedicated effect events described below instead.

Building appearance and effects in the child Blueprint

1. Create BP_Slime

Right-click EnemyBase under C++ Classes in the content browser and create a Blueprint based on this class. Name it BP_Slime.

Where you workSetting
ComponentsAdd a Static Mesh named Body as a child of SceneRoot
Body → Static MeshSphere from Engine's BasicShapes
Body → TransformLocation = (0, 0, 0), Scale = (0.6, 0.6, 0.4)
Body → MobilityMovable
Body → Collision PresetsNoCollision
Class Defaults → Enemy → StatusMax Health = 50
Class DefaultsConfirm Can Be Damaged is enabled

If you cannot find Sphere, enable Show Engine Content in the asset picker. We do not attack via collision here — we send damage directly to a designated target — so NoCollision is fine.

Placing a Sphere Body under SceneRoot and setting max HP to 50 in Class Defaults

Compile, save, and place one BP_Slime in a Third Person level where it is visible from the Player Start. Set the whole Actor's Scale to (1, 1, 1) and float it slightly above the floor. Play, and if the sphere is visible and the Output Log shows HP: 50 / 50, the C++ initialization and the child BP's setting are connected.

MaxHealth's EditDefaultsOnly means the child BP's default can be edited while per-instance values in the level cannot. This example uses it to keep max HP consistent across enemies made from the same BP_Slime.

2. Print text when hit

In BP_Slime's Event Graph, add "Event On Enemy Hit Visual." It is inherited from C++, so choose it from the right-click search. It is not an operation of creating a new Custom Event with the same name.

Connect the white exec line to a Print String with In String "Hit" and Duration 0.5.

Connecting On Enemy Hit Visual, called from C++, to a Print String showing Hit

Start with text to confirm the call reaches BP. Replacing this later with sound or a hit flash leaves the HP math unchanged.

3. Flatten on death and vanish after 2 seconds

Add "Event On Enemy Defeated Visual" to the same Event Graph and connect in this order.

  1. Set Actor Scale 3D: Target = Self, New Scale 3D = (1, 1, 0.2).
  2. Delay: Duration = 2.0.
  3. From Completed, Destroy Actor: Target = Self.
From the death event, scaling this Actor's height to 0.2 and continuing to the next step

The rest is cleanup afterwards.

After flattening, waiting 2 seconds and removing the same Actor with Destroy Actor

The A in the two diagrams is the continuation of the white line. You do not create a node called A. Self refers to the BP_Slime running this logic.

Separately from Body's Scale, we change the whole Actor's height to 0.2, producing a flattened look. Setting Delay to 5.0 makes it linger 5 seconds. The part that decides when to vanish, matched to the effect, lives in the child BP.

Sponsored

Sending 20 damage at a time to confirm the split

1. Create a reference to the placed enemy

Select BP_Slime in the level and open the Level Blueprint. Right-click the graph and choose "Create a Reference to BP_Slime," or drag that instance from the Outliner.

A reference specifies which single instance in the level you are sending to. It is not the operation of designating the plan in the content browser.

From the H key's Pressed, connect to an "Is Valid" with white exec pins. Put the placed BP_Slime reference in Input Object and continue the exec line from the Is Valid side. Leave the Is Not Valid side unconnected.

On pressing H, proceeding only if the placed BP_Slime is still valid

Is Valid confirms the target is still usable. It is there so you do not send damage to an enemy that already vanished. The 2 seconds while it lies flattened are still valid, but damage during that time is stopped by C++'s already-dead check.

2. Pass the same enemy to Apply Damage

Connect the continuing white line to Apply Damage. Connect the same BP_Slime reference to Damaged Actor.

InputSetting here
Damaged ActorThe placed BP_Slime reference
Base Damage20.0
Event Instigator / Damage CauserUnconnected
Damage Type ClassUnspecified
Continuing from the validity check, sending 20 damage to the same BP_Slime

The B in the diagram is also the continuation of the white line. The BP_Slime appearing in both diagrams is the same instance, and it is fine to branch one reference output to two places.

Since this is test input, we do not pass the attacking Controller or weapon. When integrating into a real attack, pass the target hit and the attack source as in the basics of Apply Damage.

3. Play and confirm one hit at a time

Compile and save everything, click the Play view, and press H. Filtering the Output Log by HP: makes the numbers easy to follow. Press at intervals where you can read the text.

ActionHP logVisible result
Start Play50 / 50The sphere appears
Press H once30 / 50"Hit" appears
Press H twice10 / 50"Hit" appears
Press H three times0 / 50"Hit" appears, the sphere flattens, and it vanishes in 2s
Press H after it flattensNo new HP logThe vanish countdown does not restart
Press H after it vanishesNo new HP logNothing happens

Next, stop Play and change BP_Slime's Max Health to 70. Play again and it takes four hits: 70 → 50 → 30 → 10 → 0. You tuned health with a default value alone, leaving the HP-reducing C++ untouched.

4. Make another enemy from the same parent

Duplicate BP_Slime as BP_Golem, set Max Health = 120, and change Body's Static Mesh to Cube. Change the hit text to "Hit the golem" and the vanish Delay to 5.0.

Place it elsewhere in the level and swap the enemy reference used in the Level Blueprint to that BP_Golem. Align both Is Valid and Apply Damage on the same instance. At 20 damage it reaches 0 in six hits and vanishes after 5 seconds.

Adding an enemy required no copying of the HP subtraction. You can confirm through operation the split of "parent for shared rules, child for health and effects."

When it doesn't work

SymptomWhere to check
The C++ build failsThe API macro, whether the parent is Actor, the two files and include order, whether UE was closed
The effect events aren't in searchWhether the child BP's parent is EnemyBase, build success, the BlueprintImplementableEvent declaration
HP doesn't dropPlay view focus, H's Pressed, the Is Valid side's line, Damaged Actor, Can Be Damaged
HP drops twiceWhether another subtraction exists in Event AnyDamage or the child BP
The sphere doesn't flattenThe white line from the death event, Set Actor Scale 3D's Target = Self, Z = 0.2, Mobility
It doesn't vanishWhether Delay's Completed connects to Destroy Actor
It hits the wrong enemyWhether the placed instance's reference is aligned on both the check and the send

Bonus: before extending this to your own game

Adding an HP bar and real effects

GetHealthPercent() returns current HP ÷ max HP. On a 50 HP enemy it goes 1.0 → 0.6 → 0.2 → 0.0. It is the entry point for feeding a Progress Bar's Percent when you add an HP bar. In this exercise we confirmed the numbers in the log before building a bar.

Sound and Niagara also get added to the child BP's effect events. This is the place for "showing," so do not mix in HP recomputation or duplicate-reward checks. In multiplayer, the communication design between the side deciding HP and the side showing effects is a separate matter.

When you want shared effects in C++

BlueprintImplementableEvent leaves the body to BP, as here. BlueprintNativeEvent, by contrast, puts default logic in C++ that specific child BPs can change. It is a candidate when you want to extend to "a common sound normally, and a different effect only on special enemies."

Do not move existing BPs all at once

Parts your current BPs build well are fine as they are. If you do split, try it on one feature such as the HP math and confirm the result matches what it was before the move.

When changing an existing BP's parent, prepare a copy and tidy up same-named variables, events, and defaults before using Reparent Blueprint. Changing the parent alone does not automatically move the original node logic into C++.

Blueprint Header View lets you inspect C++-style declarations, but you still have to move the function bodies yourself. For post-change builds versus Live Coding, see the first-steps article.

Next time you tune an enemy, ask which of "change a number," "fix shared math," or "change how it looks" you are doing. If each has a settled place to open, the split is helping your production.

Summary

  • Put shared logic in C++, and per-kind appearance and effects in child Blueprints
  • Have C++ only call the events, leaving it unaware of what the effects do
  • Tune health and time-to-vanish in the details panel without changing C++

The question when you are unsure how to split is: "is this the same for every enemy, or does it vary by kind?" Same goes in C++; varies goes in Blueprint.

To confirm the C++ side's syntax, go to First steps from Blueprint into C++; to broaden how things connect, go to Blueprint communication methods.

References: Blueprint vs C++, UFunctions, TakeDamage, Referencing assets, UE Diff Tool.

Unreal Engine Notes in this section98