虚幻四Gameplay Ability System入门(1)

虚幻四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系统的基本构成

  1. Ability System Component,GAS系统的大脑,拥有Abilities(技能)和Attributes(属性),可以把它理解为技能系统的中枢。
  2. Ability,技能。可以把它理解为某项能力,比如火球术,跳跃等等,它应该包含较为完整的逻辑,可以添加给角色的技能系统,也可以从技能系统中移除。
  3. Attribute和AttibuteSet,属性和属性集。Attribute代表了角色的某种属性,比如Health,Mana等等。
  4. Tags,层次化的标签。它代表了某种状态或者属性。比如角色处于燃烧状态,那么这个状态的标签就为Character.State.Burning,我们可以自定义状态并在技能和效果中设置tag之间的关系,比如角色处于燃烧标签时会受到伤害,但如果被带有Water标签的效果影响,就可以移除燃烧标签。我认为Tag应该是技能系统的核心和魅力所在了。
  5. Gameplay Effect(GE),技能效果,它本身只是一个数据集,代表了对某些Attribute和Tag的修改。比如GE_Damage中应该设置为对Attribute:Health修改Add -20,表示为伤害效果为扣血20点。它还可以添加修改Tag,或者被Tag所影响,和第四点的例子一样,火焰的伤害效果本质是一个GE,如果另外有一个带有Water标签的GE作用于角色,那么它会移除燃烧效果GE。理论上GAS中对于Tag和Attribute的修改尽可能都要使用GE
  6. Ability Task,表示为Ability中的一个任务。比如接下来几乎每一个技能都会用到的一个Task是PlayMontageAndWait,可以看到它创建并返回了一个Async Task.
1.png
  1. Gameplay cue,GC执行的是非游戏逻辑的效果比如声音特效,粒子特效,摄像机抖动等等,GC通过关联的Tag触发,还是举角色燃烧的粒子,当角色身上有Burning的标签时,就可以设置Gameplay Cue Tag, 然后就会触发GC_Burning,让角色身上有一个燃烧的粒子特效。

下图是我对GAS各个组件关系之间的简单理解,并不完整且正确,只是帮助大致理解各个组件之间的关系。

2.png

GAS基础设置

这一部分涉及到虚幻四C++和蓝图的知识,需要拥有一定的基础。

首先打开虚幻四,创建一个C++的空白项目,如果觉得配置麻烦也可以创建一个第三人称项目。

这里我直接使用了epic商城中的素材,将素材直接添加到工程

3.png

1.角色基础配置

因为这篇文章不是介绍虚幻四C++入门的,因此我就不具体介绍角色基础配置的说明了。

首先新建Character C++类,命名为CharacterBase

4.png

在Project Setting中的Input添加Axis Mappings

5.png

打开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<USpringArmComponent>(TEXT("Camera Boom"));
   CameraBoom->SetupAttachment(RootComponent);
   CameraBoom->TargetArmLength = 300.0f;
   CameraBoom->bUsePawnControlRotation = true;

   FollowCamera = CreateDefaultSubobject<UCameraComponent>(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

6.png

创建AnimBlueprint,命名为AnimBP_Character,设置角色相应的动画。

7.png

这里只需要locomotion就够了,把locomotion连接到output pose上即可

8.png
9.png
10.png

角色基本设置完成。

2.GAS系统基础配置

  1. 使用GAS系统首先需要在Plugins中Enable插件Gameplay Abilities
11.png

然后在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<UAbilitySystemComponent>("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<UGameplayAbility> Ability);

实现

void ACharacterBase::GiveAbility(TSubclassOf<UGameplayAbility> Ability)
{
    if(AbilitySystem)
    {
        if(HasAuthority() && Ability)
        {
            AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability, 1));
        }
        AbilitySystem->InitAbilityActorInfo(this, this);
    }
}

蓝图中调用的例子

12.png

4.创建和实现基础的AttributeSet

在第一步我们实际上还用不到Attribute,但方便起见还是先创建一下。命名为AttributeSetBase

13.png

打开。这里我只展现了创建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<FLifetimeProperty>& 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<FLifetimeProperty>& 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<UAttributeSetBase>("Attribute Set");

OK,到这一步角色的基础GAS配置就完成了

5.测试

创建一个GameplayAbility类的蓝图,命名为BP_Test

14.png

打开后,这里完成的是启动ability,打印一个hello在屏幕上,然后结束Ability

15.png

然后打开角色的蓝图,在beginplay中调用GiveAbility函数

16.png

点击鼠标左键,启用ability。

17.png

运行游戏,点击鼠标左键应该就可以看到Hello了。

18.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容