Unity DOTS中的Archetype与Chunk

Unity DOTS中的Archetype与Chunk

在Unity中,archetype(原型)用来表示一个world里具有相同component类型组合的entity。也就是说,相同component类型的entity在Unity内部会存储到一起,共享同一个archetype。

Unity DOTS中的Archetype与Chunk1

使用这样的设计,可以高效地通过component类型进行查询。例如,如果想查找所有具有component类型 A 和 B 的entity,那么只需找到表示这两个component的所有archetype,这比扫描所有entity更高效。

entity和componenet并不直接存储在archetype中,而是存储在称为chunk的连续内存块中。每个块由16KB组成,它们实际可以存储的entity数量取决于chunk所属archetype中component的数量和大小。 这样做可以更好匹配缓存行,同时也能更好支持并行操作。

在这里插入图片描述

对于一个chunk,它除了包含用于每种component类型的数组之外,还包含一个额外的数组来存储entity的ID。例如,在具有component类型 A 和 B 的archetype中,每个chunk都有三个数组:一个用于component A的数组,一个用于component B的数组,以及一个用于entity ID的数组。

Unity DOTS中的Archetype与Chunk3

chunk中的数据是紧密连续的,chunk的每个entity都存储在每个数组连续的索引中。添加一个新的entity时,会将其存储在第一个可用索引中。而当从chunk中移除entity时,chunk的最后一个entity将被移动以填补空隙。添加entity时,如果对应archetype的所有chunk都已满,则Unity将创建一个全新的chunk。类似地,当从chunk中移除最后一个entity时,Unity会将该chunk进行销毁。

在编辑器中,Unity提供了Archetypes窗口,可以方便预览当前所有的archetype。我们先用代码创建4个entity,然后给它们分别添加两种component:

public partial struct FirstSystem : ISystem
{public void OnCreate(ref SystemState state) { Entity e1 = state.EntityManager.CreateEntity();Entity e2 = state.EntityManager.CreateEntity();Entity e3 = state.EntityManager.CreateEntity();Entity e4 = state.EntityManager.CreateEntity();state.EntityManager.AddComponentData(e1, new FirstComponent { Value = 1 });state.EntityManager.AddComponentData(e2, new FirstComponent { Value = 2 });state.EntityManager.AddComponentData(e3, new FirstComponent { Value = 3 });state.EntityManager.AddComponentData(e3, new SecondComponent { Value = 13 });state.EntityManager.AddComponentData(e4, new FirstComponent { Value = 4 });state.EntityManager.AddComponentData(e4, new SecondComponent { Value = 14 });}
}public struct FirstComponent : IComponentData
{public int Value;
}public struct SecondComponent : IComponentData
{public int Value;
}

那么,根据之前的分析,e1和e2应该属于同一个archetype,而e3和e4则属于另一个archetype。在编辑器中运行,观察Archetypes窗口,可以发现的确如此:

Unity DOTS中的Archetype与Chunk4

Unity DOTS中的Archetype与Chunk5

根据官网的解释,Archetypes窗口中可以观察到以下信息:

PropertyDescription
Archetype nameThe archetype name is its hash, which you can use to find the archetype again across future Unity sessions.
EntitiesNumber of entities within the selected archetype.
Unused EntitiesThe total number of entities that can fit into all available chunks for the selected Archetype, minus the number of active entities (represented by the entities stat).
ChunksNumber of chunks this archetype uses.
Chunk CapacityThe number of entities with this archetype that can fit into a chunk. This number is equal to the total number of Entities and Unused Entities.
ComponentsDisplays the total number of components in the archetype and the total amount of memory assigned to them in KB. To see the list of components and their individual memory allocation, expand this section.
External ComponentsLists the Chunk components and Shared components that affect this archetype.

自此,我们对Unity的archetype和chunk有了基本的认识。接下来我们来看看源代码,深入一下细节部分。

在上面的例子中,我们使用EntityManager.CreateEntity这个函数来创建entity。函数内部会在创建entity之前,检查是否存在相应的archetype。由于此时尚不存在表示一个不含任何component类型的空entity的archetype,那么函数会先去创建archetype:

