16. UE5 RPG获取GE应用的回调,并根据Tag设置数据显示到窗口

在上一篇介绍了对标签如何在项目中设置,这一篇先讲解一下如何在GE里面使用GameplayTag标签。
之前我在第十一章节中 11. UE5 RPG使用GameplayEffect修改角色属性(二)介绍了一些GE的属性,在UE 5.3版本中,修改的配置方式,需要在组件里面设置需要的组件
下图为与标签相关的组件
在这里插入图片描述

查看当前actor拥有的标签

首先,我们在一个用于持续性时间的GE上面添加一个标签,比如我在一个回血的GE上面添加了一个Health的标签
在这里插入图片描述
然后运行游戏,按~建打开debug
在这里插入图片描述
左上角会显示当前actor拥有的标签,当前actor是没有任何标签的
在这里插入图片描述
在应用了对应的GE后,会发现角色身上多出了对应的标签,在GE效果消失后,对应的标签也被剔除。后面的括号内的数值代表当前添加的数量,它也可以被堆叠。注意,通过GE堆叠的方式是无法让标签产生堆叠的。如果设置不堆叠,每个是单独的GE,那么会出现标签堆叠的效果。
在这里插入图片描述

ASC使用委托监听GE

接下来我们要实现GE被应用时,使用委托触发回调,进行其它处理
翻看ASC.h的源码,会发现有相应的委托代码,委托返回ASC,GE的实例,GE的引用三个参数
在这里插入图片描述
接着往下看,会发现基于这个委托宏,创建了多个委托属性,有添加GE给自身触发的,有添加给目标触发的,有持续时间的GE添加给自身触发的,还有周期性触发的GE的委托等等。
在这里插入图片描述

鼠标悬停到属性上面也能够查看到对应的返回
在这里插入图片描述
接下来,我们要实现的就是绑定委托,在给自身添加GE时,打印GE附加的Asset Tag

在AbilitySystemComponentBase 技能组件基类里面,我们添加一个AbilityActorInfoSet()函数,这个函数用于初始化委托的注册。
然后添加一个委托触发的回调函数EffectApplied()这个函数将在GE被添加的时候触发。

// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemComponentBase.generated.h"

/**
 * 技能系统组件
 */
UCLASS()
class AURA_API UAbilitySystemComponentBase : public UAbilitySystemComponent
{
	GENERATED_BODY()

public:
	void AbilityActorInfoSet();

protected:
	void EffectApplied(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle);
};

在cpp中,我们将对其进行实现,AbilityActorInfoSet()中先绑定了一个GE添加到自身的委托,事件触发时将调用EffectApplied()函数,EffectApplied()函数内,现在将获取到GE身上的AssetTags,并通过遍历的形式打印出来。

// 版权归暮志未晚所有。


#include "AbilitySystem/AbilitySystemComponentBase.h"

void UAbilitySystemComponentBase::AbilityActorInfoSet()
{
	OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAbilitySystemComponentBase::EffectApplied);
}

void UAbilitySystemComponentBase::EffectApplied(UAbilitySystemComponent* AbilitySystemComponent,
                                                const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle)
{
	// GEngine->AddOnScreenDebugMessage(1, 8.f, FColor::Blue, FString("Effect Applied!"));
	FGameplayTagContainer TagContainer;
	EffectSpec.GetAllAssetTags(TagContainer);
	for(const FGameplayTag& Tag : TagContainer)
	{
		//TODO: 将tag广播给Widget Controller
		const FString Msg = FString::Printf(TEXT("GE Tag: %s"), *Tag.ToString()); //获取Asset Tag
		GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Cyan, Msg); //打印到屏幕上 -1 不会被覆盖
	}
}

ASC的基础类添加了,我们还需要对其进行调用,事件委托的绑定需要在ASC初始化完成后调用。

在所有的角色基类上面我们添加一个virtual void InitAbilityActorInfo();这个函数用于初始化ASC的委托注册,下面为初始化的代码

