Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Adding multiple items to DynamicBuffer causing Structural Changes error?

Discussion in 'Entity Component System' started by jwvanderbeck, Jun 14, 2020.

  1. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    Not sure what I'm doing wrong here but i'm 100% certain its my fault. Can someone help me understand?

    I am creating a bunch of new entities and adding them into a DynamicBuffer on an already existing Entity. But only the first item gets added, and on the second it throws an exception saying:

    InvalidOperationException: Attempted to access ArchetypeChunkBufferType<EconSim.ItemReferenceElement> which has been invalidated by a structural change.

    Code (CSharp):
    1.        
    2.         [Button]
    3.         public List<Entity> CreateItemsInContainer(int itemID, Entity container, int itemCount)
    4.         {
    5.             if (itemID >= items.Count) return null;
    6.             if (container.Equals(Entity.Null)) return null;
    7.             if (!entityManager.Exists(container)) return null;
    8.             if (!entityManager.HasComponent<VolumeCapacityData>(container)) return null;
    9.            
    10.             var itemDefinition = items[itemID];
    11.             var containerVolume = entityManager.GetComponentData<VolumeCapacityData>(container).value;
    12.             var totalItemVolume = itemDefinition.shippingVolume * itemCount;
    13.          
    14.             if (totalItemVolume > containerVolume) return null;
    15.  
    16.             var createdEntities = new List<Entity>(itemCount);
    17.             var itemBuffer = entityManager.GetBuffer<ItemReferenceElement>(container);
    18.  
    19.             for (var i = 0; i < itemCount; i++)
    20.             {
    21.                 var entity = entityManager.CreateEntity(itemArchetype);
    22.                 entityManager.AddComponentData(entity, new ItemIDData { value = itemID });
    23.                 entityManager.AddComponentData(entity, new ItemPhysicalData { currentDurability = itemDefinition.durability });
    24.                 entityManager.AddComponentData(entity, new ItemCargoData
    25.                 {
    26.                     cargoClass = itemDefinition.cargoClass,
    27.                     massOfShippingVolume =  itemDefinition.massOfShippingVolume,
    28.                     shippingVolume = itemDefinition.shippingVolume,
    29.                 });
    30.                 entityManager.AddComponentData(entity, new ContainerReferenceData { value = container });
    31.                
    32.                 createdEntities.Add(entity);
    33.                 itemBuffer.Add(entity);
    34.             }
    35.            
    36.             // Update the container's remaining volume
    37.             entityManager.SetComponentData(container, new VolumeCapacityData { value = containerVolume - totalItemVolume });
    38.            
    39.             return createdEntities;
    40.         }
    41.  
     
  2. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    If I change my code to get the buffer each time before adding, it seems to work but that feels horribly inefficient. Is that really neccesary?
     
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    Rukhanka likes this.
  4. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    If adding/removing items from a buffer is going to cause a sync point with with every change then I'm very concerned that using them will be incredibly inefficient.

    EDIT: Also I didn't realize sync points even existed when doing standard main thread code in the first place (IE this isn't a job, just a normal function on a GO), so clearly I understand even less than I thought :(
     
  5. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    Adding to the buffer is not causing the sync point.

    Using EntityManager is causing the sync point (AddComponentData, CreateEntity) which invalidates the buffer.
     
  6. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    The buffer is on a different entity than the one that is being modified with EntityManager though. Modifying one entity invalidates all the others?

    Hmm.

    How do I get an ECB outside of a Job? The documentation only shows getting one inside of a Job/System.
     
  7. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    Ok I just refactored things to do all the adds after everything else is done.

    I will look more into the ECBs but up until now I thought those were only neccesary inside Jobs/Systems.

    Code (CSharp):
    1.             var itemBuffer = entityManager.GetBuffer<ItemReferenceElement>(container);
    2.             foreach (var createdEntity in createdEntities)
    3.             {
    4.                 itemBuffer.Add(createdEntity);
    5.             }
    6.  
     
  8. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    Been searching for a bit now and I can't find anything about using ECB/ECBSystems outside of a Job.

    The example in the docs on using ECB within a job, at the end passes the job to the ECBSystem:

    m_EndSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);

    So I am unsure of what to do if I don't have a job. Do I need to do something else to register my additions so that they get run later?
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,626
    You can just create an ecb and play it back yourself.

    var ecb= new EntityCommandBuffer(Allocator.TempJob);

    ecb.AddCompoent(entity, new Component());
    ecb.AddCompoent(entity2, new Component());
    ecb.AddCompoent(entity3, new Component());

    ecb.PlayBack(EntityManager);
    ecb.Dispose();

    this way you can make a bunch of changes without invalidating your buffer.

    any structural change call to EM will invalidate everything.
     
  10. jwvanderbeck

    jwvanderbeck

    Joined:
    Dec 4, 2014
    Posts:
    820
    Thank you very much for that.

    I've reworked things to use an ECB. Took a bit to figure out the buffer handling, but the key was to set a NEW buffer through the ECB and then add to that.

    I'm not sure how I would handle this if I needed to append to an existing one though, but in this case I don't.

    One problem I do have though is this method is supposed to return a List<Entity> of the newly created entities, which it can't do because the ECB creates them, and if I try to store the references the indices are all invalid.

    This isn't KEY, and I can currently do without that List but again not sure how I would handle it if I needed it.

    This is what I ended up with.

    Code (CSharp):
    1.  
    2. var ecb = new EntityCommandBuffer(Allocator.Temp);
    3. var itemBuffer = ecb.SetBuffer<ItemReferenceElement>(container);
    4.  
    5.  
    6. for (var i = 0; i < itemCount; i++)
    7. {
    8.     // create the new item entity
    9.     var entity = ecb.CreateEntity(itemArchetype);
    10.     ecb.AddComponent(entity, new ItemIDData { value = itemID });
    11.     ecb.AddComponent(entity, new ItemIDData { value = itemID });
    12.     ecb.AddComponent(entity, new ItemPhysicalData { currentDurability = itemDefinition.durability });
    13.     ecb.AddComponent(entity, new ItemCargoData
    14.     {
    15.         cargoClass = itemDefinition.cargoClass,
    16.         massOfShippingVolume =  itemDefinition.massOfShippingVolume,
    17.         shippingVolume = itemDefinition.shippingVolume,
    18.     });
    19.     ecb.AddComponent(entity, new ContainerReferenceData { value = container });
    20.    
    21.     // Add the item to the return array as well as the container's buffer
    22.     createdEntities.Add(entity);
    23.     itemBuffer.Add(entity);
    24. }
    25.  
    26. // Update the container's remaining volume
    27. ecb.SetComponent(container, new VolumeCapacityData { value = containerVolume - totalItemVolume });
    28. ecb.Playback(entityManager);
    29. ecb.Dispose();
    30.  
     
    Elapotp, Mikael-H and Atheos001 like this.
  11. sietse85

    sietse85

    Joined:
    Feb 22, 2019
    Posts:
    99
    if only i could add FrozenRenderSceneTag with ecb