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);
代码很简单,大致可以归纳为:
- 维护一个GameplayTag与RPGGameplayEffectContainer的映射表EffectContainerMap。
- 创建FRPGGameplayEffectContainerSpec。(可以通过GameplayTag查找EffectContainerMap或者通过指定的RPGGameplayEffectContainer)。
- 通过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里也没有使用这个函数,可以看得出即使不用这个函数也不会影响该框架别的功能(可能会对联机游戏产生影响)。
经过@键盘侠·伍德 的指导,我才知道用法:
声明一个用于映射输入的枚举类
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. };
- 在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"));
}
- 在执行GiveAbility函数(注册能力)时,设置输入id。输入id为枚举类中对应的枚举值。(例如本案例中UseAbility1为0,UseAbility2为1,UseAbility3为2)
FGameplayAbilitySpec(TSubclassOf<UGameplayAbility> InAbilityClass, int32 InLevel, int32 InInputID, UObject* InSourceObject)
- 在项目设置——输入中,按照所设置的输入id所对应的枚举,添加ActionMapping。例如:UseAbility1、UseAbility2、UseAbility3。
这样做可以达到对Control解耦的目的。因为你调用GiveAbility或者ClearAbility时会自动绑定输入。而不需要手动去角色类或者控制类中手动设置。
有关InputPressed与InputReleased
执行了上述输入绑定措施,你就可以通过重写InputPressed与InputReleased,来执行对应的逻辑。
调用这两个虚函数的逻辑在UAbilitySystemComponent中的AbilitySpecInputPressed与AbilitySpecInputReleased。
个人认为这些逻辑还会有可能会在蓝图中编写,所以新继承的类可以创建新的BlueprintNativeEvent,这样对工程开发会更加友好。