Unity DOTS中的baking(二)Baker的触发

Unity DOTS中的baking(二)Baker的触发

我们知道,当传入Baker的authoring component的值发生变化时,就会触发baking。不过在有些情况下,component所引用的对象没有变化,而是对象自身内部的一些属性发生了变化。这种情况下,是否会触发baking呢?我们来动手验证一下。

首先定义一个继承自ScriptableObject的ImageGeneratorInfo类:

[CreateAssetMenu(menuName = "ImageGeneratorInfo")]
public class ImageGeneratorInfo : ScriptableObject
{
    [Range(0.0f, 1.0f)]
    public float Spacing;
    public Mesh Mesh;
    public Material Material;
}

这样,我们就可以利用这个脚本创建asset了,随便给它塞点东西:

Unity DOTS中的baking(二)1

然后我们修改下baker脚本,在ECS Component中新增一个spacing,读取assets中的Spacing值:

public class MyAuthoring : MonoBehaviour
{
    public int bakeIntData = 0;
    public ImageGeneratorInfo info;

    class MyBaker : Baker<MyAuthoring>
    {
        public override void Bake(MyAuthoring authoring)
        {
            Debug.Log("==========================Bake Invoked!========================== " + authoring.name);

            if(authoring.info == null) return;

            var entity = GetEntity(TransformUsageFlags.None);
            AddComponent(entity, new MyComponent {
                value = authoring.bakeIntData,
                spacing = authoring.info.Spacing
            });
        }
    }
}

public struct MyComponent : IComponentData
{
    public int value;
    public float spacing;
}

我们在SubScene中新建一个空的GameObject,挂上MyAuthoring脚本,并在Info中设置上刚刚创建的ImageGeneratorInfo asset,随即就会触发baking:

Unity DOTS中的baking(二)2

Unity DOTS中的baking(二)3

此时,如果直接去修改asset本身,例如将asset的spacing参数设置为其他的值,并不会触发baking。这就意味着,转换后的Entity所拥有的MyComponent数据是错误的。对此,Unity官方文档给出了解释:

However, Unity doesn’t automatically track data from other sources, such as authoring components or assets. You need to add a dependency to the baker so it can track this kind of data. To do this, use the methods that the Baker class provides to access other components and GameObjects instead of the methods provided by the GameObject:

Unity只会自动地追踪依赖authoring component自身的变化,至于component所引用的资源,则需要使用DependsOn来显式定义依赖:

public override void Bake(MyAuthoring authoring)
{
    Debug.Log("==========================Bake Invoked!========================== " + authoring.name);

    DependsOn(authoring.info);

    if(authoring.info == null) return;

    var entity = GetEntity(TransformUsageFlags.None);
    AddComponent(entity, new MyComponent {
        value = authoring.bakeIntData,
        spacing = authoring.info.Spacing
    });
}

此时再修改Spacing参数,就会触发baking了:

Unity DOTS中的baking(二)4

这个DependsOn背后究竟做了什么呢?查看一下DependsOn的源码:

/// <summary>
/// This will take a dependency on Object of type T.
/// </summary>
/// <param name="dependency">The Object to take a dependency on.</param>
/// <typeparam name="T">The type of the object. Must be derived from UnityEngine.Object.</typeparam>
/// <returns>The Object of type T if a dependency was taken, null otherwise.</returns>
public T DependsOn<T>(T dependency) where T : UnityEngine.Object
{
    _State.Dependencies->DependResolveReference(_State.AuthoringSource.GetInstanceID(), dependency);

    // Transform component takes an implicit dependency on the entire parent hierarchy
    // since transform.position and friends returns a value calculated from all parents
    var transform = dependency as Transform;
    if (transform != null)
        _State.Dependencies->DependOnParentTransformHierarchy(transform);

    return dependency;
}

这里最关键的函数就是这个DependResolveReference了,再来看下它的源码:

public void DependResolveReference(int authoringComponent, UnityEngine.Object referencedObject)
{
    // Tricky unity details ahead:
    // A UnityEngine.Object might be
    //  - actual null (ReferenceEquals(referencedObject, null)  -> instance ID zero)
    //  - currently unavailable (referencedObject == null)
    //      - If it is unavailable, it might still have an instanceID. The object might for example be brought back through undo or putting the asset in the same path / guid in the project
    //        In that case it will be re-established with the same instanceID and hence we need to have a dependency on when an object
    //        that previously didn't exist now starts existing at the instanceID that previously mapped to an invalid object.
    //  - valid (referencedObject != null) (instanceID non-zero)
    var referencedInstanceID = ReferenceEquals(referencedObject, null) ? 0 : referencedObject.GetInstanceID();
    if (referencedInstanceID != 0)
    {
        AddObjectReference(referencedInstanceID);

        var obj = Resources.InstanceIDToObject(referencedInstanceID);
        var objTypeId = TypeManager.GetTypeIndex(referencedObject.GetType());
        AddObjectExist(new ObjectExistDependency { InstanceID = referencedInstanceID, exists = (obj != null), Type = objTypeId });

#if UNITY_EDITOR
        //@todo: How do we handle creation / destruction of assets / components?
        if (EditorUtility.IsPersistent(referencedObject))
            AddPersistentAsset(referencedObject.GetInstanceID());
#endif
    }
}

从源码的注释得知,Unity需要处理依赖的object是fake null(例如object引用为missing)的情况。ObjectExistDependency这个类记录了所引用的object对应的asset是否存在。当调用DependsOn时,就会新建一个该类的对象保存起来。之后,再检测到asset变化时,会触发CalculateObjectExistDiffsJob这个job。该job会获取记录的所有objects对应的asset当前存在的状态,与之前ObjectExistDependency保存时的状态进行比较,如果不一致,说明asset发生了变化(从无到有或者从有到无),需要重新进行baking:

// Resolve the objectIds (Get Object)
// Check if they are null (If they are null)
NativeArray<bool> objectExists = new NativeArray<bool>(objectIds.Length, Allocator.TempJob);

InstanceIDsToValidArrayMarker.Begin();
Resources.InstanceIDsToValidArray(objectIds.AsArray(), objectExists);
InstanceIDsToValidArrayMarker.End();

var diffJob = new CalculateObjectExistDiffsJob()
{
    objectExistDependencies = _StructuralObjectExistDependency,
    objectExists = objectExists,
    deduplicatedObjIds = deduplicatedObjIds,
    changedComponentsPerThread = changedComponentsPerThread
};
var diffJobHandle = diffJob.Schedule(DependenciesHashMapHelper.GetBucketSize(_StructuralObjectExistDependency), 64);

传入job的四个参数,objectExistDependencies表示之前记录的dependency,objectExists就是当前asset的状态,deduplicatedObjIds代表去重过的object instance id,changedComponentsPerThread顾名思义就是记录需要重新baking的authoring component。job具体执行的代码如下:

public void ProcessEntry(int threadIndex, in UnsafeParallelMultiHashMap<int, ObjectExistDependency> hashMap, in int key, in ObjectExistDependency value)
{
    // Add them if the exist state has changed (State has changed)
    int existsID = deduplicatedObjIds[value.InstanceID];
    if (value.exists != objectExists[existsID])
    {
        changedComponentsPerThread.Add(key, m_ThreadIndex);
        IncrementalBakingLog.RecordComponentBake(key, ComponentBakeReason.ObjectExistStructuralChange, value.InstanceID, value.Type);
    }
}

传入的key和value取自于objectExistDependencies这个数据结构,分别表示authoring component的instance id,以及记录的ObjectExistDependency对象。在解释完这些数据结构的含义之后,这里job执行的逻辑就很清晰了。

哎,等一下,这里到目前为止所说的,都是关于asset是否存在的依赖,那么asset本身是否修改,这里的依赖是在哪里注册的呢?实际上,还是在这个DependResolveReference函数里:

AddObjectReference(referencedInstanceID);

这个函数会把authoring component和所依赖的object instance id关联,最终保存到一个UnsafeParallelMultiHashMap<int, int>类型的_PropertyChangeDependency变量里。然后,当components,gameobjects,或是assets发生变化时,都会触发相应的job,对该dependency进行扫描。

JobHandle nonStructuralChangedComponentJobHandle = default;
if (incrementalConversionDataCache.ChangedComponents.Length > 0)
{
    var nonStructuralChangedComponentJob = new NonStructuralChangedComponentJob()
    {
        changedComponents = incrementalConversionDataCache.ChangedComponents,
        reversePropertyChangeDependency = _ReversePropertyChangeDependency,
        changedComponentsPerThread = changedComponentsPerThread
    };
    nonStructuralChangedComponentJobHandle = nonStructuralChangedComponentJob.Schedule(incrementalConversionDataCache.ChangedComponents.Length, 64, calculateGlobalReversePropertyJobHandle);
}

