Unity DOTS中的Entity

news2024/11/25 3:38:38

Unity DOTS中的Entity

在DOTS中entity往往只被看作一个ID,用来查找component,但实际上Unity为了有效地管理entity,在背后还做了一些其他的工作。首先是Entity类本身的定义,它的确跟一个ID差不多,只包含了两个int类型的成员:

public struct Entity : IEquatable<Entity>, IComparable<Entity>
{
    /// <summary>
    /// The ID of an entity.
    /// </summary>
    /// <value>The index into the internal list of entities.</value>
    /// <remarks>
    /// Entity indexes are recycled when an entity is destroyed. When an entity is destroyed, the
    /// EntityManager increments the version identifier. To represent the same entity, both the Index and the
    /// Version fields of the Entity object must match. If the Index is the same, but the Version is different,
    /// then the entity has been recycled.
    /// </remarks>
    public int Index;
    /// <summary>
    /// The generational version of the entity.
    /// </summary>
    /// <remarks>The Version number can, theoretically, overflow and wrap around within the lifetime of an
    /// application. For this reason, you cannot assume that an Entity instance with a larger Version is a more
    /// recent incarnation of the entity than one with a smaller Version (and the same Index).</remarks>
    /// <value>Used to determine whether this Entity object still identifies an existing entity.</value>
    public int Version;
}

这里的Index,并非是表示entity在chunk中的存储位置,它是一个二级索引,指向管理entity的block。block才真正记录了entity位于哪个chunk,以及在chunk中的具体位置。之所以这样设计,是因为在structural change时(例如AddComponent,RemoveComponent)entity会从一个chunk移动到另一个chunk。使用二级索引,就可以保证在发生移动时,entity的index保持不变,变化的只是block中的内容。

在这里插入图片描述

另外,从代码中的注释可知,entity的Index是会循环利用的,当一个entity销毁时,另一个创建的entity可能会复用到之前entity的Index,所以entity还包含一个Version字段。只有当Index和Version同时相等时,两个entity才会被视为相等。Version字段的默认值为0,每当entity创建时都会+1,销毁时也会+1,这意味着只有当Version为奇数时,entity才是存在的。

管理entity的block是全局唯一的,它是一个长度为16K的64位指针数组,其中每个指针指向一个更小的data block,data block最多可记录8K数量的entity状态。所以Unity全局最多可以支持16K * 8K数量的entity。

struct DataBlock
{
    // 8k entities per DataBlock
    public fixed ulong allocated[k_EntitiesInBlock / 64];
    public fixed ulong entityInChunk[k_EntitiesInBlock];
    public fixed int versions[k_EntitiesInBlock];
#if !DOTS_DISABLE_DEBUG_NAMES
    public fixed int nameByEntityIndex[k_EntitiesInBlock];
#endif
}

const int k_EntitiesInBlock = 8192;

这里可以看到记录data block当前分配entity使用的是8192 / 64大小的unsigned long数组,这是由于unsigned long本身是64位的,每一个二进制位都可以表示是否被分配,所以数组的大小可以省下来。

来看下创建entity的具体逻辑,看上去比较复杂:

