GAS/RPGAction学习笔记

文章目录

  • 大钊-深入GAS框架
  • GASSimple
    • Tag
    • ASC
    • GA
    • GE
    • AS
    • Block
    • Break
    • PlayMontageAndWaitForEvent
  • ActionRPG
    • LoadingScreen
    • Tag
    • GA
      • UPRGTargetType
      • UPRGGameplayAbility
      • GA_AbilityBase
        • GA_Meleebase
        • **GA_PotionBase**
        • GA_SkillBase
        • **GA_SpawnProjectileBase**
    • AS
    • GE
    • Item
    • ASC
    • GamePlay
      • Character
      • montage
      • PlayerController
    • 其他

GAS官方视频https://www.bilibili.com/video/BV1X5411V7jh

GAS文档https://docs.unrealengine.com/5.0/zh-CN/using-gameplay-abilities-in-unreal-engine/

GAS/RPGAction学习笔记_第1张图片

堡垒之夜也是用的这个框架

GAS/RPGAction学习笔记_第2张图片

GAS/RPGAction学习笔记_第3张图片

其他的技能系统:

  • Able Ability System
  • Ascent Combat Framework (ACF)

GAS/RPGAction学习笔记_第4张图片

GAS/RPGAction学习笔记_第5张图片

GAS/RPGAction学习笔记_第6张图片

GA流程

GAS/RPGAction学习笔记_第7张图片

GAS/RPGAction学习笔记_第8张图片

GAS/RPGAction学习笔记_第9张图片

sample

GAS/RPGAction学习笔记_第10张图片

在模块中要引用

PrivateDependencyModuleNames.AddRange(
				"GameplayAbilities",
				"GameplayTags",
				"GameplayTasks",
			}

character要继承IAbilitySystemInterface接口,以此被GAS识别

在character中声明一个UAbilitySystemComponent(ASC)组件

实现接口方法:

virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;

在构造函数中对ASC使用CreateDefaultSubObject去实例化

在character中去声明一个Ability数组去存储各种ability

UPROPERTY()
TArray<TSubclassof<UGameplayAbility>> MyAbilities;

像是打击受击掉血的流程,有两个能力GA-Attack和BeAttack,有一个GE behitt描述了伤害

属性(AS)中如血量等值不是float,而是FGameplayAttributeData结构体,有base值和cur值,方便GE(buff)作用后的数值回滚。

GAS/RPGAction学习笔记_第11张图片

MakeOutgoingGameplayEffectSpec指可以定一泛类型GE的一个模板,然后在GE生成时读表或者数据库动态改变。更好的是通过IDetailCustomization去定制GA的UI。

GAS/RPGAction学习笔记_第12张图片

GAS中文文档https://blog.csdn.net/pirate310/article/details/106311256

GAS英文文档https://github.com/tranek/GASDocumentation

教学博客https://www.cnblogs.com/JackSamuel/p/7155500.html

大钊-深入GAS框架

https://www.bilibili.com/video/BV1zD4y1X77M?spm_id_from=333.999.0.0GAS/RPGAction学习笔记_第13张图片

GAS/RPGAction学习笔记_第14张图片

GAS/RPGAction学习笔记_第15张图片

GAS/RPGAction学习笔记_第16张图片

GAS/RPGAction学习笔记_第17张图片

GAS/RPGAction学习笔记_第18张图片

GAS/RPGAction学习笔记_第19张图片

GAS/RPGAction学习笔记_第20张图片

GAS/RPGAction学习笔记_第21张图片

GAS/RPGAction学习笔记_第22张图片

GAS/RPGAction学习笔记_第23张图片

GAS/RPGAction学习笔记_第24张图片

l

GAS/RPGAction学习笔记_第25张图片

GAS/RPGAction学习笔记_第26张图片

GAS/RPGAction学习笔记_第27张图片

GAS/RPGAction学习笔记_第28张图片

GAS/RPGAction学习笔记_第29张图片

GAS/RPGAction学习笔记_第30张图片

ASC对于单机游戏可以放在Pawn上,对于联机游戏可以放在playstate上以广播和持续的保存

GAS/RPGAction学习笔记_第31张图片

GAS/RPGAction学习笔记_第32张图片

GASSimple

链接:https://pan.baidu.com/s/10U3QOB0oz7vMT1B-IXbMMw 提取码:uenb

Tag

先看看核心的tag是怎么标定的:

比较简单,定义了一层,分别是攻击,格挡,受击,主动

GAS/RPGAction学习笔记_第33张图片

ASC

GASSample中ASC是挂载在character上的,在构造函数中初始化

注意这个Character继承了IAbilitySystemInterface接口,其中只有一个GetAbilitySystemComponent方法用于返回此Actor上的ASC,通过这种方式让此Actor被GAS系统所识别和使用。

class AGASSampleCharacter : public ACharacter,
	public IAbilitySystemInterface //修改:继承接口
{
    GENERATED_BODY()
        
public:
	AGASSampleCharacter();    
        
protected:
	//修改:添加BeginPlay
	virtual void BeginPlay() override;
        
public:

	// 修改:申明ASC
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = GameplayAbilities, meta = (AllowPrivateAccess = "true"))
	class UAbilitySystemComponent* AbilitySystem;

	// 修改:实现接口方法
	UAbilitySystemComponent* GetAbilitySystemComponent()const override;

	// 修改:声明Ability数组
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Abilities)
	TArray<TSubclassOf<UGameplayAbility>> MyAbilities;

    //此actor的属性集
	UPROPERTY()
	USampleAttributeSet* AttributeSet;
        
}
AGASSampleCharacter::AGASSampleCharacter()
{
    // 修改:实例化ASC
	AbilitySystem = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));


	// 在OwnerActor的构造方法中创建的AttributeSet将会自动注册到ASC
	AttributeSet = CreateDefaultSubobject<USampleAttributeSet>(TEXT("AttributeSet"));
}

void AGASSampleCharacter::BeginPlay()
{
	Super::BeginPlay();

	if (nullptr != AbilitySystem)
	{
		// 修改:给ASC赋予技能
		if (HasAuthority() && MyAbilities.Num() > 0)
		{
			for (auto i = 0; i < MyAbilities.Num(); i++)
			{
				if (MyAbilities[i] == nullptr)
				{
					continue;
				}
				AbilitySystem->GiveAbility(FGameplayAbilitySpec(MyAbilities[i].GetDefaultObject(), 1, 0));
			}
		}

		// 修改:初始化ASC
		AbilitySystem->InitAbilityActorInfo(this, this);
	}
}

注意GA是通过GiveAbility赋给ASC的,一个GA相当于一个模板,通过使用一个GameplayAbility以及相关参数如等级,生成一个能力实例FGameplayAbilitySpec

FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& AbilitySpec);

这个基础的GASSampleCharacter有三个GA,GA是游戏逻辑的主要书写处

GAS/RPGAction学习笔记_第34张图片

GA

看看GA_Attack_Attr以此为例,梳理下流程。

GA_Attack_Attr的ability tag为positive,代表这是个主动效果,并且BlockAbilitiesWithTag也是positive tag,代表这个tag存在时会阻塞其他的positive tag。

GAS/RPGAction学习笔记_第35张图片

这里通过GetActorInfo拿到此GA拥有者的相关信息,包括OwnerActor(攻击方)、AvatarActor(受击方)、SKMesh、ASC等。

在这个攻击动画蒙太奇中有一个NotifyName为Hit的event,播放攻击蒙太奇触发HitEvent,判断是否找到AvatarActor。

GAS/RPGAction学习笔记_第36张图片

找到则在受击者的ASC上触发一个受击的效果GE_hit以及收到伤害扣血的效果GE_Damage。

