GameplayAbilitySystem入门与实战(一):初始化

前言

引用官网的一段话介绍一下GAS系统

Gameplay技能系统 是一个高度灵活的框架,可用于构建你可能会在RPG或MOBA游戏中看到的技能和属性类型。你可以构建可供游戏中的角色使用的动作或被动技能,使这些动作导致各种属性累积或损耗的状态效果,实现约束这些动作使用的”冷却”计时器或资源消耗,更改技能等级及每个技能等级的技能效果,激活粒子或音效,等等。简单来说,此系统可帮助你在任何现代RPG或MOBA游戏中设计、实现及高效关联各种游戏中的技能,既包括跳跃等简单技能,也包括你喜欢的角色的复杂技能集。

此篇为GameplayAbilitySystem入门文档的开篇,

此系列文档会从零开始记录用UE4 GAS插件为基础, 尝试开发一个简单的ARPG游戏的案例

准备工作

  • 创建C++工程SuperRoad(也可以先创建蓝图工程,然后添加任意c++类), 目前引擎已经升级到4.26.0, 就以此版本为基础开发
  • 暂不导入美术资源, 使用默认TopDown模板的基础资源
  • 打开引擎插件, 开启GameplayAbilities并重启项目

打开项目Build.cs

暂添加如下模块

1
2
3
4
PrivateDependencyModuleNames.AddRange(new string[] {
"GameplayAbilities",
"GameplayTags",
"GameplayTasks" });

初始化流程

GAS系统必须使用c++, 这个是目前逃不掉的规则, 后续可以考虑部分扩展成蓝图来更直观的连连看,

创建各种类

创建各种必要的基类和GAS相关类, 因为其中部分类初始化必须需要用到,下图仅供参考

image-20201209110513636

此项目多数类以SR为前缀

TargetData

首先为了使用TargetData必须找一个地方在尽量早的时机执行UAbilitySystemGlobals::InitGlobalData(),可以自定义一个SubsystemEngine或者如ActionRPG自定义一个AssetManager到这个类里面执行

ASC

ASCAbilitySystemComponent, GAS系统的核心关键, 使用GAS的各类功能都是围绕这个组件展开的

一般流行的方法中关于ASC的创建会放到PlayerState(PS)或者Character/Pawn中,两种方式都可以,初始化略有区别, 但是都是围绕着一点 在客户端和服务端都在合适的时机调用初始化方法

这里有一点需要注意

如果ASCPS上,那么必须增加NetUpdateFrequency的值,因为默认情况下PS的优先级不高,会导致技能延迟

还有一个规则

如果组件的OwnerActor和作用目标不是同一个,那么必须实现IAbilitySystemInterface接口,同时必须重写里面的唯一的方法GetAbilitySystemComponent

1
2
3
4
UAbilitySystemComponent * ASRCharacterBase::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}

既然ASC需要在服务端和客户端都进行初始化,对于Pawn来说,可以在服务端用PossessedBy,在客户端用PlayerControllerAcknowledgePawn初始化

1
2
3
4
5
6
7
8
9
10
void ASRCharacterBase::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);

if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
SetOwner(NewController);
}
1
2
3
4
5
6
7
8
9
10
void ASRPlayerControllerBase::AcknowledgePossession(APawn* P)
{
Super::AcknowledgePossession(P);

AVGCharacterBase* CharacterBase = Cast<ASRCharacterBase>(P);
if (CharacterBase)
{
CharacterBase->GetAbilitySystemComponent()->InitAbilityActorInfo(CharacterBase, CharacterBase);
}
}

对于ASCPS中创建的,可以在客户端用PawnOnRep_PlayerState内初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Server only
void AHeroCharacter::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);

AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{

AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());


PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
// Client only
void AHeroCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();

AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
}

}

因为考虑怪物也需要释放技能, 那么我就干脆直接把ASC创建到基础角色中,即SRCharacterBase

1
2
3
4
5
6
7
8
9
10
ASRCharacterBase::ASRCharacterBase(const FObjectInitializer& ObjectInitializer):Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = true;
AbilitySystemComponent = CreateDefaultSubobject<USRAbilitySystemComponent>(TEXT("AbilityComponent"));
AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);

AttributeSet = CreateDefaultSubobject<USRAttributeSetBase>(TEXT("AttributeSet"));

}

然后就按照之前所说的, 在PossessedByController中的AcknowledgePossession中初始化ASC

