[中文直播] 第21期 | UE4数据驱动开发 | Epic 大钊课程笔记

Posted Tanzq*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[中文直播] 第21期 | UE4数据驱动开发 | Epic 大钊课程笔记相关的知识,希望对你有一定的参考价值。

视频链接

点击我进入B站观看视频

课程资源在简介上也写了,可以进入下载,资源很小不用借助其他的软件,直接百度网盘下载就可以了。
最近百度网盘青春版也出来了,不过不推荐保存这个进去下载,有次数限制,加这个进去就太亏咯!

关于无法编译成功的问题

将启用状态改成false。

DataDriven.Build.cs中添加PublicDependencyModuleNames.Add("DeveloperSettings");

然后就可以成功编译运行了!

在名为Watch代码的文件夹中有部分插件中的代码,可以借鉴参考一下。

为什么要使用数据驱动?

  • 使用数据驱动的话能将职责进行分离,不用经常在代码里面进行数据的修改,而是在外部的数据表中进行修改。
  • 减少了硬编码数据。
  • 减少了硬编码类, 可以储存类的链接进行软引用。
  • 自动化配置数据流程,比如说可以直接在游戏运行中实时修改数据,提高了调试效率。

大钊大佬将配置数据的方式分为了四个方面:初级、中级、高级、工具,我将从以上四个方面进行整理,让大家和自己以后要用的时候方便提取。

初级

BP CDO/Instance

这个就是直接在实例或者蓝图类中直接进行赋值的方式。

优点缺点
使用简单,能够满足简易的数据配置需求散乱到各个蓝图中,不方便统一处理

C++ ConstructHelpers

类似这种,直接将链接添加到其中:
ConstructorHelpers::FObjectFinder<USkeletalMesh> objFinder(TEXT("/Game/Mannequin/Character/Mesh/SK_Mannequin"));

(感觉很麻烦。。。)

这个存在的一个问题就是:如何去写他的路径。

  • 类名’/路径/包名.对象名:子对象名’
  • 类名其实会被忽略掉,所以不写也行
  • 类: /路径/包名.类名_C (包名往往和类名一致)
  • 对象:/路径/包名.对象名(包名往往和对象一致)
  • 路径:/Root/Path1/Path2/……
  • 路径Root:/Engine, /Game , /Module

右键对象目标文件,有个 Copy File Path ,然后就可以得到他的绝对路径,对路径进行修改即可。

Class Reference

在C++ 中可以这么用,<>里面表示的是一个类,所有这个类的子类都可以加载到这个变量中进行保存。
TSubclassOf<UBaseItemWdget> ItemWidgetClass;

在蓝图中就比较简单了,这个创建类型的时候点击这个紫色的就可以了。

中级

DataTable/CompositeDataTable

  • DataTable

先在蓝图/C++中创建一个结构体,然后新建:

然后填表就可以了。

优点 :

  1. 容易编辑
  2. 可以导入导出CSV/JSON
  3. 可用FDataTableRowHandle引用一行
  4. 配置大量数据很方便

缺点:

  1. 不能包含UObjects
  2. 无父子层级关系
  3. 无序,用TMap<FName,T>保存
  4. 不可引用其他表格
  5. 依然强绑定资产的加载
  • CompositeDataTable

他可以堆叠覆盖多个DataTable。

从图中可以看出来,后来的数据表中的属性值会覆盖掉前面数据表中有相同属性的属性值,保留不同的。

DataAsset/PrimaryDataAsset

在这个位置可以新建一个DataAsset

优点:

  • 容易子类化

  • 可快速引用其他资产 UMyDataAsset*

  • 可用BulkEdit快速编辑多个DataAsset:

  • 可容纳UObject实例

  • 灵活的数据配置方式

  • 方便配置树状组织数据,全局配置,数据对象集合, 每一个DataAsset都是一份数据对象

  • 继承自UPrimaryDataAsset可方便管理数据对象的加载释放

缺点:

  • 不好管理大量的配置对象

(关于具体如何使用,先不写,等到用到这个功能了再来补充这块的笔记。)

CurveTable/CompositeCurveTables

创建一个.csv文件,配置对应的数据,
格式:

然后导入到虚幻引擎中,

选择CurveTable即可。
这个也支持数据覆盖。

Custom WorldSettings

创建一个继承自AWorldSettings的类,
class AShopWorldSettings : public AWorldSettings

在其中添加上需要的属性值。


然后在项目设置中配置上这个属性,重启编译器,新建关卡,就可以在关卡中看到这个值啦!

在C++中可以通过这种方式来调用这个属性值:

AWorldSettings* settings= UGameplayStatics::GetGameMode(this)->GetWorld()->GetWorldSettings();
AShopWorldSettings* shopSettings=Cast<AShopWorldSettings>(settings);

优点:

  • 可对每个关卡进行单独的全局配置

缺点:

  • 数据依赖存在于关卡内部

高级

Settings(.ini)

创建一个类,像下面这个例子一样的写法写就行:

UCLASS(config = DataDrivenProjectSettings, defaultconfig)
class UDataDrivenProjectSettings : public UDeveloperSettings

	GENERATED_BODY()
public:
	/** 选择配置设置是在项目还是在编辑器中。 */
	virtual FName GetContainerName() const override  return TEXT("Project"); 
	/** 获取设置的类别,一些高级分组,例如,编辑器、引擎、游戏……等*/
	virtual FName GetCategoryName() const override  return TEXT("DataDriven"); 
	/** 设置部分的唯一名称使用类的FName。*/
	virtual FName GetSectionName() const override  return TEXT("DataDriven"); 
public:
	UDataDrivenProjectSettings();
	
	UFUNCTION(BlueprintPure, DisplayName = "DataDrivenProjectSettings")
		static UDataDrivenProjectSettings* Get()  return GetMutableDefault<UDataDrivenProjectSettings>(); 
public:
	UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = Shop)
		float PriceOff = 50.f;

	UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = Player)
		float PlayerStartLevel = 10.f;

;

对应的效果:

还可以给Actor类中加上类似这种属性:

  • .h
UCLASS(config = CustomConfig) // 定义一个配置名
class ACustomConfigActor :public AActor

	GENERATED_BODY()
public:
	ACustomConfigActor();
public:
	UPROPERTY(Config, EditAnywhere, BlueprintReadWrite, Category = MyCustom) // 参与配置,那么就使用 Config,让引擎对他进行序列化。
		float MaxLife = 100.f;

	UPROPERTY(transient, EditAnywhere, BlueprintReadWrite, Category = MyCustom) //不参与配置,使用 transient,引擎对他不进行序列化。
		float CurrentLife = 50.f;
public:
	virtual void BeginPlay()override;

	UFUNCTION(BlueprintCallable)
	void AddMaxLifeAndSave(float val);
;
  • .cpp
void ACustomConfigActor::BeginPlay()

	Super::BeginPlay();
	// 在初始化的时候加载配置
	LoadConfig();


void ACustomConfigActor::AddMaxLifeAndSave(float val)

	// 修改属性值,然后进行保存
	MaxLife+=val;
	SaveConfig();

然后再让蓝图继承他,并让他调用AddMaxLifeAndSave函数。

将其拖入到项目中,编译保存,你就可以在\\Saved\\Config\\Windows目录下找到我们自定义的这个配置文件。

ConsoleVariables

这个我感觉对我目前这个阶段帮助最大!!
他可以在控制台读取设置的变量,大大提高了调试的效率!!

只要将我们要控制的变量改成这样的形式就可以对他进行修改:

static TAutoConsoleVariable<bool> CVarDDShopEnabled(
	TEXT("dd.ShopEnabled"),
	false,
	TEXT("The switch to enable shop or not."),
	ECVF_Default);


static TAutoConsoleVariable<类型名> 变量名(
	TEXT("控制台命令"),
	赋的初始值,
	TEXT("提示文本"),
	ECVF_Default);

获取变量值:CVarDDShopEnabled.GetValueOnGameThread()

Raw:txt/csv/json/xml

大佬给出了读取文件的使用方法:

FString ARawDataActor::ReadTxtFile(FString path)

	FString realPath = FPaths::ProjectDir() / path;
	FString content;
	FFileHelper::LoadFileToString(content, *realPath);
	
	return content;


bool ARawDataActor::LoadCsvAsTable(FString path, TArray<FShopItem>& Items)

	UDataTable* DataTable = NewObject<UDataTable>(GetTransientPackage(), FName(TEXT("TempDataTable")));
	DataTable->RowStruct = FShopItem::StaticStruct();

	FString realPath = FPaths::ProjectDir() / path;
	FString content;

	if (!FFileHelper::LoadFileToString(content, *realPath))
	
		return false;
	

	DataTable->CreateTableFromCSVString(content);	//CreateTableFromJSONString

	static const FString ContextString(TEXT("GENERAL"));
	DataTable->ForeachRow<FShopItem>(ContextString, [&Items](const FName& key, const FShopItem& value)
		
			Items.Add(value);
		);

	return true;


bool ARawDataActor::LoadJsonAsTable(FString path, TArray<FShopItem>& Items)

	FString realPath = FPaths::ProjectDir() / path;
	FString content;
	FFileHelper::LoadFileToString(content, *realPath);

	TSharedPtr<FJsonObject> jsonRoot = MakeShareable(new FJsonObject());
	TSharedRef<FJsonStringReader> jsonReader= FJsonStringReader::Create(content);

	TArray<TSharedPtr<FJsonValue>> OutArray;

	if (FJsonSerializer::Deserialize<TCHAR>(jsonReader, OutArray))
	
		for (TSharedPtr<FJsonValue> itemJson: OutArray)
		
			const TSharedPtr<FJsonObject>& obj=itemJson->AsObject();
			FShopItem& newItem= Items.AddDefaulted_GetRef();
			newItem.Name=FText::FromString(obj->GetStringField(TEXT("Name")));
			newItem.Icon=LoadObject<UTexture2D>(nullptr,*obj->GetStringField(TEXT("Icon")));
			newItem.Price = obj->GetNumberField(TEXT("Price"));
		
		return true;
	
	return false;