internal EntityArchetype CreateArchetype(ComponentType* types, int count, bool addSimulateComponentIfMissing)
{Assert.IsTrue(types != null || count == 0);EntityComponentStore->AssertCanCreateArchetype(types, count);ComponentTypeInArchetype* typesInArchetype = stackalloc ComponentTypeInArchetype[count + 2];var cachedComponentCount = FillSortedArchetypeArray(typesInArchetype, types, count, addSimulateComponentIfMissing);return CreateArchetype_Sorted(typesInArchetype, cachedComponentCount);
}

每个空的entity都要额外添加一个Simulate类型的component,这也是上面的截图中为何会多出一个component的原因。Unity首先会尝试从已有的空闲archetypes中寻找符合条件的archetype,没找到才会去真正创建一个。

public Archetype* GetOrCreateArchetype(ComponentTypeInArchetype* inTypesSorted, int count)
{var srcArchetype = GetExistingArchetype(inTypesSorted, count);if (srcArchetype != null)return srcArchetype;srcArchetype = CreateArchetype(inTypesSorted, count);return srcArchetype;
}

Unity的Archetype是一个非常复杂的struct,它记录了所代表的component信息,以及chunk信息,还有它们当前的状态:

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct Archetype
{public ArchetypeChunkData Chunks;public UnsafeList<ChunkIndex> ChunksWithEmptySlots;public ChunkListMap FreeChunksBySharedComponents;public ComponentTypeInArchetype* Types; // Array with TypeCount elementspublic int* EnableableTypeIndexInArchetype; // Array with EnableableTypesCount elements// back pointer to EntityQueryData(s), used for chunk list cachingpublic UnsafeList<IntPtr> MatchingQueryData;// single-linked list used for invalidating chunk list cachespublic Archetype* NextChangedArchetype;public int EntityCount;public int ChunkCapacity;public int TypesCount;public int EnableableTypesCount;public int InstanceSize;public int InstanceSizeWithOverhead;public int ScalarEntityPatchCount;public int BufferEntityPatchCount;public ulong StableHash;public ulong BloomFilterMask;// The order that per-component-type data is stored in memory within an archetype does not necessarily match// the order that types are stored in the Types/Offsets/SizeOfs/etc. arrays. The memory order of types is stable across// runs; the Types array is sorted by TypeIndex, and the TypeIndex <-> ComponentType mapping is *not* guaranteed to// be stable across runs.// These two arrays each have TypeCount elements, and are used to convert between these two orderings.// - MemoryOrderIndex is the order an archetype's component data is actually stored in memory. This is stable across runs.// - IndexInArchetype is the order that types appear in the archetype->Types[] array. This is *not* necessarily stable across runs.//   (also called IndexInTypeArray in some APIs)public int* TypeMemoryOrderIndexToIndexInArchetype; // The Nth element is the IndexInArchetype of the type with MemoryOrderIndex=Npublic int* TypeIndexInArchetypeToMemoryOrderIndex; // The Nth element is the MemoryOrderIndex of the type with IndexInArchetype=N// These arrays each have TypeCount elements, ordered by IndexInArchetype (the same order as the Types array)public int*    Offsets; // Byte offset of each component type's data within this archetype's chunk buffer.public ushort* SizeOfs; // Size in bytes of each component typepublic int*    BufferCapacities; // For IBufferElementData components, the buffer capacity of each component. Not meaningful for non-buffer components.// Order of components in the types array is always:// Entity, native component data, buffer components, managed component data, tag component, shared components, chunk componentspublic short FirstBufferComponent;public short FirstManagedComponent;public short FirstTagComponent;public short FirstSharedComponent;public short FirstChunkComponent;public ArchetypeFlags Flags;public Archetype* CopyArchetype; // Removes cleanup componentspublic Archetype* InstantiateArchetype; // Removes cleanup components & prefabspublic Archetype* CleanupResidueArchetype;public Archetype* MetaChunkArchetype;public EntityRemapUtility.EntityPatchInfo* ScalarEntityPatches;public EntityRemapUtility.BufferEntityPatchInfo* BufferEntityPatches;// @macton Temporarily store back reference to EntityComponentStore.// - In order to remove this we need to sever the connection to ManagedChangesTracker//   when structural changes occur.public EntityComponentStore* EntityComponentStore;public fixed byte QueryMaskArray[128];
}

