在上一篇,我们实现了通过AI行为树控制战士敌人靠近攻击目标触发近战攻击技能,并在蒙太奇动画中触发事件激活攻击的那一刻的伤害判断,在攻击时,我们绘制了一个测试球体,用于伤害范围。
在之前实现的火球术中,我们实现的是一个单体伤害技能,在近战中,我们想实现对范围内的敌人判断,并造成伤害。这也是RPG游戏的通常做法。
所以,我们将在这篇中,实现角色的死亡逻辑,然后接着实现一个函数去获取一定范围内的敌人并造成伤害。
实现死亡逻辑
在敌人接口这里,我们增加两个函数,一个用于判断角色是否死亡,另一个获取角色的Avatar,它们都被设置了BlueprintNativeEvent,它会通过蓝图初始化虚函数,并且可以在蓝图中覆写(如果在蓝图中覆写,C++版本的实现将会失效)
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)bool IsDead() const; //获取当前角色是否死亡UFUNCTION(BlueprintNativeEvent, BlueprintCallable)AActor* GetAvatar(); //获取当前角色
由于每个角色都需要此功能,我们将在角色基类上面去修改它,首先覆盖它们的父类函数
/* ICombatInterface战斗接口 */virtual UAnimMontage* GetHitReactMontage_Implementation() override;virtual FVector GetCombatSocketLocation_Implementation() const override;virtual bool IsDead_Implementation() const override;virtual AActor* GetAvatar_Implementation() override;virtual void Die() override;/* ICombatInterface战斗接口 结束 */
创建一个变量,来记录当前是否死亡
protected:// Called when the game starts or when spawnedvirtual void BeginPlay() override;bool bDead = false; //当前角色死亡状态
实现前面创建的两个函数
bool ARPGCharacter::IsDead_Implementation() const
{return bDead;
}AActor* ARPGCharacter::GetAvatar_Implementation()
{return this;
}
我们现在能获取了,还需要一个设置死亡的地方,我们在实现角色死亡这里实现了一个死亡函数,它在内部调用触发每个客户端都会运行此函数,我们可以把设置逻辑写到此函数中。
在函数底部增加设置死亡的变量
实现获取范围内的敌人函数
我们现在需要实现一个函数来获取一定范围内的敌人。所以,在我们函数库增加一个新的函数用于获取。
要实现这个功能,我们可以查找以下引擎的库里面是否包含此类型的函数,稍微修改一下,在GameplayStatics文件中,有个名为ApplyRadialDamageWithFalloff函数,它和我们所需的类型差不多,创建一个球的配置项,然后忽略掉一些Actor,然后进行查询,获取到对应的Actors,跟我们所需的效果一致,我们只需要返回即可。
我们创建一个蓝图库函数,可以通过此函数获取到所有攻击位置的Actor
//获取到攻击位置半径内的所有动态ActorUFUNCTION(BlueprintCallable, Category="MyAbilitySystemLibrary|GameplayMechanics")static void GetLivePlayersWithinRadius(const UObject* WorldContextObject, TArray<AActor*>& OutOverlappingActors, const TArray<AActor*>& ActorsToIgnore, float Radius, const FVector& SphereOrigin);
在实现这里,仿造官方库的写法,获取到所有与设置的碰撞体碰撞的数组,然后对数据遍历,将所需的对象返回
void URPGAbilitySystemBlueprintLibrary::GetLivePlayersWithinRadius(const UObject* WorldContextObject,TArray<AActor*>& OutOverlappingActors, const TArray<AActor*>& ActorsToIgnore, float Radius,const FVector& SphereOrigin)
{FCollisionQueryParams SphereParams; //创建一个碰撞查询的配置SphereParams.AddIgnoredActors(ActorsToIgnore); //添加忽略的ActorTArray<FOverlapResult> Overlaps; //创建存储检索到的与碰撞体产生碰撞的Actorif (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) //获取当前所处的场景,如果获取失败,将打印并返回Null{//获取到所有与此球体碰撞的动态物体World->OverlapMultiByObjectType(Overlaps, SphereOrigin, FQuat::Identity, FCollisionObjectQueryParams(FCollisionObjectQueryParams::InitType::AllDynamicObjects), FCollisionShape::MakeSphere(Radius), SphereParams);for(FOverlapResult& Overlap : Overlaps) //遍历所有获取到的动态Actor{//判断当前Actor是否包含战斗接口 Overlap.GetActor() 从碰撞检测结果中获取到碰撞的Actorconst bool ImplementsCombatInterface = Overlap.GetActor()->Implements<UCombatInterface>();//判断当前Actor是否存活,如果不包含战斗接口,将不会判断存活(放置的火堆也属于动态Actor,这样保证不会报错)if(ImplementsCombatInterface && !ICombatInterface::Execute_IsDead(Overlap.GetActor())) {OutOverlappingActors.AddUnique(Overlap.GetActor()); //将Actor添加到返回数组,AddUnique 只有在此Actor未被添加时,才可以添加到数组}}}
}
接着我们在技能类里面,调用创建的此函数,中心设置为在角色上面设置的攻击位置,将自身传入忽略的数组,设置好半径,然后将返回的数组遍历,绘制测试图形,记得在遍历结束后,结束此技能。
接着查看攻击效果,可以看出,攻击时,不但可以在玩家角色身上绘制测试图形,也可以在敌人身上绘制。
应用GE
现在我们能够获得到需要造成伤害的Actor,那么接下来,我们将实现伤害的应用。按照我们之前的经验 ,我们需要一个GameplayEffect去对攻击目标造成伤害,在前面的章节49. UE5 RPG 使用Execution Calculations处理对目标造成的最终伤害我们实现了一个造成伤害的GE。并且在54. UE5 RPG 增加伤害类型 增加了技能可以造成多种属性的伤害,我们可以给近战攻击技能直接使用这个GE。
我们需要做的就是创建GE的实例,然后使用SetByCaller将技能的伤害数值传递给GE的实例,接下来,我们将在技能蓝图中通过连连看实现此功能,然后在c++中实现我们实际需要使用的函数。
首先,我们先配置技能的造成伤害的GE,并设置当前技能造成的伤害,我们这里设置的造成物理伤害,并且我们需要在曲线表格中新增加一条伤害曲线使用。
伤害我们设置了从1级到40级的伤害
完成了准备工作,接下来,我们就可以修改蓝图的逻辑,在获取到攻击目标后,我们遍历攻击目标数组,从伤害类型中,获取到所有的Keys,然后遍历获取到对应的Value(在蓝图无法直接遍历),我这个现在写的还有问题,就是给每个类型的伤害创建的一个Spec,如果只设置了一种类型的伤害,那么效果是一致的。这里主要也是给展示一下如何实现。
根据等级获取技能伤害节点是通过代码实现的节点,卸载蓝图函数库中
那么接下来,我们在C++中实现这个逻辑,这样在蓝图中只需要调用一个方法即可。
我们在RPGDamageGameplayAbility.h类里面增加一个函数,用于给目标应用伤害
UFUNCTION(BlueprintCallable)void CauseDamage(AActor* TargetActor);
在cpp文件中实现它
void URPGDamageGameplayAbility::CauseDamage(AActor* TargetActor)
{//创建GEFGameplayEffectSpecHandle DamageSpecHandle = MakeOutgoingGameplayEffectSpec(DamageEffectClass, 1.f);//通过SetByCaller设置属性伤害for(auto Pair : DamageTypes){const float ScaleDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel());UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(DamageSpecHandle, Pair.Key, ScaleDamage);}//将GE应用给目标GetAbilitySystemComponentFromActorInfo()->ApplyGameplayEffectSpecToTarget(*DamageSpecHandle.Data.Get(),UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor));
}
编译打开UE,在蓝图中修改成我们创建的节点,针对每个目标调用一次函数即可。
处理玩家角色被敌人攻击不显示伤害数字的问题
我们在被敌人攻击后,无法在角色身上显示伤害的数字,我们去查看一下在AttributeSet里面如何创建的伤害数字的显示。
在显示伤害数字函数中,我们是通过获取到角色的PlayerController,然后通过调用PlayerController身上的函数来实现的。如果是敌人攻击玩家角色,我们是无法在敌人身上获取到PlayerController的。
所以这里的修改可以修改为,如果从SourceCharacter无法获取到PlayerController,那么我们判断一下目标角色身上获取,然后从目标角色身上获取PlayerController进行调用显示,这样,和这次技能有关的角色都会显示对应的伤害。
void URPGAttributeSet::ShowFloatingText(const FEffectProperties& Props, const float Damage, bool IsBlockedHit, bool IsCriticalHit)
{//调用显示伤害数字if(Props.SourceCharacter != Props.TargetCharacter){//从技能释放者身上获取PC并显示伤害数字if(ARPGPlayerController* PC = Cast<ARPGPlayerController>(Props.SourceCharacter->Controller)){PC->ShowDamageNumber(Damage, Props.TargetCharacter, IsBlockedHit, IsCriticalHit); //调用显示伤害数字}//从目标身上获取PC并显示伤害数字if(ARPGPlayerController* PC = Cast<ARPGPlayerController>(Props.TargetCharacter->Controller)){PC->ShowDamageNumber(Damage, Props.TargetCharacter, IsBlockedHit, IsCriticalHit); //调用显示伤害数字}}
}
接着运行查看效果。
处理多人模式下的问题
我们将运行模式修改,数量修改为两人
运行起来后,发现敌人被攻击后,会报错,原因是因为AIController为null
我们加个条件判断即可,判断AIController和黑板组件有没有被设置,如果存在才可以调用。
void ARPGEnemy::HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{bHitReacting = NewCount > 0;GetCharacterMovement()->MaxWalkSpeed = bHitReacting ? 0.f : BaseWalkSpeed;//设置黑板键的值if(RPGAIController && RPGAIController->GetBlackboardComponent()){RPGAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), bHitReacting);}// GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Green, FString::Printf(TEXT("Hit React bool: %i"), bHitReacting));
}