Cast<UAbilitySystemComponentBase>(AbilitySystemComponent)->AbilityActorInfoSet();

在基类上,我们不去做实现,实现在英雄类和敌人类里面去实现。

在英雄类里面,在原来的基础上,初始化ASC后进行调用

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

	//初始化ASC的OwnerActor和AvatarActor
	InitAbilityActorInfo();

	//设置OwnerActor的Controller
	SetOwner(NewController);
}

void AHeroCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	//初始化ASC的OwnerActor和AvatarActor
	InitAbilityActorInfo();
}

void AHeroCharacter::InitAbilityActorInfo()
{
	APlayerStateBase* PlayerStateBase = GetPlayerState<APlayerStateBase>();
	check(PlayerStateBase); //检测是否有效,无限会暂停游戏
	//从playerState获取ASC和AS
	AbilitySystemComponent = PlayerStateBase->GetAbilitySystemComponent();
	AttributeSet = PlayerStateBase->GetAttributeSet();
	//初始化ASC
	AbilitySystemComponent->InitAbilityActorInfo(PlayerStateBase, this);
	//触发Actor的技能信息设置回调
	Cast<UAbilitySystemComponentBase>(AbilitySystemComponent)->AbilityActorInfoSet(); 

	//获取PC
	if(APlayerControllerBase* PlayerControllerBase = Cast<APlayerControllerBase>(GetController()))
	{
		if(AMyHUD* HUD = Cast<AMyHUD>(PlayerControllerBase->GetHUD()))
		{
			HUD->InitOverlay(PlayerControllerBase, PlayerStateBase, AbilitySystemComponent, AttributeSet);
		}
	}
	
}

在敌人类里面,初始化委托方式一致

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

	InitAbilityActorInfo();
}

void AEnemyBase::InitAbilityActorInfo()
{
	AbilitySystemComponent->InitAbilityActorInfo(this, this);
	Cast<UAbilitySystemComponentBase>(AbilitySystemComponent)->AbilityActorInfoSet();
}

在初始化完成后,我们在UE里面的GE添加AssetTagsGameplayEffectComponent,并且添加对应的Tags,测试应用时是否有打印。
在这里插入图片描述

使用委托将ASC和Widget Controller绑定

上面我们实现了获取添加GE时的回调,并能够打印对应GE上面添加的Tags,接下来,我们要实现ASC和Widget Controller之间的沟通,在GE添加时,在WidgetController里面也能够获取到Broadcast广播,并触发对应的回调。

ASC.h在之前的基础上,添加一个委托宏设置FEffectAssetTags,然后定义一个属性EffectAssetTags用于后续的广播。

// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemComponentBase.generated.h"

DECLARE_MULTICAST_DELEGATE_OneParam(FEffectAssetTags, const FGameplayTagContainer& /* AssetTags */)

/**
 * 技能系统组件
 */
UCLASS()
class AURA_API UAbilitySystemComponentBase : public UAbilitySystemComponent
{
	GENERATED_BODY()

public:
	void AbilityActorInfoSet();

	FEffectAssetTags EffectAssetTags;

protected:
	void EffectApplied(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle);
};

在cpp里,EffectApplied是添加GE后的事件回调,之前我们是直接在这里打印的tags,现在我们修改成获取到TagContainer后,将TagContainer进行委托广播出去。

// 版权归暮志未晚所有。


#include "AbilitySystem/AbilitySystemComponentBase.h"

void UAbilitySystemComponentBase::AbilityActorInfoSet()
{
	OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAbilitySystemComponentBase::EffectApplied);
}

void UAbilitySystemComponentBase::EffectApplied(UAbilitySystemComponent* AbilitySystemComponent,
                                                const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle)
{
	// GEngine->AddOnScreenDebugMessage(1, 8.f, FColor::Blue, FString("Effect Applied!"));
	FGameplayTagContainer TagContainer;
	EffectSpec.GetAllAssetTags(TagContainer);

	EffectAssetTags.Broadcast(TagContainer);
}

