The attack looks like it landed, but the enemy's HP does not drop. In that situation, appearance alone cannot tell you whether "the damage logic was never called" or "it was called but treated as invincible."
So we print short messages along the way. "Got this far." "Damage is 25." "Invincible, so skipping." Once what happened internally is visible, you can narrow down where to look next. Investigating behavior and fixing problems like this is debugging, and the records you leave as clues are logs.
In this article we try printing messages with Blueprint's Print String and reading them back in the editor's Output Log. The hands-on needs only one Actor placed, so you can proceed even without weapon or enemy systems.
What You'll Learn
- Confirming that logic was reached and what a variable holds with Print String
- Finding your own messages in the Output Log
- Narrowing down causes by "was it called, what was the value, which way did it go"
- Choosing between Key for repeated display and C++ log output
Print one line with Print String first
Print String is a node that outputs a specified string. A string is a run of characters such as Hello or HP: 100. You can choose to show it in the top-left of the screen, record it in the Output Log, or both.

Let's build the small check Actor we also use in the hands-on. An Actor is something you can place in a level. Since this one exists to print messages, we give it no visual model.
- Create a "Blueprint Class" in the content browser with "Actor" as parent class and name it
BP_DebugEnemy. - Open it and go to the "Event Graph." That is the workspace where you connect nodes representing logic.
- Drag from the white exec pin on the right of
Event BeginPlay, searchPrint String, and add it. BeginPlay is the event called when this Actor starts running in the game. - Enter
[DebugEnemy] Startin Print String's "In String." From the expand arrow at the bottom of the node, turn on "Print to Screen" and "Print to Log" and set "Duration" to5.0. - Press "Compile" and "Save," then drag exactly one
BP_DebugEnemyfrom the content browser into the level. Save the level too and press "Play."
[DebugEnemy] Start in the top-left means success. [DebugEnemy] is a marker you added yourself, used later to search for this experiment's logs. Since the Actor has no model, no enemy appearing on screen is fine.
White lines represent the order in which logic runs. Merely placing Print String does nothing; a white line from BeginPlay must be connected.
| Setting | Its role here |
|---|---|
| In String | The string to print |
| Print to Screen | Display it in the game view |
| Print to Log | Emit it to the Output Log as a normal log |
| Text Color | The color of the on-screen text |
| Duration | Seconds it stays on screen. 5.0 means 5 seconds |
| Key | A name for updating the same on-screen message. Left None here |
Read vanished messages back in the Output Log
Print String's on-screen display disappears over time. When you missed it, or want to read several operations in order, open the Output Log. It is the screen where you can see your own messages plus warnings and errors UE emitted.

- Stop the game with "Stop" and open "Window → Output Log."
- Enter
DebugEnemyin the search box. - Find
[DebugEnemy] Start. Reading back text that vanished on screen means success.
If you cannot find it, check whether Print String's "Print to Log" is on and whether the Output Log's filter is narrowed to errors and warnings only. Even when a log appears absent, it may just be hidden by a filter.
The LogTemp and similar at the start of a log line is the category, the name separating "which kind of logic emitted this record." Rather than memorizing categories early on, searching from your own marker as we did here is easier.
Hands-On: investigating why HP isn't dropping
Next we add logic to BP_DebugEnemy that subtracts 25 from HP. But we deliberately turn "invincible" on at first. Read the log, confirm why HP is not dropping, then fix it.

The diagram shows the in-game situation. In this exercise we build no attack or collision and run the damage logic once from BeginPlay. That lets us focus on preparing to investigate the damage calculation.
Preparation: three values to investigate
Stop Play and create the following variables from the "+" in BP_DebugEnemy's "My Blueprint → Variables." Variables remember values such as HP. After "Compile," set each variable's initial value in "Default Value."
| Variable | Type | Default | Meaning |
|---|---|---|---|
Health | Float | 100.0 | Current HP |
IncomingDamage | Float | 25.0 | The damage received this time |
bIsInvincible | Boolean | true (checked) | true when invincible, false to take damage |
Float is a number that can hold decimals, and Boolean is a type holding only true / false. Here we represent "am I invincible" with a Boolean. The leading b in the variable name is a marker showing it is a Boolean; it has no special behavior.

1. Was it called this far, and what is the value
The [DebugEnemy] Start you made first shows the logic arrived from BeginPlay. Add a Print String after it that prints the damage amount.

For readability, the diagram draws value lines uniformly in blue. In the actual editor, colors vary by value type. "Next: Branch" points to the next diagram; it is not a node to add.
- Connect from the first Print String's white output pin to a second Print String.
- Drag
IncomingDamageinto the graph and choose "Get." Get reads the value stored in a variable. - Place an
Appendnode that joins strings and enter[DebugEnemy] Damage:in A. - Connect IncomingDamage's output to Append's B. A node converting the number to a string is inserted automatically. Connect Append's "Return Value" to the second Print String's "In String."
Return Value is the result that node produced. Here it holds [DebugEnemy] Damage: 25, combining text and a number. Decimal display depends on the conversion settings, so 25 and 25.0 are the same value.
Turn on screen and log output on the second one too, set Duration to 5.0, Compile, Save, and Play. Search DebugEnemy in the Output Log and confirm the Start and Damage lines in order. So far you know "the logic started and the damage value was 25."
2. Which condition did it take
Stop Play and place a Branch after the second Print String. Branch is a node that splits where logic goes based on a condition. Connect a Get of bIsInvincible to "Condition."
Since we ask "am I invincible" directly, the True side skips and the False side applies damage.