GAS/RPGAction学习笔记_第37张图片

在调用蒙太奇后直接调用了CommitAbility,以提交技能进行计算如技能消耗等,这里不commit在场景中攻击就不会扣蓝。

GAS/RPGAction学习笔记_第38张图片

CommitAbility的流程可以看下面,可以知道CommitAbility是最后可以判定GA失败的时机,不然之后GA就一定成功。

GAS/RPGAction学习笔记_第39张图片

在蒙太奇播完后主动触发结束GE的event,GE结束时停止蒙太奇。

GAS/RPGAction学习笔记_第40张图片

这里播放蒙太奇使用的是UE原生的节点,有说法是UE原生节点在网络同步时会有问题,一定要使用GAS的蒙太奇节点

GAS蒙太奇组合了tag和event,更加系统方便,同时还有自动结束蒙太奇的功能,比起原生节点更加强大,还是使用这个吧。。。

GAS/RPGAction学习笔记_第41张图片

GE

GA_Attack_Attr定义了攻击的消耗属性即GE_Cost(demo中的体现即为5点蓝耗)

img

应用策略是Instant立即生效(Infinite是持续生效,HasDuration是一定时间后生效),GE设置简单的对SampleAttributeSet.Physical(这个属性是需要代码自定义的)属性即蓝量应用计算 Add -5,

应用概率ChanceToAppleToTarget为1,必定生效。

GAS/RPGAction学习笔记_第42张图片

AS

来看看这个SampleAttribute如何在代码中定义,一开始定义了一个宏

// 定义一个增加各种Getter和Setter方法的宏
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

进入内部看看,作用注释。官方注释说得很清楚了,在自己的AS中使用ATTRIBUTE_ACCESSORS宏。

/**
 * This defines a set of helper functions for accessing and initializing attributes, to avoid having to manually write these functions.
 * It would creates the following functions, for attribute Health
 *
 *	static FGameplayAttribute UMyHealthSet::GetHealthAttribute();
 *	FORCEINLINE float UMyHealthSet::GetHealth() const;
 *	FORCEINLINE void UMyHealthSet::SetHealth(float NewVal);
 *	FORCEINLINE void UMyHealthSet::InitHealth(float NewVal);
 *
 * To use this in your game you can define something like this, and then add game-specific functions as necessary:
 * 
 *	#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
 *	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
 *	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
 *	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
 *	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
 * 
 *	ATTRIBUTE_ACCESSORS(UMyHealthSet, Health)
 */


#define GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	static FGameplayAttribute Get##PropertyName##Attribute() \
	{ \
		static FProperty* Prop = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \
		return Prop; \
	} 

#define GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	FORCEINLINE float Get##PropertyName() const \
	{ \
		return PropertyName.GetCurrentValue(); \
	} //当前值

#define GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	FORCEINLINE void Set##PropertyName(float NewVal) \
	{ \
		UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); \
		if (ensure(AbilityComp)) \
		{ \
			AbilityComp->SetNumericAttributeBase(Get##PropertyName##Attribute(), NewVal); \
		}; \
	}//设置属性新值

#define GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) \
	FORCEINLINE void Init##PropertyName(float NewVal) \
	{ \
		PropertyName.SetBaseValue(NewVal); \
		PropertyName.SetCurrentValue(NewVal); \
	}//进行初始化

看一下SimpleAttributeSet的简单框架,以属性physic(蓝耗)为例。

一个属性集AttributeSet集成了多个属性FGameplayAttributeData,从而使得这一套属性可以在不同的ASC上使用。

属性的基本类型是FGameplayAttributeData而非float,FGameplayAttributeData中定义了currentvalue和basevalue,用于处理buff和数值回滚的情况。

UCLASS()
class GASSAMPLE_API USampleAttributeSet : public UAttributeSet
{
	GENERATED_BODY()
public:
    //网络同步
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

public:
	UPROPERTY(BlueprintReadOnly, Category = "Physical", ReplicatedUsing = OnRep_MaxPhysical)
	FGameplayAttributeData Physical; //定义蓝耗属性值
	ATTRIBUTE_ACCESSORS(USampleAttributeSet, Physical) //定义属性的getset和init接口
	UFUNCTION()
	void OnRep_MaxPhysical(const FGameplayAttributeData& OldValue);//属性同步时客户端调用

public:
	// 属性修改前回调
	virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue);

	// GE执行后属性回调
	virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;
};

在PreAttributeChange函数中对新设置的值进行范围限定,在PostGameplayEffectExecute进行值改变后的回调处理,这里直接调用AvatarActor进行相关处理不是好的写法,只是简单demo示例

void USampleAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	...
	DOREPLIFETIME(USampleAttributeSet, Physical); //对蓝耗进行网络同步
}

//进行范围限定
void USampleAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	if (Attribute == GetHealthAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0.f, GetMaxHealth());
	}
	if (Attribute == GetPhysicalAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0.f, GetMaxPhysical());
	}
}

void USampleAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	// 仅在instant GameplayEffect使Attribute的 BaseValue改变时触发。
	Super::PostGameplayEffectExecute(Data);

	AGASSampleCharacter* TargetCharacter = nullptr;
	if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
	{
		AActor* TargetActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
		TargetCharacter = Cast<AGASSampleCharacter>(TargetActor);
	}
	if (nullptr != TargetCharacter)
	{
		if (Data.EvaluatedData.Attribute == GetHealthAttribute())
		{
			TargetCharacter->OnHealthChanged();
		}
		if (Data.EvaluatedData.Attribute == GetPhysicalAttribute())
		{
			TargetCharacter->OnPhysicalChanged();
		}
	}
	
}

//进行网络同步处理,这里通知ASC处理网络同步
void USampleAttributeSet::OnRep_Physical(const FGameplayAttributeData& OldValue)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(USampleAttributeSet, Physical, OldValue);
}

看看GAMEPLAYATTRIBUTE_REPNOTIFY宏干了什么,通过在RepNotify中调用,以通知ASC处理客户端预测修改网络同步的属性。

/**
 *	This is a helper macro that can be used in RepNotify functions to handle attributes that will be predictively modified by clients.
 *	
 *	void UMyHealthSet::OnRep_Health(const FGameplayAttributeData& OldValue)
 *	{
 *		GAMEPLAYATTRIBUTE_REPNOTIFY(UMyHealthSet, Health, OldValue);
 *	}
 */

#define GAMEPLAYATTRIBUTE_REPNOTIFY(ClassName, PropertyName, OldValue) \
{ \
	static FProperty* ThisProperty = FindFieldChecked<FProperty>(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \
	GetOwningAbilitySystemComponent()->SetBaseAttributeValueFromReplication(FGameplayAttribute(ThisProperty), PropertyName, OldValue); \
}

看完GA,GE,AS接着返回看下挂载了ASC的角色蓝图

通过按键调用TryActivateAbilityByClass方法激活ASC的GA,注意这里的GA一定要已经生成对应的FGameplayAbilitySpec实例赋给ASC,否则激活也不会生效。

GAS/RPGAction学习笔记_第43张图片GAS/RPGAction学习笔记_第44张图片

整体性的大概看完了,看下一些零碎的点。

通过以下两个函数去拿到挂有ASC的acotr的属性cur值和base值

img

Block

看看Demo场景中的block是怎么做的

格挡角色附带了一个新GA img

这个GA_Block的ActivationOwnedTags属性有一个Block Tag,意味着这个GA激活时ASC会带有一个Block的Tag状态。

而GE_Demage伤害GE中,在ApplicationTagReq中标记了Block Tag为Ignore,意味带有Block Tag的AvatarActor会忽视该GE,即无法造成伤害。

这里面还有个坑,我改了Block的GA,发现蓝图调用TryActivateAbilityByClass失败,找不到这个GA_Block类,我又重新指定了一下就行了。

GAS/RPGAction学习笔记_第45张图片img

Break

对于打断效果同样有一个对应的GA

img

对于GA_BeAttack_Break,将CancelAbilitiesWithTag设置为了Positive,代表这个GA激活时会取消掉Positive类型的GA,即打断的效果。同样的,取消后摇也可以通过这个cancel实现。

img

下面设置了GA的触发器,被Hit时触发Break

GAS/RPGAction学习笔记_第46张图片

GAS/RPGAction学习笔记_第47张图片

GE_Hit会为受击方添加Hit标签

PlayMontageAndWaitForEvent

最后贴一下PlayMontageAndWaitForEvent的代码

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Abilities/Tasks/AbilityTask.h"
#include "GameplayTagContainer.h"
#include "RPGAbilityTask_PlayMontageAndWaitForEvent.generated.h"

class URPGAbilitySystemComponent;

/** Delegate type used, EventTag and Payload may be empty if it came from the montage callbacks */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRPGPlayMontageAndWaitForEventDelegate, FGameplayTag, EventTag, FGameplayEventData, EventData);