JobHandle nonStructuralChangedGameObjectPropertiesJobHandle = default;
if (incrementalConversionDataCache.ChangedGameObjectProperties.Length > 0)
{
    var nonStructuralChangedGameObjectPropertiesJob = new NonStructuralChangedGameObjectPropertiesJob()
    {
        changedGameObjects = incrementalConversionDataCache.ChangedGameObjectProperties,
        reversePropertyChangeDependency = _ReversePropertyChangeDependency,
        reverseGameObjectPropertyChangeDependency = _ReverseObjectPropertyDependency,
        changedComponentsPerThread = changedComponentsPerThread
    };
    nonStructuralChangedGameObjectPropertiesJobHandle = nonStructuralChangedGameObjectPropertiesJob.Schedule(incrementalConversionDataCache.ChangedGameObjectProperties.Length, 64, JobHandle.CombineDependencies(calculateGameObjectPropertyReverseJobHandle, calculateGlobalReversePropertyJobHandle));
}

JobHandle nonStructuralChangedAssetsJobHandle = default;
#if UNITY_EDITOR
if (incrementalConversionDataCache.ChangedAssets.Length > 0)
{
    var nonStructuralChangedAssetsJob = new NonStructuralChangedAssetsJob()
    {
        changedAssets = incrementalConversionDataCache.ChangedAssets,
        reversePropertyChangeDependency = _ReversePropertyChangeDependency,
        changedComponentsPerThread = changedComponentsPerThread
    };
    nonStructuralChangedAssetsJobHandle = nonStructuralChangedAssetsJob.Schedule(incrementalConversionDataCache.ChangedAssets.Length, 64, calculateGlobalReversePropertyJobHandle);
}
#endif

NonStructuralChangedComponentJob这个job为例:

public void Execute(int i)
{
    var component = changedComponents[i];
    changedComponentsPerThread.Add(component.instanceID, m_ThreadIndex);

    IncrementalBakingLog.RecordComponentBake(component.instanceID, ComponentBakeReason.ComponentChanged, component.instanceID, component.unityTypeIndex);
    IncrementalBakingLog.RecordComponentChanged(component.instanceID);

    foreach (var dep in reversePropertyChangeDependency.GetValuesForKey(component.instanceID))
    {
        changedComponentsPerThread.Add(dep, m_ThreadIndex);

        IncrementalBakingLog.RecordComponentBake(dep, ComponentBakeReason.GetComponentChanged, component.instanceID, component.unityTypeIndex);
    }
}

首先,肯定是将change component自身记录进来,它是需要重新baking的;然后,对当前记录的dependency进行扫描,取出依赖该change component的authoring components,它可能存在多个,需要进行遍历,一一记录。这样,所有依赖change component的autoring components全部都会重新baking。

此外,我们的authoring component也有可能需要获取自身GameObject上的某些component,例如我们在ECS Component中新增一个float3的position,读取transform上的position:

public override void Bake(MyAuthoring authoring)
{
    Debug.Log("==========================Bake Invoked!========================== " + authoring.name);

    DependsOn(authoring.info);

    if(authoring.info == null) return;

    var transform = authoring.transform;

    var entity = GetEntity(TransformUsageFlags.None);
    AddComponent(entity, new MyComponent {
        value = authoring.bakeIntData,
        spacing = authoring.info.Spacing,
        position = transform.position
    });
}

public struct MyComponent : IComponentData
{
    public int value;
    public float spacing;
    public float3 position;
}

然而,直接使用authoring.transform获取到的transform,如果发生了变化,并不会重新触发baking。也就是说我们也需要手动指定依赖。Unity官方推荐使用GetComponent函数来获取component。

It is important to access other authoring components by using the GetComponent methods, because doing so registers dependencies. If we would have used authoring.transform here instead, a dependency would not have been registered, and moving the authoring GameObject while live baking would not rerun the baker as it should.

那我们就换用GetComponent<Transform>()来测试一下:

Unity DOTS中的baking(二)5

的确修改transform就会触发baking了。秉着知其然知其所以然的精神,我们来看看GetComponent背后的代码:

/// <summary>
/// Retrieves the component of Type T in the GameObject
/// </summary>
/// <param name="gameObject">The GameObject to get the component from</param>
/// <typeparam name="T">The type of component to retrieve</typeparam>
/// <returns>The component if a component matching the type is found, null otherwise</returns>
/// <remarks>This will take a dependency on the component</remarks>
private T GetComponentInternal<T>(GameObject gameObject) where T : Component
{
    var hasComponent = gameObject.TryGetComponent<T>(out var returnedComponent);

    _State.Dependencies->DependOnGetComponent(gameObject.GetInstanceID(), TypeManager.GetTypeIndex<T>(), hasComponent ? returnedComponent.GetInstanceID() : 0, BakeDependencies.GetComponentDependencyType.GetComponent);

    // Transform component takes an implicit dependency on the entire parent hierarchy
    // since transform.position and friends returns a value calculated from all parents
    var transform = returnedComponent as Transform;
    if (transform != null)
        _State.Dependencies->DependOnParentTransformHierarchy(transform);

    return returnedComponent;
}

