A Beginners approach to Entity Systems with Astronomical Physics

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:

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    static void Init()
    {
        Rigidbody[] gravityObjects = GameObject.FindObjectsOfType<Rigidbody>();

        for (int i = 0; i < gravityObjects.Length; i++)
            gravityObjects[i].mass = gravityObjects[i].GetComponent<MassComponent>().Value.Value;
           gravityObjects[i].AddRelativeForce(gravityObjects[i].GetComponent<VelocityComponent>().Value.Value);
    }

With this, i could write a simple hybrid component system, that can now move my sphere with the given values:

public class AccelerateSystem : ComponentSystem
{
    unsafe public struct AccelerationGroup
    {
        public Rigidbody rigidbody;
        public Acceleration* acceleration;
    }

    unsafe protected override void OnUpdate()
    {
        var entities = GetEntities<AccelerationGroup>();
        for (int i = 0; i < entities.Length; i++)
        {
            var entity = entities[i];
            entity.rigidbody.AddForce(entity.acceleration->a * Time.deltaTime);
        }
    }
}

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:

public class AccelerationSystem : JobComponentSystem
{
    #region BurstCompile AccelerationCalculationJob
    [BurstCompile]
    struct AccelerationCalculationJob : IJobProcessComponentData<Acceleration, Mass, NetForce>
    {
        public void Execute(ref Acceleration acceleration, [ReadOnly]ref Mass mass, [ReadOnly]ref NetForce netForce)
        {
            acceleration.a = netForce.F / mass.Value;
        }
    }
    #endregion

    #region JobHandle OnUpdate
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new AccelerationCalculationJob();
        return job.Schedule(this, inputDeps);
    }
    #endregion
}

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 !

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.

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.

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.

1 Like