internal void AllocateEntities(Entity* entities, int totalCount, ChunkIndex chunkIndex, int firstEntityInChunkIndex)
{
    var entityInChunkIndex = firstEntityInChunkIndex;

    for (int i = 0; i < k_BlockCount; i++)
    {
        var blockCount = Interlocked.Add(ref m_EntityCount[i], 0);

        if (blockCount == k_BlockBusy || blockCount == k_EntitiesInBlock)
        {
            continue;
        }

        var blockAvailable = k_EntitiesInBlock - blockCount;
        var count = math.min(blockAvailable, totalCount);

        // Set the count to a flag indicating that this block is busy (-1)
        var before = Interlocked.CompareExchange(ref m_EntityCount[i], k_BlockBusy, blockCount);

        if (before != blockCount)
        {
            // Another thread is messing around with this block, it's either busy or was changed
            // between the time we read the count and now. In both cases, let's keep looking.
            continue;
        }

        DataBlock* block = (DataBlock*)m_DataBlocks[i];

        // Be careful that the block might exist even if the count is zero, checking the pointer
        // for null is the only valid way to tell if the block exists or not.
        if (block == null)
        {
            block = (DataBlock*)Memory.Unmanaged.Allocate(k_BlockSize, 8, Allocator.Persistent);
            UnsafeUtility.MemClear(block, k_BlockSize);
            m_DataBlocks[i] = (ulong)block;
        }

        int remainingCount = math.min(blockAvailable, count);
        var allocated = block->allocated;
        var versions = block->versions;
        var entityInChunk = block->entityInChunk;
        var baseEntityIndex = i * k_EntitiesInBlock;

        while (remainingCount > 0)
        {
            for (int maskIndex = 0; maskIndex < k_EntitiesInBlock / 64; maskIndex++)
            {
                if (allocated[maskIndex] != ~0UL)
                {
                    // There is some space in this one

                    for (int entity = 0; entity < 64; entity++)
                    {
                        var indexInBlock = maskIndex * 64 + entity;
                        var mask = 1UL << (indexInBlock % 64);

                        if ((allocated[maskIndex] & mask) == 0)
                        {
                            allocated[maskIndex] |= mask;

                            *entities = new Entity
                            {
                                Index = baseEntityIndex + indexInBlock,
                                Version = versions[indexInBlock] += 1
                            };

                            if (chunkIndex != ChunkIndex.Null)
                            {
                                ((EntityInChunk*)entityInChunk)[indexInBlock] = new EntityInChunk
                                {
                                    Chunk = chunkIndex,
                                    IndexInChunk = entityInChunkIndex,
                                };
                            }
                            else
                            {
                                entityInChunk[indexInBlock] = 0;
                            }

                            entities++;
                            entityInChunkIndex++;
                            remainingCount--;

                            if (remainingCount == 0)
                            {
                                break;
                            }
                        }
                    }

                    if (remainingCount == 0)
                    {
                        break;
                    }
                }
            }
        }

        Assert.AreEqual(0, remainingCount);

        var resultCheck = Interlocked.CompareExchange(ref m_EntityCount[i], blockCount + count, k_BlockBusy);

        Assert.AreEqual(resultCheck, k_BlockBusy);

        totalCount -= count;

        if (totalCount == 0)
        {
            return;
        }
    }

    throw new InvalidOperationException("Could not find a data block for entity allocation.");
}

虽然有一定代码量,但是要做的事情其实是很直观的。具体来说可以分为以下几个步骤:

  • 找到第一个可用的data block,如果某个block分配已满(entity数量为8K),或者被其他线程所占用(entity数量被置为busy),则说明不可用;
  • 对可用的data block尝试加锁(即将其entity数量置为busy),如果加锁失败,说明被其他线程抢先一步占用,直接放弃尝试下一个data block;
  • 加锁成功,查看data block当前的分配状态,如果分配状态为空,创建一个新的内存块并指向它;
  • 在分配状态中寻找空闲的二进制位,根据它的位置计算出entity的Index,也就是指向block的二级索引,同时entity的Version自增,表示新建;
  • 把entity在chunk中的实际位置信息(位于哪个chunk,以及在chunk中的位置)记录到block中;
  • 释放之前加的锁,恢复data block的entity数量为正确的值。

当有了一个entity之后,又如何判断它是否还真实存在呢?这时候Version就派上用场了,data block里存着的永远是最新的Version,直接对比一下就好:

internal bool Exists(Entity entity)
{
    var blockIndex = entity.Index / k_EntitiesInBlock;
    var indexInBlock = entity.Index % k_EntitiesInBlock;

    var block = (DataBlock*)m_DataBlocks[blockIndex];
    if (block == null) return false;

    if (((uint)entity.Version & 1) == 0 || block->versions[indexInBlock] != entity.Version) return false;
    return true;
}

销毁entity的逻辑也是类似的,只不过销毁只处理data block分配状态的二进制位,将其置回为0,Version字段继续自增,表示已销毁。另外,就算block的分配状态为全空,也不会将block的内存回收掉。

for (int j = startIndex, indexInEntitiesArray = rangeStart; j < endIndex; j++, indexInEntitiesArray++)
{
    var indexInBlock = j % k_EntitiesInBlock;

    if (versions[indexInBlock] == entities[indexInEntitiesArray].Version)
    {
        // Matching versions confirm that we are deallocating the intended entity

        var mask = 1UL << (indexInBlock % 64);

        versions[indexInBlock]++;
        allocated[indexInBlock / 64] &= ~0UL ^ mask;

#if !DOTS_DISABLE_DEBUG_NAMES
        block->nameByEntityIndex[indexInBlock] = default;
#endif

        blockCount--;
    }
}

// Do not deallocate the block even if it's empty. Versions should be preserved.

