因为本人正在学习虚幻四当中的Gameplay Ability System系统,发现在网上的中文教程较少,于是希望将自己学习的过程和方法记录下来,也希望可以帮助到一些有需要的人。当然,因为我也是刚刚学习,所以这篇教程会有很多问题和缺陷,也请读者理解。
注意使用游戏能力系统必须用到C++,所以我这里默认看这篇文章的老哥具备一定的虚幻四基础知识了。废话不多说,开始!
这一篇文章参考了Youtube的视频视频链接
1. 添加GAS系统
使用GAS系统首先需要在Plugins中Enable插件Gameplay Abilities
然后在ProjectName.Build.cs中,添加三个依赖,分别是GameplayAbilities, GameplayTasks, GameplayTags
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore",
"HeadMountedDisplay", "GameplayAbilities", "GameplayTasks", "GameplayTags" });
然后重新build solution
2. 然后理解一下GAS系统中几个基本类型的作用:
(1)AbilitySystemComponent(ASC)是一个component,负责处理技能系统的所有交互。可以把它理解为游戏技能系统的心脏。
(2)Gameplay Tags是一系列层次化的标签,如果是武器的攻击,可以定义为Weapon.Hit。可以通过这些标签设置各种关系,比如拥有魔法抗性标签的不会被魔法攻击标签的造成伤害。也可以用Gameplay Cue来响应对应Tage的GE。
(3)Attributes和Attribute Set中负责定义技能系统中的属性,包括Health, Stamina等。通常只能被Gameplay Effect修改,包括角色属性值的初始化。
(4)Gameplay Effects(GE)是Abilities改变自己或别人的Attributes和Gameplay Tags的方法,比如伤害,生命值恢复等几乎一切修改数据的方法,它本身也是一个数据类型,不应该有逻辑,只表示如何修改,修改多少等数据信息。
(5)Gameplay Abilities表示游戏中Actor所能做得行动,比如射击,近战攻击等。它的执行次序如下。
(6)Ability Tasks,技能任务
(7)Gameplay Cues,当Gameplay Effect被成功应用后,配置在Gameplay Tags的所有Gameplay Cues都会被触发。
下图是我理解的GAS系统的部分关系图,只是大致的关系与个人理解,并不准确,参考一下即可。
3. 接下来开始添加所需要的类,在虚幻四中新建Ability System Component, Gameplay Ability和一个Attribute Set。我这里命名都是GAS。
4. 然后打开visual studio(Rider)。在ProjectName.h中,添加enum类
UENUM(BlueprintType)
enum class EGASAbilityInputID: uint8
{
None,
Confirm,
Cancel,
Punch
};
5.然后定义角色的Attribute Set,在这里我希望角色拥有生命值。
首先定义一个macro方法,这个方法方便于修改和初始化属性值,可以直接生成某个Attribute属性的getter, setter等方法。这个方法的实现在AttributeSet.h中,可以进入查看,在最底部的位置,直接复制过来即可。
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
然后声明构造函数和GetLifeTimeReplicatiedProps方法。
UGASAttributeSet();
/** Returns properties that are replicated for the lifetime of the actor channel */
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
接下来定义生命值Attribute。ReplicatedUsing表示该函数在属性值发生修改后就会被调用。FGameplayAttributeData 用于创建Attribute,它作为一个Struct,在技能系统中被建议使用。具体实现也在AttributeSet.h中,可自行查看。
UPROPERTY(BlueprintReadOnly, Category="Attributes", ReplicatedUsing=OnRep_Health)
FGameplayAttributeData Health;
//这里使用了之前定义的macro方法
ATTRIBUTE_ACCESSORS(UGASAttributeSet, Health);
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
然后在cpp中生成这些函数的实现。
OnRep_Health直接使用了Attribute.h中,用于处理被modify,修改了的attribute的方法。
void UGASAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGASAttributeSet, Health, OldHealth);
}
GetLifetimeReplicatedProps 的实现(这个函数的作用我还不太理解,但教程里都做了相应的要求实现)
UGASAttributeSet::UGASAttributeSet()
{
}
/** Allows gamecode to specify RepNotify condition*/
void UGASAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UGASAttributeSet, Health, COND_None, REPNOTIFY_Always);
}
Attribute Set的代码就完成了。
6.然后在GASGameplayAbility.h中。声明一个能力输入的enum变量,该enum类我们之前在第四步实现了。
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="Ability")
EGASAbilityInputID AbilityInputID = EGASAbilityInputID::None;
7.还需要写代码的最后一步啦,也是最长的一步,打开自己的character.h。先继承AbilitySystem接口
#include "AbilitySystemInterface.h"
class AGASCharacter : public ACharacter, public IAbilitySystemInterface
在character中需要一个AbilitySystemComponent(GAS系统的心脏)和AttributeSet(attributes集合)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera, meta = (AllowPrivateAccess = "true"))
class UGASAbilitySystemComponent* AbilitySystemComponent;
UPROPERTY()
class UGASAttributeSet* Attributes;
还需要重写一个方法,返回character的AbilitySystemComponent
virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
UAbilitySystemComponent* AGASCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
在角色的构造函数中,创建AbilitySystemComponent和Attribute Set
AbilitySystemComponent = CreateDefaultSubobject<UGASAbilitySystemComponent>("AbilitySystem");
AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Minimal);
Attributes = CreateDefaultSubobject<UGASAttributeSet>("Attributes");
现在如果Build Solution,就可以在角色的Movement component下看到AbilitySystemComponent
但是现在动作系统没有Ability可以被激活,也没有和输入绑定。
首先我们需要实现初始化动作系统可以使用的abilities。需要一个DefaultAbilities将这些能力“给予”动作系统。
然后对角色的attributes赋予初值,由于动作系统对attributes的修改都需要借助gameplay effect,即使是初始化,所以我们需要一个DefaultAttributeEffect,初始化Attribute
在public中定义两个对象。之后会在蓝图里赋值。
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="GAS")
TSubclassOf<class UGameplayEffect> DefaultAttributeEffect;
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="GAS")
TArray<TSubclassOf<class UGASGameplayAbility>> DefaultAbilities;
有了DefaultAttributeEffect后。我们需要使用默认的gameplay effect初始化角色的属性值。
virtual void InitializeAttributes();
实现
// 初始化Attributes
void AGASCharacter::InitializeAttributes()
{
if(AbilitySystemComponent && DefaultAttributeEffect)
{
FGameplayEffectContextHandle EffectContext = AbilitySystemComponent->MakeEffectContext();
EffectContext.AddSourceObject(this);
FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(DefaultAttributeEffect, 1, EffectContext);
if(SpecHandle.IsValid())
{
FActiveGameplayEffectHandle GEHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
}
我觉得要理解功能看蓝图应该更加容易一点,以上的功能在蓝图中的实现如下。
FGameplayEffectContextHandle描述:
/** Handle that wraps a FGameplayEffectContext or subclass, to allow it to be polymorphic and replicate properly*/
FGameplayEffectSpsecHandle在虚幻四的中的描述如下:
/** Allows blueprints to generate a GameplayEffectSpec once and then reference it by handle, to apply it multiple times/multiple targets. */
最后一个函数ApplyGameplayEffectSpecToSelf,将创建的Gameplay effect作用于目标,在这里通过作用于自己来进行初始化属性。
接下来还需要将DefaultAbilities赋予给技能系统。
virtual void GiveAbilities();
实现
void AGASCharacter::GiveAbilities()
{
if(HasAuthority() && AbilitySystemComponent)
{
for(TSubclassOf<UGASGameplayAbility>& StartupAbility: DefaultAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, 1,static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
}
}
FGameplayAbilitySpec需要一个GameplayAbility作为输入,其余输入:
通过InitAbilityActorInfo告诉动作系统,谁才是他的主人!要听主人的命令。
然后Ability和Attribute的初始化就完成了。
最后需要将动作系统和输入绑定。
绑定函数需要两个参数,一个是角色Input Component的指针,一个是FGameplayAbilityInputBinds, FGameplayAbilityInputBinds的构造函数至少需要3个参数:前两个参数是字符串,用于定义”Confirm”和”Cancel”的输入命令,第三个参数为所有输入的UEnum,就是之前我们在头文件中定义的那个。我们在OnRep_PlayerState()中完成绑定。
virtual void OnRep_PlayerState() override;
实现
void AGASCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
AbilitySystemComponent->InitAbilityActorInfo(this, this);
InitializeAttributes();
if(AbilitySystemComponent && InputComponent)
{
const FGameplayAbilityInputBinds Binds("Confirm", "Cancel", "EGASAbilityInputID",
static_cast<int32>(EGASAbilityInputID::Confirm), static_cast<int32>(EGASAbilityInputID::Cancel));
AbilitySystemComponent->BindAbilityActivationToInputComponent(InputComponent, Binds);
}
}
最后重写PossessedBy函数,因为Character的主人实际上也是一个Character Controller,所以需要让controller来当技能系统的“主人”
virtual void PossessedBy(AController* NewController) override;
函数实现
void AGASCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
// Server Gas Init
AbilitySystemComponent->InitAbilityActorInfo(this, this);
InitializeAttributes();
GiveAbilities();
}
8. OK,到这一步,代码部分就全部完成了。接下来进入虚幻四Editor。
新建蓝图类GameplayEffect,命名为GE_CharacterDefault,作用为初始化角色的Attributes
在该Gameplay Effect中,只需要在Modifiers中添加一个元素。
将GE_CharacterDefault赋值给角色蓝图中的Default Attribute Effect
运行游戏,输入命令(这一步应该在添加Ability后,即第九步,否则会报错)
可以看到属性被初始化了
9.接下来实现角色攻击的能力。
首先在Input Setting中添加Action Mapping
同样的,新建蓝图类
命名为GA_Punch
这里我们实现最简单的,使用Punch能力后,会播放一次动画,然后结束能力。
完成后,将鼠标左键和能力绑定。
接下来运行,点击鼠标左键,我们就可以看到角色可以播放攻击动画了。
10. 实现攻击伤害
角色具有攻击能力后,还不能造成伤害,为了实现伤害能力。首先创建造成伤害的Gameplay effect
和第八步一样,命名为GE_Damage
注意,这里要将Gameplay Cue Tags设置为GameplayCue.TakeDamage,这个名字可以自己创建。
我们不希望角色在攻击的全过程都能形成伤害判定,所以需要在关键帧后启动碰撞判定。
新建蓝图类AnimNotify,命名为AN_Punch
打开AN_Punch,让播放动画的对象在接收到Notify后,会调用角色的Punch事件。这里的事件还没有实现。
在攻击动画中添加Notify。
实现Punch Event
这里大致实现的过程是,在启动punch后,会在右手命名为Weapon的Socket位置启动一个球状碰撞检测器,如果检测器检测到碰撞的pawn(valid),就会向该actor发送一个Gameplay Event,注意,这里的Event Tag需要为Weapon.Hit,可以通过添加Tag创建。
11. 完善Punch Ability
回到GA_Punch中,我们需要在结束activity前,等待因为碰撞出发引发的event,收到event后,向目标施加一个Gameplay Effect,就是我们之前实现的GE_Damage
现在虽然可以造成伤害,但是并没有反馈,我们希望敌人被伤害后会播放一个被攻击动画。新建一个Gameplay Cue, 命名为GC_TakaDamage
注意,GC_TakaDamage中的Class Setting需要将这里的tag设置为Gameplay.TakeDamage,和我们在GE_Damage中的一样,这意味着该Gameplay Cue就会相应所有tag为这个的Effect。
12. 完成
开始游戏
可以看到被攻击的单位会播放Hit动画。完成!