Saturday, June 12, 2021

Gravity Part 1: An update

Introduction

Artistic representation of Earth's gravitation field (Image credit: Shutterstock)

It has been a while since I last added a new post entry. For the past year (pandemic at full) I did lots of testing regarding my physics-based gravity project.

This entry is the first of a series of 3 in which I will update what have been done so far:

The update

The code described here has basically the same functionality as the original Physics-based Solar System Lessons post. The foundations, insights and issues are described in detail in the mentioned blog entry. So, I highly recommend checking it out for references.

I did not want to change that post so I decided to create a new one with cleaner code, explaining the major changes and calculations.

I did a major code refactoring to have everything clearer and more cohesive. The code is divided into 2 blocks:
  • Generic:
    • GameManger.cs: Controls the time and space scale logic.
    • Constants.cs: Controls generic constants/ enums used in the code.
  • Gravity implementation:
    • SpaceObject.cs: This is the core logic used by the objects.
    • Gravity.cs This is the core to calculate gravity.

Generic:

The GameManager.cs class has changed quite a bit on content to only use what is important:
  • To modify the fixed delta time, everything else was not necessary and generated issues when used.
  • To calculate a “new” gravitational constant. Previously I did all the force calculations using the original units N*m^2/kg^2. Now, the distance and time is calculated using the scaled units defined in GameManager.cs. The advantage is that we would never run out of precision, regardless of the actual values (just imagine if we want to use this to simulate gravity movement in the Milky Way).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    [Header("Scaling options")]
    [SerializeField]
    [Tooltip("1 unit = SpaceScale kilometers")]
    private float _spaceScale = 6000;

    [SerializeField]
    [Tooltip("1 unit = TimeScale seconds")]
    private float _timeScale = 100000; //1 unity unit = timeScale seconds

    [SerializeField]
    [Tooltip("Fixed update loop time (default = 0.02)")]
    private float _modifiedFixedDeltaTime = 0.001f;

    public float SpaceScaleKm => _spaceScale;
    public float TimeScale => _timeScale;

    public double GravitationalConstantUnityScaled { get; private set; }
    public float ModifiedDeltaTime => _modifiedFixedDeltaTime;

    private void Awake()
    {
        Time.fixedDeltaTime = ModifiedDeltaTime;
        GravitationalConstantUnityScaled = Constants.GRAVITATIONAL_CONSTANT_KM * Math.Pow(TimeScale, 2) / Math.Pow(SpaceScaleKm, 3);
    }
}

The Constant.cs class has increased to add generic enums used throughout the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static class Constants
{
    public static readonly double GRAVITATIONAL_CONSTANT_KM = 6.67430e-20; //N*km^2/kg^2
    public static readonly double EPSILON = 0.01;

    public const int FREE_VELOCITY = 0;
    public const int CIRCULAR_ORBIT_VELOCITY = 1;
    public const int ESCAPE_ORBIT_VELOCITY = 2;
	
    public enum InitialVelocity
    {
        Free = FREE_VELOCITY,
        CircularOrbit = CIRCULAR_ORBIT_VELOCITY,
        EscapeOrbit = ESCAPE_ORBIT_VELOCITY
    }
}

Gravity Implementation:

Originally, I had 3 different classes (GravitationalForces, SpaceObject and Satellite). Of these, “ Satellite” is a specific case of SpaceObject and the logic between the other two is entangled. So now everything has been refactored into 2 classes:
  • Gravity.cs: The logic that does all the gravity calculation.
  • SpaceObject.cs: Any object that affects or is affected by gravitation.
For an object to be affected by this gravity implementation I am using the tag: SpaceObject. I am using the same name as the script name so it is easy to connect both.

SpaceObject Tag
Tag that enables the object to be affected by gravity

The implementation of Gravity.cs class now only takes care of functions to obtain gravitation pull force and velocities from gravitational situations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System;
using UnityEngine;

public class Gravity : MonoBehaviour
{
    protected GameObject[] _spaceObjects;    
    protected GameManager _gameManager;
    private double _massSource;

    /// <summary>
    /// Get gravitational pull force from this object, depending on the distance from it
    /// </summary>
    /// <param name="distanceFromObject"> distance between 2 objects in unity units </param>
    /// <returns></returns>
    public double GetGravitationalPullForce(double distanceFromObject)
    {
        return _gameManager.GravitationalConstantUnityScaled * _massSource / Math.Pow(distanceFromObject, 2);
    }

    /// <summary>
    /// Get the needed velocity to have a stable circular orbit around a gravitational object
    /// </summary>
    /// <param name="distanceFromObject"> distance between 2 objects in unity units </param>
    /// <returns></returns>
    public double GetVelocityForCircularOrbit(double distanceFromObject)
    {
        var velocity = Math.Sqrt(_gameManager.GravitationalConstantUnityScaled * _massSource / distanceFromObject);
        return velocity;
    }

    /// <summary>
    /// Get the needed velocity to escape a gravitational object's gravity
    /// </summary>
    /// <param name="distanceFromObject"> distance between 2 objects in unity units </param>
    /// <returns></returns>
    public double GetEscapeVelocity(double distanceFromObject)
    {
        return Math.Sqrt(2 * _gameManager.GravitationalConstantUnityScaled * _massSource / distanceFromObject);
    }

    protected void InitializeGravity(GameObject[] spaceObjects, double mass)
    {
        _spaceObjects = spaceObjects;
        _massSource = mass;

        _gameManager = GameObject.Find("GameManager").GetComponent<GameManager>();
    }

