Health/Damage/Death handling in Unreal
Added 2023-04-24 23:56:42 +0000 UTCThere are a bunch of ways of going about damage systems in Unreal, but the first key takeaway here is that: Unreal doesn't exactly provide a health or damage system. They give you functions, that are exposed in sometimes useful ways, but they don't actually do anything on their own. Nothing even touches the arguments, they're just passed through as-is. Which means when you approach the system, you might be wondering how you Should do things, and the answer is kinda- well, however you want?
So first let's ask the obvious: Why not just use the naked TakePointDamage / TakeRadialDamage and append a brick-stupid health++/-- behind them?
My answer is, because there's no logic layer before those fire that can ask things like "was my shield up?" or "was it a glancing blow and so I took half damage?". FPSes that use this system naked probably get around this by doing the actual damage calculation on weapon-side and also play flinches and hit-reactions like particles directly on the hit target themselves, and that, uh. Sure is a way of doing things that horrifies me. So I don't do that.
Next, let's lay down a few basic cautions:
One big thing that was drilled into me is don't just write your own from scratch. Yes, there are some somewhat silly aspects of the skeleton Unreal provides, but just, work within it anyways. It'll save you energy down the road. So we're building around that.
Please also note, this system will PROBABLY NOT WORK ONLINE. Because of a quirk of how TakeDamage is a server-only call. There's a pretty straightforward way of fixing that, which I've already planned, but until I actually test that? We'll just keep it simple heh.
Now then, the system itself:
We're going to make a HealthComponent as the hub of our logic, and in it, we start with this:

The OnTakePointDamage and OnTakeRadialDamage will fire, respectively, when you call ApplyPointDamage or ApplyRadialDamage on a Character. As a matter of interest, there's also OnTakeAnyDamage, but that fires in addition to one of these. Meaning if you want to be capable of responding to point VS radial damage differently, you don't want to use it. And you do care about this stuff, because check out the arguments:

The function you use really is going to dictate how it feels to pass data through. Here, I felt the arguments for RadialDamage were more useful than the overly specific PointDamage ones, so I ended up making a standardized DamageWasApplied that leans more that direction.

You can also see that my PointDamage handler is more involved right now (just cus I haven't done much radial damage yet), but see how my DamageWasApplied is closer to Radial args? How I ignore a lot of the PointDamage inputs? Yeah, that's just a choice I made.
Because remember! These contents of the arguments on TakeDamage are completely arbitrary. Let me say that again: the values you pass into ApplyPointDamage/ApplyRadialDamage mean literally nothing other than what you say they do, because nothing else reads them. So if you don't wanna use a value? Just don't. Like here's some of my damage tracing logic for a sword swing:

See that HitResult I'm just making up there and passing through cus it can't be NULL? Yep! I don't care about HitResult, I never use it, so it can be effectively empty. BTW, the $3+ tier post this months explains that Fake Swept Pill thing in there, so yeah.
Anyways, that's kind of it?

This little guy is kinda all you need for applying the damage (I call this up there too), just so you can extract the moment they died and such, and then,

These are just Event Dispatchers I defined in the component. That's it, done. The rest of your Actor listens to these if it needs to care about damage handling, and well, there you go. That's my health system. Pretty basic, but more than capable enough!