前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UE5的StructUtils

UE5的StructUtils

作者头像
quabqi
发布2023-03-07 19:53:47
1.2K0
发布2023-03-07 19:53:47
举报
文章被收录于专栏:Dissecting UnrealDissecting Unreal

StructUtils是UE5新增的一个针对结构体存储和反射的辅助插件。之前在说UE5的ECS框架Mass时有粗略提到这个插件里的相关内容,比如Mass在实现ECS的Component就是使用了FInstancedStruct来保存元信息。有了FInstancedStruct,Component不必在C++预先定义好,可以直接在蓝图进行定义或组合,甚至让ECS支持lua或其他脚本都很容易,相比于其他C++常见的ECS框架,这也是UE5的ECS很有优势的一个点。

除了FInstancedStruct外,StructUtils这个插件里还有很多其他非常有用的类,比如StructTypeBitSet,StructView,SharedStruct,PropertyBag等,可以很大程度增强引擎的UStruct功能,又因为这个插件本身没有过多依赖,所以移植到UE4使用也不难。StructUtils这么有用的库,搜索全网基本没有发现相关资料,独乐乐不如众乐乐(封面番《孤独摇滚》就讲的这故事),为了造福国内开发者普及UE5,本文主要就来揭秘这些类的使用方法和内部原理。

FInstancedStruct

这个结构非常简单,只有两个成员变量,一个是结构体的类型,一个是实际的内存。使用时,可以根据ScriptStruct的实际信息,把StructMemory转化为实际的结构,如下提供了一系列获取实际结构的模板函数,就能像普通结构体一样使用。

FInstancedStruct通常是作为一个类的Property来使用,就如下面注释所示。

Property如果本身是个结构体对象,里包含的UScriptStruct本身就能描述实际的类型了,为什么又封装一层搞一个FInstancedStruct出来,这不是多此一举吗?其实UE5这么做,最主要的原因是,虚幻的结构体不像UObject对象,没有一个公共的基类UObject,这样在配置时就无法支持多态:一个配置文件里要填一个UStruct对象,就只能是这个对象本身,不能是对象的子类。

下面具体来说多这一层和没有多这一层的区别,看下面这3个结构体:

然后在蓝图中作为成员变量(配置)来使用:

可以发现这里结构体并不像UObject一样,不能设置为子类。UObject是可以的,就比如Character的移动组件,可以设为抛物线移动组件,角色移动组件等。我们现在改为FInstancedStruct再看看:

可以看到默认值这里不一样了,多了一个类型选择的框,这里即可以选择基类MyBaseStruct,也可以选择子类:

这样就实现了结构体配置的多态。这也就是FInstancedStruct最大的用处。

你可能会说,需要多态的时候直接定义成UObject不就好了,为什么非要用UStruct。这是因为引擎里有不少的配置项限定了只能使用UStruct,比如数据表Datatable定义表结构时,就只能使用FTableRowBase的子类,这个类就是F开头的结构体,结构体内部是不能保存UObject实例作为UPROPERTY的,这时如果想制作一个通用的数据表,在UE5之前,我们就只能定义一个完整的表结构,包含所有可能用到的数据,用不到的数据列就会浪费掉,当可变的项越多浪费的就越多。而UE5就可以使用FInstancedStruct让表格实现多态。

最常用的一个例子:比如做一个物品表,物品可能是一次性使用道具,也可能是一个装备,不同类型的物品配置项肯定是不同的,这时我们使用FInstancedStruct作为物品表的结构描述,然后每种实际的物品配置都定义成子结构,这样我们就可以把这些不同类型的装备,道具配置在一张表里,也不会有额外的内存浪费。

当然这里在蓝图里直接指定FInstancedStruct,并不能限定可接受的结构体类型只想限定MyBaseStruct子类,这就不符合我们最初的想法,如下图所示,可以看到下拉菜单里可以选择任意结构体:

那么要怎样限定呢?我们知道,Mass中大量使用了FInstancedStruct作为Fragment的配置,在设置关联Fragment时并没有出现任意的结构体,只能选择FMassFragment的子类,如下图所示。

找到AssortedFragmentsTrait实际源码,可以看到是这样做的:

这里额外指定了一个meta,限定了基类,并且排除基类本身。引擎额外对FInstancedStruct制作了一个自定义的编辑器,编辑器会读取meta信息,限定指定的基类。

FInstancedStructArray

StructUtils还提供了FInstancedStructArray,不过很可惜并没有提供编辑器,所以在蓝图中看不到。