之前我们制作的,先初始化ASC完成以后,然后通过HUD类对Widget用户控件初始化,然后再对Widget Controller初始化的。所以,在初始化Widget Controller时,ASC是初始化完成的。
我们在Widget Controller绑定事件委托里面,添加对ASC的委托EffectAssetTags进行监听即可。

以下是用户控件使用的Widget Controller的初始化委托监听的函数,前面的是之前监听AS属性值变化的代码,后面我们通过AddLambda添加了一个匿名函数,用于监听EffectAssetTags,并在里面进行测试打印。

void UOverlayWidgetController::BindCallbacksToDependencies()
{
	const UAttributeSetBase* AttributeSetBase = CastChecked<UAttributeSetBase>(AttributeSet);

	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
		AttributeSetBase->GetHealthAttribute()).AddUObject(this, &UOverlayWidgetController::HealthChanged);

	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
	AttributeSetBase->GetMaxHealthAttribute()).AddUObject(this, &UOverlayWidgetController::MaxHealthChanged);

	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
		AttributeSetBase->GetManaAttribute()).AddUObject(this, &UOverlayWidgetController::ManaChanged);

	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(
		AttributeSetBase->GetMaxManaAttribute()).AddUObject(this, &UOverlayWidgetController::MaxManaChanged);

	//AddLambda 绑定匿名函数
	Cast<UAbilitySystemComponentBase>(AbilitySystemComponent)->EffectAssetTags.AddLambda(
		[](const FGameplayTagContainer& AssetTags)
		{
			for(const FGameplayTag& Tag : AssetTags)
			{
				//将tag广播给Widget Controller
				const FString Msg = FString::Printf(TEXT("GE Tag in Widget Controller: %s"), *Tag.ToString()); //获取Asset Tag
				GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Cyan, Msg); //打印到屏幕上 -1 不会被覆盖
			}
		}
	);
}

接着运行代码,发现左上角打印了对应的tags,代表代码无误。
在这里插入图片描述

创建标签消息委托

上面我们实现了可以在添加GE的时候,打印GE身上的Tag,接下来,将实现在Widget里面通过委托获取GE的应用标签。

首先,在WidgetController里面,创建一个结构体,这个结构体可以被蓝图使用。
结构体内主要存储一些数据,用于在ui上面展示使用,所以,我们需要GE的信息Tag,提示文本信息,使用的Widget控件,以及显示的图片都存储到结构体内。

USTRUCT(BlueprintType)
struct FUIWidgetRow : public FTableRowBase
{
	GENERATED_BODY();

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	FGameplayTag MessageTag = FGameplayTag();

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	FText Message = FText();

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TSubclassOf<class UMyUserWidget> MessageWidget;

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	UTexture2D* Image = nullptr;
};

编译后,创建一个数据表格,类就选择我们创建的结构体。
在这里插入图片描述
接下来,我们要实现数据使用的内容,首先创建信息Tag,在GameplayTag标签管理器里面,添加信息标签。
在这里插入图片描述
到现在我们还没有创建Widget显示控件,先把别的内容填上
在这里插入图片描述
接下来在WidgetController上面增加一个配置项,用于配置数据

	//EditDefaultsOnly 说明此属性可以通过属性窗口编辑,但只能在原型上进行。
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Widget Data")
	TObjectPtr<UDataTable> MessageWidgetDataTable;

将刚才创建的数据表格设置
在这里插入图片描述
接着创建一个可以通过Tag在数据表格中寻找对应的数据的函数,为了兼容不同的数据表格,这里我们使用不确定的T类型,在调用时指定返回类型。

	//根据传入的表格和Tag返回查找到的数据,表格类型不确定,所以使用T来表示,在使用此函数时,需要指定对应类型
	template<typename T>
	T* GetDataTableRowByTag(UDataTable* DataTable, const FGameplayTag& Tag);
