Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

A Beginners approach to Entity Systems with Astronomical Physics

Discussion in 'Data Oriented Technology Stack' started by Held0fTheWelt, Feb 5, 2019.

  1. Held0fTheWelt

    Held0fTheWelt

    Joined:
    Sep 29, 2015
    Posts:
    173
    Lately i am trying out a lot with the new Entity Component System, as many of you do.
    In my Background this is absolutely new to me, i understand a lot about the concepts, and i think, i understand the most, that is thrown upon me on the internet and youtube - especially from Unite - that is shown to me.

    But i don't really know much of all the use cases and scenarios, you would want to use, if you want to write new code.

    Lately - i began, thinking about Newtonial Gravitation, which looks like a perfect scenario for entities and the ecs, so i tried to start.

    This thread is for both:

    Showing new and old ones, how i approach this Scenario and second, asking about things, where i now get stuck.

    I will use this entry post as edit area, and will extend it with usage of this thread.

    ------------------------------------------------------------------------------------------------------------------

    First: The bit of math for those ones, who are not aware of, what i am trying now.

    Newtons Law of motion says, that a forced vector F is equal to a mass and its acceleration vector:
    F = m * A
    Newtons Law of Gravitation says, the force acting is equal to the gravitational constant G multiplied by the masses of two bodies, divided by the square of their masses center.
    F = G * m1 * m2 / (d*d)
    This is enough to begin, writing the first components and systems.

    I know, that i can stick those two together and make up a smaller calculation later, but for clearance of doing this, i first decided to keep those two terms untouched, and begin with the Law of Motion.

    As i want to have collisions later, i preferred to use a hybrid approach first, adding a rigidbody with no gravity setup to a simple sphered object, and i wrote two components, one for storing the value of mass, second storing a float3 for acceleration.

    After scene load, i update the rigidbodies mass to match the mass set up in the component:

    Code (CSharp):
    1. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    2.     static void Init()
    3.     {
    4.         Rigidbody[] gravityObjects = GameObject.FindObjectsOfType<Rigidbody>();
    5.  
    6.         for (int i = 0; i < gravityObjects.Length; i++)
    7.             gravityObjects[i].mass = gravityObjects[i].GetComponent<MassComponent>().Value.Value;
    8.            gravityObjects[i].AddRelativeForce(gravityObjects[i].GetComponent<VelocityComponent>().Value.Value);
    9.     }
    With this, i could write a simple hybrid component system, that can now move my sphere with the given values:

    Code (CSharp):
    1. public class AccelerateSystem : ComponentSystem
    2. {
    3.     unsafe public struct AccelerationGroup
    4.     {
    5.         public Rigidbody rigidbody;
    6.         public Acceleration* acceleration;
    7.     }
    8.  
    9.     unsafe protected override void OnUpdate()
    10.     {
    11.         var entities = GetEntities<AccelerationGroup>();
    12.         for (int i = 0; i < entities.Length; i++)
    13.         {
    14.             var entity = entities[i];
    15.             entity.rigidbody.AddForce(entity.acceleration->a * Time.deltaTime);
    16.         }
    17.     }
    18. }
    19.  
    Lately i found out, that in many cases, the compiler wants me to use pointers.
    I don't really know much about the why, but i follow, and everything is working again.
    I don't really know, if that's a good approach, but i get it working that way.

    Now i had to introduce a Force to change that acceleration.
    I called it the NetForce, because its value is a force value, calculated out of a net of forces, acting onto this body.

    This looks quite easy too:

    Code (CSharp):
    1. public class AccelerationSystem : JobComponentSystem
    2. {
    3.     #region BurstCompile AccelerationCalculationJob
    4.     [BurstCompile]
    5.     struct AccelerationCalculationJob : IJobProcessComponentData<Acceleration, Mass, NetForce>
    6.     {
    7.         public void Execute(ref Acceleration acceleration, [ReadOnly]ref Mass mass, [ReadOnly]ref NetForce netForce)
    8.         {
    9.             acceleration.a = netForce.F / mass.Value;
    10.         }
    11.     }
    12.     #endregion
    13.  
    14.     #region JobHandle OnUpdate
    15.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    16.     {
    17.         var job = new AccelerationCalculationJob();
    18.         return job.Schedule(this, inputDeps);
    19.     }
    20.     #endregion
    21. }
    You see:
    I can now write Burst Code very easy, without too much trouble !
    Maybe i should learn a little bit more about the BurstCompilerFlags in future, to see, if i can optimize this, but it doesn't look too bad, as i think.
    Couldn't be very much easier, if i wouldn't try to concatenate things here, to maybe do them in a row, but logically, if i think about that, what the laws mean to do, i needn't, or better say: it would be false to do that and don't even match reality.
    They can just calculate around as they do, that's fine ...

    Now i am coming to my first real greater Job ...

    I have multiple of these objects flying around, all acting on each other.
    If i would say, i have a sun, an earth and mars, the netforce vector for my earth would be the addition of the calculated forcevector with the sun and the force vector with mars.
    So now, i need a possibility to compare entities with each other.
    1. Step here is to make a job for each entity to loop over all entities, that are inside the scene and calculate out a netforce vector out of all forces alltogether, but i don't only write that code, i also saw very early that i should try to decrease the count of calculation by using some logics...

    Coming back to my example, having sun, earth and mars, the forces acting onto two bodies are opposite with each other !
    That leads to a nice result by reducing the needed amount of calculation by 2.
    I also don't need a stored reference to the object itself, so as it would look first, that i would need at least 6 calculations to do that, i am ending up by 3.

    Example:
    Calculating Sun:
    With Sun - not needed
    With Earth - calculate force
    With Mars - calculate force
    Calculating Earth:
    With Sun - negative to sun
    With Earth - not needed
    With Mars - calculate force
    Calculate Mars:
    With Sun - negative to sun
    With Earth - negative to earth
    With Mars - not needed

    This is now, where i am stuck ...

    I first don't really know on how i would setup a job for this...
    Do i use a NativeArray ? Are there good examples, to see usage more easy ?
    Second is: How would i know, if a value is already calculated ? I could wear a flag, or something like this.
    I am thinking on store the session each frame in something like a NetForceCalculatorEntity, so i can put the calculated values somewhere and just run through the job for each entity.

    ------------------------------------------------------------------------------------------------------------------

    It would be a great help, if there are encouraged people, helping me with this bit of example, as it has several things and tries, that would help me to understand a lot for future implementations.

    As told before, i will continue adding noticable result and code to the section above !

    Thank you for reading !
     
    Last edited: Feb 8, 2019
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    5,166
    It is relatively straight forward.
    There is of course few way to do so.
    But providing each celestial body is an entity with position, then you need add component (s), with mass, and velocity vector.

    Then you could for example add component tags like, gravitational pull and apply gravity.
    Meaning, any celestial body with this gravitational pull component data, will pull other celestial bodies with apply gravity component data.

    Now you could use IJobProcessComponentData (WithEntity), but for simplicity, I here could suggest basics single threaded IJob.

    You then may need two groups in same system, one with gravitational pull component data and one with apply gravity component data. System of course will execute, if at least one component exists, in any group, so need to be aware.

    But then simply iterate through all entities, with apply gravity component data, and then nested for loop, with entities with gravitational pull component data. You sum results of nested loop, and apply new force, to entity (celestial body) from main loop.

    And that should be it.

    Of course you can do it in parallel etc., if you like.
     
  3. Held0fTheWelt

    Held0fTheWelt

    Joined:
    Sep 29, 2015
    Posts:
    173
    With the (final) formula, any body is pulling on any body, more or less.
    I want to split it later into physics systems, so i can a little bit decide, who is acting with each other.
    I get along with moon only be influenced by sun and earth and maybe mars, we'll see, i don't think so.
    So this will shorten down the calculation rate.
     
  4. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    5,166
    Not sure if that will be any use, or even necessary in your case, but you can always use separate worlds, to keep physics completely separate.

    Float3 length squared should be in math library, if I am correct.
     
    Last edited: Feb 6, 2019
    Held0fTheWelt likes this.
  5. Held0fTheWelt

    Held0fTheWelt

    Joined:
    Sep 29, 2015
    Posts:
    173
    First:
    This looks way better, yes

    Force = G * entityA.Mass->Value * entityB.Mass->Value / math.lengthsq(distance)
     
  6. Held0fTheWelt

    Held0fTheWelt

    Joined:
    Sep 29, 2015
    Posts:
    173
    So now i only come up with a ComponentSystem.
    Since here, i am not able to see, how to write jobs out of it, which should be the next step.

    Code (CSharp):
    1.  
    2.     public class NetForceCalculatorSystem : ComponentSystem
    3.     {
    4.         #region unsafe struct NetForceGroup
    5.         /// <summary>
    6.         /// A NetForceGroup is defined by a NetForce value, a mass and a position
    7.         /// </summary>
    8.         unsafe public struct NetForceGroup
    9.         {
    10.             public NetForce* NetForce;
    11.             public Mass* Mass;
    12.             public Position* Position;
    13.         }
    14.         #endregion
    15.  
    16.         /// <summary>
    17.         /// Value for gravitational constant gamma, short "big" G
    18.         /// </summary>
    19.         public const float G = 6.67408E-11f;
    20.  
    21.         #region OnUpdate
    22.         unsafe protected override void OnUpdate()
    23.         {
    24.             var entities = GetEntities<NetForceGroup>();
    25.  
    26.             // Cache the length, because we use it very often in here
    27.             int entitiesLength = entities.Length;
    28.  
    29.             // Open an array with enough space to handle value pairs of O(n) work
    30.             float3[] gravityValuePairs = new float3[GetLengthForArray(entitiesLength-1)];
    31.  
    32.             // don't forget to count this !
    33.             int count = 0;
    34.  
    35.             // loop for every entity
    36.             for (int i = 0; i < entitiesLength; i++)
    37.             {
    38.                 var entityA = entities[i];
    39.                 // If there are upper entities
    40.                 if (i + 1 <= entitiesLength)
    41.                 {
    42.                     // loop for every entity, that is upper this entity in the array
    43.                     for (int j = i + 1; j < entitiesLength; j++)
    44.                     {
    45.                         // Newtons Law of Motion says
    46.                         // F = G * m1 * m2 / (distance between m1&m2 squared)
    47.                         var entityB = entities[j];
    48.                         // calculate the distance
    49.                         float3 distance = entityA.Position->Value - entityB.Position->Value;
    50.                         // calculate the gravitational force for this pair of entities
    51.                         float force = G * entityA.Mass->Value * entityB.Mass->Value / math.lengthsq(distance);
    52.                         // remember the current force by its normalized distance value
    53.                         gravityValuePairs[count] = new float3( math.normalizesafe(distance) * force);
    54.                         // and don't forget to count this array !!!
    55.                         count++;
    56.                     }
    57.                 }
    58.             }
    59.  
    60.             // we reuse this int counter, but it means sth. different now
    61.             count = 0;
    62.  
    63.             // Loop over every entity
    64.             for (int i = 0; i < entitiesLength; i++)
    65.             {
    66.                 // get a cache value for the current NetForceValue
    67.                 float3 NetForceValue = new float3();
    68.  
    69.                 // we have to access the array two side, one for adding values, one for subtracting values.
    70.                 // the count is directly related to the way of filling the array, so both
    71.                 // are highly dependend on each other
    72.            
    73.                 // Problem:
    74.            
    75.                 //      0 | 1 | 2 | 3 | 4 | 5 | 6       0 | 1 | 2 | 3 | 4 | 5 | 6
    76.                 // ------------------------------   ------------------------------
    77.                 //  0 | X | X | X | X | X | X | X   0 |
    78.                 //  1 | 0 | X | X | X | X | X | X   1 | 0
    79.                 //  2 | 1 | 6 | X | X | X | X | X   2 | 1 | 6
    80.                 //  3 | 2 | 7 | 11| X | X | X | X   3 | 2 | 7 | 11
    81.                 //  4 | 3 | 8 | 12| 15| X | X | X   4 | 3 | 8 | 12| 15
    82.                 //  5 | 4 | 9 | 13| 16| 18| X | X   5 | 4 | 9 | 13| 16| 18
    83.                 //  6 | 5 | 10| 14| 17| 19| 20| X   6 | 5 | 10| 14| 17| 19| 20
    84.                 //
    85.            
    86.                 // Arraysolution:
    87.  
    88.                 // 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |  1 | 2  |  2 |  2 |  2 |  3 |  3 |  3 |  4 |  4 |  5
    89.                 // 1 | 2 | 3 | 4 | 5 | 6 | 2 | 3 | 4 | 5 |  6 | 3  |  4 |  5 |  6 |  4 |  5 |  6 |  5 |  6 |  6
    90.                 //---------------------------------------------------------------------------------------------
    91.                 // 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20
    92.  
    93.                 // Upper part
    94.  
    95.                 // the length in upper part is defined by the length of entities - 1 - currentEntityIndex
    96.                 int countFirst = entitiesLength - i - 1;
    97.  
    98.                 for (int j = 0; j < countFirst; j++)
    99.                 {
    100.                     // we count this, so it is more easy to cycle through here
    101.                     NetForceValue -= gravityValuePairs[count];
    102.                     count++;
    103.                 }
    104.                 // Second count is equal to i
    105.                 int countSecond = i;
    106.  
    107.                 for (int j = 0; j < i; j++)
    108.                 {
    109.                     // the index value is more complex, but easy to calculate
    110.                     // we reduce our current index by 1 and add a value, defined by sequence GetPositionInArray
    111.                     NetForceValue += gravityValuePairs[(countSecond - 1) + GetPositionInArray(j, entitiesLength - 2)];
    112.                 }
    113.  
    114.                 // Set this Value the current entities netforce
    115.                 entities[i].NetForce->F = NetForceValue;
    116.             }
    117.         }
    118.         #endregion
    119.  
    120.         #region GetPositionInArray
    121.         /// <summary>
    122.         /// This method is capable to calculate out the next needed position in the defined array
    123.         /// </summary>
    124.         /// <param name="_count">how much calculations are done</param>
    125.         /// <param name="_entityLength">the value to calculate with is originally entityLength -2, but we do this in the function call!</param>
    126.         /// <returns></returns>
    127.         private int GetPositionInArray(int _count, int _entityLength)
    128.         {
    129.             // Setup a return value
    130.             int returnValue = 0;
    131.             // As long as i is smaller our count
    132.             for (int i = 0; i < _count; i++)
    133.             {
    134.                 // add the current length value
    135.                 returnValue += _entityLength;
    136.                 // and reduce it by one for another run
    137.                 _entityLength--;
    138.             }
    139.             // return this value
    140.             return returnValue;
    141.         }
    142.         #endregion
    143.  
    144.         #region GetLengthForArray
    145.         /// <summary>
    146.         /// Calculates the current value for the length of the force value pairs array
    147.         /// This is defined by the algorithm of it's length of entities
    148.         /// We subtract -1 in the method call before !!!
    149.         /// F.e.:
    150.         /// Length = 7
    151.         /// Length of Array = 6+5+4+3+2+1 = 21
    152.         /// </summary>
    153.         /// <param name="_numberOfEntities">Number of entities</param>
    154.         /// <returns>Needed length for current force value pairs array</returns>
    155.         private int GetLengthForArray(int _numberOfEntities)
    156.         {
    157.             // return value
    158.             int count = 0;
    159.  
    160.             // As described in method description, add i as it is smaller than _numberOfEntities;
    161.             for (int i = _numberOfEntities; i > 0; i--)
    162.             {
    163.                 count += i;
    164.             }
    165.             return count;
    166.         }
    167.         #endregion
    168.     }
    169.  
    170.  
    What i do here is, setting up a one-dimensional Array, that is capable to been read out without knowing any index.
    For one Force, this Value is positive, for another it is negative, and the way it is filled, it can be read out.

    I had to figure a bit about this, writing a small cheat sheet, i still think of, if it fits, but it looks, like it rotates my planet and moon fine, and next i will see, how it fits to more Objects.

    Anyway:

    How do i write a job system out of this now ?
     
    Last edited: Feb 8, 2019
  7. Held0fTheWelt

    Held0fTheWelt

    Joined:
    Sep 29, 2015
    Posts:
    173
    So this is now working and should give away a 2 job system, maybe 3, if i split down the lower section, where i read out the array.

    But i don't know yet, how to do that ^^

    Btw. It's really rotating quite fine ! ;)

    Last to do is make a formula, that is capable, to precalculate the given orbit.



     
    Last edited: Feb 9, 2019
  8. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    901
    I will post an example with jobs when I get home. If I understand you right you want to calculate force pairs and combined force per entity
     
    Held0fTheWelt likes this.
  9. Held0fTheWelt

    Held0fTheWelt

    Joined:
    Sep 29, 2015
    Posts:
    173
    Yup ! That's what happens
     
  10. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    901
    here is the example
    - I did not validate the calculation, but it should be ok, unless I swapped an index somewhere
    - I did not build on your code, but my general understanding of what you want to do (I don't like to read code)

    edit: after posting, I noted that I left a calculation in the loop (i* Length) --- moved this out of the loop in second job, same could be done for first job.

    Code (CSharp):
    1.  
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6. using Unity.Transforms;
    7. using Unity.Mathematics;
    8.  
    9.  
    10. namespace GravitationForceExample
    11. {
    12.     public struct PlanetMass: IComponentData
    13.     {
    14.         public float Value;
    15.     }
    16.    
    17.     public struct GravitationForce: IComponentData
    18.     {
    19.         public float Value;
    20.     }
    21.    
    22.     public class GravitationForceJobSystem : JobComponentSystem
    23.     {
    24.         public ComponentGroup planet_Group;
    25.        
    26.         const float G = 6.67408E-11f;
    27.        
    28.         [BurstCompile]
    29.         struct CalcualteGravitationPairsJob : IJobParallelFor
    30.         {
    31.             [ReadOnly] public float G;
    32.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<Position> PlanetPositions;
    33.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<PlanetMass> PlanetMasses;
    34.             [ReadOnly] public int Length;
    35.             [NativeDisableParallelForRestriction] [WriteOnly] public NativeArray<float> GravitationPairs;
    36.            
    37.             public void Execute(int i)
    38.             {
    39.                 for (int j = i+1; j < Length; j++)
    40.                 {
    41.                     var gf = G * PlanetMasses[i].Value * PlanetMasses[j].Value / math.lengthsq(PlanetPositions[i].Value - PlanetPositions[j].Value);
    42.                     GravitationPairs[j + i * Length] = gf;
    43.                     GravitationPairs[i + j * Length] = -gf;
    44.                 }
    45.             }
    46.         }
    47.        
    48.         [BurstCompile]
    49.         struct ApplyGravitationForceJob : IJobProcessComponentDataWithEntity<GravitationForce>
    50.         {
    51.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float> GravitationPairs;
    52.             [ReadOnly] public int Length;
    53.            
    54.             public void Execute(Entity e, int i, [WriteOnly] ref GravitationForce gravitationForce)
    55.             {
    56.                 float gf = 0;
    57.                 int offset = i * Length;
    58.                 for (int j = 0; j < Length; j++)
    59.                 {
    60.                     gf += GravitationPairs[j + offset];
    61.                 }
    62.                 gravitationForce.Value = gf;
    63.             }
    64.         }
    65.  
    66.         protected override void OnCreateManager()
    67.         {
    68.             SetupPlanets();
    69.         }
    70.    
    71.         protected override JobHandle OnUpdate(JobHandle inputDependencies)
    72.         {
    73.             var length = planet_Group.CalculateLength();
    74.             var planetPositions = planet_Group.ToComponentDataArray<Position>(Allocator.TempJob);    // I think this is the replacement for GetComponentDataArray
    75.             var planetMasses = planet_Group.ToComponentDataArray<PlanetMass>(Allocator.TempJob);    // and I think it is slower, as it creates a copy instead of moving a pointer
    76.             // for simplicity, I stored the positive & negative values, i.e. I wasted some space vs. length * (length - 1) / 2
    77.             var gravitationPairs = new NativeArray<float>(length * length, Allocator.TempJob);
    78.  
    79.             inputDependencies = new CalcualteGravitationPairsJob
    80.             {
    81.                 G                        = G,
    82.                 PlanetPositions            = planetPositions,
    83.                 PlanetMasses            = planetMasses,
    84.                 Length                    = length,
    85.                 GravitationPairs        = gravitationPairs
    86.             }.Schedule(planet_Group.CalculateLength(), 32, inputDependencies);
    87.  
    88.             inputDependencies = new ApplyGravitationForceJob
    89.             {
    90.                 GravitationPairs        = gravitationPairs,
    91.                 Length                    = length
    92.             }.Schedule(this, inputDependencies);
    93.        
    94.             return inputDependencies;
    95.         }
    96.        
    97.         void SetupPlanets ()
    98.         {
    99.             var initPositions = new Position[]
    100.             {
    101.                 new Position {Value = new float3 (0,0,0)},
    102.                 new Position {Value = new float3 (-1,-1,0)},
    103.                 new Position {Value = new float3 (-1,1,0)},
    104.                 new Position {Value = new float3 (1,1,0)},
    105.                 new Position {Value = new float3 (1,-1,0)}
    106.             };
    107.            
    108.             var initMass = new PlanetMass[]
    109.             {
    110.                 new PlanetMass {Value = 2f},
    111.                 new PlanetMass {Value = 1f},
    112.                 new PlanetMass {Value = 1f},
    113.                 new PlanetMass {Value = 1f},
    114.                 new PlanetMass {Value = 1f}
    115.             };
    116.            
    117.             if (initPositions.Length != initMass.Length) return;
    118.            
    119.             var planetComponentTypes = new ComponentType[]{ComponentType.Create<Position>(), ComponentType.Create<PlanetMass>(), ComponentType.Create<GravitationForce>()};
    120.             planet_Group = GetComponentGroup(planetComponentTypes);
    121.            
    122.             for (int i = 0; i < initPositions.Length; i++)
    123.             {
    124.                 var e = EntityManager.CreateEntity(planetComponentTypes);
    125.                 EntityManager.SetComponentData<Position>(e, initPositions[i]);
    126.                 EntityManager.SetComponentData<PlanetMass>(e, initMass[i]);
    127.             }
    128.         }
    129.        
    130.     }
    131. }
    132.  
    133.  
     
    Last edited: Feb 9, 2019
    Held0fTheWelt likes this.
  11. Held0fTheWelt

    Held0fTheWelt

    Joined:
    Sep 29, 2015
    Posts:
    173
    I will study this !
    Thank you !