不难发现关键代码就在DependOnGetComponent这个函数了:

public void DependOnGetComponent(int gameObject, TypeIndex type, int returnedComponent, GetComponentDependencyType dependencyType)
{
    if (returnedComponent != 0)
        AddObjectReference(returnedComponent);

    AddGetComponent(new GetComponentDependency {GameObject = gameObject, Type = type, ResultComponent = returnedComponent, DependencyType = dependencyType});
}

returnedComponent代表获取到的component的instance id,如果component存在,就需要追踪它本身是否发生变化,因此这里就和前面一样,使用AddObjectReference这个函数记录依赖。AddGetComponent会把要获取的GameObject,component,component的type index,以及依赖类型给记录下来,最终保存到_StructuralGetComponentDependency这个hashmap中。CalculateStructuralGetComponentDependencyJob会用到保存的数据,当检测到components,gameobjects,或是assets变化时,job就会触发。

public void ProcessEntry(int threadIndex, in UnsafeParallelMultiHashMap<int, GetComponentDependency> hashMap, in int key, in GetComponentDependency value)
{
    if (!value.IsValid(ref components, ref hierarchy))
    {
        changedComponentsPerThread.Add(key, m_ThreadIndex);
        IncrementalBakingLog.RecordComponentBake(key, ComponentBakeReason.GetComponentStructuralChange, value.ResultComponent, value.Type);
    }
}

从字面含义来看,只有GetComponentDependency类型的value判断为invalid时,才会把对应的key标记为需要重新baking。这里的key和value就是hashmap中的键值对,key表示authoring component,value里记录了当时使用GetComponent获取到的component依赖。那么,什么时候IsValid返回的是false呢?

public bool IsValid(ref GameObjectComponents components, ref SceneHierarchy hierarchy)
{
    switch (DependencyType)
    {
        case GetComponentDependencyType.GetComponentInParent:
            return GameObjectComponents.GetComponentInParent(ref components, ref hierarchy, GameObject, Type) == ResultComponent;
        case GetComponentDependencyType.GetComponent:
            return components.GetComponent(GameObject, Type) == ResultComponent;
        case GetComponentDependencyType.GetComponentInChildren:
            return GameObjectComponents.GetComponentInChildren(ref components, ref hierarchy, GameObject, Type) == ResultComponent;
    }
    return false;
}

这里的DependencyType为GetComponentDependencyType.GetComponent,可以看到这里会再去获取一下记录的GameObject身上最新的component,如果和之前记录的component instance id不同,说明component经历了从无到有或者从有到无的过程,它已经不是之前的那个对象了,此时就会返回false。总结一下,GetComponentInternal函数,不仅对component内部的值发生变化进行了依赖注册,还对component自身引用发生变化也进行了依赖注册。

如果DependsOn和GetComponent所依赖的component,它是个transform,Unity还会调用DependOnParentTransformHierarchy这个函数做一些额外的依赖工作。这是因为transform本身就不是独立的,它依赖整个parent hierarchy。这个事情是必要的,比如我们给当前挂有MyAuthoring脚本的GameObject建立一个父节点,如果父节点的transform发生变化,或者层级关系发生了变化,那么子节点的world transform必然也发生了变化,理应就要再次触发baking。

Unity DOTS中的baking(二)6

DependOnParentTransformHierarchy的实现如下:

public void DependOnParentTransformHierarchy(Transform transform)
{
    if (transform != null)
    {
        var hashGenerator = new xxHash3.StreamingState(false);
        GameObject go = transform.gameObject;
        int goInstanceID = go.GetInstanceID();

        // We take the dependency on the parent hierarchy.
        transform = transform.parent;
        while (transform != null)
        {
            hashGenerator.Update(transform.gameObject.GetInstanceID());

            AddObjectReference(transform.GetInstanceID());
            transform = transform.parent;
        }

        var hash = new Hash128(hashGenerator.DigestHash128());
        AddGetHierarchy(new GetHierarchyDependency {GameObject = goInstanceID, Hash = hash, DependencyType = GetHierarchyDependencyType.Parent});
    }
}

