BlueRoseNote/03-UnrealEngine/Gameplay/GAS/(3)UGameplayAbility.md
2023-06-29 11:55:02 +08:00

9.9 KiB
Raw Permalink Blame History

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.
};
  1. 在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"));
}
  1. 在执行GiveAbility函数注册能力设置输入id。输入id为枚举类中对应的枚举值。例如本案例中UseAbility1为0UseAbility2为1UseAbility3为2
FGameplayAbilitySpec(TSubclassOf<UGameplayAbility> InAbilityClass, int32 InLevel, int32 InInputID, UObject* InSourceObject)
  1. 在项目设置——输入中按照所设置的输入id所对应的枚举添加ActionMapping。例如UseAbility1、UseAbility2、UseAbility3。

这样做可以达到对Control解耦的目的。因为你调用GiveAbility或者ClearAbility时会自动绑定输入。而不需要手动去角色类或者控制类中手动设置。

有关InputPressed与InputReleased

执行了上述输入绑定措施你就可以通过重写InputPressed与InputReleased来执行对应的逻辑。 调用这两个虚函数的逻辑在UAbilitySystemComponent中的AbilitySpecInputPressed与AbilitySpecInputReleased。

个人认为这些逻辑还会有可能会在蓝图中编写所以新继承的类可以创建新的BlueprintNativeEvent这样对工程开发会更加友好。