This commit is contained in:
2023-06-29 11:55:02 +08:00
commit 36e95249b1
1236 changed files with 464197 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
---
title: Exploring the Gameplay Ability System (GAS) with an Action RPG Unreal Fest 2022学习笔记
date: 2022-11-10 21:04:35
excerpt:
tags: GAS
rating: ⭐
---
## 前言
原视频:[Exploring the Gameplay Ability System (GAS) with an Action RPG Unreal Fest 2022](https://www.youtube.com/watch?v=tc542u36JR0&t)
## InitialSetup
首先需要考虑:
- 哪些`AttributeSets`需要创建
- 如何让角色获取`GrantAbility`
- 如何绑定按键与控制器输出事件
## CharacterSetup
- 继承并实现`IAbilitySystemInterface`
- 挂载ASC但一般还是挂载到PlayerState上。
- 在构造函数中创建默认子物体:
- AbilitySystemComponent
- AttributeSetObject
### PossessedBy
- AbilitySystemComponent->InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor)
- InitializeAttributes
- Listen for tags of interest(via UAsyncATaskGameplayTagAddedRemoved)
- Add start-up effects
- Bind to Ability activation failed callback
### BeginPlay
Listen to Health Attribute change
Bind to ASC->OnImmunity
## AttributeSets
- PrimaryAttributes——核心RPG状态绝大多数实体需要
- Health,MaxHealth,Shields,MaxShields,Stamina
- Core RPG Style values比如StrengthFortitude坚韧
- SecondaryAttributes——一定深度的RPG状态非所有实体需要
- Elemental resistances元素抵抗表,Elemental bonuses元素奖励,Current Elemental build-Ups当前元素构建方式
- TertiaryAttributes——Bounses主要用于玩家控制的角色
- Interaction speed bonuses,other specific bonuses and stats
- Weapon archetype specific Attribute武器相关属性
- Charge Percent充能百分比,Spread扩散,Charge Count充能次数
### 后续改进
- PrimaryAttributes
- 将Health等基本属性分割成自己的属性集
- 目前如果我们给一个UObject等设置了一个属性集来跟踪它们的Health但它们也会保存其他不必要的值。
- SecondaryAttributes
- 对这个设计很满意因为它都是与元素有关的状态以及buff/debuff属性。
- TertiaryAttributes
- 一些属性可以被分割合并到实体特化的属性集中。
- Weapon archetype specific attributes
- 一些int类型的Attribute可以使用Stackable GE来代替属性集只有float类型通过查询堆栈数目来获取数据。
## 演讲者团队增加的额外功能
- UGameplayAbility
- Input
- UInputAction InputAction(Enhanced Input System)
- Custom Ability Input ID Enum
- Booleans/Bitfields 开启一些核心功能 我们不想对所有Ability增加通过GameplayTags实现的功能
- Activate On Granted
- Activate On Input
- Can Activate Whilst Interacting
- Cannot Activate Whilst Stunned
- Cached Owning Character
- AsyncAbilityTasks
- Attribute Changed
- Cooldown Changed
- GameplayTag Added Removed
- Wait GE Class Added Removed
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111112940.png)
## 使用DataTable与C++来初始化属性集
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111114735.png)
或者就是实现一个结构体基于FAttributeSetIniDiscreteLevel。
## 创建自定义节点来传递GE计算结果与Tags
多次GE的积累计算结果。主要是通过AbilitySystemGlobals来实现
- GE_Damage
- Custom Calculation ClassC++ Damage Execution Calculation
- Conditional GEs
- GE_BuildUp_Stun
- Custom Calculation Class: C++ Build Up Calculation
- Conditional GE: GE_Stunned
- GE_BuildUp_Burn
- Custom Calculation Class: C++ Build Up Calculation
- Conditional GE: GE_Burned
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111140430.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111140516.png)
## 装备属性变化
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111141459.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111141539.png)
## AbilityTraitsTableExample
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111141647.png)
## 该团队AbilityEntitySystem设计
用来实现子弹变量、AOE、Cnnstruct、角色等带有ASC的实体。
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111142713.png)
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/20221111142509.png)

View File

@@ -0,0 +1,181 @@
## 前言
本人最近看了GameplayAbility的wiki与官方的ActionRPG案例大致对此有了一定了解。所以在此分享相关经验顺便作为学习笔记。
wiki采用了第三人称模板来进行讲解。讲解了几个主要类的概念、参数以及用法。
ActionRPG则是一个较为完整的案例它通过c++ 往几个GameplayAbility的基础类中添加了若干逻辑使得之更加适合于RPG项目。大部分改动都很不错甚至直接复制让它作为你的RPG模板都是没问题。它的主要逻辑以及表现都在于蓝图中(技能与效果)所以它也是一个c++与蓝图结合开发的好例子。(注意GameAbility无法完全通过蓝图开发)
所以接下来我也会适当讲解actionRPG的结构。<br>
注意:有关网络同步的我都会略过。
## 相关资料
wiki
https://wiki.unrealengine.com/index.php?title=GameplayAbilities_and_You#Common_Issues
GameplayAbility文档
https://docs.unrealengine.com/en-us/Gameplay/GameplayAbilitySystem
actionRPG案例文章
https://docs.unrealengine.com/en-US/Resources/SampleGames/ARPG/index.html
## 启用GameAbility插件并且在你的项目中添加该模块
1. 在编辑Edit->插件Plugins找到Gameplay Ability 并启用。
2. 在你的c++项目(ProjectName).Build.cs文件的PublicDependencyModuleNames变量中添加 "GameplayAbilities", "GameplayTags", "GameplayTasks"。
例如:
```
PublicDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks", "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });
```
### 注意事项
**在模块文件以及c++中包含GameplayBility前先需要确定是否在项目中启用GameplayAbility插件默认是不启用的。不然你的项目编译时会通过但运行时会显示无法加载游戏模块。**
## 建议
在模块h文件与cpp中定义这个模块的log标签这样你可以对log进行过滤显示。
```
//在h文件中
ACTIONRPG_API DECLARE_LOG_CATEGORY_EXTERN(LogActionRPG, Log, All);
```
```
//在cpp文件中
DEFINE_LOG_CATEGORY(LogActionRPG);
```
## 在角色类中挂载自定义的UGameplayAbilityComponent
actionRPG中定义了URPGAbilitySystemComponent作为挂载组件类它实现了以下函数
```
//通过Tag可以是多个tag来获取激活的技能
void GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<URPGGameplayAbility*>& ActiveAbilities);
//取得角色等级这个意义不大因为不是所有的rpg游戏都有等级这一说
int32 GetDefaultAbilityLevel() const;
//通过全局的AbilitySystem来获取对应Actor所绑定的组件指针
static URPGAbilitySystemComponent* GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent = false);
```
```
void URPGAbilitySystemComponent::GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<URPGGameplayAbility*>& ActiveAbilities)
{
TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
GetActivatableGameplayAbilitySpecsByAllMatchingTags(GameplayTagContainer, AbilitiesToActivate, false);
for (FGameplayAbilitySpec* Spec : AbilitiesToActivate)
{
TArray<UGameplayAbility*> AbilityInstances = Spec->GetAbilityInstances();
for (UGameplayAbility* ActiveAbility : AbilityInstances)
{
ActiveAbilities.Add(Cast<URPGGameplayAbility>(ActiveAbility));
}
}
}
URPGAbilitySystemComponent* URPGAbilitySystemComponent::GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent)
{
return Cast<URPGAbilitySystemComponent>(UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor, LookForComponent));
}
```
## 定义角色类
ARPGCharacterBase继承于ACharacter与接口类IAbilitySystemInterface。
actionRPG中定义了ARPGCharacterBase作为角色类的基础类。因为这个类会适用于主角、友善NPC、敌方NPC所有我们不应该在这个类中进行输入绑定、以及各种Camera、movementComponent等非共用性组件的绑定与设置。
接下来我会讲解我阅读代码的顺序具体代码可以就请读者参考actionRPG。
首先
```
//GameplayAbilityComponent指针
class URPGAbilitySystemComponent* AbilitySystemComponent;
//因为继承IAbilitySystemInterface所以需要实现这个接口
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
//用以判断这个Actor是否初始化了GameplayAbilityComponent
bool bAbilitiesInitialized;
//用于存储GameplayAbility角色技能的容器并且会在游戏开始时向AbilitySystemComponent进行注册
//读者可以在看了我下一篇文章再添加
TArray<TSubclassOf<URPGGameplayAbility>> GameplayAbilities;
```
除此之外actionRPG还重写了PossessedBy以此来实现向AbilitySystemComponent注册Ability的功能。Wiki上的教程选择在Beginplay事件中进行注册之后再PossessedBy中进行刷新判断技能TArray是否有变化如果有就进行相应得修改。在本教程中我选择actionRPG的方案。
```
ARPGCharacterBase::ARPGCharacterBase()
{
AbilitySystemComponent = CreateDefaultSubobject<URPGAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
bAbilitiesInitialized = false;
}
void ARPGCharacterBase::AddStartupGameplayAbilities()
{
if (!bAbilitiesInitialized)
{
for (TSubclassOf<URPGGameplayAbility>& StartupAbility : GameplayAbilities)
{
AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(StartupAbility,1,INDEX_NONE,this));
}
bAbilitiesInitialized = true;
UE_LOG(LogActionRPG, Warning, TEXT("%s"), *FString("All Ablities registered"));
}
}
//RemoveStartupGameplayAbilities在actionRPG中的SetCharacterLevel函数中被调用
//但这个函数和GameplayAbility框架关系不大我就省略了
void ARPGCharacterBase::RemoveStartupGameplayAbilities()
{
if (bAbilitiesInitialized)
{
TArray<FGameplayAbilitySpecHandle> AbilitiesToRemove;
for (const FGameplayAbilitySpec& Spec : AbilitySystemComponent->GetActivatableAbilities())
{
if ((Spec.SourceObject == this) && GameplayAbilities.Contains(Spec.Ability->GetClass()))
{
AbilitiesToRemove.Add(Spec.Handle);
}
}
for (int32 i = 0; i < AbilitiesToRemove.Num(); i++)
{
AbilitySystemComponent->ClearAbility(AbilitiesToRemove[i]);
}
bAbilitiesInitialized = false;
}
}
void ARPGCharacterBase::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (AbilitySystemComponent)
{
AddStartupGameplayAbilities();
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
}
```
## UAbilitySystemComponent中的委托
```
/** Used to register callbacks to ability-key input */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAbilityAbilityKey, /*UGameplayAbility*, Ability, */int32, InputID);
/** Used to register callbacks to confirm/cancel input */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FAbilityConfirmOrCancel);
/** Delegate for when an effect is applied */
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnGameplayEffectAppliedDelegate, UAbilitySystemComponent*, const FGameplayEffectSpec&, FActiveGameplayEffectHandle);
/** Called on server whenever a GE is applied to self. This includes instant and duration based GEs. */
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToSelf;
/** Called on server whenever a GE is applied to someone else. This includes instant and duration based GEs. */
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToTarget;
/** Called on both client and server whenever a duraton based GE is added (E.g., instant GEs do not trigger this). */
FOnGameplayEffectAppliedDelegate OnActiveGameplayEffectAddedDelegateToSelf;
/** Called on server whenever a periodic GE executes on self */
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnSelf;
/** Called on server whenever a periodic GE executes on target */
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnTarget;
/** Register for when an attribute value changes */
FOnGameplayAttributeValueChange& GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute);
/** Callback anytime an ability is ended */
FAbilityEnded AbilityEndedCallbacks;
/** Called with a failure reason when an ability fails to execute */
FAbilityFailedDelegate AbilityFailedCallbacks;
```

View File

@@ -0,0 +1,98 @@
## 前言
之前使用GameplayTasks的事件处理会出现延迟问题。具体体现在如果使用机制来添加BlockAbilityTag在狂按技能的情况下会出现Tag不能正常生成导致可以不停ActivateAbility的问题。
## 问题成因
1. 问题的原因就是使用GameplayTasks不能即时添加GE会有一定延迟。
2. 清除GameplayEffect方式有问题应该使用只清除该技能所附加的GE。
## 解决思路
在解决清除GE问题的情况下
1. GASDocument中的解决方法使用AbilityTag作为BlockAbilityTag之后通过AnimNotify发送EndAbility事件Tag提前结束Ability。不会结束掉Montage播放
2. 在AnimNotifyBegin函数中直接添加GE根据TotalDuration设置持续时间。但如果Montage突然被中断就很难操作了而且这样会与Ability耦合
3. 增加公共CD测试过效果不佳
## PlayMontage修改
1. 传递事件使用专门的Event.Montage.xxxx作为事件标签
2. 重写AnimNotify的GetNotifyName函数使用标签名作为AnimNotify的显示名称。
```
void UGDGA_FireGun::EventReceived(FGameplayTag EventTag, FGameplayEventData EventData)
{
// Montage told us to end the ability before the montage finished playing.
// Montage was set to continue playing animation even after ability ends so this is okay.
if (EventTag == FGameplayTag::RequestGameplayTag(FName("Event.Montage.EndAbility")))
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
return;
}
// Only spawn projectiles on the Server.
// Predicting projectiles is an advanced topic not covered in this example.
if (GetOwningActorFromActorInfo()->GetLocalRole() == ROLE_Authority && EventTag == FGameplayTag::RequestGameplayTag(FName("Event.Montage.SpawnProjectile")))
{
AGDHeroCharacter* Hero = Cast<AGDHeroCharacter>(GetAvatarActorFromActorInfo());
if (!Hero)
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}
FVector Start = Hero->GetGunComponent()->GetSocketLocation(FName("Muzzle"));
FVector End = Hero->GetCameraBoom()->GetComponentLocation() + Hero->GetFollowCamera()->GetForwardVector() * Range;
FRotator Rotation = UKismetMathLibrary::FindLookAtRotation(Start, End);
FGameplayEffectSpecHandle DamageEffectSpecHandle = MakeOutgoingGameplayEffectSpec(DamageGameplayEffect, GetAbilityLevel());
// Pass the damage to the Damage Execution Calculation through a SetByCaller value on the GameplayEffectSpec
DamageEffectSpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), Damage);
FTransform MuzzleTransform = Hero->GetGunComponent()->GetSocketTransform(FName("Muzzle"));
MuzzleTransform.SetRotation(Rotation.Quaternion());
MuzzleTransform.SetScale3D(FVector(1.0f));
FActorSpawnParameters SpawnParameters;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AGDProjectile* Projectile = GetWorld()->SpawnActorDeferred<AGDProjectile>(ProjectileClass, MuzzleTransform, GetOwningActorFromActorInfo(),
Hero, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
Projectile->DamageEffectSpecHandle = DamageEffectSpecHandle;
Projectile->Range = Range;
Projectile->FinishSpawning(MuzzleTransform);
}
}
```
```
/** Apply a gameplay effect to the owner of this ability */
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName="ApplyGameplayEffectToOwner", meta=(ScriptName="ApplyGameplayEffectToOwner"))
FActiveGameplayEffectHandle BP_ApplyGameplayEffectToOwner(TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel = 1, int32 Stacks = 1);
/** Non blueprintcallable, safe to call on CDO/NonInstance abilities */
FActiveGameplayEffectHandle ApplyGameplayEffectToOwner(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const UGameplayEffect* GameplayEffect, float GameplayEffectLevel, int32 Stacks = 1) const;
/** Apply a previously created gameplay effect spec to the owner of this ability */
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName = "ApplyGameplayEffectSpecToOwner", meta=(ScriptName = "ApplyGameplayEffectSpecToOwner"))
FActiveGameplayEffectHandle K2_ApplyGameplayEffectSpecToOwner(const FGameplayEffectSpecHandle EffectSpecHandle);
FActiveGameplayEffectHandle ApplyGameplayEffectSpecToOwner(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle) const;
// -------------------------------------
// Apply Gameplay effects to Target
// -------------------------------------
/** Apply a gameplay effect to a Target */
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName = "ApplyGameplayEffectToTarget", meta=(ScriptName = "ApplyGameplayEffectToTarget"))
TArray<FActiveGameplayEffectHandle> BP_ApplyGameplayEffectToTarget(FGameplayAbilityTargetDataHandle TargetData, TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel = 1, int32 Stacks = 1);
/** Non blueprintcallable, safe to call on CDO/NonInstance abilities */
TArray<FActiveGameplayEffectHandle> ApplyGameplayEffectToTarget(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayAbilityTargetDataHandle& Target, TSubclassOf<UGameplayEffect> GameplayEffectClass, float GameplayEffectLevel, int32 Stacks = 1) const;
/** Apply a previously created gameplay effect spec to a target */
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName = "ApplyGameplayEffectSpecToTarget", meta=(ScriptName = "ApplyGameplayEffectSpecToTarget"))
TArray<FActiveGameplayEffectHandle> K2_ApplyGameplayEffectSpecToTarget(const FGameplayEffectSpecHandle EffectSpecHandle, FGameplayAbilityTargetDataHandle TargetData);
TArray<FActiveGameplayEffectHandle> ApplyGameplayEffectSpecToTarget(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle, const FGameplayAbilityTargetDataHandle& TargetData) const;
```

View File

