前景提醒:下面的部分含有大量MMO部分(迫真)
↑XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX↑
完成这个挺麻烦的,要做的话做好心理准备和一大堆东西的增加和修改。建议先看看内容再决定要不要进行项目的更改。或者参考隔壁的升级文章
1.UE5多人MOBA+GAS 27、死亡被动(用于作为击杀奖励,爆金币和是增加经验)-CSDN博客
2.UE5多人MOBA+GAS 28、创建资产类来管理GAS通用的资产、设置经验表来升级以及用MMC计算升级添加的属性值-CSDN博客
两篇完成。

不说那么多吓人的了,这一块介绍实现逻辑。
MVC架构(Model-View-Controller)。M与V通过C保持数据同步(好吧我也不是很懂
反正简单理解就是,Model进行逻辑处理,View就是能看得到的,Controller就是幕后工作,一个在Model与View之间进行传信的(Model或者View有任何改变都可以通过Controller告知对方并传输数据)
所以,接下来我们就要进行预备工作。因为原来的项目里面是没有Controller,所以我们现在就要创建。每隔一点内容我就会总结一下目前创建了什么东西。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
在此内容中,我们目前拥有的组件
WarriorWidgetBase.h
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1。创建Controller,类型为UObject,命名为WidgetController_Base.h
接着,再创建一个继承于这个Base类,命名为OverlayWidgetController.h
(现在我们拥有了两个Controller类)
2。接着,我们需要进入WarriorWidgetBase.h,我们在里面需要一个Controller类的对象。所以
public:
UPROPERTY(BlueprintReadOnly) //蓝图可读取,但不能修改
TObjectPtr<UObject> WidgetController;
然后再添加一个用于蓝图的函数,这个函数在控制器被设置后就调用
protected:
UFUNCTION(BlueprintImplementableEvent) //c++里不能定义,可以调用,蓝图中 无返回值可作为通知,有返回值还可以覆盖重写
void WidgetControllerSet();
再添加一个函数,在蓝图内我们可以通过此函数修改别的Widget的Controller
public:
UFUNCTION(BlueprintCallable) //蓝图可调用
void SetWidgetController(UObject* InWidgetController);
void UWarriorWidgetBase::SetWidgetController(UObject* InWidgetController)
{
WidgetController = InWidgetController;
WidgetControllerSet();
}
3。因为原来的项目里没有为玩家创建的PlayerState,所以我们自己创建一个PlayerState.命名为XMBState
接下来进入WidgetControllerBase,我们需要在这里面设置几个变量。
protected:
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<AXMBPlayerController> PlayerController;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<APlayerState> PlayerState;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY(BlueprintReadOnly, Category="WidgetController")
TObjectPtr<UAttributeSet> AttributeSet;
然后添加一个结构,用于设置控制器的相关引用
USTRUCT(BlueprintType)
struct FWidgetControllerParams
{
GENERATED_BODY()
FWidgetControllerParams(){}
FWidgetControllerParams(AXMBPlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
: PlayerController(PC),
PlayerState(PS),
AbilitySystemComponent(ASC),
AttributeSet(AS)
{}
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<AXMBPlayerController> PlayerController = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<APlayerState> PlayerState = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UAttributeSet> AttributeSet = nullptr;
};
继续添加一个函数,这个函数用于设置控制器的相关参数
UFUNCTION(BlueprintCallable)
void SetWidgetControllerParams(const FWidgetControllerParams& WCParams);
void UWidgetController_Base::SetWidgetControllerParams(const FWidgetControllerParams& WCParams)
{
PlayerController = WCParams.PlayerController;
PlayerState = WCParams.PlayerState;
AbilitySystemComponent = WCParams.AbilitySystemComponent;
AttributeSet = WCParams.AttributeSet;
}
4。接着,创建HUD,命名为XMBHUD
在HUD内,我们创建一个Widget变量
public:
UPROPERTY()
TObjectPtr<UWarriorWidgetBase> OverlayWidget;
然后再创建一个用于存储实例化的OverlayWidgetClass类,创建一个实例化的OverlayWidgetController,创建一个OverlayWidgetControllerClass
public:
UPROPERTY(EditAnywhere)
TSubclassOf<UWarriorWidgetBase> OverlayWidgetClass;
private:
UPROPERTY()
TObjectPtr<UOverlayWidgetController> OverlayWidgetController;
UPROPERTY(EditAnywhere)
TSubclassOf<UOverlayWidgetController> OverlayWidgetControllerClass;
然后创建一个函数,用于获取实例的控制器。并且控制器的实例化,需要在HUD里进行。
控制器的实例若有就返回,没有就在此创建。
UOverlayWidgetController* AXMBHUD::GetOverlayWidgetController(const FWidgetControllerParams& WCParams)
{
if(OverlayWidgetController == nullptr)
{
OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
OverlayWidgetController->SetWidgetControllerParams(WCParams);
}
return OverlayWidgetController;
}
然后再创建一个用于初始Widget和Controller的函数,Widget和Controller的初始化以及创建在HUD内实现。
//初始化用户控件和控制器层对象
void InitOverlay(AXMBPlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS);
void AXMBHUD::InitOverlay(AXMBPlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
{
checkf(OverlayWidgetClass, TEXT("OverlayWidgetClass 未设置,请在HUD上面设置")); //会打印相关信息到log列表
checkf(OverlayWidgetControllerClass, TEXT("OverlayWidgetControllerClass 未设置,请在HUD上面设置"));
if (GetWorld())
{
UWarriorWidgetBase* Widget = CreateWidget<UWarriorWidgetBase>(PC, OverlayWidgetClass); //创建Overlay用户控件
OverlayWidget = Cast<UWarriorWidgetBase>(Widget);
Widget->AddToViewport(); //添加到视口
}
}
然后,我们需要对HUD进行初始化。
打开WarriorPlayer,创建一个用于初始化HUD的函数
protected
UFUNCTION(BlueprintCallable)
void InitAbilityActorInfo(AXMBPlayerController* PlayerController);
void AWarriorPlayer::InitAbilityActorInfo(AXMBPlayerController* PlayerController)
{
AXMBState* PlayerStateBase = Cast<AXMBState>(GetPlayerState());
check(PlayerStateBase); //检测是否有效,无限会暂停游戏
//获取PC
if(PlayerController)
{
if(AXMBHUD* HUD = Cast<AXMBHUD>(PlayerController->GetHUD()))
{
HUD->InitOverlay(PlayerController, PlayerStateBase, GetXMBAbilitySystemComponent(), GetXMBAttributeSet());
}
}
}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
目前我们已经创建了一个ControllerBase,OverlayController,HUD,并且在WarriorPlayer内设置了HUD的初始化
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5。接着,启动项目。
创建XMBHUD的蓝图版本

(此处的Controller就先用CPP版本)

接下来测试有没有设置成功,打开OverlayWidget

接着,我们需要打开GA_DrawOverlayWidget

运行后,会打印出当前控制器的名称。
6。打开WidgetController_Base,创建函数
virtual void BroadcastInitialValues();
然后进入OverlayWidgetController覆写它
virtual void BroadcastInitialValues() override;
virtual void BindCallbacksToDependencies() override;
void UOverlayWidgetController::BroadcastInitialValues()
{
}
接下来我们需要找一个位置调用这个函数,因为OverlayWidgetController是在HUD里面进行实例化的,所以我们需要在HUD里面进行调用。在HUD的InitOverlay()内。
(首先需要设置控制器层,这时会触发蓝图里面可以调用的设置控制器层回调通知,用户控件会以此来绑定广播回调。然后再广播初始的值,用户控件里就实现了对值的初始化设置。)
void AXMBHUD::InitOverlay(AXMBPlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
{
checkf(OverlayWidgetClass, TEXT("OverlayWidgetClass 未设置,请在HUD上面设置")); //会打印相关信息到log列表
checkf(OverlayWidgetControllerClass, TEXT("OverlayWidgetControllerClass 未设置,请在HUD上面设置"));
if (GetWorld())
{
UWarriorWidgetBase* Widget = CreateWidget<UWarriorWidgetBase>(PC, OverlayWidgetClass); //创建Overlay用户控件
OverlayWidget = Cast<UWarriorWidgetBase>(Widget);
//需要设置一个当前widget的变量
const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS); //创建参数结构体
OverlayWidgetController = GetOverlayWidgetController(WidgetControllerParams); //获取控制器层
OverlayWidget->SetWidgetController(OverlayWidgetController); //设置用户控件的控制器层
OverlayWidgetController->BroadcastInitialValues();
Widget->AddToViewport(); //添加到视口
}
}
然后,我们修改OverLaiWidgetController
UCLASS(BlueprintType,Blueprintable)
class ARPG_GRIVITY_API UOverlayWidgetController : public UWidgetController_Base
7。此处,我们创建经验条,等级,还有BP_OverlayWidgetController
经验条的创建直接复制TPWBP

等级数字直接创建一个warriorwidget

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
截止至上一次,在HUD内对OverlayWidgetController进行了初始化。添加至视口测试成功,可以输出当前OverlayWIdgetController的名称。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
以上完成了MVC的搭建
接下来是MVC的使用(实现角色升级的系统)
8。我们需要创建一个角色升级的数据资产。命名为PlayerDataAsset_LevelUp.我们用它来存储每升一级需要的经验
在这个DA内,我们创建一个用于存储有关升级的数据(可自己拓展)
//角色升级数据结构体
USTRUCT(BlueprintType)
struct FXMBLevelUpInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly)
int32 LevelUpRequirement = 0; //升到此等级所需经验值
};
public:
UPROPERTY(EditDefaultsOnly)
TArray<FXMBLevelUpInfo> LevelUpInformation; //当前所有等级的升级数据
float FindLevelForXP(float XP); //通过经验值值获取角色的等级
float UPlayerDataAsset_LevelUp::FindLevelForXP(float XP)
{
//找到对应的等级
int32 Level = 1;
bool bSearching = true;
while (bSearching)
{
//检查升级信息
if (LevelUpInformation.Num() - 1 <= Level) return Level;
if (XP >= LevelUpInformation[Level].LevelUpRequirement) ++Level;
else bSearching = false;
}
return Level;
}
启动项目,创建这个类的蓝图

因为角色的初始等级为1,所以索引为0的等级我们就填入0。以上,从1级到2级需要的总经验为5,从2级到3级的总经验为5+15=20。
9。然后我们打开XMBState.h。
我们在此设置委托
//等级改变
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPlayerStateChanged,float/*XP*/);
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnLevelChanged, int32/*StatValue*/,bool/*bLevelUp*/)
public:
FOnPlayerStateChanged OnXPChangedDelegate; //经验值变动委托
FOnLevelChanged OnLevelChangedDelegate; //等级变动委托
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UPlayerDataAsset_LevelUp> LevelUpInfo; //设置升级相关数据
private:
UPROPERTY(VisibleAnywhere)
float Level = 1.f;
UPROPERTY(VisibleAnywhere)
float XP = 0.f;
然后,给经验与等级制作添改查函数,用于操作修改属性
public:
FORCEINLINE float GetPlayerLevel() const {return Level;} //获取角色等级
void AddToLevel(float InLevel); //增加等级
void SetLevel(float InLevel); //设置当前等级
FORCEINLINE float GetXP() const {return XP;} //获取角色当前经验值
void AddToXP(float InXP); //增加经验值
void SetXP(float InXP); //设置当前经验值
void AXMBState::AddToLevel(float InLevel)
{
Level += InLevel;
OnLevelChangedDelegate.Broadcast(Level,true);
}
void AXMBState::SetLevel(float InLevel)
{
Level = InLevel;
OnLevelChangedDelegate.Broadcast(Level,false);
}
void AXMBState::AddToXP(float InXP)
{
XP += InXP;
OnXPChangedDelegate.Broadcast(XP);//执行广播
}
void AXMBState::SetXP(float InXP)
{
XP += InXP;
OnXPChangedDelegate.Broadcast(XP);
}
XMBState完整代码如下
XMBState.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemInterface.h"
#include "AbilitySystem/Abilities/XMBGameplayAbility.h"
#include "Data/LevelUp/PlayerDataAsset_LevelUp.h"
#include "GameFramework/PlayerState.h"
#include "XMBState.generated.h"
//等级改变
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPlayerStateChanged,float/*XP*/);
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnLevelChanged, int32/*StatValue*/,bool/*bLevelUp*/)
/**
*
*/
UCLASS()
class ARPG_GRIVITY_API AXMBState : public APlayerState ,public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AXMBState();
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override; //覆盖虚函数获取asc
UAttributeSet* GetAttributeSet() const { return AttributeSet; } //获取as
FOnPlayerStateChanged OnXPChangedDelegate; //经验值变动委托
FOnLevelChanged OnLevelChangedDelegate; //等级变动委托
UPROPERTY(EditDefaultsOnly)
TObjectPtr<UPlayerDataAsset_LevelUp> LevelUpInfo; //设置升级相关数据
FORCEINLINE float GetPlayerLevel() const {return Level;} //获取角色等级
void AddToLevel(float InLevel); //增加等级
void SetLevel(float InLevel); //设置当前等级
FORCEINLINE float GetXP() const {return XP;} //获取角色当前经验值
void AddToXP(float InXP); //增加经验值
void SetXP(float InXP); //设置当前经验值
protected:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
UPROPERTY()
TObjectPtr<UAttributeSet> AttributeSet;
private:
UPROPERTY(VisibleAnywhere)
float Level = 1.f;
UPROPERTY(VisibleAnywhere)
float XP = 0.f;
};
XMBState.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "State/XMBState.h"
#include "AbilitySystem/XMBAbilitySystemComponent.h"
#include "AbilitySystem/XMBAttributeSet.h"
AXMBState::AXMBState()
{
AbilitySystemComponent = CreateDefaultSubobject<UXMBAbilitySystemComponent>("AbilitySystemComponent");
AttributeSet = CreateDefaultSubobject<UXMBAttributeSet>("AttributeSet");
}
UAbilitySystemComponent* AXMBState::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
void AXMBState::AddToLevel(float InLevel)
{
Level += InLevel;
OnLevelChangedDelegate.Broadcast(Level,true);
}
void AXMBState::SetLevel(float InLevel)
{
Level = InLevel;
OnLevelChangedDelegate.Broadcast(Level,false);
}
void AXMBState::AddToXP(float InXP)
{
XP += InXP;
OnXPChangedDelegate.Broadcast(XP);//执行广播
}
void AXMBState::SetXP(float InXP)
{
XP += InXP;
OnXPChangedDelegate.Broadcast(XP);
}
10。接下来制作更新经验条的委托,进入OverlayWidgetController.h
//用于属性值改变的委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAttributeChangedSignature,float, NewXP);
//用于等级改变的委托
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnLevelChangedSignature, float, NewLevel, bool, bLevelUp);
UPROPERTY(BlueprintAssignable, Category="GAS|Attributes")
FOnAttributeChangedSignature OnXPPercentChangedDelegate;
UPROPERTY(BlueprintAssignable, Category="GAS|Level")
FOnLevelChangedSignature OnPlayerLevelChangeDelegate; //等级变动回
然后创建一个函数,用于监听经验被动后的回调,在经验变动后,重新计算经验百分比。
void OnXPChanged(float NewXP) const; //经验变动后的回调
在BindCallBacksToDependencies()内,我们对经验与等级绑定回调函数
void UOverlayWidgetController::BindCallbacksToDependencies()
{
const UXMBAttributeSet* AttributeSetBase = CastChecked<UXMBAttributeSet>(AttributeSet);
AXMBState* XMBPlayerState = CastChecked<AXMBState>(PlayerState);
//绑定经验相关回调
XMBPlayerState->OnXPChangedDelegate.AddUObject(this,&ThisClass::OnXPChanged);
//绑定等级相关回调
XMBPlayerState->OnLevelChangedDelegate.AddLambda([this](float NewLevel,bool bLevelUp)
{
OnPlayerLevelChangeDelegate.Broadcast(NewLevel,bLevelUp);
});
}
经验回调函数,在接收到经验值发生变动后,重新计算经验条的百分比,并向经验条广播这个百分比。
void UOverlayWidgetController::OnXPChanged(float NewXP) const
{
const AXMBState* XMBPlayerState = CastChecked<AXMBState>(PlayerState);
UPlayerDataAsset_LevelUp* LevelUpInfo = XMBPlayerState->LevelUpInfo;
checkf(LevelUpInfo, TEXT("无法查询到等级相关数据,请查看PlayerState是否设置了对应的数据"));
const float Level = LevelUpInfo->FindLevelForXP(NewXP); //获取当前等级
const float MaxLevel = LevelUpInfo->LevelUpInformation.Num(); //获取当前最大等级
if(Level <= MaxLevel && Level > 0)
{
const float LevelUpRequirement = LevelUpInfo->LevelUpInformation[Level].LevelUpRequirement; //当前等级升级所需经验值
const float PreviousLevelUpRequirement = LevelUpInfo->LevelUpInformation[Level-1].LevelUpRequirement; //上一级升级所需经验值
const float XPPercent = static_cast<float>(NewXP - PreviousLevelUpRequirement) / (LevelUpRequirement - PreviousLevelUpRequirement); //计算经验百分比
OnXPPercentChangedDelegate.Broadcast(XPPercent); //广播经验条比例
}
}
启动项目,我们为敌人们添加击杀的经验奖励

