1. 从蓝图到C++:UE5数组操作的全景图
如果你在UE5里折腾过数据,无论是用蓝图拖线还是用C++写逻辑,数组(Array)绝对是你绕不开的一个坎。它太基础了,基础到很多教程都默认你会,但真用起来,从简单的增删改查到性能优化、内存管理,处处是细节。今天我们不聊那些高深莫测的渲染管线或网络同步,就扎扎实实地把UE5里数组那点事掰开揉碎了讲清楚。你会发现,这个看似简单的数据结构,在UE5的特定语境下,有着一套非常独特且强大的玩法,尤其是在处理游戏对象、配置数据、状态管理时,用对了能极大提升开发效率和运行性能。
UE5中的数组,本质上是一个动态的、类型安全的容器。说它动态,是因为它不像C语言里的原生数组那样大小固定,你可以随时往里添加(Add)或移除(Remove)元素,底层会自动处理内存的分配与回收。说它类型安全,是因为UE5通过强大的反射系统和模板,确保了数组里只能存放你声明的特定类型的对象,比如一个TArray<AActor*>里就只能放Actor指针,你塞个FString进去编译器立马报错,这比用void*指针安全多了。
为什么UE5要自己搞一套TArray,而不是直接用C++标准库的std::vector?这是个好问题。简单来说,是为了深度集成。TArray和UE5的垃圾回收(Garbage Collection)、序列化(Serialization,用于保存/加载游戏)、网络复制(Replication)、蓝图暴露等核心系统是“原生好友”。一个UPROPERTY()标记的TArray,可以无缝地在编辑器中编辑、在蓝图中访问、通过网络同步、被垃圾回收器安全地管理。而std::vector是标准的C++容器,UE5的这些系统不认识它,你需要做大量的额外工作才能让它融入UE的生态。所以,在UE5项目里,除非有极其特殊的性能考量或第三方库要求,否则TArray是默认且推荐的选择。
2. 蓝图中的数组:可视化操作的便捷与陷阱
对于策划、美术或初学编程的开发者来说,蓝图是快速实现游戏逻辑的神器。数组在蓝图里以一个漂亮的、可以展开的列表形式存在,操作直观。
2.1 核心节点与基础操作
在蓝图中,你创建一个数组变量后,会看到一系列专为它生成的函数节点。最常用的莫过于“添加”(Add)和“获取”(Get)。添加节点很简单,把元素连上去,执行引脚一触发,元素就进数组了。获取节点则需要一个“索引”(Index),也就是位置编号(从0开始),它会把对应位置的元素输出给你。
这里有个新手常踩的坑:“获取”节点是“纯”节点(Pure),它不执行任何操作,只是读取数据。而“设置”节点是“执行”节点(Impure),它会改变数组本身。如果你只是想看看数组里某个位置是什么,用“获取”;如果你想修改那个位置的值,必须用“设置”。我曾见过有人试图用“获取”节点输出的值去连接一个修改逻辑,然后纳闷为什么数组没变——因为你根本没调用“设置”。
另一个高频操作是“查找”(Find)。蓝图提供了“查找数组元素”节点,你可以输入一个值,它会返回该值第一次出现的位置索引(如果没找到则返回-1)。这个节点在判断某个物品是否在背包、某个状态是否在列表中时非常有用。但要注意,对于自定义结构体或对象,你需要确保它们有正确的“等于”(==)比较运算符,否则查找可能失效。
2.2 遍历与“For Each Loop”的注意事项
当需要对数组里每一个元素都做点事情时,“For Each Loop”节点就派上用场了。它会把数组拆开,每次循环取出一个元素和当前的循环索引给你处理。
这里有一个性能与逻辑上的重要细节:不要在遍历数组的过程中,直接对这个数组进行“添加”或“移除”操作。想象一下,你正在数一队士兵,数到一半,突然有个新兵插队进来,或者一个士兵离队,你的计数和顺序就全乱了,很可能导致漏数、多数甚至崩溃。蓝图的“For Each Loop”在内部会记录数组的当前大小,如果你在循环里增删元素,这个内部记录就可能失效。
那如果确实需要在遍历时修改数组怎么办?一个常见的技巧是使用一个“临时数组”。先遍历原数组,把需要“移除”的元素索引或需要“添加”的新元素记录到另一个临时数组或变量里,等遍历完全结束后,再一次性处理这些修改。对于添加,你可以先把新元素都加到一个“待添加数组”里,遍历结束后再用“追加数组”(Append)节点合并。对于移除,更安全的做法是使用“移除索引”(Remove Index)节点,但要从后往前移除。因为从前面移除会改变后面所有元素的索引,而从后往前移除则不影响尚未处理到的元素索引。
2.3 数组的“引用”与“副本”迷思
蓝图里有个容易混淆的概念:当你把一个数组变量“获取”出来,然后传递给一个函数(或连接到另一个变量的“设置”),你传递的是它的“引用”还是一个“副本”?
默认情况下,在蓝图函数之间传递数组参数,传递的是“副本”。这意味着,函数内部对数组的修改(如添加、移除、排序)不会影响到外部的原始数组。这有时是好事(保护原始数据),但有时会导致意料之外的结果(你以为修改了,其实没有)。
如果你希望函数能修改原始数组,有几种方法:
- 将数组变量本身作为目标:直接对那个数组变量调用“添加”、“移除”等执行节点。
- 使用“按引用传递”的输出引脚:在自定义函数中,你可以将数组参数设置为“输出”(Output),并勾选“按引用传递”(Pass by Reference)。这样传入的数组就能在函数内部被直接修改。
- 使用“数组变量”节点:通过“获取数组变量”的“引用”(Reference)输出,你可以得到一个对该数组变量的直接引用,后续操作都会作用到原变量上。
理解这一点,能避免很多“我的数组怎么没变?”的调试之夜。
3. C++中的TArray:性能与控制的利器
当你从蓝图步入C++,TArray的强大和灵活性才真正展现。它是UE5容器库的基石,源码在Runtime/Core/Public/Containers/Array.h里,有兴趣可以深挖,其设计哲学(如SOA优化、内存分配器)非常值得学习。
3.1 声明、初始化与内存布局
在C++头文件中声明一个数组很简单:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Inventory") TArray<FString> ItemNames; // 一个可编辑的字符串数组 TArray<AActor*> NearbyEnemies; // 一个Actor指针数组 TArray<int32> Scores; // 一个整数数组UPROPERTY()宏让这个数组能被蓝图和编辑器访问,这是UE生态整合的关键。
初始化也有多种姿势:
// 直接初始化列表 (C++11风格) TArray<int32> Primes = {2, 3, 5, 7, 11}; // 使用AddDefaulted或Emplace直接构造 TArray<FMyStruct> MyStructs; MyStructs.AddDefaulted(5); // 添加5个默认构造的FMyStruct MyStructs.Emplace(Arg1, Arg2); // 在数组末尾就地构造一个FMyStruct,传入构造参数Emplace比先创建对象再Add更高效,因为它避免了临时对象的构造和移动/拷贝操作。
TArray在内存中通常是一块连续的内存空间。这带来了一个关键特性:缓存友好性。当你遍历一个TArray时,CPU可以高效地将相邻元素预加载到高速缓存中,这比遍历一个元素分散在内存各处的链表(如TLinkedList)要快得多。这也是UE5在很多性能敏感场景推荐使用TArray而非其他容器的原因之一。
3.2 增删改查的API哲学
TArray的API设计非常丰富,几乎涵盖了所有你能想到的操作。
添加元素:
Add(const T& InItem): 拷贝或移动一个已存在对象到数组末尾。Emplace(Args&&... Args): 在数组末尾就地构造一个新对象,性能通常更好。Insert(const T& InItem, int32 Index): 在指定索引处插入元素,后面元素后移。Append(const TArray& SourceArray): 将另一个数组的所有元素追加到末尾。
访问元素:
operator[]: 像原生数组一样用下标访问,但不进行边界检查,访问越界会导致未定义行为(很可能崩溃)。仅在百分百确定索引有效时使用。GetData(): 获取底层裸指针,用于需要C风格数组指针的场合(如某些第三方C库接口)。Last(int32 IndexFromEnd = 0): 方便地获取最后一个元素(Last())或倒数第N个元素。
安全的访问:
IsValidIndex(int32 Index): 检查索引是否有效。Num(): 获取数组当前元素数量。在循环前用Num()获取长度,而不是用GetData()后靠结束指针判断,更符合UE风格。
for (int32 i = 0; i < MyArray.Num(); ++i) { // 安全循环 }查找元素:
Find(const T& Item): 返回首次出现的索引,找不到返回INDEX_NONE。FindLast(const T& Item): 返回最后一次出现的索引。Contains(const T& Item): 返回是否存在该元素。IndexOfByKey(const KeyType& Key): 这是一个强大的函数,你可以传入一个“键”,并提供一个自定义的投影函数(通常用Lambda),来查找数组中满足某个条件的元素。例如,在一堆FPlayerInfo结构体中查找PlayerId为特定值的元素。
int32 Index = PlayerArray.IndexOfByKey(DesiredPlayerId, [](const FPlayerInfo& Info){ return Info.PlayerId; });移除元素:
RemoveAt(int32 Index, int32 Count = 1, bool bAllowShrinking = true): 从指定索引开始移除Count个元素。bAllowShrinking决定是否允许释放多余内存。RemoveSingle(const T& Item): 移除第一个匹配项。RemoveAll(const T& Item): 移除所有匹配项。RemoveAll([&](const T& Item){ return /*条件*/; }): 用Lambda表达式条件移除,非常灵活。Empty(): 清空数组。Reset(): 清空数组并可能释放内存。
这里要重点提一下**“移除”操作的成本**。RemoveAt不是简单地把那个位置置空,它为了保持内存的连续性,会将后面所有元素都向前移动一位。如果一个有1000个元素的数组,你移除索引0的元素,那么后面的999个元素都需要移动,这是一个O(n)操作。如果你需要频繁从大型数组的头部或中部移除元素,这可能会成为性能瓶颈。此时可以考虑使用TArray的RemoveAtSwap方法。
3.3 RemoveAt 与 RemoveAtSwap:理解“顺序”的代价
RemoveAt和RemoveAtSwap是两种不同的移除策略,理解它们的区别对写出高性能代码至关重要。
RemoveAt(int32 Index):保持顺序,可能移动大量元素。它移除指定位置的元素后,会将后面所有元素向前移动一格,以填补空缺。这样,除了被移除的元素,其余元素的相对顺序保持不变。代价是移动操作可能很昂贵。RemoveAtSwap(int32 Index):不保持顺序,但速度极快。它移除指定位置的元素后,不会移动后面所有元素,而是简单地将数组的最后一个元素拿过来,放到这个空缺的位置上,然后把数组大小减1。这样做,整个操作是O(1)的,非常快。但代价是:数组的元素顺序被改变了(除了被移除的那个,最后一个元素跑到了前面)。
如何选择?
- 如果你关心元素的顺序(比如数组代表一个队列、一个按时间排序的列表),用
RemoveAt。 - 如果你不关心顺序,只关心“集合”本身(比如一堆需要处理的敌人,处理完哪个就移除哪个),那么
RemoveAtSwap是性能更优的选择。游戏里很多“对象池”、“活跃对象列表”都采用这种方式。
// 假设我们有一个敌人数组,不关心顺序 TArray<AEnemy*> ActiveEnemies; // ... 某个敌人死亡 int32 DeadEnemyIndex = /* 找到死亡敌人的索引 */; ActiveEnemies.RemoveAtSwap(DeadEnemyIndex); // 快速移除,最后一个敌人补位4. 进阶操作:排序、过滤、变换与算法集成
基础的增删改查满足不了复杂的需求,TArray和UE5的算法库提供了更高级的操作。
4.1 排序:自定义你的规则
TArray的Sort函数允许你传入一个自定义的比较函数(Lambda、函数指针或函数对象),来决定排序的规则。
TArray<FPlayerScore> PlayerScores; // 按分数降序排序 PlayerScores.Sort([](const FPlayerScore& A, const FPlayerScore& B) { return A.Score > B.Score; // 返回true表示A应该排在B前面 }); // 如果元素类型本身定义了 operator<,可以直接排序 TArray<int32> Numbers = {5, 1, 4, 2, 3}; Numbers.Sort(); // 默认升序排序排序是原地进行的,会修改原数组。对于大型数组,排序是一个O(n log n)的操作,需注意性能。
4.2 过滤与变换:从TArray到TArrayView
有时我们需要从一个数组中筛选出满足条件的子集,或者对每个元素进行某种转换。虽然TArray没有直接的Filter和Map方法(像一些函数式语言库那样),但我们可以结合C++的算法和TArray的特性轻松实现。
过滤(Filter):
TArray<AActor*> AllActors; // 获取所有活着的敌人 TArray<AActor*> AliveEnemies; for (AActor* Actor : AllActors) { AEnemy* Enemy = Cast<AEnemy>(Actor); if (Enemy && !Enemy->IsDead()) { AliveEnemies.Add(Enemy); } } // 或者使用算法库(需要包含<algorithm>) TArray<AActor*> AliveEnemies2; AliveEnemies2.Reserve(AllActors.Num()); // 预分配内存,避免多次扩容 std::copy_if(AllActors.begin(), AllActors.end(), std::back_inserter(AliveEnemies2), [](AActor* Actor){ /* 判断条件 */ });变换(Map):
TArray<FString> Names; TArray<int32> NameLengths; NameLengths.Reserve(Names.Num()); for (const FString& Name : Names) { NameLengths.Add(Name.Len()); }对于只读的遍历或传递给函数,使用TArrayView<const T>或const TArray<T>&是更好的选择,它们避免了不必要的拷贝,只是提供了一个数据的“视图”。
4.3 算法库的强力支持
UE5在Algo命名空间下提供了一系列泛型算法,它们能很好地与TArray(以及其它容器)协作。
Algo::Find(Array, Predicate): 查找第一个满足条件的元素,返回指针。Algo::FindByKey(Array, Key, Projection): 类似IndexOfByKey,返回元素指针。Algo::Transform(SourceArray, DestArray, TransformFunc): 将源数组的每个元素经过变换函数处理后,放入目标数组。Algo::Accumulate(Array, InitValue, BinaryOp): 累积计算(如求和、求积)。
#include "Containers/Array.h" #include "Algo/Accumulate.h" TArray<int32> Values = {1, 2, 3, 4, 5}; int32 Sum = Algo::Accumulate(Values, 0); // 求和,结果为15这些算法让代码更简洁,意图更明确,也减少了手写循环可能引入的错误。
5. 性能优化与内存管理实战心得
数组用起来简单,但用得好、用得高效,需要一些经验和技巧。
5.1 预分配内存:避免反复“搬家”
TArray是动态数组,底层是一块连续内存。当不断Add元素,容量(Capacity)不足时,它会分配一块更大的新内存,把旧数据全部搬过去,然后释放旧内存。这个“搬家”操作(重分配,Reallocation)的成本是O(n)的,如果频繁发生,会严重影响性能。
解决方案是:如果你事先知道或能估算出数组大概会存放多少元素,使用Reserve(int32 Number)函数预分配足够的内存。
TArray<FVector> PathPoints; PathPoints.Reserve(ExpectedPathLength); // 预分配内存 for (int32 i = 0; i < ExpectedPathLength; ++i) { PathPoints.Add(CalculatePoint(i)); // 添加时不会再触发重分配 }Reserve只影响容量(Capacity),不影响大小(Num)。Slack函数也有类似作用,但它保证容量至少为指定值,如果当前容量已经更大,则什么都不做。
5.2 元素类型的选择:对象、指针还是TSharedPtr?
数组里放什么类型的元素,对性能和内存管理有巨大影响。
存放对象(
TArray<FMyStruct>):适用于小型、简单的POD(Plain Old Data)类型或轻量结构体(如FVector,FRotator,FString)。优点是内存局部性好,访问速度快。缺点是如果结构体很大,拷贝成本高(比如Add一个已有对象会发生拷贝)。存放原始指针(
TArray<AActor*>):在UE中非常常见,尤其是存放AActor派生类的指针。你需要非常清楚这些指针所指向对象的生命周期。UE的垃圾回收系统会管理UObject派生类,但数组本身不拥有这些对象。如果对象被销毁了,数组里就成了悬空指针(Dangling Pointer),访问会导致崩溃。通常,这类数组用于临时收集场景中的对象(如UGameplayStatics::GetAllActorsOfClass返回的数组),不会长期持有。存放智能指针(
TArray<TSharedPtr<FMyObject>>):适用于需要共享所有权的非UObject自定义C++对象。TSharedPtr提供引用计数,当数组和所有其他持有者都释放了引用,对象才会被自动删除。这避免了内存泄漏和悬空指针,但引入了少量的引用计数开销。TUniquePtr(独占所有权)通常不放在TArray里,因为它的所有权语义和数组的拷贝行为有冲突。存放
TWeakObjectPtr(TArray<TWeakObjectPtr<AActor>>):当你需要引用一个UObject,但又不想阻止它被垃圾回收时使用。TWeakObjectPtr是安全的弱引用,即使对象被销毁,它也不会导致崩溃(IsValid()会返回false)。非常适合用于缓存、观察者列表等场景。
选择原则:优先考虑对象本身(如果很小且拷贝廉价),其次考虑原始指针(如果你能严格管理生命周期,比如数组生命周期短于对象),需要共享所有权时用TSharedPtr,需要弱引用UObject时用TWeakObjectPtr。
5.3 数组的“收缩”与内存碎片
当你从一个大数组中移除了很多元素后,数组的Capacity可能远大于当前的Num,造成内存浪费。你可以调用Shrink()函数来释放未使用的内存,将容量缩减到刚好容纳当前元素。
但是,Shrink本身也可能触发一次内存重分配和拷贝。不要在每个Remove操作后都调用Shrink,这会导致频繁的内存操作。通常是在确定数组大小已经稳定,并且未来不会很快增长到之前的大小时(比如一局游戏结束,清理临时数据),才调用一次Shrink。
另外,TArray的Empty()函数有一个可选参数int32 Slack。Empty(0)会清空元素并尽可能释放内存(Capacity设为0)。而Empty(100)则会清空元素,但将容量保持在至少100,为后续添加预留空间,避免立即重分配。
6. 结合热词:解决实际开发中的高频问题
看看网络上的搜索热词,很多都是数组相关具体问题的缩影,我们来针对性解答。
“对象数组去重”:这是非常常见的需求。TArray本身没有直接的Unique函数,但实现起来不难。一种方法是先Sort排序,然后使用std::unique算法配合Erase函数。但前提是元素类型定义了相等的概念。对于自定义类型,你需要提供自定义的比较函数给Sort,并且std::unique默认使用==运算符,你可能需要提供自定义的二元谓词。
TArray<FString> Strings = {"A", "B", "A", "C", "B"}; Strings.Sort(); // 排序是去重的前提 Strings.SetNum(Algo::Unique(Strings)); // Algo::Unique将重复项移到末尾,返回新结尾迭代器,SetNum裁剪数组更通用的做法是使用TSet。TSet本身就是一个不包含重复元素的集合。你可以把数组元素添加到TSet中,自动去重,然后再转回数组(如果还需要数组特性的话)。但要注意TSet不保持插入顺序。
“vector动态数组不能直接输出名字吗”:这个问题可能源于对std::vector和UE反射系统的不熟悉。在C++标准库中,std::vector<int>当然可以直接输出其内容。但在UE的蓝图或编辑器细节面板中,想要“显示”或“编辑”一个容器,需要容器类型被UE的反射系统所知。TArray是UE原生支持的,添加了UPROPERTY()就能在编辑器中显示为可折叠的列表。而std::vector没有UE反射支持,所以无法在蓝图中直接访问或在编辑器细节面板中可视化。这不是“不能输出名字”,而是生态整合的问题。
“对于一个数组,其连续的相同段叫做一个平台...”:这是一个经典的算法问题,在游戏里也可能遇到,比如计算连续得分奖励、分析地形平坦度等。解决思路是遍历数组,用一个变量CurrentValue记录当前平台的值,一个变量CurrentLength记录当前平台长度,一个变量MaxLength记录最大平台长度。当元素值改变时,结算上一个平台,并开始新的平台计数。用TArray实现这个非常直接。
“给你一个整数数组nums和一个整数k,请你返回其中出现频率前k高的元素”:这是LeetCode经典的Top K问题。在UE开发中,可能会用于分析玩家行为数据、热门物品统计等。解决方案通常使用“哈希表计数 + 优先队列(堆)”。
- 遍历数组,用
TMap<int32, int32>记录每个数字出现的频率。 - 维护一个大小为K的小顶堆(Min-Heap),堆元素是
(频率, 数字)对。 - 遍历哈希表,将每个数字的频率放入堆中,如果堆大小超过K,则弹出堆顶(当前最小的频率)。
- 遍历结束后,堆中剩下的就是频率最高的K个数字。 UE中可以用
TArray作为堆的容器,配合std::make_heap,std::push_heap,std::pop_heap算法,或者使用TBinaryHeap模板类(如果项目自定义了的话)。
“指针、数组、结构体本质区别”:这在C++和UE中都是核心概念。
- 指针:一个变量,其值是另一个变量的内存地址。它本身大小固定(如4或8字节),用于间接访问数据。在UE中,原始指针需要谨慎管理生命周期。
- 数组:一种数据结构,用于在连续内存中存储多个相同类型的元素。通过索引(偏移量)直接访问元素。
TArray是对原生数组的封装,增加了动态大小、内存管理等功能。 - 结构体:一种数据类型,用于将多个不同类型的变量组合成一个整体。它定义了一个内存布局。在UE中,
FStruct(如FVector)通常用于数据聚合,值语义,拷贝时进行位拷贝(浅拷贝,除非成员有深拷贝需求)。
在TArray<FMyStruct>中,数组存储的是结构体对象本身(值)。在TArray<FMyStruct*>中,数组存储的是指向结构体的指针(地址)。前者管理结构体的生命周期(随数组创建销毁),后者需要你另外管理结构体对象的内存。
7. 避坑指南与最佳实践总结
最后,分享一些我踩过坑后总结的经验,希望能帮你少走弯路。
坑1:迭代器失效。这是C++容器的经典陷阱,在TArray中同样存在。当你对数组进行添加(可能导致重分配)或移除操作时,所有指向该数组元素的指针、引用和迭代器都可能失效。这意味着,如果你在遍历数组(特别是使用基于范围的for循环或迭代器)时修改了数组,后续的行为是未定义的。
// 错误示范! for (auto& Item : MyArray) { if (ShouldRemove(Item)) { MyArray.Remove(Item); // 移除操作可能导致迭代器失效,循环行为不可预测! } } // 正确做法1:使用RemoveAll配合Lambda(推荐) MyArray.RemoveAll([&](const T& Item){ return ShouldRemove(Item); }); // 正确做法2:记录索引,从后往前移除 for (int32 i = MyArray.Num() - 1; i >= 0; --i) { if (ShouldRemove(MyArray[i])) { MyArray.RemoveAt(i); } }坑2:UPROPERTY()与网络复制。如果你希望一个数组能在客户端和服务器之间同步,必须用UPROPERTY(Replicated)标记它,并在GetLifetimeReplicatedProps函数中正确设置复制条件(如DOREPLIFETIME)。复制整个大数组的每一帧是灾难性的,通常需要设计增量更新或只复制变化的部分。对于TArray里的元素类型,如果它们是UObject派生类,也需要考虑其本身的复制属性。
坑3:默认构造函数与AddUninitialized。TArray的某些操作要求元素类型有默认构造函数。例如AddDefaulted()、InsertDefaulted()、SetNum()增加元素时。如果你的自定义结构体没有默认构造函数,这些操作会编译失败。此时,你可以使用AddUninitialized()或SetNumUninitialized(),但你必须随后手动初始化这些新元素,否则它们的内容是未定义的,可能导致崩溃。
坑4:跨模块边界使用。如果你的TArray类型(特别是元素类型)在某个模块(如Game模块)中定义,并试图在另一个模块(如插件模块)中通过UPROPERTY暴露或传递,可能会遇到链接错误。确保类型定义被所有使用它的模块共享(通常放在一个公共的模块中),并且正确设置了模块依赖(.Build.cs文件)。
最佳实践清单:
- 优先使用
TArray,除非有充分理由使用std::vector或其他容器。 - 预估大小,善用
Reserve,避免运行时频繁重分配。 - 根据场景选择移除策略:关心顺序用
RemoveAt,追求性能用RemoveAtSwap。 - 传递大型数组时,使用
const TArray<T>&或TArrayView避免不必要的拷贝。 - 在蓝图中,理清“引用”与“副本”,明确你的修改意图。
- 对于需要查找的集合,考虑
TSet(无序唯一)或TMap(键值对),它们的查找效率是O(1)或O(log n),远高于TArray的O(n)。 - 使用
IsValidIndex()进行安全检查,特别是在使用用户输入或不确定的索引前。 - 保持数组元素类型的轻量,如果元素很大,考虑存储指针或智能指针。
- 在性能关键循环中,将
Num()的调用提到循环外,避免每次循环都调用函数。 - 善用
Algo命名空间下的算法,让代码更清晰、更安全。
数组是数据的基石,在UE5里用好TArray,是构建高效、稳定游戏逻辑的第一步。从蓝图的直观操作到C++的精细控制,理解其背后的原理和设计取舍,能让你在应对各种数据集合场景时游刃有余。