【UE4C++-ActionRougelike-15】作业4+玩家重生+控制台变量
一、第四次作业
作业要求:新的AI行为:当低血量时会藏起来回血
- Service节点:检查AI是否低血量
- EQS:找到距离AI近的可隐藏地点
- BT Task:治疗自己到最大生命值
- 该行为60s只能运行一次
1、修改行为树
按照作业要求我们知道该行为大致流程为:检查是否残血(Service Node)->找到躲藏地点(EQS生成地点)->回血(TaskNode执行任务)
注意IsLowHealth的Observer aborts要设置为低优先级(Lower Priority),表示中止此节点右侧所有节点,这样能够在AI执行攻击时,如果AI变为残血,会中止攻击而执行躲藏回血行为。
2、创建检查残血ServiceNode
以BTService为父类创建检查残血服务节点类MyBTService_CheckHealth
UCLASS()
class ACTIONROGUELIKE_API UMyBTService_CheckHealth : public UBTService
{
GENERATED_BODY()
public:
UMyBTService_CheckHealth();
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
//设置“是否残血”的黑板键
UPROPERTY(EditAnywhere, Category = "AI")
FBlackboardKeySelector LowHealthKey;
//残血的界限
UPROPERTY(EditAnywhere, Category = "AI")
float LowHealthFraction;
};
在MyBTService_CheckHealth.cpp中给出检查残血的实现
UMyBTService_CheckHealth::UMyBTService_CheckHealth()
{
//初始化残血界限值
LowHealthFraction = 0.3f;
}
void UMyBTService_CheckHealth::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APawn* MyPawn = Cast<APawn>(OwnerComp.GetAIOwner()->GetPawn());
if (ensure(MyPawn))
{
UMyAttributeComponent* AIAttributeComp = UMyAttributeComponent::GetAttributes(MyPawn);
if (AIAttributeComp)
{
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
//检查是否残血(要在MyAttributeComponent中新增GetHealth方法,代码很简单return Health即可)
bool bLowHealth = AIAttributeComp->GetHealth() / AIAttributeComp->GetHealthMax() < LowHealthFraction;
BlackboardComp->SetValueAsBool(LowHealthKey.SelectedKeyName, bLowHealth);
}
}
}
3、创建查询躲藏点EQS
这里可以采用适合的生成节点和参数来进行生成,此处与课程中选择一样为Donut生成器节点
3.1 Distance节点设置
- Test Purpose设置为
Filter and score
,表示根据距离进行过滤和计分。 - Filter设置为Mininum和0,表示小于0的点全部过滤(在我们的生成器情况下等于保留全部点)
- Scoring Factor设置为-5.0,表示离目标(自己)越近的点得分越高(这里只要为负数,数值可以自行设置)
3.2 Trace节点设置
- Test Purpose设置为Filter Only,表示仅过滤
- Context设置为TargetActor,表示用于检测查询生成的位置与TargetActor之间是否存在遮挡物
3.3 PathFinding节点设置
- TestMode设置为Path Length,表示计算从上下文参考点(TargetActor)到查询生成的点之间的路径长度
- Filter 设置同Distance一样
4、创建回血TaskNode
以BTTaskNode为父类创建回血类任务节点MyBTTask_HealSelf
UCLASS()
class ACTIONROGUELIKE_API UMyBTTask_HealSelf : public UBTTaskNode
{
GENERATED_BODY()
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
在MyBTTask_HealSelf.cpp中重写UBTTaskNode 的 ExecuteTask 函数,用于在行为树中执行回血
EBTNodeResult::Type UMyBTTask_HealSelf::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
APawn* MyPawn = Cast<APawn>(OwnerComp.GetAIOwner()->GetPawn());
if (MyPawn == nullptr)
{
return EBTNodeResult::Failed;
}
UMyAttributeComponent* AttributeComp = UMyAttributeComponent::GetAttributes(MyPawn);
if (ensure(AttributeComp))
{
AttributeComp->ApplyHealthChange(MyPawn, AttributeComp->GetHealthMax());
}
return EBTNodeResult::Succeeded;
}
5、效果演示
二、玩家重生
1、游戏模式中添加玩家重生逻辑
在MyGameMode.h中添加重生玩家函数,重写OnActorKilled函数。
class ACTIONROGUELIKE_API AMyGameModeBase : public AGameModeBase
{
protected:
//用于在指定时间后重生玩家角色
UFUNCTION()
void RespawnPlayerElapsed(AController* Controller);
public:
//OnActorKilled是一个虚函数,可以在继承自AActor的类中重载。会在该Actor被杀死时被调用,参数VictimActor为被杀死的Actor,Killer为杀死该Actor的Actor
virtual void OnActorKilled(AActor* VictimActor, AActor* Killer);
}
在MyGameMode.cpp中处理玩家角色的重生。当玩家角色被击败时,触发OnActorKilled
函数,该函数将为玩家角色设置一个2秒的重生延迟,然后通过RespawnPlayerElapsed
函数重置玩家角色。
void AMyGameModeBase::RespawnPlayerElapsed(AController* Controller)
{
if (ensure(Controller))
{
// 解除角色的占用
Controller->UnPossess();
// 解除角色的占用
RestartPlayer(Controller);
}
}
void AMyGameModeBase::OnActorKilled(AActor* VictimActor, AActor* Killer)
{
// 尝试将被击败的角色转换为 AMyCharacter 类型
AMyCharacter* Player = Cast<AMyCharacter>(VictimActor);
// 如果转换成功,说明被击败的角色是一个玩家角色
if (Player)
{
// 创建一个定时器句柄
FTimerHandle TimerHandle_RespawnDelay;
// 创建一个定时器代理,并绑定 RespawnPlayerElapsed 函数
FTimerDelegate Delegate;
Delegate.BindUFunction(this, "RespawnPlayerElapsed", Player->GetController());
// 设置重生延迟
float RespawnDelay = 2.0f;
// 设置定时器,2秒后触发 RespawnPlayerElapsed 函数
GetWorldTimerManager().SetTimer(TimerHandle_RespawnDelay, Delegate, RespawnDelay, false);
}
UE_LOG(LogTemp, Log, TEXT("OnActorKilled: Victim: %s, Killer: %s"), *GetNameSafe(VictimActor), *GetNameSafe(Killer));
}
2、玩家死亡时调用Actor死亡函数
MyAttribute.cpp中的ApplyHealthChange添加死亡判断逻辑,如果玩家死亡,则触发GameMode中的OnActorKilled函数。
bool UMyAttributeComponent::ApplyHealthChange(AActor* InstigatorActor, float Delta)
{
if (ActualDelta < 0.0f && Health == 0.0f)
{
// 获取游戏模式对象 AMyGameModeBase
AMyGameModeBase* GM = GetWorld()->GetAuthGameMode<AMyGameModeBase>();
if (GM)
{
// 触发游戏模式中的 OnActorKilled 函数,通知角色被击败
GM->OnActorKilled(GetOwner(), InstigatorActor);
}
}
return ActualDelta != 0;
}
3、效果演示
三、控制台变量和静态函数库
1、控制台变量
1.1 禁止生成机器人
在MyGameModeBase.cpp中添加是否禁止生成机器人控制台变量CVarSpawnBots
// 定义一个静态控制台变量(Console Variable),用于控制是否通过计时器生成机器人
static TAutoConsoleVariable<bool> CVarSpawnBots(
TEXT("su.SpawnBots"), // 控制台变量名
true, // 默认值,表示默认允许通过计时器生成机器人
TEXT("Enable spawning of bots via timer."), // 描述文本
ECVF_Cheat // 控制台变量标志,表示这是一个作弊命令
);
在SpawnBotTimerElapsed方法最开始处添加根据静态控制台变量CVarSpawnBots来禁止机器人生成的逻辑
void AMyGameModeBase::SpawnBotTimerElapsed()
{
// 在游戏线程上获取 CVarSpawnBots 控制台变量的值
if (!CVarSpawnBots.GetValueOnGameThread())
{
// 如果 CVarSpawnBots 的值为 false(禁止生成机器人),则打印一条警告日志,直接返回,不执行后续的生成机器人操作。
UE_LOG(LogTemp, Warning, TEXT("Bot spawning disabled via cvar 'CVarSpawnBots'."));
return;
}
}
1.2 伤害倍数
在MyGameModeBase.cpp中添加伤害倍数控制台变量CVarDamageMultiplier
// 定义一个静态控制台变量(Console Variable),用于控制全局伤害乘数
static TAutoConsoleVariable<float> CVarDamageMultiplier(
TEXT("su.DamageMultiplier"), // 控制台变量名
1.0f, // 默认值,表示默认的伤害乘数为 1.0
TEXT("Global Damage Modifier for Attribute Component."), // 描述文本
ECVF_Cheat // 控制台变量标志,表示这是一个作弊命令
);
在ApplyHealthChange中添加伤害倍数的逻辑
bool UMyAttributeComponent::ApplyHealthChange(AActor* InstigatorActor, float Delta)
{
// 如果 Delta 值小于 0,即表示要对角色造成伤害
if (Delta < 0.0f)
{
// 获取 CVarDamageMultiplier 控制台变量在游戏线程上的值
float DamageMultiplier = CVarDamageMultiplier.GetValueOnGameThread();
// 根据全局伤害乘数调整 Delta 值
Delta *= DamageMultiplier;
}
}
1.3 显示调试绘图
在SInteractionComponent.cpp中添加是否显示调试线控制台变量CVarDebugDrawInteraction
// 定义一个静态控制台变量(Console Variable),用于控制是否显示交互组件的调试线
static TAutoConsoleVariable<bool> CVarDebugDrawInteraction(
TEXT("su.InteractionDebugDraw"), // 控制台变量名
false, // 默认值,表示默认情况下禁用调试线
TEXT("Enable Debug Lines for Interact Component."), // 描述文本
ECVF_Cheat // 控制台变量标志,表示这是一个作弊命令
);
之后的展示调试信息代码就可以加上if语句来判断是否显示调试绘图信息
bool bDebugDraw = CVarDebugDrawInteraction.GetValueOnGameThread();
if(bDebugDraw)
{
//...调试信息
}
注:这里之前检测玩家与其他物品交互时选取的是角色视角的位置,会产生偏差。在MyCharacter中我们对GetPawnViewLocation进行重写,调用了CameraComp->GetComponentLocation()
来获取摄像机组件的位置。这个位置作为角色视角的位置返回,当获取角色视角位置时,可以确保使用的是与角色关联的摄像机的位置。使得能够正确计算射线投射。
FVector AMyCharacter::GetPawnViewLocation() const
{
// 返回摄像机组件的位置,作为角色视角的位置
return CameraComp->GetComponentLocation();
}
1.4 效果演示
2、攻击击飞效果
2.1 新建全局静态函数库
UBlueprintFunctionLibrary是Unreal Engine 4中的一个基类,用于创建自定义的全局静态函数库。通过继承UBlueprintFunctionLibrary类并实现自定义函数,开发者可以在蓝图和C++代码中使用这些函数,方便地实现游戏逻辑和功能。
以BlueprintFunctionLibrary为父类新建C++类MyGameplayFunctionLibrary
UCLASS()
class ACTIONROGUELIKE_API UMyGameplayFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
//实现对目标角色应用伤害
UFUNCTION(BlueprintCallable, Category = "Gameplay")
static bool ApplyDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount);
//实现对目标角色应用方向性伤害
UFUNCTION(BlueprintCallable, Category = "Gameplay")
static bool ApplyDirectionalDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount, const FHitResult& HitResult);
};
在MyGameplayFunctionLibrary.cpp中实现两个应用伤害方法
bool UMyGameplayFunctionLibrary::ApplyDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount)
{
UMyAttributeComponent* AttributeComp = UMyAttributeComponent::GetAttributes(TargetActor);
if (AttributeComp)
{
// 对目标角色应用伤害,调用 UMyAttributeComponent 的 ApplyHealthChange 函数
return AttributeComp->ApplyHealthChange(DamageCauser, -DamageAmount);
}
return false;
}
bool UMyGameplayFunctionLibrary::ApplyDirectionalDamage(AActor* DamageCauser, AActor* TargetActor, float DamageAmount, const FHitResult& HitResult)
{
// 如果成功应用伤害
if (ApplyDamage(DamageCauser, TargetActor, DamageAmount))
{
// 获取受到伤害的组件
UPrimitiveComponent* HitComp = HitResult.GetComponent();
// 如果组件存在并且在命中的骨骼上模拟物理效果
if (HitComp && HitComp->IsSimulatingPhysics(HitResult.BoneName))
{
// 在受到伤害的位置添加一个反方向的冲击力
HitComp->AddImpulseAtLocation(-HitResult.ImpactNormal * 300000.f, HitResult.ImpactPoint, HitResult.BoneName);
}
// 返回 true,表示成功应用方向性伤害
return true;
}
// 如果未成功应用伤害,则返回 false
return false;
}
2.2 调用攻击击飞函数
在MyMagicProjectile.cpp中修改攻击命中函数OnActorOverlap,应用新的可击飞攻击伤害函数。
void AMyMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && OtherActor != GetInstigator())
{
if (UMyGameplayFunctionLibrary::ApplyDirectionalDamage(GetInstigator(), OtherActor, DamageAmount, SweepResult))
{
Explode();
}
}
}
注:要在MyAICharacter中设置碰撞设置和死亡后禁止碰撞体
//构造函数中
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Ignore);
GetMesh()->SetGenerateOverlapEvents(true);
//AI死亡逻辑中
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetCharacterMovement()->DisableMovement();