11。设置好表格后,我们需要按照表格里的类型获取到经验。我们将在XMBSurvialGameMode.h内设置表格与类型。
先打开Data_EnemyStartUpData.h,创建一个结构用于设置敌人的经验表格(这里看着不顺眼的话也可以移入GameMode内。)
USTRUCT(BlueprintType)
struct FCharacterClassXPInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly,Category = "Class Defaults | XP")
FScalableFloat XPReward = FScalableFloat();
};
然后打开DataAsset_StartUpDataBase.h,我们需要在这里创建一个存储角色类型的枚举(我还没有完成boss与小怪的区分,以后这里会修改)
//角色类型
UENUM()
enum class EXMBCharacterClass
{
Elementalist,//法师
Warrior,//战士
Range//远程
};
然后打开XMBSurvialGameMode.h,创建类型与表格的tmap
public:
UPROPERTY(EditDefaultsOnly,Category = "Character Class Defaults")
TMap<EXMBCharacterClass,FCharacterClassXPInfo> XMBEnemyClassInfo;
这样子我们就设置好对应类型的经验表格了。然后我们需要对敌人进行类型区分。打开CharacterBase.h
Protected:
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category = "Character Class Defaults")
EXMBCharacterClass CharacterClass = EXMBCharacterClass::Warrior;
12。接下来制作通过敌人类型获取经验的函数
//根据敌人类型和玩家等级/难度设置玩家获取经验的值
UFUNCTION(BlueprintCallable,Category = "XMBWorrior|FunctionLibrary",meta = (Player = nullptr))
static float GetXPRewardForClassAndLevel(const UObject* WorldContextObject,AEnemyBase* Enemy, EXMBCharacterClass EnemyClass ,AWarriorPlayer* Player,UWorld* World);
float UXMBWarriorFunctionLibrary::GetXPRewardForClassAndLevel(const UObject* WorldContextObject,AEnemyBase* Enemy, EXMBCharacterClass EnemyClass ,AWarriorPlayer* Player,UWorld* World)
{
//从实例获取到关卡角色的配置
check(WorldContextObject);
float level = Enemy->AbilityApplyLevel;
// UDataAsset_EnemyStartUpData* CharacterData_StartUp = GetCharacterClassInfo(WorldContextObject);
FCharacterClassXPInfo XPInfo = XMBGetCharacterClassInfo(WorldContextObject,EnemyClass,Enemy);
const float XPReward = XPInfo.XPReward.GetValueAtLevel(level);
return XPReward;
}
这个函数有很多可以更改的,比如根据游戏难度获取经验倍率等。
再制作一个函数,用于从GameMode内获取角色的信息
//从gamemode里获取到角色的信息
UFUNCTION(BlueprintCallable,Category = "AuraAbilitySystemLibrary|CharacterClassDefaults")
static FCharacterClassXPInfo XMBGetCharacterClassInfo(const UObject* WorldContextObject, EXMBCharacterClass EnemyClass,AEnemyBase* Enemy);
FCharacterClassXPInfo UXMBWarriorFunctionLibrary::XMBGetCharacterClassInfo(const UObject* WorldContextObject,EXMBCharacterClass EnemyClass, AEnemyBase* Enemy)
{
const AXMBSurvialGameMode* InGameMode = Cast<AXMBSurvialGameMode>(UGameplayStatics::GetGameMode(WorldContextObject));
if (InGameMode == nullptr) return FCharacterClassXPInfo();
return *InGameMode->XMBEnemyClassInfo.Find(EnemyClass);
}
接下来我们需要获取角色的类型。打开PawnCombatInterface.h
//获取角色当前的职业类型
UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
EXMBCharacterClass GetCharacterClass();
然后在CharacterBase.h内覆写
virtual EXMBCharacterClass GetCharacterClass_Implementation() override;
EXMBCharacterClass ACharacterBase::GetCharacterClass_Implementation()
{
return CharacterClass;
}
13。接下来创建经验值,打开AttributeSet.h
UPROPERTY(BlueprintReadOnly, Category = "Difficulty")
FGameplayAttributeData IncomingXP;//处理传入的经验
ATTRIBUTE_ACCESSORS(UXMBAttributeSet, IncomingXP)
UXMBAttributeSet::UXMBAttributeSet()
{
InitIncomingXP(0.f);
}
打开XMBGameplayTag
/* Attribute Tag */
ARPG_GRIVITY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Attributes_Meta_IncomingXP);
/* Attribute Tag */
UE_DEFINE_GAMEPLAY_TAG(Attributes_Meta_IncomingXP, "Attributes.Meta.IncomingXP");
然后启动项目,我们创建一个GE,命名为GE_Player_XPReward,用于监听经验改动并设置经验。