- True side: connect to a Print String with In String
[DebugEnemy] Skip: Invincible. - False side: connect to
Set Health. Drag the Health variable into the graph and choose "Set" to place it. Set writes a new value into a variable.
Build the value fed into Set Health with a Float subtraction node. Drag from Health's Get and add Subtract (-), with Health on the top input and IncomingDamage on the bottom. Connect the result to Set Health's value input.

Finally add a Print String from Set Health's white output. Place an Append as before with A set to [DebugEnemy] HP: and B connected to Set Health's value output. Insert the number-to-string conversion and pass Append's Return Value to Print String's In String. That displays HP after the write. Turn on screen and log output and set Duration to 5.0 on this Print String too.

The Set Health in the diagram is the same node as in the previous one. There is no need to place another.
3. Compare the logs and fix exactly one thing
Compile, Save, and Play, and at first these three lines appear. New lines may stack on top on screen, so confirm the order in the Output Log. Logs from previous Plays remain too, so look at this run's lines at the end.
[DebugEnemy] Start
[DebugEnemy] Damage: 25
[DebugEnemy] Skip: Invincible
The damage amount was 25, but it took the invincible branch. HP did not drop this time because it skipped before the calculation.
Stop Play and change the Blueprint's bIsInvincible default to false (unchecked). Compile, Save, and Play again, and instead of Skip you get [DebugEnemy] HP: 75. HP with 25 subtracted from 100 was actually written.
Next, stop Play and change only IncomingDamage's default to 40.0. Play again and Damage should be 40 with an updated HP of 60. Since you restart Play each time, Health starts from its default of 100.
Investigate your own attack logic in the same order
In a real game, put a Start-equivalent log at the head of the logic that runs when damage is taken. If you use UE's damage feature, the entry point is Event AnyDamage. Hit, which reports collisions, and Overlap, which reports overlaps, have different roles, so match your own logic's entry point (→ building health and damage).
| What you confirmed | Where to look next |
|---|---|
| The first log doesn't appear | Display settings, log filters, whether the target Actor exists, the connections to the event |
| Logic arrived but damage is 0 | The side passing damage, and where the value is read |
| The value is right but it skips | Conditions such as invincibility, and the logic changing that value |
| Updated HP drops but the HP bar doesn't | The display update logic, and which enemy the display references |
One log does not reveal the whole cause. Even so, you can push the investigation past the point you confirmed, which saves you from changing unrelated settings in bulk.
Update the same on-screen line with Key
Sometimes you want to follow a constantly changing value, such as movement speed, for a short time. Put a Print String on Event Tick, called every frame — every screen update — and the screen fills with lines of text.
That is what Key is for. Specify a name other than None, such as PlayerSpeed, and screen messages with the same Key overwrite each other.

Key is for organizing the on-screen display. It is not a feature for collapsing Output Log records into one line or for keeping text past Duration. When watching a value temporarily on Tick, turn "Print to Log" off to avoid flooding the normal log. For values like HP where you want to know the moment of change, printing right after the change logic is enough to start.
Logging in C++: UE_LOG
To record values the same way in C++, use UE_LOG. If you are working in Blueprint, come back when you need it.
For example, adding these two lines after the existing Super::BeginPlay(); inside your own Actor's .cpp prints HP to the log.
const float HealthForDebug = 100.0f;
UE_LOG(LogTemp, Log, TEXT("[DebugEnemy] HP: %.1f"), HealthForDebug);
LogTemp is a category usable for temporary checks, and the second Log is the record's severity. The %.1f slot takes the number to one decimal place, printing HP: 100.0. TEXT(...) is UE's way of writing strings. Preparing your own category can wait until logs multiply.
Verbosity is the classification of a log's severity and detail. Separating logs indicating problems from routine records makes filtering in the Output Log easier.

| Level | Rule of thumb for use |
|---|---|
| Error | Problems such as failing to load required data |
| Warning | States worth noting, such as correcting an unexpected value |
| Display | Information you also want in the console, such as startup settings |
| Log | Routine behavior checks, like the HP here |
Error records a problem; it is not by itself an instruction to stop the game. Fatal, which records a fatal error and crashes, is a separate level. And while printf can print text in some situations, UE_LOG is what fits UE's categories, severities, and log window.
Tips for leaving investigable logs
- Pair the name with the value:
Damage: 25reads better later than just25. When investigating several enemies, include the Actor name too. - Change one thing at a time: here, fix the invincible default first, then change the damage amount. You never lose track of why a result changed.
- Also show normal records: when hunting your own Print Strings, clear filters limited to Error and Warning. Conversely, narrow to those when hunting UE-side problems.
- Clean up your check logic: after the experiment, remove BP_DebugEnemy from the working level. Delete temporary logs you added to your own game once they have served their purpose.
Bonus: Good to Know Up Front
Confirm positions and volumes with diagrams. Even when a log tells you "the attack query ran," whether the query volume reaches the enemy is hard to follow in text alone. Combining it with Draw Debug and the Visual Logger, which draws lines and spheres in the game view, makes it easier to confirm.
- What Accessed None means: it is a clue that there was no usable reference, such as a target you tried to operate on not being set. Open the Blueprint and node reported in the log and investigate from the logic that retrieves the target.
- Print String is for development checks: it is unavailable in normal Shipping builds. Do not use it as a substitute for a shipping HP display or player notifications.
- You can read it from files too: in normal editor runs, logs are saved under the project's
Saved/Logs/. To investigate after closing, check the file matching the date and time. - There is also a stop-and-inspect method: once you have narrowed the location, the Blueprint Debugger, which pauses logic so you can look at variables, helps.
Summary
Put markers along your logic with Print String and read back the order and values in the Output Log. Combining the two lets you follow behavior that appearance alone cannot reveal.
The first things to confirm are the three: "was it called this far," "what was the value," and "which way did it go." As with the invincible setting here, try small changes and compare results to narrow down where to look.