GE2 Major Features

GE2 has a number of major features. These are listed and details about their use are provided.

The major scene components are described in scene components.

Maneuvers

Manuevers are used to specify a change in the state of a body at some future time. For example, a Hohmann transfer requires two changes in the state of the body. The first changes from a circular orbit to an elliptical orbit that intercepts the destination orbit and the second circularizes the orbit when the destination orbit is reached. GE2 implements this with a maneuver mechanism in the GECore API (TransferShip uses this API). The essential idea is that we need to tell GECore to change the state of a specific body by a specified amount at a designated future time.

This is done with the API:

        /// <summary>
        /// Add a maneuver to the list of pending maneuvers. If no timeOffset is given it is assumed
        /// the t_relative in the maneuver is to be added to the current time to establish the overall
        /// maneuver time. 
        /// 
        /// ...
        /// 
        /// </summary>
        /// <param name="id"></param>
        /// <param name="m"></param>
        /// <param name="timeOffsetGE"></param>
        public void ManeuverAdd(int id, GEManeuver m, double timeOffsetWorldTime = double.NaN)

The key information for this is provided in the GEManeuver class. All maneuvers will specify a time they are to be applied. There are four types of maneuvers that can be designated in this class:

    // SET_VELOCITY: Use velocityParam as the new velocity vector
    // APPLY_DV: add velocityParam to the current velocity
    // SCALAR_DV: Change the magnitude of the velocity to the new value, keep direction the same
    //            (use velocityParam.x as the magnitude of the new velocity)
    //            e.g. to increase by 1% set velocityParm.x to 1.01
    public enum ManeuverType { SET_VELOCITY, APPLY_DV, SCALAR_DV };

Depending on the type chosen different parameters in the class are used and are applied differently.

Implementation: The application of a maneuver is performed by calling GEManeuverStruct.ApplyManeuver and providing the current velocity of the body and the velocity of the center body.

In all cases if the body is in orbit around a center body, the center velocity component of the body velocity is subtracted to get a velocity relative to the center body. The maneuver change is computed and then the center body velocity is re-added. This means that all velocity changes for bodies in orbit are relative and do not need to take into account the motion of the body they are orbiting around.

Type

Struct Field Used

Action Taken

SET_VELOCITY

velocityParam

Sets new velocity to velocityParam

APPLY_DV

velocityParam

Adds to velocityParam to current velocity

SCALAR_DV

velocityParam.x

Sets velocity in same direction as current, with magnitude velocityParam.x

As with (almost) all API calls into GE2 the values are provided in world units/time. Also, adding a maneuver can only be done when the GE is not running (if in Job mode), so the code to add a maneuver might need to be in a GE callback to ensure it is executed when the GE physics evolution has paused.

A maneuver may also provide a callback function of the form:

    public delegate void DoneCallback(GEManeuver gem, GEPhysicsCore.PhysEvent physEvent);

This will be called by GE once the physics loop in which the maneuver executes completes. Note that GE2 may have further evolved the system beyond the maneuver time before the callback happens.

The execution of a maneuver also means that any active trajectory predictions will need to be recalculated and GE will undertake this. When using IMMEDIATE execution mode this may chew up some real-time as the computation is performed.

GE does not retain maneuvers after they have been executed.

Implementation Details

Internally GE morphs the maneuver into a struct (converting everything into physics units) and makes this information available to GEPhysicsCore (GEPC). As the main physics loop runs the maneuver will be handled in one of two ways:

  • at the start of each numerical timestep the execution time of the earliest maneuver in the list is examined. If the timestep has progressed beyond the maneuver time, then the maneuver is executed. This causes the appropriate state change to be applied to the indicated body

  • if the physics core is all on rails then it will normally step directly to the requested maneuver time. If there is a maneuver before this end time it will evolve the system to the maneuver time, apply the maneuver and then continue to the requested end time

In the first case above the maneuver will generally not be executed at exactly the requested time. Generally the time error (due to the \(dt\) step size of numerical integration) will be quite small.