和TArray<FInstancedStruct>区别是,FInstancedStructArray中的元素,在内存上是连续的,每个元素类型是有可能不同的,大小也是不一样的,下图就是这两种容器的内存分布情况。注意FInstancedStructArray的内存是示意图,实际情况每个元素之间还可能会有额外的alignment,容器总Size大于等于所有元素内存之和。

FInstancedStructStream,FChunkedStructBuffer

这两个类就不细说了,也是混合结构体的容器,相比于FInstancedStructArray或者TArray<FInstancedStruct>的区别就是不支持随机访问,只能按顺序迭代访问

PropertyBag

前面有说UScriptStruct里面不能以UObject作为Property(编辑器直接禁用了),即使使用FInstancedStruct间接保存UObject类型的成员Property,也会出现存不住的问题(可以绕过编辑器的禁用代码,能填但是保存后再读取就会变为空,应该是编辑器的禁用逻辑没考虑完善的Bug)。如果就是想要在结构体中保存对象,这时可以使用StructUtils插件中的PropertyBag来实现,同时也支持任意增加,删除内部的属性,是一个非常强大又有用的类。

具体用法注释写的非常详细,在结构体中只需要使用FInstancedPropertyBag作为Property即可。引擎中的StateTree的参数就是使用PropertyBag来实现的:

在编辑器中可以看到,支持添加任意类型,保存StateTree时,Parameters数据也能正常保存。

研究FInstancedPropertyBag内部实现,可以看到里面其实就是包了一层FInstancedStruct。

而这个InstancedStruct类型被强制指定为了UPropertyBag类型。

这个类型非常有意思,只有一个成员就是FPropertyBagPropertyDesc数组,看GetOrCreateFromDescs的源码可以发现,其实这个ScriptStruct并不是在编译或者在蓝图阶段提前就创建好的一个类,而是在运行时根据传入的子类型的hash来动态创建不同的UPropertyBag,这样就保证了不同结构的PropertyBag是不一样的类型,而相同的结构类型是一致的。

我们自己业务如果有动态创建虚幻带反射的类型需求时,比如版本更新后,想用脚本热更新创建新的类型,就可以考虑参考这样的实现。

FInstancedPropertyBag内部也单独实现了序列化函数Serialize,有兴趣可以自行查看源码来了解。

TStructTypeBitSet

这个类型内部本质就是一个BitArray。在Mass中也有大量使用:ECS需要快速获取Archtype中Component的多个类型信息,直接遍历会非常不效率,这个类就相当于是将引擎中所有的类都进行唯一编码,每个类型占1位,当Archtype使用了哪个类型,对应类型的Bit就置1,这样就能快速获取类型了。而这些类型第一次传入时就会被注册,因此一共占多少位,并且每个类型对应编码是多少,是按先来后到的顺序确定的,每次运行都有可能不一样,但同一次运行时的编码是唯一确定的。

可以看到,默认的编码都注册到了FStructTracker里,是一个全局的静态成员变量,因此在当前运行时能保证编码唯一。我们如果也有需求想使用TStructTypeBitSet,就可以通过第二个模板参数指定我们自己的FStructTracker,这样可以保证编码和Mass的编码不会产生冲突。

FSharedStruct

看注释也写的非常清楚。这个类本质上和TSharedPtr<FInstancedStruct>是差不多的。TSharedPtr内部会保存一个指针,而FInstancedStruct内部也会保存一个指针,这就导致了要取到实际的数据需要用指针的指针,使用起来就会很难受。

整个实现其实没什么特别的,就是将两层指针改为了一层指针,关键点就是内存实际大小是这个结构后续的部分,这里用uint8 StructMemory[0],手动Malloc实际的内存来使用,自定义了智能指针的Deleter来保证可以Free掉之前Malloc的内存。其实这里做的还是不太彻底,使用的MakeShareable创建的智能指针,因为智能指针本身的计数器也额外分配了一次内存,可以考虑将整个内存合并一次申请,就像MakeShared那样。估计是因为StructUtils是个插件,也不太好改动引擎原始代码所以没这么做。

FSharedStruct也提供了const版本FConstSharedStruct

FStructView,FStructArrayView

这个类就更简单了,内部成员和FInstancedStruct一样,但其实只提供了访问的对应函数。从名字View结尾可以看到,就类似ArrayView一样,只是一个FInstancedStruct的视图,并不负责实际的存储。同样提供了Const版本FConstStructView,以及数组版本FStructArrayView

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-01-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • FInstancedStruct
  • FInstancedStructArray
  • FInstancedStructStream,FChunkedStructBuffer
  • PropertyBag
  • TStructTypeBitSet
  • FSharedStruct
  • FStructView,FStructArrayView
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档