/**
 * This task combines PlayMontageAndWait and WaitForEvent into one task, so you can wait for multiple types of activations such as from a melee combo
 * Much of this code is copied from one of those two ability tasks
 * This is a good task to look at as an example when creating game-specific tasks
 * It is expected that each game will have a set of game-specific tasks to do what they want
 */
UCLASS()
class URPGAbilityTask_PlayMontageAndWaitForEvent : public UAbilityTask
{
	GENERATED_BODY()

public:
	// Constructor and overrides
	URPGAbilityTask_PlayMontageAndWaitForEvent(const FObjectInitializer& ObjectInitializer);
	virtual void Activate() override;
	virtual void ExternalCancel() override;
	virtual FString GetDebugString() const override;
	virtual void OnDestroy(bool AbilityEnded) override;

	/** The montage completely finished playing */
	UPROPERTY(BlueprintAssignable)
	FRPGPlayMontageAndWaitForEventDelegate OnCompleted;

	/** The montage started blending out */
	UPROPERTY(BlueprintAssignable)
	FRPGPlayMontageAndWaitForEventDelegate OnBlendOut;

	/** The montage was interrupted */
	UPROPERTY(BlueprintAssignable)
	FRPGPlayMontageAndWaitForEventDelegate OnInterrupted;

	/** The ability task was explicitly cancelled by another ability */
	UPROPERTY(BlueprintAssignable)
	FRPGPlayMontageAndWaitForEventDelegate OnCancelled;

	/** One of the triggering gameplay events happened */
	UPROPERTY(BlueprintAssignable)
	FRPGPlayMontageAndWaitForEventDelegate EventReceived;

	/**
	 * Play a montage and wait for it end. If a gameplay event happens that matches EventTags (or EventTags is empty), the EventReceived delegate will fire with a tag and event data.
	 * If StopWhenAbilityEnds is true, this montage will be aborted if the ability ends normally. It is always stopped when the ability is explicitly cancelled.
	 * On normal execution, OnBlendOut is called when the montage is blending out, and OnCompleted when it is completely done playing
	 * OnInterrupted is called if another montage overwrites this, and OnCancelled is called if the ability or task is cancelled
	 *
	 * @param TaskInstanceName Set to override the name of this task, for later querying
	 * @param MontageToPlay The montage to play on the character
	 * @param EventTags Any gameplay events matching this tag will activate the EventReceived callback. If empty, all events will trigger callback
	 * @param Rate Change to play the montage faster or slower
	 * @param bStopWhenAbilityEnds If true, this montage will be aborted if the ability ends normally. It is always stopped when the ability is explicitly cancelled
	 * @param AnimRootMotionTranslationScale Change to modify size of root motion or set to 0 to block it entirely
	 */
	UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
	static URPGAbilityTask_PlayMontageAndWaitForEvent* PlayMontageAndWaitForEvent(
		UGameplayAbility* OwningAbility,
		FName TaskInstanceName,
		UAnimMontage* MontageToPlay,
		FGameplayTagContainer EventTags,
		float Rate = 1.f,
		FName StartSection = NAME_None,
		bool bStopWhenAbilityEnds = true,
		float AnimRootMotionTranslationScale = 1.f);

private:
	/** Montage that is playing */
	UPROPERTY()
	UAnimMontage* MontageToPlay;

	/** List of tags to match against gameplay events */
	UPROPERTY()
	FGameplayTagContainer EventTags;

	/** Playback rate */
	UPROPERTY()
	float Rate;

	/** Section to start montage from */
	UPROPERTY()
	FName StartSection;

	/** Modifies how root motion movement to apply */
	UPROPERTY()
	float AnimRootMotionTranslationScale;

	/** Rather montage should be aborted if ability ends */
	UPROPERTY()
	bool bStopWhenAbilityEnds;

	/** Checks if the ability is playing a montage and stops that montage, returns true if a montage was stopped, false if not. */
	bool StopPlayingMontage();

	/** Returns our ability system component */
	UAbilitySystemComponent* GetTargetASC();

	void OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted);
	void OnAbilityCancelled();
	void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted);
	void OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload);

	FOnMontageBlendingOutStarted BlendingOutDelegate;
	FOnMontageEnded MontageEndedDelegate;
	FDelegateHandle CancelledHandle;
	FDelegateHandle EventHandle;
};
// Copyright Epic Games, Inc. All Rights Reserved.

#include "RPGAbilityTask_PlayMontageAndWaitForEvent.h"
#include "GameFramework/Character.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemGlobals.h"
#include "Animation/AnimInstance.h"

URPGAbilityTask_PlayMontageAndWaitForEvent::URPGAbilityTask_PlayMontageAndWaitForEvent(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	Rate = 1.f;
	bStopWhenAbilityEnds = true;
}

UAbilitySystemComponent* URPGAbilityTask_PlayMontageAndWaitForEvent::GetTargetASC()
{
	return AbilitySystemComponent;
}

void URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted)
{
	if (Ability && Ability->GetCurrentMontage() == MontageToPlay)
	{
		if (Montage == MontageToPlay)
		{
			AbilitySystemComponent->ClearAnimatingAbility(Ability);

			// Reset AnimRootMotionTranslationScale
			ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
			if (Character && (Character->GetLocalRole() == ROLE_Authority ||
							  (Character->GetLocalRole() == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
			{
				Character->SetAnimRootMotionTranslationScale(1.f);
			}

		}
	}

	if (bInterrupted)
	{
		if (ShouldBroadcastAbilityTaskDelegates())
		{
			OnInterrupted.Broadcast(FGameplayTag(), FGameplayEventData());
		}
	}
	else
	{
		if (ShouldBroadcastAbilityTaskDelegates())
		{
			OnBlendOut.Broadcast(FGameplayTag(), FGameplayEventData());
		}
	}
}

void URPGAbilityTask_PlayMontageAndWaitForEvent::OnAbilityCancelled()
{
	// TODO: Merge this fix back to engine, it was calling the wrong callback

	if (StopPlayingMontage())
	{
		// Let the BP handle the interrupt as well
		if (ShouldBroadcastAbilityTaskDelegates())
		{
			OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
		}
	}
}

void URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	if (!bInterrupted)
	{
		if (ShouldBroadcastAbilityTaskDelegates())
		{
			OnCompleted.Broadcast(FGameplayTag(), FGameplayEventData());
		}
	}

	EndTask();
}

void URPGAbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload)
{
	if (ShouldBroadcastAbilityTaskDelegates())
	{
		FGameplayEventData TempData = *Payload;
		TempData.EventTag = EventTag;

		EventReceived.Broadcast(EventTag, TempData);
	}
}

URPGAbilityTask_PlayMontageAndWaitForEvent* URPGAbilityTask_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(UGameplayAbility* OwningAbility,
	FName TaskInstanceName, UAnimMontage* MontageToPlay, FGameplayTagContainer EventTags, float Rate, FName StartSection, bool bStopWhenAbilityEnds, float AnimRootMotionTranslationScale)
{
	UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(Rate);

	URPGAbilityTask_PlayMontageAndWaitForEvent* MyObj = NewAbilityTask<URPGAbilityTask_PlayMontageAndWaitForEvent>(OwningAbility, TaskInstanceName);
	MyObj->MontageToPlay = MontageToPlay;
	MyObj->EventTags = EventTags;
	MyObj->Rate = Rate;
	MyObj->StartSection = StartSection;
	MyObj->AnimRootMotionTranslationScale = AnimRootMotionTranslationScale;
	MyObj->bStopWhenAbilityEnds = bStopWhenAbilityEnds;

	return MyObj;
}

void URPGAbilityTask_PlayMontageAndWaitForEvent::Activate()
{
	if (Ability == nullptr)
	{
		return;
	}

	bool bPlayedMontage = false;
	UAbilitySystemComponent* RPGAbilitySystemComponent = GetTargetASC();

	if (RPGAbilitySystemComponent)
	{
		const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
		UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
		if (AnimInstance != nullptr)
		{
			// Bind to event callback
			EventHandle = RPGAbilitySystemComponent->AddGameplayEventTagContainerDelegate(EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnGameplayEvent));

			if (RPGAbilitySystemComponent->PlayMontage(Ability, Ability->GetCurrentActivationInfo(), MontageToPlay, Rate, StartSection) > 0.f)
			{
				// Playing a montage could potentially fire off a callback into game code which could kill this ability! Early out if we are  pending kill.
				if (ShouldBroadcastAbilityTaskDelegates() == false)
				{
					return;
				}

				CancelledHandle = Ability->OnGameplayAbilityCancelled.AddUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnAbilityCancelled);

				BlendingOutDelegate.BindUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageBlendingOut);
				AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, MontageToPlay);

				MontageEndedDelegate.BindUObject(this, &URPGAbilityTask_PlayMontageAndWaitForEvent::OnMontageEnded);
				AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, MontageToPlay);

				ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
				if (Character && (Character->GetLocalRole() == ROLE_Authority ||
								  (Character->GetLocalRole() == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
				{
					Character->SetAnimRootMotionTranslationScale(AnimRootMotionTranslationScale);
				}

				bPlayedMontage = true;
			}
		}
		else
		{
			ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent call to PlayMontage failed!"));
		}
	}
	else
	{
		ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent called on invalid AbilitySystemComponent"));
	}

	if (!bPlayedMontage)
	{
		ABILITY_LOG(Warning, TEXT("URPGAbilityTask_PlayMontageAndWaitForEvent called in Ability %s failed to play montage %s; Task Instance Name %s."), *Ability->GetName(), *GetNameSafe(MontageToPlay),*InstanceName.ToString());
		if (ShouldBroadcastAbilityTaskDelegates())
		{
			OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
		}
	}

	SetWaitingOnAvatar();
}

void URPGAbilityTask_PlayMontageAndWaitForEvent::ExternalCancel()
{
	check(AbilitySystemComponent);

	OnAbilityCancelled();

	Super::ExternalCancel();
}

void URPGAbilityTask_PlayMontageAndWaitForEvent::OnDestroy(bool AbilityEnded)
{
	// Note: Clearing montage end delegate isn't necessary since its not a multicast and will be cleared when the next montage plays.
	// (If we are destroyed, it will detect this and not do anything)

	// This delegate, however, should be cleared as it is a multicast
	if (Ability)
	{
		Ability->OnGameplayAbilityCancelled.Remove(CancelledHandle);
		if (AbilityEnded && bStopWhenAbilityEnds)
		{
			StopPlayingMontage();
		}
	}

	UAbilitySystemComponent* RPGAbilitySystemComponent = GetTargetASC();
	if (RPGAbilitySystemComponent)
	{
		RPGAbilitySystemComponent->RemoveGameplayEventTagContainerDelegate(EventTags, EventHandle);
	}

	Super::OnDestroy(AbilityEnded);

}

bool URPGAbilityTask_PlayMontageAndWaitForEvent::StopPlayingMontage()
{
	const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
	if (!ActorInfo)
	{
		return false;
	}

	UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
	if (AnimInstance == nullptr)
	{
		return false;
	}

	// Check if the montage is still playing
	// The ability would have been interrupted, in which case we should automatically stop the montage
	if (AbilitySystemComponent && Ability)
	{
		if (AbilitySystemComponent->GetAnimatingAbility() == Ability
			&& AbilitySystemComponent->GetCurrentMontage() == MontageToPlay)
		{
			// Unbind delegates so they don't get called as well
			FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(MontageToPlay);
			if (MontageInstance)
			{
				MontageInstance->OnMontageBlendingOutStarted.Unbind();
				MontageInstance->OnMontageEnded.Unbind();
			}

			AbilitySystemComponent->CurrentMontageStop();
			return true;
		}
	}

	return false;
}

FString URPGAbilityTask_PlayMontageAndWaitForEvent::GetDebugString() const
{
	UAnimMontage* PlayingMontage = nullptr;
	if (Ability)
	{
		const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
		UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();

		if (AnimInstance != nullptr)
		{
			PlayingMontage = AnimInstance->Montage_IsActive(MontageToPlay) ? MontageToPlay : AnimInstance->GetCurrentActiveMontage();
		}
	}

	return FString::Printf(TEXT("PlayMontageAndWaitForEvent. MontageToPlay: %s  (Currently Playing): %s"), *GetNameSafe(MontageToPlay), *GetNameSafe(PlayingMontage));
}

这个demo比较简单,算是看完了,接下来看ActionRPG

ActionRPG

LoadingScreen

添加游戏项目的额外模块要记得在项目的target文件夹中添加代码

public RPGLearnTarget(TargetInfo Target) : base(Target)
	{
		Type = TargetType.Game;
		DefaultBuildSettings = BuildSettingsVersion.V2;
		ExtraModuleNames.Add("RPGLearn");
		ExtraModuleNames.Add("RPGLearnLoadingScreen"); // extra module
	}

loading的屏幕只在客户端显示,并且需要在PreLoadingScreen阶段加载

		{
			"Name": "RPGLearnLoadingScreen",
			"Type": "ClientOnly",
			"LoadingPhase": "PreLoadingScreen"
		}

播放loading窗口使用MoviePlayer类

Tag

还是先从tag看起

GAS/RPGAction学习笔记_第48张图片GAS/RPGAction学习笔记_第49张图片

img

Ability.Item标明是物体,如武器,药品

Ability.Melee标明攻击,CloseFar代表近战远程,游戏中主要通过这个tag进行连击comboo判断

Ability.Skill标明是技能

Event.Montage中的Tag用于触发对应的GE和蒙太奇动画Player.Combo是玩家特有的连击,而Shared Tag下是怪物和玩家共用的Tag,UseItem\UseSkill是使用物品和技能,WeaponHit是第一段普通攻击

Status.DamageImmune标明了免疫不受伤害

Cooldown.Skill用于技能GA的冷却,是冷却GE的tag

GA

UPRGTargetType

ActionRPG 定义了一个URPGTargetType类去设置GA作用目标的寻找方式(在GASSimple中,只是简单的在GA激活的时候调用其中编写的检测函数),返回所有的检测结果,可以通过基础URPGTargetType去实现各种各样的检测方式,更加方便管理结构清晰。