可以看到,Unity会对当前节点到根节点整个parent hierarchy进行扫描,对每个父节点的transform都建立了依赖关系,然后还记录了每个父节点GameObject的instance id,把它们汇总在一起生成一个128位的hash值。这个hash值就是用来标记当前的hierarchy。值得一提的是,Unity ECS 1.0.16版本之前的这段代码是有问题的,之前的版本也会把当前节点的instance id也统计进去,这点在Unity的changelog里有说明:

Changelog

[1.0.16] - 2023-09-11

Fixed

  • Fixed a hash mismatch on DependOnParentTransformHierarchy

之后,CalculateStructuralGetHierarchyDependencyJob这个job会使用之前记录的GetHierarchyDependency,判断hierarchy是否发生了变化(如果是1.0.16之前的版本,这里就必然会发生变化了,两边计算的方式压根就不一致)

public void ProcessEntry(int threadIndex, in UnsafeParallelMultiHashMap<int, GetHierarchyDependency> hashMap, in int key, in GetHierarchyDependency value)
{
    if (!value.IsValid(ref hierarchy))
    {
        changedComponentsPerThread.Add(key, m_ThreadIndex);
        IncrementalBakingLog.RecordComponentBake(key, ComponentBakeReason.GetHierarchyStructuralChange, 0, default);
    }
}

具体的判断逻辑位于IsValid

public Hash128 GetParentsHash(ref SceneHierarchy hierarchy, int instanceId)
{
    var hashGenerator = new xxHash3.StreamingState(false);

    if (hierarchy.TryGetIndexForInstanceId(instanceId, out int currentIndex))
    {
        while (currentIndex != -1)
        {
            int parentIndex = hierarchy.GetParentForIndex(currentIndex);
            if (parentIndex != -1)
            {
                int parentInstanceID = hierarchy.GetInstanceIdForIndex(parentIndex);
                hashGenerator.Update(parentInstanceID);
            }
            currentIndex = parentIndex;
        }
    }
    return new Hash128(hashGenerator.DigestHash128());
}

public bool IsValid(ref SceneHierarchy hierarchy)
{
    Hash128 returnValue = default;
    switch (DependencyType)
    {
        case GetHierarchyDependencyType.Parent:
            returnValue = GetParentsHash(ref hierarchy, GameObject);
            break;
        case GetHierarchyDependencyType.ImmediateChildren:
            returnValue = GetChildrenHash(ref hierarchy, GameObject, false);
            break;
        case GetHierarchyDependencyType.AllChildren:
            returnValue = GetChildrenHash(ref hierarchy, GameObject, true);
            break;
    }
    return (returnValue == Hash);
}

DependOnParentTransformHierarchy注册的dependency type为GetHierarchyDependencyType.Parent,因此这里计算出的hash值表示当前的parent hierarchy。如果两个hash值不同,说明两个hierarchy不同,那么authoring component需要重新运行baking。而且xxhash算法是一种非常快速的非加密哈希算法,它基本不可能发生碰撞,所以这里如果两个hash值相等,就可以认为两个hierarchy是完全相同的。

最后,我们来实际演示一下:

Unity DOTS中的baking(二)7

Reference

[1] Baker overview

[2] EntityComponentSystemSamples

[3] xxHash

  • 19
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Unity DOTS(DOTS: Data-Oriented Technology Stack)是Unity的一种数据导向技术堆栈,它旨在提高游戏性能和可扩展性。它基于实体组件系统(Entity-Component-System,ECS)的概念,其游戏对象被拆分为实体(Entity)和组件(Component),并通过系统(System)进行处理。 在Unity DOTS,可以使用实体组件系统来管理和处理游戏对象。通过将游戏对象转换为实体和组件的形式,可以实现高效的数据处理和并行计算。例如,在创建实体时,可以使用GameObjectConversionUtility.ConvertGameObjectHierarchy函数将GameObject转换为Entity,并使用IConvertGameObjectToEntity接口进行自定义转换。然后,可以使用系统(System)对实体和组件进行处理,例如旋转方块事件。 Unity DOTS的优势包括更高的性能,更好的可扩展性和更方便的并行计算。通过采用数据导向的设计,可以减少内存访问和数据处理的开销,从而提高游戏的帧率和响应性。 总而言之,Unity DOTS是一种数据导向的技术堆栈,通过实体组件系统和并行计算来提高游戏性能和可扩展性。它可以通过转换游戏对象为实体和组件的形式,并使用系统进行处理来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [UnityDots技术入门](https://blog.csdn.net/m0_37920739/article/details/108181541)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Unity DOTS简明教程](https://blog.csdn.net/mango9126/article/details/105219215)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值