其中,TypesCount表示archetype所保存的数据种类,一般就是entity+component类型数量,这里空entity自带一个Simulate的component,那么TypesCount即为2;Types就是对应的类型数组了;EntityCount为当前archetype所拥有的entity数量;ChunksWithEmptySlots表示archetype中所有空闲chunk的索引列表,archetype就是通过它来为entity指定chunk的;然后,一系列First打头的Component(FirstBufferComponentFirstManagedComponentFirstTagComponentFirstSharedComponentFirstChunkComponent)表示在type数组中第一个出现该类型的索引;SizeOfs是一个比较重要的字段,它表示每个component type所占的内存大小,通过它可以进而计算出archetype最多可拥有entity的数量,也就是ChunkCapacity字段;在此基础上,还可以计算出每个component type在chunk中的起始位置Offsets;最后,Chunks是一个ArchetypeChunkData类型的字段,它用来记录archetype中所有chunk的状态。

有了archetype之后,就可以进入创建entity的阶段了,那么下一步,Unity会先查找是否有可用的chunk,没有的话就得真正分配内存,去创建一个:

ChunkIndex GetChunkWithEmptySlots(ref ArchetypeChunkFilter archetypeChunkFilter)
{var archetype = archetypeChunkFilter.Archetype;fixed(int* sharedComponentValues = archetypeChunkFilter.SharedComponentValues){var chunk = archetype->GetExistingChunkWithEmptySlots(sharedComponentValues);if (chunk == ChunkIndex.Null)chunk = GetCleanChunk(archetype, sharedComponentValues);return chunk;}
}

注意到这里函数返回的是ChunkIndex类型,它不是真正意义上的chunk,而是对chunk的二次封装,用来方便快捷地存取chunk的数据,以及记录chunk的各种状态与属性。ChunkIndex只有一个int类型的value变量,表示当前chunk在所有chunk中的位置。虽然Unity声称每个chunk只有16KB大小,但是实际分配内存时,并不是按16KB进行分配,而是会一口气先分配好1 << 20字节也就是64 * 16KB大小的内存,这块内存被称为mega chunk。所有mega chunk的内存指针都保存在大小为16384个ulong类型的连续内存中,因此ChunkIndex记录的是全局索引,第一步先索引到某个mega chunk,然后再定位到真正的chunk地址。

在这里插入图片描述

internal Chunk* GetChunkPointer(int chunkIndex)
{var megachunkIndex = chunkIndex >> log2ChunksPerMegachunk;var chunkInMegachunk = chunkIndex & (chunksPerMegachunk-1);var megachunk = (byte*)m_megachunk.ElementAt(megachunkIndex);var chunk = megachunk + (chunkInMegachunk << Log2ChunkSizeInBytesRoundedUpToPow2);return (Chunk*)chunk;
}

Chunk是一个相对简单的数据结构,前64字节为chunk头部,后面才是数据内容。

[StructLayout(LayoutKind.Explicit)]
internal unsafe struct Chunk
{// Chunk header START// The following field is only used during serialization and won't contain valid data at runtime.// This is part of a larger refactor, and this field will eventually be removed from this struct.[FieldOffset(0)]public int ArchetypeIndexForSerialization;// 4-byte padding to keep the file format compatible until further changes to the header.[FieldOffset(8)]public Entity metaChunkEntity;// The following field is only used during serialization and won't contain valid data at runtime.// This is part of a larger refactor, and this field will eventually be removed from this struct.[FieldOffset(16)]public int CountForSerialization;[FieldOffset(28)]public int ListWithEmptySlotsIndex;// Special chunk behaviors[FieldOffset(32)]public uint Flags;// SequenceNumber is a unique number for each chunk, across all worlds. (Chunk* is not guranteed unique, in particular because chunk allocations are pooled)[FieldOffset(kSerializedHeaderSize)]public ulong SequenceNumber;// NOTE: SequenceNumber is not part of the serialized header.//       It is cleared on write to disk, it is a global in memory sequence ID used for comparing chunks.public const int kSerializedHeaderSize = 40;// Chunk header END// Component data buffer// This is where the actual chunk data starts.// It's declared like this so we can skip the header part of the chunk and just get to the data.public const int kBufferOffset = 64; // (must be cache line aligned)[FieldOffset(kBufferOffset)]public fixed byte Buffer[4];public const int kChunkSize = 16 * 1024;public const int kBufferSize = kChunkSize - kBufferOffset;public const int kMaximumEntitiesPerChunk = kBufferSize / 8;public const int kChunkBufferSize = kChunkSize - kBufferOffset;
}

