Parent Component Disappearing (Debug Help or Work-Around)

Hello!

Been working on some Archery VR game using ECS to learn and practice. Added a ICollisionEventsJob to the ArrowCollisionSystem to detect collision of an Arrow with another collider.

public void Execute(CollisionEvent collisionEvent)
            {
                bool isEntityARotationFollowsLinearVelocity =
                    RotationFollowsLinearVelocityGroup.Exists(collisionEvent.EntityA);
                bool isEntityBRotationFollowsLinearVelocity =
                    RotationFollowsLinearVelocityGroup.Exists(collisionEvent.EntityB);

                if (isEntityARotationFollowsLinearVelocity && isEntityBRotationFollowsLinearVelocity
                    || !isEntityARotationFollowsLinearVelocity && !isEntityBRotationFollowsLinearVelocity)
                {
                    return;
                }

                Entity arrowEntity;
                if (isEntityARotationFollowsLinearVelocity)
                {
                    arrowEntity = collisionEvent.EntityA;

                    RotationFollowsLinearVelocityGroup[arrowEntity] =
                        new RotationFollowsLinearVelocityComponent() { IsActive = false };
                }
                else if (isEntityBRotationFollowsLinearVelocity)
                {
                    arrowEntity = collisionEvent.EntityB;

                    RotationFollowsLinearVelocityGroup[arrowEntity] =
                        new RotationFollowsLinearVelocityComponent() { IsActive = false };
                }
                else
                {
                    return;
                }

                Entity otherEntity = !isEntityARotationFollowsLinearVelocity
                    ? collisionEvent.EntityA
                    : collisionEvent.EntityB;

                float3 collisionHeading =
                    LocalToWorldGroup[arrowEntity].Position -
                    LocalToWorldGroup[otherEntity].Position;

                float angle = Float3Utility.Angle(
                    collisionHeading,
                    PhysicsVelocityGroup[arrowEntity].Linear);             

                if (angle > 45f)
                {
                    // TODO: Turn off all physics and reparent arrow
                }
            }

Collision is working, but I want to reparent my Arrow to whatever it hit such that it will follow that entity, whether it be a wall or eventually an enemy.

That’s where the problems arise. This is what I’ve tried so far:

  • The Arrow entity doesn’t have a Parent component because while flying through the air it shouldn’t have a parent. So I decided to add a Parent component to the entity. Problem is that later on when setting the Parent component through using ComponentDataFromEntity in it says that the Arrow entity doesn’t have a Parent component. This is confirmed by the Entity Debugger.
  • So I decided that maybe the Parent component will be removed if no data is given to the Parent so I decided to start doing some dumb tests; setting the Parent to struct default, setting the Parent to itself, and setting the Parent to Entity.Null. All of these hits the problem of Parent not being added for some reason.
  • Try adding the Parent component when we instantiate the Arrow entity. At this point I could have the Parent be the ArrowManagerEntity, an entity that would have components such as arrow count and arrow pooling (maybe). ArrowManagerEntity is currently doing nothing but holding the ArrowEntityReferenceComponent.
 public void Convert(
            Entity entity,
            EntityManager entityManager,
            GameObjectConversionSystem conversionSystem)
        {
            entityManager.SetName(entity, "ArrowManagerEntity");

            ArrowEntity =
                     GameObjectConversionUtility.ConvertGameObjectHierarchy(
                         _arrowEntityPrefab.gameObject,
                         _conversionSettings);

            entityManager.SetName(ArrowEntity, "ConvertedArrowEntity");

            entityManager.AddComponentData(
                entity,
                new ArrowEntityReferenceComponent(ArrowEntity));

            entityManager.AddComponentData(
                entity,
                new ArrowManagerEntityReferenceComponent(entity));
        }

Which is then is used in the ArrowShootSystem:

Entities
                .WithName("CreateArrowEntityJob")
                .ForEach(
                    (in ArrowEntityReferenceComponent arrowReference,
                    in ArrowManagerEntityReferenceComponent managerReference) =>
                    {
                        Translation arrowTranslation =
                            new Translation { Value = arrowPosition };

                        Rotation arrowRotation =
                            new Rotation { Value = arrowQuaternion };

                        Entity arrowEntityInstance =
                            EntityUtility.InstantiateEntity(
                                arrowReference.Value,
                                _entityManager,
                                arrowTranslation,
                                arrowRotation);

                        _entityManager.SetName(
                            arrowEntityInstance,
                            "Arrow");

                        _entityManager.AddComponentData(
                            arrowEntityInstance,
                            new Parent { Value = managerReference.Value });

                        PhysicsVelocity physicsVelocity =
                            _entityManager.GetComponentData<PhysicsVelocity>(
                                arrowEntityInstance);

                        PhysicsMass physicsMass =
                            _entityManager.GetComponentData<PhysicsMass>(
                                arrowEntityInstance);

                        PhysicsVelocity updatedVelocity = new PhysicsVelocity
                        {
                            Linear =
                                physicsVelocity.Linear +
                                BowUtility.CalculateArrowShootVelocity(
                                    arrowTranslation,
                                    arrowRotation,
                                    physicsMass.InverseMass,
                                    bowShotPercentage)
                        };

                        _entityManager.SetComponentData(
                            arrowEntityInstance,
                            updatedVelocity);
                       
                    })
                .WithStructuralChanges()
                .Run();

This is where I decided to add the Parent component and have it be ArrowEntityManager. Problem is that for some reason setting that as the Parent immediately stops all physics and just makes the arrow appear in a specific position.

  • So finally I decided to add the Parent all the way in the job of the ArrowCollisionSystem. I didn’t know how to add components inside a ICollisionEventsJob, and it felt like it would violate the separation of needed for Burst compilation, but decided to look into it. Found this forum using EntityCommandBuffer, albeit in a IJobForEachWithEntity job: AddComponent from inside JobComponentSystem Job? . Now I’m having trouble finding what the job index is as ICollisionEventsJob.Execute does not provide that information.

So that’s where I am now. Now I’m playing with the idea to add a new component to my arrow called ArrowCollisionDataComponent that would have the following:

  • public bool HasCollided;
  • public Entity CollidedEntity;
  • public float3 PositionOnCollision;
  • public quaternion RotationOnCollision;

Then I would have another system check which arrows have collided and set up and add the Parent component appropriately. Would like some help either debugging why Parent component isn’t getting added or if there is a work around. Any further discussion is also appreciated.