In general the Leapfrog integrator loses precision if a variable timestep is used, hence we avoid integrating to exactly the maneuver time. This logic may be made integrator-dependent in a future release.

Once a maneuver has been executed it is added to a completed list in the GEPC. Once the current GE physics loop completes then GE will call callbacks for those maneuvers that were executed and manage the internal maneuver lists appropriately.

Patch Mode/On-Rails

Patch mode is a property of a GSBody that can be enabled if the body is not using a GRAVITY mode propagator. The non-GRAVITY propagators are deterministic (i.e. given a time they can compute a position). This allows the body motion to be described using multiple propagator instances with each responsible for a distinct time interval. Let’s look at two user-cases to make that more concrete.

PIC: Patches for Hohman

First, consider a ship with a Kepler propagator that will start a Hohmann transfer at \(t=t_1\) from an “inner” orbit to an “outer” orbit, evolve on the transfer orbit until \(t=t_2\) and then circularize it’s “outer” orbit and move in this orbit when \(t > t_2\). If the GSBody has patched mode selected then this results in three propagator patches internally:

  • KEPLER mode (for inner circular orbit) for \(t < t_1\)

  • KEPLER mode (for transfer orbit) for \(t_1 < t < t_2\)

  • KEPLER mode (outer circular orbit) for \(t > t_2\) Why do this, instead or say maneuvers? The patches allow GE to determine where the ship should be for any time \(t\). This means that user code can change the GE time forwards or backwards and GE can update the ship position. NOTE: This will only be possible if all the bodies in the GE can be handled in a deterministic way. As soon as even one body requires numerical integration the system is not deterministic and time cannot be jumped around.

The second use case is to handle a scenario called “patched-conics” in Orbital Mechanics textbooks. In this scenario the goal is to model the path of a ship as it traverses from e.g. the Earth’s gravity to the Moon’s gravity in a deterministic way using only one of the Earth or Moon as the source for gravity at a time. As the ship moves in the Earth gravity the trajectory (an ellipse, parabola or hyperbola i.e. a conic) uses an orbit propagator centered on the Earth. Once the ship enters the Moon’s sphere of influence then it switches to a propagator centered on the Moon. This method was historically used to find “first look” trajectories for transfer to the Moon. These were then refined with more careful numerical simulations for real missions. However, in the context of a playable Earth-Moon scenario patched conics can be useful.

TODO: The Early Access version does not yet have a tutorial for setting up an Earth-Moon transfer. The full version will.

Trajectories

General N-body interactions do not usually allow simple orbit prediction according to a predictive equation (such as Kepler’s equation). In these cases the future path of the objects must be determined by actually evolving the equations forward in time and recording the result. GE2 supports this via the trajectory feature.

When using scene components. this feature has three elements:

  • one or more GSDisplayBody that want to display a future trajectory

  • a LineRenderer assigned to these display bodies to describe the width and color of the trajectory line

  • the ‘time ahead’ and ‘number of steps’ parameters for the trajectory (set in the GSController inspector)

An example of this setup can be found in the sample scene folder 2_Trajectories in the ‘Going Further’ tutorial section. Here there are two fixed masses (known as the Euler problem [ref]). As a result simple orbit prediction is not applicable.

Internally the primary GravityEngine is creating a second instance of the GravityEngine class with the same initial data and evolving it forward, recording data in a circular buffer. This process is triggered by a call to TrajectorySetup inside the GSController. Trajectory information is then accessed by the GSDisplay component and used to update the line renderers. When something in the system changes, for example a ship maneuvers, then a new future path must be recomputed from the present state. This all happens transparently to the user but it should be noted that this may result in a burst of CPU activity as the new look ahead is determined. This can be mitigated to some extent by using IJOB_LATEUPDATE mode in the GSController.

Trajectory Scripting

In scripting use GravityEngine can be configured for trajectories with the API call:

  public void TrajectorySetup(double t_ahead, int numSteps, List<int> bodiesToRecord)

This API call is also used when a trajectory for an additional body is required. Once it has been added to GE setup is re-run. The trajectory will be recomputed from the current state.