@@ -0,0 +1,196 @@
## 前言
TargetActor是GAS用于获取场景中物体、空间、位移等数据的机制同时也可以用于制作可视化debug工具。,所以非常有必要掌握它。
一般流程为使用WaitTargetData_AbilityTask生成TargetActor之后通过TargetActor的内部函数或者射线获取场景信息最后通过委托传递携带这些信息构建的FGameplayAbilityTargetDataHandle。
本文部分描述摘自GASDocumentation_Chinese翻译的还不错也请大家给此项目点赞。
## TargetData
TargetData也就是FGameplayAbilityTargetData是用于通过网络传输定位数据的通用结构体。它主要用于存储目标数据(一般是TArray<TWeakObjectPtr<AActor> >)、FHitResult。当然可以传递一些自定义数据这个可以参考源码中的FGameplayAbilityTargetData_LocationInfo。
TargetData一般由TargetActor或者手动创建(较少), 供AbilityTask或者GameplayEffect通过EffectContext使用. 因为其位于EffectContext中,所以Execution,MMC,GameplayCue和AttributeSet的后处理函数都可以访问该TargetData.
GAS不会直接传递数据而是会借助FGameplayAbilityTargetDataHandle来进行传递。具体过程是
1. 创建TargetData并且填充数据。
2. 创建FGameplayAbilityTargetDataHandle对象也可以使用带参的构造函数直接构建并且调用Add()添加上面创建的TargetData。
3. 进行传递。
源代码中GameplayAbilityTargetTypes.h中实现了以下TargetData
- FGameplayAbilityTargetData_LocationInfo
- FGameplayAbilityTargetData_ActorArray
- FGameplayAbilityTargetData_SingleTargetHit
基本上是够用了如果需要创建新的TargetData类型就需要视携带的数据类型实现以下虚函数
```c++
virtual TArray<TWeakObjectPtr<AActor>> GetActors() const;
virtual bool SetActors(TArray<TWeakObjectPtr<AActor>> NewActorArray);
virtual bool HasHitResult() const;
virtual const FHitResult* GetHitResult();
virtual void ReplaceHitWith(AActor* NewHitActor, const FHitResult* NewHitResult)
virtual bool HasOrigin() const;
virtual FTransform GetOrigin() const;
virtual bool HasEndPoint() const;
virtual FVector GetEndPoint() const;
virtual FTransform GetEndPointTransform() const;
/** See notes on delegate definition FOnTargetActorSwapped */
virtual bool ShouldCheckForTargetActorSwap() const;
```
debug相关虚函数
```c++
/** Returns the serialization data, must always be overridden */
virtual UScriptStruct* GetScriptStruct() const
{
return FGameplayAbilityTargetData::StaticStruct();
}
/** Returns a debug string representation */
virtual FString ToString() const;
```
源代码在实现完类型后,还有附带下面这一段代码,看注释应该和网络同步序列化有关,反正依瓢画葫芦复制+替换类型名称即可。
```c++
template<>
struct TStructOpsTypeTraits<FGameplayAbilityTargetData_ActorArray> : public TStructOpsTypeTraitsBase2<FGameplayAbilityTargetData_ActorArray>
{
enum
{
WithNetSerializer = true // For now this is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work
};
};
```
## TargetActor
你可以把TargetActor理解为一个场景信息探测器用来获取场景中数据以及进行可视化Debug。一般都是在Ability中通过AbilityTask_WaitTargetData来生成TargetActor(WaitTargetDataUsingActor用来监听已有的TargetActor)之后再通过ValidData委托接收TargetData。
## GameplayAbilityWorldReticles
当使用non-Instant TargetActor定位时TargetActor可以选择使用ReticleActor(GameplayAbilityWorldReticles)标注当前目标。
默认情况下Reticle只会显示在TargetActor的当前有效Target上,如果想要让其显示在其他目标上就需要自定义TargetActor手动管理确定与取消事件让目标持久化。
ReticleActor可以通过FWorldReticleParameters进行初始化(在TargetActor设置FWorldReticleParameters变量)但FWorldReticleParameters只有一个AOEScale变量很明显完全不够用。所以你可以通过自定义ActorTarget与参数结构来改进这个功能。
Reticle默认是不可同步的,但是如果你想向其他玩家展示本地玩家正在定位的目标,那么它也可以被设置为可同步的。
ReticleActor还带有一些面向蓝图的BlueprintImplementableEvents
```c++
/** Called whenever bIsTargetValid changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnValidTargetChanged(bool bNewValue);
/** Called whenever bIsTargetAnActor changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnTargetingAnActor(bool bNewValue);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnParametersInitialized();
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamFloat(FName ParamName, float value);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamVector(FName ParamName, FVector value);
```
GAS默认的UAbilityTask_WaitTargetData节点会在FinalizeTargetActor()中调用AGameplayAbilityTargetActor_Trace::StartTargeting(),进行ReticleActor的Spawn。
GASShooter中的逻辑为
1. GA_RocketLauncherSecondaryAAset在Configure节点中指定为ReticleClass为BP_SingleTargetReticle。
2. 在UGSAT_WaitTargetDataUsingActor::FinalizeTargetActor()中调用StartTargeting()
3. 最后会在AGSGATA_Trace(GASShooter中定义的TargetActor的基类)的StartTargeting()中进行Spawn。
GASShooter中的实现了BP_SingleTargetReticle大致实现为
1. 在根组件下连接一个WidgetComponent
2. 使用Widget3DPassThrough_Masked_OneSided材质并且绑定一个UI_TargetReticle UMGAsset
## GameplayEffectContext
可以认为这是一个传递数据用的结构体。GameplayEffectContext结构体存有关于GameplayEffectSpec创建者(Instigator)和TargetData的信息,可以向ModifierMagnitudeCalculation/GameplayEffectExecutionCalculation, AttributeSet和GameplayCue之间传递任意数据.
其他案例:
https://www.thegames.dev/?p=62
使用方法:
1. 继承FGameplayEffectContext。
2. 重写FGameplayEffectContext::GetScriptStruct()。
3. 重写FGameplayEffectContext::Duplicate()。
4. 如果新数据需要同步的话, 重写FGameplayEffectContext::NetSerialize()。
5. 对子结构体实现TStructOpsTypeTraits, 就像父结构体FGameplayEffectContext有的那样.
6. 在AbilitySystemGlobals类中重写AllocGameplayEffectContext()以返回一个新的子结构体对象。(AbilitySystemGlobals还需要注册请看下节)
```c++
FGameplayEffectContext* UGSAbilitySystemGlobals::AllocGameplayEffectContext() const
{
return new FGSGameplayEffectContext();
}
```
在ExecutionCalculation中,你可以通过FGameplayEffectCustomExecutionParameters获取FGameplayEffectContext
```c++
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(Spec.GetContext().Get());
```
在GameplayEffectSpec中获取EffectContext:
```c++
FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(MutableSpec->GetContext().Get());
```
在ExecutionCalculation中修改GameplayEffectSpec时要小心.参看GetOwningSpecForPreExecuteMod()的注释.
```c++
/** Non const access. Be careful with this, especially when modifying a spec after attribute capture. */
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;
```
PS.GASShooter实现了带有FGameplayAbilityTargetDataHandle变量的FGSGameplayEffectContext以此来实现在GameplayCue中访问TargetData, 特别是对于霰弹枪来说十分有用, 因为它可以击打多个敌人并且产生效果。
## InitGlobalData()
>从UE 4.24开始, 必须调用UAbilitySystemGlobals::InitGlobalData()来使用TargetData, 否则你会遇到关于ScriptStructCache的错误, 并且客户端会从服务端断开连接, 该函数只需要在项目中调用一次. Fortnite从AssetManager类的起始加载函数中调用该函数, Paragon是从UEngine::Init()中调用的. 我发现将其放到UEngineSubsystem::Initialize()是个好位置, 这也是样例项目中使用的. 我觉得你应该复制这段模板代码到你自己的项目中以避免出现TargetData的使用问题.
>如果你在使用AbilitySystemGlobals GlobalAttributeSetDefaultsTableNames时发生崩溃, 可能之后需要像Fortnite一样在AssetManager或GameInstance中调用UAbilitySystemGlobals::InitGlobalData()而不是在UEngineSubsystem::Initialize()中. 该崩溃可能是由于Subsystem的加载顺序引发的, GlobalAttributeDefaultsTables需要加载EditorSubsystem来绑定UAbilitySystemGlobals::InitGlobalData()中的委托.
上面说的意思是初始化需要通过Subsystem与其节点来进行初始化这样就不需要通过继承来实现GASDocument推荐在UEngineSubsystem(UEngine阶段)初始化时执行。下面是自定义AbilitySystemGlobals的注册方式。
```c++
UAbilitySystemGlobals::Get().InitGlobalData();
```
可以参考UGSEngineSubsystem。
>AbilitySystemGlobals类保存有关GAS的全局信息. 大多数变量可以在DefaultGame.ini中设置. 一般你不需要和该类互动, 但是应该知道它的存在. 如果你需要继承像GameplayCueManager或GameplayEffectContext这样的对象, 就必须通过AbilitySystemGlobals来做.
>想要继承AbilitySystemGlobals, 需要在DefaultGame.ini中设置类名:
```ini
[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"
```
## RPGTargetType
这是GameplayEffectContainer提供的一种方便的产生TargetData方法。但因为使用CDO(Class Default Object)运行所以不支持用户输入与取消等功能功能上也不如TargetActor多。在官方的ActionRPG可以看到演示。这些Target类型定义于URPGTargetType你可以根据获取Target的方式进行拓展调用TargetType获取目标的逻辑位于MakeEffectContainerSpecFromContainer
```c++
if (Container.TargetType.Get())
{
TArray<FHitResult> HitResults;
TArray<AActor *> TargetActors;
const URPGTargetType *TargetTypeCDO = Container.TargetType.GetDefaultObject();
AActor *AvatarActor = GetAvatarActorFromActorInfo();
TargetTypeCDO->GetTargets(OwningCharacter, AvatarActor, EventData, HitResults, TargetActors);
ReturnSpec.AddTargets(HitResults, TargetActors);
}
```
ActionRPG在c++中实现了UseOwner与UseEventData类型在蓝图中实现了SphereTrace与LineTrace。
### GASShooter中的用法
GASShooter在以下Asset中使用了TargetData与TargetActor
- GA_RiflePrimaryInstant
- GA_RocketLauncherPrimaryInstant
- GA_RocketLauncherPrimaryInstant
GASShooter中实现了AGSGATA_LineTrace与AGSGATA_SphereTrace等AGameplayAbilityTargetActor拥有共同的父类AGSGATA_Trace
步骤:
在AbilityActivate中
1. 类内指定AGSGATA_LineTrace对象作为变量。
2. 调用AGSGATA_LineTrace对象的ResetSpread(),初始化参数。
3. 调用UGSAT_ServerWaitForClientTargetData并将ValidData委托绑定HandleTargetData。
4. 之后执行客户端逻辑。
5. 调用AGSGATA_LineTrace对象的Configure(),来设置具体参数。
6. 将AGSGATA_LineTrace对象传入UGSAT_WaitTargetDataUsingActor并将ValidData委托绑定HandleTargetData。

View File

@@ -0,0 +1,187 @@
## GAS中的传递数据、解耦与其他技巧
本文大部分内容来着GASDocumentation与GASDocumentation_Chinese因为感觉很重要所以在此简单归纳一下。
https://github.com/tranek/GASDocumentation
https://github.com/BillEliot/GASDocumentation_Chinese
<!--more-->
## 响应GameplayTags的变化
这个没什么好说的直接上GASDocumentation_Chinese中的解释
>ASC提供了一个委托(Delegate)用于在GameplayTag添加或移除时触发, 其中EGameplayTagEventType参数可以明确是该GameplayTag添加/移除还是其TagMapCount发生变化时触发.
```
AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);
```
回调函数拥有变化的GameplayTag参数和新的TagCount参数.
```
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
```
## 向GameplayAbility传递数据
一共有4中方法
- 通过设置Ability中的Trigger之后再在ActivateAbilityFromEvent事件中进行处理。
- 使用WaitGameplayEvent之类的AbilityTask对接收到的事件进行处理。
- 使用TargetData。
- 存储数据在OwnerActor或者AvatarActor中之后再通过ActorInfo中的Owner或者Avatar来获取数据。
前两种都可以通过往Event中添加Payload来传递额外信息。Payload可以使用自定义的UObject。TargetData涉及到FGameplayAbilityTargetData、AGameplayAbilityTargetActor等东西下一篇文章会说。
## 通过Event/Tag来激活GameplayAbility
Ability Trigger是一种不错的解耦方式,大致的使用步骤:
1. 在Ability中的设置Trigger中的Ability Trigger在Class Defaults中
2. 实现ActivateAbilityFromEvent事件通过Event中Payload获取自定义UObject中的信息。
3. Event的触发方式通过调用SendGameplayEventToActor()向对应拥有ASC的Actor/Character发送GameplayEvent来触发对应Ability。
4. Tag的触发方式通过GamplayEffect来添加对应Tag来触发对应Ability。AddLooseGameplayTag()我没试过,但估计也是可以的)
使用Tag的好处在于Owned Tag Present,即让Ability与Tag保持一样的生命周期,不过很可惜Tag的触发方式并不支持Ability的AbilityTags。注册过程位于ASC的OnGiveAbility()中主要是通过绑定MonitoredTagChanged事件来实现触发。
PS.本人使用这个功能实现了FightingStateHandle即进入战斗从背上拔出武器离开战斗则将武器放回背上。
### GASShooter中的另类用法
这里我还想说一下GASShooter中使用Ability Trigger机制的另类用法Interact项目中宝箱与按钮门都使用了它。
这个系统主要由GA_InteractPassive、GA_InteractActive以及IGSInteractable接口。
#### IGSInteractable
首先所有可以互动的物体都需要继承并且按照对应需求实现对应的几个接口函数:
- IsAvailableForInteraction
- GetInteractionDuration
- GetPreInteractSyncType
- GetPostInteractSyncType
- PreInteract
- PostInteract
- CancelInteraction
- RegisterInteracter
- UnregisterInteracter
- InteractableCancelInteraction
#### GA_InteractPassive与GA_InteractActive
GA_InteractPassive与GA_InteractActive会在游戏开始时在BP_HeroCharacter中注册。
GA_InteractPassive是一个GrantAbility它会在游戏开始就被激活。它会执行UGSAT_WaitInteractableTarget不断地进行射线判断。如果有可用的Interactable目标出现就会进行一些系列的判断在绑定的InputAction按下后最终给角色类ASC发送GameplayTask中设置的GameplayEventPayload中带有Interactable目标的TargetData
因为GA_InteractActive中设置了Ability Trigger所以它会接收到GameplayEvent并且处理。Interactable触发后的逻辑都在这里处理通过TargetData调用IGSInteractable接口函数
## GameplayEffect的GrantAbilities
在GameplayEffect也有类似Ability Trigger就是GrantAbilities,并且有多种移除Ability方式以供选择。但GameplayEffect需要符合一下要求
- StackingType需要为AggregateBySource/AggregateByTarget
- DurationType需要为HasDuration/Infinite
- 设置的Ability必须没有注册过(GiveAbility)
同时GameplayAbility类还需要实现OnAvatarSet()虚函数该函数主要用于处理GrantAbility
```
UCLASS()
class RPGGAMEPLAYABILITY_API URPGGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
{
virtual void OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec) override;
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "RPGGameplayAbility")
bool ActivateAbilityOnGranted;
};
```
```
void URPGGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec)
{
if (ActivateAbilityOnGranted)
{
ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, true);
}
}
```
其中ActivateAbilityOnGranted用于判断这个Ability是不是GrantAbility不然所有所有的Ability都会调用这个函数。
## 运行时创建动态GameplayEffect
```
void UGameplayAbilityRuntimeGE::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
}
// Create the GE at runtime.
UGameplayEffect* GameplayEffect = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeInstantGE"));
GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // Only instant works with runtime GE.
const int32 Idx = GameplayEffect->Modifiers.Num();
GameplayEffect->Modifiers.SetNum(Idx + 1);
FGameplayModifierInfo& ModifierInfo = GameplayEffect->Modifiers[Idx];
ModifierInfo.Attribute.SetUProperty(UMyAttributeSet::GetMyModifiedAttribute());
ModifierInfo.ModifierMagnitude = FScalableFloat(42.f);
ModifierInfo.ModifierOp = EGameplayModOp::Override;
// Apply the GE.
FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // "new", since lifetime is managed by a shared ptr within the handle
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec));
}
EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}
```
## 延长GE的持续时间
在GASDocument的4.5.16 中介绍了这个方法,直接上代码了:
```
bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
{
if (!Handle.IsValid())
{
return false;
}
const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
if (!ActiveGameplayEffect)
{
return false;
}
FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
if (NewDuration > 0)
{
AGE->Spec.Duration = NewDuration;
}
else
{
AGE->Spec.Duration = 0.01f;
}
AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
ActiveGameplayEffects.MarkItemDirty(*AGE);
ActiveGameplayEffects.CheckDuration(Handle);
AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
OnGameplayEffectDurationChange(*AGE);
return true;
}
```
PS.本人使用使用这个技巧来刷新战斗状态的持续时间。
## 其他技巧
### 添加给ACS附加标签不通过GE与GA
这个方法不适合联机,只在本地起效果。可以作为一些用于本地效果控制。
```
FGameplayTag DeadTag = FGameplayTag::RequestGameplayTag(FName("Status.Dead"));
AbilitySystemComponent->AddLooseGameplayTag(DeadTag);
```
### 动态修改Ability等级
- 移除旧Ability之后再添加新Ability
- 增加GameplayAbilitySpec的等级在服务端上找到GameplayAbilitySpec, 增加它的等级, 并将其标记为Dirty以同步到所属(Owning)客户端。
### AbilityInputID绑定
GASDocument的项目定义了EGSAbilityInputID枚举用于定义所有Ability的输入绑定名字需要与InputAction一一对应并且在UGSGameplayAbility中设置AbilityInputID变量用于输入绑定。
```
for (TSubclassOf<UGSGameplayAbility>& StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
}
```
这里还通过AbilityLevel来传递AbilityID。
PS.但本人感觉这个方法在一些动作游戏中这个方法可能会不太合适。

View File

