GE2 Major Features
Contents
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 |
---|---|---|
|
|
Sets new velocity to |
|
|
Adds to |
|
|
Sets velocity in same direction as current, with magnitude |
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.
Maneuvers in Patch Mode¶
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 trajectorya
LineRenderer
assigned to these display bodies to describe the width and color of the trajectory linethe ‘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 |
---|---|
|
a collision has occured between two bodies with GE2 colliders attached |
|
SGP4 propagation has encountered an error condition |
|
A Booster external acceleration has reported an event (e.g. fuel out) |
|
A Kepler propagator has reported an error |
|
Evolution of a patched body has transitioned from one patch to another |
|
A requested maneuver could not be applied |
|
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:
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
enumadd 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:
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()