Getting started
Your first flight
From opening the playground to a stable orbit, with the smallest program that gets there.
This page takes the default rocket from the default launch site to a stable orbit with a program of about forty lines. It does nothing else: the booster is thrown away, the payload stays aboard, and the upper stage is left in orbit. The rest of this section is about everything this program leaves out.
Every number below comes from flying this exact program in the simulator. Where a number depends on the weather, it is from the default mission's seed 7, and the spread over sixty flights is given at the end.
What the playground opens on
The playground opens on the pad with three things already chosen.
- The vehicle is Aster, a two-stage kerolox rocket: nine Merlin 1D engines under 400 t of propellant, one Merlin 1D under 100 t, and 1.5 t of payload on top. It weighs 535.5 t on the pad. The vehicle explains every one of those numbers.
- The mission is the orbital profile from Starbase, Texas, launching due east (an azimuth of 90°), in random weather rolled from a seed.
- The program is the Full mission example: the reference flight software, about 930 lines, that flies both stages to orbit and home again. It is worth reading once you have written something smaller yourself.
The workbench down the left edge has four panels: Vehicle, Program, Mission and Docs. Open Program, and from its menu choose New program from the starter. The playground asks for a name and opens the starter template, which lifts off, begins a turn and then stops. Replace it with what follows, piece by piece.
The plan
Getting to orbit is two jobs done in the wrong order for intuition. First the rocket climbs out of the thick air, where speed is expensive. Then it goes sideways fast enough that its fall never reaches the ground: about 7.8 km/s at 200 km. The booster does most of the first job, the upper stage most of the second.
A flight program is a function, update(fc), that the simulator calls fifty
times per simulated second with the vehicle's flight computer. It reads the
state, issues commands, and returns. The usual way to organise it is a variable
that names the current phase, and a switch on it. This program has five
phases: liftoff, ascent, staging, insertion and orbit.
Liftoff
let phase = 'liftoff';
function update(fc) {
fc.phase = phase;
switch (phase) {
case 'liftoff':
fc.throttle(1);
fc.steer(90);
fc.ignite();
phase = 'ascent';
break;
phase is declared outside update, so it survives from one call to the next.
Programs run in strict mode, so a variable that was never declared is an error,
not a new global. Writing it into fc.phase puts the name in the top bar, which
is only a label: nothing in the simulator reads it.
The order of the three commands matters a little. An engine lights at whatever
throttle is set when fc.ignite() is called, and a setting persists until it is
changed, so programs set it first. fc.steer(90) asks the built-in autopilot to
hold the nose straight up. fc.ignite() lights all nine engines.
Nothing moves at once. On a real launch site each Merlin gives no thrust for its 0.3 s ignition dead time and then spools up over 0.5 s, and the hold-down clamps release only once the engines are running and push more than 1.02 times the vehicle's weight. Aster leaves the pad at T+0.8 s.
The phase changes in the same call, so fc.ignite() is called exactly once.
Ascent: the gravity turn
case 'ascent':
if (fc.surfaceSpeed > 60) fc.steer(Math.min(82, fc.prograde));
if (fc.propellant <= 0) phase = 'staging';
break;
The rocket rises vertically until it is moving at 60 m/s, which takes about
fourteen seconds. Then it tips 8° over, to a pitch of 82°, towards the launch
direction. From there it does not steer at all in any real sense: once the path
has bent below 82°, fc.prograde — the direction of travel relative to the
ground, as a pitch angle — is the smaller number, and the nose follows
it. Gravity pulls the path over and the nose goes with it. This is the gravity
turn.
Following the direction of travel is what keeps the rocket alive through the thick air. The bending load on the airframe is the dynamic pressure times the angle of attack, and the structure fails at 250 kPa·°. On this flight the dynamic pressure peaks at 44.8 kPa at 12.4 km, 72 s after launch; had the nose been 6° off the airflow there, the vehicle would have broken up. Following the path, the load peaks at 37 kPa·°.
The kick is the whole trajectory. Everything after it follows the path the kick started, so 82° at 60 m/s is not a detail: it decides how steep the rocket is when the booster runs out. The last section shows what 4° either way does.
fc.steer(pitch) with no second argument is fc.steer(pitch, 0): no yaw, so
the rocket stays in the launch plane, the vertical plane along the launch
azimuth. Steering and guidance explains planes and yaw
properly.
The booster burns until it is empty. fc.propellant counts only propellant the
engines can still burn: on real launch sites 1 % of every stage's load is
trapped in lines and sumps and never reaches an engine, which on the booster is
4 t. It reads zero at the moment of flameout, 144.3 s after launch, at 80 km,
climbing at 1.4 km/s with an orbital speed of 3.3 km/s.
Staging
case 'staging':
if (fc.engineState === 'off' && fc.stageNumber === 1) fc.separate();
if (fc.stageNumber === 2) {
fc.setPlane('orbit');
fc.steer(fc.orbitalPrograde);
fc.ullage(true);
if (fc.propellantSettled) { fc.throttle(1); fc.ignite(); phase = 'insertion'; }
}
break;
The program separates only once the booster's engines are off, and
fc.stageNumber then changes from 1 to 2: it is the design index of whichever
stage is now at the bottom. The upper stage lights in the same step, 144.4 s
after launch.
fc.setPlane('orbit') changes what pitch is measured against. In the launch
plane, 0° is horizontal along the launch azimuth over the ground. In the orbit
plane, 0° is horizontal in the direction the vehicle is actually travelling in
space. From here on that is the direction that matters.
fc.ullage(true) fires small thrusters that push the vehicle gently forward.
In free fall, liquid floats away from the tank outlets, and an engine lit on
unsettled propellant fails the start and spends one of its ignitions anyway.
Here the upper stage lights before its propellant has had time to float — the
booster was accelerating it until a few hundredths of a second earlier — so the
ullage is insurance. Coast for a quarter of a minute first and it stops being
insurance: in free fall the propellant counts as unsettled after about twelve
seconds.
Nothing flies the booster after separation. The program has no booster(fc)
function, so the stage falls back uncontrolled and breaks up in the air on the
way down. Aster's booster carries legs and grid fins, but hardware does not land
a rocket; a program does. Coming back down is about that
program.
Insertion
case 'insertion': {
fc.ullage(false);
const climb = (200e3 - fc.altitude) / 60; // m/s: the climb rate we want
const accel = (climb - fc.verticalSpeed) / 10; // m/s²: the vertical acceleration that gets there
const sin = (accel + fc.effectiveGravity) * fc.mass / fc.maxThrust;
fc.steer(deg(Math.asin(clamp(sin, -0.5, 0.8))));
if (fc.orbit.periapsis > 180e3) { fc.shutdown(); phase = 'orbit'; }
break;
}
This is the only piece of guidance in the program, and it is three lines.
The first line asks for a climb rate proportional to the height still to go: at 60 km below the target it wants 1 km/s, at 6 km below it 100 m/s, and at the target nothing. Followed, that brings the vehicle to 200 km with its climb run out, on a time constant of about a minute.
The second line turns the difference between that climb rate and the real one into a vertical acceleration, closing the gap in ten seconds.
The third finds the pitch that delivers it. The engine's acceleration is , and the share of it that points up is , where is the pitch. That share has to supply the acceleration wanted, , plus whatever gravity takes:
is fc.effectiveGravity: gravity, less the centrifugal relief
of the vehicle's horizontal speed. On the pad it is 9.78 m/s². At staging,
about 3 km/s of horizontal speed has already taken 1.4 m/s² off gravity's
9.6, leaving 8.2. In orbit it is zero, and that is the whole reason an upper
stage whose engine cannot lift its own weight — the designer rates Aster's at
0.86 times its weight at ignition — reaches orbit at all. The faster it goes,
the less weight there is to hold up.
The clamp is there for two reasons. Math.asin of a number outside −1 to 1 is
NaN, and fc.steer(NaN) throws an error that stops the program; the vehicle
then flies on with its last commands and no software. The clamp also keeps the
pitch between −30° and +53°.
The burn ends when the lowest point of the orbit clears 180 km.
fc.orbit.periapsis is measured from the Earth's equatorial radius, 6,378 km,
not from the ground below the vehicle: at Starbase's latitude the two differ by
4 km. The simulator's own objective asks for a periapsis above 140 km.
On seed 7 the law does something that looks wrong and is not. Right after staging the stage is 120 km below its target, so it asks for a steeper climb and pitches up to 53° for half a minute. Then the altitude term catches up with the 1.4 km/s climb the booster left it, and it points 30° below the horizon for a minute and a half, bleeding that climb away. The last three minutes are nearly all horizontal speed.
Orbit
case 'orbit':
fc.steer(0);
break;
}
}
With the engine off, fc.steer(0) holds the nose horizontal along the direction
of travel. The autopilot does it with the upper stage's cold-gas thrusters.
The whole program
let phase = 'liftoff';
function update(fc) {
fc.phase = phase;
switch (phase) {
case 'liftoff':
fc.throttle(1);
fc.steer(90);
fc.ignite();
phase = 'ascent';
break;
case 'ascent':
if (fc.surfaceSpeed > 60) fc.steer(Math.min(82, fc.prograde));
if (fc.propellant <= 0) phase = 'staging';
break;
case 'staging':
if (fc.engineState === 'off' && fc.stageNumber === 1) fc.separate();
if (fc.stageNumber === 2) {
fc.setPlane('orbit');
fc.steer(fc.orbitalPrograde);
fc.ullage(true);
if (fc.propellantSettled) { fc.throttle(1); fc.ignite(); phase = 'insertion'; }
}
break;
case 'insertion': {
fc.ullage(false);
const climb = (200e3 - fc.altitude) / 60; // m/s: the climb rate we want
const accel = (climb - fc.verticalSpeed) / 10; // m/s²: the vertical acceleration that gets there
const sin = (accel + fc.effectiveGravity) * fc.mass / fc.maxThrust;
fc.steer(deg(Math.asin(clamp(sin, -0.5, 0.8))));
if (fc.orbit.periapsis > 180e3) { fc.shutdown(); phase = 'orbit'; }
break;
}
case 'orbit':
fc.steer(0);
break;
}
}
clamp and deg are helpers the playground provides to every program, along
with lerp, rad, wrap180, a PID controller and the constants G0, MU,
EARTH_RADIUS and OMEGA. The flight program lists
what else a program can and cannot see.
Launch
Press L, or Ctrl + Enter from inside the editor. A syntax error stops the launch and is underlined in the editor before anything burns; an error at run time appears in the console with its line, and the vehicle keeps flying on the commands it last received.