{
    var resultCheck = Interlocked.CompareExchange(ref m_EntityCount[blockIndex], blockCount, k_BlockBusy);
    Assert.AreEqual(resultCheck, k_BlockBusy);
}

最后我们来看一下chunk的创建与销毁。当出现无处安放的entity时,就需要创建新的chunk。在前一篇文章中我们提到过,chunk不是单独分配的,而是按照64 * 16K大小进行分配的,这块内存成为mega chunk。Unity最多可以创建16384个mega chunk。 那么,Unity最多需要管理16384*64个chunk,为了提高效率,Unity使用了两个查找表,方便快速定位到空闲的chunk。

Ulong16384 m_chunkInUse;
Ulong256   m_megachunkIsFull;

m_megachunkIsFull把所有mega chunk分为256组,每组64个,那么每个mega chunk刚好可以用一个unsigned long表示;同样地,m_chunkInUse可表示16384个mega chunk,而每一个unsigned long,刚好可以表示每一个chunk的状态。

public int AllocateContiguousChunks(out ChunkIndex value, int requestedCount, out int actualCount)
{
    int gigachunkIndex = 0;
    for(; gigachunkIndex < m_megachunkIsFull.Length; ++gigachunkIndex)
        if(m_megachunkIsFull.ElementAt(gigachunkIndex) != ~0L)
            break;
    int firstMegachunk = gigachunkIndex << 6;
    actualCount = math.min(chunksPerMegachunk, requestedCount); // literally can't service requests for more
    value = ChunkIndex.Null;
    while(actualCount > 0)
    {
        for(int offset = 0; offset < megachunksInUniverse; ++offset)
        {
            int megachunkIndex = (firstMegachunk + offset) & (megachunksInUniverse-1); // index of current megachunk
            long maskAfterAllocation, oldMask, newMask, readMask = m_chunkInUse.ElementAt(megachunkIndex); // read the mask of which chunks are allocated
            int chunkInMegachunk; // index of first chunk allocated in current megachunk
            do {
                oldMask = readMask;
                if(oldMask == ~0L)
                    goto NEXT_MEGACHUNK; // can't find any bits, try the next megachunk
                if(!ConcurrentMask.foundAtLeastThisManyConsecutiveZeroes(oldMask, actualCount, out chunkInMegachunk, out int _)) // find consecutive 0 bits to allocate into
                    goto NEXT_MEGACHUNK; // can't find enough bits, try the next megachunk
                newMask = maskAfterAllocation = oldMask | ConcurrentMask.MakeMask(chunkInMegachunk, actualCount); // mask in the freshly allocated bits
                if(oldMask == 0L) // if we're the first to allocate from this megachunk,
                    newMask = ~0L; // mark the whole megachunk as full (busy) until we're done allocating memory
                readMask = Interlocked.CompareExchange(ref m_chunkInUse.ElementAt(megachunkIndex), newMask, oldMask);
            } while(readMask != oldMask);
            int firstChunkIndex = (megachunkIndex << log2ChunksPerMegachunk) + chunkInMegachunk;
            if(oldMask == 0L) // if we are the first allocation in this chunk...
            {
                long allocated = (long)Memory.Unmanaged.Allocate(MegachunkSizeInBytes, CollectionHelper.CacheLineSize, Allocator.Persistent); // allocate memory
                if (allocated == 0L) // if the allocation failed...
                    return AllocationFailed(firstChunkIndex, actualCount);
                Interlocked.Exchange(ref m_megachunk.ElementAt(megachunkIndex), allocated); // store the pointer to the freshly allocated memory
                Interlocked.Exchange(ref m_chunkInUse.ElementAt(megachunkIndex), maskAfterAllocation); // change the mask from ~0L to the true mask after our allocation (which may be ~0L)
            }
            if(maskAfterAllocation == ~0L)
                ConcurrentMask.AtomicOr(ref m_megachunkIsFull.ElementAt(megachunkIndex>>6), 1L << (megachunkIndex & 63));
            value = new ChunkIndex(firstChunkIndex);
            return kErrorNone;
            NEXT_MEGACHUNK:;
        }
        actualCount >>= 1;
    }
    return kErrorNoChunksAvailable;
}

