Transform sync can end up out of sync, get wrong transform data

Hello

We’ve run into an issue which we believe to be a bug.

We have a bunch of object which still rely to some part on standard systems, hence we’re still very much working in a hybrid way and utilizing CopyTransformFromGameObject on most such objects. These are instantiated with IConvertGameObjectToEntity, along with ConvertToEntity set to ConvertAndInjectOriginal

In certain situations it appears as if the LocalToWorld component data can get incorrect values from the system handling the syncing. We noticed it when we had enemes targeting the player, and for one frame targeting something completely different position.

It became very apparent when we had a component we added to an entity, removed it and replaced it with another component, and then removed that one in the same frame. (Code examples below)
The problem went away when we used the same components and methods, but delayed the replacement and removal one frame. I.e., instead of Add → Remove/Add another component, it works if we do Add → Remove → Next frame → Add

Are we doing something horriby wrong, or is this a bug?
Wild guess, but I’m assuming that there occurs some mismatch in the copy transform system due to the entity changing archetypes in an unorthodox manner.

Our authoring scripts

public class TestUnitProxy : MonoBehaviour,  IConvertGameObjectToEntity
{
    public Entity entity { get; private set; }
    public EntityManager entityManager { get; private set; }


    void Update()
    {
        if(Input.GetKeyDown(KeyCode.Return))
        {
            if(!entityManager.HasComponent<TestUnitComponentData>(entity))
            {
                entityManager.AddComponentData<TestUnitComponentData>(entity, new TestUnitComponentData());
            }
        }
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        this.entity = entity;
        entityManager = dstManager;

        entityManager.AddComponentData<CopyTransformFromGameObject>(this.entity, new CopyTransformFromGameObject());

        dstManager.SetName(entity, name);
    }
}
public class StationaryProxy : MonoBehaviour, IConvertGameObjectToEntity
{
    public Entity entity { get; private set; }
    public EntityManager entityManager { get; private set; }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        this.entity = entity;
        entityManager = dstManager;
        dstManager.SetComponentData<LocalToWorld>(entity, new LocalToWorld
        {
            Value = new float4x4(transform.rotation, transform.position)
        });

        dstManager.AddComponentData<CopyTransformFromGameObject>(entity, new CopyTransformFromGameObject());

#if UNITY_EDITOR
        dstManager.SetName(entity, name);
#endif
    }
}

TestUnitProxy adds an arbitrary component on hitting enter. The other type is just as a counterpoint, I tihnk there needs to be at least two archetypes for it to happen

The components. Does not seem related to whether they have data or are just tags, I’ve reproduced it with both cases

public struct TestUnitComponentData : IComponentData
{
}
public struct TestUnitSecondComponentData : IComponentData
{
}

The systems

public sealed class TestUnitSystem : ComponentSystem
{
    #region Fields
    EntityQuery group;
    #endregion // Fields

    #region Methods
    #region ComponentSystem
    protected override void OnCreate()
    {
        base.OnCreate();

        group = GetEntityQuery(ComponentType.ReadWrite<TestUnitComponentData>());
    }

    protected override void OnUpdate()
    {
        NativeArray<Entity> entities = group.ToEntityArray(Allocator.TempJob);

        for(int i = 0; i < entities.Length; ++i)
        {
            Entity entity = entities[i];

            PostUpdateCommands.AddComponent<TestUnitSecondComponentData>(entity, new TestUnitSecondComponentData());
            PostUpdateCommands.RemoveComponent<TestUnitComponentData>(entity);
        }

        entities.Dispose();
    }
   
    #endregion // ComponentSystem
    #endregion // Methods
}
public sealed class SecondTestUnitSystem : ComponentSystem
{
    #region Fields
    EntityQuery group;
    #endregion // Fields

    #region Methods
    #region ComponentSystem
    protected override void OnCreate()
    {
        base.OnCreate();

        group = GetEntityQuery(ComponentType.ReadOnly<TestUnitSecondComponentData>());
    }
   
    protected override void OnUpdate()
    {
        EntityManager.RemoveComponent(group, typeof(TestUnitSecondComponentData));
    }
   
    #endregion // ComponentSystem
    #endregion // Methods
}

I’ve also uploaded a package with the project I used to reproduce the issue.

4710764–445172–LTWCopyTransformBug.unitypackage (29.2 KB)

Show systems order from entity debugger

Here ya go

So your test unit system updates before CopyTransformFromGameObject therefore any game object changes won’t be reflected in your system till the next frame.

(this is actually an issue I have with the transform setup atm. it seems to meant to be end of frame but this isn’t possible a lot of the time.)

Yes, but I believe that is not the case here?

I’m not doing any actual transform modifications in my test case, but it appears as if an Entity’s LocalToWorld component gets transform data form the wrong GameObject when it copies the transform data. I’m assuming the arrays gets mismatched/unsynced.

Also, a tangent based off of what you mentioned: that is my issue as well with how transform sync work, that both CopyTo and CopyFrom are run at end of frame. At least I think they are, I recall they were setup that way in the beginning but haven’t used it since. According to me, it would make more sense if CopyFrom happened at beginning of the frame, and CopyTo and the end. Or vice versa.
That way you could get all transform data from the beginning of the frame, operate on the LocalToWorld data in your systems, and at the end of the frame sync it back to the transforms. But that is unrelated to the issue at hand, just venting ^^