然后再创建一个GA,命名为GA_ListenForEvent,其中,通过监听XP用于激活刚刚创建好的GE。
这里需要注意的是,其一,匹配标签处需要将精确匹配去掉,这样子可以匹配到其中的"子类"。
其二,需要创建一个GE变量,并且将这个变量设置为刚刚创建好的GE。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
创建了角色升级的数据资产。在XMBState内设置了等级改变的委托,制作了等级与经验改变的添加、修改、查找的函数。在OverlayWidgetController内制作了等级与属性(经验时AttributeSet的一员)的改变委托,还制作了绑定回调函数的函数。在DATA内制作了角色的类型分类。在Library内制作了通过击杀敌人的敌人类型获取经验值的函数。新建了一个GE与GA用于监听经验事件。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14。接下来我们需要将这个GA设置为玩家的被动技能。
打开CharacterBase.h,在此处创建一个被动技能的变量(此处我考虑到将来敌人也会有被动技能,若不考虑敌人身上的话可以将其放置Player内)
//被动
UPROPERTY(EditAnywhere, Category="Attributes")
TArray<TSubclassOf<UGameplayAbility>> StartUpPassiveAbilities; //角色初始被动技能设置
打开XMBAbilitySystemComponent.h,我们需要在ASC内对被动技能进行激活
//应用被动技能
void AddCharacterPassiveAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartUpPassiveAbilities);
void UXMBAbilitySystemComponent::AddCharacterPassiveAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartUpPassiveAbilities)
{
//被动数量
for(const TSubclassOf<UGameplayAbility> AbilityClass : StartUpPassiveAbilities)
{
FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
GiveAbilityAndActivateOnce(AbilitySpec); //应用技能并激活一次
}
}
我们在ASC内设置了激活函数,接下来我们需要找一个位置用于调用ASC内的这个激活函数。因为玩家和敌人都需要被动,所以我们进入CharacterBase.h内进行被动技能的应用
//激活角色的被动技能
void AddCharacterAbilities() const;
通过判断被动技能是否为空以此来作为应用被动技能的判断
void ACharacterBase::AddCharacterAbilities() const
{
UXMBAbilitySystemComponent* ASC = CastChecked<UXMBAbilitySystemComponent>(GetAbilitySystemComponent());
// if(!HasAuthority()) return; //查询是否拥有网络权限,应用技能需要添加给服务器
if (!StartUpPassiveAbilities.IsEmpty())
{
//调用初始化被动技能
ASC->AddCharacterPassiveAbilities(StartUpPassiveAbilities);
}
}
再在角色的初始化内进行调用就可。找到PossessedBy函数,在其中添加被动技能的应用。
void ACharacterBase::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (XMBAbilitySystemComponent)
{
XMBAbilitySystemComponent->InitAbilityActorInfo(this, this);
AddCharacterAbilities();
ensureMsgf(!CharacterStartUpData.IsNull(),TEXT("Forgot to assign start up data to %s"),*GetName());
}
}
启动项目后,我们打开BP_XMBCharacter,细节栏内搜索StartUpPassiveAbilities,往里面添加被动技能的GA

