虚幻四Gameplay Ability System入门(2)
我最近在学习虚幻四的Gameplay Ability System,这个名字可以被理解为技能系统框架(大概),接下来我就简称为GAS或技能系统。在网上找了很久,发现相关的中文教程比较少,所以打算把自己的学习过程和对技能系统的理解写成文章,既帮助我理解,也希望可以帮助到其它想要学习GAS的朋友。之前写过一篇教程,但感觉很不满意,于是打算重写一遍。接下来进入正题。
什么是Gameplay Ability System?
在很多的游戏中,角色会拥有很多的技能,比如火球术,治疗术等等。这些技能会消耗法力值,存在冷却时间,可以造成伤害。同时一个角色可以拥有多个技能,技能之间也会相互关联,比如火球术无法对使用冰霜护盾的敌人造成伤害。
实现以上的种种效果需要一个较为复杂的框架,而虚幻四的GAS系统就为我们提供了一个管理技能的系统,可以很方便的实现技能需要,但GAS目前离不开C++,而且官方目前还没有提供方便入门的教程与文档,因此学习起来还是比较痛苦的,以下是我认为比较好的入门教程:
Bilibili
UE4官方的视频教程,中文
https://www.bilibili.com/video/BV1X5411V7jh
Youtube
有条件的可以翻墙去看
UE4官方视频教程,英文,相较于中文的视频,我认为这个教程讲解的更深入一点。
https://www.youtube.com/watch?v=YvXvWa6vbAA
Github
这一篇是我认为最好的文档了,包含一个项目和较为完整的说明,但仍然较为复杂,建议看完上面两个视频对GAS有了一定了解再看。
https://github.com/tranek/GASDocumentation
GAS系统的基本构成
- Ability System Component,GAS系统的大脑,拥有Abilities(技能)和Attributes(属性),可以把它理解为技能系统的中枢。
- Ability,技能。可以把它理解为某项能力,比如火球术,跳跃等等,它应该包含较为完整的逻辑,可以添加给角色的技能系统,也可以从技能系统中移除。
- Attribute和AttibuteSet,属性和属性集。Attribute代表了角色的某种属性,比如Health,Mana等等。
- Tags,层次化的标签。它代表了某种状态或者属性。比如角色处于燃烧状态,那么这个状态的标签就为Character.State.Burning,我们可以自定义状态并在技能和效果中设置tag之间的关系,比如角色处于燃烧标签时会受到伤害,但如果被带有Water标签的效果影响,就可以移除燃烧标签。我认为Tag应该是技能系统的核心和魅力所在了。
- Gameplay Effect(GE),技能效果,它本身只是一个数据集,代表了对某些Attribute和Tag的修改。比如GE_Damage中应该设置为对Attribute:Health修改Add -20,表示为伤害效果为扣血20点。它还可以添加修改Tag,或者被Tag所影响,和第四点的例子一样,火焰的伤害效果本质是一个GE,如果另外有一个带有Water标签的GE作用于角色,那么它会移除燃烧效果GE。理论上GAS中对于Tag和Attribute的修改尽可能都要使用GE
- Ability Task,表示为Ability中的一个任务。比如接下来几乎每一个技能都会用到的一个Task是PlayMontageAndWait,可以看到它创建并返回了一个Async Task.
- Gameplay cue,GC执行的是非游戏逻辑的效果比如声音特效,粒子特效,摄像机抖动等等,GC通过关联的Tag触发,还是举角色燃烧的粒子,当角色身上有Burning的标签时,就可以设置Gameplay Cue Tag, 然后就会触发GC_Burning,让角色身上有一个燃烧的粒子特效。
下图是我对GAS各个组件关系之间的简单理解,并不完整且正确,只是帮助大致理解各个组件之间的关系。
GAS基础设置
这一部分涉及到虚幻四C++和蓝图的知识,需要拥有一定的基础。
首先打开虚幻四,创建一个C++的空白项目,如果觉得配置麻烦也可以创建一个第三人称项目。
这里我直接使用了epic商城中的素材,将素材直接添加到工程
1.角色基础配置
因为这篇文章不是介绍虚幻四C++入门的,因此我就不具体介绍角色基础配置的说明了。
首先新建Character C++类,命名为CharacterBase
在Project Setting中的Input添加Axis Mappings
打开CharacterBase.h和Character.cpp
添加Camera和SprintArm,函数MoveForward和MoveRight
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera", meta=(AllowPrivateAccess = "true"))
class USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Camera", meta=(AllowPrivateAccess = "true"))
class UCameraComponent* FollowCamera;
protected:
// Character Movement
void MoveForward(float Value);
void MoveRight(float Value);
cpp实现
// Sets default values
ACharacterTesting::ACharacterTesting()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
bUseControllerRotationPitch = false;
bUseControllerRotationRoll = false;
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
CameraBoom = CreateDefaultSubobject(TEXT("Camera Boom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 300.0f;
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject(TEXT("Follow Camera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
}
// Called to bind functionality to input
void ACharacterTesting::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &ACharacterTesting::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ACharacterTesting::MoveRight);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
}
void ACharacterTesting::MoveForward(float Value)
{
if((Controller != nullptr) && (Value != 0.0f))
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
}
void ACharacterTesting::MoveRight(float Value)
{
if((Controller != nullptr) && (Value != 0.0f))
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, Value);
}
}
在UE4 Editor中创建CharacterBase子类BP_Character,给角色选择mesh
创建AnimBlueprint,命名为AnimBP_Character,设置角色相应的动画。
这里只需要locomotion就够了,把locomotion连接到output pose上即可
角色基本设置完成。
2.GAS系统基础配置
- 使用GAS系统首先需要在Plugins中Enable插件Gameplay Abilities
然后在ProjectName.Build.cs中,添加三个依赖,分别是GameplayAbilities, GameplayTasks, GameplayTags
PrivateDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks" });
然后Build Solution
2. 添加ASC
打开CharacterBase.h
创建Ability System Component,这里需要继承一个接口,然后实现接口中的纯虚函数GetAbilitySystemComponent(),它的作用是返回AbilitySystem,这个函数的作用是在不知道当前角色是否具有AbilitySystem的时候时,我们就可以调用这个函数,而不需要使用Cast_To
UCLASS()
class GAS__API ACharacterBase : public ACharacter, public IAbilitySystemInterface
{
//.......
public:
// ......
// Ability System Component
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="CharacterBase")
UAbilitySystemComponent* AbilitySystem;
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const;
}
GetAbilitySystemComponent()实现
ACharacterBase::ACharacterBase()
{
//......
AbilitySystem = CreateDefaultSubobject("AbilitySystem");
}
// override AbilityInterface virtual function
UAbilitySystemComponent* ACharacterBase::GetAbilitySystemComponent() const
{
return AbilitySystem;
}
这样子Character就拥有了Ability System Component了。
3.接下来实现一个方法可以给ASC添加Ability,这个功能可以在BP中调用。
// Add Ability to Character
UFUNCTION(BlueprintCallable, Category="Ability System")
void GiveAbility(TSubclassOf Ability);
实现
void ACharacterBase::GiveAbility(TSubclassOf Ability)
{
if(AbilitySystem)
{
if(HasAuthority() && Ability)
{
AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability, 1));
}
AbilitySystem->InitAbilityActorInfo(this, this);
}
}
蓝图中调用的例子
4.创建和实现基础的AttributeSet
在第一步我们实际上还用不到Attribute,但方便起见还是先创建一下。命名为AttributeSetBase
打开。这里我只展现了创建Attribute:Health和MaxHealth的方法。
最前面的define是一种mecro方法,它在AttributeSet中实现,它可以自动地帮你实现Attribute的getter, setter等方法。
attribute需要一个ReplicatedUsing方法。
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
/**
*
*/
UCLASS()
class GAS__API UAttributeSetBase : public UAttributeSet
{
GENERATED_BODY()
public:
UAttributeSetBase();
virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override;
// Health and MaxHealth
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Abilities", ReplicatedUsing=OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UAttributeSetBase, Health);
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Abilities", ReplicatedUsing=OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UAttributeSetBase, MaxHealth);
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);
};
void UAttributeSetBase::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UAttributeSetBase, MaxHealth, COND_None, REPNOTIFY_Always);
}
void UAttributeSetBase::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, Health, OldHealth);
}
void UAttributeSetBase::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, MaxHealth, OldMaxHealth);
}
最后一步是把AttributeSet添加给角色。
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category="Abilities")
UAttributeSetBase* AttributeSet;
AttributeSet = CreateDefaultSubobject("Attribute Set");
OK,到这一步角色的基础GAS配置就完成了
5.测试
创建一个GameplayAbility类的蓝图,命名为BP_Test
打开后,这里完成的是启动ability,打印一个hello在屏幕上,然后结束Ability
然后打开角色的蓝图,在beginplay中调用GiveAbility函数
点击鼠标左键,启用ability。
运行游戏,点击鼠标左键应该就可以看到Hello了。