Search Unity

ECS Pattern for accessing large unchanging arrays?

Discussion in 'Entity Component System' started by pakfront, May 30, 2019.

  1. pakfront

    pakfront

    Joined:
    Oct 6, 2010
    Posts:
    551
    I want a large, parallel-readable table of float3s, FormationOffsets. It is built at runtime game launch, and then not modified.

    I have many (low thousands) entities with Components that will access the single FormationOffsets array from Jobs:
    Code (CSharp):
    1.     public struct FormationMember : IComponentData
    2.     {
    3.         public int Index;
    4.         public Entity Parent;
    5.         public float3 Position;
    6.     }
    Index will change infrequently, at most a few times a minute, but usually only once every few minutes. When Index changes, I will build a parallel job to
    Position = FormationOffsets[Index] + SomeValue;


    What is the ECS way to store and access a 'static-like' or singleton array? Looking through docs, nothing seemed obvious.
    My best guess was a SingletonDataComponent with a DynamicBuffer. Is that correct? If so:
    in OnCreate() How do I associate the Buffer with a Singleton? SetSingleton only takes IComponentData.

    During Update, how do I access the DynamicBuffer? GetSingleton is also IComponentData only.
    Assuming I get that figured out, Is it best to GetNativeArray in the System OnUpdate and pass that NativeRaay to the job struct? Or should I keep it as DynamicBuffer all the way through?

    Other patterns are welcome.

    Thanks.
     
  2. pakfront

    pakfront

    Joined:
    Oct 6, 2010
    Posts:
    551
    I think I got the creation part figured - you have to create a 'tag' IComponentData, then add the buffer to that entity. In this case I created an empty tag called FormationSingleton
    Still working through the rest, and if this is the wrong approach, please let me know.
     
    Last edited: May 31, 2019
  3. pakfront

    pakfront

    Joined:
    Oct 6, 2010
    Posts:
    551
    Ok, got it working as a DynamicBuffer on a Singleton:
    Code (CSharp):
    1.     [Serializable] public struct FormationSingleton : IComponentData { }
    2.  
    3.     [InternalBufferCapacity(8)]
    4.     public struct FormationOffsets : IBufferElementData
    5.     {
    6.         // These implicit conversions are optional, but can help reduce typing.
    7.         public static implicit operator float3(FormationOffsets e) { return e.Value; }
    8.         public static implicit operator FormationOffsets(float3 e) { return new FormationOffsets { Value = e }; }
    9.         // Actual value each buffer element will store.
    10.         public float3 Value;
    11.     }
    12.  
    13.     public class UnitFormationSystem : JobComponentSystem
    14.     {
    15.  
    16.         protected override void OnCreate() {
    17.             var formationSingletonEntity = EntityManager.CreateEntity(typeof(FormationSingleton));
    18.             EntityManager.SetName(formationSingletonEntity, "FormationSingleton");
    19.             SetSingleton(new FormationSingleton{ });
    20.  
    21.             EntityManager.AddBuffer<FormationOffsets>(formationSingletonEntity);
    22.             var buffer = EntityManager.GetBuffer<FormationOffsets>(formationSingletonEntity);
    23.             for (int i = 0; i < 8; i++)
    24.             {
    25.                 // temp data for testing
    26.                 buffer.Add(new float3(i*20,0,i*100));
    27.             }
    28.         }
    29. ...
    30.         [BurstCompile]
    31.         [RequireComponentTag(typeof(Unit))]
    32.         struct SetOffsetJob : IJobForEach<FormationMember>
    33.         {
    34.             [ReadOnly] public DynamicBuffer<FormationOffsets> Offsets;
    35.             public void Execute(ref FormationMember formationElement)
    36.             {
    37.                 formationElement.Position = Offsets[formationElement.Index];
    38.             }
    39.         }
    40.  
    41.         protected override JobHandle OnUpdate(JobHandle inputDependencies)
    42.         {
    43.  
    44.             var formationSingletonEntity = GetSingletonEntity<FormationSingleton>();
    45.             // this also works, but may not have readonly flag:
    46.             // var buffer = EntityManager.GetBuffer<FormationOffsets>(formationSingletonEntity);
    47.  
    48.             var lookup = GetBufferFromEntity<FormationOffsets>(true);
    49.             var buffer = lookup[formationSingletonEntity];
    50.          
    51.             var outputDeps = new SetOffsetJob()
    52.             {
    53.                 Offsets = buffer
    54.             }.Schedule(this, inputDependencies);
    55.  
    56.  
    57.             return outputDeps;
    58.         }
    59.     }
    60. }
     
  4. TLRMatthew

    TLRMatthew

    Joined:
    Apr 10, 2019
    Posts:
    65
    Is FormationOffsets required in multiple systems, or just in UnitFormationSystem? If it's only used by one System, why not simply store it as a member variable of the System? Create it in OnCreate and pass it into the job in OnUpdate.
     
  5. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Your requirement seems more like the use case of blob data. Vs. dynamic array is that the dynamic array you are hosting it on an entity (singleton) for everyone to use. But blob could be allocated once independent of any entity, and hand the reference to all entity's blob field on one of its component. This way each entity could access the data on their own in the job without knowing special object. Look BlobificationTests.cs to find BlobBuilder example.
     
    MooKeeGames likes this.
  6. Razmot

    Razmot

    Joined:
    Apr 27, 2013
    Posts:
    346
    Why dont you just "keep it stupid simple" and use a NativeArray<float3> ?

    Code (CSharp):
    1.  
    2. public class UnitFormationSystem : JobComponentSystem {
    3.  
    4. public NativeArray<float3> FormationOffsets;
    5.  
    6. void OnCreate() { FormationOffsets = new NativeArray<float3>(3000,Allocator.Persistant); }
    7. void OnDestroy{ FormationOffsets.Dispose(); }
    8.  
    9. protected override JobHandle OnUpdate(JobHandle inputDependencies)
    10. {
    11. var outputDeps =new  SetOffsetJob()  {
    12.     Offsets = FormationOffsets         }.Schedule(this, inputDependencies);
    13. return outputDeps;
    14. }
    15.  
    16.  
    17. [BurstCompile]
    18.         [RequireComponentTag(typeof(Unit))]
    19.         struct SetOffsetJob : IJobForEach<FormationMember>
    20.         {
    21.            [ReadOnly] NativeArray<float3> Offsets;
    22.             public void Execute(ref FormationMember formationElement)
    23.             {
    24.                 formationElement.Position = Offsets[formationElement.Index];
    25.             }
    26.         }
    27.  
    And when you need it in another System :
    Code (CSharp):
    1.  
    2. public class OherAwesomeSystem : JobComponentSystem {
    3. private NativeArray<float3> _formationOffsets;
    4.  
    5. void OnCreate() {
    6. _formationOffsets =  World.GetOrCreateSystem<UnitFormationSystem >().FormationOffsets;
    7. }
    8.  
    9. protected override JobHandle OnUpdate(JobHandle inDeps){
    10.         var j= new WhateverJob()     {
    11.               Offsets = _formationOffsets };
    12.  
    13. }
    14.  
    NativeArray is a "reference like" thing, you can pass it around, and the [Readonly] in your jobs is enough for optimizations + enforcing you dont write to it accidentally.

    I do that in my code extensively with NativeHashmaps, but they are not even readonly and everyting works fine :)
     
    pakfront likes this.
  7. pakfront

    pakfront

    Joined:
    Oct 6, 2010
    Posts:
    551
    Fantastic input, thanks all, and especially thanks for the example code.
    A member variable makes sense here - I was unsure of the safety of using NativeArray as a member variable for parallel access.
    I will look into Blobs as well - I've seen them mentioned as a storage method but don't know what their use case is. Why might blobs be better or worse than a member variable, assuming I will be accessing from multiple systems?
     
  8. psuong

    psuong

    Joined:
    Jun 11, 2014
    Posts:
    126
    Blobs can be shared amongst multiple systems and be used across multiple threads. You have the exact same case I have with an immutable array of float/float2/float3s. A member variable in a system would isolate the data to the system if that's what you need and if you need a secondary system you'll need to grab the system and then grab the member variable.

    All in all blobs are certainly flexible since you can guarantee their memory safety due to its immutable state.
     
  9. TLRMatthew

    TLRMatthew

    Joined:
    Apr 10, 2019
    Posts:
    65
    Yeah personally if something is being used over multiple systems I'd prefer to use BlobData or a Singleton component, so as not to have my systems too tightly coupled with each other.

    Of course, if the data changes each frame, then it makes sense to have the system that updates the data also own the data, as the coupling will be unavoidable.
     
  10. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,708
    Just wondering where can I find the BlobificationTests.cs example.
     
  11. BrendonSmuts

    BrendonSmuts

    Joined:
    Jun 12, 2017
    Posts:
    86
    The ECS system also has behavior that makes it “aware” of blob data referenced by components using BlobAssetReference. This means it can be tracked as part of the overall world state for things like serialization/deserialization/memory cleanup. Conversely variables that sit inside ComponentSystems are outside this ECS awareness and are left entirely up to you to ensure they are available to all code points that require them when needed and cleaned up when no longer used. Wether this is useful for your case is questionable but it is a difference.
     
    MintTree117 and pakfront like this.
  12. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    All installed packages source in your progect folder:
    Assets->Library->PackagesCache
     
  13. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,708
    Awesome thank you