15。接下来我们处理经验发送事件,我们需要有一个位置处理敌人在死亡时通过自身的类型获取到经验表的值并发送出去,从而设置player的经验值。
打开AttributeSet.h,我们将在这里制作发送经验的事件。
但是在那之前,我们需要创建一个结构体,用于保存这个类型FGameplayEffectModCallbackData传回来的数据。我们可以从其中获取到源与目标的角色、ASC、Controller等属性。但是我们在这里最主要是源与目标的几个变量。
USTRUCT()
struct FEffectProperties
{
GENERATED_BODY()
FEffectProperties(){}
FGameplayEffectContextHandle EffectContextHandle;
//Source
UPROPERTY()
UAbilitySystemComponent* SourceASC = nullptr;
UPROPERTY()
AActor* SourceAvatarActor = nullptr;
UPROPERTY()
AController* SourceController = nullptr;
UPROPERTY()
ACharacter* SourceCharacter = nullptr;
//Target
UPROPERTY()
UAbilitySystemComponent* TargetASC = nullptr;
UPROPERTY()
AActor* TargetAvatarActor = nullptr;
UPROPERTY()
AController* TargetController = nullptr;
UPROPERTY()
ACharacter* TargetCharacter = nullptr;
bool IsSourceActorDie = false;
bool IsTargetActorDie = false;
};
然后创建一个用于设置这个结构体的函数
void SetEffectProperties(const FGameplayEffectModCallbackData& Data, FEffectProperties& Props) const;
void UXMBAttributeSet::SetEffectProperties(const FGameplayEffectModCallbackData& Data,FEffectProperties& Props) const
{
//创建了一个结构体,使用结构体里的变量来存储上下文变量
//这行代码获取了当前Gameplay Effect的应用上下文(即谁应用了这个效果)
Props.EffectContextHandle = Data.EffectSpec.GetContext();
//有source的都是来自"来源"处获取的数据
//通过上下文获取到应用此效果的原始施法者的Ability System Component,
Props.SourceASC = Props.EffectContextHandle.GetOriginalInstigatorAbilitySystemComponent();
//区分.和->运算符的区别
//确保指针有效
if (IsValid(Props.SourceASC) && Props.SourceASC->AbilityActorInfo.IsValid() && Props.SourceASC->AbilityActorInfo->AvatarActor.IsValid())
{
//存储来自source的avataractor
Props.SourceAvatarActor = Props.SourceASC->AbilityActorInfo->AvatarActor.Get();
Props.SourceController = Props.SourceASC->AbilityActorInfo->PlayerController.Get();
//如果没有PlayerController但有AvatarActor,尝试从Pawn中获取Controller
if (Props.SourceController == nullptr && Props.SourceAvatarActor != nullptr)
{
if (const APawn* Pawn = Cast<APawn>(Props.SourceAvatarActor))
{
Props.SourceController = Pawn->GetController();
}
}
// 如果成功获取了Controller,设置SourceCharacter
if (Props.SourceController)
{
Props.SourceCharacter = Cast<ACharacter>(Props.SourceController->GetPawn());
}
}
//从"目标"获取数据
if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
{
Props.TargetAvatarActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
Props.TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
Props.TargetCharacter = Cast<ACharacter>(Props.TargetAvatarActor);
Props.TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Props.TargetAvatarActor);
}
}
然后创建函数SendXPEvent
//发送经验事件
void SendXPEvent(const FEffectProperties& Props);
void UXMBAttributeSet::SendXPEvent(const FEffectProperties& Props)
{
if(IPawnCombatInterface* CombatInterface = Cast<IPawnCombatInterface>(Props.TargetCharacter))
{
//从战斗接口获取等级和职业,通过蓝图函数获取可提供的经验值
const int32 TargetLevel = IPawnCombatInterface::Execute_XMBGetPlayerLevel(Props.TargetCharacter);
//获取 敌人(Target) 的类型
const EXMBCharacterClass TargetClass = IPawnCombatInterface::Execute_GetCharacterClass(Props.TargetCharacter); //c++内调用BlueprintNativeEvent函数需要这样调用
//转化玩家与敌人
AEnemyBase* Enemy =Cast<AEnemyBase>(Props.TargetCharacter);
AWarriorPlayer* Player = Cast<AWarriorPlayer>(Props.SourceCharacter);
const int32 XPReward = UXMBWarriorFunctionLibrary::GetXPRewardForClassAndLevel(Props.TargetCharacter,Enemy,TargetClass,Player,GetWorld());
FGameplayEventData Payload; //创建Payload
Payload.EventTag = XMBGameplayTags::Attributes_Meta_IncomingXP;
Payload.EventMagnitude = XPReward;
//将payload发送给player
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Props.SourceCharacter, XMBGameplayTags::Attributes_Meta_IncomingXP, Payload);
}
}
我们需要制作一个用于处理伤害的函数
//伤害处理
void HandleIncomingDamage(const FEffectProperties& Props);
//处理传入的伤害
void UXMBAttributeSet::HandleIncomingDamage(const FEffectProperties& Props)
{
if (Props.SourceASC == nullptr) return;
const float LocalIncomingDamage = GetDamageTaken();
SetDamageTaken(0.f);
if (LocalIncomingDamage > 0.f)
{
// const float NewHealth = GetCurrentHealth() - LocalIncomingDamage;
const bool bFatal = GetCurrentHealth() <= 0.f;
//致命
if (bFatal)
{
IPawnCombatInterface* CombatInterface = Cast<IPawnCombatInterface>(Props.TargetAvatarActor);
//击杀奖励
SendXPEvent(Props);
}
}
}
SendXPEvent将在死亡时调用。我们在函数PostGameEffectExecute内,先将传回来的数值设置给我们刚刚创建好的结构体的变量。
//当游戏效果结束后,会触发这个函数,Data就是触发该函数的GameplayEffect//可以从data里知道,正在改变哪个属性
Super::PostGameplayEffectExecute(Data);
FEffectProperties Props;
SetEffectProperties(Data,Props);
在接收伤害属性这里,加入刚刚的伤害处理
//伤害处理
if (Data.EvaluatedData.Attribute == GetDamageTakenAttribute())
{
const float OldHealth = GetCurrentHealth();
const float DamageDone = GetDamageTaken();
const float NewCurrentHealth = FMath::Clamp(OldHealth - DamageDone, 0.f, GetMaxHealth());
SetCurrentHealth(NewCurrentHealth);
const FString DebugString = FString::Printf(
TEXT("OldHealth : %f, damage Done :%f, NewCurrentHealth:%f"),
OldHealth,
DamageDone,
NewCurrentHealth
);
//将当前生命广播出去(不管变没变化都要广播)
PawnUIComponent->OnCurrentHealthChanged.Broadcast(GetCurrentHealth()/GetMaxHealth());
//伤害处理
HandleIncomingDamage(Props);
}
接着,我们需要对当前生命进行判断
if (Props.TargetCharacter == nullptr) return;
//生命为0时向目标身上添加死亡标签
if (GetCurrentHealth() == 0.f)
{
UXMBWarriorFunctionLibrary::AddGameplayTagToActorIfNone(Data.Target.GetAvatarActor(), XMBGameplayTags::Shared_Status_Dead);
}
AttributeSet.cpp内PostGameplayEffectExecute完整代码如下
void UXMBAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
//获取到目标的UIComponent,则可以用广播
if (!CachedPawnUIInterface.IsValid())
{
CachedPawnUIInterface = TWeakInterfacePtr<IPawnUIInterface>(Data.Target.GetAvatarActor());
}
checkf(CachedPawnUIInterface.IsValid(), TEXT("%s didn`t implement IPawnUIInterface"),*Data.Target.GetAvatarActor()->GetActorNameOrLabel());
UPawnUIComponent* PawnUIComponent = CachedPawnUIInterface->GetPawnUIComponent();
checkf(PawnUIComponent,TEXT("Couldn`t extrac a PawnUiComponent from %s"),*Data.Target.GetAvatarActor()->GetActorNameOrLabel());
/*↓-----------------------XP------------------------------------------↓*/
//当游戏效果结束后,会触发这个函数,Data就是触发该函数的GameplayEffect//可以从data里知道,正在改变哪个属性
Super::PostGameplayEffectExecute(Data);
FEffectProperties Props;
SetEffectProperties(Data,Props);
/*↑-----------------------XP------------------------------------------↑*/
//设置难度倍率
if (AEnemyBase* Enemy = Cast<AEnemyBase>(GetOwningAbilitySystemComponent()->GetAvatarActor()))
{
CurrentDifficultyRate = Enemy->XMBGetEnemyLevel() / 2.f;
// SetCurrentDifficulty(Enemy->XMBGetEnemyLevel());
}
//Data.EvaluatedData.Attribute是当前被修改的属性
//生命处理
if (Data.EvaluatedData.Attribute == GetCurrentHealthAttribute())//找到正在修改的属性
{
const float NewCurrentHealth = FMath::Clamp(GetCurrentHealth(),0.f, GetMaxHealth());
SetCurrentHealth(NewCurrentHealth);
//广播变化后的生命
PawnUIComponent->OnCurrentHealthChanged.Broadcast(GetCurrentHealth()/GetMaxHealth());
}
//怒气处理
if (Data.EvaluatedData.Attribute == GetCurrentRageAttribute())
{
const float NewCurrentRage = FMath::Clamp(GetCurrentRage(),0.f, GetMaxRage());
SetCurrentRage(NewCurrentRage);
//按断当前rage的值
if (GetCurrentRage() == GetMaxRage())
{
UXMBWarriorFunctionLibrary::AddGameplayTagToActorIfNone(Data.Target.GetAvatarActor(),XMBGameplayTags::Player_Status_Rage_Full);
}
else if (GetCurrentRage() == 0.f)
{
UXMBWarriorFunctionLibrary::AddGameplayTagToActorIfNone(Data.Target.GetAvatarActor(),XMBGameplayTags::Player_Status_Rage_None);
}
else
{
UXMBWarriorFunctionLibrary::RemoveGameplayFromActorIfFound(Data.Target.GetAvatarActor(),XMBGameplayTags::Player_Status_Rage_Full);
UXMBWarriorFunctionLibrary::RemoveGameplayFromActorIfFound(Data.Target.GetAvatarActor(),XMBGameplayTags::Player_Status_Rage_None);
}
if (UPlayerUIComponent* PlayerUIComponent = CachedPawnUIInterface->GetPlayerUIComponent())
{
PlayerUIComponent->OnCurrentRageChanged.Broadcast(GetCurrentRage()/GetMaxRage());
}
}
//伤害处理
if (Data.EvaluatedData.Attribute == GetDamageTakenAttribute())
{
const float OldHealth = GetCurrentHealth();
const float DamageDone = GetDamageTaken();
const float NewCurrentHealth = FMath::Clamp(OldHealth - DamageDone, 0.f, GetMaxHealth());
SetCurrentHealth(NewCurrentHealth);
const FString DebugString = FString::Printf(
TEXT("OldHealth : %f, damage Done :%f, NewCurrentHealth:%f"),
OldHealth,
DamageDone,
NewCurrentHealth
);
//将当前生命广播出去(不管变没变化都要广播)
PawnUIComponent->OnCurrentHealthChanged.Broadcast(GetCurrentHealth()/GetMaxHealth());
//伤害处理
HandleIncomingDamage(Props);
}
/*↓-----------------------XP------------------------------------------↓*/
if (Props.TargetCharacter == nullptr) return;
//生命为0时向目标身上添加死亡标签
if (GetCurrentHealth() == 0.f)
{
UXMBWarriorFunctionLibrary::AddGameplayTagToActorIfNone(Data.Target.GetAvatarActor(), XMBGameplayTags::Shared_Status_Dead);
}
//经验处理
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
}
/*↑-----------------------XP------------------------------------------↑*/
}
16。接下来我们创建一个接口CharacterInterface,用于设置获取到玩家身上有关经验与等级的函数。
public:
//获取XP
UFUNCTION(BlueprintNativeEvent)
float GetXP() const;
//寻找当前经验对应的等级
UFUNCTION(BlueprintNativeEvent)
float FindLevelForXP(float InXP) const;
//将经验添加到等级
UFUNCTION(BlueprintNativeEvent)
void AddToPlayerLevel(float InPlayerLevel);
//升级
UFUNCTION(BlueprintNativeEvent)
void LevelUp();
//添加给XP
UFUNCTION(BlueprintNativeEvent)
void AddToXP(float InXP);
用CharacterBase进行继承,然后打开WarriorPlayer进行覆写。在WarriorPlayer内
public:
//Begin ICharacter Interface
virtual void AddToXP_Implementation(float InXP) override;
virtual void LevelUp_Implementation() override;
virtual void AddToPlayerLevel_Implementation(float InPlayerLevel) override;
virtual float FindLevelForXP_Implementation(float InXP) const override;
virtual float GetXP_Implementation() const override;
//End ICharacter Interface
void AWarriorPlayer::AddToXP_Implementation(float InXP)
{
AXMBState* PlayerStateBase = GetPlayerState<AXMBState>();
check(PlayerStateBase); //检测是否有效,无限会暂停游
PlayerStateBase->AddToXP(InXP);
}
void AWarriorPlayer::LevelUp_Implementation()
{
//用来放置效果
Debug::Print(TEXT("升级成功"));
Super::LevelUp_Implementation();
}
void AWarriorPlayer::AddToPlayerLevel_Implementation(float InPlayerLevel)
{
AXMBState* PlayerStateBase = GetPlayerState<AXMBState>();
check(PlayerStateBase); //检测是否有效,无限会暂停游戏
PlayerStateBase->AddToLevel(InPlayerLevel);
}
float AWarriorPlayer::FindLevelForXP_Implementation(float InXP) const
{
const AXMBState* PlayerStateBase = GetPlayerState<AXMBState>();
check(PlayerStateBase); //检测是否有效,无限会暂停游戏
return PlayerStateBase->LevelUpInfo->FindLevelForXP(InXP);
}
float AWarriorPlayer::GetXP_Implementation() const
{
const AXMBState* PlayerStateBase = GetPlayerState<AXMBState>();
check(PlayerStateBase); //检测是否有效,无限会暂停游戏
return PlayerStateBase->GetXP();
}
好,接下来我们需要记住,我们在XMBState内对玩家的等级进行了加、查、改,在CharacterInterface内制作了一些有关等级与经验的函数,在WarriorPlayer内进行了实现。则在AttributeSet内对等级与经验进行操作的时候,我们将通过WarriorPlayer 调用在XMBState内与之相关的函数。
17。在AttributSet内,我们制作一个用于处理经验事件的函数(不过我刚刚发现我这里有误,在const float LocalincomingXP = GetIncomingXP(),这里获取的是AttributeSet里用于处理击杀敌人时获取的经验值而不是玩家身上的经验值,不过我想着作为初始化的话都是0,应该没问题)
//经验处理
void HandleIncomingXP(const FEffectProperties& Props);
void UXMBAttributeSet::HandleIncomingXP(const FEffectProperties& Props)
{
const float LocalIncomingXP = GetIncomingXP();
SetIncomingXP(0.f);
//Source Character is the owner, since GA_ListenForEvents applies GE_EventBasedEffect, adding to IncomingXP
//SourceCharacter是玩家,从GA_ListenForEvents添加给GE_EventBasedEffect, 一直在检测IncomingXP
if (Props.SourceCharacter->Implements<UCharacterInterface>())
{
const float CurrentLevel = IPawnCombatInterface::Execute_XMBGetPlayerLevel(Props.SourceCharacter);
const float CurrentXP = ICharacterInterface::Execute_GetXP(Props.SourceCharacter);
const float NewLevel = ICharacterInterface::Execute_FindLevelForXP(Props.SourceCharacter, CurrentXP + LocalIncomingXP);
const int32 NumLevelUps = NewLevel - CurrentLevel;
if (NumLevelUps > 0.f)
{
ICharacterInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);
ICharacterInterface::Execute_LevelUp(Props.SourceCharacter);
}
//玩家状态添加xp,PlayerState广播XP委托WidgetController接收委托,然后又广播他自身的委托
ICharacterInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
}
}
然后在PostGameplayEffectExecute函数内,进行经验处理的部分
//经验处理
if (Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
{
HandleIncomingXP(Props);
}
然后打开OverlayWidgetController.h,我们之前创建了函数,这个函数用于绑定回调有关的函数
virtual void BindCallbacksToDependencies() override;
我们在里面绑定XMBState内的函数,当State进行这些函数时,就会触发在OverlayWidgetController内绑定的函数。通过这些绑定的函数,我们就可以告诉UI这些属性发生了改变,并且将改变后的值广播给UI,我们再在蓝图内接收这些值,就可以实现UI的更改了。因为OverlayWidgetController在HUD内实例化的,所以我们应该进入XMBHUD.cpp,在函数GetOverlayWidget()内//调用在OverlayWidgetController内对经验条与等级的绑定函数。
UOverlayWidgetController* AXMBHUD::GetOverlayWidgetController(const FWidgetControllerParams& WCParams)
{
if(OverlayWidgetController == nullptr)
{
OverlayWidgetController = NewObject<UOverlayWidgetController>(this, OverlayWidgetControllerClass);
OverlayWidgetController->SetWidgetControllerParams(WCParams);
//调用在OverlayWidgetController内对经验条与等级的绑定函数
OverlayWidgetController->BindCallbacksToDependencies();
}
return OverlayWidgetController;
}
18。启动项目
我们创建XMBState与XMBHUD的蓝图版本,并且分别设置


然后打开BP_SurvialGameMode

然后打开WBP_PlayerOverlay,向里面添加了我们制作的经验条鱼等级栏后,

在进入到图标,我们需要在WBP_PlayerOverlay内调用控制器的SetWidgetController函数,并通过这个函数来设置经验条与等级框的WidgetController

进入到WBP_XPBar

进入WBP_LevelText

19。进入到各类型敌人蓝图内,细节栏搜索class,进行敌人的类型设置。默认的类型是warrior,玩家不用改变。
内容很多,挺乱的、我也不知道哪里多了哪里少了、哪里漏了。其实好像还有其他的地方也有改动,但是实在记不得了.
推荐几个打点的位置用于测试玩家在击杀敌人后有没有获取经验、是否实现了等级提升。
①在OverlayController内,我们需要确定是否绑定与执行了有关经验等级改变的函数,并且在经验改变后是否能正确将当前的经验百分比广播出去

②:在HUD内,我们需要确定是否将当前创建的Widget给到了视口

③:在WarriorPlayer内,我们需要检测是否成功调用了XMBState内的函数

接着在XMBState内,我们需要对是否广播出正确的值进行检测

④:在PlayerDateAsset_LevelUp内,我们需要打点是否正确返回了当前角色身上的经验所对应的等级

⑤:在DataAsset_EnemyStartUpData内对是否正确返回了敌人的类型进行检测

⑥:在XMBWarriorFunctionLibrary内,检查是否返回了敌人的正确类型与是否获取正确的经验值

⑦:在AttributeSet内
检查是否发送给Actor

检查经验奖励

真的不知道有什么记得有什么不记得了。下次再搞这么长的一定画一个流程图。(下次一定设计好点的结构x,像在GameMode内设置一个经验表格就很不合理x)
接下来应该很快就会完成基于MMC的属性修改吧。或许是增容2与3的结合(