UCLASS(Blueprintable, meta = (ShowWorldContextPin))
class ACTIONRPG_API URPGTargetType : public UObject
{
	GENERATED_BODY()

public:
	/** Called to determine targets to apply gameplay effects to */
	UFUNCTION(BlueprintNativeEvent)
	void GetTargets(ARPGCharacterBase* TargetingCharacter, 
    AActor* TargetingActor, 
    FGameplayEventData EventData, 
    TArray<FHitResult>& OutHitResults, 
    TArray<AActor*>& OutActors) const;
};

项目中实现的是蓝图类TargetType_SphereTrace,是一个球体射线检测。其中定义了如下几个值,检测起始点偏移,检测长度和半径,检测的对象类型,debug信息。

GAS/RPGAction学习笔记_第50张图片

实现方面只是很简单的实现了一个球体检测

GAS/RPGAction学习笔记_第51张图片

锤子第三段攻击的检测数据

GAS/RPGAction学习笔记_第52张图片GAS/RPGAction学习笔记_第53张图片

可以很明确的看出锤子攻击的检测范围

此外还继承实现了另外两个特殊的非蓝图类型TargetType用于处理特殊情况

  • URPGTargetType_UseOwner,用于处理GA目标是自己的情况
  • URPGTargetType_UseEventData,从FGameplayEventData中获取目标的情况,这个EventData在武器的蓝图中通过胶囊体碰撞检测生成的Hit Res生成后,通过SendGamePlayEventToActor发送

GAS/RPGAction学习笔记_第54张图片

UPRGGameplayAbility

有了TargetType检测方式以后,ARPG项目声明了一个FRPGGameplayEffectContainer类去将检测方式和GE对应起来,同时将container与GA对应,则一个GA可以使用多个GE,实现连击comboo的不同阶段

GAS/RPGAction学习笔记_第55张图片

表示这一组GE都是使用这种检测方式。

然后又声明了一个自己的实例类FRPGGameplayEffectContainerSpec,去存储FRPGGameplayEffectContainer中的GE在运行时生成的实例FGameplayEffectSpecHandle以及GA目标对象的实例FGameplayAbilityTargetDataHandle,以便后续将GE应用于GA。

GAS/RPGAction学习笔记_第56张图片

而GA的作用目标,就是通过TargetType类中的检测方法获取的。

void FRPGGameplayEffectContainerSpec::AddTargets(const TArray<FHitResult>& HitResults, const TArray<AActor*>& TargetActors)
{
    //处理单次hit伤害,结果封装在hitres中
	for (const FHitResult& HitResult : HitResults)
	{
		FGameplayAbilityTargetData_SingleTargetHit* NewData = new FGameplayAbilityTargetData_SingleTargetHit(HitResult);
		TargetData.Add(NewData);
	}

    //处理AOE的GE
	if (TargetActors.Num() > 0)
	{
		FGameplayAbilityTargetData_ActorArray* NewData = new FGameplayAbilityTargetData_ActorArray();
		NewData->TargetActorArray.Append(TargetActors);
		TargetData.Add(NewData);
	}
}

ARPG派生了GA,实现了自己的URPGGameplayAbility,使用上面提到的Container类,方便的将GA的目标检测,GE的应用,以及Tag联系起来,同时更便于GE对Trigger的触发(通过蒙太奇中的事件通知作为Trigger,事件触发时传入tag,从container中查找对应的GEs,并触发他们)。

一般来说不同的项目都会根据需要实现自己的GA,关键方法注释见下图

GAS/RPGAction学习笔记_第57张图片

贴一下相关的关键代码

FRPGGameplayEffectContainerSpec URPGGameplayAbility::MakeEffectContainerSpecFromContainer(
    const FRPGGameplayEffectContainer& Container, 
    const FGameplayEventData& EventData, 
    int32 OverrideGameplayLevel)
{
	// First figure out our actor info
	FRPGGameplayEffectContainerSpec ReturnSpec;
	AActor* OwningActor = GetOwningActorFromActorInfo();
	ARPGCharacterBase* OwningCharacter = Cast<ARPGCharacterBase>(OwningActor);
	URPGAbilitySystemComponent* OwningASC = URPGAbilitySystemComponent::GetAbilitySystemComponentFromActor(OwningActor);

	if (OwningASC)
	{
		// If we have a target type, run the targeting logic. This is optional, targets can be added later
		if (Container.TargetType.Get())
		{
			TArray<FHitResult> HitResults;
			TArray<AActor*> TargetActors;
			const URPGTargetType* TargetTypeCDO = Container.TargetType.GetDefaultObject();
			AActor* AvatarActor = GetAvatarActorFromActorInfo();
			//通过TargetType进行射线检测,拿到GA作用的目标
			TargetTypeCDO->GetTargets(OwningCharacter, AvatarActor, EventData, HitResults, TargetActors);
			//将检测结果赋值给GA target数据
			ReturnSpec.AddTargets(HitResults, TargetActors);
		}

		// If we don't have an override level, use the default on the ability itself
		if (OverrideGameplayLevel == INDEX_NONE)
		{
			OverrideGameplayLevel = OverrideGameplayLevel = this->GetAbilityLevel(); //OwningASC->GetDefaultAbilityLevel();
		}

		// Build GameplayEffectSpecs for each applied effect
		for (const TSubclassOf<UGameplayEffect>& EffectClass : Container.TargetGameplayEffectClasses)
		{
			//根据等级生成GE的实例
			ReturnSpec.TargetGameplayEffectSpecs.Add(MakeOutgoingGameplayEffectSpec(EffectClass, OverrideGameplayLevel));
		}
	}
	return ReturnSpec;
}

TArray<FActiveGameplayEffectHandle> URPGGameplayAbility::ApplyEffectContainerSpec(const FRPGGameplayEffectContainerSpec& ContainerSpec)
{
	TArray<FActiveGameplayEffectHandle> AllEffects;

	// Iterate list of effect specs and apply them to their target data
	for (const FGameplayEffectSpecHandle& SpecHandle : ContainerSpec.TargetGameplayEffectSpecs)
	{
		//直接应用GE
		AllEffects.Append(K2_ApplyGameplayEffectSpecToTarget(SpecHandle, ContainerSpec.TargetData));
	}
	return AllEffects;
}

GA_AbilityBase

蓝图派生了一个GA蓝图,GA_AbilityBase,其内定义了一个CallOnAbilityEnd委托(后续再看),再派生不同的RPGGA去实现各自的功能,游戏有四种GA:GA_MeleeBase, GA_SkillBase, GA_PotionBase, GA_SpawnProjectileBase

GAS/RPGAction学习笔记_第58张图片

一个一个看一下

GA_Meleebase

GAS/RPGAction学习笔记_第59张图片

战斗的GA只是简单的播放了一下蒙太奇,在蒙太奇动画中定义适时的notify,将tag通知到RPGGA的方法从而将container 中这个tag对应GE apply.

以斧头攻击的GA(GA_PlayerAxeMelee)为例

GAS/RPGAction学习笔记_第60张图片

这个GA指定了多段攻击的蒙太奇动画.具有一个Ability.Melee tag,代表是战斗GA,同时赋予的3种普通攻击的GE,可以看见三种攻击的tag和对应的GE

GAS/RPGAction学习笔记_第61张图片

可以看见在蒙太奇中,适当的时机会进行tag的通知,从而触发对应的target寻找和GE应用.

其他两种武器攻击的GA大同小异,值得一提的是SharedTag下是某些GA共享Tag,如hit tag,是所有武器普通攻击第一段的tag.

