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.
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++
- The basic shape: different enemies from the same parent
- What you judge is "what do you want to change"
- A guide for when placement is unclear
- What moving to C++ does not solve on its own
- Hands-On: separating HP management from the falling effect
- Building appearance and effects in the child Blueprint
- Sending 20 damage at a time to confirm the split
- Bonus: before extending this to your own game
- Summary
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.

| Where it lives | Its responsibility here |
|---|---|
| The C++ parent | Reduce HP on damage. Never go below 0. Do not run twice once already dead |
| The child Blueprint | Choose 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?

"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 build | Example first placement | The judgment point |
|---|---|---|
| Max HP, movement speed, mesh used | Child Blueprint, Data Asset | Do you want to tune it without opening code? |
| Hit and death sounds and visuals | Blueprint | Do you swap it while looking at it? |
| Shared rules such as HP math | A shared parent or Component | Is Blueprint enough, or is there an advantage to C++? |
| Structs and Enums used from both | Define the type in C++ | Do you need the same type on both sides? |
| Enemy AI or level gimmicks in prototype | Blueprint | Try the behavior first and split once shared parts appear |
| A loop measured and found heavy | Consider 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.

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 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.

We move between C++ and Blueprint in this order.
- A test Blueprint calls Apply Damage.
- The enemy's C++ receives the damage and updates HP.
- C++ tells the child Blueprint "you were hit" and "you fell."
- The child Blueprint changes its appearance in response.

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.

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 work | Setting |
|---|---|
| Components | Add a Static Mesh named Body as a child of SceneRoot |
| Body → Static Mesh | Sphere from Engine's BasicShapes |
| Body → Transform | Location = (0, 0, 0), Scale = (0.6, 0.6, 0.4) |
| Body → Mobility | Movable |
| Body → Collision Presets | NoCollision |
| Class Defaults → Enemy → Status | Max Health = 50 |
| Class Defaults | Confirm 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.

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.

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.
- Set Actor Scale 3D: Target = Self, New Scale 3D = (1, 1, 0.2).
- Delay: Duration = 2.0.
- From Completed, Destroy Actor: Target = Self.

The rest is cleanup afterwards.

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.
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.

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.
| Input | Setting here |
|---|---|
| Damaged Actor | The placed BP_Slime reference |
| Base Damage | 20.0 |
| Event Instigator / Damage Causer | Unconnected |
| Damage Type Class | Unspecified |

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.
| Action | HP log | Visible result |
|---|---|---|
| Start Play | 50 / 50 | The sphere appears |
| Press H once | 30 / 50 | "Hit" appears |
| Press H twice | 10 / 50 | "Hit" appears |
| Press H three times | 0 / 50 | "Hit" appears, the sphere flattens, and it vanishes in 2s |
| Press H after it flattens | No new HP log | The vanish countdown does not restart |
| Press H after it vanishes | No new HP log | Nothing 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
| Symptom | Where to check |
|---|---|
| The C++ build fails | The API macro, whether the parent is Actor, the two files and include order, whether UE was closed |
| The effect events aren't in search | Whether the child BP's parent is EnemyBase, build success, the BlueprintImplementableEvent declaration |
| HP doesn't drop | Play view focus, H's Pressed, the Is Valid side's line, Damaged Actor, Can Be Damaged |
| HP drops twice | Whether another subtraction exists in Event AnyDamage or the child BP |
| The sphere doesn't flatten | The white line from the death event, Set Actor Scale 3D's Target = Self, Z = 0.2, Mobility |
| It doesn't vanish | Whether Delay's Completed connects to Destroy Actor |
| It hits the wrong enemy | Whether 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.