## 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 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 ApplyEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec); /** 调用MakeEffectContainerSpec生成FRPGGameplayEffectContainerSpec,再让Effect对目标生效 */ UFUNCTION(BlueprintCallable, Category = Ability, meta = (AutoCreateRefTerm = "EventData")) virtual TArray 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为0,UseAbility2为1,UseAbility3为2) ``` FGameplayAbilitySpec(TSubclassOf InAbilityClass, int32 InLevel, int32 InInputID, UObject* InSourceObject) ``` 4. 在项目设置——输入中,按照所设置的输入id所对应的枚举,添加ActionMapping。例如:UseAbility1、UseAbility2、UseAbility3。 这样做可以达到对Control解耦的目的。因为你调用GiveAbility或者ClearAbility时会自动绑定输入。而不需要手动去角色类或者控制类中手动设置。 ### 有关InputPressed与InputReleased 执行了上述输入绑定措施,你就可以通过重写InputPressed与InputReleased,来执行对应的逻辑。 调用这两个虚函数的逻辑在UAbilitySystemComponent中的AbilitySpecInputPressed与AbilitySpecInputReleased。 个人认为这些逻辑还会有可能会在蓝图中编写,所以新继承的类可以创建新的BlueprintNativeEvent,这样对工程开发会更加友好。