受击反馈HitReact、死亡效果Death Dissolve、Floating伤害值Text(末尾附 客户端RPC )
受击反馈HitReact
设置角色受击标签
(GameplayTag基本了解待补充)
角色监听标签并设置移动速度
创建一个受击技能,并应用GE
实现设置角色的受击蒙太奇动画
实现角色受击时播放蒙太奇动画,为了保证通用性,将其设置为一个函数,并设置到战斗接口中,这样只需要在战斗接口中获取对应角色的蒙太奇即可。(每个角色的受击动画不一定一样)
UCLASS()
class AURA_API AAuraCharacterBase : public ACharacter, public IAbilitySystemInterface, public ICombatInterface
{
GENERATED_BODY()
public:
//HitReact
virtual UAnimMontage* GetHitReactMontage_Implementation() override;
private:
UPROPERTY(EditAnywhere, Category = "Combat")
TObjectPtr<UAnimMontage> HitReactMontage;
};
UAnimMontage* AAuraCharacterBase::GetHitReactMontage_Implementation()
{
return HitReactMontage;
}
别忘了设置对应的HitReact_AM
激活受击技能
在CharacterClassInfo.h里增加一个参数,用于设置创建敌人时所拥有的初始技能
在函数技能库里新增一个函数用于初始化角色技能
UCLASS()
class AURA_API UMyASBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
//初始化角色的技能
UFUNCTION(BlueprintCallable, Category="MyBlueprintFunctionLibrary|CharacterClassDefaults")
static void GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC);
};
void UMyASBlueprintFunctionLibrary::GiveStartupAbilities(const UObject* WorldContextObject, UAbilitySystemComponent* ASC)
{
//获取到当前关卡的GameMode实例
const AAuraGameModeBase* GameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if(GameMode == nullptr) return;
//从实例获取到关卡角色的配置
UCharacterClassInfo* CharacterClassInfo = GameMode->CharacterClassInfo;
//遍历角色拥有的技能数组
for(const TSubclassOf<UGameplayAbility> AbilityClass : CharacterClassInfo->CommonAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1); //创建技能实例
ASC->GiveAbility(AbilitySpec); //只应用不激活
}
}
在敌人基类里,开始事件时,我们调用函数库的初始化技能函数,将技能应用到角色身上
void AAuraEnemy::BeginPlay()
{
Super::BeginPlay();
InitAbilityActorInfo();
//初始化角色的技能
UMyASBlueprintFunctionLibrary::GiveStartupAbilities(this, AbilitySystemComponent);
...
}
在PostGameplayEffectExecute函数里,之前设置血量下面有判断,我们在角色没有被击杀时,让其触发受击技能。
void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
...
//查看IncomingDamage修改的是否为元属性
if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
//获取到元属性的值备用,并将属性集上的值设置为0,等待下一次设置。
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if(LocalIncomingDamage > 0.f)//伤害传入的时机
{
const float NewHealth = GetHealth() - LocalIncomingDamage; //受到伤害后的新生命值
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth())); //设置新的生命值
const bool bFatal = NewHealth <= 0.f; //血量小于等于0时,角色将会死亡 致命的(Fatal)
if(!bFatal) //在角色没有被击杀时,让其触发受击技能。
{
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FMyGameplayTags::Get().Effects_HitReact); //添个眼 这里原本是Effects.HitReact 对应GA一样 我是修改了
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer); //根据tag标签激活技能
}
}
}
接下来是蓝图中的设置(已做修改)
我们将使用标签激活技能,所以还需要将受击标签设置给技能
我们不需要在每次激活时创建一个新的实例,只需要一个角色生成一个实例重复利用即可。
由于它一个角色对应一个单例,每次触发都是相同的实例,所以,我们需要在其播放完成后,将其结束,才可以重新触发,然后,在敌人的受击动画播放完成后,我们需要将敌人身上的受击标签清楚(如果GE添加的,只需要将对应的GE清除,标签也会随着清除)并结束技能。
死亡效果Death/Dissolve
我们实现了敌人受到攻击后会播放受击动画,并且还给角色设置了受击标签。并在角色受击时,在角色身上挂上受击标签,在c++里,如果挂载了此标签,速度将降为0 。
受击有了,接下来我们将实现角色的死亡逻辑,角色血量为0或者小于0时,我们将触发它的死亡功能
实现死亡
在战斗接口类里增加一个纯虚函数无法创建函数的实现,必须在子类里面去覆写它。
virtual void Die() = 0;
在角色基类里面覆写
virtual void Die() override;
接着我们增加一个在每个客户端上执行的函数,被Die函数调用。
NetMulticast设置后,这个函数被调用时,将在服务器执行,然后复制到每个客户端。和它对应的还有(Server:只在服务器运行,Client:只在调用此函数的客户端运行)这种情况的函数实现需要在后面加上_Implementation
Reliable: 这是一个传输属性,表示该函数的数据应该以可靠的方式发送。
UFUNCTION(NetMulticast, Reliable)
virtual void MulticastHandleDeath();
void AAuraCharacterBase::Die()
{
//将武器从角色身上分离(Detach)
Weapon->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
MulticastHandleDeath(); //NetMulticast在服务器执行,然后复制到每个客户端()
}
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
//开启武器物理效果
Weapon->SetSimulatePhysics(true); //开启模拟物理效果
Weapon->SetEnableGravity(true); //开启重力效果
Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly); //开启物理碰撞通道
//开启角色物理效果
GetMesh()->SetSimulatePhysics(true); //开启模拟物理效果
GetMesh()->SetEnableGravity(true); //开启重力效果
GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly); //开启物理碰撞通道
GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block); //开启角色与静态物体产生碰撞
//关闭角色碰撞体碰撞通道,避免其对武器和角色模拟物理效果产生影响
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
在敌人基类里面也覆盖Die函数,并在死亡时设置它的清除时间
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Combat")
float LifeSpan = 5.f; //设置死亡后的存在时间
void AAuraEnemy::Die()
{
SetLifeSpan(LifeSpan); //在Enemy死亡后尸体会存在五秒 对象不再需要时会自动销毁
Super::Die();
}
接下来在AttributeSet的PostGameplayEffectExecute函数里,增加Die函数调用的逻辑处理,我们在死亡时调用即可
void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
。。。
//查看IncomingDamage修改的是否为元属性
if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
//获取到元属性的值备用,并将属性集上的值设置为0,等待下一次设置。
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if(LocalIncomingDamage > 0.f)//伤害传入的时机
{
const float NewHealth = GetHealth() - LocalIncomingDamage; //受到伤害后的新生命值
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth())); //设置新的生命值
const bool bFatal = NewHealth <= 0.f; //血量小于等于0时,角色将会死亡 致命的(Fatal)
if(bFatal)
{
//调用死亡函数
ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
if(CombatInterface)
{
CombatInterface->Die();
}
}
else
{
在角色没有被击杀时,让其触发受击技能。
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FMyGameplayTags::Get().Abilities_HitReact); //添个眼 这里原本是Effects.HitReact 对应GA一样 我是修改了
Props.TargetASC->TryActivateAbilitiesByTag(TagContainer); //根据tag标签激活技能
}
}
}
测试效果
溶解材质(目前渲染方面不做了解)
小怪死亡定时直接清除掉,显得太突兀,大部分游戏中的做法就是使用溶解效果来实现它的缓慢消失的效果。
实现溶解效果
实现从普通材质切换MI到溶解材质,并实现通过程序修改Dissolve溶解的标量Scalar Parameter参数数值。(步骤
增加两个变量,用于设置角色和武器的溶解材质
增加一个溶解函数,在角色死亡时调用
溶解是需要一个时间过程,所以可以在蓝图里面实现时间轴,所以增加一个蓝图可实现函数,这个函数在代码里调用,在蓝图实现。
// YanWei.
UCLASS()
class AURA_API AAuraCharacterBase : public ACharacter, public IAbilitySystemInterface, public ICombatInterface
{
GENERATED_BODY()
public:
//--ICombatInterface
virtual UAnimMontage* GetHitReactMontage_Implementation() override;//HitReact
virtual void Die() override; //Death OnServer 服务器端
UFUNCTION(NetMulticast, Reliable) //NetMulticast:在服务器执行,然后复制到每个客户端
virtual void MulticastHandleDeath();
protected:
//--Dissolve Effect 实现思路:是将avatar和weapon的材质叫换成下面的
//溶解函数
void Dissolve();
//蓝图可实现事件
UFUNCTION(BlueprintImplementableEvent)
void StartDissolveTimeline(const TArray<UMaterialInstanceDynamic*>& DynamicMaterialInstance);
//接受材质实例dyn 数组用于存储材质s
UPROPERTY(EditAnywhere,BlueprintReadOnly)
TObjectPtr<UMaterialInstance> DissolveMaterialInstance;
//对应的材质实例
UPROPERTY(EditAnywhere,BlueprintReadOnly)
TObjectPtr<UMaterialInstance> WeaponDissolveMaterialInstance;
//
};
//cpp
void AAuraCharacterBase::Dissolve()
{
TArray<UMaterialInstanceDynamic*> DynMatArray;
if (DissolveMaterialInstance)
{
UMaterialInstanceDynamic* DynamicMatInst = UMaterialInstanceDynamic::Create(DissolveMaterialInstance, this);
GetMesh()->SetMaterial(0, DynamicMatInst);
DynMatArray.Add(DynamicMatInst);
}
if (WeaponDissolveMaterialInstance)
{
UMaterialInstanceDynamic* DynamicMatInst = UMaterialInstanceDynamic::Create(WeaponDissolveMaterialInstance, this);
Weapon->SetMaterial(0, DynamicMatInst);
DynMatArray.Add(DynamicMatInst);
}
StartDissolveTimeline(DynMatArray);
}
void AAuraCharacterBase::Die()
{
//将武器从角色身上分离(Detach)
Weapon->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
MulticastHandleDeath(); //NetMulticast在服务器执行,然后复制到每个客户端()
}
void AAuraCharacterBase::MulticastHandleDeath_Implementation()
{
...
//设置角色溶解
Dissolve();
}
打开UE,在敌人基类里面覆写StartDissolveTimeline (增加线结点<Add Reroute>)
创建一个时间轴 Timeline 并点击它
Set Scalar Parameter Value
效果图
伤害值浮动FloatingText
实现技能或者攻击对敌人造成的伤害数值,并直观的显示出来。接下来,还要实现一个用户控件,来设置伤害数值的UI表现。并且还需要一个用户控件的组件来修改伤害UI显示的数值。在创建完成这些以后,我们还需要实现从AttributeSet里面实现在目标位置(也就是受到伤害的角色位置)创建组件,并实现在每个客户端上面显示数值。
创建Damage用户控件
新建一个控件蓝图,这里不需要其它脚本直接基于UserWidget创建即可。我们将其命名为WBP_DamageText
在控件里面增加一个覆层,覆层下面添加一个文本显示文字
在控件尺寸这里,我们使用自定义,来设置一个固定的尺寸,这样它的尺寸不会乱动,不会在更新时频繁修改尺寸影响性能。
然后设置文本的样式,并将其设置为变量,方便后期修改其显示的内容
创建伤害文字动画 (视觉反馈很重要!)
动画创建完成后,在事件构造时调用动画播放
创建用户控件组件
用户控件创建完成,我们还需要创建用户控件组件,用于实现用户控件的实例化并播放动画。
所以,我们接下来要创建一个用户组件的控件,创建基于最基本的组件即可,我们不需要从控制器获取内容,只需要一个设置显示数字的方法即可。命名为DamageTextComponent
增加一个函数,设置伤害的数值,可以在蓝图内实现此函数,并且可以在蓝图内调用
基于此类创建一个蓝图类,命名为BP_DamageTextComponent
现在这个蓝图相当于一个未挂载在对象上面的组件类,但是我们可以设置它的默认参数。我们将空间修改为屏幕空间,并将创建的用户控件设置上去
在蓝图内,我们实现SetDamageText,将设置的类型转换为我们创建的用户组件类型,调用它的更新数字方法,然后在延迟一秒钟(动画播放一秒钟)后,将自身销毁,组件也会跟随销毁。
其中 UpdateDamageText 鼠标处设0是为了只显示整数部分
调用实现伤害显示
现在,我们有了显示伤害数值的用户控件和组件,接下来就要考虑实现如何调用并显示对应的内容。
现在有个问题,就是我们是通过在AttributeSet里面通过元属性获取到当前的伤害的,它是在服务器上运行的,因此,我们要实现在AttributeSet里面去调用函数生成,并且,还需要获取到玩家控制器,因为你需要将数值显示给玩家查看得,所以,我们将数值在玩家控制器里面实现在每个客户端上面调用是一个很好的选择。
所以,我们打开玩家控制器的基类,在里面增加一个函数,用于复制到每个客户端,并且只会在客户端执行所以我们设置了Client (只在客户端执行)
在函数内部实现在目标角色身上显示伤害数字。
(制作一个客户端RPC函数,如果在服务端调用它,它将在服务端执行,如果玩家控制器是本地Local的话;但是如果玩家控制器是Remote远程的话,这个函数将被执行在remote远程的客户端)
// YanWei.
class UDamageTextComponent;
/**
*
*/
UCLASS()
class AURA_API AAuraPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AAuraPlayerController();
virtual void PlayerTick(float DeltaTime) override;
//客户端RPC函数
UFUNCTION(Client, Reliable)
void ShowDamageNumber(float DamageAmount, ACharacter* TargetCharacter); //在每个客户端显示伤害数值
private:
。。。
//DamageText 创建一个用于设置显示伤害数值的组件类,后续可以使用它去实例化多个实例,显示多个伤害数值
UPROPERTY(EditDefaultsOnly)
TSubclassOf<UDamageTextComponent> DamageTextComponentClass;
};
// 客户端的具体实现
void AAuraPlayerController::ShowDamageNumber_Implementation(float DamageAmount,ACharacter* TargetCharacter)
{
if(IsValid(TargetCharacter) && DamageTextComponentClass)
{
//在内部实例化组件,并注册,在实例化时,第一个参数相当于作为此组件的父类,当PlayerController被销毁时,它也会被销毁。
UDamageTextComponent* DamageText = NewObject<UDamageTextComponent>(TargetCharacter, DamageTextComponentClass);
DamageText->RegisterComponent(); //动态创建的组件需要调用注册
DamageText->AttachToComponent(TargetCharacter->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); //先附加到角色身上,使用角色位置
DamageText->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); //然后从角色身上分离,保证在一个位置播放完成动画
DamageText->SetDamageText(DamageAmount); //设置显示的伤害数字
}
}
// YanWei.
/**
*
*/
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
GENERATED_BODY()
protected:
//用于服务器端调用客户端RPC函数,在客户端执行
static void ShowFloatingText(const FEffectProperties& Props, const float Damage);
};
// 服务器调用客户端函数的实现
void UAuraAttributeSet::ShowFloatingText(const FEffectProperties& Props, const float Damage)
{
//调用显示伤害数字
if(Props.SourceCharacter != Props.TargetCharacter)
{
if(AAuraPlayerController* PC = Cast<AAuraPlayerController>(UGameplayStatics::GetPlayerController(Props.SourceCharacter, 0)))
{
//调用客户端RPC函数
PC->ShowDamageNumber(Damage, Props.TargetCharacter); //调用显示伤害数字
}
}
}
void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
。。。
if(LocalIncomingDamage > 0.f)//伤害传入的时机
{
。。。
//TODO::设置显示float伤害值的区域
ShowFloatingText(Props, LocalIncomingDamage);// 服务端请求客户端执行
}
}
在BP_Controller控制器中设置bpCompDamageText
测试结果
每个客户端只能看到自己所造成的浮动伤害值,其他客户端看不到别的客户端(也包括监听服务器端)造成浮动伤害值,而(监听)服务端可以看到所有的浮动伤害值。