To retrieve the trajectory information the code in GSDisplay.TrajectoryUpdate is a good guide. The key GE API calls are:

  NativeArray<GEBodyState> recordedOutput = geTrajectory.RecordedOutputGEUnits();
  NativeArray<int> recordedBodies = geTrajectory.RecordedBodies();

The recordedOutput is a reference to the internal array in GE units. If the buffer were converted to world units each time it was read, then the next iteration would do an extra conversion on the buffer contents and double scale the result. As a result the conversion is done as the points are mapped into the display space.

GECore Event Listeners

There may be events during GECore evolution that are of interest to the game logic. Examples include:

  • collision event between two bodies

  • events from an external acceleration component (e.g. a booster running out of fuel)

  • error conditions from a propagator (e.g. SGP4 propagator detecting an orbit that has decayed)

These events can be communicated to game logic by having the game logic registewr a listener for events. GECore will report all events generated during an Evolve() cycle to each of the registered listeners when the physics loop of the current event loop completes. Event listeners are registered to GECore via the API:

    public void PhysicsEventListenerAdd(PhysicsEventCallback physEventCB)

The required callback method is defined as:

    public delegate void PhysicsEventCallback(GECore ge, GEPhysicsCore.PhysEvent physEvent);

The details of the event are encapsulated in a GEPhysicsCore.PhysEvent. Event types are:

    public enum EventType { COLLISION, SGP4_ERROR, BOOSTER, KEPLER_ERROR, PATCH_CHANGE, MANEUVER_ERROR, MANEUVER };

Type

Description

COLLISION

a collision has occured between two bodies with GE2 colliders attached

SGP4_ERROR

SGP4 propagation has encountered an error condition

BOOSTER

A Booster external acceleration has reported an event (e.g. fuel out)

KEPLER_ERROR

A Kepler propagator has reported an error

PATCH_CHANGE

Evolution of a patched body has transitioned from one patch to another

MANEUVER_ERROR

A requested maneuver could not be applied

MANEUVER

A maneuver has been executed

In addition to the event type the GEPhysicsCore.PhysEvent contains details on the body, its position and velocity and the exact time of the event. If the event involves a second body (e.g. collision) state information on that body is also provided. Addition fields can be used to provide auxillary information depending on the event type.

Earth Orbit Propagators: SGP4 and PKEPLER

There are two Earth orbit propagators provided by GE2:

  • SGP4 SGP4 is a satellite propagator developed by NASA and NORAD. It models the effects of a non-spherical Earth, atmosphere and (for higher orbits) lunar/solar perturbation to the satellite’s orbit. See Wikipedia. The initial data for an orbit is accurate to about 1km and over a few days this degrades to 2-3 km.

  • PKEPLER The canonical textbook, Vallado’s “Funadamentals of Astrodynamics and Applications” provides a simpler model that handles the non-spherical orbit and a very simple (optional) atmospheric drag. This can be used for a simpler model where the oblate Earth effect is necessary, for example it is a key driver of the physics of a Sun-synchronous orbit.

Ephemeris Propagation

An ephemeris is a table of values for the position and velocity of a body as a function of time. It is one of the propagator options in GSBody. A body with an ephemeris can be added to GE with the BodyAddWithEphemeris method and an EphemerisData class instance.

For the GSBody use case the inspector will appear as:

Ephemeris

The EPHEMERIS mode requires the name of a text file found in a resources folder in the Unity project. This resource file is a CSV format with each line providing a time, position (x, y, z) and velocity (x, y, z) all in the default world units selected for the GSController that will be handling this body.

An example of this can be found in the samples directory 5_Ephemeris in the Samples tree. This example highlights how arbitrary the ephemeris can be by moving two ships in a triangular pattern around a star. It would be more common to adapt output from a satellite simulation tool or from the JPL horizons database to model the motion of a spacecraft or a celestial body.

External Forces: GSExternalAcceleration

GE2 models Newtonian gravity internally. In some cases there may be additional forces to be added. Examples include:

  • rocket thrust

  • atmospheric drag

  • continuous low-thrust such as an ion drive or solar sail

  • non-spherical gravitational forces e.g. J2 multi-pole from an oblate planet