添加技能/初始化属性

因为我们需要使用技能, 而技能必须被添加到ASC中, 可以理解为注册技能, 我们这里给SRCharacterBase加入几个初始化方法;

也别忘了给角色添加几个变量来设置初始内容

1
2
3
4
5
6
7
8
9
10
11
//角色技能
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "SR | Abilities")
TArray<TSubclassOf<class USRGameplayAbilityBase>> CharacterAbilities;

//初始效果,用来初始化属性
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "SR | Abilities")
TSubclassOf<class UGameplayEffect> DefaultAttributeEffect;

//初始应用的GE效果,例如魔法回复
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "SR | Abilities")
TArray<TSubclassOf<class UGameplayEffect>> StartupEffects;
1
2
3
4
5
6
//初始化属性
virtual void InitAttributes();
//添加初始技能
virtual void AddCharacterStartupAbilities();
//添加初始效果
virtual void AddStartUpEffects();

分别实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void ASRCharacterBase::InitAttributes()
{
if (!AbilitySystemComponent)
{
UE_LOG(SRLog, Warning, TEXT("InitAttributes failed [no ASC] !!"));
return;
}
if (!DefaultAttributeEffect)
{
UE_LOG(SRLog, Warning, TEXT("InitAttributes failed [no DefaultAttributeEffect] !!"));
return;
}

FGameplayEffectContextHandle EffectContextHandle = AbilitySystemComponent->MakeEffectContext();
EffectContextHandle.AddSourceObject(this);

FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(DefaultAttributeEffect, GetCharacterLevel(), EffectContextHandle);
if (SpecHandle.IsValid())
{
FActiveGameplayEffectHandle ActiveGEHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), AbilitySystemComponent);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ASRCharacterBase::AddCharacterStartupAbilities()
{
if (GetLocalRole() != ROLE_Authority || !AbilitySystemComponent || AbilitySystemComponent->bHasGiveCharacterAbilities)
{
return;
}
for (auto ga : CharacterAbilities)
{

FGameplayAbilitySpec spec = FGameplayAbilitySpec(ga, GetAbilityLevel(ga.GetDefaultObject()->AbilityName), static_cast<int32>(ga.GetDefaultObject()->InputID), this);
AbilitySystemComponent->GiveAbility(spec);

}
AbilitySystemComponent->bHasGiveCharacterAbilities = true;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ASRCharacterBase::AddStartUpEffects()
{
if (GetLocalRole() != ROLE_Authority || !AbilitySystemComponent || AbilitySystemComponent->bHasApplyStartupEffects)
{
UE_LOG(SRLog, Warning, TEXT("Add Startup Effects failed"));
return;
}

FGameplayEffectContextHandle EffectContextHandle = AbilitySystemComponent->MakeEffectContext();
EffectContextHandle.AddSourceObject(this);
for (auto ge : StartupEffects)
{
FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(ge, GetCharacterLevel(), EffectContextHandle);
if (SpecHandle.IsValid())
{
FActiveGameplayEffectHandle ActiveGEHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), AbilitySystemComponent);
}
}
AbilitySystemComponent->bHasApplyStartupEffects = true;
}

上面初始化属性实际上也是激活了一次GameplayAbilityEffect效果, 这个关于GAGE之类的内容在后续展开

我们现在随意创建一个GA和一个GE

为了防止重复调用, 上述代码中会有两个bool变量(bHasApplyStartupEffects,bHasGiveCharacterAbilities)需要稍微注意, 当然不是必须的

关于调用时机, 我们把他们放在了服务端的函数 PossessedBy中,关于客户端是否需要调用后续看需求跟进.

此函数目前状态

1
2
3
4
5
6
7
8
9
10
11
void ASRCharacterBase::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
InitAttributes();
AddCharacterStartupAbilities();
AddStartUpEffects();
}

按键绑定

ASC有一个非常快捷的按键绑定方式,即调用方法BindAbilityActivationToInputComponent(...)

传入参数第二个参数FGameplayAbilityInputBinds非常有意思,先看一下构造函数

1
2
3
4
5
6
7
FGameplayAbilityInputBinds(FString InConfirmTargetCommand, FString InCancelTargetCommand, FString InEnumName, int32 InConfirmTargetInputID = INDEX_NONE, int32 InCancelTargetInputID = INDEX_NONE)
: ConfirmTargetCommand(InConfirmTargetCommand)
, CancelTargetCommand(InCancelTargetCommand)
, EnumName(InEnumName)
, ConfirmTargetInputID(InConfirmTargetInputID)
, CancelTargetInputID(InCancelTargetInputID)
{ }
  • InEnumName

