尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

UE5 ECS 原理

UE5 ECS 原理 ECS的核心思想是数据Component与逻辑System分离MassEntity的核心理念也遵循此道并拥有自己的一套术语。一、 为什么需要ECS传统OOP的“血泪史”在游戏开发中最经典的反面教材就是“继承爆炸”和“虚表开销”。“钻石继承”与臃肿的基类假设你有一个Animal基类派生Dog、Bird。但如果要做一个“会飞的狗”Boss战你是继承Dog还是Flying为了复用逻辑往往会在基类里塞满用不上的变量如WingSpan导致内存浪费和逻辑耦合。灾难性的缓存命中率传统OOP中遍历一个std::vectorAActor*数组指针指向的内存是分散在堆上的。CPU 需要频繁从内存中加载数据而CPU访问主内存的速度比访问缓存Cache慢上百倍。当你的“千军万马”成千上万个单位同时更新时CPU 时间全花在“等数据”上而不是“算逻辑”上。修改困难想要给所有“带物理属性的可移动物体”加一个全局重力影响如果它们散落在不同的类继承树中改起来极其痛苦。二、 ECS解决了什么ECS实体-组件-系统把“对象”拆解成“数据组件”和“逻辑系统”带来了三大革命性优势极致的内存连续性Cache-Friendly同类组件比如所有实体的位置FTransformFragment被紧密排列在连续内存中。遍历 1 万个实体更新位置CPU 只需顺序读取速度比传统OOP快数倍甚至数十倍。极低的组合成本Composition over Inheritance想要“会飞的狗”只需给这个实体添加FlyingTag和WingFragment不需要修改任何类继承关系。逻辑解耦与并行化每个“系统”只关心它需要的组件组合Query。因为系统之间不共享状态数据都在组件里UE5 的 Mass 可以轻易将这些系统分配到多线程上并行执行。三、 什么时候必须/强烈建议使用 ECSMass核心指标每个实体的逻辑复杂度更新频率数据是否适合批量处理是否需要大量 UObject/Actor 功能表现、物理、动画和网络需求Mass 带来的架构复杂度是否值得。使用场景具体例子是否推荐大规模群体 AI城市里的行人、蚂蚁搬家、军团对战千军万马、僵尸潮。强烈推荐这就是为它设计的开放世界环境交互满地的落叶、飘动的粒子草、被风吹动的布条、大量的可破坏碎片。推荐复杂的弹幕/投射物同时存在上千颗子弹、弹片或追踪飞弹且需要碰撞计算。推荐动态交通系统GTA 风格的车流每辆车都独立寻路和避障。推荐高频数据计算LOD细节层次距离计算、视锥剔除、网络同步的副本Replication筛选。推荐四、ECS速度快的核心原理首先CPU 并不能直接操作位于主板上的 DDR 内存条RAM。CPU 真正“直接”读取的是它内部自带的缓存Cache数据流动的完整链条是这样的硬盘/SSD → 内存条RAM → CPU 三级缓存L3 → CPU 二级缓存L2 → CPU 一级缓存L1 → CPU 寄存器Registers → 逻辑运算单元ALU当 CPU 核心需要读取entities[i].x的值时会严格按照由近到远、由快到慢的顺序向下查找层级名称容量大小延迟时钟周期延迟纳秒核心归属①寄存器 (Registers)几十个 ~ 几百个 Byte0 ~ 1 个周期~0.1 ns当前核心独占↓ 未命中则下探②L1 缓存 (一级缓存)32KB ~ 64KB1 ~ 3 个周期~1 ns当前核心独占↓ 未命中则下探③L2 缓存 (二级缓存)256KB ~ 512KB10 ~ 15 个周期~5 ns当前核心独占↓ 未命中则下探④L3 缓存 (三级缓存 / LLC)8MB ~ 32MB40 ~ 60 个周期~15 ns所有核心共享↓ 未命中则下探⑤内存条 (RAM / 主存)16GB ~ 128GB300 ~ 500 个周期~100 ns所有核心共享总线↓ 极其罕见缺页⑥硬盘 (虚拟内存/页面文件)极大百万级周期~10,000,000 ns噩梦级延迟打个比方你在工位上CPU寄存器写代码工位抽屉L1缓存里没有笔你让前台去楼下仓库RAM内存找笔等你拿到笔时可能已经过去了一节课的时间。1. RAM内存条全称RandomAccessMemory随机存取存储器。通俗理解计算机的“工作台”或“短期记忆”。核心特性极其重要易失性Volatile断电后数据立即全部清空。速度纳秒级ns极快。作用存放 CPU 即将要处理的指令和数据。我们之前笔记中提到的步骤 ⑤延迟 ~100ns指的就是它。名词解释为什么叫“随机”Random因为它允许 CPU 以相同的速度访问内存条上的任意一个地址不需要从头开始找区别于老式的磁带。2. SSD固态硬盘全称SolidStateDrive固态驱动器 / 固态硬盘。通俗理解计算机的“大型仓库”或“长期记忆”。核心特性极其重要非易失性Non-Volatile断电后数据永久保存。速度微秒级μs甚至毫秒级ms比 RAM 慢千倍至万倍。作用存放操作系统、UE5 工程文件、贴图资源、以及 Windows 的“虚拟内存分页文件”。我们之前笔记中提到的步骤 ⑥噩梦级延迟指的就是它。逻辑上的“清理”你作为游戏开发者看到的当你关闭游戏进程比如退出 UE5 编辑器或打包好的游戏 .exe时操作系统Windows会立即收回所有物理内存条RAM的使用权。不管你的游戏是用了 2GB 还是 64GB 内存Windows 会一次性把这些内存页Pages全部标记为“空闲”Free。你的游戏程序彻底“失忆”了。游戏代码里所有指向内存的指针地址都作废了。游戏再也无法访问之前加载的模型、贴图或 Mass 实体的位置数据。不会“通知”游戏去清理操作系统不会调用你的析构函数Destructor。它是直接“拔网线”式的收回而不是客客气气地让你收拾垃圾。这也是为什么就算游戏有内存泄漏忘记delete关掉游戏后电脑内存也会恢复正常——操作系统会强制替它擦屁股。游戏存档的内容是存到了SSD了. 量化对比ECS vs OOP传统 Actor的命中率差距假设你要遍历 1 万个实体只读取它们的“位置X坐标”传统 OOPActor 模式内存布局对象散落在堆内存各处链表状。读取过程CPU 读ActorA的 X加载了 64 字节 Cache Line。但这 64 字节里包含的是虚表指针、名字、UI引用等乱七八糟的冷数据只有 4 个字节是 X。命中率Cache Line 有效利用率极低约 6%。CPU 刚用完这 4 字节剩下的 60 字节全是废数据被迫频繁去 RAM 搬新的。结果L1 缓存命中率极低可能不到 10%CPU 大部分时间在“等数据”。ECS MassEntity 模式内存布局所有实体的FTransformFragment包含 X被塞进一个纯粹的、连续的数组SoA即结构体数组。读取过程CPU 读Entity[0].X加载 64 字节 Cache Line。因为数组连续这 64 字节里全是下一个实体的 X 坐标假设 X 是 float能装下 16 个实体的 X。命中率当 CPU 循环到Entity[1]时数据早就躺在 L1 缓存里了。结果L1 缓存命中率高达90%~95%。内存连续性空间的局部性ECS 强制把同类型数据如位置放在一起。这不仅让 L1 命中还触发了 CPU 的硬件预取器Prefetcher。CPU 觉得你顺序读得这么爽干脆提前把后面几十个实体的数据从 RAM 自动搬到 L2 缓存里等着。等你用到时连去 RAM 的 100 纳秒都省了。只取热数据数据的纯净性在 Mass 处理器Processor中你只查询需要的组件比如只查FTransform。内存里没有夹杂虚表VTable和函数指针。Cache Line 里塞满了 100% 有效的“弹药”没有一颗是臭弹废数据。避免“伪共享False Sharing”的硬件锁ECS 将只读数据和只写数据分开比如位置只读、速度只写。确保一个 Cache Line64字节不会同时被两个 CPU 核心争抢强行让缓存命中率维持在高位极致的并行性因为每个系统只操作连续数组的不同段几乎没有数据依赖UE5 可以将工作拆分成多个线程Job System让多个 CPU 核心同时干活。如何判断一个数据在哪一个层级寄存器的数据100% 确定判断法则只有正在被 CPU 算术逻辑单元ALU加减乘除的那个数才在寄存器里。实战你无法在调试器里单独看一个“变量”属于哪个寄存器因为寄存器每纳秒都在换数据。只要变量刚刚参与过计算它就在寄存器一旦计算完它就被“写回”缓存或内存了。当CPU开始遍历第0个实体取位置时entities[0].x因为第0个和第1个实体的位置在物理内存上紧挨着CPU读取第0个实体时硬件已经强制把第1个实体甚至第2个部分的位置数据连同第0个一起物理加载进了L1缓存即使是不连续的数据只要你读取了其中一个CPU 的硬件机制依然会把它所在的那块连续 64 字节Cache Line强行拽进 L1。既然指针指向的“目标对象”旁边也会被加载假设你有 10,000 个实体每个位置占 24 字节FVector。总数据量约 240KB远超 L164KB的容量。CPU 的真实动作是“流水线滚动”第一步加载 ACPU 读取 A硬件确实把A、B、C64字节一起拽进了 L1。此时 L1 里躺着 A、B、C。第二步处理 BCPU 处理 B发现 B 已经在 L1零延迟读取。同时硬件预取器算出“这小子在顺序读”于是偷偷把D、E、F下一个 64 字节从 RAM 提前搬到了L2 缓存甚至 L3里候着。第三步处理 CCPU 处理 CC 也在 L1。第四步处理 D当 CPU 需要 D 时发现D 并不在 L1因为 L1 太小且 A、B、C 已经用完了。但是由于预取器提前把 D、E、F 搬到了L2CPU 只需要花~5 纳秒L2 延迟就能把 D 从 L2 请进 L1不需要去 RAM~100 纳秒。问所以到第三步之前AB是同时在L1的第三步的时候是L1存不下C所以存到了L2了这个时候将L1清空然后把L2的C搬到L1上去. 纠正你的两个关键认知必看误解一“C 是因为 L1 满了才存到 L2 的。”真相C一直都存在 RAM 里。在 CPU 处理 A 的时候硬件预取器早就把 C 和 D 所在的下一行64字节主动从 RAM 搬进 L2 缓存里“候着”了。C 进 L2跟 L1 满不满没有半毛钱关系纯粹是预取器在提前“预热”。误解二“第三步的时候将 L1 清空然后把 L2 的 C 搬到 L1。”真相L1 永远不会“清空”自己。它执行的是“按行覆盖Eviction”。当 CPU 需要 C 时如果包含 C 的那一行不在 L1L1 会把最久没用或根据特定算法的那一行比如包含 A 的那一行直接覆盖掉腾出位置给 C。包含 B 的那一行可能还在 L1 的另一个角落里安然无恙。设你的实体位置FVector占 24 字节64 字节的缓存行可以塞下 A24字节 B24字节 C 的前 16 字节。我们按顺序拆解时间点CPU你在干什么后台硬件在干什么L1 缓存槽里到底放着什么数据块L2 缓存里放着什么① 读取 A你喊“把 A 给我”内存控制器从 RAM 里取出块1ABC前半把它塞进 L1 的槽位 #1。槽位 #1 块1含A、B、C的前半段空的还没开始干活② 预取你正在低头处理 A预取器预测你下一步要处理后续数据于是额外从 RAM 里取出块2C后半DEF前半。但它没有乱塞进 L1而是先存进L2 缓存里备着。依然是槽位 #1 块1存着块2含D、E③ 读取 B你处理完 A伸手拿 B。无额外动作查 L1 槽位 #1发现里面是块1B 就在块1里直接命中秒拿。依然存着块2④ 读取 C你伸手拿完整的 C地址48~71。L1 发现 C不完整前半在块1后半在块2。L1 控制器决定把最旧的槽位 #1块1覆盖掉向 L2 发出请求“把块2给我”拿到块2后CPU 在寄存器中把“块1里的前半段”和“块2里的后半段”拼凑成完整的 C 进行计算。槽位 #1 被覆盖旧的块1被擦除换成刚从 L2 拿来的块2。块2里实际装着C后半段 D E F前半段块2 被转移走后L2 暂时清空或继续预取后续的块3、块4。⑤ 读取 D、E你接着拿 D、E。预取器继续去 RAM 拿后续的块3、块4存进 L2为后续 F、G 做准备查 L1 槽位 #1发现里面是块2D 和 E 完整地躺在块2里直接命中秒拿。预取器已经把块3、块4搬进了 L2等着你下一轮替换。如果说“缓存”是 CPU 的“高速工作台”那么Cache Line缓存行就是这个工作台上“最小的操作单位”。Cache Line 是 CPU 缓存L1、L2、L3与内存条RAM之间数据交换的最小固定块大小固定为 64 / 128 字节Byte。多核时代的“隐形杀手”伪共享False Sharing这是 ECS 必须把数据拆开的最核心理由也是游戏多线程开发中最容易踩的深坑。场景CPU 有两个核心Core 0 和 Core 1它们同时在并行处理两个不同的逻辑变量。Core 0 要疯狂修改实体 A 的位置变量 X。Core 1 要疯狂修改实体 B 的速度变量 Y。最坏布局变量 X 和 Y 在物理内存上紧挨着正好处于同一条 Cache Line64字节里。硬件冲突MESI 协议Core 0 把这条 Cache Line 读进自己的 L1修改了 X。CPU 硬件规定同一条 Cache Line 不能同时被两个核心以“修改Modified”状态持有。当 Core 1 要修改 Y 时它发现自己 L1 里的这条 Cache Line 已经“过期”了被 Core 0 污染。它必须强制 Core 0 把这条 Cache Line 写回 RAM然后重新从 RAM 搬到 Core 1 的 L1。紧接着 Core 0 又要改 X它发现自己的 L1 又过期了被 Core 1 污染又得去 RAM 重新搬。结果本来 X 和 Y 毫无逻辑关系但因为挤在了一条 64 字节的“小船上Cache Line”两个核心疯狂地互相踢皮球来回抢夺这条 Cache Line 的所有权。这导致原本应该是 L1 命中1纳秒的操作硬生生变成了频繁访问 RAM100纳秒性能直接崩塌百倍。这就是臭名昭著的伪共享False Sharing。3. 灵魂拷问数据怎么避免被“切碎”对齐 Alignment你之前特别关心的“C 被切两半”问题根子就在 Cache Line 的边界上。未对齐跨行如果一个 24 字节的FVector从地址48开始存放那么它的后半段在地址64之后。为了读这一个 FVectorCPU 必须去 RAM 搬运两次 Cache Line先搬 0~63再搬 64~127延迟翻倍。对齐Aligned如果强制让每个FVector都从 64 的整数倍地址如0,64,128开始存放那么每一个FVector都完美地躺在独立的 Cache Line 里永远不会有跨行读取。例子分析假设有一个FItem结构体它的大小sizeof(FItem)是 20 字节而它的对齐要求alignof(FItem)是 8 字节。如果不做处理直接连续存放 20 字节的结构体下一个元素的起始地址就会是 20。20 不能被 8 整除就导致未对齐。但通过Align(20, 8)结果会是24。这意味着在存放完第一个FItem后会填充Padding4 个无用的字节让第二个FItem从地址 24 开始存放。这样每个元素都对齐到了 8 字节的边界上。在追求极致性能的 MassEntity 框架中内存对齐直接关系到CPU缓存的利用效率。实际场景在设计一个行人的实体时可能会包含以下 Fragment数据片段Fragment类型大小 (bytes)对齐要求访问频率Transform6416高Velocity124高LODState44中Avoidance3216高例子分析对齐与大小选择Transform和Avoidance的大小64和32都是它们对齐要求16的整数倍这样的设计非常“干净”。缓存行Cache Line友好CPU的缓存行通常是 64 字节。Transform正好 64 字节意味着一个Transform数据可以恰好填满一个缓存行。当处理器需要更新所有实体的位置时它可以一次性将连续的多个Transform数据加载到L1缓存中实现极高的缓存命中率。分组策略Mass 还会将相同“原型Archetype”的实体数据组织在连续的内存块Chunk中。比如所有行人实体的Transform数据会被紧密排列在一起形成一个纯Transform的大数组。这种布局让CPU在遍历时能进行高效的顺序读取。使用USTRUCT时的正确做法对于被USTRUCT宏标记的结构体由于UHTUnreal Header Tool的限制不能直接使用标准的Calignas关键字。❌ 错误示例像下面这样写会编译报错cpp// 这会导致 UHT 解析失败 USTRUCT(BlueprintType) struct alignas(64) FSomeStruct { GENERATED_BODY() // ... };✅ 正确做法官方的推荐方案是嵌套一个原生C结构体定义一个普通的、并使用alignas的C结构体。将这个结构体作为USTRUCT的唯一成员变量。cpp// 1. 定义对齐的原生结构体 struct alignas(64) FAlignedNativeData { float X, Y, Z; // ... 其他数据 }; // 2. 在 USTRUCT 中嵌套使用 USTRUCT(BlueprintType) struct FMyUStruct { GENERATED_BODY() UPROPERTY() FAlignedNativeData Data; };通过这种方式FMyUStruct的实例在内存中就能达到 64 字节对齐同时避免与 UHT 发生冲突。片段 (Fragment)实体的“数据零件”Fragment是 MassEntity 中最基础的数据单位代表一个原子的数据块。它只包含数据不包含任何逻辑。类比就像乐高积木中的基础颗粒本身没有功能但组合起来就能拼成各种东西。代码示例cpp// 一个自定义的片段存储实体的生命值 USTRUCT() struct FHealthFragment : public FMassFragment { GENERATED_BODY() // 实际数据 float Health 100.0f; };️ 标签 (Tag)实体的“身份标记”Tag是一种特殊的空片段不包含任何数据。它的存在或缺失本身就是一种数据。作用主要用于标记和分类实体。它的存在与否用于查询和过滤而无需存储额外数据。类比就像乐高积木上的颜色或形状标记。一个红色颗粒和一个蓝色颗粒结构相同但“红色”这个标签让它们有了不同身份。代码示例cpp// 一个标签用于标记“已经死亡”的实体 USTRUCT() struct FDeadTag : public FMassTag { GENERATED_BODY() // 注意这里没有任何成员变量 };之后处理器可以专门查询带有FDeadTag的实体来进行清理或者忽略它们。 原型 (Archetype)实体的“分类模板”Archetype是具有完全相同 Fragment 和 Tag 组合的一类实体。拥有相同“零件”和“标记”的实体就属于同一个Archetype。作用将实体分类同一Archetype的实体数据在内存中被紧密排列在一起实现缓存友好的高性能访问。类比工厂里生产同一款乐高套装的流水线。这条流水线上所有零件和说明书都完全一样。特性实体的构成可以在运行时改变。比如当一个实体生命值归零给它加上FDeadTag时它的“构成”就变了因此会被从一个 Archetype 迁移到另一个 Archetype。 块 (Chunk) 与 块片段 (ChunkFragment)数据分组的“货盘”Chunk是内存中一个连续的、固定大小的区域用于存放同一个Archetype的多个实体数据。ChunkFragment则是关联到整个Chunk的数据而不是单个实体。作用Chunk确保数据连续以提高性能ChunkFragment用于存储该组实体的共享信息避免为每个实体重复存储相同数据。类比Chunk像物流中心的标准货盘把同一款套装集中码放。ChunkFragment像贴在货盘上的标签如目的地属于整盘货物而非单个盒子。同一种 Fragment 在同一个 Chunk 内连续。也就是说Chunk 里不是这样存E0: Transform, Velocity, Health E1: Transform, Velocity, Health E2: Transform, Velocity, Health而更像这样存Chunk for Archetype: Transform Velocity Health FEnemyTag Entities: E0, E1, E2, E3 ... Transform: T0, T1, T2, T3 ... // 连续 Velocity: V0, V1, V2, V3 ... // 连续 Health: H0, H1, H2, H3 ... // 连续 Tags: FEnemyTag // Tag 通常不占每实体数据 ChunkFrag: Shared data for this chunk问我怎么感觉fragment怎么和component有点像呢为什么你觉得“像”概念层面的相似都是为了“组合”在传统 UE 中你给一个Actor挂载MovementComponent让它能移动在 Mass 中你给一个Entity添加FTransformFragment让它有位置。都遵循“组合优于继承”你不需要写一个FlyingDog类只需要把FlyingTag和WingFragment组合在一起。打住相似之处仅此而已。接下来的区别决定了它们完全是两个世界的产物。为什么“不像”本质上的天壤之别我们把它们拆成四个维度对比你一眼就能看穿对比维度UActorComponent传统组件FMassFragmentECS 片段① 本质身份它是一个UObject对象。带有虚表VTable、指针、引用计数。它是一个POD纯数据结构。只有成员变量没有虚表没有析构函数像个 C 语言的结构体。② 内存位置散落在堆Heap各个角落。每个 Component 都是new出来的地址随机。全部塞在连续的大数组Chunk里。所有实体的位置紧挨着排成一条线。③ 谁管逻辑组件自带逻辑如TickComponent。数据和逻辑绑在一起。片段不带逻辑。数据只是“肉”逻辑全在“处理器Processor”里。④ 访问速度Cache 命中率极低。遍历 1000 个 Actor 的位置CPU 要跳转 1000 次内存地址。Cache 命中率 90%。遍历 1000 个位置CPU 像读流水账一样顺过去。举一个最“扎心”的例子假设你要遍历 1000 个敌人修改它们的位置Location传统 Component 做法cpp// 你要拿到敌人的 Actor再找到它的 RootComponent再拿位置 for (AActor* Actor : Enemies) { FVector Loc Actor-GetRootComponent()-GetRelativeLocation(); Loc ...; // 这里的每一步都是指针寻址CPU 等到心碎 }Mass Fragment 做法cpp// Processor 直接拿到所有位置组成的纯数组 for (FTransformFragment Frag : TransformArray) { Frag.Location ...; // 数据在内存里排着队等你CPU 全速狂奔 }问那我又觉得system和processor很像呢在经典的 ECS 理论如 Unity DOTS 或 Flecs中这个逻辑处理模块确实就叫System系统。而在 UE5 的 MassEntity 中它叫Processor处理器。举个例子Processor具体在代码中使用的例子场景设定一个简单的移动系统我们要写一个系统让所有带有位置Transform和速度Velocity的实体每帧向前移动。1. 头文件.h—— 定义这个“工人”cpp// MyMovementProcessor.h #pragma once #include MassProcessor.h #include MyMovementProcessor.generated.h UCLASS() class UMyMovementProcessor : public UMassProcessor { GENERATED_BODY() public: UMyMovementProcessor(); protected: // 重写两个核心函数 virtual void ConfigureQueries() override; virtual void Execute(FMassEntityManager EntityManager, FMassExecutionContext Context) override; private: // 这就是“筛选器”相当于工人的“招聘条件” FMassEntityQuery EntityQuery; };2. 配置查询ConfigureQueries—— 招工条件cpp// MyMovementProcessor.cpp #include MyMovementProcessor.h #include MassCommonFragments.h // 包含 FTransformFragment #include MassMovementFragments.h // 包含 FVelocityFragment UMyMovementProcessor::UMyMovementProcessor() { // 告诉调度器这个系统在“物理模拟后”运行 ExecutionFlags (int32)(FMassProcessorExecutionFlags::PostPhysics); } void UMyMovementProcessor::ConfigureQueries() { // 开始配置招聘条件 EntityQuery.AddRequirementFTransformFragment(EMassFragmentAccess::ReadWrite); EntityQuery.AddRequirementFVelocityFragment(EMassFragmentAccess::ReadOnly); EntityQuery.RegisterWithProcessor(*this); }解读这个系统明确宣布“我要处理所有同时拥有FTransformFragment和FVelocityFragment的实体。位置我要改ReadWrite速度我只看看ReadOnly。”3. 执行逻辑Execute—— 这就是 System 在代码里真正干的事cppvoid UMyMovementProcessor::Execute(FMassEntityManager EntityManager, FMassExecutionContext Context) { // 1. 获取每帧的微小时间增量DeltaTime const float DeltaTime Context.GetDeltaTimeSeconds(); // 2. 执行查询把符合条件的数据拽出来批量处理 EntityQuery.ForEachEntityChunk(EntityManager, Context, [this, DeltaTime](FMassExecutionContext Context) { // 【重点来了】这里是系统真正工作的“心脏” // 3. 获取两个纯数据数组的指针它们在内存里完全连续 // 这就是 ECS 缓存友好的核心系统直接拿到两排紧挨着的纯数据。 TArrayViewFTransformFragment TransformList Context.GetMutableFragmentViewFTransformFragment(); TArrayViewconst FVelocityFragment VelocityList Context.GetFragmentViewFVelocityFragment(); // 4. 获取这一块Chunk里有多少个实体 const int32 NumEntities Context.GetNumEntities(); // 5. 【纯 CPU 友好循环】没有任何虚函数没有指针跳转 for (int32 i 0; i NumEntities; i) { // 从“速度数组”里取数据只读 const FVector Vel VelocityList[i].Velocity; // 从“位置数组”里取数据并修改可写 FVector Loc TransformList[i].Transform.GetLocation(); // 物理移动公式新位置 老位置 速度 * 时间 Loc Vel * DeltaTime; } }); }数组遍历速度快100% 是因为内存连续。而且它和你刚刚学的 ECS 跑得快的底层逻辑是同一套硬件机制
返回列表