template <typename T>
T* UOverlayWidgetController::GetDataTableRowByTag(UDataTable* DataTable, const FGameplayTag& Tag)
{
	T* Row = DataTable->FindRow<T>(Tag.GetTagName(), TEXT(""));
	return Row;
}

在之前我们实现的匿名函数里面,首先添加的GE内是否包含Message的Tag,如果包含,则广播出去。

	//AddLambda 绑定匿名函数
	Cast<UAbilitySystemComponentBase>(AbilitySystemComponent)->EffectAssetTags.AddLambda(
		[this](const FGameplayTagContainer& AssetTags) //中括号添加this是为了保证内部能够获取类的对象
		{
			for(const FGameplayTag& Tag : AssetTags)
			{

				//对标签进行检测,如果不是信息标签,将无法进行广播
				FGameplayTag MessageTag = FGameplayTag::RequestGameplayTag(FName("Message"));
				// "A.1".MatchesTag("A") will return True, "A".MatchesTag("A.1") will return False
				if(Tag.MatchesTag(MessageTag))
				{
					FUIWidgetRow* Row = GetDataTableRowByTag<FUIWidgetRow>(MessageWidgetDataTable, Tag);
					MessageWidgetRowDelegate.Broadcast(*Row); //前面加*取消指针引用
				}
			}
		}
	);

代码逻辑基本实现到这,然后我们需要在药瓶的GE里面添加Tag
在这里插入图片描述
然后做一个测试,在Widget中,先将Controller类型转换为OverlayWidgetController
在这里插入图片描述
接着监听数据返回,打印信息
在这里插入图片描述
数据能够打印,证明准确无误
在这里插入图片描述

创建信息Widget

结构体内,我们还有一项没有实现,那就是Widget组件,接下来,我们将实现这个组件。
首先创建一个用户控件,基类就使用我们之前实现的基类
在这里插入图片描述
在Widget里面,创建一个显示图片和文本的对象组件。
在这里插入图片描述
如果以水平框作为最外层,会被父节点拉伸,我们可以使用一个覆层包裹一下。
在这里插入图片描述

图表里面增加一个设置图片和文本的蓝图函数。
在这里插入图片描述
创建完成,我们可以将数据表格的数据填充上去对于的Widget
在这里插入图片描述

接下来,我们就可以修改Overlay的debug的蓝图,不用再打印文本,而是直接添加widget到视口。
下图为修改后的蓝图节点,我们使用数据创建一个widget,并设置显示在屏幕中间,
在这里插入图片描述
接下来,我们给widget制作了一个新的动画,并添加到了设置函数上面,在设置完成属性后,播放动画
在这里插入图片描述
播放动画后面的执行,是直接执行的,不是在动画播放完成执行,所以,我们需要制作一个事件,在延迟动画的时间后,销毁掉,并在销毁回调中测试
在这里插入图片描述
在测试捡起药瓶后,检查动画是否播放完成,回调是否触发,这样不会造成内存溢出。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/452998.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用零一万物 200K 模型和 Dify 快速搭建模型应用

本篇文章&#xff0c;我们聊聊如何使用 LLM IDE (Dify) 快速搭建一个模型应用&#xff0c;以及使用超长上下文的 200K 模型&#xff0c;完成懒人式的电子书翻译。 准备工具 最近在 GitHub 上看到了前 HuggingFace 员工&#xff0c;前 transformers 核心贡献者之一的 Stas Bek…

通过一篇文章让你了解什么是函数栈帧

函数栈帧的创建和销毁 前言一、什么是函数栈帧二、 理解函数栈帧能解决什么问题三、 函数栈帧的创建和销毁解析3.1 什么是栈3.2 认识相关寄存器和汇编指令相关寄存器eaxebxebpespeip 相关汇编命令 3.3 解析函数栈帧的创建和销毁3.3.1 预备知识3.3.2 函数的调用堆栈3.3.4 准备环…

