# A Beginners approach to Entity Systems with Astronomical Physics

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

1. ### 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.

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):
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;
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];
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 !

Last edited: Feb 8, 2019
2. ### 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

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

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

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

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

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

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

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

10. ### 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.         {
32.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<Position> PlanetPositions;
33.             [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<PlanetMass> PlanetMasses;
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;
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

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