The mechanism to do this in GE2 is via an external acceleration that will be invoked in the inner loop of the gravity engine as bodies are evolved. In the GravityEngine API this is done via the ExternalAccelerationAdd API call. The in-scene wrapper for this in a component that extends the GSExternalAcceleration interface. Examples provided include:

  • GravityJ2

  • InverseR

  • IonDriveExternalAccel

Some of these augment the gravitational acceleration (Ion Drive, J2) while other replace the gravitation acceleration completely (InverseR).

The essential idea for an external acceleration is that for each body that is evolving under GRAVITY there is a slot for a callback to be invoked.

External Acceleration

GE2 allows external accelerations to be added to bodies in the gravity engine numerical integration of motion. Applications for this feature include:

  • modelling non-spherical masses

  • drag due to an atmosphere

  • continuous low-thrust engine (e.g. ion drive or solar sail) The API allows the external acceleration to be added to the Newtonian gravitational acceleration or to replace it entirely.

External forces apply to objects that are evolving with numerical integration (including particles). Applying a force to an object using e.g. a Kepler propagator will have no effect on the motion of that body.

The GE API call is:

   public int ExternalAccelerationAdd(int bodyId,
                           ExternalAccel.ExtAccelType eaType,
                           ExternalAccel.AccelType accelType,
                           double3[] accelData = null)

The influence of external acceleration is determined by the ExtAccelType: { SELF, ON_OTHER }. This is used to distinguish between a force that acts on the body that is adding it (e.g. an Ion drive of rocket booster) or if the force causing the acceleration is one that acts from this body on other massive and massless bodies.

The specific external acceleration is indicted by the AccelType enum. The current values are:

public enum AccelType {
                INVERSE_R = 0,
                GRAVITY_J2 = 1,
                ION_DRIVE = 2,
                BOOSTER = 3,
                EARTH_ATMOSPHERE = 4,
                // Add custom External Forces here
                CUSTOM = 100,
        };

Note

Ideally the choice of force would be via a delegate function but the Unity Job system does not allow function pointers (technically it does but code must be marked unsafe and Unity assets may not do that, fair enough).

The application of an acceleration is done in the numerical integration code because that is where the gravitational acceleration is determined. The ExternalAccel class has a static method ExtAccelFactory that uses the acceleration type enum to steer the acceleration calculation into the appropriate code. Custom forces will require a changes in the ExternalAccel:

  • add to the AccelType enum

  • add to the switch statement in ExternalAcel.ExtAccelFactory.

The API for the acceleration calculation can vary but in general will take advantage of information provided from the current state of the numerical integrator as well as some details about the specific external acceleration:

  • EAStateData

  • EADesc

  • parameter data (a NativeArray of parameters for all forces)

The EAStateData provides the information that might be needed by a substitute force computation:

  • the r, v, and mu (\(G*mass\)) of the to and from bodies when ON_OTHER

  • the r, v and mu for the body (if SELF)

There is also an EADesc structure provided to the function to facilitate the use of block of data provided when the force was added to GECore. The most important members are those that indicate where in the eaData array the block of data for this instance of the force can be found: paramBase and paramLen.

GEPhysicsCore maintains a native array of double3 to hold parameters for the external acceleration. This corresponds to a concatenation of the double3[] accelData provided in all the external acceleration adds. Each external acceleration implementation has a unique recipe for how to pack the parameter data into this array and provides helper functions to preload it. This data may be modified as evolution proceeds and there may be cases where reading back a value (e.g. fuel remaining) is provided by the code for the external acceleration.

Let’s look at a concrete example: an Ion drive that adds some acceleration in the direction of the current velocity of a body.

To add the force the script extends GSExternalAcceleration and implements the required method:

public void AddToGE(int id, GECore ge, GBUnits.Units units)
{
    this.ge = ge;
    double3[] data = new double3[1];
    double scaleT = ge.GEScaler().ScaleTimeWorldToGE(1.0);
    double accelGE =  ge.GEScaler().ScaleAccelWorldToGE(accelSI);
    double timeStartGE = timeStart * scaleT;
    double timeEndGE = timeEnd * scaleT;
    data[0] = new double3(accelGE, timeStartGE, timeEndGE);
    extId = ge.ExternalAccelerationAdd(id, 
                                        ExternalAccel.ExtAccelType.SELF, 
                                        ExternalAccel.AccelType.ION_DRIVE, 
                                        data);
}

Note the data array contains a single double3 and the components represent the acceleration (in GE units) and the time for the ion drive to start and stop. Note the use of a reference to the GEScaler from the GECore to adjust the world units to GE units.

The call in the ExtAccelFactory is tho the following routine:

 public static (int status, double3 a_out) IonDriveAccel (ref ExternalAccel.EAStateData eaState,
                         ref ExternalAccel.EADesc eaDesc,
                         double t,
                         double dt,
                         double3 a_in,
                         NativeArray<double3> data)
 {
     double3 a_out = double3.zero;
     int b = eaDesc.paramBase;
     double tStart = data[b + 0].y;
     double tEnd = data[b + 0].z;
     if (tStart < 0)
         return (0, a_out);

     if ((t >= tStart) && (t <= tEnd)) {
         a_out = data[b+0].x * eaState.v_from;
     }
     return (0, a_out);
 }

This function decodes a single entry in the double3[] array using the offsets in the eaDesc to provide:

  • acceleration (.x component)

  • time start in GE units (.y component)

  • time end (.z component)

The use of a time start and end allows this force function to be evaluated into the future for trajectory prediction. If this was not needed then a simple on/off value could be provided.

The function used the paramBase to compute an offset in the data array (which is an array of the data for all the forces in the core). The current velocity direction is retrieved from eaState and then the value is returned in a_out.

Examples of implementations can be found in the tutorial scripts in the classes: IonDriveExternalAccel, InverseRForce and GravityJ2. A more extensive example can be found in the section on Rocket Launch Tutorial in the Booster class.

Collisions

Unity has a sophisticated collision detection system built into its internal physics engine. GE2 provides a parallel physics engine and as a consequence cannot take direct advantage of the Unity collision system. GE2 has a component GSCollider. This component is a simplified collider class that allows the GE2 engine to monitor for collisions (based on the GSBody radius) and then to take an action to absorb one of the bodies or to generate a “bounce” based on a point contact model of two spheres colliding.

Colliders can be attached to meshes on display objects. These could then be used as triggers for collision detection at the per-frame timeframe. This may suffice for some game scenarios where additional game code is added to determine the correct GE2 actions to take.

A GSCollider object must be attached to a game object with a GSBody. The inspector view of this component is:

GSCollider

Collision Type: ABSORB will remove the body with the lower inertial mass. BOUNCE will compute a rebound based on a hard sphere collision.

Bounce Factor: The coefficient for the recoil. The average value of the bounce factor for the two bodies will be used.

Use GSBody Mass: Use the GSBody mass as the mass for a bounce collision. (In many cases GSBody objects will have zero mass, since this simplifies their gravitational interactions. In this case inertial mass can be specified in the collider directly.

Inertial Mass: A mass value for bounce collisions when the body is considered “massless” in a gravitational context.

Implementation

The GEPhysicsCore code checks for collisions after each integration step. This is done by cross-comparing each body that has a collider with each other body based on their physics location and radius. If a collision is detected then code is run to resolve the issue. In the case of an ABSORB the body to be absorbed will be immediately removed from evolution so that it is not evolved on the next time step.

When a collision is handled in the core code a ColliderInfo event is generated and added to a list. GravityEngine checks this list each time that the physics loop completes. Note that when asked to do a long evolution in batch mode there is the potential for multiple collisions to accumulate.

When GE evolution is driven by a GSController the controller will register as a listener for GE events. When a body is absorbed due to a collision, GE will remove the body from its internal records and a BodyRemoved callback will be executed for any registered listeners. This allows the controller to do book-keeping associated with the body that was removed and to ask any GSDisplay elements that are displaying this body to remove it. The callback code is GSController.GEListenerIF.BodyRemoved()