简介

伤害(Damage)是游戏中常见的概念,几乎所有的游戏都具有伤害,难怪 UE 在 Actor 中自带了承担伤害的方法。

如果让开发者自己写一套伤害系统,第一个想法应该是通过 Interface 或 Actor 组件去承担伤害逻辑,不会想到 UE 中已经提供了一个简单的伤害逻辑。

伤害的代码主要在 Actor. h 中,此外 GameplayStatics. h 封装了 Actor 的 TakeDamage 方法。没想到的是,Controller 和 PrimitiveComponent 中也有 Damage 相关的代码,Controller 可获得造成伤害时的事件,PrimitiveComponent 可在接受伤害时应用冲击力。

  • 对 Actor 应用伤害的方法
1
2
3
4
5
6
7
8
9
10
11
// Actor.h
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser);

// GameplayStatics.h
static bool ApplyRadialDamage(const UObject* WorldContextObject, float BaseDamage, const FVector& Origin, float DamageRadius, TSubclassOf<class UDamageType> DamageTypeClass, const TArray<AActor*>& IgnoreActors, AActor* DamageCauser = NULL, AController* InstigatedByController = NULL, bool bDoFullDamage = false, ECollisionChannel DamagePreventionChannel = ECC_Visibility);

static bool ApplyRadialDamageWithFalloff(const UObject* WorldContextObject, float BaseDamage, float MinimumDamage, const FVector& Origin, float DamageInnerRadius, float DamageOuterRadius, float DamageFalloff, TSubclassOf<class UDamageType> DamageTypeClass, const TArray<AActor*>& IgnoreActors, AActor* DamageCauser = NULL, AController* InstigatedByController = NULL, ECollisionChannel DamagePreventionChannel = ECC_Visibility);

static float ApplyPointDamage(AActor* DamagedActor, float BaseDamage, const FVector& HitFromDirection, const FHitResult& HitInfo, AController* EventInstigator, AActor* DamageCauser, TSubclassOf<class UDamageType> DamageTypeClass);

static float ApplyDamage(AActor* DamagedActor, float BaseDamage, AController* EventInstigator, AActor* DamageCauser, TSubclassOf<class UDamageType> DamageTypeClass);

b14a2b8261840d3b1f0d6d598d8cca24_MD5

  • Actor 中提供了蓝图实现的事件,可用于触发减少生命值、死亡等逻辑。
1
2
3
4
5
6
7
8
9
//Actor.h
/** Event when this actor takes ANY damage */
void ReceiveAnyDamage(float Damage, const class UDamageType* DamageType, class AController* InstigatedBy, AActor* DamageCauser);

/** Event when this actor takes RADIAL damage */
void ReceiveRadialDamage(float DamageReceived, const class UDamageType* DamageType, FVector Origin, const struct FHitResult& HitInfo, class AController* InstigatedBy, AActor* DamageCauser);

/** Event when this actor takes POINT damage */
void ReceivePointDamage(float Damage, const class UDamageType* DamageType, FVector HitLocation, FVector HitNormal, class UPrimitiveComponent* HitComponent, FName BoneName, FVector ShotFromDirection, class AController* InstigatedBy, AActor* DamageCauser, const FHitResult& HitInfo);

f0d7752076ff99b5cceaa1e8fd7827d8_MD5

  • 此外,Actor 中还提供了对应的伤害委托事件,开发者可监听这些伤害事件。
1
2
3
4
5
6
7
8
9
10
11
12
// Actor.h
/** Called when the actor is damaged in any way. */
UPROPERTY(BlueprintAssignable, Category="Game|Damage")
FTakeAnyDamageSignature OnTakeAnyDamage;

/** Called when the actor is damaged by point damage. */
UPROPERTY(BlueprintAssignable, Category="Game|Damage")
FTakePointDamageSignature OnTakePointDamage;

/** Called when the actor is damaged by radial damage. */
UPROPERTY(BlueprintAssignable, Category="Game|Damage")
FTakeRadialDamageSignature OnTakeRadialDamage;

7ef0b228531072a3afa56bfe84c77a1e_MD5

  • Actor 中有两个变量与伤害相关,bCanBeDamaged 决定该 Actor 是否可接受伤害,Instigator 则用于伤害逻辑,后面介绍。
1
2
3
4
5
6
// Actor.h
/** Whether this actor can take damage. Must be true for damage events. */
uint8 bCanBeDamaged:1;

/** Pawn responsible for damage and other gameplay events caused by this actor. */
class APawn* Instigator;

伤害的几个重要概念

DamageCauser

直接造成该次伤害的 Actor,比如子弹、炸弹、刀等。如果是喝了毒药掉血,可以把 DamageCauser 设为自己,也可以设为 null。

Insigator

Instigator(伤害发起者),可用于伤害逻辑,表示该 Actor 对其它 Actor 照成伤害后,负责该次伤害的 Pawn。

