跳至主要內容

NT-2.3|数据结构

Mr.Si大约 9 分钟unreal

数据结构|结构体

头像
一般哪些数据结构可以承载数据?
头像
结构体数组够简单也够暴力!配合Json转结构体插件还能网络数据同步。
头像
也可以用DataTable

数据结构|Datatable|表格

USTRUCT(BlueprintType)
struct  FCharacterStruct : public FTableRowBase
{
	GENERATED_BODY()
public:
	
	UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(DisplayName="Name"))
	FString Name;

	UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(DisplayName="Icon", MakeStructureDefaultValue="None"))
	TObjectPtr<UTexture2D> Icon;
	
	UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(DisplayName="SkeletalMesh", MakeStructureDefaultValue="None"))
	TObjectPtr<USkeletalMesh> SkeletalMesh;
	
	UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(DisplayName="SkeletalAnim", MakeStructureDefaultValue="None"))
	TObjectPtr<UAnimSequence> SkeletalAnim;

	UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(DisplayName="SkeletalAnimSpeed", MakeStructureDefaultValue="1.000000"))
	float SkeletalAnimSpeed = 1.0;
	
	UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(DisplayName="id", MakeStructureDefaultValue="0"))
	int32 id  = 0;

};
头像
蓝图中不支持游戏运行时读写表格,C++可以。实际开发中会配合调用现成的API来获取对应数据。

数据结构|Json-API

API(Application Programming Interface)是一组定义软件组件之间交互的规范。简而言之,API定义了不同软件之间如何通信、相互调用功能的方式。

头像
一般公司里会有前后端大佬负责编写服务端逻辑,同时会暴露一些地址给你,你通过HTTP请求可以获取到对应的数据。例如这样的:

数据结构|Json

头像
获取到数据一般是Json格式加密数据,我们需要通过特定手段解析成我们的游戏数据。这些数据有可能是临时的、也可能是持久的!

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它以易于阅读和编写的文本形式表示数据。

{
  "key1": "value1",
  "key2": 123,
  "key3": true,
  "key4": ["item1", "item2"],
  "key5": {
    "nestedKey": "nestedValue"
  },
  "key6": null
}

数据结构|数据库

头像
然而这些数据并非空穴来风,需要一个远程物理机器保存,比如百度网盘能够保存你的文件数据,这里我们有专门的数据库和存储空间保存用户的个人信息。

数据结构|数据资产

头像
后续会有详细介绍

抛砖引玉

Table+子系统+资产实现配表驱动

  1. 使用子系统UInventorySubsystem

    • 引入专门的物品子系统,例如 UInventorySubsystem,用于处理物品的管理和交互。
  2. 暴露切换网络接口 (ENUM)

    • 使用枚举类型(ENUM)定义网络切换的接口,确保系统在本地测试和网络测试时有不同的行为。
    • 示例:InventorySubsystem->SetNetworkMode(NetworkMode);
  3. 本地部分使用流程

    • 在适当时机注册 InventoryHUDLayout,可参考Lyra的UIExtension。
    • 示例:InventorySubsystem->RegisteInventoryHUDLayout(InventoryHUDLayout);
  4. 在适当的位置注册总物品表 TotalItemTable 到物品子系统

    • 示例:InventorySubsystem->RegisterTotalTable(TotalItemTable);
    • 例如:
      • 行名1(武器.近身.匕首), UDataTable* SubTable
      • 行名2(武器.近身.长剑), UDataTable* SubTable
      • 行名3(道具.血包), UDataTable* SubTable
  5. 准备Spawn的物体Actor中配置GameTag和Index

    • 示例:ItemActor->ConfigureItem(GameTag, Index);
  6. Actor中Over事件调用子系统查表

    • 获取子表格的引用,例如:UDataTable* SubTable = InventorySubsystem->GetSubTableFromTotalTable(GameTag);
    • 通过GameTag直接定位到总表格的对应行,获取子表格的引用。
  7. 子系统根据Fragments实例化内容

    • 根据Fragments数组中的 UInventoryItemFragment 子类实例,配置对应的内容。
    • 例如:基础 TextureFrag(Icon)、AS(属性)、MeshComponent(网格体)、AddWidget(比如根据 HUD.SlotUIClass 配置要显示的 UI)等
  8. 子系统暴露交互键注册函数

    • 子系统应当暴露接口,用于注册交互键,可能涉及 UI 的绑定以及信息更新。
    • 示例:子系统接口RegisterInteractionKey

职责思考

头像
虽然看上去很详尽,停下脚步来思考。这个InventorySubsystem到底充当一个什么角色?
头像
更像一个数据管理者,同时能够转发一些数据变化。
头像
没错,他就是一个图书馆管理员。负责定位书籍、同时广播一些信息。
头像

既然是图书馆管理员,那么他的职责不应该只限管理儿童读物,其他用户数据也可以归他管理。

头像
这么看来,改名成PlayerDataSubsystem更加合适。
头像
不急于下定论,先来讨论几个问题。

注册

注册|注册时机

头像
注册加载时机?
头像
UGameInstanceSubsystem,我觉得在GameInstance启动阶段注册数据最佳。
/**注册装备数据表。**/
UFUNCTION(BlueprintCallable, Category="PlayerDataSubsystem|InventoryData")
void RegisterInventoryTable(UDataTable* Table);

/**获取装备表格。**/
UFUNCTION(BlueprintPure, Category="PlayerDataSubsystem|InventoryData")
UDataTable* GetInventoryTable() const;