我们先看这个参数, 他需要你定义一个枚举变量, 此枚举成员变量的名字就需要对应到游戏项目设置里的 Input栏内的Action名称,当然有两个例外,后面讲, 先定义一个枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UENUM(BlueprintType)
enum class ESRAbilityInputID : uint8
{
NONE,
CONFIRM,
CANCEL,
ABILITY1,
ABILITY2,
ABILITY3,
ABILITY4,
ABILITY5,
ABILITY6,
ABILITY7,
ABILITY8,
ABILITY9,
ABILITY10,
};
  • InConfirmTargetCommand/InCancelTargetCommand

这俩参数是确认取消命令的Action名称, 即你打开你游戏项目设置里的 Input栏内的Action名称

如果输入为空,就使用之前枚举里定义的两个对应的枚举名称;如果设置了其他名称, 那么你Action内的这俩功能的名称可以改成你设置的;

  • ConfirmTargetInputID/CancelTargetInputID

这俩是具体枚举里面的对应确认取消命令的成员变量

最后实现如下

1
2
3
4
5
6
7
8
9
void ASRHero::BindASCInput()
{
if (!bHasBindInput && AbilitySystemComponent && InputComponent)
{
AbilitySystemComponent->BindAbilityActivationToInputComponent(InputComponent, FGameplayAbilityInputBinds(TEXT("CONFIRM"), TEXT("CANCEL"),
TEXT("ESRAbilityInputID"), static_cast<int32>(ESRAbilityInputID::CONFIRM), static_cast<int32>(ESRAbilityInputID::CANCEL)));
bHasBindInput = true; //防止多次绑定
}
}

实测发现, 自定义的ConfirmCancel会调用到ASC的虚函数virtual void LocalInputConfirm(); 和 virtual void LocalInputCancel();

而并不能成功调用已经注册的例如InputID=CF的技能

当然理论上可以绕一圈重写上面方法来调用到对应技能

所以建议还是BindAbilityActivationToInputComponent的前两个参数设置成对应的ConfirmCancel的名称或者直接设置成空也是可以映射到枚举的

然后就是函数执行时机问题,这里有个问题需要考虑, 你调用此函数的时候需要确保或者尽量保证InputComponent已经存在了,服务端没什么问题, 在服务端在SetupPlayerInputComponent中执行绑定函数;

关键就是客户端, 客户端通过PlayerController::ClientRestart()函数然后创建的InputComponent,我们重写了OnRep_PlayerState来执行客户端事件, 也调用绑定事件, 因为有bool变量来防止多次绑定, 那么此举也是为了保险起见(GASDoc项目是这么建议的)

自定义GA类

上面添加技能的方法中有一条语句是我们自定义的GA类的内容

1
2
FGameplayAbilitySpec spec = FGameplayAbilitySpec(ga, GetAbilityLevel(ga.GetDefaultObject()->AbilityName), static_cast<int32>(ga.GetDefaultObject()->InputID), this);
AbilitySystemComponent->GiveAbility(spec);

这里有两个参数InputIDAbilityName;

我们创建USRGameplayAbilityBase 继承自 UGameplayAbility

新建如下变量

1
2
3
4
5
6
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
FString AbilityName;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
ESRAbilityInputID InputID = ESRAbilityInputID::NONE;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
bool bAutoActivate = false;

重写方法OnAvatarSet

1
2
3
4
5
6
7
8
void USRGameplayAbilityBase::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec)
{
Super::OnAvatarSet(ActorInfo, Spec);
if (bAutoActivate)
{
ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
}
}

目的是如果是自动释放的技能就直接释放;

至此我们技能的与按键就对应起来了

测试

测试之前我们创建几个测试性的GAGE, 这里我已经把初始化属性也做了, 但这是后续内容, 这里不展开; 我们先测试能否正确触发技能

创建GA_Test,内容如下

image-20201210104931838

蒙太奇动画是一个跳跃动作(别忘记给默认动画蓝图加一个插槽),然后配置按键

image-20201210105408009

丢一个AI怪通过如下方式一直释放技能

image-20201210105530613

然后开测

录制_2020_12_10_10_56_14_650

完成!!!