The Core Code

The essential physics of Gravity Engine 2 is done by the GECore (GE) and GEPhysicsCore (GEPC) classes. This section provides an overview of their architecture and usage. In very general terms GECore manages the data and book-keeping and GEPhysicsCore does the physics evolution via numerical integration and/or propagators as well as applying maneuvers as appropriate.

GECore

One of the key design objectives of the GECore (GEC) class was to ensure that it could be used as a stand-alone engine to model orbital mechanics problems without references to Unity Monobehaviour classes. This ensures that instances/copies can be used to run calculations for “what if” scenarios in e.g. path finding. A consequence of this decision is that references to bodies in GECore are not directly tied to a GSBody object. All bodies in GEC are referred to by an integer body identifier that is created when a body is added to GEC. (If a GSController is being used it manages the recording of these ids in its interaction with GEC.)

Initializing

The constructor for GECore requires some key information for setting up the physics evolution that follows. Once initialized these values cannot be altered.

public GECore(Integrators.Type integrator,
                    GBUnits.Units defaultUnits,
                    double orbitScale,
                    double massScale,
                    double stepsPerOrbit,
                    double startTimeJD = 0 // only needed if using SGP4
                    )

Currently GEC supports two numerical integration algorithms listed by the enum Integrator.Type. Currently the choices are LEAPFROG and RK4. (Leapfrog integration is also called Verlet or Stormer-Verlet integration. RK4 is an abbreviation of fourth-order Runge-Kutta integration.) Both integrators are capable and reasonably accurate when given an appropriate step size for the intial conditions (more below).

Numerical integration is a deep and nuanced subject and useful overviews are presented in many orbital mechanics textbooks. My favorite currently is the discussion in Tremaine (2023).

Leapfrog is a simple second-order scheme that does a great job of conserving the energy of the system and it supports reversability. RK4 is a fourth order scheme that is slightly more accurate (it does four force evaluations per step vs leapfrog’s one) but does require more calculation.

The choice of a step size is the key decision in using an integrator. This decision is made more “intuitive” by the GEC parameters: orbitScale, massScale and stepsPerOrbit. These are combined to compute a time step based on the period of the reference orbit and the indicated number of steps per orbit.

orbitScale size of a typical circular orbit in the defaultUnits for the simulation

massScale mass of the central body for the typical orbit

stepsPerOrbit the number of integration steps to be computed for an orbit. For a circular orbit a number of about 200 is reasonable. As the orbit becomes more eccentric the velocity at periapsis gets larger and more points are needed to ensure smooth and accurate evolution.

startTimeJD In some cases the information for bodies in the system is specified at a specific world time, termed their epoch time. For example the orbital elements for the International Space Station (ISS) in a TLE might be provided for Feb 15, 2024 at 11:30 UTC. Other bodies may have different start times. The startTimeJD indicates the time at which GEC evolution is to start. This must be later than any orbit element reference times since the orbit propagators only work for times after their epoch time. This time is specified in Julian date format (see scaling for more).

Managing Bodies

Each body added to GEC has some common information (e.g. mass) and additional information about the initial physical state. The simplest description of initial state is the position vector \(r\) and velocity \(v\) in absolute world units. While simple, this leaves a lot of work to be done outside GEC if the goal it have the initial state be e.g. “in a circular orbit around body with id=0”. Consequently there are extension to this API to allow initial data to be provided in the form of classical orbit elements (COE), relative RV, satellite data from a two-line element data set etc.

Bodies are added to GEC by using one of the variants of the BodyAdd API:

  • BodyAdd

  • BodyAddInOrbitWithRVRelative

  • BodyAddInOrbitWithCOE

  • BodyAddInOrbitWithTLE

  • BodyAddWithEphemeris

(see the code for the complete list of arguments). In each case the integer id GEC assignes to the body is returned by the API call.

The body add functions also may allow the specification of how the physics of the body is to be handled:

  • N-body gravity via numerical integration

  • algorithmic propagation around a center body (KEPLER, PKEPLER, SGP4)

  • use the body’s mass but do not move it (FIXED)

  • move according to a table of \((t, r, v)\) (EPHEMERIS)

These options correspond to an GEPC enum type GEPhysicsCore.Propagator.

Evolving a System of Bodies

The evolution of a system of bodies can be done directly with an in-line call or can be run on a worker thread using the Unity job system. The API calls for this are:

  • EvolveNow(double tUntilWorld run evolution until the internal physics time is greater than tWorldUntil. (Due to the integrator timestep there may be a slight overshoot)

  • Schedule(double tUntilWorld schedule a job to run the physics evolution. The job can be requested to complete by calling Complete(). The current completion status can be checked via IsCompleted().

These calls evolve the system to tUntilWorld (the end time in world units). During this evolution the system will:

  • apply any maneuvers that have an execution time before the end time

  • detect collisions if colliders have been specified for bodies

  • handle propagation errors (e.g. SGP4 satellites may report they have decayed and can no longer evolve)

The API has variants of these API calls that include recording of the intermediate values of the state for a list of specified bodies.

In either case the core run loop is the same and is described in GEPhysicsCore

FIG: Core Run Loop

Maneuvers and Physics State Changes

GEPhysicsCore

GEPhysicsCore (GEPC) is where all the physics (including propagators) happens. It contains the numerical integration, propagator invocation, maneuver and collision detection code. The code is written to allow both direct execution and execution as part of the Unity Job system. The Job system imposes strong limitations on data types (e.g. no class references) to ensure it can operate in a thread-safe manner. The implementation and architecture of GEPC

“Headless” GE and Recording Output

The GECore class can be used directly with scripting to model and evolve specific scenarios without the scene component utility methods that simplify “in-scene” use of GE2. This section provides details on the scripting setup required and illustrates the use of a non-interactive gravitational evolution and the recording of the output and after-the-fact displaying.

The example scene BasicRecord and the controller RecordController provide an example of headless GE operation. To make the result easier to interpret the recorder does use line renderers to display the recorded paths after GE has run to completion.

The first thing the controller does as part of its Start() is to create and initialize a GECore:

  double mu = 1000.0;
  ge = new GECore(integrator: Integrators.Type.RK4,
                                        defaultUnits: GBUnits.Units.DL,
                                        orbitScale: 100.0,
                                        massScale: mu,
                                        stepsPerOrbit: 1000.0,
                                        immediateMode: true);