这里分配chunk的代码也是比较直观的,从流程上可以分为以下步骤:

  • 首先通过m_megachunkIsFull,找到空闲的mega chunk组gigachunkIndex
  • 通过gigachunkIndex,得到第一个空闲的mega chunk的index,再查找m_chunkInUse,得到此时该mega chunk中64个chunk的空闲情况;
  • 如果chunk都已分配完毕,或者此时正被其他线程所占用,那么放弃该mega chunk,尝试下一个可用的mega chunk;
  • 如果找到空闲的chunk,那么尝试加锁,如果加锁失败则一直等待,除非发生上一步的情况,才会放弃该mega chunk;
  • 加锁成功,如果整个mega chunk都是空闲的,说明这个mega chunk压根还没实际分配内存,需要分配完内存后保存地址指针;
  • 如果mega chunk不再有空闲的chunk,则需要记录回m_megachunkIsFull中;
  • 返回可用的chunk。

销毁chunk的逻辑也是类似的,当chunk中entity数量为0时就会触发。由于chunk是以mega chunk为单位进行分配的,所以只有当整个mega chunk都是空闲时,才会真正地释放整个mega chunk的内存。

Reference

[1] Entity concepts

[2] Unity DOTS中的Archetype与Chunk

[3] Interlocked.CompareExchange Method

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot实现单文件上传

一、在springBoot项目中的pom.xml添加依赖。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency> 二、在资源目录下的static目录下中创建一个upload.html的表单文件…

CNN—LeNet:从0开始神经网络学习,实战MNIST和CIFAR10~

文章目录 前言一、CNN与LeNet介绍二、LeNet组成及其名词解释2.1 输入2.2 卷积层2.3池化层2.4 全连接层2.5 总结 三、MNIST实战3.1 构建神经网络3.2 数据处理3.3 &#xff08;模板&#xff09;设置优化器&#xff0c;损失函数&#xff0c;使用gpu(如果是N卡有cuda核心)&#xff…

PVE系统中风扇驱动安装——linux 硬件驱动安装(IT8613E为例)

本文提供全流程命令代码,IT8613E的Github下载地址,pve头文件官方下载地址 对网卡驱动感兴趣的可以看这篇文章 linux系统下 usb网卡的驱动安装_0bda:a192-CSDN博客文章浏览阅读1.5w次,点赞16次,收藏72次。本文介绍如何通过lsusb查找USB网卡vid:pid,使用google搜索驱动信息…

美国人工智能国家安全备忘录核心解读(下)

文章目录 三、美国国内和国际人工智能治理策略1.保证AI政策有效执行的协调措施2.推进AI治理格局的优势地位&#xff08;1&#xff09;对于美国盟友&#xff1a;试图向盟友保证其将从美国的战略中获益。&#xff08;2&#xff09;对于美国的战略竞争对手&#xff1a;介绍了超越竞…

工具学习_Docker

0. Docker 简介 Docker 是一个开源平台&#xff0c;旨在帮助开发者构建、运行和交付应用程序。它通过容器化技术将应用程序及其所有依赖项打包在一个标准化的单元&#xff08;即容器&#xff09;中&#xff0c;使得应用程序在任何环境中都能保持一致的运行效果。Docker 提供了…

红黑树模拟实现STL中的map与set

1.map 在C标准模板库(STL)中&#xff0c;std::map是一种非常实用且强大的容器&#xff0c;它提供了键值对的存储机制。这使得std::map成为处理具有唯一关键的关联数据的理想选择。 1.1 map的特性 1、键值对存储&#xff1a;std::map通过键值对的形式存储数据&#xff0c;其中…

【数据结构专栏】二叉搜索树(Binary Search Tree)的剖析?

文章目录 &#x1f9e8;前言1、二叉搜索树的基本概念&#xff1f;2、二叉搜索树的节点结构组成&#xff1f;3、二叉搜索树的插入操作&#xff1f;4、二叉搜索树的删除操作&#xff1f;5、二叉搜索树的遍历&#xff1f; 6、二叉搜索树的性能分析&#xff1f; &#x1f389;完整代…

FastApi学习第三天:两表联查

两表联查 在 FastAPI 中&#xff0c;使用 Tortoise ORM 查询两表联查&#xff08;通常是通过外键关系进行联接&#xff09;是非常简单的。可以使用 select_related 或 prefetch_related 来执行联表查询&#xff0c;它们类似于 Django ORM 的 select_related 和 prefetch_relate…

Redis原理及应用

Redis简介 Redis是开源的&#xff08;BSD许可&#xff09;&#xff0c;数据结构存储于内存中&#xff0c;被用来作为数据库&#xff0c;缓存和消息代理。它支持多种数据结构&#xff0c;例如&#xff1a;字符串&#xff08;string&#xff09;&#xff0c;哈希&#xff08;hash…