//装备表格
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta=(AllowPrivateAccess))
UDataTable* InventoryTable;
头像
GameInstance拥有仅次于Engine的超长生命周期,目前来说,这个阶段启动阶段注册表格比较合理。
头像
需要稍微加点延迟,因为GameInstanceSubsystem比宿主GameInstance会稍微晚一点。
头像
也就是可能出现没有加载数据,UI不刷新的情况?
头像

这点咱也考虑到了,所以我们所有的UI和数据都是用委托绑定的,一旦数据更新或者真正加载完成也能执行刷新任务。

注册|动态更新

头像
一种简单的思路就是绑定一个委托。

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDataUpDateDelegate)

UPROPERTY(BlueprintAssignable, Category = "DataSubsystem")
FDataUpDateDelegate DataInitDelegate;
头像
为什么没有参数?
头像
当数据获取渠道是固定时,往往只需要一个无参委托就能通知绑定的UI进行自我刷新,始终牢记他和我们UI管理系统的区别。

注册|标签关联

进一步思考

头像
假设我们现在有更多的表格数据要处理,每次都要写一个注册函数?
void RegisterInventoryTable(UDataTable* Table);
void RegisterPlayerDataTable(UDataTable* Table);
......
头像
虽然这种数据表格可能不会太多,但总觉得这种链表式的注册不太友好。也许可以改成通用点的标签关联表格?然后借用Tag来获取对应的表格数据。
	//增加表格
	UFUNCTION(BlueprintCallable, Category="DataSubsystem")
	void InsertDataTableByTag(UDataTable* Table,FGameplayTag Tag);
	
	//删除表格
	UFUNCTION(BlueprintCallable, Category="DataSubsystem")
	void DeleteDataTableByTag(FGameplayTag Tag);

	//查找数据
	UFUNCTION(BlueprintCallable, Category="DataSubsystem")
	UDataTable* SelectDataTableByTag(FGameplayTag Tag,bool & IsFind );
	
    TMap<FGameplayTag, UDataTable*> DataTableMap;
头像
本质上更像一个数据库,负责增删改查,但那些更具体的数据呢?比如某个血条?
头像
没错,说到关键上了,Data作为更加抽象的系统,有绝对统治权、一票否决权, 而前文的UI子系统,更加贴近UI,同时和我们的UI业务逻辑也更加耦合。可以简单画个图:

注意现在的所有讨论都是一种思维演变的讨论,不应该作为最终版本的参考。

转换

数据转换|数据源

头像
等会等会,数据源呢?光顾着讨论建表了。
头像
其实这个问题也有迹可循,就像Mysql这种数据库作为一个系统,虽然不直接提供 HTTP 请求 API,通过 MySQL 协议(通常是基于 TCP/IP 的)与客户端进行数据交互。
头像
早在最开始的HTTP篇我们就讨论过了这一步主要是将一些业务逻辑封装成一个一个请求链接通过POST、GET等方法获取数据。
头像
那么这个过程的本质是什么?
头像
HTTP 的本质是传递字符串,但它可以通过规范的格式和约定来传输各种类型的数据。
头像
SO问题也就变成了怎么解析这些字符串。

数据转换|HTTP请求

头像
这个部分可以用官方插件来做,咱先不讨论实现细节

数据转换|格式转换

头像
那么后面的Json到DataTable呢?
头像
虽然C++中没有Json这样的数据结构,UE官方给我们提供了一些转换函数。
//jsontostring
bool FJsonObjectWrapper::JsonObjectToString(FString& Str) const
{
    TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> JsonWriter = TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Str, 0);
    return FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter, true);
}
//jsonfromstring
bool FJsonObjectWrapper::JsonObjectFromString(const FString& Str)
{
    TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(Str);
    return FJsonSerializer::Deserialize(JsonReader, JsonObject);
}

数据转换|RuntimeDataTable

头像
OK,这也是本文的重点,UE默认都是编辑器上编辑DataTable的,官方认为这是开发者维护的只读权威表格。 他推荐我们使用数据资产/Datetable/SaveObject/Config来完成数据序列化。
头像
这也太麻烦了,牵连的东西多了维护起来就麻烦,我已经可以预见这个系统的复杂度了,改个东西还要调用设置类啥的真的麻烦。
void UPlayerDataSubsystem::SetHeroesSkinIDMap(TMap<FName, int32>& InPlayerSelectedHero)
{
	UExorcistSettingsLocal* Settings = UExorcistSettingsLocal::Get();
	Settings->SetHeroesSkinIDMap(InPlayerSelectedHero);
	Settings->SaveSettings();
}

权限

修改权限

头像
这个DataTable并不是不能修改。只是怎么修改、能修改哪些——本质是一个修改权限问题。

建议咱们回顾一下 NT-2.网络同步-01|Actor复制 对网络模型的介绍部分

头像
咱们可以按照约定,指定一些权限规则。
权限级别可修改数据可修改贴图可修改皮肤ID修改方式
0直接
1直接
2请求

独立模式

  • 用户拥有权限级别 0 - 2,因为客户端和服务器都由用户自己控制。权限 0 更像是开发者模式,开启状态。

监听服务器模式

  • 服务器端拥有权限级别 0 和 1,因为对于其他客户端来说,服务器是权威版本。其他客户端只有权限级别 2。

专用服务器模式

  • 所有客户端都只有权限级别 2,因为服务器是专用服务器,不允许其他客户端修改数据。
头像
注意:这样无法避免替换本地同名的情况。
头像
让我想起当年CFTP扫盘时代了,替换了喷漆皮肤,虽然只能本地看也很爽。
头像
这里的DataTable在不同阶段用途也不同。