概念页
容器与字符串
Containers & Strings
UE 不用 STL,而用自带的值类型容器:TArray(连续数组)、TMap/TMultiMap(哈希映射)和 TSet(哈希集合),都假定元素可按字节直接搬移。字符串分三种:FName 存索引、比较便宜,FText 用于可本地化的界面文本,FString 可以修改但开销最大。选错类型的代价主要是性能和本地化。
容器:值语义与内存布局
- TArray 是最常用的容器,「快、省内存、安全」。它是值类型:拥有自己的元素,拷贝时复制全部元素、不共享状态,销毁时元素一并销毁;不应
new/delete它,也不应继承它。模板参数是元素类型和可选的分配器 (allocator),默认分配器从堆上分配,并在容量不够时一次多分配几个元素的空间。(Array Containers in Unreal Engine) - Add 与 Emplace:Add 先构造一个临时对象再拷贝或移动进数组,Emplace 直接用参数在数组里构造。官方经验法则:平凡类型用 Add,其他类型用 Emplace;Emplace 永远不会比 Add 慢。(Array Containers in Unreal Engine)
- TMap / TSet 基于哈希:键类型要提供
GetTypeHash和operator==(或自定义 KeyFuncs)。底层是稀疏数组,删除元素会留下空洞,之后新增的元素可能填进空洞,所以遍历顺序不可靠、元素在内存中也不连续。扩容时整块存储会重新分配,所以指向元素的指针可能失效。TMap 遇到重复键时用新值替换旧值;TMultiMap 则两个都保留。TSet 的增删查都是常数时间。(Map Containers in Unreal Engine、Set Containers in Unreal Engine) - 三者都假定元素是「可平凡搬移 (trivially relocatable)」的,扩容时直接按原始字节拷贝;元素里如果有指向自身内部的指针,这个假设会出问题。
| 容器 | 结构 | 顺序 | 适用 |
|---|---|---|---|
| TArray | 连续内存 | 稳定 | 绝大多数列表;遍历快、对缓存友好 |
| TMap | 稀疏数组 + 哈希 | 不保证 | 按键查找 |
| TSet | 稀疏数组 + 哈希 | 不保证 | 去重、判断是否存在 |
(表格依据上述三页归纳。)
三种字符串
| 类型 | 原理 | 用途 | 注意 |
|---|---|---|---|
| FName | 全局唯一字符串表;FName 只存索引和实例号,用哈希表做字符串到 FName 的转换 | 资产名、骨骼名、标识符等需要频繁比较的字符串 | 不区分大小写、不可变;== 只比较索引,「大量节省 CPU」 |
| FText | 关联本地化数据 | 所有面向玩家的文本 | 与 FString 互转会丢失本地化数据 |
| FString | 自带字符数组 | 需要搜索、修改、拼接的字符串 | 可修改,所以比另外两种更贵 |
(String Handling in Unreal Engine、FName in Unreal Engine、FText in Unreal Engine、FString in Unreal Engine)
转换的坑(官方原文标为「危险」):FString → FName 会丢信息,因为 FName 不区分大小写;FText → FString 对某些语言可能丢信息;FText 不能直接转成 FName,要先转 FString。FText::FromString 在编辑器构建中不会把文本标为文化无关 (culture invariant),保存到资产里的 FText 属性后仍可能变成可本地化文本;真正不需要本地化的文本用 AsCultureInvariant 或 INVTEXT。(FText in Unreal Engine)只想判断某个名字是否已在名称表里、又不想新增条目时,用 FName(TEXT("x"), FNAME_Find),不存在时得到 NAME_None。(FName in Unreal Engine)
实践建议
- 界面上显示的字符串一律用 FText,否则后期本地化要返工。
- 热路径里避免反复把 FString 构造成 FName(每次都要查哈希表);常用名字应缓存为静态 FName。
- 容器作为
UPROPERTY时,GC 才能看到里面的 UObject 引用,见 UObject 与反射系统、垃圾回收与对象生命周期。(与 GC 的关联依据已有页面。)
出处
- Array Containers in Unreal Engine UE 5.8 文档
- Map Containers in Unreal Engine UE 5.8 文档
- Set Containers in Unreal Engine UE 5.8 文档
- String Handling in Unreal Engine UE 5.8 文档
- FString in Unreal Engine UE 5.8 文档
- FName in Unreal Engine UE 5.8 文档
- FText in Unreal Engine UE 5.8 文档
相关页面
游戏开发 · UE5智能指针与 TObjectPtrSmart Pointers & TObjectPtrUE 有两套对象寿命管理:普通 C++ 对象用引用计数智能指针(TSharedPtr/TSharedRef/TWeakPtr/TUniquePtr),UObject 由垃圾回收管理,不能放进这些智能指针。UE5 新增的 TObjectPtr 用来替代 UPROPERTY 里的 UObject 裸指针:编辑器构建中提供动态解析和访问跟踪,非编辑器构建中与裸指针表现相同。游戏开发 · UE5UObject 与反射系统UObject & Reflection System虚幻引擎 (Unreal Engine) 的对象基类是 UObject。在 C++ 类、属性、函数上加 UCLASS/UPROPERTY/UFUNCTION 等宏,再由构建前的预处理工具虚幻头文件工具 (Unreal Header Tool, UHT) 扫描头文件生成代码,引擎就能在运行时“看见”这些类型——这就是反射系统 (Reflection System)。垃圾回收、序列化、编辑器面板、蓝图、网络复制都建立在它之上。游戏开发 · UE5配置文件系统Configuration FilesUE 的 .ini 配置按类别(Engine、Game、Input……)分层加载:从引擎 Base 文件到项目 Default 文件,再到平台文件和用户文件,后加载的同名键覆盖先加载的。数组可以用 +、-、! 等运算符跨层增删。UCLASS(config=X) 加上 UPROPERTY(Config) 可以让类的默认值从 ini 自动读取;命令行 -ini: 可以临时覆盖。游戏开发 · UE5本地化LocalizationUE 的本地化以 FText 类型为中心,国际化(数字、日期、复数规则、文化回退)依赖 ICU 库。可本地化的文本由命名空间、键、源字符串三部分标识;本地化管线按目标 (Localization Target) 依次收集 (Gather) 到 manifest、翻译进各文化的 archive(通常经 PO 文件导出导入)、编译成运行时加载的 .locres。Localization Dashboard 是管理这条管线的推荐工具。游戏开发容器与哈希表Containers & Hash Tables容器的职责都是容纳和管理零个或多个元素,差别在于实现方式与取舍。GEA 概览了常见容器与操作、迭代器、大 O 复杂度与内存布局的综合考量,讨论引擎为何常自研容器、C++ 标准库 / Boost / Folly / Loki 的利弊,并重点讲了动态数组的增长策略与哈希表(开放哈希与封闭哈希、哈希函数、线性/二次探测、Robin Hood 哈希)。游戏开发字符串与唯一标识符Strings & Unique Identifiers字符串在 C/C++ 中只是字符数组,变长意味着要么硬编码长度上限、要么动态分配;比较和复制都是 O(n)。引擎内部大量用字符串做资源名和对象 ID,处理不当会严重拖慢游戏。GEA 的建议:运行时代码尽量避免字符串类,若用则了解其成本并按引用传递;路径类是值得专门做的例外;对象与资产的唯一标识用哈希字符串 ID(string id),兼得字符串的可读性与整数的比较速度。游戏开发游戏本地化与 UnicodeGame Localization & Unicode本地化(国际化,I18N)是把软件改编为其他语言版本,最好从第一天起规划并贯穿每个开发步骤。GEA 介绍了 Unicode 与 UTF-32/UTF-8/UTF-16/UCS-2 编码、char 与 wchart、Windows 的字符串 API 约定、主机上的选择,字符串之外的本地化问题,以及核心基础设施——字符串数据库 + 按 ID 查找当前语言字符串的运行时函数,并以 Naughty Dog 的本地化工具为例。游戏开发 · UE5垃圾回收与对象生命周期Garbage Collection & Object LifecycleUE 用标记-清除式的可达性分析管理 UObject 内存:从根集合 (Root Set) 出发沿反射可见的强引用遍历对象图,找不到的对象在下次垃圾回收 (Garbage Collection, GC) 时被删除。因此“谁持有 UPROPERTY 引用”决定了对象活多久;Actor 调用 Destroy() 只是请求销毁,真正释放仍取决于引用是否全部放开。