此外可以发现不同的GE的检测方式不同,23种是通过射线检测实现的,而普通工具的检测是通过weapon蓝图中胶囊体overlap事件检测,生成FGameplayEventData,再send到actor中的,所以其检测方式是URPGTargetType_UseOwner类型

GAS/RPGAction学习笔记_第62张图片

GAS/RPGAction学习笔记_第63张图片

拾取武器的GA其实和伤害攻击GA是一致的,定义了几段攻击方式,捡起时赋予玩家

GA_PotionBase

吃药GA同攻击GA一样,播放了一个蒙太奇动画,并在蒙太奇动画发出通知(TagName=“Event.Montage.Shared.UseItem”)的时候触发药的GE.不过最后还额外进行了药品库存的计算

GAS/RPGAction学习笔记_第64张图片

根据槽拿到item并数量-1

药品具体的GA也大同小异,以GA_PotionHealth血瓶GA为例

GAS/RPGAction学习笔记_第65张图片

可以看到这是一个持有物品的能力Ability.item,作用方式targetType是owner即作用于自己.GE_Health中定义了回复百分之50的血量.

GA_SkillBase

Base蓝图与Melee功能完全一致,只是Ability 的Tag是skill,表明这是一个技能

其实技能和攻击差不多,实现路径以及GAGE应用路径相同.

GAS/RPGAction学习笔记_第66张图片

只不过多了一个冷却以及魔法消耗的GE

GAS/RPGAction学习笔记_第67张图片

GA可以指定一个处理技能冷却的GameplayEffect。冷却决定了技能多长时间能够被再次施放,处在冷却中的技能无法被施放。Cooldown GE必须是一个Duration GameplayEffect,不带Modifiers,在GameplayEffect的GrantedTags (“Cooldown Tag”)中配置代表每个GameplayAbility 或Ability Slot(槽位装备技能)的唯一的GameplayTag。实际上是由GA检查Cooldown Tag而不是Cooldown GE。默认情况下,Cooldown GEs是支持预测的,所以最好不要使用ExecutionCalculations,建议只使用MMCs完成冷却计算。

刚开始,你可能会为每一个带有冷却的GA创建一个对应的Cooldown GE。高阶方法是,对于多个GAs复用一个Cooldown GE,仅通过GA的冷却数据(冷却时间和Cooldown Tag定义在GA上)修改从Cooldown GE创建出的GameplayEffectSpec。仅能用于****Instanced Abilities

GA可以指定一个处理技能消耗的GameplayEffect。如果无法满足消耗,则技能不会被激活。Cost GE必须是一个Instant GameplayEffect其中可以有一个或多个Modifiers用于减去技能所需的属性消耗。默认情况下,Cost GEs是支持预测的,所以最好不要使用ExecutionCalculations,建议只使用MMCs完成消耗计算。

刚开始,你可能会为每一个带有消耗的GA创建一个对应的Cost GE。高阶方法是,对于多个GAs复用一个Cost GE,仅通过GA的消耗数据(消耗值定义在GA上)修改从Cost GE创建出的GameplayEffectSpec。仅能用于****Instanced Abilities

详见GAS文档

GA_SpawnProjectileBase

投射物的GA,游戏中的体现是一个毒球和一个火球.这个ProjectileBase也是一种特殊的技能,触发GE的Trigger Tag同样为Event.Montage.Shared.UseSkill,能力Tag同样为Ability.Skill.不过GA蓝图有区别

前面几种GA,在触发tigger后会立即apply ge,但是projectile不一样,他只是预先生成GE实例,然后生成飞行物(注意这里将GE的container作为spawn参数传了进去,以便飞行物应用GE)并投射,当飞行物击中玩家时再触发GE.

GAS/RPGAction学习笔记_第68张图片

看看飞行物的蓝图BP_AbilityProjectileBase

其实和武器的蓝图逻辑大同小异(其实飞行物也算一种武器,笑),进行碰撞检测,然后通过container将事先预处理的GE作用到目标ASC上.

GAS/RPGAction学习笔记_第69张图片

具体飞行物的GA与skill的大同小异

GA部分的整体架构大概就如下所示,其实拆分一下也挺简洁明了的.

GAS/RPGAction学习笔记_第70张图片

AS

项目定义了自己的RPGAS,内含攻击防御生命魔法移动速度等属性

其中伤害(Damage)值没有定义同步回调OnRep_,因为这个值是临时在GE应用时计算得出的

Damage Done = Damage * AttackPower / DefensePower

AS重写了PreAttributeChange,去处理最大值(生命魔法)变化时,当前值与最大值的比例。

重写了PostGameplayEffectExecute,在GE作用后,处理相关事件,这里直接通过handle拿到GE的ownerActor和avatarActor并调用相关处理。

void URPGAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	Super::PostGameplayEffectExecute(Data);

	FGameplayEffectContextHandle Context = Data.EffectSpec.GetContext();
	UAbilitySystemComponent* Source = Context.GetOriginalInstigatorAbilitySystemComponent();
	const FGameplayTagContainer& SourceTags = *Data.EffectSpec.CapturedSourceTags.GetAggregatedTags();

	// Compute the delta between old and new, if it is available
	float DeltaValue = 0;
	if (Data.EvaluatedData.ModifierOp == EGameplayModOp::Type::Additive)
	{
		// If this was additive, store the raw delta value to be passed along later
		DeltaValue = Data.EvaluatedData.Magnitude;
	}

	// Get the Target actor, which should be our owner
	AActor* TargetActor = nullptr;
	AController* TargetController = nullptr;
	ARPGCharacterBase* TargetCharacter = nullptr;
	if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
	{
		TargetActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
		TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
		TargetCharacter = Cast<ARPGCharacterBase>(TargetActor);
	}

	if (Data.EvaluatedData.Attribute == GetDamageAttribute())
	{
		// Get the Source actor
		AActor* SourceActor = nullptr;
		AController* SourceController = nullptr;
		ARPGCharacterBase* SourceCharacter = nullptr;
		if (Source && Source->AbilityActorInfo.IsValid() && Source->AbilityActorInfo->AvatarActor.IsValid())
		{
			SourceActor = Source->AbilityActorInfo->AvatarActor.Get();
			SourceController = Source->AbilityActorInfo->PlayerController.Get();
			if (SourceController == nullptr && SourceActor != nullptr)
			{
				if (APawn* Pawn = Cast<APawn>(SourceActor))
				{
					SourceController = Pawn->GetController();
				}
			}

			// Use the controller to find the source pawn
			if (SourceController)
			{
				SourceCharacter = Cast<ARPGCharacterBase>(SourceController->GetPawn());
			}
			else
			{
				SourceCharacter = Cast<ARPGCharacterBase>(SourceActor);
			}

			// Set the causer actor based on context if it's set
			if (Context.GetEffectCauser())
			{
				SourceActor = Context.GetEffectCauser();
			}
		}

		// Try to extract a hit result
		FHitResult HitResult;
		if (Context.GetHitResult())
		{
			HitResult = *Context.GetHitResult();
		}

		// Store a local copy of the amount of damage done and clear the damage attribute
		const float LocalDamageDone = GetDamage();
		SetDamage(0.f);

		if (LocalDamageDone > 0)
		{
			// Apply the health change and then clamp it
			const float OldHealth = GetHealth();
			SetHealth(FMath::Clamp(OldHealth - LocalDamageDone, 0.0f, GetMaxHealth()));

			if (TargetCharacter)
			{
				// This is proper damage
				TargetCharacter->HandleDamage(LocalDamageDone, HitResult, SourceTags, SourceCharacter, SourceActor);

				// Call for all health changes
				TargetCharacter->HandleHealthChanged(-LocalDamageDone, SourceTags);
			}
		}
	}
	else if (Data.EvaluatedData.Attribute == GetHealthAttribute())
	{
		// Handle other health changes such as from healing or direct modifiers
		// First clamp it
		SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));

		if (TargetCharacter)
		{
			// Call for all health changes
			TargetCharacter->HandleHealthChanged(DeltaValue, SourceTags);
		}
	}
	else if (Data.EvaluatedData.Attribute == GetManaAttribute())
	{
		// Clamp mana
		SetMana(FMath::Clamp(GetMana(), 0.0f, GetMaxMana()));

		if (TargetCharacter)
		{
			// Call for all mana changes
			TargetCharacter->HandleManaChanged(DeltaValue, SourceTags);
		}
	}
	else if (Data.EvaluatedData.Attribute == GetMoveSpeedAttribute())
	{
		if (TargetCharacter)
		{
			// Call for all movespeed changes
			TargetCharacter->HandleMoveSpeedChanged(DeltaValue, SourceTags);
		}
	}
}

