148 lines
9.9 KiB
Markdown
148 lines
9.9 KiB
Markdown
|
## 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为0,UseAbility2为1,UseAbility3为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,这样对工程开发会更加友好。
|