I’ve gone ahead and added a new system that checks a new component called ArrowPenetrationComponent.

 public class ArrowPenetrationSystem : SystemBase
    {
        private EntityManager _entityManager;

        protected override void OnCreate()
        {
            _entityManager =
                World.DefaultGameObjectInjectionWorld.EntityManager;
        }

        protected override void OnUpdate()
        {
            Entities
                .WithName("AttachArrowToPenetratedEntityJob")
                .ForEach(
                    (ref LocalToWorld arrowLocalToWorld,
                    in ArrowPenetrationComponent arrowPenetrationComponent) =>
                    {
                        if(arrowPenetrationComponent.IsPenetrated)
                        {
                            _entityManager.AddComponentData(
                                arrowPenetrationComponent.PenetratingArrow,
                                new Parent
                                {
                                    Value = arrowPenetrationComponent.PenetratedObject
                                });

                            arrowLocalToWorld =
                                arrowPenetrationComponent.ArrowLocalToWorld;
                        }
                    })
                .WithStructuralChanges()
                .Run();
        }

Not really happy with it since now I need to run it WithStructuralChanges() instead of having it be run in parallel with Burst. Here’s how the ArrowCollisionJob checks out now:

[BurstCompile]
        struct ArrowCollisionJob : ICollisionEventsJob
        {
            public ComponentDataFromEntity<ArrowPenetrationComponent>
                ArrowPenetrationGroup;
            public ComponentDataFromEntity<RotationFollowsLinearVelocityComponent>
                RotationFollowsLinearVelocityGroup;
            [ReadOnly] public ComponentDataFromEntity<PhysicsVelocity>
                PhysicsVelocityGroup;
            [ReadOnly] public ComponentDataFromEntity<LocalToWorld>
                LocalToWorldGroup;

            public void Execute(CollisionEvent collisionEvent)
            {
                bool isEntityARotationFollowsLinearVelocity =
                    RotationFollowsLinearVelocityGroup.Exists(collisionEvent.EntityA);
                bool isEntityBRotationFollowsLinearVelocity =
                    RotationFollowsLinearVelocityGroup.Exists(collisionEvent.EntityB);

                if (isEntityARotationFollowsLinearVelocity && isEntityBRotationFollowsLinearVelocity
                    || !isEntityARotationFollowsLinearVelocity && !isEntityBRotationFollowsLinearVelocity)
                {
                    return;
                }

                Entity arrowEntity;
                if (isEntityARotationFollowsLinearVelocity)
                {
                    arrowEntity = collisionEvent.EntityA;

                    RotationFollowsLinearVelocityGroup[arrowEntity] =
                        new RotationFollowsLinearVelocityComponent() { IsActive = false };
                }
                else if (isEntityBRotationFollowsLinearVelocity)
                {
                    arrowEntity = collisionEvent.EntityB;

                    RotationFollowsLinearVelocityGroup[arrowEntity] =
                        new RotationFollowsLinearVelocityComponent() { IsActive = false };
                }
                else
                {
                    return;
                }

                //TODO: Remove collider or make it not throw any more events.
                if(ArrowPenetrationGroup[arrowEntity].IsPenetrated)
                {
                    return;
                }

                Entity otherEntity = !isEntityARotationFollowsLinearVelocity
                    ? collisionEvent.EntityA
                    : collisionEvent.EntityB;

                ArrowPenetrationGroup[arrowEntity] =
                        new ArrowPenetrationComponent(
                            arrowEntity,
                            LocalToWorldGroup[arrowEntity],
                            otherEntity);
            }

Still would be interested in a discussion get my other portion in parallel or get it all within ArrowCollisionJob. Thanks!

Ok figured out how to get it running with BurstCompile.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Transforms;

namespace JuegosFrantasticos
{
    public class ArrowCollisionSystem : JobComponentSystem
    {
        private BuildPhysicsWorld _buildPhysicsWorld;
        private StepPhysicsWorld _stepPhysicsWorld;
        private EntityQuery _rotationFollowsLinearVelocityGroup;
        private EntityCommandBufferSystem _entityCommandBufferSystem;
        private EntityQuery _arrowEntityGroup;

        protected override void OnCreate()
        {
            _buildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
            _stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();

            _rotationFollowsLinearVelocityGroup =
                GetEntityQuery(new EntityQueryDesc
                {
                    All = new ComponentType[]
                    {
                        typeof(RotationFollowsLinearVelocityComponent)
                    }
                });

            _entityCommandBufferSystem =
                World.GetOrCreateSystem<EntityCommandBufferSystem>();

            _arrowEntityGroup = GetEntityQuery(new EntityQueryDesc
            {
                All = new ComponentType[]
                {
                    typeof(ArrowPenetrationComponent),
                    typeof(LocalToWorld),
                }
            });

            base.OnCreate();
        }

        protected override JobHandle OnUpdate(JobHandle inputDependencies)
        {
            JobHandle jobHandle = new JobHandle();

            if (_rotationFollowsLinearVelocityGroup.CalculateEntityCount() == 0)
            {
                return jobHandle;
            }

            CollisionJob collisionJob = new CollisionJob
            {
                PhysicsVelocityGroup =
                    GetComponentDataFromEntity<PhysicsVelocity>(true),
                LocalToWorldGroup =
                    GetComponentDataFromEntity<LocalToWorld>(true),
                ArrowPenetrationGroup =
                    GetComponentDataFromEntity<ArrowPenetrationComponent>(),
                RotationFollowsLinearVelocityGroup =
                    GetComponentDataFromEntity<RotationFollowsLinearVelocityComponent>(),
            };

            jobHandle = collisionJob.Schedule(
                _stepPhysicsWorld.Simulation,
                ref _buildPhysicsWorld.PhysicsWorld,
                inputDependencies);

            AttachJob penetrationJob = new AttachJob
            {
                EntityCommandBuffer =
                    _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
                ArrowPenetrationArchetypeChunk =
                    GetArchetypeChunkComponentType<ArrowPenetrationComponent>(),
                EntityChunkType =
                    GetArchetypeChunkEntityType()
            };

            jobHandle = penetrationJob.Schedule(_arrowEntityGroup, jobHandle);

            return jobHandle;
        }

        [BurstCompile]
        private struct CollisionJob : ICollisionEventsJob
        {
            public ComponentDataFromEntity<ArrowPenetrationComponent>
                ArrowPenetrationGroup;
            public ComponentDataFromEntity<RotationFollowsLinearVelocityComponent>
                RotationFollowsLinearVelocityGroup;
            [ReadOnly] public ComponentDataFromEntity<PhysicsVelocity>
                PhysicsVelocityGroup;
            [ReadOnly] public ComponentDataFromEntity<LocalToWorld>
                LocalToWorldGroup;

            public void Execute(CollisionEvent collisionEvent)
            {
                bool isEntityARotationFollowsLinearVelocity =
                    RotationFollowsLinearVelocityGroup.Exists(collisionEvent.EntityA);
                bool isEntityBRotationFollowsLinearVelocity =
                    RotationFollowsLinearVelocityGroup.Exists(collisionEvent.EntityB);

                if (isEntityARotationFollowsLinearVelocity && isEntityBRotationFollowsLinearVelocity
                    || !isEntityARotationFollowsLinearVelocity && !isEntityBRotationFollowsLinearVelocity)
                {
                    return;
                }

                Entity arrowEntity;
                if (isEntityARotationFollowsLinearVelocity)
                {
                    arrowEntity = collisionEvent.EntityA;

                    RotationFollowsLinearVelocityGroup[arrowEntity] =
                        new RotationFollowsLinearVelocityComponent() { IsActive = false };
                }
                else if (isEntityBRotationFollowsLinearVelocity)
                {
                    arrowEntity = collisionEvent.EntityB;

                    RotationFollowsLinearVelocityGroup[arrowEntity] =
                        new RotationFollowsLinearVelocityComponent() { IsActive = false };
                }
                else
                {
                    return;
                }

                //TODO: Remove collider or make it not throw any more events.
                if(ArrowPenetrationGroup[arrowEntity].IsPenetrated)
                {
                    return;
                }

                Entity otherEntity = !isEntityARotationFollowsLinearVelocity
                    ? collisionEvent.EntityA
                    : collisionEvent.EntityB;

                ArrowPenetrationGroup[arrowEntity] =
                        new ArrowPenetrationComponent(
                            arrowEntity,
                            LocalToWorldGroup[arrowEntity],
                            otherEntity);
            }
        }

        [BurstCompile]
        private struct AttachJob : IJobChunk
        {
            public EntityCommandBuffer.Concurrent EntityCommandBuffer;
            public ArchetypeChunkComponentType<ArrowPenetrationComponent>
                ArrowPenetrationArchetypeChunk;

            [ReadOnly]
            public ArchetypeChunkEntityType EntityChunkType;

            public void Execute(
                ArchetypeChunk chunk,
                int chunkIndex,
                int firstEntityIndex)
            {
                NativeArray<Entity> entities =
                    chunk.GetNativeArray(EntityChunkType);
                NativeArray<ArrowPenetrationComponent> arrowPenetrationArray =
                    chunk.GetNativeArray(ArrowPenetrationArchetypeChunk);

                for (int i = 0; i < entities.Length; ++i)
                {
                    if (arrowPenetrationArray[i].IsPenetrated)
                    {
                        EntityCommandBuffer.AddComponent(
                            chunkIndex,
                            entities[i],
                            new Parent
                            {
                                Value = arrowPenetrationArray[i].PenetratedObject
                            });

                        EntityCommandBuffer.SetComponent(
                            chunkIndex,
                            entities[i],
                            arrowPenetrationArray[i].ArrowLocalToWorld);
                    }
                }
            }
        }
    }
}

Still unsure why the heck Parent component is removed from the arrow when I added that component. Now if you can excuse me, I need to go back and refactor some stuff to follow this Burst paradigm.

I see that you’re using AddComponent() with new Parent { Value = xxxx; }, but in order to properly reparent, don’t you also need to add the arrow to the new parent’s DynamicBuffer of children?

I’m asking this not because I know the answer, but because I’m about to do something similar and my initial theory is that in order to do a full “reparent” at runtime we would need to do these steps:

  1. if the child has an old parent, remove the child from the old parent’s buffer of children.
  2. set the child’s new parent (like you did).
  3. add the child to the new parent’s buffer of children.

I unfortunately wasn’t able to find an answer. I do know for a fact the re-parenting is working using the EntityDebugger. In any case, the conversation moved to the Physics DOTS forum (I didn’t know that existed when creating this thread). There is a couple others trying to help though I’m taking a break from solving this at the moment. https://forum.unity.com/threads/und…-physics-execution-order.964994/#post-6352599

Alternatively, I’ve found that I can give something a parent, but if I also give something children, all link components are removed. I can add the components like I see them when parented game objects are converted, but something isn’t right. I’ve also taken a break from it for the moment. I wonder if it is an order of operations thing, and the transforms parent systems need something a little more specific that we are not noticing.

Really? So unit.physics is smart enough to recognize that a parent changed and perform the other two steps automatically (removing from the old parent list of children, and adding it to the new parent’s list of children)?

Sorry I can’t help much with the arrow collision physics part… I’m still just learning unity.physics.

Unity.Entites can do auto reparent. and these a hidden SystemStateComponentData call PreviousParent. that help with that.
So user should just add/remove/change Parent component and leave Child buffer as it is.

2 Likes

Use EntityCommandBuffer to add parent component.

Initially I wrote a function that just adds the parent (like you suggested), but I noticed that it was not getting added as a child to the parent. So then I wrote a more complex function to add the child to the parent’s list of children and remove the child from the previous parent’s list of children. It did not work, and I don’t know why.

Then I discovered an easier way… it is possible, like you said, to just add the parent to the child and let Unity.Entities do the “routing”, HOWEVER, it’s very important that the child has a LocalToParent object!

if (!EntityManager.HasComponent<LocalToParent>(child)) { EntityManager.AddComponentData(child, new LocalToParent()); }

Once I added that, things started working as expected. For GameObjects, it’s so simple - just child.transform.SetParent(); , but with DOTS we get to figure it out by trial and error.

1 Like

Oh, I never noticed that. I use GameObject convention all the time. So there’s always a LocalToParent, Translation and Rotation. It would be much easier to use GameObject and GameObject prefab as entity archetype.

I am also using a GameObject. I made a GameObject at the root level in the class hierarchy with a Sprite Renderer component and ConvertToEntity. The resulting entity did not have a LocalToParent component. I think that if the GameObject resides at the root level, the ConvertToEntity script won’t add LocalToParent. So I had to make a converter script that adds Parent and LocalToParent.

using Unity.Entities;
using UnityEngine;
using Unity.Transforms;

public class ChildConverter : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponent<Parent>(entity);
        dstManager.AddComponent<LocalToParent>(entity);
    }
}

Oh, sorry while I was typing LocalToParent I was thinking LocalToWorld…
LocalToParent do needs to be added

1 Like