不过没太懂,在处理damage的段中,这里GetHealth 属性,拿到的为什么会时旧的值,GE不是自动应用的么,为什么还要SetHealth手动设置一下。然后后面的elseif中又没有手动设置,似是GetXXX 属性拿到的是当前值而不是旧值。。。后续看一下

原来使用自定义表达式ExecCalc AS不会自动结算,我将SetHealth去掉后不掉血了。。。亦或是我将ExecCalc去掉,使用原生的modified,也是扣血的。其实后面想一想也知道原因了,ExecCalc只是数值的计算,并没有指定对AS中某个值进行修改,只能计算得到临时值后手动处理。

GAS/RPGAction学习笔记_第71张图片

GAS/RPGAction学习笔记_第72张图片

这个AS是角色和怪物通用的。

GAS/RPGAction学习笔记_第73张图片

GE

GE_Damage 伤害GE的base类,作用方式是instant立即起效,项目实现了自己的RPGDamageExecution类去处理GE数据计算过程,同时伤害基础值是通过曲线表去拿的。此外可以看到TargetTag的ignore有个Status.DamageImmune标签,代表有这个标签的ASC会忽略此GE即无法造成伤害。

(这个BackingData选项指定了下面curvetable填充的值,最后的计算结果是 [旧值 Modifierop 表中对应的值])

GAS/RPGAction学习笔记_第74张图片

其中基础值是通过curveTable去取的,对于斧子攻击来说,全等级伤害相同。

img

看看这个计算类URPGDamageExecution

需要继承UGameplayEffectExecutionCalculation类并重载Execute方法

此外还需要使用DECLARE_ATTRIBUTE_CAPTUREDEF DEFINE_ATTRIBUTE_CAPTUREDEF两个宏去声明和定义捕捉输入表达式的变量

struct RPGDamageStatics
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower);
	DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);

	RPGDamageStatics()
	{
		// Capture the Target's DefensePower attribute. Do not snapshot it, because we want to use the health value at the moment we apply the execution.
		DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false);

		// Capture the Source's AttackPower. We do want to snapshot this at the moment we create the GameplayEffectSpec that will execute the damage.
		// (imagine we fire a projectile: we create the GE Spec when the projectile is fired. When it hits the target, we want to use the AttackPower at the moment
		// the projectile was launched, not when it hits).
		DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true);

		// Also capture the source's raw Damage, which is normally passed in directly via the execution
		DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true);
	}
};

static const RPGDamageStatics& DamageStatics()
{
	static RPGDamageStatics DmgStatics;
	return DmgStatics;
}

注意DEFINE_ATTRIBUTE_CAPTUREDEF最后一个bool参数,代表是否为这个值创建一个临时快照(在游戏中会提前创建好GE实例,比如GA投射飞行物时,在创建飞行物时GE实例已经创建好了),我们需要将Damage和AttackPower创建一个快照(最后一个参数为true),以保证应用GE时使用的是GE创建时的值(即飞行物的数据应该是在创建时就决定好了的,比如这个火球的基础伤害值时多少),而DefensePower需要实时获取(即飞行物命中时再去计算防御力)。

URPGDamageExecution::URPGDamageExecution()
{
	RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef);
	RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef);
	RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
}

void URPGDamageExecution::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
	UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();

	AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->GetAvatarActor_Direct() : nullptr;
	AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->GetAvatarActor_Direct() : nullptr;

	const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

	// Gather the tags from the source and target as that can affect which buffs should be used
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;

	// --------------------------------------
	//	Damage Done = Damage * AttackPower / DefensePower
	//	If DefensePower is 0, it is treated as 1.0
	// --------------------------------------

	float DefensePower = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DefensePowerDef, EvaluationParameters, DefensePower);
	if (DefensePower == 0.0f)
	{
		DefensePower = 1.0f;
	}

	float AttackPower = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackPowerDef, EvaluationParameters, AttackPower);

	float Damage = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);

	float DamageDone = Damage * AttackPower / DefensePower;
	if (DamageDone > 0.f)
	{
		OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().DamageProperty, EGameplayModOp::Additive, DamageDone));
	}
}

拿到目标和源ASC的tag,对值进行预处理后表达式计算结果再输出。

GE_DamageImmune、GE_GodMode,赋予角色免疫的tag,这两个GE完全相同(雾)

GAS/RPGAction学习笔记_第75张图片

GE_StateBase,用于管理角色的属性值信息,派生出怪物和角色的状态GE,根据等级覆盖旧值

GAS/RPGAction学习笔记_第76张图片

状态的值也是定义在curve table中,在StartingStats中

GAS/RPGAction学习笔记_第77张图片

有一个GE_SpiderCharge不知道是不是项目写错了。。。不知道伤害类技能为啥修改的是防御系数。

GAS/RPGAction学习笔记_第78张图片

整个GE大概的结构如下:

GAS/RPGAction学习笔记_第79张图片

Item

ActionRPG定义了一套自己的物品资源,结构如下,使用三个类FRPGItemSlot\FRPGItemData\URPGItem将物品槽-物品库存-物品资源 拆开,使用两个map进行组合绑定,以及声明物品相关事件

GAS/RPGAction学习笔记_第80张图片

项目将这套资源定义成了虚幻主资源PrimaryData(有四种 分为别 weapon,Token,Skill,Potion)。可以通过继承UPrimaryDataAsset并重写GetPrimaryAssetId方法注册自己的主资源。

主要主资源要在projectsetting中手动或代码注册,指定资源路径和资源类型。

GAS/RPGAction学习笔记_第81张图片

最后子蓝图进行派生,在编辑器中进行资源的设置,以weapon axe为例

GAS/RPGAction学习笔记_第82张图片

可以看到一个URPGItem指定了这个物品对应赋予角色的GA,GA等级,price(不同item解释不同,武器price为伤害,axs为100),item等级,以及相应的资源等等。

ActionRPG并且自己注册了一个新的拓展RPGAssetManager去控制资源加载卸载,并重载了AssetManager::StartInitialLoading(),在资源初始化时加载GAS的全局数据表和tag

void URPGAssetManager::StartInitialLoading()
{
	Super::StartInitialLoading();

	UAbilitySystemGlobals::Get().InitGlobalData();
}

还有一个接口去强制加载item资源

URPGItem* URPGAssetManager::ForceLoadItem(const FPrimaryAssetId& PrimaryAssetId, bool bLogWarning)
{	
	FSoftObjectPath ItemPath = GetPrimaryAssetPath(PrimaryAssetId);

	// This does a synchronous load and may hitch
	URPGItem* LoadedItem = Cast<URPGItem>(ItemPath.TryLoad());

	if (bLogWarning && LoadedItem == nullptr)
	{
		UE_LOG(LogActionRPG, Warning, TEXT("Failed to load item for identifier %s!"), *PrimaryAssetId.ToString());
	}

	return LoadedItem;
}