优点:

  • 可快速编辑大量数据
  • 可比较容易源码管理比较

缺点:

  • 被排除在虚幻烘焙工具之外,需要手动添加
  • 不好做数据合法性验证
  • 没有UI做数值限制
  • 容易搞错格式
  • UE的Reference System无法识别,所以无法
  • 绑定是否可安全删除一个配置
  • 写对UE的资产引用路径很麻烦

Enhanced: sql/excel

  • excel

是DirectExcel插件的使用,我没有买这个插件,所以无法提供笔记,hhh~

  • sql

SQLiteStudio下载地址
觉得下载慢的话可以使用网易加速器,上GitHub特别快!

点击添加



创建一个表:

输入表的名字:

添加相应的字段:

然后点击:Add column

点击OK即可:

然后可以点击加号进行添加对应的信息:

这里可以输入对应的信息进行查询:

也可以在项目中直接进行查询:

bool ASqlDataActor::LoadSqlAsTable(FString path, TArray<FShopItem>& Items)

	FSQLiteDatabaseConnection db;
	if (!db.Open(*path, nullptr, nullptr))
	
		return false;
	

	FString query = FString::Printf(TEXT("SELECT Name,Icon,Price FROM ShopItems"));

	FDataBaseRecordSet* outRecords = nullptr;
	if (!db.Execute(*query, outRecords))
	
		delete outRecords;
		return false;
	

	int count = outRecords->GetRecordCount();
	if (count == 0)
	
		delete outRecords;
		return false;
	

	int result = -1;
	for (FDataBaseRecordSet::TIterator i(outRecords); i; ++i)
	
		FShopItem& newItem = Items.AddDefaulted_GetRef();
		for (FDatabaseColumnInfo column : i->GetColumnNames())
		
			if (column.ColumnName == TEXT("Name"))
			
				newItem.Name = FText::FromString(i->GetString(*column.ColumnName));

			
			else if (column.ColumnName == TEXT("Icon"))
			
				newItem.Icon = LoadObject<UTexture2D>(nullptr, *i->GetString(*column.ColumnName));
			
			else if (column.ColumnName == TEXT("Price"))
			
				newItem.Price = i->GetFloat(*column.ColumnName);
			

		

	
	delete outRecords;
	db.Close();
	
	return true;

工具

BulkEdit

这个讲了,点击移动到出现位置

EditorUtility

将场景中的信息提取到表格里面。


其中有一些函数是无法直接弄出来的,所以要在C++中添加一些东西,将函数暴露在蓝图界面中:

  • .h
UCLASS(Blueprintable, Category = "DataDriven")
class UMyDataTableFunctionLibrary : public UBlueprintFunctionLibrary

	GENERATED_BODY()
public:
#if WITH_EDITOR
	UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName = "AddRowToDataTable")
		static void AddVolumeToDataTable(UDataTable* DataTable, FName rowName,const FVolumeItem& row);

	UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName = "ClearDataTable")
		static void ClearDataTable(UDataTable* DataTable);

	UFUNCTION(BlueprintCallable, Category = "Editor Scripting | DataTable", DisplayName = "DataTable_RemoveRow")
		static void DataTable_RemoveRow(UDataTable* DataTable, FName rowName);
#endif
;
  • .cpp
void UMyDataTableFunctionLibrary::AddVolumeToDataTable(UDataTable* DataTable, FName rowName, const FVolumeItem& row)

	if (DataTable == nullptr)
	
		return;
	
	DataTable->AddRow(rowName, row);
	DataTable->GetOutermost()->MarkPackageDirty();


void UMyDataTableFunctionLibrary::ClearDataTable(UDataTable* DataTable)

	if (DataTable == nullptr)
	
		return;
	
	DataTable->EmptyTable();
	DataTable->GetOutermost()->MarkPackageDirty();


void UMyDataTableFunctionLibrary::DataTable_RemoveRow(UDataTable* DataTable, FName rowName)

	if (DataTable == nullptr)
	
		return;
	
	DataTable->RemoveRow(rowName);
	DataTable->GetOutermost()->MarkPackageDirty();

DataTable->GetOutermost()->MarkPackageDirty();
这个代码就让小星星出现,代表已经对他进行了修改。

以上是关于[中文直播] 第21期 | UE4数据驱动开发 | Epic 大钊课程笔记的主要内容,如果未能解决你的问题,请参考以下文章

[中文直播] 第21期 | UE4数据驱动开发 | Epic 大钊课程笔记

[中文直播] 第21期 | UE4数据驱动开发 | Epic 大钊课程笔记

HMS Core Discovery第14期直播预告~纵享丝滑剪辑,释放视频创作力

DTSE Tech Talk | 第9期:EiPaaS驱动企业数字化转型

DTT第7期直播回顾 | 低代码应用构建流程和适用场景,与你想的一样吗?

UE4数据驱动开发总结