GameplayEffects
GameplayEffects介绍
GameplayEffect
是UGameplayEffect类型的对象,我们使用GameplayEffect
来改变属性和Tags,
GameplayEffect
只用作于处理数据,而不执行逻辑,其通过Modifiers
和 Executions
来修改属性,通常设计者只需要创建UGameplayEffect
的蓝图派生类
修改器(Modifiers
)用于修改属性并且是属性修改预测的仅有方式。一个GameplayEffect
可以有0个或多个Modifiers
。每一个修改器只能通过下述方式修改一个属性:Add,Mutiply,DIvide,Override
Attribute
的CurrentValue
是一系列Modifiers
添加到BaseValue
的聚合结果(就像Vue中的computed,每当Effect变化,都会重新执行该公式重新计算当前属性)。聚合Modifiers
的公式如下所示(FAggregatorModChannel::EvaluateWithBase
)
((InlineBaseValue + Additive) * Multiplicitive) / Division
一共有四种Modifiers
:Scalable Float, Attribute Based, Custom Calculation Class, and Set By Caller。这些全部通过浮点值和操作符改变Modifier
的Attribute
。
修改器类型 | 描述 |
---|---|
Scalable Float | FScalableFloats是一种能够指向Data Table(行表示变量,列表示等级)的结构。 Scalable Floats将根据当前技能等级(或者是在GameplayEffectSpec覆盖的等级)自动读取值。这个值可以根据系数进一步处理。如果没有指定数据表,值会被当作是1,需要硬编码系数作为实际的值(忽略等级) |
Attribute Based | Attribute Based Modifiers 基于源(GameplayEffectSpec创建者)或目标(GameplayEffectSpec接收者)的支持属性的CurrentValue 或BaseValue并且可通过系数、Pre/Post Multiply Additive Value进一步处理。快照(Snapshot)意味着取GameplayEffectSpec被创建时属性的值,否则取GameplayEffectSpec被应用时属性的值。 |
Custom Calculation Class | Custom Calculation Class 是最灵活和最复杂的Modifiers。这个Modifier需要创建一个ModifierMagnitudeCalculation类并且可通过系数、Pre/Post Multiply Additive Value进一步处理。 |
Set By Caller | SetByCaller Modifiers是在GameplayEffect之外由Ability在运行时设置或者由GameplayEffectSpec的创建者设置。 例如,当你想根据按钮按下的时间决定伤害大小时可以使用SetByCaller。SetByCallers本质是存在于GameplayEffectSpec上的TMap<FGameplayTag, float>,Modifier仅仅是告诉聚合器通过GameplayTag去检索值。 SetByCallers仅能使用GameplayTag不能使用FName。如果没有在GameplayEffectSpec中找到GameplayTag对应的值,游戏将会抛出一个运行时错误并且返回0。如果运算是除法你就悲剧了。 具体使用详见SetByCallers。 |
GameplayEffectExecutionCalculations
(ExecutionCalculation
, Execution
(在插件代码中经常会看到这个术语), or ExecCalc
) 是GameplayEffects
改变ASC
的一种最有力的方式。与ModifierMagnitudeCalculations
类似,ExecCalc
可以获取Attributes
。 与MMCs
不同的是,ExecCalc
可以改变多个属性并且高效的处理任何事情。
GameplayEffects
有三种持续类型:立即(Instant
),持续( Duration
),和无限(Infinite
)。
后面我们再逐一使用他们来构建Gameplay
改进之前的EffectActor
立即效果
在之前有一个AuraEffectActor,我们把它当作生命药水来使用
//TODO Change this to apply Gameplay Effect. But now,using const_cast as a hack
我们有这样一个TODO,现在我们就要使用GameplayEffect来改进它
我们清空整个EffectActor,我们想要整个Actor在蓝图中很灵活且方便控制,建立一个Root根组件,可以在蓝图中配置子项
//AuraEffectActor.cpp
AAuraEffectActor::AAuraEffectActor()
{
PrimaryActorTick.bCanEverTick = true;
SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneRoot"));
}
然后在蓝图中配置BP_HealthPotion
然后添加一个sphere,大小调整,然后设置Mesh中的碰撞为NoCollision
然后我们在蓝图事件中
放置一个节点,组件开始重叠时,我们就要为目标添加一个Effect,回到C++中
//AAuraEffectActor.h
//...
protected:
virtual void BeginPlay() override;
//要对目标添加的EffectClass
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
TSubclassOf<UGameplayEffect> InstantGameplayEffectClass;
//对目标添加Effects的执行逻辑
UFUNCTION(BlueprintCallable)
void ApplyEffectToTarget(AActor* Target,TSubclassOf<UGameplayEffect> GameplayEffectClass);
};
//AAuraEffectActor.cpp
void AAuraEffectActor::ApplyEffectToTarget(AActor* Target, TSubclassOf<UGameplayEffect> GameplayEffectClass)
{
//这个函数是工具库,可以获取实现了IAbilitySystem接口的Actor的AbilitySystem
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target);
if(!TargetASC) return;
//检查是否Class不为null
check(GameplayEffectClass)
//获取ASC的EffectContext
FGameplayEffectContextHandle EffectContextHandle = TargetASC->MakeEffectContext();
//为ContextHandle添加源(就是这个效果的添加者)
EffectContextHandle.AddSourceObject(this);
//制作一个Spec
FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass,1.f,EffectContextHandle);
//为Target添加效果
TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
}
//...
然后在蓝图中,创建GE_PotionHeal,作为GameplayEffect
的子类,我们在其中设置治疗效果
然后设置InstantGameplayEffectClass,并在重叠时调用ApplyEffectToTarge t
在网络模式下也正常
Quest:制作Mana药水
现在生命药水和法力药水都做好了
持续时间效果
创建一个基于持续时间的效果,在10秒内增加自己的最大生命值100,10秒后撤回
在AuraEffectActor.h中添加一个DurationGameplayEffectClass
//AuraEffectActor.h
//...
protected:
//...
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
TSubclassOf<UGameplayEffect> DurationGameplayEffectClass;
//...
然后直接在蓝图中设置
周期性持续时间效果
周期性效果,在10秒内,每秒回复10血量(在之前持续效果的水晶中修改)
Period
周期,每隔一段时间后会触发Exectute Periodic Effect on Application
立即执行第一个周期的效果,否则等待一个周期后执行
然后就可以发现血量每隔一秒回复一次
无限持续时间效果
我们想要做一个火堆,在角色踩上时,会一直造成伤害
火堆
事件
Effect
但是在游戏中,我们想要离开火堆时,也会持续永久造成伤害
所以,我们想要设置一些ApplicationPolicy来规范化代码
//AuraEffectActor.h
UENUM(BlueprintType)
//在Overlap开始时还是结束时应用Effect效果
enum class EEffectApplicationPolicy
{
ApplyOnOverlap,
ApplyOnEndOverlap,
DoNotApply
};
//在Overlap结束时是否移除Effect效果(无限时长的Effect)
UENUM(BlueprintType)
enum class EEffectRemovalPolicy
{
RemoveOnEndOverlap,
DoNotRemove
};
//...
protected:
UFUNCTION(BlueprintCallable)
void OnOverlap(AActor* TargetActor);
UFUNCTION(BlueprintCallable)
void OnEndOverlap(AActor* TargetActor);
//...
//是否在效果移除时摧毁自身
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
bool bDestroyOnEffectRemoval = false;
//...
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
EEffectApplicationPolicy InstantEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
EEffectApplicationPolicy DurationEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
EEffectApplicationPolicy InfiniteEffectApplicationPolicy = EEffectApplicationPolicy::DoNotApply;
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
EEffectRemovalPolicy InfiniteEffectRemovalPolicy = EEffectRemovalPolicy::DoNotRemove;
我们想要经过一系列判断,来确定某个Effect类是在Overlap开始时调用还是结束时调用还是不调用,或者某个无限时长的Effect类是否要在Overlap结束时移除
//AuraEffectActor.cpp
void AAuraEffectActor::OnOverlap(AActor* TargetActor)
{
if(InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
{
ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
}
if(DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
{
ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass);
}
if(InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnOverlap)
{
ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
}
}
void AAuraEffectActor::OnEndOverlap(AActor* TargetActor)
{
if(InstantEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
{
ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
}
if(DurationEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
{
ApplyEffectToTarget(TargetActor,DurationGameplayEffectClass);
}
if(InfiniteEffectApplicationPolicy == EEffectApplicationPolicy::ApplyOnEndOverlap)
{
ApplyEffectToTarget(TargetActor,InstantGameplayEffectClass);
}
if(InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
//去除InfiniteEffect
}
}
但是,我们现在还不知道如何去除InfiniteEffect
//去除附着在Actor上的GameplayEffect函数
UAbilitySystemComponent::RemoveActiveGameplayEffect(FActiveGameplayEffectHandle Handle);
有了这个函数,我们就可以做到去除InfiniteEffect了,首先我们要先获取到FActiveGameplayEffectHandle
AAuraEffectActor::ApplyEffectToTarget(...){
//...
//为Target添加效果,然后获取FActiveGameplayEffectHandle
FActiveGameplayEffectHandle ActiveEffectHandle =
TargetASC->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
//如果这个Effect为无限时长的类,且当前无限时长类的policy为RemovedOnEndOverlap,
//则需要存储ActiveEffectHandle,以便后续删除
bool bIsInfinite = false;
if(EffectSpecHandle.Data->Def.Get()->DurationPolicy == EGameplayEffectDurationType::Infinite)
{
bIsInfinite = true;
}
if(bIsInfinite && InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
ActiveEffectHandles.Add(ActiveEffectHandle,TargetASC);
}
//...
AAuraEffectActor::OnEndOverlap(...){
//...
if(InfiniteEffectRemovalPolicy == EEffectRemovalPolicy::RemoveOnEndOverlap)
{
//去除InfiniteEffect
UAbilitySystemComponent* TargetASC =
UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
TArray<FActiveGameplayEffectHandle> RemovedKeys;
for(TTuple<FActiveGameplayEffectHandle, UAbilitySystemComponent*>& Tuple:ActiveEffectHandles)
{
if(TargetASC == Tuple.Value)
{
TargetASC->RemoveActiveGameplayEffect(Tuple.Key);
RemovedKeys.Add(Tuple.Key);
}
}
for(auto& Key:RemovedKeys)
{
ActiveEffectHandles.Remove(Key);
}
}
}
//AuraEffectActor.h
//...
TMap<FActiveGameplayEffectHandle ,UAbilitySystemComponent*> ActiveEffectHandles;
- ActiveEffectHandles
键值对**ActiveEffectHandles
** 是用来存储FActiveGameplayEffectHandle的,为什么要以FActiveGameplayEffectHandle
为key呢?只有FActiveGameplayEffectHandle
不会造成key冲突,如果以UAbilitySystemComponent或者AActor为key的话,都有可能造成冲突,导致value被更改;
最后就是修改蓝图,将InfiniteEffectRemovalPolicy改为RemovedOnEndOverlap
此时,我们就达成了目的
如果将3个火堆堆在一起,然后stackSize设为3,当我们从3个火堆的区域进入到2个火堆区域时,所有的stack都消除了,这是因为RemoveActiveGameplayEffect
函数有第二个参数,stack,默认为-1 UAbilitySystemComponent::RemoveActiveGameplayEffect(FActiveGameplayEffectHandle Handle,int32 StacksToRemove =-1);
-1就代表将所有stack清空;当我们将第二个参数设为1后,3个火堆退出到2个火堆时,就不会出现stack全部消除的情况了
PreAttributeChange
在之前的扣血时,我们发现,血量可以被扣到了负数,但是这不应该发生,所以我们应该加一个扣血前的回调函数,让血量不会下降到0以下,或者上升到MaxHealth以上
我们应该对其进行限制,其方法就是一个虚函数PreAttributeChange
,官方文档是如下解释的:
//AttributeSet.h
/**
* Called just before any modification happens to an attribute. This is lower level than PreAttributeModify/PostAttribute modify.
* There is no additional context provided here since anything can trigger this. Executed effects, duration based effects, effects being removed, immunity being applied, stacking rules changing, etc.
* This function is meant to enforce things like "Health = Clamp(Health, 0, MaxHealth)" and NOT things like "trigger this extra thing if damage is applied, etc".
*
* NewValue is a mutable reference so you are able to clamp the newly applied value as well.
*/
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }
- 文档释义
-
这个函数会在任何属性被修改之前调用。属性可以是游戏角色的状态(例如 健康值、法力值、力量 等)。
-
它比其他修改属性的回调(如
PreAttributeModify
或PostAttributeModify
)更低层次。即,这个函数是在更底层的情况下运行的,发生在更早的阶段。 -
调用此函数时,没有提供任何额外的上下文信息,因为任何操作都可能触发它。比如,可能是因为效果应用、持续时间影响、效果移除、免疫应用、叠加规则改变等。
-
它可以在以下几种场景中被触发:
-
执行的效果(例如:增加伤害、治疗等)
-
基于持续时间的效果(例如:随着时间的推移逐渐增加或减少属性)
-
效果被移除(例如:移除一个增益或减益效果)
-
免疫应用(例如:免疫某些效果的影响)
-
叠加规则的变化(例如:属性叠加的规则发生改变)
-
这个函数的主要作用是强制执行某些约束或规则,比如限制某个属性的值在一个特定范围内。
-
举例来说,如果是生命值(
Health
),可以在这个函数中强制将其限定在一个范围,比如:- 不能低于 0(防止出现负数生命值)
- 不能高于最大生命值(
MaxHealth
)
-
类似
Health = Clamp(Health, 0, MaxHealth)
的操作。 -
此函数 不 应该用于触发额外的操作,比如检测是否应用了伤害并触发额外效果。它的目的是对属性进行简单的约束,而不是执行复杂的逻辑。
-
在属性发生改变之前,你可以对即将应用的新值(
NewValue
)进行处理或限制,例如对其进行Clamp
操作(将其限制在某个范围内)。
-
如上所说的Clamp
是虚幻中FMath工具类中的方法,将NewValue限制到某个范围内,所以我们现在就可以开始改代码了,我们对Health和Mana进行限制
//AuraAttributeSet.h
//...
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
//AuraAttributeSet.cpp
//...
void UAuraAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
if(Attribute == GetHealthAttribute())
{
NewValue = FMath::Clamp(NewValue,0.f,GetMaxHealth());
}
if(Attribute == GetManaAttribute())
{
NewValue = FMath::Clamp(NewValue,0.f,GetMaxMana());
}
}
但是,NewValue
指的是当前的属性值,这个值可以受到多个修改器的叠加影响。
在 PreAttributeChange
里,你可以影响即将应用的新值,但 它不会永久性地改变任何修改器,也就是说它不改变修改器的基础值,只是暂时调整 返回的属性值。
之后,系统会从所有影响这个属性的 修改器(Modifiers) 中 重新计算 ,并将其作为最终的属性值。因此,任何变化都可能会被叠加或再次修改。
比如,我们修改PotionHeal中的Modifiers,当前比如95滴血,满血100
喝到生命药水后是92滴血还是97滴呢?
97滴血,这就证明了,虽然在PreAttributeChange
里Clamp了NewValue,还是经过了又一次的Modifier计算,然后最终为97滴而不是先加10后减8的92滴血,所以,PreAttributeChange
不是最终结果,我们不能在里面去进行一些效果比如"反伤刺甲",在受到Effect的时候去进行其他操作
那么有什么方法能够得到应用某个Effect之后最终的Attribute值呢?
PostGameplayEffectExecute
PostGameplayEffectExecute
是一种常用于自定义处理游戏效果执行后的逻辑的方法。它通常在派生自 UGameplayEffectExecutionCalculation
或 UGameplayAbility
类的类中重载,用于在游戏效果执行后进行进一步处理。
我们可以在PostGameplayEffectExecute
中获取关于源、目标和Effect的所有信息
//AuraAttributeSet.h
//...
//能够在PostGameplayEffectExecute中获取到的信息,放到一个结构体中方便获取
USTRUCT()
struct FEffectProperties
{
GENERATED_BODY()
FEffectProperties(){}
FGameplayEffectContextHandle EffectContextHandle;
//Source 代表这个Effect是从哪个Actor释放的,Target代表自身(拥有该AttributeSet的角色)
UPROPERTY()
UAbilitySystemComponent* SourceASC=nullptr;
UPROPERTY()
AActor* SourceAvatarActor=nullptr;
UPROPERTY()
AController* SourceController=nullptr;
UPROPERTY()
ACharacter* SourceCharacter=nullptr;
UPROPERTY()
UAbilitySystemComponent* TargetASC=nullptr;
UPROPERTY()
AActor* TargetAvatarActor=nullptr;
UPROPERTY()
AController* TargetController=nullptr;
UPROPERTY()
ACharacter* TargetCharacter=nullptr;
};
//...
public:
//在GameplayEffect被添加时的回调
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
private:
//从PostGameplayEffectExecute的Data中获取所有的属性,包括ContextHandle,Actor,Character等等
void SetEffectProperties(const FGameplayEffectModCallbackData& Data,FEffectProperties& Props);
//AuraAttributeSet.cpp
//...
void UAuraAttributeSet::SetEffectProperties(const struct FGameplayEffectModCallbackData& Data,FEffectProperties& Props)
{
Props.EffectContextHandle = Data.EffectSpec.GetContext();
Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
if(IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid()
&& Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
{
Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
//如果GetPlayerController为null,可能该Controller不是PlayerController
if(Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
{
if(const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
{
Props.SourceController = Pawn->GetController();
}
}
if(Props.SourceController)
{
Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
}
}
if(Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
}
}
void UAuraAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
FEffectProperties Props;
//在这之后可以使用Props来进行各种操作了
}
//...
CurveTable
在Effect的设置中,可以看到,除了使用固定值,还可以使用CurveTable
(曲线表)来去设置修改值
我们创建一个曲线表来试试
然后进入表格,点击三角形来添加点和数据
然后点击图表就可以看到整个曲线了
然后在PotionHeal中添加该CurveTable
我们发现实际修改值是我们的静态数字乘以CurveTable的当前值
但是,CurveTable中的键,在EffectActor中并不存在,所以我们需要在EffectActor中添加一些"药水等级",来让它当作CurveTable的键
在ApplyEffectToTarget方法中,在创建Spec的时候我们其实传递了一个level 1.f,所以当我们捡起药水时,它其实会将level = 1 来当作CurveTable的键
现在我们要添加一个 ActorLevel 而不是硬编码这个level
//AuraEffectActor.h
//...
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category="Applied Effects")
float ActorLevel = 1.f;
//...
//AuraEffectActor.cpp
//...ApplyEffectToTarget
//制作一个Spec
FGameplayEffectSpecHandle EffectSpecHandle =
TargetASC->MakeOutgoingSpec(GameplayEffectClass,ActorLevel,EffectContextHandle);
//...
在这里就可以设置ActorLevel了
而且,ActorLevel是float,也就意味着可以为小数,小数对应着的就是图表线上的点
我们可以添加多个曲线,然后让法力值也可以运用这个曲线