随后,Unity会在该chunk中为要创建的entity分配索引,索引为当前chunk的entity数量,因为之前提到chunk的数据都是连续排布的,中间并不存在空隙。

static int AllocateIntoChunk(Archetype* archetype, ChunkIndex chunk, int count, out int outIndex)
{outIndex = chunk.Count;var allocatedCount = Math.Min(archetype->ChunkCapacity - outIndex, count);SetChunkCount(archetype, chunk, outIndex + allocatedCount);archetype->EntityCount += allocatedCount;return allocatedCount;
}

根据索引,就可以将chunk中属于该entity的部分(entity id,component type)进行初始化,clear内存等操作,自此,一个entity就算创建完成了。

for (var t = 1; t != typesCount; t++)
{var offset = offsets[t];var sizeOf = sizeOfs[t];var dst = dstBuffer + (offset + sizeOf * dstIndex);if (types[t].IsBuffer){for (var i = 0; i < count; ++i){BufferHeader.Initialize((BufferHeader*)dst, bufferCapacities[t]);dst += sizeOf;}}else{UnsafeUtility.MemClear(dst, sizeOf * count);}
}

接下来,再为entity添加component,那么原来entity所在的chunk和archetype不再满足要求,需要将entity移动到新的chunk和archetype中,这种行为在Unity中被称作structural change。在AddComponent之前,Unity首先会检查entity是否已有该component,同一类型的component只允许存在一个。这个时候archetype就发挥作用了,只需要去archetype中查找component type是否存在即可:

public bool HasComponent(Entity entity, ComponentType type)
{if (Hint.Unlikely(!Exists(entity)))return false;var archetype = GetArchetype(entity);return ChunkDataUtility.GetIndexInTypeArray(archetype, type.TypeIndex) != -1;
}

如果entity没有该component,那么就需要找一下符合条件的archetype以及空闲的chunk。

ChunkIndex GetChunkWithEmptySlotsWithAddedComponent(ChunkIndex srcChunk, ComponentType componentType, int sharedComponentIndex = 0)
{var archetypeChunkFilter = GetArchetypeChunkFilterWithAddedComponent(srcChunk, componentType, sharedComponentIndex);if (archetypeChunkFilter.Archetype == null)return ChunkIndex.Null;return GetChunkWithEmptySlots(ref archetypeChunkFilter);
}

新的archetype和chunk都有了,剩下的就是move操作了,先把数据从源chunk拷贝到目标chunk中去,更新目标archetype,然后再清理源chunk,更新源archetype。

int Move(in EntityBatchInChunk srcBatch, ChunkIndex dstChunk)
{var srcChunk = srcBatch.Chunk;var srcArchetype = GetArchetype(srcChunk);var dstArchetype = GetArchetype(dstChunk);var dstUnusedCount = dstArchetype->ChunkCapacity - dstChunk.Count;var srcCount = math.min(dstUnusedCount, srcBatch.Count);var srcStartIndex = srcBatch.StartIndex + srcBatch.Count - srcCount;var partialSrcBatch = new EntityBatchInChunk{Chunk = srcChunk,StartIndex = srcStartIndex,Count = srcCount};ChunkDataUtility.Clone(srcArchetype, partialSrcBatch, dstArchetype, dstChunk);fixed (EntityComponentStore* store = &this){ChunkDataUtility.Remove(store, partialSrcBatch);}return srcCount;
}

前面提到过,当从chunk中移除entity时,chunk的最后一个entity将被移动以填补空隙。其实就是个简单的拷贝内存操作:

var fillStartIndex = chunk.Count - fillCount;Copy(entityComponentStore, chunk, fillStartIndex, chunk, startIndex, fillCount);

Reference

[1] Archetypes concepts