First:
This looks way better, yes

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

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.

    public class NetForceCalculatorSystem : ComponentSystem
    {
        #region unsafe struct NetForceGroup
        /// <summary>
        /// A NetForceGroup is defined by a NetForce value, a mass and a position
        /// </summary>
        unsafe public struct NetForceGroup
        {
            public NetForce* NetForce;
            public Mass* Mass;
            public Position* Position;
        }
        #endregion

        /// <summary>
        /// Value for gravitational constant gamma, short "big" G
        /// </summary>
        public const float G = 6.67408E-11f;

        #region OnUpdate
        unsafe protected override void OnUpdate()
        {
            var entities = GetEntities<NetForceGroup>();

            // Cache the length, because we use it very often in here
            int entitiesLength = entities.Length;

            // Open an array with enough space to handle value pairs of O(n) work
            float3[] gravityValuePairs = new float3[GetLengthForArray(entitiesLength-1)];

            // don't forget to count this !
            int count = 0;

            // loop for every entity
            for (int i = 0; i < entitiesLength; i++)
            {
                var entityA = entities[i];
                // If there are upper entities
                if (i + 1 <= entitiesLength)
                {
                    // loop for every entity, that is upper this entity in the array
                    for (int j = i + 1; j < entitiesLength; j++)
                    {
                        // Newtons Law of Motion says
                        // F = G * m1 * m2 / (distance between m1&m2 squared)
                        var entityB = entities[j];
                        // calculate the distance
                        float3 distance = entityA.Position->Value - entityB.Position->Value;
                        // calculate the gravitational force for this pair of entities
                        float force = G * entityA.Mass->Value * entityB.Mass->Value / math.lengthsq(distance);
                        // remember the current force by its normalized distance value
                        gravityValuePairs[count] = new float3( math.normalizesafe(distance) * force);
                        // and don't forget to count this array !!!
                        count++;
                    }
                }
            }

            // we reuse this int counter, but it means sth. different now
            count = 0;

            // Loop over every entity
            for (int i = 0; i < entitiesLength; i++)
            {
                // get a cache value for the current NetForceValue
                float3 NetForceValue = new float3();

                // we have to access the array two side, one for adding values, one for subtracting values.
                // the count is directly related to the way of filling the array, so both
                // are highly dependend on each other
            
                // Problem:
            
                //      0 | 1 | 2 | 3 | 4 | 5 | 6       0 | 1 | 2 | 3 | 4 | 5 | 6
                // ------------------------------   ------------------------------
                //  0 | X | X | X | X | X | X | X   0 |
                //  1 | 0 | X | X | X | X | X | X   1 | 0
                //  2 | 1 | 6 | X | X | X | X | X   2 | 1 | 6
                //  3 | 2 | 7 | 11| X | X | X | X   3 | 2 | 7 | 11
                //  4 | 3 | 8 | 12| 15| X | X | X   4 | 3 | 8 | 12| 15
                //  5 | 4 | 9 | 13| 16| 18| X | X   5 | 4 | 9 | 13| 16| 18
                //  6 | 5 | 10| 14| 17| 19| 20| X   6 | 5 | 10| 14| 17| 19| 20
                //
            
                // Arraysolution:

                // 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |  1 | 2  |  2 |  2 |  2 |  3 |  3 |  3 |  4 |  4 |  5
                // 1 | 2 | 3 | 4 | 5 | 6 | 2 | 3 | 4 | 5 |  6 | 3  |  4 |  5 |  6 |  4 |  5 |  6 |  5 |  6 |  6
                //---------------------------------------------------------------------------------------------
                // 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20

                // Upper part

                // the length in upper part is defined by the length of entities - 1 - currentEntityIndex
                int countFirst = entitiesLength - i - 1;

                for (int j = 0; j < countFirst; j++)
                {
                    // we count this, so it is more easy to cycle through here
                    NetForceValue -= gravityValuePairs[count];
                    count++;
                }
                // Second count is equal to i
                int countSecond = i;

                for (int j = 0; j < i; j++)
                {
                    // the index value is more complex, but easy to calculate
                    // we reduce our current index by 1 and add a value, defined by sequence GetPositionInArray
                    NetForceValue += gravityValuePairs[(countSecond - 1) + GetPositionInArray(j, entitiesLength - 2)];
                }

                // Set this Value the current entities netforce
                entities[i].NetForce->F = NetForceValue;
            }
        }
        #endregion

        #region GetPositionInArray
        /// <summary>
        /// This method is capable to calculate out the next needed position in the defined array
        /// </summary>
        /// <param name="_count">how much calculations are done</param>
        /// <param name="_entityLength">the value to calculate with is originally entityLength -2, but we do this in the function call!</param>
        /// <returns></returns>
        private int GetPositionInArray(int _count, int _entityLength)
        {
            // Setup a return value
            int returnValue = 0;
            // As long as i is smaller our count
            for (int i = 0; i < _count; i++)
            {
                // add the current length value
                returnValue += _entityLength;
                // and reduce it by one for another run
                _entityLength--;
            }
            // return this value
            return returnValue;
        }
        #endregion

        #region GetLengthForArray
        /// <summary>
        /// Calculates the current value for the length of the force value pairs array
        /// This is defined by the algorithm of it's length of entities
        /// We subtract -1 in the method call before !!!
        /// F.e.:
        /// Length = 7
        /// Length of Array = 6+5+4+3+2+1 = 21
        /// </summary>
        /// <param name="_numberOfEntities">Number of entities</param>
        /// <returns>Needed length for current force value pairs array</returns>
        private int GetLengthForArray(int _numberOfEntities)
        {
            // return value
            int count = 0;

            // As described in method description, add i as it is smaller than _numberOfEntities;
            for (int i = _numberOfEntities; i > 0; i--)
            {
                count += i;
            }
            return count;
        }
        #endregion
    }

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 ?

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 ! :wink:

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

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

1 Like

Yup ! That’s what happens

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.

Example

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;


namespace GravitationForceExample
{
    public struct PlanetMass: IComponentData
    {
        public float Value;
    }
   
    public struct GravitationForce: IComponentData
    {
        public float Value;
    }
   