参考线平滑 - FemPosDeviation算法

FemPosDeviation参考线平滑方法是离散点平滑方法 参考文章&#xff1a; &#xff08;1&#xff09;参考线平滑-FemPosDeviation-OSQP &#xff08;2&#xff09;Planning基础库——散点曲线平滑 &#xff08;3&#xff09;参考线平滑-FemPosDeviation-SQP &#xff08;4&#x…

微信小程序开发系列(二十六)·小程序运行机制(启动、前后台状态、挂起、销毁)和小程序更新机制

目录 1. 小程序运行机制 1.1 启动 1.2 前台和后台状态 1.3 挂起 1.4 销毁 2. 小程序更新机制 1. 小程序运行机制 1.1 启动 小程序启动可以分为两种情况&#xff0c;一种是冷启动&#xff0c;一种是热启动。 冷启动&#xff1a;如果用户首次打开&#xff0c;或小…

基于Gui Guider进行LVGL的页面绘制和移植

在之前的文章里讲过一种页面切换的方式&#xff0c;那就是&#xff1a;定义和创建页面对象绘制页面内容切换页面。参考这篇文章&#xff1a; LVGL如何创建页面并实现页面的切换-CSDN博客 这篇文章讲了如何绘制并切换页面。 但是现在遇到一个问题&#xff0c;那就是页面绘制&…

力扣爆刷第93天之hot100五连刷51-55

力扣爆刷第93天之hot100五连刷51-55 文章目录 力扣爆刷第93天之hot100五连刷51-55一、200. 岛屿数量二、994. 腐烂的橘子三、207. 课程表四、208. 实现 Trie (前缀树)五、46. 全排列 一、200. 岛屿数量 题目链接&#xff1a;https://leetcode.cn/problems/number-of-islands/d…

php7.3.4连接sqlserver(windows平台)

前言 有个项目需要手上laravel连接客户的sqlserver数据库读取数据&#xff0c;故在本地开发的lnmp环境中&#xff0c;php需要增加扩展 过程 从微软官网下载sqlsrv扩展,注意注意php版本&#xff0c;下载地址 解压的文件会有nts和ts两个版本&#xff0c;本地打开phpinfo查看 将…

Claude3 正式发布,支持多模态(附注册使用教程)

免费使用教程请看到最后&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; AnthropicAI 官推发布消息&#xff0c;正式推出Claude 3&#xff0c;沉寂了很久的Anthropic 终于亮剑放了大招。Claude 3 系列模型&#xff0c;包括Claude 3 Opus、Claude 3 Sonnet 和 C…

耐腐蚀PFA气体洗涤瓶可多级串联透明特氟龙塑料氢气吸收装置

洗气瓶是一种常用于净化和干燥各种气体的实验室器皿&#xff0c;以去除其中的水分、油脂、颗粒物等杂质&#xff0c;从而使需要用到的气体满足实验要求。 PFA洗气瓶的工作原理&#xff1a; 主要是通过液体吸收、溶解或发生化学反应来去除气体中的杂质。在洗气过程中&#xff…

优思学院|为什么企业要做质量管理体系认证?

在二战后的美国&#xff0c;公司对自己的产品质量颇为自满。市场需求旺盛&#xff0c;产品销售状况良好&#xff0c;即便产品存在质量缺陷&#xff0c;消费者似乎也能接受。这种态度导致了一种现象&#xff1a;即使在生产结束时发现了一定比例的缺陷&#xff0c;公司也能通过加…

Day39-2-Rsync企业级备份工具讲解

Day39-2-Rsync企业级备份工具讲解 1. 什么是rsync?2. 什么是全量和增量&#xff1f;3. 为什么要用rsync&#xff1f;4. rsync功能特性5. 增量复制原理6. rsync三种工作模式介绍6.1 本地&#xff08;local&#xff09;6.2 远程Shell模式6.2.1 远程Shell模式企业场景和实践&…