比如该 Actor 是一颗子弹,则需要将子弹的 Instigator 设置为枪的拥有者,对方 Actor 在接受到伤害后就知道该伤害是由谁负责,所以 Instigator 的类型是 Pawn。这是最常见的一种情况,想一下如果没有 Instigator,在做 FPS 游戏的时候,查找击杀者可能就有些绕了。

  • 调用 ApplyDamage 方法,需要传递的是 Insigator 的 Controller 而不是 Insigator。

DamageType(伤害类型)

DamageType 表示该伤害的类型,比如火属性、冰属性等。

一般游戏中可能会使用枚举来表示不同的伤害类型,而 UE 中则使用了单独的类来作为伤害类型,麻烦之处在于有多少个伤害类型就要创建多少个 UDamageType 的蓝图(不推荐直接使用 C++ 类),优点则是可以创建复杂的伤害类型。

记住,直接使用的是 UDamageType 的 class 而不是对象,不应该为 DamageType 创建实例,一个 class 只能表示一种伤害类型。

3 种不同的伤害种类

UE 提供了 3 中不同的伤害事件,PointDamage (点伤害)、RadialDamage(辐射伤害)、Damage(普通伤害)。

PointDamage (点伤害)

点伤害算是最常见的伤害事件,一般通过 Overlap 或 Hit 事件触发,比如子弹击中、武器攻击、陷阱等,都可使用 PointDamage 来应用伤害。

b14a2b8261840d3b1f0d6d598d8cca24_MD5

  • HitFromDirection:该次击打的方向,比如子弹运动方向。
  • HitInfo:FHitResult,Overlap 或 Hit 事件的 HitResult。

RadialDamage(辐射伤害)

辐射伤害主要用于处理爆炸物的伤害,它有一个点和半径,生成一个球并检测该球内所有的 Overlap 物体,判断物体是否可接受伤害(是否被其它物体阻挡、是否在 IgnoreActors 内)并应用辐射伤害,所以 ApplyRadialDamage 没有固定的 DamagedActor 参数。

5fd0e9fc62fb9043332b3d762a6926a8_MD5

  • Origin:球心
  • DamageRadius:球半径
  • IgnoreActors:忽略这些 Actor,不对其照成伤害
  • DoFullDamage:如果为 true,则伤害不会衰减
  • DamagePreventionChannel:用于物体到球心的射线检测,Visibility 表示可见的物体会阻挡这次伤害
  • MinimunDamage:即使应用衰减,伤害也不会低于此值
  • DamageInnerRadius:伤害衰减起始半径
  • DamageOuterRadius:伤害衰减终止半径,也是最大半径
  • DamageFallOff:伤害衰减指数,为 0 表示不衰减

Damage(普通伤害)

普通伤害不会附带 HitResult 等额外信息,可用于中毒、出血等 buff 带来的简单伤害,通常在伤害不会造成物理表现的时候使用。

与伤害有关的其它类

Controller

在 Controller 控制的 Pawn 作为 Instigator 向其它 Actor 应用伤害时,该 Controller 也会得到一个事件通知。

1
2
3
4
5
6
// Controller.cpp
void AController::InstigatedAnyDamage(float Damage, const class UDamageType* DamageType, class AActor* DamagedActor, class AActor* DamageCauser)
{
ReceiveInstigatedAnyDamage(Damage, DamageType, DamagedActor, DamageCauser);
OnInstigatedAnyDamage.Broadcast(Damage, DamageType, DamagedActor, DamageCauser);
}

PrimitiveComponent

物理组件在 bApplyImpulseOnDamage 设为 true 时,受到点伤害或辐射伤害时会添加一个冲击力的表现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// PrimitiveComponent.h
uint8 bApplyImpulseOnDamage : 1;

// PrimitiveComponent.cpp
void UPrimitiveComponent::ReceiveComponentDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
if (bApplyImpulseOnDamage)
{
UDamageType const* const DamageTypeCDO = DamageEvent.DamageTypeClass ? DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>() : GetDefault<UDamageType>();
if (DamageEvent.IsOfType(FPointDamageEvent::ClassID))
{
FPointDamageEvent* const PointDamageEvent = (FPointDamageEvent*)&DamageEvent;
if ((DamageTypeCDO->DamageImpulse > 0.f) && !PointDamageEvent->ShotDirection.IsNearlyZero())
{
if (IsSimulatingPhysics(PointDamageEvent->HitInfo.BoneName))
{
FVector const ImpulseToApply = PointDamageEvent->ShotDirection.GetSafeNormal() * DamageTypeCDO->DamageImpulse;
AddImpulseAtLocation(ImpulseToApply, PointDamageEvent->HitInfo.ImpactPoint, PointDamageEvent->HitInfo.BoneName);
}
}
}
else if (DamageEvent.IsOfType(FRadialDamageEvent::ClassID))
{
FRadialDamageEvent* const RadialDamageEvent = (FRadialDamageEvent*)&DamageEvent;
if (DamageTypeCDO->DamageImpulse > 0.f)
{
AddRadialImpulse(RadialDamageEvent->Origin, RadialDamageEvent->Params.OuterRadius, DamageTypeCDO->DamageImpulse, RIF_Linear, DamageTypeCDO->bRadialDamageVelChange);
}
}
}
}