注意实现自己的AssetManager要在DefaultEngine.ini中手动指定类类型

GAS/RPGAction学习笔记_第83张图片

ASC

看了这么久终于能看到ASC了

GAS/RPGAction学习笔记_第84张图片

好像就如此简单。。。没啥好看的

GamePlay

Character

玩家角色同样继承了IAbilitySystemInterface,除此之外还继承了IAbilitySystemInterface,实现了

IAbilitySystemInterface::GetGenericTeamId用于玩家和AI队伍判定。

UCLASS()
class ACTIONRPG_API ARPGCharacterBase : public ACharacter, public IAbilitySystemInterface, public IGenericTeamAgentInterface
{
}

重写了PossessedBy去初始化Character的GAGE信息,绑定物品库存相关委托,注意这里也初始化了ASC的ActorInfo,InitAbilityActorInfo两个参数,第一个参数指定ASC的owner,ActionRPG中是玩家的Character,但实际网络项目中可能是PlayerState,第二个参数指定ASC作用的Actor,也是玩家Character,所以填入了两个this。

void ARPGCharacterBase::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

    、、、、、、
	// Initialize our abilities
	if (AbilitySystemComponent)
	{
		AbilitySystemComponent->InitAbilityActorInfo(this, this);
		AddStartupGameplayAbilities();
	}
}

在AddStartupGameplayAbilities中进行了GA的初始化,包括初始化设置的GAs以及物品栏的GA,然后初始化GE,这个GE就是GE_StatusBase,包含了玩家的基础生命值魔法值等信息。

此外这是查找一个GA 冷却的方法

bool ARPGCharacterBase::GetCooldownRemainingForTag(FGameplayTagContainer CooldownTags, float& TimeRemaining, float& CooldownDuration)
{
	if (AbilitySystemComponent && CooldownTags.Num() > 0)
	{
		TimeRemaining = 0.f;
		CooldownDuration = 0.f;

		FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
		TArray< TPair<float, float> > DurationAndTimeRemaining = AbilitySystemComponent->GetActiveEffectsTimeRemainingAndDuration(Query);
		if (DurationAndTimeRemaining.Num() > 0)
		{
			int32 BestIdx = 0;
			float LongestTime = DurationAndTimeRemaining[0].Key;
			for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
			{
				if (DurationAndTimeRemaining[Idx].Key > LongestTime)
				{
					LongestTime = DurationAndTimeRemaining[Idx].Key;
					BestIdx = Idx;
				}
			}

			TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
			CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;

			return true;
		}
	}
	return false;
}

当Controller被网络更新时,ASC的ActorInfo也应该及时更新

void ARPGCharacterBase::OnRep_Controller()
{
	Super::OnRep_Controller();

	// Our controller changed, must update ActorInfo on AbilitySystemComponent
	if (AbilitySystemComponent)
	{
		AbilitySystemComponent->RefreshAbilityActorInfo();
	}
}

看看character的蓝图

GAS/RPGAction学习笔记_第85张图片

其实也很简单,吃药和攻击主要就是激活对应物品slot的相应的能力,

skill的话就直接通过tag激活有对应tag的能力

GAS/RPGAction学习笔记_第86张图片

再来看看玩家的BP,翻滚没有走GAS,只是强制播放的一个蒙太奇,所以你可以发现在任何时候都可以强制无后摇翻滚

GAS/RPGAction学习笔记_第87张图片

游戏中的comboo系统是怎么做的呢

其实也很简单,在攻击时,先判断当前是否有在攻击即Melee Tag,如果没有,就激活武器槽对应的GA,获取Melee Tag,再次按下攻击后判断当前已经正在攻击,则设置蒙太奇播放下一段comboo动画。

GAS/RPGAction学习笔记_第88张图片

跳转蒙太奇section这里,可以根据蒙太奇notify的name去决定下一段攻击是什么,像斧子的话就只有一种第三段,剑的话第三段有三种,在三种里随机。

GAS/RPGAction学习笔记_第89张图片

敌人的蓝图也很简单就不说了

GAS/RPGAction学习笔记_第90张图片

montage

在招式的蒙太奇中定义了jumpsection的notify,去通知到character。

注意character的jumpcomboo函数中有一个bEnableComboPeriod变量用来控制是否进行下一段comboo,每段comboo触发后会置否,当下一次jumpsectionnotify触发时会置true,从而防止comboo的无限跳转(其实不用这个bool也没关系,不过melee时你持续触发comboo那个notify实例可能会为空会报错)

GAS/RPGAction学习笔记_第91张图片

看看其他的notify

img会在触发开始到结束期间给玩家一个GE_DamageImmune的GE,这就是许多动作游戏中的无敌帧的概念吧。

img阻塞玩家的移动,大风车技能不能进行自主移动

img还有一个Slomonotify,用于实现顿帧效果,即comboo里面有些招式的慢放,是通过SetGlobalTimeDilation实现的,也可以通过console命令实现

GAS/RPGAction学习笔记_第92张图片GAS/RPGAction学习笔记_第93张图片

详细的可以看看Slomo

img技能notify,简单的发送了下tag的event

GAS/RPGAction学习笔记_第94张图片

img普通攻击的notify

直接调用到武器蓝图的attack方法,在武器攻击时生效武器的碰撞体,攻击结束时禁用掉。

注意还传入了几个参数AttackDelayTime和MaxAttackDelayCount,用于实现击中怪物时的顿帧,实现一种打击感的效果。(ps,项目里AttackDelayTime没用到。。顿帧事件是写死的,不知道为啥)

GAS/RPGAction学习笔记_第95张图片

img用于攻击时的位移

GAS/RPGAction学习笔记_第96张图片

PlayerController

ARPGPlayerControllerBase继承了IRPGInventoryInterface,主要就是处理玩家物品栏相关的操作,内有两个map将item、slot和object相互映射,并声明相关的委托。

BP_Controller中主要做的就是UI创建,玩家移动控制、sequence播放等功能了。

其他的也没啥太大必要说了,就看到这吧,有坑后续再补。

大致学习了下GAS整体使用,ActionRPG只展示了部分用法,详细的等实际用到的时候再看看文档吧。。。详见GAS文档

其他

学习这个项目也给了我一些资产、代码管理的理解。

  • GameInstance处理保存,mode管理,全局数据

  • PlayerControll处理物品

  • GameMode处理游戏内容和UI

  • 暴露给蓝图的函数使用K2_前缀

  • UI蓝图使用WB_前缀(widgetBlueprint)

  • GE_,GA_不必说了

-Abilities

​  -xxx

​    -GA_ GE_ otherRelatived

​  -DataTable

-Animations

​  -xxx

​    -AM_ (Montage)

-Assets

​  -Sounds

​    -xxx

​      -A (Audio)

-Blueprints

​  -Core

​  -xxx

-Characters

​  -ABP_ (AnimationBP)

​  -BS_ (BlendSpace)

​  -PA_ (Physical Asset)

​  -SK_ (Skeleton)

​  -Tex

​  -M (Material)

​  -T (Texture)

-Effects

​  -FX_

​    -P_ (Particle)

​    -NS_ (Niagara System)

​  -XXX1

​    -SM_ (Static Mesh)

-Environment

​  -Foliage

​    -SM_XXX

​  -Materials

​    -MF (Material Function)

​    -MI (Material Instance)

​    -M

​  -Meshes

​    -SM_

​  -Physical

​  -Tex

-Maps

-Sequences

-UI

​  -Material

​  -Tex

​  -Curve

​  -Fonts

ActionRPG学习差不多到这,有错误的地方谢谢大佬指出。

你可能感兴趣的:(Ue4,ue4)