The timeline across the top of the flight view marks the milestones as they happen. On seed 7 they fall like this:
| Time | Event |
|---|---|
| T+0.0 s | Stage 1 ignition, nine Merlin 1D |
| T+0.8 s | Liftoff: the clamps release |
| T+47 s | Mach 1, at 6.2 km |
| T+72 s | Maximum dynamic pressure, 44.8 kPa at 12.4 km |
| T+144.3 s | Stage 1 flameout |
| T+144.4 s | Separation, and stage 2 ignition |
| T+159 s | 100 km |
| T+438.5 s | Orbit achieved, the periapsis past 140 km while still burning |
| T+438.6 s | Stage 2 cutoff |
The orbit it coasts on is 194 × 203 km at an inclination of 25.85°, with a period of 88.2 minutes. The inclination is set by the launch site: a rocket launched due east cannot reach an orbit tilted less than its starting latitude, measured from the Earth's centre. On the WGS84 ellipsoid that is 25.85° at Starbase, a little under the 26.00° on a map. The upper stage has 8.9 t of propellant left.
Figure · the first flight, replayed
- fc.phase
- insertion
- ENGINES
- lit
- ALTITUDE
- 87.8 km
- ORBITAL SPEED
- 3,336 m/s
- PITCH
- 53.2 °
- DYNAMIC PRESSURE
- 0.0 kPa
- PROPELLANT
- 97.4 t
- fc.orbit
- sub-orbital · apoapsis 201 km
How far this is from a mission
The objectives list will show liftoff, space and orbit done, and the payload, the re-entry and the landing still pending. Left alone, the upper stage stays in orbit.
Three things are missing, and they are the rest of this section. The payload is still attached. The booster was thrown away, which on Aster costs the vehicle its most expensive part. And there is no plan for coming home: nothing times a de-orbit burn over the landing site, turns the stage tail-first for entry, or lights the landing burn. The flight program covers how to structure something that large without losing track of it.
How sure this is
The program was flown sixty times: ten weather seeds each from Starbase, Cape Canaveral SLC-40, Vandenberg SLC-4E and Kourou, ten more from Starbase, and ten on the classic equatorial range. All sixty reached orbit, with periapses from 184 to 200 km and apoapses from 200 to 306 km, and between 3.6 t and 12.0 t of propellant left.
The margin that is thinnest is not propellant; it is the bending load. On the real sites the worst flight reached 169 kPa·° against the 250 kPa·° limit, but one classic-range day reached 241. The gravity turn follows the ground-relative velocity, and on a day with strong winds aloft the air meets the rocket at an angle the ground cannot see. The reference program steers inside a cone around the airflow instead, for exactly this reason.
What to try changing
Lock the seed in the Mission panel first. Every launch otherwise rolls a new day's weather, and a change that seems to help may only have met a calmer day.
- Move the kick. At 78° instead of 82°, eight of ten Starbase days reach orbit. The other two turn too flat, stage low, and the upper stage breaks up pointing 38° off the airflow in air too thick for it. At 86°, only four of ten do: the trajectory is too steep, and the upper stage runs dry before its periapsis clears the atmosphere.
- Follow the airflow sideways from the pad. Replace the ascent steer with
fc.steer(Math.min(82, fc.airPrograde), fc.airProgradeYaw). It sounds more careful and is not. At 60 m/s straight up, the sideways part of the air's motion past the rocket is almost all wind, so following its yaw turns the rocket towards whatever the wind suggests, and the gravity turn locks that heading in. Five of ten days reach orbit, at inclinations of 27° to 46° instead of 25.85°; on the other five the trajectory is too flat, and the vehicle burns up or breaks up in the thick air. - Aim higher. Change
200e3to400e3and180e3to380e3. All ten days reach orbit, but with 2 to 5 t of propellant left instead of 6 to 10. - Deploy the payload. In the
orbitphase, addif (fc.payloadAttached) fc.deployPayload();. The fairing goes first, the payload becomes a vehicle of its own in a 185 × 198 km orbit, and the objective ticks. - Launch from elsewhere. From Vandenberg the mission launches south, at an azimuth of 180°, and the same program reaches a polar orbit inclined about 83°. From Kourou, five degrees from the equator, the orbit is inclined 5.2°.