GAS文档https://docs.unrealengine.com/5.0/zh-CN/using-gameplay-abilities-in-unreal-engine/
堡垒之夜也是用的这个框架
其他的技能系统:
GA流程
sample
在模块中要引用
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)作用后的数值回滚。
MakeOutgoingGameplayEffectSpec指可以定一泛类型GE的一个模板,然后在GE生成时读表或者数据库动态改变。更好的是通过IDetailCustomization去定制GA的UI。
GAS中文文档https://blog.csdn.net/pirate310/article/details/106311256
GAS英文文档https://github.com/tranek/GASDocumentation
教学博客https://www.cnblogs.com/JackSamuel/p/7155500.html
https://www.bilibili.com/video/BV1zD4y1X77M?spm_id_from=333.999.0.0
l
ASC对于单机游戏可以放在Pawn上,对于联机游戏可以放在playstate上以广播和持续的保存
链接:https://pan.baidu.com/s/10U3QOB0oz7vMT1B-IXbMMw 提取码:uenb
先看看核心的tag是怎么标定的:
比较简单,定义了一层,分别是攻击,格挡,受击,主动
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是游戏逻辑的主要书写处
看看GA_Attack_Attr以此为例,梳理下流程。
GA_Attack_Attr的ability tag为positive,代表这是个主动效果,并且BlockAbilitiesWithTag也是positive tag,代表这个tag存在时会阻塞其他的positive tag。
这里通过GetActorInfo拿到此GA拥有者的相关信息,包括OwnerActor(攻击方)、AvatarActor(受击方)、SKMesh、ASC等。
在这个攻击动画蒙太奇中有一个NotifyName为Hit的event,播放攻击蒙太奇触发HitEvent,判断是否找到AvatarActor。
找到则在受击者的ASC上触发一个受击的效果GE_hit以及收到伤害扣血的效果GE_Damage。
在调用蒙太奇后直接调用了CommitAbility
,以提交技能进行计算如技能消耗等,这里不commit在场景中攻击就不会扣蓝。
CommitAbility的流程可以看下面,可以知道CommitAbility是最后可以判定GA失败的时机,不然之后GA就一定成功。
在蒙太奇播完后主动触发结束GE的event,GE结束时停止蒙太奇。
这里播放蒙太奇使用的是UE原生的节点,有说法是UE原生节点在网络同步时会有问题,一定要使用GAS的蒙太奇节点
GAS蒙太奇组合了tag和event,更加系统方便,同时还有自动结束蒙太奇的功能,比起原生节点更加强大,还是使用这个吧。。。
GA_Attack_Attr定义了攻击的消耗属性即GE_Cost(demo中的体现即为5点蓝耗)
应用策略是Instant立即生效(Infinite是持续生效,HasDuration是一定时间后生效),GE设置简单的对SampleAttributeSet.Physical(这个属性是需要代码自定义的)属性即蓝量应用计算 Add -5,
应用概率ChanceToAppleToTarget为1,必定生效。
来看看这个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,否则激活也不会生效。
整体性的大概看完了,看下一些零碎的点。
通过以下两个函数去拿到挂有ASC的acotr的属性cur值和base值
看看Demo场景中的block是怎么做的
格挡角色附带了一个新GA
这个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类,我又重新指定了一下就行了。
对于打断效果同样有一个对应的GA
对于GA_BeAttack_Break,将CancelAbilitiesWithTag设置为了Positive,代表这个GA激活时会取消掉Positive类型的GA,即打断的效果。同样的,取消后摇也可以通过这个cancel实现。
下面设置了GA的触发器,被Hit时触发Break
GE_Hit会为受击方添加Hit标签
最后贴一下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
添加游戏项目的额外模块要记得在项目的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看起
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
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信息。
实现方面只是很简单的实现了一个球体检测
锤子第三段攻击的检测数据
可以很明确的看出锤子攻击的检测范围
此外还继承实现了另外两个特殊的非蓝图类型TargetType用于处理特殊情况
有了TargetType检测方式以后,ARPG项目声明了一个FRPGGameplayEffectContainer
类去将检测方式和GE对应起来,同时将container与GA对应,则一个GA可以使用多个GE,实现连击comboo的不同阶段
表示这一组GE都是使用这种检测方式。
然后又声明了一个自己的实例类FRPGGameplayEffectContainerSpec
,去存储FRPGGameplayEffectContainer
中的GE在运行时生成的实例FGameplayEffectSpecHandle以及GA目标对象的实例FGameplayAbilityTargetDataHandle,以便后续将GE应用于GA。
而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,关键方法注释见下图
贴一下相关的关键代码
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蓝图,GA_AbilityBase,其内定义了一个CallOnAbilityEnd委托(后续再看),再派生不同的RPGGA去实现各自的功能,游戏有四种GA:GA_MeleeBase, GA_SkillBase, GA_PotionBase, GA_SpawnProjectileBase
一个一个看一下
战斗的GA只是简单的播放了一下蒙太奇,在蒙太奇动画中定义适时的notify,将tag通知到RPGGA的方法从而将container 中这个tag对应GE apply.
以斧头攻击的GA(GA_PlayerAxeMelee)为例
这个GA指定了多段攻击的蒙太奇动画.具有一个Ability.Melee tag,代表是战斗GA,同时赋予的3种普通攻击的GE,可以看见三种攻击的tag和对应的GE
可以看见在蒙太奇中,适当的时机会进行tag的通知,从而触发对应的target寻找和GE应用.
其他两种武器攻击的GA大同小异,值得一提的是SharedTag下是某些GA共享Tag,如hit tag,是所有武器普通攻击第一段的tag.
此外可以发现不同的GE的检测方式不同,23种是通过射线检测实现的,而普通工具的检测是通过weapon蓝图中胶囊体overlap事件检测,生成FGameplayEventData,再send到actor中的,所以其检测方式是URPGTargetType_UseOwner类型
拾取武器的GA其实和伤害攻击GA是一致的,定义了几段攻击方式,捡起时赋予玩家
吃药GA同攻击GA一样,播放了一个蒙太奇动画,并在蒙太奇动画发出通知(TagName=“Event.Montage.Shared.UseItem”)的时候触发药的GE.不过最后还额外进行了药品库存的计算
根据槽拿到item并数量-1
药品具体的GA也大同小异,以GA_PotionHealth血瓶GA为例
可以看到这是一个持有物品的能力Ability.item,作用方式targetType是owner即作用于自己.GE_Health中定义了回复百分之50的血量.
Base蓝图与Melee功能完全一致,只是Ability 的Tag是skill,表明这是一个技能
其实技能和攻击差不多,实现路径以及GAGE应用路径相同.
只不过多了一个冷却以及魔法消耗的GE
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,游戏中的体现是一个毒球和一个火球.这个ProjectileBase也是一种特殊的技能,触发GE的Trigger Tag同样为Event.Montage.Shared.UseSkill,能力Tag同样为Ability.Skill.不过GA蓝图有区别
前面几种GA,在触发tigger后会立即apply ge,但是projectile不一样,他只是预先生成GE实例,然后生成飞行物(注意这里将GE的container作为spawn参数传了进去,以便飞行物应用GE)并投射,当飞行物击中玩家时再触发GE.
看看飞行物的蓝图BP_AbilityProjectileBase
其实和武器的蓝图逻辑大同小异(其实飞行物也算一种武器,笑),进行碰撞检测,然后通过container将事先预处理的GE作用到目标ASC上.
具体飞行物的GA与skill的大同小异
GA部分的整体架构大概就如下所示,其实拆分一下也挺简洁明了的.
项目定义了自己的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中某个值进行修改,只能计算得到临时值后手动处理。
这个AS是角色和怪物通用的。
GE_Damage 伤害GE的base类,作用方式是instant立即起效,项目实现了自己的RPGDamageExecution类去处理GE数据计算过程,同时伤害基础值是通过曲线表去拿的。此外可以看到TargetTag的ignore有个Status.DamageImmune标签,代表有这个标签的ASC会忽略此GE即无法造成伤害。
(这个BackingData选项指定了下面curvetable填充的值,最后的计算结果是 [旧值 Modifierop 表中对应的值])
其中基础值是通过curveTable去取的,对于斧子攻击来说,全等级伤害相同。
看看这个计算类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完全相同(雾)
GE_StateBase,用于管理角色的属性值信息,派生出怪物和角色的状态GE,根据等级覆盖旧值
状态的值也是定义在curve table中,在StartingStats中
有一个GE_SpiderCharge不知道是不是项目写错了。。。不知道伤害类技能为啥修改的是防御系数。
整个GE大概的结构如下:
ActionRPG定义了一套自己的物品资源,结构如下,使用三个类FRPGItemSlot\FRPGItemData\URPGItem将物品槽-物品库存-物品资源 拆开,使用两个map进行组合绑定,以及声明物品相关事件
项目将这套资源定义成了虚幻主资源PrimaryData(有四种 分为别 weapon,Token,Skill,Potion)。可以通过继承UPrimaryDataAsset并重写GetPrimaryAssetId方法注册自己的主资源。
主要主资源要在projectsetting中手动或代码注册,指定资源路径和资源类型。
最后子蓝图进行派生,在编辑器中进行资源的设置,以weapon axe为例
可以看到一个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中手动指定类类型
看了这么久终于能看到ASC了
好像就如此简单。。。没啥好看的
玩家角色同样继承了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的蓝图
其实也很简单,吃药和攻击主要就是激活对应物品slot的相应的能力,
skill的话就直接通过tag激活有对应tag的能力
再来看看玩家的BP,翻滚没有走GAS,只是强制播放的一个蒙太奇,所以你可以发现在任何时候都可以强制无后摇翻滚
游戏中的comboo系统是怎么做的呢
其实也很简单,在攻击时,先判断当前是否有在攻击即Melee Tag,如果没有,就激活武器槽对应的GA,获取Melee Tag,再次按下攻击后判断当前已经正在攻击,则设置蒙太奇播放下一段comboo动画。
跳转蒙太奇section这里,可以根据蒙太奇notify的name去决定下一段攻击是什么,像斧子的话就只有一种第三段,剑的话第三段有三种,在三种里随机。
敌人的蓝图也很简单就不说了
在招式的蒙太奇中定义了jumpsection的notify,去通知到character。
注意character的jumpcomboo函数中有一个bEnableComboPeriod变量用来控制是否进行下一段comboo,每段comboo触发后会置否,当下一次jumpsectionnotify触发时会置true,从而防止comboo的无限跳转(其实不用这个bool也没关系,不过melee时你持续触发comboo那个notify实例可能会为空会报错)
看看其他的notify
会在触发开始到结束期间给玩家一个GE_DamageImmune的GE,这就是许多动作游戏中的无敌帧的概念吧。
还有一个Slomonotify,用于实现顿帧效果,即comboo里面有些招式的慢放,是通过SetGlobalTimeDilation实现的,也可以通过console命令实现
详细的可以看看Slomo
普通攻击的notify
直接调用到武器蓝图的attack方法,在武器攻击时生效武器的碰撞体,攻击结束时禁用掉。
注意还传入了几个参数AttackDelayTime和MaxAttackDelayCount,用于实现击中怪物时的顿帧,实现一种打击感的效果。(ps,项目里AttackDelayTime没用到。。顿帧事件是写死的,不知道为啥)
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学习差不多到这,有错误的地方谢谢大佬指出。