BlueRose
文章97
标签28
分类7
UGameplayAbility

UGameplayAbility

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

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

有关InputPressed与InputReleased

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

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