[2] 深入理解ECS:Archetype(原型)

[3] Archetypes window reference

[4] Structural changes concepts

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/453573.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React是如何工作的?

从编写组件到最后屏幕生成界面&#xff0c;如上图所示&#xff0c;我们现在需要知道的就是后面几步是如何运行的。 概述 这张图解释了 React 渲染过程的几个阶段&#xff1a; 渲染触发&#xff1a;通过更新某处的状态来触发渲染。渲染阶段&#xff1a;React 调用组件函数&…

智能优化算法-生物地理学算法(BBO)(附源码)

目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1.内容介绍 生物地理学优化算法 (Biogeography-Based Optimization, BBO) 是一种基于生物地理学原理的元启发式优化算法&#xff0c;由Dan Simon于2008年提出。BBO通过模拟物种在不同栖息地之间的迁移过程来搜索最优解&…

Dongle Sentinal在Jenkins下访问不了的问题

背景&#xff1a; 工作站部署的jenkins的脚本无法正常打包&#xff0c;定位后发现是本地获取不了license&#xff0c;但是使用usb over network的远程license都能获取并正常打包 分析&#xff1a; 获取不了license的原因是本地无法识别dongle。根据提供信息&#xff0c;之前…

卡特兰数解释相关的样例以及补充例题

目录 拓展的场景分析 1.圆上连接线段 2.二叉树问题 3.多边形划分三角形问题 补充的例题 P1976 鸡蛋饼 P1722 矩阵 II 通过取模处理判断选择用哪个式子​编辑 P2532 [AHOI2012] 树屋阶梯 P3978 [TJOI2015] 概率论 拓展的场景分析 1.圆上连接线段 一个圆上有2*n个点&am…

nginx中的HTTP 负载均衡

HTTP 负载均衡&#xff1a;如何实现多台服务器的高效分发 为了让流量均匀分配到两台或多台 HTTP 服务器上&#xff0c;我们可以通过 NGINX 的 upstream 代码块实现负载均衡。 方法 在 NGINX 的 HTTP 模块内使用 upstream 代码块对 HTTP 服务器实施负载均衡&#xff1a; upstr…

基于微博评论的自然语言处理情感分析

目录 一、项目概述 二、需要解决的问题 三、数据预处理 1、词汇表构建&#xff08;vocab_creat.py&#xff09; 2、数据集加载&#xff08;load_dataset.py&#xff09; 四、模型构建&#xff08;TextRNN.py&#xff09; 1、嵌入层&#xff08;Embedding Layer&#xff…

Unity通过高德开放平台获取天气信息

一、注册高德开放平台账号&#xff0c;获取天气接口Key 1、构建自己的应用 网址&#xff1a;https://lbs.amap.com/api/webservice/guide/api/weatherinfo 最终调用api的地址形式&#xff1a; https://restapi.amap.com/v3/weather/weatherInfo?city110101&key<用户…

ionic Capacitor 生成 Android 应用

官方文档 https://ionic.nodejs.cn/developing/android/ https://capacitorjs.com/docs/getting-started 1、创建新的 Capacitor 应用程序 空目录下面 npm init capacitor/app2、install Capacitor npm install npm start在这里插入图片描述 3、生成dist目录 npm run buil…

华为eNSP:MAC地址漂移防止与检测

一、什么是MAC地址漂移&#xff1f; MAC地址漂移是指在计算机网络中&#xff0c;MAC&#xff08;Media Access Control&#xff09;地址被动态更改的现象。每个网络接口设备都有一个唯一的MAC地址&#xff0c;用来标识该设备在网络中的身份。然而&#xff0c;有些恶意软件或网…

15.JVM垃圾收集算法

一、垃圾收集算法 1.分代收集理论 分代收集理论是JAVA虚拟机进行垃圾回收的一种思想&#xff0c;根据对象存活周期的不同将内存分成不同的几个区域&#xff1b;一般将JAVA堆内存分为新生代和老年代&#xff1b;根据每个分代特点选择不同的垃圾收集器&#xff1b; 在新生代中&am…

深入理解 TypeScript 中的 as 关键字

