# Simple planetary orbits

(Difference between revisions)

These scripts are designed for quickly setting up basic rigidbody-driven 2D orbital physics.

Included are two Monobehaviours (Orbiter and OrbitRenderer) and two supporting classes (OrbitalEllipse and OrbitState). Attach Orbiter.js to the object that you would like to put into orbit. Optionally add the OrbitRenderer component for visual feedback while adjusting Orbiter properties. All objects must lie on the same x/y plane.

## Orbiter.js

```#pragma strict
@script RequireComponent(Rigidbody)

//==============================//
//===        Orbiter         ===//
//==============================//

/*
Required component. Add Orbiter.js to the object that you would like to put into orbit.

Dependencies:
OrbitalEllipse.js - calculates the shape, orientation, and offset of an orbit
OrbitState.js - calculates the initial state of the orbiter
*/

var orbitAround : Transform;
var orbitSpeed : float = 10.0; // In the original orbital equations this is gravity, not speed
var apsisDistance : float; // By default, this is the periapsis (closest point in its orbit)
var startingAngle : float = 0; // 0 = starting apsis, 90 = minor axis, 180 = ending apsis
var circularOrbit : boolean = false;
var counterclockwise : boolean = false;

private var gravityConstant : float = 100;
private var rb : Rigidbody;
private var trans : Transform;
private var ellipse : OrbitalEllipse;
private var orbitState : OrbitState;

// Accessor
function Ellipse () : OrbitalEllipse {
return ellipse;
}

function Transform() : Transform {
return trans;
}
function GravityConstant () : float {
return gravityConstant;
}

// Setup the orbit when the is added
function Reset () {
if (!orbitAround)
return;
ellipse = new OrbitalEllipse(orbitAround.position, transform.position, apsisDistance, circularOrbit);
apsisDistance = ellipse.endingApsis; // Default to a circular orbit by setting both apses to the same value
}
function OnApplicationQuit () {
ellipse = new OrbitalEllipse(orbitAround.position, transform.position, apsisDistance, circularOrbit);
}

function OnDrawGizmosSelected () {
if (!orbitAround)
return;
// This is required for the OrbitRenderer. For some reason the ellipse var is always null
// if it's set anywhere else, even including OnApplicationQuit;
if (!ellipse)
ellipse = new OrbitalEllipse(orbitAround.position, transform.position, apsisDistance, circularOrbit);
if (apsisDistance == 0) {
apsisDistance = ellipse.startingApsis;
}
}

function Start () {
// Cache transform
trans = transform;
// Cache & set up rigidbody
rb = rigidbody;
rb.drag = 0;
rb.useGravity = false;
rb.isKinematic = false;

// Bail out if we don't have an object to orbit around
if (!orbitAround) {
Debug.LogWarning("Satellite has no object to orbit around");
return;
}

// Update the ellipse with initial value
if (!ellipse)
Reset();
ellipse.Update(orbitAround.position, transform.position, apsisDistance, circularOrbit);

// Calculate starting orbit state
orbitState = new OrbitState(startingAngle, this, ellipse);

// Position the orbiter
trans.position = ellipse.GetPosition(startingAngle, orbitAround.position);

StartCoroutine("Orbit");
}

// Coroutine to apply gravitational forces on each fixed update to keep the object in orbit
function Orbit () {
while (true) {
// Debug.DrawLine(orbitState.position - orbitState.tangent*4, orbitState.position + orbitState.tangent*4);
var difference = trans.position - orbitAround.position;
rb.AddForce(-difference.normalized * orbitSpeed * gravityConstant * Time.fixedDeltaTime / difference.sqrMagnitude, ForceMode.VelocityChange);
yield WaitForFixedUpdate();
}
}
</javascript>

== OrbitRenderer.js ==
<syntaxhighlight lang="javascript">
#pragma strict
@script RequireComponent(Orbiter)

//===============================//
//===     Orbit Renderer      ===//
//===============================//

/*
Optional component. Display the Orbiter component's properties in the editor. Does nothing in-game.
*/

var orbitPointsColor : Color = Color(1,1,0,0.5); // Yellow
var orbitPointsSize : float = 0.5;
var ellipseResolution : float = 24;
//var renderAsLines : boolean = false;

var startPointColor : Color = Color(1,0,0,0.7); // Red
var startPointSize : float = 1.0;

private var orbiter : Orbiter;
private var ellipse : OrbitalEllipse;

function Awake () {
// Remove the component in the compiled game. Likely not a noticeable optimization, just an experiment.
if (!Application.isEditor)
Destroy(this);
}

function Reset () {
orbiter = GetComponent(Orbiter);
}
function OnApplicationQuit () {
orbiter = GetComponent(Orbiter);
}

function OnDrawGizmosSelected () {
if (!orbiter)
orbiter = GetComponent(Orbiter);

// Bail out if there is no object to orbit around
if (!orbiter.orbitAround)
return;

// Recalculate the ellipse only when in the editor
if (!Application.isPlaying) {
if (!orbiter.Ellipse())
return;
orbiter.Ellipse().Update(orbiter.orbitAround.position, transform.position, orbiter.apsisDistance, orbiter.circularOrbit);
}

DrawEllipse();
DrawStartingPosition();
}

function DrawEllipse () {
for (var angle = 0; angle < 360; angle += 360 / ellipseResolution) {
Gizmos.color = orbitPointsColor;
Gizmos.DrawSphere(orbiter.Ellipse().GetPosition(angle, orbiter.orbitAround.position), orbitPointsSize);
}
}

function DrawStartingPosition () {
Gizmos.color = startPointColor;
Gizmos.DrawSphere(orbiter.Ellipse().GetPosition(orbiter.startingAngle, orbiter.orbitAround.position), startPointSize);
}
</javascript>

== OrbitalEllipse.js ==
<syntaxhighlight lang="javascript">
#pragma strict

//===================================//
//===  Elliptical orbit datatype  ===//
//===================================//

/*
Calculates an ellipse to use as an orbital path
*/

class OrbitalEllipse extends Object {

// "Starting" apsis is the position of the transform.position of the orbiter.
// "Ending" apsis is the distance that we've defined in the inspector.
// Each apsis defines the distance from the object we're orbiting to the orbiter
var startingApsis : float;
var endingApsis : float;

var semiMajorAxis : float;
var semiMinorAxis : float;
var focalDistance : float;
var difference : Vector3; // difference between the object we're orbiting and the orbiter

//==== Instance Methods ====//

// Constructor
function OrbitalEllipse (orbitAroundPos : Vector3, orbiterPos : Vector3, endingApsis : float, circular : boolean) {
Update(orbitAroundPos, orbiterPos, endingApsis, circular);
}

// Update ellipse when orbiter properties change
function Update (orbitAroundPos : Vector3, orbiterPos : Vector3, endingApsis : float, circular : boolean) {
this.difference = orbiterPos - orbitAroundPos;
this.startingApsis = difference.magnitude;
if (endingApsis == 0 || circular)
this.endingApsis = this.startingApsis;
else
this.endingApsis = endingApsis;
this.semiMajorAxis = CalcSemiMajorAxis(this.startingApsis, this.endingApsis);
this.focalDistance = CalcFocalDistance(this.semiMajorAxis, this.endingApsis);
this.semiMinorAxis = CalcSemiMinorAxis(this.semiMajorAxis, this.focalDistance);
}

// The global position
function GetPosition (degrees : float, orbitAroundPos : Vector3) : Vector3 {
// Use the difference between the orbiter and the object it's orbiting around to determine the direction
// that the ellipse is aimed
// Angle is given in degrees
var ellipseDirection : float = Vector3.Angle(Vector3.left, difference); // the direction the ellipse is rotated
if (difference.y < 0) {
ellipseDirection = 360-ellipseDirection; // Full 360 degrees, rather than doubling back after 180 degrees
}

var beta : float = ellipseDirection * Mathf.Deg2Rad;
var sinBeta : float = Mathf.Sin(beta);
var cosBeta : float = Mathf.Cos(beta);

var alpha = degrees * Mathf.Deg2Rad;
var sinalpha = Mathf.Sin(alpha);
var cosalpha = Mathf.Cos(alpha);

// Position the ellipse relative to the "orbit around" transform
var ellipseOffset : Vector3 = difference.normalized * (semiMajorAxis - endingApsis);

var finalPosition : Vector3 = new Vector3();
finalPosition.x = ellipseOffset.x + (semiMajorAxis * cosalpha * cosBeta - semiMinorAxis * sinalpha * sinBeta) * -1;
finalPosition.y = ellipseOffset.y + (semiMajorAxis * cosalpha * sinBeta + semiMinorAxis * sinalpha * cosBeta);

// Offset entire ellipse proportional to the position of the object we're orbiting around
finalPosition += orbitAroundPos;

return finalPosition;
}

//==== Private Methods ====//

private function CalcSemiMajorAxis (startingApsis : float, endingApsis : float) : float {
return (startingApsis + endingApsis) * 0.5;
}
private function CalcSemiMinorAxis (semiMajorAxis : float, focalDistance : float) : float {
var distA : float = semiMajorAxis + focalDistance*0.5;
var distB : float = semiMajorAxis - focalDistance*0.5;
return Mathf.Sqrt( Mathf.Pow(distA+distB,2) - focalDistance*focalDistance ) * 0.5;
}
// private function CalcEccentricity (semiMajorAxis : float, focalDistance : float) : float {
// 	return focalDistance / (semiMajorAxis * 2);
// }
private function CalcFocalDistance (semiMajorAxis : float, endingApsis : float) : float {
return (semiMajorAxis - endingApsis) * 2;
}
}
</javascript>

== OrbitState.js ==
<syntaxhighlight lang="javascript">
#pragma strict

//================================//
//===   Orbit State datatype   ===//
//================================//

/*
The OrbitState is the initial state of the orbiter at a particular point along the ellipse
The state contains all of the information necessary to apply a force to get the orbiter moving along the ellipse
*/

class OrbitState extends Object {
var position : Vector3; // local position relative to the object we're orbiting around
var normal : Vector3;
var tangent : Vector3;
var velocity : Vector3;
private var orbiter : Orbiter;
private var ellipse : OrbitalEllipse;

//==== Instance Methods ====//

// Constructor
function OrbitState (angle : float, orbiter : Orbiter, ellipse : OrbitalEllipse) {
Update(angle, orbiter, ellipse);
}

// Update the state of the orbiter when its position along the ellipse changes
// Note: Make sure the ellipse is up to date before updating the orbit state
function Update (orbiterAngle : float, orbiter : Orbiter, ellipse : OrbitalEllipse) {
this.orbiter = orbiter;
this.ellipse = ellipse;
this.normal = CalcNormal(orbiterAngle);
this.tangent = CalcTangent(normal);
this.position = ellipse.GetPosition(orbiterAngle, orbiter.orbitAround.position);
this.velocity = CalcVelocity(orbiter.orbitSpeed * orbiter.GravityConstant(), position, orbiter.orbitAround.position);
}

//==== Private Methods ====//

// Returns the normal on the ellipse at the given angle
// Assumes a vertical semi-major axis, and a rotation of 0 at the top of the ellipse, going clockwise
private function CalcNormal (rotationAngle : float) : Vector3 {
// Part 1: Find the normal for the orbiter at its starting angle
// Rotate an upward vector by the given starting angle around the ellipse. Gives us the normal for a circle.
var localNormal : Vector3 = Quaternion.AngleAxis(rotationAngle, Vector3.forward*-1) * Vector3.up;
// Sqash the normal into the shape of the ellipse
localNormal.x *= ellipse.semiMajorAxis/ellipse.semiMinorAxis;

// Part 2: Find the global rotation of the ellipse
var ellipseAngle : float = Vector3.Angle(Vector3.up, ellipse.difference);
if (ellipse.difference.x < 0)
ellipseAngle = 360-ellipseAngle; // Full 360 degrees, rather than doubling back after 180 degrees

// Part 3: Rotate our normal to match the rotation of the ellipse
var globalNormal : Vector3 = Quaternion.AngleAxis(ellipseAngle, Vector3.forward*-1) * localNormal;
return globalNormal.normalized;
}

private function CalcTangent (normal : Vector3) : Vector3 {
var angle : float = 90;
var direction : int = orbiter.counterclockwise ? -1 : 1;
var tangent = Quaternion.AngleAxis(angle*direction, Vector3.forward*-1) * normal;
return tangent;
}

private function CalcVelocity (gravity : float, orbiterPos : Vector3, orbitAroundPos : Vector3) : Vector3 {
// Vis Viva equation
var speed : float = Mathf.Sqrt( gravity * (2/Vector3.Distance(orbiterPos, orbitAroundPos) - 1/ellipse.semiMajorAxis ) );
var velocityVec : Vector3 = tangent * speed;
return velocityVec;
}
}
</javascript>```