vue.js 页面中设置多个swiper

效果&#xff1a; 设置主要设置了 动态的 包含类、 左右按钮的类 <template><div class"swiper-container_other"><!-- 右侧按钮 --><div :class"[(id)?swiper-button-nextid:swiper-button-next, swiper-button-next]"></div…

每日一题 第一期 洛谷 铺地毯

[NOIP2011 提高组] 铺地毯 https://www.luogu.com.cn/problem/P1003 题目描述 为了准备一个独特的颁奖典礼&#xff0c;组织者在会场的一片矩形区域&#xff08;可看做是平面直角坐标系的第一象限&#xff09;铺上一些矩形地毯。一共有 n n n 张地毯&#xff0c;编号从 1 …

探秘C语言扫雷游戏实现技巧

本篇博客会讲解&#xff0c;如何使用C语言实现扫雷小游戏。 0.思路及准备工作 使用2个二维数组mine和show&#xff0c;分别来存储雷的位置信息和排查出来的雷的信息&#xff0c;前者隐藏&#xff0c;后者展示给玩家。假设盘面大小是99&#xff0c;这2个二维数组都要开大一圈…

北京公司注册地址想要迁到新疆该如何操作

尊敬的客户&#xff0c;您好&#xff01;我是经典世纪胡云帅&#xff08;游览器搜经典世纪胡云帅&#xff09;&#xff0c;您选择了北京经典世纪集团有限公司-资 质代办&#xff0c;我们将竭诚为您服务&#xff01;如果您的公司注册地址想要迁到新疆&#xff0c;这里有一些重要…

SSM整合和实战练习笔记1

SSM整合和实战练习1 SSM整合和实战练习springmvc配置业务层 service aop tx的配置mybatis整合配置&#xff08;方式2容器初始化配置类访问测试mapper层service层controller层前端程序搭建 SSM整合和实战练习 springmvc配置 业务层 service aop tx的配置 mybatis整合配置&#…

亲测抖音小程序备案流程,抖音小程序如何备案,抖音小程序备案所需准备资料

抖音小程序为什么要备案&#xff0c;抖音官方给出如下说明&#xff1a; 1、2024年3月15日后提交备案的小程序将不保证2024年3月31日前平台可初审通过&#xff1b; 2、2024年3月31日后未完成备案小程序将被下架处理。 一&#xff0c;备案前需准备资料 &#xff08;一&#xff0…

bpmn-js系列之Palette

前边写了四篇文章介绍了bpmn.js的基本使用&#xff0c;最近陆续有小伙伴加我催更&#xff0c;感谢对我这个半吊子前端的信任&#xff0c;接着更新bpmn.js的一些高级用法&#xff0c;本篇介绍对左侧工具栏Palette的隐藏和自定义修改 隐藏shape 左侧工具栏Palette有些图标我用不…

【数据挖掘】实验2:R入门2

实验2&#xff1a;R入门2 一&#xff1a;实验目的与要求 1&#xff1a;熟悉和掌握R数据类型。 2&#xff1a;熟悉和掌握R语言的数据读写。 二&#xff1a;实验内容 1&#xff1a;R数据类型 【基本赋值】 Eg.1代码&#xff1a; x <- 8 x Eg.2代码&#xff1a; a city …

碳实践 | 基于“界、源、算、质、查”五步法,实现企业组织碳核算

碳排放核算是夯实碳排放统计的基础&#xff0c;提高碳排放数据质量的关键&#xff0c;同时&#xff0c;将推动能耗“双控”向碳排放“双控”转变。总体来看&#xff0c;碳核算分为区域层面、组织层面和产品层面的碳核算&#xff0c;这三个层面的意义和计算方法完全不同。本文将…