Entity reference in ComponentData getting set to Entity.Null

I have the following system that is working fine, up until the point at which it “runs out” of points to process. At that point the LaneEntity field on the VehicleLaneComponent has the value of Entity.Null. According to the entity debugger, the AddNextLaneSystem has never run (makes sense since the required component has not been set, as confirmed in the debugger by the absence of the required tag), and the code to add NeedsNextLaneTag never executes due to an exception from the aforementioned LaneEntity field. The obvious work-around is to add handling for that situation, but how did I get there in the first place? VehicleLaneComponent is marked as ReadOnly, so what is causing it to be updated? None of my other systems modify that data, other than the AddNextLaneSystem, which should only process an entity after this system has marked the entity as needing to be processed by that system, which it hasn’t, and the debugger seems to confirm that.

(Yes, the AddNextLaneSystem is marked to execute before this system, which is correct because if this entity has run out of lane points, I need that system to find and add a reference to the next lane during the next update in order for this system to be able to access the next point the entity should be moving to.)

Also, feel free to point out any other bone-headed decisions I’ve made in this code.

using GeoExplorer.RoadNetwork.DOTS.Components;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

namespace GeoExplorer.RoadNetwork.DOTS.Systems
{
    [UpdateAfter(typeof(AddNextLaneSystem))]
    public class ER3D_TrafficControlSystem : JobComponentSystem
    {
        BeginInitializationEntityCommandBufferSystem _entityCommandBufferSystem;

        [RequireComponentTag(typeof(ERAutoTag))]
        private struct AutoNavigationJob : IJobForEachWithEntity<
            VehicleLaneComponent,
            VehicleStatsComponent,
            VehicleCurrentDataComponent,
            Translation,
            Rotation>
        {
            public EntityCommandBuffer.Concurrent CommandBuffer;
            public float deltaTime;

            // [NativeDisableParallelForRestriction]
            [ReadOnly] public BufferFromEntity<LanePointsComponent> VehicleLaneComponentData;

            [ReadOnly] public ComponentDataFromEntity<LaneComponent> LaneComponentData;

            public void Execute(Entity entity,
                                int index,
                                [ReadOnly] ref VehicleLaneComponent vehicleLaneComponent,
                                [ReadOnly] ref VehicleStatsComponent vehicleStats,
                                ref VehicleCurrentDataComponent vehicleCurrentData,
                                ref Translation translation,
                                ref Rotation rotation)
            {
                var speed = vehicleCurrentData.Speed;
                var laneSpeed = LaneComponentData[vehicleLaneComponent.LaneEntity].Speed;

                if (math.abs(laneSpeed - speed) > 0.01)
                {
                    // accelerate to target speed
                    var acceleration = speed <= math.min(laneSpeed, vehicleStats.MaxSpeed)
                        ? vehicleStats.Acceleration
                        : -vehicleStats.Deceleration;
                    vehicleCurrentData.Speed += deltaTime * acceleration;
                }

                var distance = vehicleCurrentData.Speed * deltaTime;

                var currentIndex = vehicleCurrentData.CurrentPositionIndex;

                var lanePointsBuffer = VehicleLaneComponentData[vehicleLaneComponent.LaneEntity];

                if ((currentIndex + 1) < lanePointsBuffer.Length)
                {
                    var nextPoint = lanePointsBuffer[currentIndex + 1].value;
                    var distanceToNextPoint = math.distancesq(translation.Value, nextPoint);

                    float distanceSq = distance * distance;
                    if (distanceSq > distanceToNextPoint)
                    {
                        vehicleCurrentData.CurrentPositionIndex++;
                        nextPoint = lanePointsBuffer[currentIndex + 1].value;
                    }

                    var currentPoint = lanePointsBuffer[currentIndex].value;
                    var direction = math.normalizesafe(nextPoint - currentPoint);

                    translation.Value += direction * distance;
                    rotation.Value = quaternion.LookRotationSafe(direction, math.up());
                }
                else
                {
                    // Let interested systems know we need the next lane
                    CommandBuffer.AddComponent(index, entity, typeof(NeedsNextLaneTag));
                }
            }
        }

        protected override void OnCreate()
        {
            // Cache in a field, so we don't have to create every frame
            _entityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
        }

        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            var lanePointsComponent = GetBufferFromEntity<LanePointsComponent>(true);
            var laneComponent = GetComponentDataFromEntity<LaneComponent>(true);

            var autoNavigationJob = new AutoNavigationJob
            {
                deltaTime = Time.DeltaTime,
                CommandBuffer = _entityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
                VehicleLaneComponentData = lanePointsComponent,
                LaneComponentData = laneComponent,
            }.Schedule(this, inputDeps);

            _entityCommandBufferSystem.AddJobHandleForProducer(autoNavigationJob);

            return autoNavigationJob;
        }
    }
 
    public struct VehicleLaneComponent : IComponentData
    {
        public Entity LaneEntity;
    }
}

The issue appears to be in my else clause where I try to add a tag component to my entity (lines 75-79 from original post):

else
{
    // Let interested systems know we need the next lane
    CommandBuffer.AddComponent(index, entity, typeof(NeedsNextLaneTag));
}

After that code executes, vehicleLaneComponent.LaneEntity is Entity.Null, and the entity does not have the NeedsNextLaneTag component associated with it (according to the entity debugger view).

Is there some issue with the AddComponent call invalidating the entity’s associated data?

Okay. Figured this one out. There were 3 things going on:

  • The debugger doesn’t seem to display data for that last time a system ran, but is instead displaying the data from the current frame. Or something. I think. So my AddNextLaneSystem was in fact running, and setting my lane entity to Null if it did not have an exit lane.
  • I had written another system to remove the NeedsNextLaneTag component. That was configured to run after the AddNextLaneSystem, and was working perfectly after the first time I wrote it (not something I’m used to with ECS). Enough time had passed between when I created it and when I figured out that there was a problem with the exit condition that I completely forgot about it. It didn’t help that it was showing up in the entity debugger at all.
  • I had an error in my code that generated the graph of lanes, where it was not connecting lanes together correctly so lanes that lanes that were visually connected were not in fact connected in the ECS data structure.

I just had to fix #3 and things started working. Mostly. I’ve got another issue with the positioning of my entities after they cross from one lane to the next, but I’ll save that for another post if I can’t figure it out.