    protected Vector3 GetExternalNetGravityVector()
    {
        var netForce = new Vector3();

        foreach (var spaceObjects in _spaceObjects)
        {
            netForce += GetGravity(spaceObjects) * Time.fixedDeltaTime;
        }

        return netForce;
    }

    private Vector3 GetGravity(GameObject spaceObjects)
    {
        var direction = spaceObjects.transform.position - gameObject.transform.position;
        var gravity = spaceObjects.GetComponent<SpaceObject>().GetGravitationalPullForce(direction.magnitude);

        return direction.normalized * (float)gravity;
    }
}
  
The class SpaceObject.cs on the other hand takes care of the initial changes to the object (initial velocities) and updating the forces applied to the object.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using System;
using System.Linq;
using UnityEngine;
using static Constants;

public class SpaceObject : Gravity
{
    #region variables
    [Header("Velocity settings")]
    [SerializeField] private Vector3 _initialDirection = new Vector3(0, 0, 1);
    [SerializeField] private InitialVelocity _velocityType = InitialVelocity.Free;
    [SerializeField] private double _initialVelocity = 0; // In standard km/s
    
    [Header("SpaceObject Properties")]

    [SerializeField]
    [Tooltip("external gravity forces do not affect this object")]
    private bool _disableGravityAffected = false;

    [SerializeField]
    [Tooltip("Kilograms")]
    private double _mass = 0; //Kg

    [SerializeField]
    [Tooltip("Kilometers")]
    private double _radius = 0; //km

    [SerializeField]
    [Tooltip("Kilometers. Distance at which entry to atmosphere happens")]
    private double _entryInterfaceDistance = 0; //Km. distance at which entry to atmosphere happens

    private Rigidbody _rigidbody;
    private GameObject _orbitAround;
    private bool initializationComplete = false;

    [Header("Debug")]
    [SerializeField] private double _currentVelocity = 0; // In standard km/s
    #endregion

    #region Properties
    public double Mass => _mass;

    public double CollisionDistance { get => (_radius + _entryInterfaceDistance) / _gameManager.SpaceScaleKm; }

    public double CurrentVelocity
    {
        get { return _currentVelocity * _gameManager.TimeScale / _gameManager.SpaceScaleKm; }
        set { _currentVelocity = value * _gameManager.SpaceScaleKm / _gameManager.TimeScale; }
    }

    public bool IsVelocityIncreasing { get; private set; } = true;
    #endregion

    void Awake()
    {
        var spaceObjects = GameObject.FindGameObjectsWithTag("SpaceObject").Where(o => o.gameObject != gameObject).ToArray();
        _rigidbody = GetComponent<Rigidbody>();

        InitializeGravity(spaceObjects, _mass);
    }

    void Update()
    {

    }

    void FixedUpdate()
    {
        try
        {
            if (!initializationComplete)
            {
                Initialization();
            }

            if (!_disableGravityAffected)
            {
                _rigidbody.velocity += GetExternalNetGravityVector();
            }

            UpdateVelocityStatus();
        }
        catch (Exception e)
        {
            throw new SystemException("Problem obtaining gravity: " + e);
        }
    }


    private void UpdateVelocityStatus()
    {
        if (CurrentVelocity != _rigidbody.velocity.magnitude)
        {
            IsVelocityIncreasing = (CurrentVelocity < _rigidbody.velocity.magnitude) ? true : false;
        }

        CurrentVelocity = _rigidbody.velocity.magnitude;
    }

    private void Initialization()
    {
        _initialVelocity = GetInitialVelocity(_velocityType);
        _rigidbody.velocity = _initialDirection.normalized * (float)_initialVelocity;

        _initialVelocity = _initialVelocity * _gameManager.SpaceScaleKm / _gameManager.TimeScale;

        initializationComplete = true;
    }


    private double GetInitialVelocity(InitialVelocity initialVelocityType)
    {
        if (_spaceObjects.Length == 0)
        {
            throw new SystemException("No SpaceObjects found. Need at least 1 to get velocity");
        }

        _orbitAround = _spaceObjects
          .OrderByDescending(o => o.GetComponent<SpaceObject>().GetGravitationalPullForce(
          (o.transform.position - transform.position).magnitude))
          .First();

        var distance = (_orbitAround.transform.position - transform.position).magnitude;
        var velocity = 0.0;

        switch (initialVelocityType)
        {
            case InitialVelocity.CircularOrbit:
                velocity = _orbitAround.GetComponent<SpaceObject>().GetVelocityForCircularOrbit(distance) + _orbitAround.GetComponent<Rigidbody>().velocity.magnitude;
                break;
            case InitialVelocity.EscapeOrbit:
                velocity = _orbitAround.GetComponent<SpaceObject>().GetEscapeVelocity(distance);
                break;
            default: //Free velocity                
                velocity = _initialVelocity * _gameManager.TimeScale / _gameManager.SpaceScaleKm;
                break;
        }

        return velocity;
    }
}

These 2 classes have taken a complete refactoring to have a better segregation at what each class is supposed to do.

Sample project

A sample project can be found on this download link with all the relevant code. 
Unity version: 2020.2.7f1

No comments:

Post a Comment

Gravity Part 3: Create a force map and display it (CPU, Compute Shader and Fragment Shader)

Introduction This is the third and last post in the series I am creating, based on the logic from Physics-based Solar System Lessons : ...