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(二)Baker的触发_第1张图片

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

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

    class MyBaker : Baker
    {
        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(二)Baker的触发_第2张图片

Unity DOTS中的baking(二)Baker的触发_第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了:

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

/// 
/// This will take a dependency on Object of type T.
/// 
/// The Object to take a dependency on.
/// The type of the object. Must be derived from UnityEngine.Object.
/// The Object of type T if a dependency was taken, null otherwise.
public T DependsOn(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 objectExists = new NativeArray(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 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类型的_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()来测试一下:

Unity DOTS中的baking(二)Baker的触发_第4张图片

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

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

    _State.Dependencies->DependOnGetComponent(gameObject.GetInstanceID(), TypeManager.GetTypeIndex(), 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 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(二)Baker的触发_第5张图片

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 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(二)Baker的触发_第6张图片

Reference

[1] Baker overview

[2] EntityComponentSystemSamples

[3] xxHash

你可能感兴趣的:(DOTS,unity,游戏引擎)