    public class GravitationForceJobSystem : JobComponentSystem
    {
        public ComponentGroup planet_Group;
       
        const float G = 6.67408E-11f;
       
        [BurstCompile]
        struct CalcualteGravitationPairsJob : IJobParallelFor
        {
            [ReadOnly] public float G;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<Position> PlanetPositions;
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<PlanetMass> PlanetMasses;
            [ReadOnly] public int Length;
            [NativeDisableParallelForRestriction] [WriteOnly] public NativeArray<float> GravitationPairs;
           
            public void Execute(int i)
            {
                for (int j = i+1; j < Length; j++)
                {
                    var gf = G * PlanetMasses[i].Value * PlanetMasses[j].Value / math.lengthsq(PlanetPositions[i].Value - PlanetPositions[j].Value);
                    GravitationPairs[j + i * Length] = gf;
                    GravitationPairs[i + j * Length] = -gf;
                }
            }
        }
       
        [BurstCompile]
        struct ApplyGravitationForceJob : IJobProcessComponentDataWithEntity<GravitationForce>
        {
            [DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float> GravitationPairs;
            [ReadOnly] public int Length;
           
            public void Execute(Entity e, int i, [WriteOnly] ref GravitationForce gravitationForce)
            {
                float gf = 0;
                int offset = i * Length;
                for (int j = 0; j < Length; j++)
                {
                    gf += GravitationPairs[j + offset];
                }
                gravitationForce.Value = gf;
            }
        }

        protected override void OnCreateManager()
        {
            SetupPlanets();
        }
   
        protected override JobHandle OnUpdate(JobHandle inputDependencies)
        {
            var length = planet_Group.CalculateLength();
            var planetPositions = planet_Group.ToComponentDataArray<Position>(Allocator.TempJob);    // I think this is the replacement for GetComponentDataArray
            var planetMasses = planet_Group.ToComponentDataArray<PlanetMass>(Allocator.TempJob);    // and I think it is slower, as it creates a copy instead of moving a pointer
            // for simplicity, I stored the positive & negative values, i.e. I wasted some space vs. length * (length - 1) / 2
            var gravitationPairs = new NativeArray<float>(length * length, Allocator.TempJob);

            inputDependencies = new CalcualteGravitationPairsJob
            {
                G                        = G,
                PlanetPositions            = planetPositions,
                PlanetMasses            = planetMasses,
                Length                    = length,
                GravitationPairs        = gravitationPairs
            }.Schedule(planet_Group.CalculateLength(), 32, inputDependencies);

            inputDependencies = new ApplyGravitationForceJob
            {
                GravitationPairs        = gravitationPairs,
                Length                    = length
            }.Schedule(this, inputDependencies);
       
            return inputDependencies;
        }
       
        void SetupPlanets ()
        {
            var initPositions = new Position[]
            {
                new Position {Value = new float3 (0,0,0)},
                new Position {Value = new float3 (-1,-1,0)},
                new Position {Value = new float3 (-1,1,0)},
                new Position {Value = new float3 (1,1,0)},
                new Position {Value = new float3 (1,-1,0)}
            };
           
            var initMass = new PlanetMass[]
            {
                new PlanetMass {Value = 2f},
                new PlanetMass {Value = 1f},
                new PlanetMass {Value = 1f},
                new PlanetMass {Value = 1f},
                new PlanetMass {Value = 1f}
            };
           
            if (initPositions.Length != initMass.Length) return;
           
            var planetComponentTypes = new ComponentType[]{ComponentType.Create<Position>(), ComponentType.Create<PlanetMass>(), ComponentType.Create<GravitationForce>()};
            planet_Group = GetComponentGroup(planetComponentTypes);
           
            for (int i = 0; i < initPositions.Length; i++)
            {
                var e = EntityManager.CreateEntity(planetComponentTypes);
                EntityManager.SetComponentData<Position>(e, initPositions[i]);
                EntityManager.SetComponentData<PlanetMass>(e, initMass[i]);
            }
        }
       
    }
}
1 Like

I will study this !
Thank you !