@@ -0,0 +1,116 @@
## DataAsset与UObject
数据资产的巨大优势在于,你可以将它们设置为自动在引擎中注册,所以每次你添加一个新的数据资产时,引擎都会自动将其添加到一个列表中。因此,如果你的武器是数据资产,当你在游戏中添加一个新的武器时,它就会自动添加到一个你可以搜索的列表中,所以你不需要每次添加一个新的武器时在你的代码中手动添加绑定。
如果不需要这种类型的东西,把它变成数据资产就没有什么好处。
## 附魔
附魔分为临时附魔与永久附魔。临时附魔以Buffer的方式存在其Handle存储在角色类中。永久附魔应用后的Handle则存储在所依附的物品类武器或者身上的其他装备用于解除装备后去除GE。
### 存在问题
1. 如果武器存在属性,附魔也存在属性,那伤害该如何计算?
## 宝石(镶嵌物)
镶嵌在有限的武器槽中,增加角色属性或者改变武器(攻击)属性。
### 实现方式
在创建新的 Inlay 物品类里面存储GameplayEffect用于调整角色属性与武器攻击属性。应用后的Handle存储在所依附的物品类武器或者身上的其他装备用于解除装备后去除GE。
武器的镶嵌物与附魔的初始信息存储在DataAsset中而玩家runtime与存档信息则通过实例化的URPGWeaponItem来存储。WeaponActor保存对应的GEHandle数据全都是临时性的实现对应的方法。
## GASDocument的武器数值实现
- 在物品上使用原始数值变量(推荐)
- 在物品类上使用独立的AttributeSet
- 在物品类上使用独立的ASC
具体可以参考GASShooter项目。不使用AttributeSet直接将数值存在在武器类或者GameplayABility中内。
### 运行时添加与移除属性级
```
//On weapon add to inventory:
AbilitySystemComponent->SpawnedAttributes.AddUnique(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
//On weapon remove from inventory:
AbilitySystemComponent->SpawnedAttributes.Remove(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
```
### 各个方案的好处与坏处
使用原始数值变量
好处:
- 避免使用限制AttributeSets见下文
限制:
- 无法使用现有GameplayEffect工作流程Cost GEs用于弹药等
- 需要工作来覆盖关键功能UGameplayAbility以检查和应用弹药成本对枪支的浮标
使用独立的AttributeSet
好处:
- 可以使用现有GameplayAbility和GameplayEffect工作流程Cost GEs用于弹药等
- 设置非常小的项目集很简单
限制:
-您必须AttributeSet为每种武器类型创建一个新类。ASCs只能在功能上拥有一个AttributeSet类的实例因为更改为在数组中Attribute查找其AttributeSet类的第一个实例ASCs SpawnedAttributes。同一AttributeSet类的其他实例将被忽略。
- 由于之前AttributeSet每个AttributeSet类一个实例的原因您只能在玩家的库存中拥有每种类型的武器之一。
- 删除 anAttributeSet是危险的。在 GASShooter 中,如果玩家从火箭中自杀,玩家会立即从他的物品栏中移除火箭发射器(包括 中AttributeSet的ASC。当服务器复制火箭发射器的弹药Attribute改变时AttributeSet客户端上不再存在ASC游戏崩溃。
## 构造GameplayEffect并且修改参数
DamageEffectSpecHandle存储着FGameplayEffectSpec的智能指针。通过MakeOutgoingGameplayEffectSpec创建并且修改参数之后再通过ApplyGameplayEffectSpecToTarget应用到指定目标。
```
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TSubclassOf<UGameplayEffect> DamageGameplayEffect;
```
```
FGameplayEffectSpecHandle DamageEffectSpecHandle = MakeOutgoingGameplayEffectSpec(DamageGameplayEffect, GetAbilityLevel());
// Pass the damage to the Damage Execution Calculation through a SetByCaller value on the GameplayEffectSpec
DamageEffectSpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), Damage);
```
### 动态创建GE并且应用
```
// Create a dynamic instant Gameplay Effect to give the bounties
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Bounty")));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;
int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);
FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
InfoXP.ModifierOp = EGameplayModOp::Additive;
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();
FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
InfoGold.ModifierOp = EGameplayModOp::Additive;
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();
Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());
```
## 持续刷新GE以延长持续时间
4.5.16 修改已激活GameplayEffect的持续时间
## AGameplayAbilityTargetActor
4.11.1 Target Data
4.11.2 Target Actor 用于获取场景中的信息,或者在场景中生成一些东西。
## Input绑定
如果你的ASC位于Character, 那么就在SetupPlayerInputComponent()中包含用于绑定到ASC的函数.
## GASDocument上常用的Abilty和Effect
1. 眩晕(Stun)
2. 奔跑(Sprint)
3. 瞄准(Aim Down Sight)
4. 生命偷取(Lifesteal)
5. 在客户端和服务端中生成随机数
6. 暴击(Critical Hits)
7. 非堆栈GameplayEffect, 但是只有其最高级(Greatest Magnitude)才能实际影响Target
8. 游戏暂停时生成TargetData
9. 按钮交互系统(Button Interaction System)

View File

@@ -0,0 +1,52 @@
# 前言
之前一直都没有研究过GAS如何debug所以大致翻译了相关内容
>https://github.com/tranek/GASDocumentation#debugging
中文翻译
>https://blog.csdn.net/pirate310/article/details/106311256
# C++ Debug Tip
在非DebugGame Editor模式下模式下Ue4会对函数进行优化这会对debug产生一定阻碍。解决方法是
1. 在VisualStudio中将解决方案设置SolutionConfigurations改成DebugGame Editor之后再进行调试。
2. 对想要进行Debug的函数增加禁用所有优化的宏。
使用方法如下:
```
PRAGMA_DISABLE_OPTIMIZATION_ACTUAL
void MyClass::MyFunction(int32 MyIntParameter)
{
// My code
}
PRAGMA_ENABLE_OPTIMIZATION_ACTUAL
```
如果在插件中使用则需要对插件执行rebuild以进行重新构建。最后记得在调试完成后将宏删除。
# 自带的Debug工具
## showdebug abilitysystem
**显示方法**:按“`”键输入命令:showdebug abilitysystem。这个界面总共三页内容分别显示Attributes、Effects、Abilities。通过**AbilitySystem.Debug.NextCategory**命令进行翻页。
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/showdebugpage1.png)
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/showdebugpage2.png)
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/showdebugpage3.png)
PS.PageUp与PageDown可以切换目标。
## GameplayDebugger
![image](https://cdn.jsdelivr.net/gh/tranek/GASDocumentation@master/Images/gameplaydebugger.png)
在进入关卡后,按下“’”键就可以开启这个工具。(就是屏幕上方出现的工具栏)
这个功能感觉是用来调试非当前操作角色的,选中的角色上会出现一个红色小恶魔图标(如图所示),你可以小键盘的数字键来开启对应的功能。
可以通过Tab键切换成飞行模式来选中需要调试角色。
# GameEffect增加/减少 委托绑定
```
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
```
```
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);
```

View File

@@ -0,0 +1,18 @@
## FScopedPredictionWindow、ServerSetReplicatedEvent、ConsumeGenericReplicatedEvent
```
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent, IsPredictingClient());
//Ability是否具有本地预测能力
if (IsPredictingClient())
{
// Tell the server about this
AbilitySystemComponent->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey(), AbilitySystemComponent->ScopedPredictionKey);
}
else
{
AbilitySystemComponent->ConsumeGenericReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey());
}
```
## 预测
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/GAS%E9%A2%84%E6%B5%8B.jpg)

View File

@@ -0,0 +1,247 @@
## 前言
大多数人应该是会通过学习UE4的ActionRPG项目来入门GAS框架的。但在最近自己做Demo的过程中才发现里面的部分不适用网络联机。比如ActionRPG将Character与Controller的指针存在GameMode因为Server与Client只有一个GameMode的关系就不符合多人联机游戏的规则。所以在经过实践之后这里我分享一些要点仅供抛砖引玉。这里我再推荐各位读者去看一下GASDocument以及附带的工程它展现了正确的设计思路。
## Ability中的网络同步节点
WaitNetSync
## GASDocument中的处理方式
将AbilitySystemComponent以及AttributeSet放在PlayerState中之后再Character类中的PossesseBy()事件(ServerOnly)与OnRep_PlayerState()事件(Client)给角色类的ASC弱智能指针与AttributeSet弱智能指针赋值。
```
AGDPlayerState::AGDPlayerState()
{
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
// Mixed mode means we only are replicated the GEs to ourself, not the GEs to simulated proxies. If another GDPlayerState (Hero) receives a GE,
// we won't be told about it by the Server. Attributes, GameplayTags, and GameplayCues will still replicate to us.
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
AttributeSetBase = CreateDefaultSubobject<UGDAttributeSetBase>(TEXT("AttributeSetBase"));
NetUpdateFrequency = 100.0f;
}
// TWeakObjectPtr<class UGDAbilitySystemComponent> AbilitySystemComponent;
// TWeakObjectPtr<class UGDAttributeSetBase> AttributeSetBase;
// Server only
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
AttributeSetBase = PS->GetAttributeSetBase();
}
//...
}
// Client only
void AGDHeroCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
// Set the ASC for clients. Server does this in PossessedBy.
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
AttributeSetBase = PS->GetAttributeSetBase();
}
// ...
}
```
## 预测
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/GAS%E9%A2%84%E6%B5%8B.jpg)
## PlayerState
PlayerState只在开发多人游戏时有用在设计上负责管理角色的变量(PlayerId、UniqueId、SessionName与自己定义的变量)。
### 指定与初始化
在GameMode中设置实现的PlayerState类之后在AController::InitPlayerState()中Spawn。
### 访问方法
- Pawn->PlayerState
- Controller->PlayerState(一般用这个)
另外还可以通过GameState->PlayerArray获取所有有效的GameState实例。
### PlayerState
PlayerState在切换地图时会默认销毁为了解决这个问题可以通过实现CopyProperties方法来解决。
```
class ARPGPlayerState : public APlayerState
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "ActionRPG")
ARPGCharacterBase* Character;
protected:
/** 这里是为了解决切换地图时的属性丢失问题 */
/** Copy properties which need to be saved in inactive PlayerState */
virtual void CopyProperties(APlayerState* PlayerState);
};
```
```
void ARPGPlayerState::CopyProperties(APlayerState* PlayerState)
{
//如果不是因为断线
if (IsFromPreviousLevel())
{
ARPGPlayerState* RPGPlayerState=Cast<ARPGPlayerState>(PlayerState);
RPGPlayerState->Character = Character;
}
}
```
### NetUpdateFrequency
PlayerState的NetUpdateFrequency和角色类的效果是一样的都是调节更新频率。默认数值对于GAS太低了会导致滞后性。100是个比较高的数值你也可以按照需求自己调节。
## Controller
### 获取PlayerControllers
- GetWorld()->GetPlayerControllerIterator()
- PlayerState->GetOwner()
- Pawn->GetController()
### BeginPlay里不做任何与GAS相关的操作
因为在服务端Possession会发生在在BeginPlay前客户端Possession发生在BeginPlay后所以我们不能在BeginPlay中写任何有关ASC的逻辑除非PlayerState已经同步。
### 相关判断服务端客户端状态函数
在Ability类中一些操作可能需要判断是服务端还是客户端比如应用GameplayEffect就在服务端执行(使用HasAuthority())而创建UI就需要在客户端执行(使用IsLocallyControlled())。
#### 蓝图
- HasAuthority()判断是否是服务端或是作为主机的玩家
- IsLocallyControlled()判断是否是本地控制器
- IsServer()通过World的ENetMode来段是服务端
#### c++
- 蓝图函数HasAuthority()在c++中为`return (GetLocalRole() == ROLE_Authority);`
- IsLocallyControlled(),c++中有对应函数。
## NetSecurityPolicy
GameplayAbility的网络安全策略决定了Ability应该在网络的何处执行. 它为尝试执行限制Ability的客户端提供了保护.
- ClientOrServer没有安全需求. 客户端或服务端可以自由地触发该Ability的执行和终止.
- ServerOnlyExecution客户端对该Ability请求的执行会被服务端忽略, 但客户端仍可以请求服务端取消或结束该Ability.
- ServerOnlyTermination客户端对该Ability请求的取消或结束会被服务端忽略, 但客户端仍可以请求执行该Ability.
- ServerOnly服务端控制该Ability的执行和终止, 客户端的任何请求都会被忽略.
## NetExecutionPolicy
推荐看这位大佬的文章https://zhuanlan.zhihu.com/p/143637846
## 属性集属性UI更新
这里推荐使用GASDocument中的GameplayTasks更新方法。
```
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AbilitySystemComponent.h"
#include "AsyncTaskAttributeChanged.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnAttributeChanged, FGameplayAttribute, Attribute, float, NewValue, float, OldValue);
/**
* Blueprint node to automatically register a listener for all attribute changes in an AbilitySystemComponent.
* Useful to use in UI.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask))
class RPGGAMEPLAYABILITY_API UAsyncTaskAttributeChanged : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FOnAttributeChanged OnAttributeChanged;
// Listens for an attribute changing.
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTaskAttributeChanged* ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute);
// Listens for an attribute changing.
// Version that takes in an array of Attributes. Check the Attribute output for which Attribute changed.
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTaskAttributeChanged* ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray<FGameplayAttribute> Attributes);
// You must call this function manually when you want the AsyncTask to end.
// For UMG Widgets, you would call it in the Widget's Destruct event.
UFUNCTION(BlueprintCallable)
void EndTask();
protected:
UPROPERTY()
UAbilitySystemComponent* ASC;
FGameplayAttribute AttributeToListenFor;
TArray<FGameplayAttribute> AttributesToListenFor;
void AttributeChanged(const FOnAttributeChangeData& Data);
};
```
```
#include "Abilities/AsyncTasks/AsyncTaskAttributeChanged.h"
UAsyncTaskAttributeChanged* UAsyncTaskAttributeChanged::ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute)
{
UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTaskAttributeChanged>();
WaitForAttributeChangedTask->ASC = AbilitySystemComponent;
WaitForAttributeChangedTask->AttributeToListenFor = Attribute;
if (!IsValid(AbilitySystemComponent) || !Attribute.IsValid())
{
WaitForAttributeChangedTask->RemoveFromRoot();
return nullptr;
}
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTaskAttributeChanged::AttributeChanged);
return WaitForAttributeChangedTask;
}
UAsyncTaskAttributeChanged * UAsyncTaskAttributeChanged::ListenForAttributesChange(UAbilitySystemComponent * AbilitySystemComponent, TArray<FGameplayAttribute> Attributes)
{
UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTaskAttributeChanged>();
WaitForAttributeChangedTask->ASC = AbilitySystemComponent;
WaitForAttributeChangedTask->AttributesToListenFor = Attributes;
if (!IsValid(AbilitySystemComponent) || Attributes.Num() < 1)
{
WaitForAttributeChangedTask->RemoveFromRoot();
return nullptr;
}
for (FGameplayAttribute Attribute : Attributes)
{
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTaskAttributeChanged::AttributeChanged);
}
return WaitForAttributeChangedTask;
}
void UAsyncTaskAttributeChanged::EndTask()
{
if (IsValid(ASC))
{
ASC->GetGameplayAttributeValueChangeDelegate(AttributeToListenFor).RemoveAll(this);
for (FGameplayAttribute Attribute : AttributesToListenFor)
{
ASC->GetGameplayAttributeValueChangeDelegate(Attribute).RemoveAll(this);
}
}
SetReadyToDestroy();
MarkPendingKill();
}
void UAsyncTaskAttributeChanged::AttributeChanged(const FOnAttributeChangeData & Data)
{
OnAttributeChanged.Broadcast(Data.Attribute, Data.NewValue, Data.OldValue);
}
```

View File

@@ -0,0 +1,75 @@
## 概述
在GameplayAbility框架中UAttributeSet负责管理各种属性。
## 定义宏
在头文件中添加:
```
// 使用AttributeSet.h的宏
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
```
参考注释可以得知使用这些宏可以让你少写各个属性的Get、Set、Init函数。如果不使用这个宏你也可以自己实现对应的函数。
这里用到了Ue4的反射。
## 添加属性
再以下格式添加属性:
```
UPROPERTY(BlueprintReadOnly, Category = "Health")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(URPGAttributeSet, Health)
```
## 重写接口
ActionRPG案例中重写了PreAttributeChange与PostGameplayEffectExecute接口。
前者会在属性被修改时调用在案例中是为了在血量或魔法上限发生变动时按比例调整当前血量或者魔法值。后者与GameplayEffect有关等到编写完GameplayEffect时再来编写。
```
void URPGAttributeSet::PreAttributeChange(const FGameplayAttribute & Attribute, float & NewValue)
{
Super::PreAttributeChange(Attribute, NewValue);
if (Attribute == GetMaxHealthAttribute())
{
AdjustAttributeForMaxChange(Health, MaxHealth, NewValue, GetHealthAttribute());
}
else if (Attribute == GetMaxManaAttribute())
{
AdjustAttributeForMaxChange(Mana, MaxMana, NewValue, GetManaAttribute());
}
}
void URPGAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
{
Super::PostGameplayEffectExecute(Data);
}
void URPGAttributeSet::AdjustAttributeForMaxChange(FGameplayAttributeData& AffectedAttribute, const FGameplayAttributeData& MaxAttribute, float NewMaxValue, const FGameplayAttribute& AffectedAttributeProperty)
{
UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent();
const float CurrentMaxValue = MaxAttribute.GetCurrentValue();
if (!FMath::IsNearlyEqual(CurrentMaxValue, NewMaxValue) && AbilityComp)
{
// Change current value to maintain the current Val / Max percent
const float CurrentValue = AffectedAttribute.GetCurrentValue();
float NewDelta = (CurrentMaxValue > 0.f) ? (CurrentValue * NewMaxValue / CurrentMaxValue) - CurrentValue : NewMaxValue;
AbilityComp->ApplyModToAttributeUnsafe(AffectedAttributeProperty, EGameplayModOp::Additive, NewDelta);
}
}
```
## 个人的使用方案
定义URPGAttributeSet作为基础属性集里面定义通用属性。之后又定义URPGCharacterAttributeSet作为角色专用的属性集。
之后在角色类中的构造函数中挂载URPGAttributeSet对象。
```
UPROPERTY()
URPGAttributeSet* AttributeSet;
```
```
AttributeSet = CreateDefaultSubobject<URPGAttributeSet>(TEXT("AttributeSet"));
```

View File

@@ -0,0 +1,147 @@
## UGameplayAbility
UGameplayAbility在GameplayAbility框架中代表一个技能也可以认为是能力它可以事件主动或者被动技能我们可以通过继承UGameplayAbility来编写新技能。
UGameplayAbility主要提供以下功能
- 使用特性技能cd、技能消耗等
- 网络同步支持
- 实例支持non-instance只在本地运行、Instanced per owner、Instanced per execution (默认)
其中GameplayAbility_Montage就是non-instanced ability的案例non-instanced在网络同步中有若干限制具体的参考源代码。
### 使用方式
在ActivateAbility事件中编写相关技能逻辑角色动作、粒子效果、角色数值变动最后根据具体情况技能是否施展成功调用CommitAbility()或EndAbility()。
如果有特殊可以在对应的事件中编写代码例如你需要技能释放结束后播放粒子特效那么就需要在onEndAbility事件中编写代码。
在c++中你需要重写ActivateAbility()函数。这里建议直接复制ActivateAbility的代码并且在它的基础上编写逻辑因为他兼顾了蓝图子类。
编写了Ability之后就需要将它注册到AbilityComponent中。但首先你需要创建正式用于编写角色逻辑的角色类ActionRPG案例中将基础的GameplayAbility逻辑都写在URPGCharacterBase类中所以现在你需要通过继承URPGCharacterBase来编写正式的角色逻辑包括各种输入、摄像机等等
此时你只需要在新建的子类的构造函数中手动添加GameplayAbilities数组即可
```
GameplayAbilities.Push(UGA_SkillBase::StaticClass());
```
### 在ActionRPG案例中的做法
在ActionRPG案例中定义了URPGGameplayAbility继承于UGameplayAbility作为项目中所有GameplayAbility的基类。它实现了实现了以下方法
```
/** Gameplay标签与GameplayEffect Map */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = GameplayEffects)
TMap<FGameplayTag, FRPGGameplayEffectContainer> EffectContainerMap;
/** 读取指定的FRPGGameplayEffectContainer来生成FRPGGameplayEffectContainerSpec */
UFUNCTION(BlueprintCallable, Category = Ability, meta = (AutoCreateRefTerm = "EventData"))
virtual FRPGGameplayEffectContainerSpec MakeEffectContainerSpecFromContainer(const FRPGGameplayEffectContainer& Container, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1);
/** 通过GameplayTag来搜索EffectContainerMap并且生成FRPGGameplayEffectContainerSpec */
UFUNCTION(BlueprintCallable, Category = Ability, meta = (AutoCreateRefTerm = "EventData"))
virtual FRPGGameplayEffectContainerSpec MakeEffectContainerSpec(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1);
/** 让FRPGGameplayEffectContainerSpec中的effect对指定目标生效 */
UFUNCTION(BlueprintCallable, Category = Ability)
virtual TArray<FActiveGameplayEffectHandle> ApplyEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec);
/** 调用MakeEffectContainerSpec生成FRPGGameplayEffectContainerSpec再让Effect对目标生效 */
UFUNCTION(BlueprintCallable, Category = Ability, meta = (AutoCreateRefTerm = "EventData"))
virtual TArray<FActiveGameplayEffectHandle> ApplyEffectContainer(FGameplayTag ContainerTag, const FGameplayEventData& EventData, int32 OverrideGameplayLevel = -1);
```
代码很简单,大致可以归纳为:
1. 维护一个GameplayTag与RPGGameplayEffectContainer的映射表EffectContainerMap。
2. 创建FRPGGameplayEffectContainerSpec。(可以通过GameplayTag查找EffectContainerMap或者通过指定的RPGGameplayEffectContainer)。
3. 通过FRPGGameplayEffectContainerSpec让内部所有effect对目标生效。
### URPGTargetType
父类是UObject只有一个GetTargets事件。之后通过各种的子类来实现各种的目标效果。
该类用于实现获取Ability所作用的目标换句话说是获取目标数据的逻辑目标Actor数组。用以实现例如单体目标范围目标等等
### FRPGGameplayEffectContainer
结构体存储了URPGTargetType对象与UGameplayEffect容器数组对象。
### FRPGGameplayEffectContainerSpec
RPGGameplayEffectContainer的处理后版本。
在URPGGameplayAbility中会调用MakeEffectContainerSpecFromContainer()生成。
如果FRPGGameplayEffectContainer存在TargetType对象就会调用它的GetTargets函数来获取HitResult数组与Actor数组。最后调用AddTargets函数来填充FRPGGameplayEffectContainerSpec中的Target信息。
填充FRPGGameplayEffectContainerSpec的FGameplayEffectSpecHandle数组FGameplayEffectSpecHandle中包含了FGameplayEffectSpec的智能指针
说了那么多其实就是将Effect应用到所有TargetActor上。
### 重要函数
从头文件中复制的注释:
```
CanActivateAbility() - const function to see if ability is activatable. Callable by UI etc
TryActivateAbility() - Attempts to activate the ability. Calls CanActivateAbility(). Input events can call this directly.
- Also handles instancing-per-execution logic and replication/prediction calls.
CallActivateAbility() - Protected, non virtual function. Does some boilerplate 'pre activate' stuff, then calls ActivateAbility()
ActivateAbility() - What the abilities *does*. This is what child classes want to override.
CommitAbility() - Commits reources/cooldowns etc. ActivateAbility() must call this!
CancelAbility() - Interrupts the ability (from an outside source).
EndAbility() - The ability has ended. This is intended to be called by the ability to end itself.
```
## 关于BindAbilityActivationToInputComponent
~~这个东西我查了Github、AnswerHUB以及轮廓没有任何资料除了作者的Wiki。看了源代码也看不出所以然而且ActionRPG里也没有使用这个函数可以看得出即使不用这个函数也不会影响该框架别的功能可能会对联机游戏产生影响。~~
经过@键盘侠·伍德 的指导,我才知道用法:
1. 声明一个用于映射输入的枚举类
```
UENUM(BlueprintType)
enum class AbilityInput : uint8
{
UseAbility1 UMETA(DisplayName = "Use Spell 1"), //This maps the first ability(input ID should be 0 in int) to the action mapping(which you define in the project settings) by the name of "UseAbility1". "Use Spell 1" is the blueprint name of the element.
UseAbility2 UMETA(DisplayName = "Use Spell 2"), //Maps ability 2(input ID 1) to action mapping UseAbility2. "Use Spell 2" is mostly used for when the enum is a blueprint variable.
UseAbility3 UMETA(DisplayName = "Use Spell 3"),
UseAbility4 UMETA(DisplayName = "Use Spell 4"),
WeaponAbility UMETA(DisplayName = "Use Weapon"), //This finally maps the fifth ability(here designated to be your weaponability, or auto-attack, or whatever) to action mapping "WeaponAbility".
//You may also do something like define an enum element name that is not actually mapped to an input, for example if you have a passive ability that isn't supposed to have an input. This isn't usually necessary though as you usually grant abilities via input ID,
//which can be negative while enums cannot. In fact, a constant called "INDEX_NONE" exists for the exact purpose of rendering an input as unavailable, and it's simply defined as -1.
//Because abilities are granted by input ID, which is an int, you may use enum elements to describe the ID anyway however, because enums are fancily dressed up ints.
};
```
2. 在SetupPlayerInputComponent函数中调用BindAbilityActivationToInputComponent函数
例如:
```
void ARPGCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
// Set up gameplay key bindings
check(PlayerInputComponent);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAxis("MoveForward", this, &ARPGCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ARPGCharacter::MoveRight);
// We have 2 versions of the rotation bindings to handle different kinds of devices differently
// "turn" handles devices that provide an absolute delta, such as a mouse.
// "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("TurnRate", this, &ARPGCharacter::TurnAtRate);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("LookUpRate", this, &ARPGCharacter::LookUpAtRate);
//PlayerInputComponent->BindAction("CastBaseSkill", IE_Pressed, this, &ARPGCharacter::CastBaseSkill);
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds("ConfirmInput", "CancelInput", "AbilityInput"));
}
```
3. 在执行GiveAbility函数注册能力设置输入id。输入id为枚举类中对应的枚举值。例如本案例中UseAbility1为0UseAbility2为1UseAbility3为2
```
FGameplayAbilitySpec(TSubclassOf<UGameplayAbility> InAbilityClass, int32 InLevel, int32 InInputID, UObject* InSourceObject)
```
4. 在项目设置——输入中按照所设置的输入id所对应的枚举添加ActionMapping。例如UseAbility1、UseAbility2、UseAbility3。
这样做可以达到对Control解耦的目的。因为你调用GiveAbility或者ClearAbility时会自动绑定输入。而不需要手动去角色类或者控制类中手动设置。
### 有关InputPressed与InputReleased
执行了上述输入绑定措施你就可以通过重写InputPressed与InputReleased来执行对应的逻辑。
调用这两个虚函数的逻辑在UAbilitySystemComponent中的AbilitySpecInputPressed与AbilitySpecInputReleased。
个人认为这些逻辑还会有可能会在蓝图中编写所以新继承的类可以创建新的BlueprintNativeEvent这样对工程开发会更加友好。

View File

@@ -0,0 +1,354 @@
## UGameplayEffect
UGameplayEffect在框架中主要负责各种数值上的效果如果技能cd、类似黑魂中的异常效果堆叠与buff甚至连角色升级时的属性点添加都可以使用它来实现。
因为大多数逻辑都是设置数据子类的操作,所以对于这个类,本人推荐使用蓝图来进行操作。
## 简单使用教程
通过继承UGameplayEffect来创建一个新的GameplayEffect类并在构造函数中对相应的属性进行设置。之后在Ability类中调用ApplyGameplayEffectToOwner函数让GameplayEffect生效。
```
if (CommitAbility(Handle, ActorInfo, ActivationInfo)) // ..then commit the ability...
{
// Then do more stuff...
const UGameplayEffect* GameplayEffect = NewObject<UGE_DamageBase>();
ApplyGameplayEffectToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, GameplayEffect, 5, 1);
K2_EndAbility();
}
```
具体操作可以参考ActionRPG模板或者是我的项目代码。
## Modifiers
本质是一个FGameplayModifierInfo结构体数组用于存储所有数值修改信息。FGameplayModifierInfo包含以下属性
- Attribute 修改的目标属性集中的属性。
- ModifierOp 修改方式。例如Override、Add、Multiply。
- Magnitude已被废弃
- ModifierMagnitude 修改的数值与类型,可以配置数据表。
- EvaluationChannelSettings (不知道为什么没在编辑器中显示,而且代码中只有一处调用,所以直接跳过)
- SourceTags 本身标签行为Effect生效所需或者忽略的标签
- TargetTags 目标标签行为Effect生效所需或者忽略的标签
可以看得出ModifierMagnitude才是Modifiers的关键而它的本质是FGameplayEffectModifierMagnitude结构体。但是我们只需要学会初始化它即可。它具有以下四种类型
- ScalableFloat 较为简单的使用方式使用ScalableFloat进行计算
- AttributeBased 基于属性执行计算。
- CustomCalculationClass 能够捕获多个属性进行自定义计算
- SetByCaller 被蓝图或者代码显式设置
### ScalableFloat的调用示例
ScalableFloat类型是用于设置固定值的简单方式同时它也支持通过CurveTable配合技能等级设置倍率。最后结果=固定值*倍率当然如果你向完全通过CurveTable来控制参数那就把固定值设置为1即可。
```
FGameplayModifierInfo info;
info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
info.ModifierOp = EGameplayModOp::Additive;
//固定值
//info.ModifierMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(100.0));
//CurveTable控制倍率
FScalableFloat damageValue = {1.0};
FCurveTableRowHandle damageCurve;
static ConstructorHelpers::FObjectFinder<UCurveTable> curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage"));
damageCurve.CurveTable = curveAsset.Object;
damageCurve.RowName = FName("Damage");
damageValue.Curve = damageCurve;
info.ModifierMagnitude = FGameplayEffectModifierMagnitude(damageValue);
Modifiers.Add(info);
```
PS.技能等级在ApplyGameplayEffectToOwner函数中设置。
### AttributeBased的调用示例
最终计算过程可以在CalculateMagnitude函数中找到。
1. 如果尝试捕获到数值不为None则将赋值给AttribValue。
2. 判断AttributeCalculationType来计算对应的AttribValue。我不太了解代码中channel的概念如果channel不存在AttribValue为原本值
3. 如果AttributeCurve存在则将AttribValue作为x轴值来查找y轴值并进行插值计算最后将结果赋值给AttribValue。
4. 最终计算公式:`$((Coefficient * (AttribValue + PreMultiplyAdditiveValue)) + PostMultiplyAdditiveValue)$`
### BackingAttribute
为GameplayEffect捕获GameplayAttribute的选项。你可以理解为Lambda表达式的捕获
- AttributeToCapture捕获属性
- AttributeSource捕获的目标自身还是目标对象
- bSnapshot属性是否需要被快照没仔细看如果为false每次都会重新获取吧
### AttributeCalculationType
默认值为AttributeMagnitude。
- AttributeMagnitude使用最后通过属性计算出来的级数
- AttributeBaseValue使用属性基础值
- AttributeBonusMagnitude使用最后计算值-基础值)
- AttributeMagnitudeEvaluatedUpToChannel不清楚使用方法关键是在编辑器中这个选项默认是不显示的
```
FGameplayModifierInfo info;
info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
info.ModifierOp = EGameplayModOp::Additive;
FAttributeBasedFloat damageValue;
damageValue.Coefficient = { 1.2f };
damageValue.PreMultiplyAdditiveValue = { 1.0f };
damageValue.PostMultiplyAdditiveValue = { 2.0f };
damageValue.BackingAttribute = FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
, EGameplayEffectAttributeCaptureSource::Source, false);
FCurveTableRowHandle damageCurve;
static ConstructorHelpers::FObjectFinder<UCurveTable> curveAsset(TEXT("/Game/ActionRPG/DataTable/Damage"));
damageCurve.CurveTable = curveAsset.Object;
damageCurve.RowName = FName("Damage");
damageValue.AttributeCurve = damageCurve;
damageValue.AttributeCalculationType = EAttributeBasedFloatCalculationType::AttributeMagnitude;
info.ModifierMagnitude = damageValue;
Modifiers.Add(info);
```
### CustomCalculationClass的调用示例
与AttributeCalculationType相比少了属性捕获多了CalculationClassMagnitudeUGameplayModMagnitudeCalculation类
```
FGameplayModifierInfo info;
info.Attribute = FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)));
info.ModifierOp = EGameplayModOp::Additive;
FCustomCalculationBasedFloat damageValue;
damageValue.CalculationClassMagnitude = UDamageMagnitudeCalculation::StaticClass();
damageValue.Coefficient = { 1.2f };
damageValue.PreMultiplyAdditiveValue = { 1.0f };
damageValue.PostMultiplyAdditiveValue = { 2.0f };
info.ModifierMagnitude = damageValue;
Modifiers.Add(info);
```
PS.如果这个计算过程还取决于外部非GameplayAbility框架的条件那么你可能需要重写GetExternalModifierDependencyMulticast()函数以获得FOnExternalGameplayModifierDependencyChange委托。从而实现当外部条件发生改变时及时更新计算结果。
### UGameplayModMagnitudeCalculation
你可以通过继承UGameplayModMagnitudeCalculation来创建自定义的Calculation类。所需实现步骤如下
1. 在构造函数中向RelevantAttributesToCapture数组添加需要捕获的属性。
2. 实现CalculateBaseMagnitude事件。因为BlueprintNativeEvent类型所以既可以在c++里实现也可以在蓝图中实现关于两者结合可以参考UGameplayAbility类中ActivateAbility()的写法。
案例代码如下:
```
UCLASS(BlueprintType, Blueprintable, Abstract)
class ACTIONRPG_API UDamageMagnitudeCalculation : public UGameplayModMagnitudeCalculation
{
GENERATED_BODY()
public:
UDamageMagnitudeCalculation();
float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
};
```
```
UDamageMagnitudeCalculation::UDamageMagnitudeCalculation()
{
RelevantAttributesToCapture.Add(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
, EGameplayEffectAttributeCaptureSource::Source, false));
}
float UDamageMagnitudeCalculation::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
float damage{ 0.0f};
FAggregatorEvaluateParameters InEvalParams;
//捕获失败的容错语句
if (!GetCapturedAttributeMagnitude(FGameplayEffectAttributeCaptureDefinition(FGameplayAttribute(FindFieldChecked<UProperty>(URPGAttributeSet::StaticClass(), GET_MEMBER_NAME_CHECKED(URPGAttributeSet, Health)))
, EGameplayEffectAttributeCaptureSource::Source, false), Spec, InEvalParams, damage)) {
//如果这个变量会作为除数的话不能为0
damage = 1.0f;
}
return damage;
}
```
## Executions
Executions更为简单而且更加自由。只需要编写Calculation Class即可。它与Modifiers的不同之处在于一个Modifiers只能修改一个属性而Executions可以同时改动多个属性。
### UGameplayEffectExecutionCalculation
这里我就直接复制actionRPG模板的代码了。
开头的RPGDamageStatics结构体与DamageStatics函数可以减少后面的代码量。可以算是FGameplayEffectAttributeCaptureDefinition的语法糖吧。
```
UCLASS()
class ACTIONRPG_API UDamageExecutionCalculation : public UGameplayEffectExecutionCalculation
{
GENERATED_BODY()
public:
UDamageExecutionCalculation();
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
```
```
struct RPGDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower);
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
RPGDamageStatics()
{
// Capture the Target's DefensePower attribute. Do not snapshot it, because we want to use the health value at the moment we apply the execution.
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false);
// Capture the Source's AttackPower. We do want to snapshot this at the moment we create the GameplayEffectSpec that will execute the damage.
// (imagine we fire a projectile: we create the GE Spec when the projectile is fired. When it hits the target, we want to use the AttackPower at the moment
// the projectile was launched, not when it hits).
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true);
// Also capture the source's raw Damage, which is normally passed in directly via the execution
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true);
}
};
static const RPGDamageStatics& DamageStatics()
{
static RPGDamageStatics DmgStatics;
return DmgStatics;
}
UDamageExecutionCalculation::UDamageExecutionCalculation()
{
RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef);
RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef);
RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
}
void UDamageExecutionCalculation::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();
AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->AvatarActor : nullptr;
AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->AvatarActor : nullptr;
const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
// Gather the tags from the source and target as that can affect which buffs should be used
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
// --------------------------------------
// Damage Done = Damage * AttackPower / DefensePower
// If DefensePower is 0, it is treated as 1.0
// --------------------------------------
//计算捕获属性的数值。
float DefensePower = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DefensePowerDef, EvaluationParameters, DefensePower);
//因为要做除数所以需要加入容错语句
if (DefensePower == 0.0f)
{
DefensePower = 1.0f;
}
float AttackPower = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackPowerDef, EvaluationParameters, AttackPower);
float Damage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);
//伤害计算公式
float DamageDone = Damage * AttackPower / DefensePower;
if (DamageDone > 0.f)
{
//这里可以修改多个属性
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, DamageDone));
}
}
```
## Period
Period指的是周期一般用于制作周期性技能。
```
//持续类型只有设置为HasDuration技能才能变成周期性的
DurationPolicy = EGameplayEffectDurationType::HasDuration;
//持续时间
DurationMagnitude = FGameplayEffectModifierMagnitude(1.0);
//周期,技能生效次数=持续时间/周期
Period = 2.0;
```
# FGameplayEffectContainer与Spec
EPIC实现了这个结构体
调用URPGGameplayAbility::MakeEffectContainerSpecFromContainer
使用FGameplayEffectContainer生成FGameplayEffectContainerSpec结构体。
Spec是实例版本存储TargetDataHandle与EffectSpecHandle。通过MakeEffectContainerSpecFromContainer进行实例化但本质是通过Spec的AddTarget进行数据填充
之后再通过
```
ReturnSpec.TargetGameplayEffectSpecs.Add(MakeOutgoingGameplayEffectSpec(EffectClass, OverrideGameplayLevel));
```
填充EffectSpec数据。
**MakeEffectContainerSpec则是个快捷函数**
通过FGameplayTag寻找对应的Effect与Target数据。EventData则用于调用TargetType类的GetTarget函数用于获取符合要求的目标Actor
在ActionRPG中URPGTargetType_UseEventData的GetTarget用到了EventData。大致逻辑为首先寻找EventData里是否带有EventData.HitResult信息可以在Send Event To Actor中设置如果没有则返回EventData.Target信息。
```
void URPGTargetType_UseEventData::GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const
{
const FHitResult* FoundHitResult = EventData.ContextHandle.GetHitResult();
if (FoundHitResult)
{
OutHitResults.Add(*FoundHitResult);
}
else if (EventData.Target)
{
OutActors.Add(const_cast<AActor*>(EventData.Target));
}
}
```
ApplyEffectContainer则是个方便函数。
# 实现在AnimNotify中向指定目标引用GameplayEffect
从GameplayAbilityComponent或者从GameplayAbility中设置.
MakeOutgoingGameplayEffectSpec=>
ApplyGameplayEffectSpecToTarget 位于UGameplayAbility
ApplyGameplayEffectToTarget
GameplayEffectSpec.GetContext().AddTarget()
RemoveGrantedByEffect()函数可以移除Ability中Instance类型的Effect。非常适合来清除翻滚免伤、技能硬直效果。
```
FRPGGameplayEffectContainerSpec URPGBlueprintLibrary::AddTargetsToEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec, const TArray<FHitResult>& HitResults, const TArray<AActor*>& TargetActors)
{
FRPGGameplayEffectContainerSpec NewSpec = ContainerSpec;
NewSpec.AddTargets(HitResults, TargetActors);
return NewSpec;
}
TArray<FActiveGameplayEffectHandle> URPGBlueprintLibrary::ApplyExternalEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec)
{
TArray<FActiveGameplayEffectHandle> AllEffects;
// Iterate list of gameplay effects
for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs)
{
if (SpecHandle.IsValid())
{
// If effect is valid, iterate list of targets and apply to all
for (TSharedPtr<FGameplayAbilityTargetData> Data : ContainerSpec.TargetData.Data)
{
AllEffects.Append(Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get()));
}
}
}
return AllEffects;
}
```
```
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, GameplayEffectClass, GameplayEffectLevel);
if (SpecHandle.Data.IsValid())
{
SpecHandle.Data->StackCount = Stacks;
SCOPE_CYCLE_UOBJECT(Source, SpecHandle.Data->GetContext().GetSourceObject());
EffectHandles.Append(ApplyGameplayEffectSpecToTarget(Handle, ActorInfo, ActivationInfo, SpecHandle, Target));
}
```
# 实用函数
Wait Input Release

View File

@@ -0,0 +1,167 @@
## UAbilityTask
UAbilityTask继承自UGameplayTaskUGameplayTask可以用来写一些行为树中的一些节点可以用来实现一些异步功能。比如播放montage后各种事件的处理。
你可以去GameplayAbilities\Public\Abilities\Tasks\目录下寻找作者编写的task类作为相关参考也可以直接使用GameplayTasks目录下的案例比较少
当然我更加推荐学习actionRPG项目中的PlayMontageAndWaitForEvent原因有1、这个task用得最多2、涉及Task的代码相对较多。
其他推荐学习的UGameplayTask_WaitDelay、UAbilityTask_WaitGameplayEvent、UAbilityTask_WaitGameplayTagAdded、UAbilityTask_WaitGameplayEffectApplied
## 大致过程
1. 声明多个动态多播委托用于处理各种事件。
2. 重写所需的虚函数,并且声明相关变量。
3. 编写主体函数。
## 代码分析
在PlayMontageAndWaitForEvent中重写了4个虚函数
```
//用于在各种委托设置完毕后开始执行真正的Tasks。
virtual void Activate() override;
//从外部取消这个Tasks默认情况下会结束任务。
virtual void ExternalCancel() override;
//返回debug字符串内容为当前播放的Montage名称以及Tasks存储的Montage名称
virtual FString GetDebugString() const override;
//结束并清理Tasks既可以在Tasks内部调用可以从该Tasks拥有者调用。
//注意请不要直接调用该函数你应该调用EndTask()或者TaskOwnerEnded()
//注意重写该函数时请确保最后调用Super::OnDestroy(bOwnerFinished)
virtual void OnDestroy(bool AbilityEnded) override;
```
## Activate()
```
void URPGAbilityTask_PlayMontageAndWaitForEvent::Activate()
{
if (Ability == nullptr)
{
return;
}
bool bPlayedMontage = false;
URPGAbilitySystemComponent* RPGAbilitySystemComponent = GetTargetASC();
if (RPGAbilitySystemComponent)
{
const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
if (AnimInstance != nullptr)
{
//绑定事件回调函数
EventHandle = RPGAbilitySystemComponent->AddGameplayEventTagContainerDelegate(EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent));
//播放montage
if (RPGAbilitySystemComponent->PlayMontage(Ability, Ability->GetCurrentActivationInfo(), MontageToPlay, Rate, StartSection) > 0.f)
{
//播放Montage后其回调函数可能会导致Ability结束所以我们需要提前结束
if (ShouldBroadcastAbilityTaskDelegates() == false)
{
return;
}
//绑定OnAbilityCancelled
CancelledHandle = Ability->OnGameplayAbilityCancelled.AddUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnAbilityCancelled);
//绑定OnMontageBlendingOut
BlendingOutDelegate.BindUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendingOut);
AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, MontageToPlay);
//绑定OnMontageEnded
MontageEndedDelegate.BindUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageEnded);
AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, MontageToPlay);
ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
if (Character && (Character->Role == ROLE_Authority ||
(Character->Role == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
{
Character->SetAnimRootMotionTranslationScale(AnimRootMotionTranslationScale);
}
bPlayedMontage = true;
}
}
else
{
ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent call to PlayMontage failed!"));
}
}
else
{
ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent called on invalid AbilitySystemComponent"));
}
//播放失败处理
if (!bPlayedMontage)
{
ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent called in Ability %s failed to play montage %s; Task Instance Name %s."), *Ability->GetName(), *GetNameSafe(MontageToPlay),*InstanceName.ToString());
if (ShouldBroadcastAbilityTaskDelegates())
{
OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
}
}
SetWaitingOnAvatar();
}
```
## ExternalCancel()
```
check(AbilitySystemComponent);
OnAbilityCancelled();
Super::ExternalCancel();
```
OnAbilityCancelled的代码
```
if (StopPlayingMontage())
{
// Let the BP handle the interrupt as well
if (ShouldBroadcastAbilityTaskDelegates())
{
OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
}
}
```
## OnDestroy()
```
void URPGAbilityTask_PlayMontageAndWaitForEvent::OnDestroy(bool AbilityEnded)
{
// Note: Clearing montage end delegate isn't necessary since its not a multicast and will be cleared when the next montage plays.
// (If we are destroyed, it will detect this and not do anything)
// This delegate, however, should be cleared as it is a multicast
if (Ability)
{
Ability->OnGameplayAbilityCancelled.Remove(CancelledHandle);
if (AbilityEnded && bStopWhenAbilityEnds)
{
//停止播放Montage
StopPlayingMontage();
}
}
URPGAbilitySystemComponent* RPGAbilitySystemComponent = GetTargetASC();
if (RPGAbilitySystemComponent)
{
//移除事件绑定
RPGAbilitySystemComponent->RemoveGameplayEventTagContainerDelegate(EventTags, EventHandle);
}
//这句必须放在最后
Super::OnDestroy(AbilityEnded);
}
```
## PlayMontageAndWaitForEvent
PlayMontageAndWaitForEvent是Tasks的主体函数。
```
URPGAbilityTask_PlayMontageAndWaitForEvent* URPGAbilityTask_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(UGameplayAbility* OwningAbility,
FName TaskInstanceName, UAnimMontage* MontageToPlay, FGameplayTagContainer EventTags, float Rate, FName StartSection, bool bStopWhenAbilityEnds, float AnimRootMotionTranslationScale)
{
//用于缩放GAS tasks变量的工具函数此为非shipping功能用于交互调试。
UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(Rate);
//使用NewAbilityTask来创建Tasks并且设置各个变量。
URPGAbilityTask_PlayMontageAndWaitForEvent* MyObj = NewAbilityTask<URPGAbilityTask_PlayMontageAndWaitForEvent>(OwningAbility, TaskInstanceName);
MyObj->MontageToPlay = MontageToPlay;
MyObj->EventTags = EventTags;
MyObj->Rate = Rate;
MyObj->StartSection = StartSection;
MyObj->AnimRootMotionTranslationScale = AnimRootMotionTranslationScale;
MyObj->bStopWhenAbilityEnds = bStopWhenAbilityEnds;
return MyObj;
}
```

View File

@@ -0,0 +1,124 @@
# 有用的GameplayTasks整理
## 前言
整理了一下GameplayTasks节点在此分享给大家。
## 自带GameplayTasks
### ApplyRootMotion系列
在对应名称的static函数中调用初始化函数SharedInitAndApply()其作用为取得CharacterMovementComponent并调用ApplyRootMotionSource应用构建的FRootMotionSource。
在TickTask实现取得Avatar角色类并且判断MovementComponent的当前FRootMotionSource是否运行结束。并且非Simulation客户端上触发OnFinish委托并强制更新Avatar。其成员变量设置了网络同步属性。该系列有
- AbilityTask_ApplyRootMotion_Base
- AbilityTask_ApplyRootMotionConstantForce
- AbilityTask_ApplyRootMotionJumpForce
- AbilityTask_ApplyRootMotionMoveToActorForce
- AbilityTask_ApplyRootMotionMoveToForce
- AbilityTask_ApplyRootMotionRadialForce
FRootMotionSource为CharacterMovementComponent的RootMotion的通用源通过一系列运算覆盖、增加、设置、忽视将位移与旋转应用到CharacterMovementComponent。可以认为是一个数值驱动、非动画驱动的RootMotion引擎中实现了以下类型
- FRootMotionSource_JumpForce:让目标像跳跃一样移动。
- FRootMotionSource_MoveToDynamicForce:在一定时间内强制将目标移动到指定世界坐标(目标可以在这段时间内随意移动)。
- FRootMotionSource_MoveToForce:在一定时间内强制将目标移动到指定世界坐标。
- FRootMotionSource_RadialForce:在世界指定坐标施加可作用于目标的一个拉扯或者推出的力。
- FRootMotionSource_ConstantForce:对目标施加一个固定数值的力。
本人测试了:
- JumpForce感觉其作用是可以编写参数化的Jump。还可以加各种Curve可改进的地方就是增加跳跃到最高点的Delegate。
- ConstantForce、RadialForce可以做一些场景、怪物击飞效果。
- MoveToForce可以做角色与交互式物体互动式的强制位移。
- MoveToActorForce可以做类似魔兽世界的飞行点快速旅行功能。
### GAS系统相关
>Ability与AttributeSet
- AbilityTask_StartAbilityState:用于监视所在Ability的状态会触发Ability的OnGameplayAbilityStateEnded与OnGameplayAbilityCancelled委托。
- AbilityTask_WaitAttributeChange:AbilityTask版本的属性监视Task拥有WaitForAttributeChange与WaitForAttributeChangeWithComparison属性比较版本
- AbilityTask_WaitAbilityActivate:实现了3个版本用于监视指定Ability的激活事件通过判断Ability的Tag触发OnActivate委托
- AbilityTask_WaitAbilityCommit:实现了2个版本用于监视指定Ability的Commit事件通过判断Ability的Tag触发OnCommit委托
- AbilityTask_WaitAttributeChangeThreshold:用于监视指定属性变化数值符合指定比较类型会触发OnChange委托。
- AbilityTask_WaitAttributeChangeRatioThreshold:用于监视由2个属性组合计算出比例变化l例如生命值与最大生命值符合指定比较类型会触发OnChange委托。
>GameplayEffect
- AbilityTask_WaitGameplayEffectApplied:GameplayEffectApplied系列基类OnApplyGameplayEffectCallback实现了标签过滤与匹配逻辑。开发时不应该直接调用该类。
- AbilityTask_WaitGameplayEffectApplied_Self:实现Tag与Query两个版本的将父类处理函数绑定到ASC的OnPeriodicGameplayEffectExecuteDelegateOnSelf。
- AbilityTask_WaitGameplayEffectApplied_Target实现Tag与Query两个版本的将父类处理函数绑定到ASC的OnPeriodicGameplayEffectExecuteDelegateOnTarget。
- AbilityTask_WaitGameplayEffectBlockedImmunity:绑定ASC的OnImmunityBlockGameplayEffectDelegate检查EffectSpec的CapturedSourceTags与CapturedTargetTags会触发Blocked委托。
- AbilityTask_WaitGameplayEffectRemoved:通过GameplayEffectHandle绑定ASC的OnGameplayEffectRemoved_InfoDelegate。会触发OnChange委托如果Handle无效则会触发InvalidHandle委托。
- AbilityTask_WaitGameplayEffectStackChange:通过GameplayEffectHandle绑定ASC的OnGameplayEffectStackChangeDelegate。会触发OnChange委托如果Handle无效则会触发InvalidHandle委托。
>GameplayEvent与GameplayTag
- AbilityTask_WaitGameplayEvent:根据OnlyMatchExact绑定ASC的GenericGameplayEventCallbacks与ASC的AddGameplayEventTagContainerDelegate。会触发EventReceived委托。
- AbilityTask_WaitGameplayTagBase:WaitGameplayTag基类因为没有实现回调函数所以开发时不应该直接调用该类。
- AbilityTask_WaitGameplayTag:根据Tag绑定ASC的RegisterGameplayTagEvent()会触发Added委托。
- AbilityTask_WaitTargetData:Spawn一个TargetData之后等待接收TargetData。
### 输入
源码的Confirm与Cancel部分有实现3个节点,对于本地端则与ASC的GenericLocalXXXXXCallbacks委托绑定本地版本OnXXXXX回调函数远程端则调用CallOrAddReplicatedDelegate绑定OnXXXXX回调函数。回调函数在处理完同步相关的逻辑后会触发FGenericGameplayTaskDelegate类型的OnXXXXX委托。AbilityTask_WaitConfirmCancel会同时监视Confirm与Cancel。
- AbilityTask_WaitCancel
- AbilityTask_WaitConfirm
- AbilityTask_WaitConfirmCancel
>Confirm与Cancel的绑定可以参考GASDocument的4.6.2章节。
AbilityTask_WaitInputPress: 绑定ASC的AbilityReplicatedEventDelegate()监视与Ability绑定的InputAction是否按下。在处理同步相关的逻辑后触发OnPress委托。
AbilityTask_WaitInputRelease:绑定ASC的AbilityReplicatedEventDelegate()监视与Ability绑定的InputAction是否松开。在处理同步相关的逻辑后触发OnRelease委托。
### 网络相关
- AbilityTask_NetworkSyncPoint:网络同步等待节点用于等待Server/Client同步完成。
### 移动类
- AbilityTask_MoveToLocation:通过TickTask来实现让目标在一定时间内移动到指定世界坐标。(官方注释说这个实现方法不太好有待改进)
- AbilityTask_WaitMovementModeChange:绑定角色类的MovementModeChangedDelegate触发OnChange委托。
- AbilityTask_WaitVelocityChange:通过TickTask实现。每帧判断Dot(MovementComponent的速度与指定方向)是否超过设定阈值超过就会触发OnVelocityChage委托。
### 工具类
- AbilityTask_WaitDelay:使用定时器实现定时触发一次OnFinish委托。
- AbilityTask_Repeat:使用定时器实现定时多次触发OnPerformAction委托。
- AbilityTask_SpawnActor:BeginSpawningActor()与FinishSpawningActor()不知道如何在蓝图中调用所以无法使用SpawnActorDeferred方式。
- UAbilityTask_VisualizeTargeting因为VisualizeTargeting的相关逻辑写在BeginSpawningActor与FinishSpawningActor所以也存在上述问题。VisualizeTargetingUsingActor则会Spawn一个TargetActor用于追踪Ability到时候后悔触发TimeElapsed委托。
- AbilityTask_WaitOverlap:绑定AvatarActor的Root组件的OnComponentHit委托。触发的OnOverlap委托会附带FGameplayAbilityTargetDataHandle(FGameplayAbilityTargetData_SingleTargetHit处理的TargetData)
## AbilityAsync
UAbilityAsync是一种Ability相关的定制BlueprintAsyncActions与GameplayAbilityTasks的不同之处它可以在任意蓝图类中调用并且生命周期与Ability无关。
- AbilityAsync_WaitGameplayEffectApplied等待指定GE可以限定Target与Source标签应用到当前Ability所在ASC中。委托来自于ASC。
- AbilityAsync_WaitGameplayEvent等待GameplayEvent。委托来自于ASC。
- AbilityAsync_WaitGameplayTag等待GameplayTag。委托来自于ASC。
## GASShooter中的GameplayTasks
- GSAT_MoveSceneCompRelLocation在设定时间内按照设定曲线移动指定的SceneComponent逻辑在TickTasks中处理。
- GSAT_PlayMontageAndWaitForEvent播放Montage并且等待GameplayEvent。
- GSAT_PlayMontageForMeshAndWaitForEvent使指定Mesh播放Montage并且等待GameplayEvent。在GASShooter中用于播放武器Montage解决播放非Avatar、Owner的Montage的问题。
- GSAT_WaitChangeFOV在设定时间内按照设定曲线设置Fov,逻辑在TickTasks中处理。
- GSAT_WaitDelayOneFrame使用GetWorld()->GetTimerManager().SetTimerForNextTick()调用函数触发OnFinish委托来实现。
- GSAT_WaitInputPressWithTags将委托AbilitySystemComponent->AbilityReplicatedEventDelegate(根据Ability与PredictionKey取得的同步事件委托)与OnPressCallback绑定测试模式下直接触发OnPressCallback函数。OnPressCallback逻辑为判断tag是否符合要求不符合则重置符合要求则移除AbilityReplicatedEventDelegate绑定根据Ability的预测类型处理ASC同步事件最后触发OnPress委托。
- GSAT_WaitInteractableTarget用于检测可交互物体使用定时器来不断进行射线求交判断。根据结果触发LostInteractableTarget与FoundNewInteractableTarget委托。
- GSAT_WaitTargetDataUsingActorGASShooter的改进版本等待接收已经生成TargetActor发送的TargetData。绑定Delegate为TargetActor的TargetDataReadyDelegate与CanceledDelegate。服务端还会绑定ASC的AbilityTargetDataSetDelegate与AbilityTargetDataCancelledDelegate以及处理同步事件。
- GSAT_ServerWaitForClientTargetData绑定ASC的AbilityTargetDataSetDelegate委托。其回调函数会消耗掉客户端ACS的TargetData同步事件。之后触发ValidData委托。即只处理服务端TargetData同步事件
这里的AsyncTask并没有使用UAbilityAsync实现而是直接继承自BlueprintAsyncActions进行实现。在项目中主要在UMG获取对应数据
- AsyncTaskGameplayTagAddedRemoved监视Tag的增加与减少情况用于判断状态。
- AsyncTaskAttributeChanged监视数据值的改变情况用于设置血条、蓝条数值。
- AsyncTaskCooldownChanged监视对应Ability的冷却情况显示技能图标的CD效果。
- AsyncTaskEffectStackChanged监视GE堆叠情况用于显示Buffer/Debuffer堆叠情况。
## 自带的Ability
GameplayAbility_Montage播放Montage并且应用设置好的GE并且结束是移除GE。没什么软用
GameplayAbility_CharacterJump使用Ability实现跳跃激活条件、跳跃方式的逻辑均调用自Character可以通过override自定义角色类中对应的函数实现扩展。
## 小部分Tasks的自动绑定机制
像UAbilityTask_WaitTargetData之类的Task会有一些函数没有被调用这些其实会被UK2Node_LatentGameplayTaskCall进行自动绑定与运行仅限蓝图在c++的话需要自己调用这些函数以及在static函数中调用ReadyForActivation();。会自动绑定运行函数与pin为
```
FName FK2Node_LatentAbilityCallHelper::BeginSpawnFuncName(TEXT("BeginSpawningActor"));
FName FK2Node_LatentAbilityCallHelper::FinishSpawnFuncName(TEXT("FinishSpawningActor"));
FName FK2Node_LatentAbilityCallHelper::BeginSpawnArrayFuncName(TEXT("BeginSpawningActorArray"));
FName FK2Node_LatentAbilityCallHelper::FinishSpawnArrayFuncName(TEXT("FinishSpawningActorArray"));
FName FK2Node_LatentAbilityCallHelper::SpawnedActorPinName(TEXT("SpawnedActor"));
FName FK2Node_LatentAbilityCallHelper::WorldContextPinName(TEXT("WorldContextObject"));
FName FK2Node_LatentAbilityCallHelper::ClassPinName(TEXT("Class"));
```

View File

@@ -0,0 +1,89 @@
## 简述
GameplayAbility框架使用GameplayTag作为各种状态判断的依据甚至使用它作为Event进行传递。
以下是actionRPG的标签设计
```
Ability能力类型使用物品、连击、随机数值技能、技能
|——item
|——Melee
|——Ranged
|——Skill
| └──GA_PlayerSkillMeteorStorm
Cooldown技能冷却存在即代表技能处于冷却状态
| └──Skill
EffectContainer
| └──Default
Event事件
| └──Montage
| |——layer
| | |——Combo
| | |——BurstPound
| | |——ChestKick
| | |——FrontalAttack
| | |——GroundPound
| | └──JumpSlam
| └──Shared
| |——UseItem
| |——UseSkill
| └──WeaponHit
Status状态
└──DamageImmune
```
## 简单用法
在编辑-项目设置-GameplayTags选项卡中可以编辑标签。
具体使用方法可以参考:
https://www.unrealengine.com/zh-CN/tech-blog/using-gameplay-tags-to-label-and-organize-your-content-in-ue4?lang=zh-CN
## 相关的Tasks类
UAbilityTask_WaitGameplayEvent与UAbilityTask_WaitGameplayTagAdded。配合SendGameplayEventToActor函数可以实现防反之类的效果。
SendGameplayEventToActor的用法可以参考actionRPG项目在WeaponActor中位于Content/Blueprint/Weapon/它的ActorBeginOverlap事件用于实现武器攻击效果。其中它会执行SendGameplayEventToActor函数给所有有效对象带有GameplayAbilityComponent添加事件标签。
## GameplayCue
介绍:
https://docs.unrealengine.com/en-US/Gameplay/GameplayAbilitySystem/GameplayAttributesAndGameplayEffects/index.html
位于窗口—GameplayCue编辑器这个编辑器可以用于创建GameplayCue但你也可以手动创建只需要使用蓝图继承GameplayCueNotify_xxx类之后再绑定GamplayCue标签即可。
GameplayCue必须使用GameplayCue开头的tag,例如GameplayCue.ElectricalSparks或者"GameplayCue.WaterSplash.Big。
你可以在蓝图或者c++中重写OnActive、WhileActive()、Removed()、Executed()进行控制。
我能找到的唯一资料就是Tom Looman大神的视频(可以算是官方教程了)
https://youtu.be/Tu5AJKNe1Ok?t=900
## 使用GameplayCue编辑器来创建
在GameplayCue编辑器中在新增按钮左边输入栏输入想要创建的标签必须以GameplayCue.开头),再点击新建即可添加成功。之后就需要为标签添加对应的处理器。
点击处理器列下方对应新增按钮之后就会新增处理器会弹出Notify创建界面以下是我对界面中说明文字的翻译
### GameplayCue通知
GameplayCue Notifies are stand alone handlers, similiar to AnimNotifies. Most GameplyCues can be implemented through these notifies. Notifies excel at handling standardized effects. The classes below provide the most common functionality needed.
GameplayCue Notifies是类似AnimNotifies的独立处理器。大多数GameplyCues都可以使用以下两个notifies实现。notifies处理标准化效果与自定义BP事件相对。下面的类提供了最常用的功能。
### GameplayCueNotifyStatic
A non instantiated UObject that acts as a handler for a GameplayCue. These are useful for one-off "burst" effects.
一个用于处理GameplayCue的非实例化的UObject适用于一次性的突发effects。
### GameplayCueNotifyActor
An instantiated Actor that acts as a handler of a GameplayCue. Since they are instantiated, they can maintain state and tick/update every frame if necessary.
一个用于处理GameplayCue的实例化的Actor。因为是实例化的所以可以维护状态并且在在必要的情况下可以在每一帧进行处理。
点击会创建AGameplayCueNotify_Actor
## 自定义BP事件
GameplayCues can also be implemented via custom events on character blueprints.
To add a custom BP event, open the blueprint and look for custom events starting with GameplayCue.*
GameplayCues 也可以通过角色蓝图上的自定义事件来实现。想要增加一个自定义蓝图事件,打开蓝图新增与标签同名的自定义事件。(这个本人没有测试成功,因为自定义事件是不能带有.的)
## 大致步骤
1、可以通过蓝图继承的方式或者GameplayCue编辑器来创建对应的notify
2、在创建的notify中设置GameplayCue Tag这一步成功Gameplay Cue Editor就能显示绑定
3、重写onActive以及其他状态函数函数。例如播放粒子效果、声音。如果需要销毁之前播放的Asset可以在onDestroy中调用reactive函数并在勾选Notify蓝图中的自动销毁
4、使用GameplayEffect在GameplayEffect蓝图中的Visible——GameplayCues中添加或者在ABility类中调用AddGameplayCueToOwner来触发Notify。
## 有关Notify总结
想要通过GameplayAbility与GameplayEffect触发需使用GameplayCueNotifyStatic、GameplayCueNotifyActor。
动画则需要使用AnimNotify与AnimNotifyState。
在actionRPG项目中全程使用AnimNotify来驱动各种效果GameplayAbility=》PlayMontage()=》AnimNotify

View File

@@ -0,0 +1,203 @@
## 前言
使用GAS实现Combo技能本人所知道的有两种思路<br>
**第一种:为每一段Combo创建一个单独的Ability定制自定义Asset与编辑器之后再通过自定义编辑器进行编辑。**
- 优点:最高的自由度,不容易出问题。
- 缺点如果有大量多段或者派生复杂的Combo那工作量将会相当爆炸。可以说除了工程量大管理麻烦没有别的缺点。
**第二种:根据GameplayEvent直接在Ability里直接实现Combo切换逻辑**
https://zhuanlan.zhihu.com/p/131381892
在此文中我分析了ActionRPG的攻击逻辑的同时自己也提出一种通过GameplayEvent实现Combat的方法。这个方法理论上没问题但实际落地的时候发现存在一个问题里面使用的PlayMontageAndWaitEvent是GameplayTasks是异步运行的而通过AnimNotify发送的SendGameplayEvent的函数确是同步函数。所以会导致PlayMontageAndWaitEvent节点接收不到任何事件的问题。经过一段事件的探索本人摸索到了另一种方案
- 优点一个起点这个Combo树一个Ability方便管理。可以尽可能地把Combo相关的逻辑都整合到一个Ability里。
- 缺点:自由度会有一定的限制;因为采用异步函数来管理事件,会导致一定的延迟。不适于格斗游戏这种对低延迟要求比较高项目。
## 2021.7.30更新
如果你使用GAS的输入绑定系统即在GiveAbility()注册时进行输入绑定参考GASDocument的4.6.2章节。可将后面介绍的ListenPlayerInputAction节点换成GAS自带的WaitInputPress或WaitInputRelease。
## 具体实现
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/ComboAbilityBP.png)
还需要设置AbilityTag与AbilityBlockTag解决狂按而导致的技能鬼畜问题。
### URPGAbilityTask_ListenPlayerInputAction
构建一个GameplayTasks来监听玩家输入,使用Character类获取InputComponent之后再进行对应Action的动态函数绑定/解绑。执行前需要判断是否是本地Controller。具体代码如下
```
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "RPGAbilityTask_ListenPlayerInputAction.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE( FOnInputActionMulticast );
UCLASS()
class RPGGAMEPLAYABILITY_API URPGAbilityTask_ListenPlayerInputAction : public UAbilityTask
{
GENERATED_UCLASS_BODY()
UPROPERTY(BlueprintAssignable)
FOnInputActionMulticast OnInputAction;
virtual void Activate() override;
UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
static URPGAbilityTask_ListenPlayerInputAction* ListenPlayerInputAction(UGameplayAbility* OwningAbility,FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume);
protected:
virtual void OnDestroy(bool AbilityEnded) override;
UPROPERTY()
FName ActionName;
UPROPERTY()
TEnumAsByte<EInputEvent> EventType;
/** 是否消耗输入消息不再传递到下一个InputComponent */
UPROPERTY()
bool bConsume;
int32 ActionBindingHandle;
};
```
```
#include "Abilities/AsyncTasks/RPGAbilityTask_ListenPlayerInputAction.h"
#include "Components/InputComponent.h"
URPGAbilityTask_ListenPlayerInputAction::URPGAbilityTask_ListenPlayerInputAction(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{}
URPGAbilityTask_ListenPlayerInputAction* URPGAbilityTask_ListenPlayerInputAction::ListenPlayerInputAction(UGameplayAbility* OwningAbility,FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume)
{
URPGAbilityTask_ListenPlayerInputAction* Task=NewAbilityTask<URPGAbilityTask_ListenPlayerInputAction>(OwningAbility);
Task->ActionName = ActionName;
Task->EventType = EventType;
Task->bConsume = bConsume;
return Task;
}
void URPGAbilityTask_ListenPlayerInputAction::Activate()
{
AActor* Owner=GetOwnerActor();
UInputComponent* InputComponent=Owner->InputComponent;
FInputActionBinding NewBinding(ActionName, EventType.GetValue());
NewBinding.bConsumeInput = bConsume;
NewBinding.ActionDelegate.GetDelegateForManualSet().BindLambda([this]()
{
if(OnInputAction.IsBound())
OnInputAction.Broadcast();
});
ActionBindingHandle=InputComponent->AddActionBinding(NewBinding).GetHandle();
}
void URPGAbilityTask_ListenPlayerInputAction::OnDestroy(bool AbilityEnded)
{
AActor* Owner = GetOwnerActor();
UInputComponent* InputComponent = Owner->InputComponent;
if (InputComponent)
{
InputComponent->RemoveActionBindingForHandle(ActionBindingHandle);
}
Super::OnDestroy(AbilityEnded);
}
```
### 控制MontageSection跳转
这里我创建一个控制跳转用Map:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/ComboMap.png)
其中Key为当前Combo名称_攻击类型。之后根据Value值进行MontageSection跳转。
### Montage偏移
为了Combo之间有更好的过渡可以通过计算进入招式派生状态的经过时间最后在Section跳转时进行偏移以实现最佳的连贯性Combo动画中需要有对应的过渡区域设计。以下是我实现的MontageSection跳转+偏移函数:
```
void URPGAbilitySystemComponent::CurrentMontageJumpToSectionWithOffset(FName SectionName,float PositionOffset)
{
UAnimInstance* AnimInstance = AbilityActorInfo.IsValid() ? AbilityActorInfo->GetAnimInstance() : nullptr;
if ((SectionName != NAME_None) && AnimInstance && LocalAnimMontageInfo.AnimMontage)
{
// AnimInstance->Montage_JumpToSection(SectionName, LocalAnimMontageInfo.AnimMontage);
FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(LocalAnimMontageInfo.AnimMontage);
if (MontageInstance)
{
UAnimMontage* Montage=LocalAnimMontageInfo.AnimMontage;
const int32 SectionID = Montage->GetSectionIndex(SectionName);
if (Montage->IsValidSectionIndex(SectionID))
{
FCompositeSection & CurSection = Montage->GetAnimCompositeSection(SectionID);
const float NewPosition = CurSection.GetTime()+PositionOffset;
MontageInstance->SetPosition(NewPosition);
MontageInstance->Play(MontageInstance->GetPlayRate());
}
}
if (IsOwnerActorAuthoritative())
{
FGameplayAbilityRepAnimMontage& MutableRepAnimMontageInfo = GetRepAnimMontageInfo_Mutable();
MutableRepAnimMontageInfo.SectionIdToPlay = 0;
if (MutableRepAnimMontageInfo.AnimMontage)
{
// we add one so INDEX_NONE can be used in the on rep
MutableRepAnimMontageInfo.SectionIdToPlay = MutableRepAnimMontageInfo.AnimMontage->GetSectionIndex(SectionName) + 1;
}
AnimMontage_UpdateReplicatedData();
}
else
{
ServerCurrentMontageJumpToSectionName(LocalAnimMontageInfo.AnimMontage, SectionName);
}
}
}
float URPGAbilitySystemComponent::GetCurrentMontagePlaybackPosition() const
{
UAnimInstance* AnimInstance = AbilityActorInfo.IsValid() ? AbilityActorInfo->GetAnimInstance() : nullptr;
if (AnimInstance && LocalAnimMontageInfo.AnimMontage)
{
FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(LocalAnimMontageInfo.AnimMontage);
return MontageInstance->GetPosition();
}
return 0.f;
}
```
```
void URPGGameplayAbility::MontageJumpToSectionWithOffset(FName SectionName,float PositionOffset)
{
check(CurrentActorInfo);
URPGAbilitySystemComponent* AbilitySystemComponent = Cast<URPGAbilitySystemComponent>(GetAbilitySystemComponentFromActorInfo_Checked());
if (AbilitySystemComponent->IsAnimatingAbility(this))
{
//调用对应函数
AbilitySystemComponent->XXXXXX();
}
}
```
### 解决因狂按而导致的技能鬼畜问题
在保证可以实现其他技能强制取消当前技能后摇的情况下,我尝试过以下方法,结果都不太理想:
- 使用带有AbilityBlockTag的GE来控制指定时间段里无法重复使用当前技能。
- 使用Ability的CoolDown功能配合GE制作公共CD效果。时间短了依然会鬼畜
最后采用给当前Ability设置标签并且把这个标签作为AbilityBlockTag来实现。取消技能后摇的效果可以通过AnimNotify调用EndAbility来实现。
### 事件处理
#### Default
处理其他没有指定的事件一般都是应用GE的。与ActionRPG不同的地方在于
1. 同一个事件第二次接收Ability会移除第一次应用的GE这是为了实现类似攻击动作霸体的效果。
2. 因为数值计算只在服务端处理所以执行前需要使用HasAuthority判断一下。
#### WeaponHit
武器其中可攻击对象时的逻辑。除此之外可以使用自定义的UObject作为EventPayloadOptionalObject传递更多的数据以此可以实现命中弱点增加伤害等效果。
#### DerivationCombat
这里其实应该是Combo但之前打错懒得改了。这里主要是设置控制Combo是否可以派生的变量。

View File

@@ -0,0 +1,11 @@
---
title: GameplayTag UPROPERTY Filter
date: 2022-09-29 23:20:58
excerpt:
tags: GAS
rating: ⭐
---
`GameplayTags` 和`GameplayTagContainers` 有可选的 `UPROPERTY` 说明符`Meta = (Categories = "GameplayCue")` ,可以用来在蓝图中实现标签的过滤,仅展示出属于父标签为`GameplayCue``GameplayTags` 。 要实现此功能也可以通过直接使用 `FGameplayCueTag` 其内部封装了一个带有`Meta = (Categories = "GameplayCue")`的 `FGameplayTag` 。
当把 `GameplayTag` 当作方法的参数时,可以通过 `UFUNCTION` specifier `Meta = (GameplayTagFilter = "GameplayCue")`完成过滤。(译者注:`GameplayTagContainer` 也已经支持Filter不再赘述

View File

@@ -0,0 +1,139 @@
## ActionRPG项目的问题
### 联机
## 攻击判定问题
### Overlap无法返回PhysicsBody的BodyIndex
因为当前物理系统使用Physx的关系所以Overlap事件中SweepResult的HitBoneName返回是的None。
### 如果取得对应的骨骼
1. 当前版本4.26,射线是可以取得对应的骨骼的。
2. 如果是角色自己跑进 Overlap空间里时返回的BodyIndex是-1但Overlap空间自己移动碰到角色时是可以正确返回BodyIndex
## 武器逻辑
### LCMCharacter中的实现
LCMCharacter里通过在构造函数里创建Mesh并且连接到角色模型的插槽
```
WeaponMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Weapon"));
WeaponMesh->SetupAttachment(GetMesh(), TEXT("Sword_2"));
```
### ActionRPG中的逻辑
创建一个WeaponActor变量之后通过调用DetachFromActor移除当前武器再调用AttachActorToComponent
在AnimNotifyState的Begin与End事件中取得Character基类取得WeaponActor变量再开启与关闭攻击判定。
## 使用GameplayTask监听输入事件
InputComponent定义于Actor.h中。
初始化输入组件
```c++
void UUserWidget::InitializeInputComponent()
{
if ( APlayerController* Controller = GetOwningPlayer() )
{
InputComponent = NewObject< UInputComponent >( this, UInputSettings::GetDefaultInputComponentClass(), NAME_None, RF_Transient );
InputComponent->bBlockInput = bStopAction;
InputComponent->Priority = Priority;
Controller->PushInputComponent( InputComponent );
}
else
{
FMessageLog("PIE").Info(FText::Format(LOCTEXT("NoInputListeningWithoutPlayerController", "Unable to listen to input actions without a player controller in {0}."), FText::FromName(GetClass()->GetFName())));
}
}
```
```c++
void UUserWidget::ListenForInputAction( FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume, FOnInputAction Callback )
{
if ( !InputComponent )
{
InitializeInputComponent();
}
if ( InputComponent )
{
FInputActionBinding NewBinding( ActionName, EventType.GetValue() );
NewBinding.bConsumeInput = bConsume;
NewBinding.ActionDelegate.GetDelegateForManualSet().BindUObject( this, &ThisClass::OnInputAction, Callback );
InputComponent->AddActionBinding( NewBinding );
}
}
void UUserWidget::StopListeningForInputAction( FName ActionName, TEnumAsByte< EInputEvent > EventType )
{
if ( InputComponent )
{
for ( int32 ExistingIndex = InputComponent->GetNumActionBindings() - 1; ExistingIndex >= 0; --ExistingIndex )
{
const FInputActionBinding& ExistingBind = InputComponent->GetActionBinding( ExistingIndex );
if ( ExistingBind.GetActionName() == ActionName && ExistingBind.KeyEvent == EventType )
{
InputComponent->RemoveActionBinding( ExistingIndex );
}
}
}
}
void UUserWidget::StopListeningForAllInputActions()
{
if ( InputComponent )
{
for ( int32 ExistingIndex = InputComponent->GetNumActionBindings() - 1; ExistingIndex >= 0; --ExistingIndex )
{
InputComponent->RemoveActionBinding( ExistingIndex );
}
UnregisterInputComponent();
InputComponent->ClearActionBindings();
InputComponent->MarkPendingKill();
InputComponent = nullptr;
}
}
```
- bConsumeInput是否应该消耗掉这次输入控制是否传递到下一个InputStack中的对象
## UE4对玩家输入的处理规则
## 有关FScopedPredictionWindowAbilityTask的网络预测逻辑
```c++
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent, IsPredictingClient());
if (IsPredictingClient())
{
// Tell the server about this
AbilitySystemComponent->ServerSetReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey(), AbilitySystemComponent->ScopedPredictionKey);
}
else
{
AbilitySystemComponent->ConsumeGenericReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, GetAbilitySpecHandle(), GetActivationPredictionKey());
}
// We are done. Kill us so we don't keep getting broadcast messages
if (ShouldBroadcastAbilityTaskDelegates())
{
OnPress.Broadcast(ElapsedTime);
}
```
- IsPredictingClient判断这个Ability的类型是否为locally predicted本地预测形如果是的就需要告诉Server。
UE4 GameplayAbilitySystem Prediction
https://zhuanlan.zhihu.com/p/143637846
### InstancingPolicy
技能执行是后通常会有一个生成技能的新对象用于定位追踪该技能。但是大多数时候技能需要频繁的创建使用可能需要会出现快速实例技能对象对性能产生一定的性能影响。AbilitySystem提供给了三种实例化技能的策略
- 按执行实例化Instanced per Execution这就是前面提到的每执行一个技能时候都会实例化一个技能对象但是如果技能被频繁的调用时候该技能就会有一定的运行开销。但是优点在于由于这个技能重新运行时候会重新生成一个实例对象因而该技能中的变量也会初始化结束技能时候不必考虑重置变量、状态等问题。如果你的技能不是很频繁使用的化可以考虑使用这种的执行策略。
- 按Actor实例化Instanced per Actor当技能首次被执行后后面每次执行这个技能时候都不会被实例化会重复使用这一个对象。这个和上面的Instanced per Execution策略相反每次执行这个技能后需要清理这个技能的变量和状态工作。这种策略适用于频繁使用某个技能使用使用可能提要执行效率。并且因为技能具有可处理变量和RPC的复制对象而不是浪费网络带宽和CPU时间在每次运行时产生新对象。
- 非实例化Non-Instanced故名思意该策略的技能被执行时候不会实例化技能对象而是技能的CDO对象。这种策略是优越于上面的两种策略的但是也有一个很大限制---这个策略要求这个技能完全都是由C++编写的因为蓝图创建图标时候需要对象实例并且即使是C++编写这个技能该技能也不能更改其成员变量、不能绑定代理、不能复制变量、不能RPC。因此这个技能在游戏中应用的相对较少但是一些简单的AI技能可以用到。
### NetExecutionPolicy
- 本地预测Local Predicted本地预测是指技能将立即在客户端执行执行过程中不会询问服务端的正确与否但是同时服务端也起着决定性的作用如果服务端的技能执行失败了客户端的技能也会随之停止并“回滚”。如果服务端和客户端执行的结果不矛盾客户端会执行的非常流畅无延时。如果技能需要实时响应可以考虑用这种预测模式。下文会着重介绍这个策略。
- 仅限本地Local Only仅在本地客户端运行的技能。这个策略不适用于专用服务器游戏本地客户端执行了该技能服务端任然没有被执行在有些情况下可能出现拉扯情况原因在于服务端技能没有被执行技能中的某个状态和客户端不一致。
- 服务器启动Server Initiated技能将在服务器上启动并PRC到客户端也执行。可以更准确地复制服务器上实际发生的情况但是客户端会因缺少本地预测而发生短暂延迟。这种延迟相对来说是比较低的但是相对于Local Predicted来说还是会牺牲一点流畅性。之前我遇到一种情况一个没有预测的技能A和一个带有预测的技能BA执行了后B不能执行 现在同时执行技能A和技能B 由于A需要等待服务器端做验证B是本地预测技能所以B的客户端会瞬间执行导致B的客户端被执行过了服务端失败出现了一些不可预料的问题了这种情况需要将技能B的网络策略修改为Server Initiated这样就会以服务端为权威运行虽然会牺牲点延迟但是也是可以在游戏接收的范围内有时候需要在此权衡。
- 仅限服务器Server Only技能将在只在服务器上运行客户端不会。服务端的技能被执行后技能修改的任何变量都将被复制并会将状态传递给客户端。缺点是比较服务端的每个影响都会由延迟同步到客户端端Server Initiated只在运行时候会有一点滞后。
## 使用GameplayTasks制作Combo技能武器的碰撞判定必须使用同步函数也就是在AnimNotify中调用

View File

@@ -0,0 +1,377 @@
# GameFeatures学习笔记
主要参考了大钊的系列文章:
- 《InsideUE5》GameFeatures架构基础用法 https://zhuanlan.zhihu.com/p/470184973
- 《InsideUE5》GameFeatures架构初始化 https://zhuanlan.zhihu.com/p/473535854
- 《InsideUE5》GameFeatures架构状态机 https://zhuanlan.zhihu.com/p/484763722
- GameFeatures插件实现了Actions执行和GameFeature的装载
- ModularGameplay插件实现AddComponent等操作
通过插件创建向导新创建的GameFeature会放在**GameFeatures**文件夹中,也必须放在**GameFeature**文件夹中。并且会创建**GameFeatureData**并且想AssetManager注册。GameFeatures的运行逻辑为调用ToggleGameFeaturePlugin=>触发GameFeatureAction=>做出对应的操作。
做出对应Action操作的Actor对象需要调用UGameFrameworkComponentManager的AddReceiver()与RemoveReceiver()进行注册与反注册。
- GameFeature缩写为GF就是代表一个GameFeature插件。
- CoreGame特意加上Core是指的把游戏的本体Module和GameFeature相区分开即还没有GF发挥作用的游戏本体。
- UGameFeatureData缩写为GFD游戏功能的纯数据配置资产用来描述了GF要做的动作。
- UGameFeatureAction单个动作缩写GFA。引擎已经内建了几个Action我们也可以自己扩展。
- UGameFeaturesSubsystem缩写为GFSGF框架的管理类全局的API都可以在这里找到。父类为UEngineSubsystem。
- UGameFeaturePluginStateMachine缩写为GFSM每个GF插件都关联着一个状态机来管理自身的加载卸载逻辑。
- UGameFrameworkComponentManager缩写为GFCM支撑AddComponent Action作用的管理类记录了为哪些Actor身上添加了哪些Component以便在GF卸载的时候移除掉。
## 初始化
### UGameFeaturesProjectPolicies
GameFeature的加载规则类。UE实现一个了默认类为UDefaultGameFeaturesProjectPolicies。默认是会加载所有的GF插件。如果我们需要自定义自己的策略比如某些GF插件只是用来测试的后期要关闭掉就可以继承重载一个自己的策略对象在里面可以实现自己的过滤器和判断逻辑。
- GetPreloadAssetListForGameFeature返回一个GF进入Loading要预先加载的资产列表方便预载一些资产比如数据配置表之类的。
- IsPluginAllowed可重载这个函数来进一步判断某个插件是否允许加载可以做更细的判断。
可以在项目设置中修改所使用的类。大钊介绍了Additional Plugin Metadata Keys的用法
>举个例子假设2.0版本的游戏要禁用掉以前1.0版本搞活动时的一个GF插件可以继承定义一个我们自己的Policy对象然后在Init里的过滤器里实现自己的筛选逻辑比如截图里就示例了根据uplugin里的MyGameVersion键来指定版本号然后对比。这里要注意的是**要先在项目设置里配置上Additional Plugin Metadata Keys**才能把uplugin文件里的自定义键识别解析到PluginDetails.AdditionalMetadata里才可以进行后续的判断。至于要添加什么键就看各位自己的项目需要了。
![](https://pic3.zhimg.com/v2-eb32702550e4d8517b1bcdbc08620c8a_r.jpg)
![](https://pic1.zhimg.com/v2-30bb4136f249d6a50ea0121bd53b4c04_r.jpg)
```c++
void UMyGameFeaturesProjectPolicies::InitGameFeatureManager()
{
auto AdditionalFilter = [&](const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions) -> bool
{ //可以自己写判断逻辑
if (const FString* myGameVersion = PluginDetails.AdditionalMetadata.Find(TEXT("MyGameVersion")))
{
float verison = FCString::Atof(**myGameVersion);
if (verison > 2.0)
{
return true;
}
}
return false;
};
UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter); //加载内建的所有可能为GF的插件
}
```
GF插件的识别是从解析.uplugin文件开始的。我们可以手动编辑这个json文件来详细的描述GF插件。这里有几个键值得一说
- BuiltInAutoState这个GF被识别后默认初始的状态共有4种。如果你设为Active就代表这个GF就是默认激活的。关于GF的状态我们稍后再详细说说。
- AdditionalMetadata这个刚才已经讲过了被Policy识别用的。
- PluginDependencies我们依然可以在GF插件里设置引用别的插件别的插件可以是普通的插件也可以是另外的GF插件。这些被依赖的插件会被递归的进行加载。这个递归加载的机制跟普通的插件机制也是一致的。
- ExplicitlyLoaded=true必须为true因为GF插件是被显式加载的不像其他插件可能会默认开启。
- CanContainContent=true必须为true因为GF插件毕竟至少得有GFD资产。
![](https://pic1.zhimg.com/v2-96d6ed62d48871fe9f6d1cda3b297af4_r.jpg)
## 状态机
对每一个GF而言我们在使用的过程中能够使用到的GF状态就4个Installed、Registered、Loaded、Active。这4个状态之间可以双向转换以进行加载卸载。
也要注意GF状态的切换流程是一条双向的流水线可以往加载激活的方向前进在下图上是用黑色的箭头来表示也可以往失效卸载的方向走图上是红色的线表示。而箭头上的文字其实就是一个个GFS类里已经提供的GF加载卸载API。 双向箭头中间的圆角矩形表示的是状态绿色的状态是我们能看见的但其实内部还有挺多过渡状态的。过渡状态的概念后面也会解释。值得注意的是UE5预览版增加了一个Terminal状态可以把整个插件的内存状态释放掉。
![](https://pic1.zhimg.com/v2-d67731c19467330cff71f51e2698705c_r.jpg)
在LoadBuiltInGameFeaturePlugin的最后一步GFS会为每一个GF创建一个UGameFeaturePluginStateMachine对象用来管理内部的GF状态切换。
```c++
void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
{
//...省略其他代码
UGameFeaturePluginStateMachine* StateMachine = GetGameFeaturePluginStateMachine(PluginURL, true);//创建状态机
const EBuiltInAutoState InitialAutoState = (BehaviorOptions.AutoStateOverride != EBuiltInAutoState::Invalid) ? BehaviorOptions.AutoStateOverride : PluginDetails.BuiltInAutoState;
const EGameFeaturePluginState DestinationState = ConvertInitialFeatureStateToTargetState(InitialAutoState);
if (StateMachine->GetCurrentState() >= DestinationState)
{
// If we're already at the destination or beyond, don't transition back
LoadGameFeaturePluginComplete(StateMachine, MakeValue());
}
else
{
StateMachine->SetDestinationState(DestinationState, FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::LoadGameFeaturePluginComplete));//更新到目标的初始状态
}
//...省略其他代码
}
```
FGameFeaturePluginState为所有状态机的基础结构体所有派生结构体都会实现所需的虚函数。
```c++
struct FGameFeaturePluginState
{
FGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties) : StateProperties(InStateProperties) {}
virtual ~FGameFeaturePluginState();
/** Called when this state becomes the active state */
virtual void BeginState() {}
/** Process the state's logic to decide if there should be a state transition. */
virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) {}
/** Called when this state is no longer the active state */
virtual void EndState() {}
/** Returns the type of state this is */
virtual EGameFeaturePluginStateType GetStateType() const { return EGameFeaturePluginStateType::Transition; }
/** The common properties that can be accessed by the states of the state machine */
FGameFeaturePluginStateMachineProperties& StateProperties;
void UpdateStateMachineDeferred(float Delay = 0.0f) const;
void UpdateStateMachineImmediate() const;
void UpdateProgress(float Progress) const;
private:
mutable FTSTicker::FDelegateHandle TickHandle;
};
```
初始化状态机时候会创建所有状态UGameFeaturePluginStateMachine::InitStateMachine())。
### 状态机更新流程
SetDestinationState()=>UpdateStateMachine()
UpdateStateMachine()除了判断与Log之外主要执行了:
- 调用当前状态的UpdateState()
- 调用当前状态的EndState()
- 调用新状态的BeginState()
#### 检查存在性
首先GF插件的最初始状态是Uninitialized很快进入UnknownStatus标明还不知道该插件的状态。然后进入CheckingStatus阶段这个阶段主要目的就是检查uplugin文件是否存在一般的GF插件都是已经在本地的可以比较快通过检测。
![](https://pic2.zhimg.com/v2-429db98dfa46f7f387de6841aae718d9_r.jpg)
#### 加载CF C++模块
下一个阶段就是开始尝试加载GF的C++模块。这个阶段的初始状态是Installed这个我用绿色表示表明它是个目标状态区分于过渡状态。目标状态意思是在这个状态可以停留住直到你手动调用API触发迁移到下一个状态比如你想要注册或激活这个插件就会把Installed状态向下一个状态转换。卸载的时候往反方向红色的线前进。接着往下
- Mounting阶段内部会触发插件管理器显式的加载这个模块因此会加载dll触发StartupModule。在以前Unmounting阶段并不会卸载C++因此不会调用ShutdownModule。意思就是C++模块一经加载就常驻在内存中了。但在UE5预览版中加上了这一步因此现在Unmounting已经可以卸载掉插件的dll了。
- WaitingForDependencies会加载之前uplugin里依赖的其他插件模块递归加载等待所有其他的依赖项完成之后才会进入下一个阶段。这点其实跟普通的插件加载策略是一致的因此GF插件本质上其实就是以插件的机制在运作只不过有些地方有些特殊罢了。
![](https://pic1.zhimg.com/v2-823dae0fa911b90c334f860d436b53f8_r.jpg)
#### 加载GameFeatureData
在C++模块加载完成之后下一步就要开始把GF自身注册到GFS里面去。其中最重要的一步是在Registering的时候加载GFD资产会触发UGameFeaturesSubsystem::LoadGameFeatureData(BackupGameFeatureDataPath);的调用从而完成GFD加载。
![](https://pic3.zhimg.com/v2-9abd72961b0ae6b08f24f22276f370ca_r.jpg)
#### 预加载资产和配置
下一个阶段就是进行加载了。Loading阶段会开始加载两种东西一是插件的运行时的ini(如…/LearnGF/Saved/Config/WindowsEditor/MyFeature.ini)与C++ dll,另外一项是可以预先加载一些资产资产列表可以由Policy对象根据每个GF插件的GFD文件来获得因此我们也可以重载GFD来添加我们想要预先加载的资产列表。其他的资产是在激活阶段根据Action的执行按需加载的。
![](https://pic2.zhimg.com/v2-95b5552a06842280cc361cf929b82351_r.jpg)
#### 激活生效
在加载完成之后我们就可以激活这个GF了。Activating会为每个GFD里定义的Action触发OnGameFeatureActivating而Deactivating触发OnGameFeatureDeactivating。激活和反激活是Action真正做事的时机。
![](https://pic4.zhimg.com/v2-645173c77bfeb968145f7eb53576e7bf_r.jpg)
### 详细状态切换图
位于GameFeaturePluginStateMachine.h
```c++
/*
+--------------+
| |
|Uninitialized |
| |
+------+-------+
+------------+ |
| * | |
| Terminal <-------------~------------------------------
| | | |
+--^------^--+ | |
| | | |
| | +------v-------+ |
| | | * | |
| ----------+UnknownStatus | |
| | | |
| +------+-------+ |
| | |
| +-----------v---+ +--------------------+ |
| | | | ! | |
| |CheckingStatus <-----> ErrorCheckingStatus+-->|
| | | | | |
| +------+------^-+ +--------------------+ |
| | | |
| | | +--------------------+ |
---------- | | | ! | |
| | --------> ErrorUnavailable +----
| | | |
| | +--------------------+
| |
+-+----v-------+
| * |
----------+ StatusKnown |
| | |
| +-^-----+---+--+
| | |
| ------~---------~-------------------------
| | | | |
| +--v-----+---+ +-v----------+ +-----v--------------+
| | | | | | ! |
| |Uninstalling| |Downloading <-------> ErrorInstalling |
| | | | | | |
| +--------^---+ +-+----------+ +--------------------+
| | |
| +-+---------v-+
| | * |
----------> Installed |
| |
+-^---------+-+
| |
------~---------~--------------------------------
| | | |
+--v-----+--+ +-v---------+ +-----v--------------+
| | | | | ! |
|Unmounting | | Mounting <---------------> ErrorMounting |
| | | | | |
+--^-----^--+ +--+--------+ +--------------------+
| | |
------~----------~-------------------------------
| | |
| +--v------------------- + +-----v-----------------------+
| | | | ! |
| |WaitingForDependencies <---> ErrorWaitingForDependencies |
| | | | |
| +--+------------------- + +-----------------------------+
| |
------~----------~-------------------------------
| | | |
+--v-----+----+ +--v-------- + +-----v--------------+
| | | | | ! |
|Unregistering| |Registering <--------------> ErrorRegistering |
| | | | | |
+--------^----+ ++---------- + +--------------------+
| |
+-+--------v-+
| * |
| Registered |
| |
+-^--------+-+
| |
+--------+--+ +--v--------+
| | | |
| Unloading | | Loading |
| | | |
+--------^--+ +--+--------+
| |
+-+--------v-+
| * |
| Loaded |
| |
+-^--------+-+
| |
+--------+---+ +-v---------+
| | | |
|Deactivating| |Activating |
| | | |
+--------^---+ +-+---------+
| |
+-+--------v-+
| * |
| Active |
| |
+------------+
*/
```
## AddComponents
主要逻辑位于ModularGameplay模块中的UGameFrameworkComponentManager。GFCM内部的实现还是蛮复杂和精巧的可以做到在一个GF激活后会把激活前已经存在场景中Actor还有激活后新生成的Actor都会被正确的添加上Component。这个顺序无关的逻辑是怎么做到的呢关键的逻辑分为两大部分:
### AddReceiver的注册
核心逻辑位于AddReceiverInternal()。我觉得大钊这里搞错了。
1. 判断Actor是否存在与当前关卡中。
2. 递归这个Actor类的父类直到AActor。
3. 从TMap<FComponentRequestReceiverClassPath, TSet<UClass*>> ReceiverClassToComponentClassMap中寻找这个类对应的Component UClass* 集。
4. 如果Component类有效则调用CreateComponentOnInstance(),创建并且挂载到Actor上并将信息放入TMap<UClass*, TSet<FObjectKey>> ComponentClassToComponentInstanceMap中。
5. 查询TMap<FComponentRequestReceiverClassPath, FExtensionHandlerEvent> ReceiverClassToEventMap并执行委托。
### AddComponents
每个GFA在Activating的时候都会调用OnGameFeatureActivating。而对于UGameFeatureAction_AddComponents这个来说其最终会调用到AddToWorld。在判断一番是否是游戏世界是否服务器客户端之类的配置之后。最后真正发生发生的作用是Handles.ComponentRequestHandles.Add(GFCM->AddComponentRequest(Entry.ActorClass, ComponentClass));
AddComponentRequest会增加ReceiverClassToComponentClassMap中的项。
1. 创建FComponentRequest并添加RequestTrackingMap中的项
2. 获取LocalGameInstance以及World,并且在World中遍历指定的Actor(ReceiverClass)
3. 如果Actor有效则调用CreateComponentOnInstance(),创建并且挂载到Actor上并将信息放入TMap<UClass*, TSet<FObjectKey>> ComponentClassToComponentInstanceMap中。
值得注意的是:
1. RequestTrackingMap是TMap<FComponentRequest, int32>的类型可以看到Key是ReceiverClassPath和ComponentClassPtr的组合那为什么要用int32作为Value来计数呢这是因为在不同的GF插件里有可能会出现重复的ActorClass-ComponentClass的组合比如GF1和GF2里都注册了A1-C2的配置。那在卸载GF的时候我们知道也只有把两个GF1和GF2统统都卸载之后这个A1-C2的配置才失效这个时候这个int32计数才=0。因此才需要有个计数来记录生效和失效的次数。
2. ReceiverClassToComponentClassMap会记录ActorClass-多个ComponentClass的组合其会在上文的AddReceiver的时候被用来查询。
3. 同样会发现根据代码逻辑ensureMsgf这个报错也只在WITH_EDITOR的时候才生效。在Runtime下依然会不管不顾的根据GFA里的配置为相应ActorClass类型的所有Actor实例添加Component。因此这个时候我们明白AddReceiver的调用准确的说不过是为了GF生效后为新Spawn的Actor添加一个依然能添加相应组件的机会。
4. 返回值为何是TSharedPtr<FComponentRequestHandle>又为何要Add进Handles.ComponentRequestHandles其实这个时候就涉及到一个逻辑当GF失效卸载的时候之前添加的那些Component应该怎么卸载掉所以这个时候就采取了一个办法UGameFeatureAction_AddComponents这个Action实例里不止一个不同的GF会生成不同的UGameFeatureAction_AddComponents实例记录着由它创建出来的组件请求当这个GF被卸载的时候会触发UGameFeatureAction_AddComponents的析构继而释放掉TArray<TSharedPtr<FComponentRequestHandle>> ComponentRequestHandles;而这是个智能指针只会在最后一个被释放的时候其实就是最后一个相关的GF被卸载时候,才会触发FComponentRequestHandle的析构继而在GFCM里真正的移除掉这个ActorClass-ComponentClass的组合然后在相应的Actor上删除Component实例。
### 一套UGameFrameworkComponent
UE5增加了UPawnComponent父类为UGameFrameworkComponentModularGameplay模块中的文件。主要是增加了一些Get胶水函数。
#### Component
Component里面一般会写的逻辑有一种是会把Owner的事件给注册进来比如为Pawn添加输入绑定的组件会在UActorComponent的OnRegister的时候把OwnerPawn的Restarted和ControllerChanged事件注册进来监听以便在合适时机重新应用输入绑定或移除。这里我是想向大家说明这也是编写Component的一种常用的范式提供给大家参考。
```c++
void UPlayerControlsComponent::OnRegister()
{
Super::OnRegister();
UWorld* World = GetWorld();
APawn* MyOwner = GetPawn<APawn>();
if (ensure(MyOwner) && World->IsGameWorld())
{
MyOwner->ReceiveRestartedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnPawnRestarted);
MyOwner->ReceiveControllerChangedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnControllerChanged);
// If our pawn has an input component we were added after restart
if (MyOwner->InputComponent)
{
OnPawnRestarted(MyOwner);
}
}
}
```
#### Actor
- 蓝图中在BeginPlay与EndPlay事件中调用GameFrameworkComponentManager的AddReceiver与RemoveReceived。
- c++ 可以参考《古代山谷》的ModularGameplayActors。里面定义的是一些更Modular的Gameplay基础Actor重载的逻辑主要有两部分一是把自己注册给GFCM二是把自己的一些有用的事件转发给GameFeature的Component比如截图上的ReceivedPlayer和PlayerTick。
- ![](https://pic3.zhimg.com/v2-d380d507c096b8b2ce14ab63c330f442_r.jpg)
## 扩展
### Action扩展
古代山谷额外实现的GFA有AddAbility、AddInputContextMapping、AddLevelInstance、AddSpawnedActor、AddWorldSystem、WorldActionBase
在实现Action的时候有一个值得注意的地方是因为GFS是继承于EngineSubsystem的因此Action的作用机制也是Engine的因此在编辑器里播放游戏和停止游戏这些Action其实都是在激活状态的。因此特意有定义了一个WordActionBase的基类专门注册了OnStartGameInstance的事件用来在游戏启动的时候执行逻辑。当然在真正用的时候还需要通过IsGameWorld来判断哪个世界是要发挥作用的世界。
### GameFeatrues依赖机制
普通的插件一般是被CoreGame所引用用来实现CoreGame的支撑功能。而GameFeature是给已经运行的游戏注入新的功能的因此CoreGame理论上来说应该对GameFeature的存在和实现一无所知所以CoreGame就肯定不能引用GF而是反过来被GF所引用。资产的引用也是同理GF的资产可以引用CoreGame的但不能被反过来引用。
### 可继承扩展的类
- UGameFeaturesProjectPolicies决定是否加载GF的策略CoreGame
- UGameFeatureStateChangeObserver注册到GFS里监听GF状态变化CoreGame
- GameFeatureData为GF添加更多描述数据 UGameFeaturesSubsystemSettings::DefaultGameFeatureDataClass CoreGame
- GameFeatureAction更多ActionCoreGame&GF
- GameFrameworkComponent更多Component CoreGame&GF
- ModularActor更多Actor CoreGame&GF
- IAnimLayerInterface改变角色动画 CoreGame&GF
### GameFeature模块协作
一些朋友可能会问一个问题一个游戏玩法用到的模块还是挺多的GameFeature都能和他们协作进行逻辑的更改吗下面这个图我列了一些模块和GameFeature的协作交互方式大家可以简单看一下。如果有别的模块需要交互还可以通过增加新的Action类型来实现。
![](https://pic1.zhimg.com/80/v2-31547d8ae95b2ea17018200c90b01e2c_720w.jpg)
### CoreGame预留好逻辑注入点
GameFeature的出现虽然说大大解耦了游戏玩法和游戏本体之间的联系CoreGame理论上来说应该对GF一无所知极致理想情况下CoreGame也不需要做任何改变。但GF明显还做不到这点还是需要你去修改CoreGame的一些代码事先预留好逻辑的注入点。要修改的点主要有这些
原先的Gameplay Actor修改为从之前的Modular里继承或者自己手动添加AddReceiver的调用。
在动画方面可以利用Anim Layer Interface在GF里可以定义不同的动画蓝图来实现这个接口。之后在组件里Link Anim Class Layers绑定上就可以修改动画了。
关于数据配置既可以用AddDataRegistrySource添加数据源到CoreGame里现有的DataRegistry里也可以在GF里用AddDataRegistry新增新的数据注册表。
至于其他的模块部分例如UI、输入这些之前已经讲过可以用Action和Component的配合来做。
移植步骤:
创建Plugins/GameFeatures目录
创建.uplugin文件
资产
移动资产到新的GF目录
在原目录Fixup redirectors
修复所有资产验证问题
代码
创建.Build.cs
迁移项目代码到GF下的Public/Private
修复include路径错误
修复代码引用错误
在DefaultEngine.ini里加CoreRedirects +ClassRedirects=(OldName="/Script/MyGame.MyClass", NewName="/Script/MyFeature.MyClass"
### Rethink in GF
- 首先面对一个项目你需要去思考哪些游戏功能应该可以拆分成GF哪些是游戏的基本机制哪些是可以动态开关的功能就是确定哪些东西应该抽出来形成一个GF。
- Actor逻辑拆分到Component重新以(组件==功能)的方式来思考而不是所有逻辑都堆在Actor中。然后这些逻辑在之前往往是直接写在各种Actor身上现在你需要重新以(组件==功能)的方式来思考在以前我们提倡Component是用来实现相对机械的独立于游戏逻辑的基础功能的现在我们为了适应GameFeature就需要把一部分Component用来单纯的当做给Actor动态插拔的游戏逻辑实现了。因此你需要把这些逻辑从Actor拆分到Component中去并做好事件回调注册和转发的相应胶水衔接工作。
- 在Gameplay的各个方面考虑注入逻辑的可能数据、UI、Input、玩法等。如果你是在做新的项目或者玩法你在一开始设计的时候就要预想到未来支持GameFeature的可能留好各种逻辑注入点。
- 进行资产区域划分哪些是CoreGame哪些是GameFeature。当然在资产部分要进行区域划分哪些是CoreGame用到的资产哪些是只在Game Feature里使用的资产各自划分到不同的文件夹里去。
- 考虑GF和CoreGame的通用通信机制事件总线、分发器、消息队列等。在某些时候你可能光是利用Action和Component还不够为了解耦CoreGame和GF你可能还会需要用到一些通用的通信机制比如事件总线分发器消息队列之类的。
- PAK和DLC是对游戏主体的补充GF是动态装载卸载功能虽然也可通过PAK动态加载。自然的有些人可能会想到GF跟Pak和dlc有一点点像是不是可以结合一下呢。这里也注意要辨析一下pak和dlc是在资产级别的补丁一般来说是在资产内容上对游戏本体的补充。而GF是在逻辑层面强调游戏功能的动态开关。但是GF也确实可以打包成一个pak来进行独立的分发下载加载因此GF也是可以配合热更新来使用的。