在TypeScript中&#xff0c;as 关键字是一种类型断言&#xff08;Type Assertion&#xff09;的语法&#xff0c;用于告诉编译器如何理解某个变量的类型。这在开发过程中非常有用&#xff0c;尤其是当你知道比编译器更多的类型信息时 基本用法 类型断言允许你在编译时更改变量的…

InnoDB引擎(架构,事务原理,MVCC详细解读)

目录 架构分析 逻辑存储结构​ 内存结构​ Buffer Pool​ ChaneBuffer 自适应哈希​ LogBuffer​ 磁盘结构​ 后台线程​ 事务原理​ redolog日志 undolog日志​ MVCC​ 三个隐藏字段​ undolog版本链 readview​ RC(读已提交原理分析)​ RR(可重复读原理分析…

动态规划之斐波那契数列

文章目录 第 N 个泰波那契数三步问题使用最小花费爬楼梯解码方法 动态规划的基本思想是利用之前已经计算过的结果&#xff0c;通过递推关系式来计算当前问题的解。 整体思路 状态表示状态转移方程初始化填表顺序返回值 第 N 个泰波那契数 题目&#xff1a; 第 N 个泰波那契数 思…

云网络验证系统云验证+卡密生成+多应用多用户管理

云网络验证系统云验证&#xff0c;多样化应用管理方式&#xff0c;多种项目任你开发&#xff0c;分布式应用开关&#xff0c;让您的应用开发更简单&#xff0c;本系统借鉴于易如意API写法及思路&#xff0c;完美实现多用户多应用管理。 源码特色 1&#xff0c;对接&#xff1…

013_django基于大数据的高血压人群分析系统2024_dcb7986h_055

目录 系统展示 开发背景 代码实现 项目案例 获取源码 博主介绍&#xff1a;CodeMentor毕业设计领航者、全网关注者30W群落&#xff0c;InfoQ特邀专栏作家、技术博客领航者、InfoQ新星培育计划导师、Web开发领域杰出贡献者&#xff0c;博客领航之星、开发者头条/腾讯云/AW…

MarkDownload 剪裁网页插件配置使用全流程

前言 写在前面&#xff0c;大家有什么问题和需要可以跟我交流 需求 之前一直使用 Joplin 的剪裁网页功能&#xff0c;但是剪裁下来后不可避免的需要使用 Joplin 对剪裁下来的内容做处理&#xff0c;Joplin 用起来不是很习惯&#xff0c;所以在想可不可以用 Obsidian 来实现网…

图的最小生成树算法--普里姆(Prim)算法和克鲁斯克尔(Kruskal)算法

一、图的最小生成树 最小生成树&#xff08;Minimum spanning tree&#xff0c;MST&#xff09;是最小权重生成树&#xff08;Minimum weight spanning tree&#xff09;的简称&#xff0c;是一个连通加权无向图中一棵权值最小的生成树。 在一给定的无向图 G ( V , E ) G …

探索 Jupyter 笔记本转换的无限可能:nbconvert 库的神秘面纱

文章目录 探索 Jupyter 笔记本转换的无限可能&#xff1a;nbconvert 库的神秘面纱背景&#xff1a;为何选择 nbconvert&#xff1f;库简介&#xff1a;nbconvert 是什么&#xff1f;安装指南&#xff1a;如何安装 nbconvert&#xff1f;函数用法&#xff1a;简单函数示例应用场…

MySQL企业常见架构与调优经验分享

文章目录 一、选择 PerconaServer、MariaDB 还是 MYSQL二、常用的 MYSQL 调优策略三、MYSOL 常见的应用架构分享四、MYSOL 经典应用架构 观看学习课程的笔记&#xff0c;分享于此~ 课程&#xff1a;MySQL企业常见架构与调优经验分享 mysql官方优化文档 一、选择 PerconaServer、…

全方面熟悉Maven项目管理工具(二)坐标、pom.xml文件的解读!

1. 坐标&#xff08;核心概念&#xff09; 1.1 数学中的坐标 使用 x、y、z 三个向量作为空间的坐标系&#xff0c;可以在空间中唯一的定位到一个点 1.2 Maven 中的坐标 1.2.1 向量说明&#xff1a; 使用三个向量在 Maven的仓库 中唯一的定位到一个 jar 包 groupId&#xf…