Unity类银河战士恶魔城学习总结(P141 Finalising ToolTip优化UI显示)

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址&#xff1a;https://www.udemy.com/course/2d-rpg-alexdev/ UI部分暂时完结&#xff01;&#xff01;&#xff01; 本章节优化了UI中物品描述的显示效果&#xff0c;技能描述的显示效果 并且可以批…

oracle的静态注册和动态注册

oracle的静态注册和动态注册 静态注册&#xff1a; 静态注册 : 指将实例的相关信息手动告知 listener 侦 听 器 &#xff0c; 可以使用netmgr,netca,oem 以及直接 vi listener.ora 文件来实现静态注册&#xff0c;在动态注册不稳定时使用&#xff0c;特点是&#xff1a;稳定&…

社交电商专业赋能高校教育与产业协同发展:定制开发AI智能名片及2+1链动商城小程序的创新驱动

摘要&#xff1a;本文围绕社交电商有望成为高校常态专业这一趋势展开深入探讨&#xff0c;剖析国家政策认可下其学科发展前景&#xff0c;着重阐述在专业建设进程中面临的师资短缺及实践教学难题。通过引入定制开发AI智能名片与21链动商城小程序&#xff0c;探究如何借助这些新…

数据指标与标签在数据分析中的关系与应用

导读&#xff1a;分享数据指标体系的文章很多&#xff0c;但讲数据标签的文章很少。实际上&#xff0c;标签和指标一样&#xff0c;是数据分析的左膀右臂&#xff0c;两者同样重要。实际上&#xff0c;很多人分析不深入&#xff0c;就是因为缺少对标签的应用。今天系统的讲解下…

使用Electron将vue2项目打包为桌面exe安装包

目录 一、下载electron模板项目 【electron-quick-start】​ 二、打开项目&#xff0c;安装所有依赖 三、在打exe包的时候报错是因为没有&#xff0c;需要检查并安装之后重新打包&#xff1b; 四、经过这么疯狂的一波操作之后&#xff0c;就可以打包出你想要的exe安装包&am…

MySQL基础大全(看这一篇足够!!!)

文章目录 前言一、初识MySQL1.1 数据库基础1.2 数据库技术构成1.2.1 数据库系统1.2.2 SQL语言1.2.3 数据库访问接口 1.3 什么是MySQL 二、数据库的基本操作2.1 数据库创建和删除2.2 数据库存储引擎2.2.1 MySQL存储引擎简介2.2.2 InnoDB存储引擎2.2.3 MyISAM存储引擎2.2.4 存储引…

Linux之NFS共享文件操作

一、注意点 以下操作使用root用户 代理端需要访问服务端的2049、111端口二、nfs下载 # 服务端和代理端都要安装 yum –y install rpcbind yum –y install nfs-utils三、配置共享目录-【服务端】 *修改/etc/exports文件&#xff0c;追加以下内容 /home/app_adm/test ip1(in…

C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用-腾讯云开发者社区-腾讯云

C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用-腾讯云开发者社区-腾讯云 C#学习笔记——窗口停靠控件WeifenLuo.WinFormsUI.Docking使用 发布于 2021-06-10 00:10:59 7.1K0 举报 文章被收录于专栏&#xff1a;c#学习笔记 一、介绍 DockPanelSuite是托管在…

杰发科技AC7840——EEP中RAM的配置

sample和手册中示例代码的sram区地址定义不一样 这个在RAM中使用没有限制&#xff0c;根据这个表格留下足够空间即可 比如需要4096字节的eep空间&#xff0c;可以把RAM的地址改成E000&#xff0c;即E000-EFFF&#xff0c;共4096bytes即可。

web-03

CSS回顾 选择器 标签选择器 标签{}ID选择器 标签中定义ID属性。 #ID值{}类选择器 标签中使用class属性 .类名{}关于DIV/span div任意的大小的长方形&#xff0c;大小css&#xff1a; width, height控制。—换行 span-- 一行内 CSS常用属性 width/height 宽度/高度 定义&…

CI配置项,IT服务的关键要素

随着现今数字经济的不断发展&#xff0c;逐渐成熟的IT 基础设施已不再是简单的竞争优势&#xff0c;而已成为企业生存和发展的基石。然而&#xff0c;仅仅拥有强大的基础设施是不够的。为了保障 IT 服务的平稳运行和持续交付&#xff0c;企业还需要重点关注 IT 服务的核心构建模…