Juno: New Origins

Juno: New Origins

評價次數不足
Introduction to Vizzy and Launch Automation
由 mreed2 發表
This guide is intended to serve as an introduction for people with minimal programming experience, using the example of automating launches to demonstrate various concepts. In addition, it contains "hints and tips" that might improve the player's experience with Vizzy.

The primary topics covered are:
* Multi-threading
* Basics of PIDs
* Very basics of vector math

The launch script itself includes:
* Roll autopilot
* Launch to inclination
   
獎勵
加入最愛
已加入最愛
移除最愛
Introduction
This guide covers how to setup a moderately complex automatic launching script incrementally. The final launch script includes support for:
  • Automatic staging
  • Automatic second burn to achieve orbit
  • Automatic launch to a desired inclination
It does not include launch to rendezvous. While this is possible, and I even have a pretty good idea how to do it, it would require bringing in an enormous amount of baggage (this, to be exact) in order to answer the question “When will the launch site lie underneath the orbital path of the target vessel.” That seems far to much for a guide intended for beginners.

This is meant as an introduction to a series of guides that I have written, each focusing on a particular technique that is generally useful for Vizzy development:

The first guide is suitable for players that are somewhat comfortable with Vizzy, and the primary topics of focus are “Defining and using custom reference frames to make your life easier,” and “Writing Proportional – Intergal – Derivative” (PID) controllers.
https://gtm.you1.cn/sharedfiles/filedetails/?id=2944674093
These two guides are meant to be used together, and cover the math behind orbital mechanics and a practical implementation of that math. The primary focus is on calculating and executing rendezvous maneuvers (both with moons and other craft).
https://gtm.you1.cn/sharedfiles/filedetails/?id=2961230801
https://gtm.you1.cn/sharedfiles/filedetails/?id=3039843312
In addition to the guides, above, I have written some additional guides on various topics:
This guide shows how to use an MFD to make a Heads-Up Display (HUD) that can be used in first person perspective to overlay graphics over the external view.
https://gtm.you1.cn/sharedfiles/filedetails/?id=2954199325
This covers how to use an undocumented feature (arguably, a bug) to “fake” stucts without using lists.
https://gtm.you1.cn/sharedfiles/filedetails/?id=3033402620
Finally, this guide demonstrates a way to use Vizzy to (depending on your perspective) exploit the Research Point systems used for career mode tech progression. This is primarily intended for people who have no interest in completing rover and plane contracts and have run out of research points that are accessible with the contracts that you currently have opened.
https://gtm.you1.cn/sharedfiles/filedetails/?id=2930785553
Prerequisites
This is intended for people who have played Juno: New Origins long enough (and successfully enough) to be able to reliably get a craft to orbit. It is expected that you know what “Delta-V,” “TWR” refer to, and you should feel confident building a simple orbital rocket from scratch (without using the tutorial) in the designer.
Audience
The ideal reader of this guide is someone who has performed some basic programming in the past. The reader should understand what a "For" and "While" loops are, what a variable is and the like.

In addition, the ideal reader should know that vectors are a thing that exists in math and the names of the trigonometric functions and that they have something to do with triangles.

If you have never written a program in your life
I would recommend starting with a programming game, as that will introduce the basics of programming in a much better way than a simple text guide can.

Specific recommendations include:
https://gtm.you1.cn/storesteam/app/375820/Human_Resource_Machine/
Human Resource Machine (make sure you get the first, rather than the second) uses a very similar GUI to Juno and does a reasonably good job at introducing the basics of programming in small chunks. I do not believe it is necessary to complete the game before starting this guide, but completing the first 2 dozen puzzles or so (without, obviously, resorting to walkthroughs or guides) would put you where you need to be to follow along in this guide. Human Resource Machine is also available on tablets, so mobile users can still access it.

https://code.org/
This is an actual educational resource intended for the purpose of teaching programming. I have not used it myself, but (at a glance) it appears that the coding environment used is highly similar to the one used in Juno (closer than Human Resource Machine). The downside is that there is no obvious order to progress through the lessons, so you may need to do some flailing about to find a reasonable progression path.

If you are an experienced programmer
"Experienced" in this context includes "I regularly play programming games for fun," "I'm in college working towards a computer related degree," in addition to "I write code for a living."
I would recommend reading through the "Hints and Tips" sections, but skipping the detailed examination of the automatic launch autopilot. You will get far more learning out of trying to work it out for yourself than any possible guide. Launch automation is, by an enormous margin, the easiest of tasks to attempt to automate, so "wasting" the opportunity to get your feet wet in the kiddie pool is highly foolish.
Conventions
To reduce the number of screenshots required, as well as improving overall readability, I will be referring to Vizzy blocks that have multiple options, such as this:

as Velocity."Orbit"
Craft file
While I feel that it is counterproductive to release a craft file and a guide like this...

https://www.simplerockets.com/c/9u2d8V/Introduction-to-Vizzy-and-Launch-Automation

There it is.
Hints and Tips: Accessing the VIzzy Editor
The primary way of accessing the Vizzy editor by clicking on the “X” button, located on the right-hand side of the Designer interface:


Alternatively, you can select any part that qualifies as a command pod (this includes Droods) plus Multi-Function Displays (“MFD”), then click on the “Part Properties” button (on the left-hand side of the Designer interface):


This opens the part properties window, which includes an “Edit Flight Program” button (the big blue button):


If you have multiple command pods (or are using an MFD), then this is one way to access the Vizzy program attached to the command pods that are not set as “Primary.” If you prefer, you can also select the part that contains the Vizzy code you wish to edit, then click on the "X".
Yes, this makes it very annoying to edit code attached to a secondary command pod or MFD. This is especially true if you are used to editing code attached to the primary control pod an automatically click on the"X". It is even worse if the command pod you are interested in editing is inside a fairing -- you either need to move the camera in the designer to be inside the fairing or move the fairing out of the way.

If you open the properties of a part that can be the primary command pod but is not currently, there will be a blue “Set as primary” button. In addition to altering which command pod's Vizzy code is edited by default this also changes the "Root Part" in the designer.
Hints and Tips: Multiple command pods on a single craft
It would seem to be useful to have multiple command pods on a single craft – one would contain, for example, an autolaunch script, another would contain an orbital autopilot, and a third would contain a docking autopilot. Using action groups, the user could activate whichever one he needed at the current time and (if the code was from a third party) the code of one part could be updated without disturbing the other command pods.

For this to work in the seamless way described above, it is necessary for any command pod to execute any function that a “Primary” command pod could do. It seems like these settings in the designer would allow us to set this up.


In particular, the “Replicate Commands” and “Replicate Stage Act[ions]” seem relevant.

However...

As far as I can tell neither of these settings has any impact at all. A secondary command pod can:
  • Issue commands to the built-in autopilot.
  • Update the current target (via "target node []") either to a PCI position vector or to a named craft / planet.
  • Use the “Set” command to change the roll input, although the effectiveness of this is significantly muted. I have not tested, but I would assume that the Pitch and Yaw control would work similarly.
A secondary command pod cannot:
  • Use the “Activate Stage” command. If you try, nothing happens.
  • Set the throttle. If you try, nothing happens.
No matter what combination of “Control Settings” you select on the primary / secondary command pod the behavior is unchanged. Given that this is Juno, I assume that this is not a bug because it is working as designed, and would be hard to change (see here for context).

Despite the above, it is possible to split code between multiple command pods on a single craft. Some notes on this:
  • Be warned that messages are only "checked" once per physics tick. If you need to send a message to another command pod then wait until you get a response back, at least two physics ticks will be consumed. This makes messages of dubious use to transfer data outside of "one off" operations.
  • However, it is possible to get read-only access to the variables of another command pod via a fUNK expression. The specific syntax is "$<P#>.VZ.<variable_name>", where "<P#>" should be replaced with the part number of the command pod that contains the variable and "<variable_name>" should be replaced by the name of the variable (as a simple string).
    If the variable to be passed is a vector, the entire string needs to be prefixed with "v:" -- "v:$153.VZ.Vector_Test" will work.
    Due to the limitations of fUNK expressions, it is not possible to retrieve strings via this method. Additionally, this is read-only access -- you cannot change the value of a variable remotely.
  • It is possible to determine if a particular command pod is the "primary" command pod (the one where the throttle and staging works) via another fUNK expression: "$<P#>.CommandPod.IsPlayerControlled", where "$<P#>" is replaced with the part number of the command pod of interest. This can be used to prompt the user to switch the control point as required.
    I suspect, but have not tested, "CommandPod.IsPlayerControlled" will report on the status of the current command pod.
With all of the above, it is possible to split code among several command pods for certain, limited, cases -- in my case, I used a command pod loaded with my full orbital mechanics code, setup some simple logic to automatically recalculate the impact position, then added messages so that other command pods could start and stop the impact position calculation as well as find out what variables to access to "peek" to access the current results. This is quite useful, as impact prediction is necessary for precision landings, but the Vizzy code for orbital mechanics is large enough that it is difficult to edit (even on the PC).

Another advantage of setting things up in this way is that the secondary command pods have their own "pot" of Vizzy instructions to draw from, so (in this specific example) the cost to update the orbital parameters of the orbit and calculate the point of impact isn't "charged" to the command pod that is executing the landing autopilot.
Hints and Tips: Important Vizzy Blocks
The remaining options in the toolbar contain blocks that you can use with Vizzy. I am not going to go over this list in detail, but I will point out some highlights:
  • is the “Events” button, and it contains the  block, which allows you to start a new thread (something that will occur frequently in this guide).
    is the “Variables” button, and allows you to create new variables (via the “Create Variable” button at the bottom), to access variables that you have already created, and to access the “Set variable [] to []” block (among others).
    is the “Custom Expressions” button, and allows you to create new custom expressions (via the “Create Custom Expressions” button at the bottom) and access ones that you have already created.
    is the “Custom Instructions” button, and allows you to create new custom instructions (via the “Create Custom Instructions” button at the bottom) and access ones that you have already created.
    If you are accessing an MFD, you will have one additional option  which contain all the commands for drawing graphics on the MFD and collecting mouse clicks from the player (on the MFD).
Hints and Tips: Renaming / deleting user-defined objects
You can rename or delete variables (by clicking on the on the pencil icon to the left of the name of the variable) freely.

You cannot delete or rename stored expressions or stored instructions so easily. To delete a stored expression or stored instruction:
  • Locate all references to the stored expression / instruction and delete them (click and drag the block to the trash can). This may require splitting code (first detaching the code that lies below the custom expression / instruction, so that you can grab just the stored expression / instruction.
  • Locate the stored expression / instruction definition and drag that to the trash can.
If you did not successfully locate all the references, the delete will fail and the dialog box will tell you how many references you missed. It will not, of course, help you find the references, much less delete them for you.

Once created, the names of stored procedures / expression cannot be altered. Nor can the names of the parameters. If you want to change these, you will either need to edit the XML file directly or you will need to delete and recreate the procedure / expression.
If you are feeling brave, you can perform a search and replace on the XML file for the craft. I've used this to rename stored function names. The directory you need is "%appdata%\..\locallow\jundroo\simplerockets 2\UserData\CraftDesigns". The name of the XML file will be the same as the name of the file in the designer. Making a backup before editing the file is strongly recommended.
Hints and Tips: Snap To Grid
There is an option to turn on (or off) a grid.

There is no option to snap blocks to that grid. I am sure there is a very good reason for this, but it escapes me.
Hints and Tips: Designating a custom target
The “Target Node []” block (in the  “Craft Instructions” section) can be used in two ways:
  • If you pass it a string, it targets that craft / planet / moon.
  • If you pass it a vector, it assumes that the vector is a PCI position vector, and places a reticle at that location labeled “Target.”
The second mode is extraordinarily useful for both troubleshooting (as you can convert a PCI position vector into something you can see) and for providing feedback to the user (you can, for example, designate the current point of impact of the craft).
Unfortunately, the targeting reticle is only visible in the normal 3D mode -- it will not be drawn at all in the map view.
Hints and Tips: Vizzy Performance
By default, Vizzy is limited to 25 instructions per physics frame. This is adequate for a surprisingly large set of purposes, but… If this is not enough for you, as of version 1.3 you can increase the number of Vizzy instructions executed per physics frame, like this:


I setup a very simple test craft and determined that the number of instructions is calculated on a per command part – if you have two command parts in a single craft with different “Instructions Per Frame” settings, then they execute at different rates. Furthermore, I did some testing to see what counts as an "Instruction." It turns out that, in general, a line of blocks in Vizzy counts as a single instruction, regardless of how complex or simple that particular line is. I specifically tested:
  • An “If” statement counts as an instruction.
  • An “ElseIf” statement that executes (that is, the parent “If” was false) counts as an instruction.
  • An “Else” branch that executes (that is, the parent “if” was false) counts as an instruction.
  • Setting a variable to a value counts as one instruction, regardless of how complex the expression is (or is not).
  • No penalty is applied for using a stored expression – the statement / expression that it is part of still only counts as one instruction, regardless of complexity.
  • Using a stored procedure costs 1 extra instruction (vs. embedding the code).
  • No penalty is applied for using comments – comments do not count as instructions.
If you want to experiment with this, this code (loaded on a command pod with no other craft supplied) is what I used for testing:


Add one or more instructions that you want to test, then increase the “2” until the number shown on the screen when the craft is deployed matches the Instructions per Frame number.

At the 1x time compression, there are 60 physics frame ticks per second. I am unsure if this changes with your frame rate, as my frame rate is a steady 60 FPS (but I have very boring looking crafts, so…) If you turn up time compression, the number of physics ticks / second drops proportionally (e.g. “2x” has 30 ticks per second, “10x” has 6 ticks per second). The important things that you want to respond to (everything in the Craft Information tab) will only update when a physics tick occur. You can access (for example) Velocity."Orbit" as many times as you wish, the value will remain constant until a tick occurs. This also occurs for Time.”Time Since Launch”, which is the easiest way to detect a physics tick has occurred.

Thankfullly, Vizzy provides an easy mechanism to say "I'm done with my processing for this physics tick, put me to sleep until the next tick starts." That is the command. In general, you should always include a “Wait [0] seconds” command at either the start or the end of all control loops. This will both ensure that the next iteration of the control loop will start with the maximum time available to complete its processing before the next physics tick and minimize the number of Vizzy instructions executed per second (each instruction executed costs a small amount of power).

As the tooltip notes, increasing the number of instructions per frame increase the power draw – by quite a lot. This launch script (and particularly my customized autopilot) requires 75 instructions per second to run at a steady 60 updates per second – which means that all the instructions required are completing in a single physics tick. This results in Vizzy consuming 17.1 W – 18.3 W (it bounces between the two values). If I increase the instructions per second to 200 (the maximum) then no additional instructions are executed (because I have “Wait [0] seconds” statements in all my loops) but the power consumption jumps to 48.8 – 45.6 W. So, yes, the cost for each instruction increases when you increase the instructions per frame.

While not a fair comparison, if I drop the instructions per frame to 25 (the default value) then I consume a steady 2.5 W of power. However, this is not a fair comparison – my autopilot code only runs 20 times per second (vs. 60), so I am executing 1/3rd the number of instructions per frame. On the other hand, the autopilot can maintain control of the craft, so… Your milage may vary. It is worth pointing out, however, that the command pod itself (even with no Vizzy code executing) consumes 60 W of power, so with this set of code the power consumption is still very reasonable even at the 200 instructions per second.
Hints and Tips: Multi-threading in Vizzy
Vizzy supports (and even encourages) splitting programs into multiple threads. The easiest, most obvious way to do this is to have multiple events in your code, but all the yellow blocks that are in the “Events” tab will spawn a new thread when the appropriate condition is met.

All threads within a single command pod share a single “pot” of Vizzy commands to execute in one second. If you use “Wait [] seconds” or “Wait until []” statements, then some threads will yield the time that they would otherwise consume to other threads, but otherwise the total number of instructions per second will not exceed the setting in the designer.

I have tried both ways, and I believe it is better to split tasks into separate threads (that automatically run concurrently) rather than attempting to package tasks into stored instructions then run them in sequence in single thread. Your milage may vary, of course.

This is how spacecraft work in the Real World™ all the way back to Apollo. The program alarm error messages when Apollo 11 was landing on the moon were caused, to use Vizzy terminology, by a physics frame overflow. Unlike Vizzy, the Apollo Guidance Computer (AGC) uses a priority based multitasking system, so the most critical functions (updating the state vector, generating and executing guidance instructions) executed first, and items of lesser importance (updating the display used by the astronauts to interact with the computer) were dropped instead.

For a little more information, this[en.wikipedia.org] article discusses the issue in a bit more detail while this video:
https://www.youtube.com/watch?v=r_eBGSe5zEQ
Recreates (using an honest-to-goodness real AGC) the problems that occurred on Apollo 11.
Hints and Tips: fUNK expressions
The operator is used by the Juno developer as a way of “Hey, let’s throw a whole bunch of data at the players without needing to organize it into Vizzy blocks.” The documentation (which, honestly, is not all that bad if the reader is an experienced developer) can be found here, with a link (at the bottom of the page) leading to this page.
Some comments on FUNK expressions:
  • FUNK provides a read-only environment. You cannot change any of the values that you access via FUNK nor can you even set a variable via FUNK alone (you can, of course, use the “set variable [] to []” to store the result of a fUNK expression -- but there is no way to set the value of a variable from within a fUNK expression).
  • Despite what the FUNK documentation claims, neither “pi” nor “e” exist in the FUNK namespace. If you need to use these values, you will need to create variables and set them to the correct values.
  • Variables can be accessed in FUNK. If the variable is a number then simply specifying the name is enough. If the result is a vector, then the name of the variable needs to be prefixed with “v:” – e.g. “v:MyVector”. Local variables (parameters in a stored instruction or the loop variable for a for loop) can be accessed in FUNK just like any other type of variable. However…
  • FUNK has zero support for strings. It cannot return a string, it cannot handle string literals, it just cannot handle strings at all. This includes variables that include strings.
  • In many, many cases it will be necessary to build the FUNK expression string at run time – generally, to insert a part number into the string, but there are other reasons as well. The “Format []” block is extremely useful in these cases.
  • A FUNK expression counts as 1 Vizzy instruction, no matter how many operators are included in the expression.
There is a practical example of using a fUNK expression in the auto-staging section of this guide, if you would like to see a practical example.
It is certainly possible to express simple or complex mathematics via a fUNK expression. For example, if you want to add "Var1" and "Var2", you could do this:


Both statements produce the exact same result, namely to place the sum of "Var1" and "Var2" into the variable "Output". The more complex the expression is the more attractive the fUNK expression becomes, but that is simply a matter of compactness.

In this guide, I avoid fUNK expressions except when they are actually required.
Hints and Tips: InputControllers
The page referenced in the previous section (this[wnp78.github.io] page) as the source for documentation of fUNK expressions is not actually titled "Documentation on fUNK expressions." Instead, it talks about an "InputController." What is an InputController?

A number of parts take an analog input that ranges between 0 and 1, with liquid fueled engines being the most obvious, but also including jet engines, motors, aero controls and probably some other items that I am forgetting about. These parts are bound to a number of available analog inputs, such as "Yaw", "Pitch", "Roll", "Throttle", "Up", "Left", "Right", "Slider 1", "Slider 2", "Slider 3", and "Brake." While the default is almost always correct, it is possible to change on a per-part basis which input is bound to which part.


The usage of this option is covered (briefly) in the initial tutorial on rovers, where it talks about changing the binding on wheels from throttle to yaw.

What is not at all obvious is that the word "Throttle" is contained within a textbox and you can edit it and type anything that you wish in that box. For example:


This is not a bug -- whatever you type into this hidden text box is treated as a fUNK expression, and the parts input will be set to whatever the expression evaluates to.

While not used in this guide, the primary use of this in the context of Vizzy programming is to specify the name of a Vizzy variable in this field. As the developer, you get to decide what the variable name is -- just make sure that you create a variable with exactly the same name in your Vizzy code. When setup properly, this allows you control any number of parts individually via dedicated Vizzy variables.

There are not an enormous number of use cases for this, but one is to individually control motors to create cranes, winches and levers. It could also be used to create an autopilot that uses differential throttling to increase control authority.

Another guide of mine:
https://gtm.you1.cn/sharedfiles/filedetails/?id=2954199325
Uses custom InputControllers to control motors repurposed as gimbals for a crude armature. While the actual implementation does not work well (due to gravity and limits of the PID uses to drive the motors) it serve as a demonstration of how InputControllers can be used.
Hints and Tips: Displaying output for the player
Vizzy provides three instructions for displaying text information to the end user:
  • which displays text in an invisible window at the top of the screen (6 lines tall and cannot be resized either in flight mode or via the designer). Messages displayed here will remain visible for a short period of time (1 second) and then disappear. Alternatively, if another display command is executed, the messages will disappear immediately.
  • which writes the information into a local log unique to the command pod that the command is running on. You can access the log in flight by left clicking on the command pod and selecting “View Log.” This is the only way to display the local log – there is no Vizzy command to open the log. This log consists of a large (100?) number of lines and each invocation of “local log []” adding a new line to the top. Annoyingly, each line is prefixed with a thread number (e.g. “3>”), but it is what it is.
  • is a relatively new command. It outputs text into a log shared between all command pods on the currently active craft (accessed via the "Flight Log" button located on the toolbar to the right hand side of the screen during flight), and displays a significant number (10?) of single lines. If the second parameter is set to true, each instance of the “flight log [] []” command gets a single line in the flight log, with future invocation updating that single line. The line visible is a header – if the player clicks on the line, they will see the first three lines of the string occupying that "spot." The expanded display cannot be expanded beyond 3 lines, but it is possible to scroll it (using the mouse wheel).
When using these commands, a subset of HTML formatting strings can be used. The specific list of tags available can be found here[docs.unity3d.com] – however, this list may not be complete, as I know for a fact that “<br>” works to start a new line. So, there is at least a chance that other HTML tags are supported.

In general, I like to ensure that my scripts include a “display []” command that gets executed at least once per physics tick to provide both myself and the user with proof that the script is running and doing something. It is, of course, highly useful in troubleshooting.

I use the “local log []” command primarily to output information that I want to persist for longer than a few seconds, which is the lifespan of the “display” command. Most commonly this is checkpoint-style information – I want to know what value a specific variable has at a specific time, so I throw it in the local log.

The code included in this example shows examples of all three commands, although the data being displayed is the same in all three cases.
Hints and tips: Format operator in Vizzy
The expression is very, very useful when building strings – both for FUNK expressions (see the previous section) but also for display to the user of the script.

The first parameter is a special format string. This string is passed to the standard C# library “Format” operator, so the exact same[learn.microsoft.com] syntax is used. The most important thing to know is that curly braces (“{}”) are used to denote where later parameters should be substituted in the thread, and the number in the braces identifies which parameter will be substituted in each location. This is a zero indexed list, so “{0}” in the above screenshot references the “10000.99” value and “{1}” would reference the empty cell to the right of this value. By far the most common modifiers are “{0:n#}” which is interpreted as “Format the floating point number with # digits to the right of the decimal point.”
Hints and Tips: Fancy custom instructions / expressions names
It is possible to create “fancy names” for stored instructions or expressions – ones that look like this:


First, use the “Create Custom Expression” button to start the creation of a custom expression / instruction:


Enter the initial portion (up to where you want the first parameter to appear) of the name and click on OK. This portion of the name must be unique among all the custom expressions and custom instructions that you define in your project.

Click on Okay and you will be taken to this dialog box:


Click on “Add Parameter” to get this:


Click on Okay to return to the previous dialog box:


Now click on “Add Text”:


Click Okay to return back to the previous dialog box:


Repeat as required to create the entire name.

Be aware that if you make a mistake – typing in text into the “Add Parameter” dialog box or typing in the name of a parameter into the “Add Text” dialog box – then you must cancel the entire custom expression creation process and start over.
Launch Automation Version 1 (1/3)
Assuming you have gotten to orbit manually, you should understand getting to orbit can be broken into several phases:
  1. Waiting for the user to get things started (by throttling up and / or staging).
  2. Going straight up (to ensure you do not collide with terrain)
  3. Slowly decreasing pitch (but not below zero) as time passes, until you apoapsis equals your targeted value
  4. Waiting until “Time to Apoapsis” reaches some low value (30 seconds – 60 seconds), then burn orbital prograde until orbit is achieved.
This can be turned into a Vizzy program like this:


Which works to get to orbit, with a few caveats:
  • All staging needs to be done by the player
  • If you wish to launch in direction other than due East, the player needs to set the heading circle to the correct value.
  • The “Throttle to maintain constant time to AP” logic at the end results in wild swings of the throttle
  • The “Throttle to maintain constant time to AP” requires a very flat trajectory or a high TWR in the upper stage to work properly.
I will go over this step by step. Starting at the top:


Sets the variable “Target_Friendly_Apoapsis” to the desired value. This is “friendly” for two reasons:
  • It is specified in “Meters above sea level,” which is far more intuitive to most players (and is also what the game displays). The correct definition of apoapsis is “Meters above the center of the planet,” and that is the definition required if you want are using the equations of orbital motion.
  • It is multiplied by 1000, so the user can specify the value in “kilometers” instead of meters.


This sets the throttle to 100%. Vizzy, as with most programming languages, represents percentages as numbers between 1 and 0, with 1 = 100% and 0 = 0%.
This command will not cause the craft to launch, because stage 0 (the initial stage) cannot have any engines added to it. If you are using this script on a lander (to take off from another planet), then you will want to delay throttling up until you are actually ready to take off.


This activates the built-in autopilot and sets the pitch to 90° and heading to 0°, assuming that your craft is sitting on the launch pad.
While not clear from the tooltip or text, this takes a “snapshot” of the current pitch and heading values, then attempts to maintain them.


The first of the “Oh, that’s kinda interesting” instructions in Vizzy is the “Wait Until” operator. This pauses execution until some condition is met – in this case, that the “Max Engine Thrust” is greater than zero. “Max Engine Thrust” is generated by iterating over all the engines that are both “Activated” (normally handled via staging) and have fuel available. To make a long story short, this statement pauses execution until the user stages the craft. Many, many, many “wait until” statements will appear in numerous contexts in this guide.
The condition is checked once per physics tick so when code resumes execution a physics tick will just be starting.


This sets the time compression mode to 2x (default, can be configured in settings), the fastest value allowed while in the atmosphere / under thrust. This is optional, of course, but once you have launched the same rocket for the 100th time trying to troubleshoot your Vizzy script, you will appreciate the speedup. 😊
This can be reconfigured (up to 10x) via the Settings menu. The Vizzy script being discussed right now will likely work properly at 10x speed (although timing the staging properly will be difficult). The more complex script presented later is very dubious above 5x. Give it a try if you would like, but all my testing was carried out at 2x.


We pause execution again until the craft is 1000 meters above ground level. This number can be changed depending on where you launch from, but 1,000 meters is very conservative.


We start a loop, which will continue to execute as long as Orbit.Apoapsis is less than “Target_Friendly_Apoapsis.”


If the current pitch is greater than 25°, command the autopilot to reduce the current pitch by 4°. This produces a smooth quasi-gravity turn. Both constants may need to be tuned to your specific craft – if your first stage has a low TWR (< 1.5) you will need to increase the minimum pitch value to ensure that a large enough percentage of the thrust goes towards maintaining vertical velocity. Reducing the “-4” will also help keep nav.Pitch higher for longer and will also minimize “Angle of Attack” (AoA) which is the angle between the direction the craft is facing and the surface velocity vector. If AoA gets too large then the craft will no longer be controllable. How high an AoA your craft can tolerate depends on how much “control authority” the craft has. Gimbals add a little control authority, RCS add more, but gimbaled (liquid fueled) engines add the bulk of control authority during ascent.
This is a “Quasi-Gravity Turn” because the pitch over is handled by the autopilot. A proper gravity turn simply sets the pitch to some large value (say, 85%), then turns the autopilot off and allows aerodynamic forces and gravity to handle the remainder of the pitch over. Not all craft can perform such a “hands off” gravity turn, but almost any craft can do one if you use the autopilot.


This is another very handy piece of Vizzy code. “Wait 0 seconds” will cause Vizzy to pause execution until the physics are updated – a “physics tick.” Until a physics tick occurs none of the values in the “Craft Information” tab will change, nor will any of the commands from the “Craft Instructions” tab take effect. Thus, there is no point to running the loop more than once per physics tick, which is very, very common in Vizzy code. All loops should include a “Wait 0 seconds” either at the start of the loop or the end.
“Wait until []” is checked only once per physics tick, and will therefore exit at the beginning of a new physics tick once its condition is met.
Launch Automation Version 1 (2/3)


This sets the throttle to zero and tells the built-in autopilot to track “Prograde.”
“Lock heading on [Prograde]” has a different meaning depending on what mode the player has currently selected for the “Speed” indicator in the HUD. Specifically, if this is set to “SURF” then this will reference “Surface Prograde,” and if it is set to “ORBT” then this will be “Orbit Prograde.”

This is… Very much less than ideal, as there is no way to force the speed indicator to a particular value nor is it possible to detect what the current value is set to. If the player does not change this manually (by clicking on the speed value) then it will stay in surface mode until periapsis is a positive number (at which point it will switch to orbital mode).
  • Speed shown in surface mode.
  • Speed shown in “Orbit” mode.
Due to this behavior, the “Lock Heading on [Prograde]” (or “{Retrograde]”) is… Of dubious value. We use it here both because it is easy (makes the script shorter) and because the default mode is "Surface Prograde," and it is unlikely that the player will change this setting. The purpose here is to minimize drag while we are still in the atmosphere.


We wait until the amount of thrust produced by the engines on the current stage reaches zero (which may take several physics ticks, depending on what engines are being used and how large they are) and for the craft’s altitude (measured “Above Sea Level”) exceeds the depth of the atmosphere on the current planet.


Now that both the engine has been fully turned off and we are outside the atmosphere we can access the “physicless” time warp modes, so we switch to “TimeWarp1” (which, by default, is 10x). This saves an enormous amount of time when coasting to apoapsis.


We wait until Orbit."Time to Apoapsis" is less than 30 seconds.
Due to the 10x timewarp, there is a fair chance that we will exit this statement slightly after 30 seconds. That's fine -- 30 seconds is just an arbitrary number.


We throttle up to 100% again, and reset the time compression mode to “Fast Forward” (x2).


Another loop, this time checking to see if periapsis is below the target value.


This “locks” the autopilot heading and pitch values to match the specified vector – in this case, “Velocity.Orbit”. This is guaranteed to point at the orbital prograde marker, avoiding the risks associated with the “Local Heading on [Prograde]” command. The downside is that “lock” in this context means “Make a copy of the input vector and point in that constant direction forever” – it does not mean “Each physics tick check to see what the value of the input is and point that direction.” Thus, unlike the “Lock heading on [Prograde]” command it is necessary to invoke this instruction on every physics tick to properly track the prograde vector. In this case, we were going to have to loop anyway, so including the command is not much of an inconvenience.

This is a very crude way of maintaining 30 seconds to apoapsis. “Performance.Engine Thrust” / “Performance.Max Engine Thrust” produces the craft’s actual throttle setting. In particular, if you are using an engine that does not respond to the throttle immediately, then “Performance.Engine Thrust” / “Performance.Max Engine Thrust” will tell you what throttle setting would correspond with the amount of thrust the engines are currently producing. In this case, this is the value that we are interested in – if time to apoapsis is greater than 30, we want to thrust less than the current value, and if time to apoapsis is less than 30 we want to increase the current thrust level.


We use the “Wait [0] seconds” to allow time to advance before we loop.

Finally, we have achieved orbit, and we can perform some cleanup tasks. Specifically, we turn off the built-in autopilot, which either saves battery (if we are using gyros for attitude control) or monopropellant (if we are using RCS), we set the throttle to 0, and we wait for the thrust to terminate before we exit.
Really, we should reset the time compression to 1x here as well -- but I did not want to update the screenshots for such a minor change.
Launch Automation Version 1 (3/3)
That is all there is to it. There is plenty of room for improvement, and the rest of the guide will implement some of the more obvious improvements:
  • Auto-staging
  • Auto-fairings
  • Roll autopilot (which helps with staging)
  • Launch to inclination
  • Improved handling for low TWR upper stages
Autostaging
The most obvious improvement to our code is to automate staging. The following code will do the trick:


Going over this in detail:

This is a second “On Start” event. Vizzy supports any number of “On Start” events, all of which will start automatically and run at the same time. In computer speak, each “On Start” event spawns a new thread.

Here, we will use this functionality to (almost) fully separate our autostage code from our launch code. You could use this code with a rocket that you steer manually, or you could replace the “automatic launch” script with a “Hover and land at target” script and it would still work. This is very powerful, and multiple “On Start” events can largely replace the need for “Custom Instructions.”


This variable is designed to be used outside of this thread, to turn the thread on and off. We will see how this is implemented in a bit, but this is a very common design in Vizzy.


Asparagus staging requires identifying a fuel tank – I will talk about exactly how this is identified and used in a bit. For now, “-1” means “No asparagus fuel tanks identified.”


This probably is not necessary – but it makes sure that all the physics have been properly initialized which is critical for the next couple of statements.


This starts at 0 (in Juno, the initial stage cannot have anything in it) and will change each time the craft is staged.


This starts at 0 and increases each time the craft is staged. This corresponds to the stage number shown in the Staging Editor in the Designer.


This calls a custom instruction that handles identifying any asparagus fuel tank. I will talk more about this later – for now, just note that “Autostage_Asparagus_Part_Num” is set to either -1 (if no tank was found) or a number greater than zero (if a tank was identified).


This loop will run forever (it has no exit condition). This is common for background threads in Vizzy code, and is why the default “While [}” loop has a “True” already filled in.


We wait indefinitely for the “Autostage_Enable” variable to be set to “True.”

As long as “Autostage_Enabled” is true, we continue to execute the inner loop. Note, however, that if “Autostage_Enabled” is set to false, then the inner loop will exit (at the next opportunity – it checks once per iteration) and the outer loop will loop. This will result in it immediately stopping at the “Wait until [Autostage_Enabled}” statement, which will pause execution indefinitely again.

This structure – a “While [true]”, followed by a “Wait until [condition]”, followed by a “While [Condition again]” – is very common in Vizzy code as it allows you to turn on and off background threads as needs change. A very common use is to set “[Condition]” to “Activation Group [].” For example, if you set “Activation Group [1]” as your condition, then pressing “1” (or otherwise activating action group 1) will start the code executing in the inner loop, while pressing it again will prevent the inner loop from running. This is incredibly useful.


If “Autostage_Prev_Stage_Num” does not match the current stage number, then a staging event has occurred. We need to reset all the “This is what normal is for this stage” values, and not perform any further processing for this stage.
This catches manual staging as well – if the user decides to stage manually, this will keep the autostage logic “in sync” with the state of the craft.


Otherwise, we check to see if the stored max engine thrust is greater than the maximum amount of thrust currently available. If it is, then one or more engines have exhausted their fuel and we need to stage. We do not need to do any other handling for the staging event at this point – the next iteration of the loop will note that staging occurred and reset all the values that we track.
While no such mod exists as of this writing, if a “Realistic Failures” mod was written then this code is suspect – if such a mod destroyed an engine, then this would trigger and staging would occur incorrectly. Today, however, we do not need to worry about such things.


We check to see if “Autostage_Asparagus_Enabled” is set to true (another toggle that allows this functionality to be disabled, if needed) and “Autostage_Asparagus_Part_Num” is greater than zero (which, as discussed earlier, means that a part number has been identified as being an “asparagus” fuel tank), then….

We get the amount of fuel remaining in the asparagus fuel tank. Yes, it is necessary to use FUNK expression to get this information – the number of attributes you can fetch on parts is very, very limited if you stick to just the normal Vizzy blocks. There is an entire section (Hints and Tips: fUNK Expressions") that discuses how this operator works, if you would like more information.


If the fuel tank identified as the “Asparagus” fuel tank has no fuel, then trigger a stage.
Yes, this is correct. If you check for "=0," then sometimes the simulation will glitch and a small amount of fuel will remain in the tank that never disappears.


Wait until the next physics tick. This is the only code that runs on every iteration of the inner loop.

In conclusion, this works no matter what other stuff is going on. You could attach this script to a rocket with no other automation and, if you set “Autostage_Enabled” to true then it would automatically stage while the player interacted with the game normally. There are a couple of caveats to all of this:
  1. I have observed autostaging firing off on its own (all engines turned off) when the craft dropped out of time warp. This seems to be a very rare glitch where the “Max Engine Thrust” is set to 0 for a single physics tick when dropping from physic-less time warp (10x or higher) to a physics time warp (<10x). The easiest way to protect against this is to simply turn off autostaging when it is not in active use – perhaps tying it to an action group.
  2. This simply fires off the stages in the order that the user specified. If the user incorrectly judged which rockets / fuel tanks would be exhausted first, then it may drop a functional stage rather than the expected stage. Check your staging, folks. 😊
Autostaging -- Asparagus Staging
Finally, it is time to look at the “Find Asparagus Fuel Tank.” First, what exactly is an “asparagus” fuel tank?

Well, the very short answer is “It is a fuel tank whose name contains the word ‘Asparagus.’” 😊 More usefully, however, an asparagus tank is a fuel tank that is not directly connected to an engine, almost always mounted radially, and is designed to feed another fuel tank (which is connected to an engine) and then dropped when it is empty. In the following picture, the orange tanks are asparagus fuel tanks:


Note that in addition to adding the tanks, you also need to adjust the properties of the radial (or “side,” if you must) interstage:


Specifically, you need to set the “Fuel Transfer Mode to “Normal.” In addition, you will need to manually set the fuel type in the asparagus fuel tanks to match the central stage. Finally, you will need to setup custom staging – it will look something like this:


Stage 1 contains all three engines associated with the rocket, stage 2 contains the radial interstages connected to the solid rocket motors, and stage 3 contains the radial interstages associated with the fuel tanks.
A couple of notes apply here:
  • First, the games delta-v calculator cannot handle asparagus staging at all. It cannot even handle parallel staging either. Such stages will show 0 delta-v, which means that the total delta-v for the vehicle will be… Very wrong. Per a developer, this is not a bug, it is working as designed, and it would be hard to fix. Make of that what you will.
    If you are unaware, parallel staging is where you have several distinct groups of rocket engines, each of which has an independent fuel supply, which operate at the same time but run out of fuel at different times. For example, the solid rocket motors on the Space Shuttle. This good in real-life because:
    • It is much, much easier to add gimbaling to liquid fueled engines versus solid rocket motors.
      Thus, if you have powerful, non-gimbaling solid rocket motors burning at the same time as gimbaling liquid fueled engines, you have far more control authority than you would if only the solid rocket motors were running.
    • Engines that are not firing in a stage is pure "dead weight." If you need, say, 8 engines to get to orbit, you will optimize performance if you design a rocket where all 8 engines are active for the entire flight to orbit.
    In-game, it is impossible (barring use of the "Tinker Panel") to make solid rocket motors that gimbal, so it will be very hard to control a craft which has a "solid only" stage.
  • The amount of fuel per second that can be transferred through a radial interstage is limited. If the engine(s) attached to the central stage consume more fuel than the game allows to pass through the interstage then… The extra fuel will be drawn from the central stage, which defeats the purpose. It is worth pointing out that if the engine is throttled down (or off) it consumes less fuel, and fuel will automatically be drawn from the asparagus tanks to refill the central tank, but… In most cases, there is a hard limit on how large of an engine(s) that you can use in conjunction with asparagus stages.
    Interestingly enough, the fuel transfer that occurs automatically (based on the radial interstage setting) is accounted for separately from the fuel transfer that is performed manually (by clicking on a fuel tank and selecting “Empty,” then clicking on another tank and selecting “Fill”). It is possible to activate this sort of fuel transfer via Vizzy code, so it is possible to increase (likely double) the fuel transfer rate. However, I have been unable to implement something like that in a “general” way (without having to hard code names or part IDs) and have not needed it for the rockets that I have built.
With asparagus defined, now we can look at the code:


This is straightforward:


By default, set “Autostage_Asparagus_Part_Num” to -1, which means that no such tank was identified.


This loop sets the local variable “I” to “Part[0].Min Part ID”, then adds 1, and continues this loop until “i” is equal to “part[0].Max Part ID”. Some comments on this:
  • There may be “null” parts between “Min Part ID” and “Max Part ID.” This will happen, among other reasons, due to staging if you added parts to earlier stages after placing the later stages. For what I am doing here, this is not an issue (although it does waste a few CPU cycles), but in other contexts you may need to try to identify and deal with these null parts.
  • The “Part [0]” syntax, while odd, is correct. Part #0 is never valid, but you can still access the properties associated with it that are craft level (such as “Min Part ID”, “Max Part ID”, and “This Part ID”).


This checks to see if the current part’s name contains the string “Asparagus,” which is used to tell the code which names. I typically edit the names of tanks for asparagus to be “Asparagus Fuel Tank” – there is no need for the tank name to be unique.


This is a safety check that is only sometimes needed. After staging, if the dropped stage remains close enough to the active craft then Juno will not remove the parts from the parts tree of the original craft. In this context, it happens when the staged parts do not stage properly due to aerodynamic forces. However, a fuel tank that is already empty when this code runs is certainly not a fuel tank that we would want to use as a trigger for staging, so the second check.
This is done in two separate if statements, rather than one statement with an “AND” block, because Vizzy does not shortcut Boolean expressions (that is, if the first part of an AND expression is false, it still evaluates the second part) and the fUNK expression will fail (crashing the Vizzy thread, I believe) if “I” is not a fuel tank. By splitting it into two statements, however, we can ensure that the second “if” is only evaluated if the first “if” is true.


Finally, we set the “Autostage_Asparagus_Part_Num” to the current part number.

This code, intentionally, will keep searching for another asparagus fuel tank after it finds a match. This is intentional (there is a “Break” block designed specifically to force an early exit of “For” loops) – there is a strong tendency for parts in later stages to have higher part numbers, so (in theory, at least), this code would support multiple levels of asparagus tanks. I have never tested it, but it might work. In any case, this code only runs once per stage, so the number of wasted Vizzy instructions is minimal.
Autofairings
This is something of a combination of autostaging and asparagus staging:


No “while” loop is required here, because the code deploys all the fairing base parts that it can find. To summarize what is going on:
  • First wait until “Autofairings_Enabled” has been has been set to true,
  • Then wait until the “Atmosphere.Air Density” is a low number (0.001 was determined by trial and error – you could also use a simple altitude trigger),
  • Then iterate over all the parts in the craft, looking for parts whose “Part Type” includes the string “Fairing,” then using a FUNK expression to check to see if the part is a fairing base. If it is indeed a fairing base, we activate it which causes the fairing to deploy.
Once the for loop completes, this thread exits altogether and cannot be revived (this is why most of the other examples use a “while [true]” loop to ensure threads can be revived if necessary).
Activating autostaging and autofairings
A simple modification to the version 1 of the autolaunching script is required to "turn on" autostaging and autofairing:


Just add three “Set variable [name] to [true]” to enable all the code that we have written and that is the only change required. This is the power of multi-threading.
Launching into a specified inclination (1/3)
This is much harder than it seems. If you Google on this topic, you will almost certainly end up on this[www.orbiterwiki.org] page. If you go ahead and implement the math given on that page you will discover that it is not as simple as it seems... 😊

After much hair pulling, I determined that the problem is that the built-in autopilot heading changes as the craft moves through its trajectory. While I will not get into the details, this is a result of the “North-East-Up” reference frame used by the built-in autopilot not being an inertial reference frame. The web page assumes that once you calculated a launch azimuth you will continue to thrust in that direction relative to the far distant stars (an inertial reference frame).

After many trials and tribulations, I managed to work out a way to get the right results, and even make the math much easier to follow.

First, we need to define the user defined function “Velocity in Circular Orbit.” (source[www.orbiterwiki.org])


This takes as input the radius of a potential orbit (which must be measured from the center of the planet) and returns the speed of the craft in that orbit. This is an implementation of one of the formulas in this page. Note that this assumes that we will reach a perfectly circular orbit – while this is inaccurate, the script is good enough to get close, and that is good enough for what we need to do.

Next, we need to implement two standard (but more advanced than are supported out of the box) vector operators.


Vector rotation is defined here[en.wikipedia.org] and vector projection is defined here[www.maplesoft.com].

While the math that implements these expressions is complex, the meaning of these expressions should be obvious from the name. The only thing that might be confusing is "What is 'the plane normal to a vector' mean?" Both of the linked pages contain graphical exmaples of how a vector that is 'normal' (at right angles to all points on) can be used to define a plane -- for the first expression, to define the plane in which the vector is rotated, and for the second expression to the plane into which the vector should be projected.
Just to add to the confusion -- "normalizing a vector" is an entirely different operation. Normalizing a vector means "Change the magnitude of a vector to 1 while preserving its direction."

A normalized vector can also be a vector that is normal to a plane, but the two terms are totally unrelated to one another. Yes, this is confusing, but it is not my fault, I swear...😊😊😊


These two user defined functions convert the vectors from PCI (“Planet Centered Inertial,” a reference frame where the axes are aligned with the center of the planet) to NEU (“North – East – Up”), where the axis match the values shown in the built-in autopilot gadget. I go into great depth about why the NEU reference frame is useful and how it is defined in my other guide:
https://gtm.you1.cn/sharedfiles/filedetails/?id=2944674093
Especially starting with the section “Redefining axes for fun and profit,” so refer to that guide for more information. For this guide, the important thing to know is that the NEU reference frame is the one used by the built-in autopilot to generate heading and pitch values and the "X/Y" portion of a vector in this reference frame is always tangent to the surface of a sphere centered on the current planet.



This function addresses a limitation of the built-in “angle” operator. The angle operator returns the angle between two vectors – but it always returns a positive number. This makes sense – “A angle B” and “B angle A” should return the same result and there is no reason to favor the negative result over the positive result. Additionally, the angle operator never returns a result greater than 180°, which also makes sense -- the angle between two vectors can always be represented by a number less than or equal to 180°.

In our current application, we do have a reason to prefer one angle over the other and we do need the negative result. The above formula (source[stackoverflow.com]) does what we want if we can provide a vector that is normal to the plane in which the two vectors reside, which we can (alternatively we could generate a vector normal to the plane by finding the cross product between the two vectors). This specific implementation returns negative results when Vec2 is to the left of Vec1, if you orient Vec1 to point straight north and inNormVec is a vector normal to the plane that contains Vec1 and Vec2. You can reverse this behavior by reversing the order of the operands in the cross product. Alternatively, you can achieve the same result by multiplying inNormVec by -1 (which reverses its direction).
This game uses a "left handed cross product." The standard used almost universally in math is a "right handed cross product." As a result, any formula that you find online that uses the vector cross product (denoted "⨯") is very likely to return the wrong results. The immediate effect of using a left handed cross product where a right handed cross product is expected is the direction of the result vector will be wrong by 180°.

The "handiness" of a cross product can be changed in two ways:
  • Reverse the order of the operands -- if "A ⨯ B" is correct when cross products are right handed, then "B ⨯ A" will return the correct results with left handed cross products are used.
    As should be obvious, cross products are not "communicative" -- the order of the operands matter.
  • Multiply the result by -1 -- if "A ⨯ B" is correct when cross products are right handed, then "-1 * (A ⨯ B)" will return the same results when left handed cross products are used.
    "*" represents scalar "multiply a vector by a scalar," which is a totally different operation than the cross product.
In this particular application, the handiness of the cross product does not matter a great deal -- it changes which direction (as measured treating inVec1's direction as "0") is positive. However, the designation of "inVec1" as the "reference vector" (from which angles should be measured) is arbitrary. If you prefer to define "inVec2" as the reference vector, then the angles will be correct but the sign will be reversed. Additionally, if you multiply "inNormVec" by the scalar "-1" then you will also reverse which direction is positive.
Launching into a specified inclination (2/3)


This is a hack, and nothing but a hack. Explanation:

If the target inclination does not match the latitude of the current location, there are two possible inertial launch azimuths that will reach this inclination. Due to the reference frame that I am using, these two solutions will differ by the sign of the X component of the direction vector. The one with a positive X will be “northerly” and the one with a negative X will be “southerly.” A picture is worth a thousand words, so...



An example of a "southerly" orbital trajectory for some particular inclination.



A "northerly" trajectory for the same inclination.

For reasons that totally escape me, if the initial latitude is positive, then the southerly solution will remain valid even as the craft tracks over the ground. On the other hand, if the initial latitude is negative, then it is the northerly solution will remain valid. If you use the wrong solution things will appear to be correct initially, but as the vehicle tracks to orbit an enormous difference in direction will appear between "This is the direction the craft should travel" and "This is the direction the craft is actually traveling." This will be much larger for low inclination orbits, and requires a high (1,000 km is what I used for testing) apoapsis to be visible.

If you allow the craft to burn away the error on the new vector, which may result in the craft burning anywhere from 45° to 90° away from the current orbital velocity vector, you will end up in an orbit with the desired inclination – but the track will be northerly if it was initially southerly, or southerly if it was initially northerly. In effect, the craft “bounces” off an invisible wall.

Common sense would indicate that this has something to do with crossing the equator, but no, that is not the problem. The crafts latitude / longitude can be shown in the flight view info panel, and the latitude is not anywhere near to zero when the problem becomes visible. Nor is it related to passing over the poles, which is not surprising as most orbital paths do not get anywhere close to the poles.

Looking at the maps, it would seem reasonable that it is related to where the trajectory should curve back towards the equator. I suspect that this is accurate, except... The maps shown above are 2D projections of a 3D model and the game works entirely in 3D. The orbital path in 3D space is simply a circle (centered on the center of the planet) that wraps around the planet, and that is how the formulas used here treat it.

To reiterate: The code shown above assigns “Launch_Aszimuth_Mult” the value of -1 * the initial sign of LLA(nav.”Position”).X (which is the latitude component, in degrees, of the latitude / longitude / altitude above seal level triple). This value does not change after it is initially set and… Everything works. Obviously, swapping the sign again should, in a reasonable world, produce the alternate trajectory, and it does indeed start returning that trajectory – but then fails.

I am completely at a loss of where to start on this. It should either work in both cases or it should fail in both cases.


With the hack out of the way, we start by calculating the "Inertial Launch Azimuth." While this is a valid name (it is the heading that the craft would need to travel on if the planet was not rotating, thus "Inertial") it is also simply the heading of the velocity vector of an object in an orbit with the desired inclination. This heading will be constant when measured versus an inertial reference frame (such as the default, PCI, reference frame used in the game) but will vary if measured relative to a non-inertial reference frame (such as the NEU reference frame used by the built-in autopilot).

Regardless of the nae, we break the calculation down into a number of steps:
  1. We calculate the ratio of the cosine of the desired inclination by the cosine of the latitude of the current craft’s position.
  2. If the number from the previous step falls into the range [-1…+1], then we set the Inertial Launch Azimuth to the arcsine of the value.
  3. Otherwise, it is not possible to reach the targeted inclination from the current location. We therefore set "Launch_Azimuth_Inertial_Radians" to either 90° (due East) or 270° (due West) depending on the sign of the value calculated in step 1. This will give us the trajectory that is closest to the targeted trajectory.
  4. In order for the math to work properly later on, we set the targeted inclination to the minimum valid value as well. This change is not persisted from iteration to iteration. In part, this is forced -- Vizzy parameters are always pass "By Value," so any changes disappear once the custom expression exits -- but is also by design. As the craft moves along its track, it may eventually reach a latitude where it is possible to reach the desired trajectory. If this occurs, the current implementation will perform a "dog-leg" maneuver to achieve the originally targeted inclination.
    The hack I ranted about earlier forces all trajectories to head towards the equator immediately, which maximizes the likelihood that this will come into play. It is more likely to be helpful when the orbital radius is high and the launch sites inclination is high. It actually works pretty well


We convert the current orbital velocity of the craft into the NEU reference frame, and then we zero the “Z” component. This net result of this is produce a velocity vector that is tangent to the surface of the planet. When the craft is sitting on the launch pad, this will return the velocity imparted to the craft due to the rotation of the planet, considering the altitude and latitude of the craft. This significantly simplifies the work that needs to be done vs. the reference page.

The source[www.orbiterwiki.org] uses 2 dimensional vectors in the form of "East - North." When I setup the NEU reference frame, I arbitrarily decided to order the components "North - East - Up." For consistency with my other work, I use NEU in this code as well, which means that the source material does not quite match up with what I implement.


Next, we calculate the velocity vector the craft would be in if it were already in the desired orbit. This is a two-step process:
  1. Use the previously defined stored expression to calculate the orbital velocity for a circular orbit at the desired apoapsis value. This the magnitude of the velocity vector.
  2. Multiply the value from the previous step by a unit vector that points in the direction of the Inertial Launch Azimuth in the NEU reference frame.
The result of this calculation is the velocity vector of a craft in a perfectly circular orbit of "Desired_Friendly_AP" with the targeted inclination.
Launching into a specified inclination (3/3)

We subtract the initial velocity from the final velocity – this produces an error vector.

This assumes that the current tangential velocity (velocity tangent to the surface of a sphere whose radius is the craft's altitude) of the launching craft will reach its maximum value once it achieves the desired circular orbit.

This assumption will always be correct for craft that are siting stationary on a launch pad. However, we will be using this custom instruction in a very different way later and the assumption will no longer always be valid then.


We convert the vector from the previous step back into a PCI vector. This is necessary because a NEU vector is only valid of a single physics tick, but a PCI vector is good forever.

Advanced Autopilot
As it turns out, the major difficulty with implementing "Launch to Inclination" into a launch script is not simply calculating the required launch azimuth. While it is not trivial, it is not all that bad.

However...

What "Heading" means in this game depends on the location of the active craft and for a craft traveling at near orbital velocity it can change fairly rapidly. The math returns a heading that is valid at take off but assumes that the direction is "locked in" at take off -- that is, it is an inertial direction after take off. So, if you just set the craft's heading to the returned launch azimuth it does a terrible job at achieving the desired inclination.

While it is possible to set the built-in autopilot to point in a direction given by a PCI vector, the single vector is used to set both the pitch and heading. This is not desirable in our use case as we need to control pitch independently to properly perform a gravity turn.

So, we need to extract the correct value of "heading" from a PCI vector. We could just dedicate a thread to that function and be done with it, but it is also desirable to control roll (which makes staging much more dependable when you are changing inclination) and why not pitch while we are at it?
Advanced Autopilot: PItch
Each of these autopilot will be setup as an independent thread, with the capability of being turned on or off individually, plus an overall enable / disable.

Out of the three, the pitch autopilot is the easiest:


This is standard enough that I do not think it is useful to break it down on a line-by-line basis. The only interesting lines are:


This adjusts the built-in autopilot’s target pitch to the craft’s current offset plus some amount. This will result in the craft pitching up (or down) at a smooth rate.


There are two reasons that we might exit the while loop – it could be because the pitch autopilot specifically was turned off, in which case we should leave the autopilot settings alone. Or it could be because the autopilot as a whole was turned off. In the later case we should disable the built-in autopilot as well.

Thoughts on further developement
Adding the option to track orbital velocity, with an offset. This would be very useful in the "Hold constant time to apoapsis" portion of the ascent would significantly simplify the ultimate autolaunch script. I did not add it because I did not want to add yet another boolean to change modes.
Advanced Autopilot: Heading

This should again be largely self-explanatory, but one line deserves some further examination.


Working from the inside out:
  • First, we find the vector that results from projecting “Alternate_Autopilot_PCI_Heading” onto the plane whose normal is nav.”Position”. The resulting vector will still be a PCI vector, its direction will be tangent to the surface of a sphere whose center is the center of the planet, and whose magnitude will be equal to the velocity tangent to the surface of the sphere. If the craft was in a circular orbit, this vector would be the orbital velocity vector.
  • We then perform a signed angle comparison where the nav.”North” vector (which is defined to be 0°) and the vector from the previous step, where the sign of the angle is determined by the nav.”Position” vector.
This strips the nav.”Pitch” component from a PCI vector, then uses it to produce a heading value suitable for feeding into the built-in autopilot.
Advanced Autopilot: Roll (1/2)
This is much harder than the other two axis because we are starting from scratch -- the game does not provide any sort of autopilot for the roll axis. Thus, we need to build a PID ("Proportional–Integral–Derivative") controller. There is, as you might expect, a Wikipedia article on this subject (here[en.wikipedia.org]). I'm pretty math heavy, and I have spent a lot of time on this subject and I find the article completely useless, but... Maybe it will be different for you?

A PID controller is defined to be a formula or process that takes an input value (in this case the current roll angle of a craft) and turns it into an output value (in this case, a value in the range [-1...+1] for the "Roll" input) that results in a predictable change in the input value. When properly configured, a PID can eventually drive the input to a desired value even when the linkages between the input and output are poorly understood. PIDs are extremely common in a wide variety of fields from aeronautics to industrial machine design.

As you might expect from the name, PIDs have up to three components:
  • Proportional: This is the simplest term and is just the error between the desired value and the current value of the system. In our case, subtracting the current roll value from the targeted roll value covers this term.
  • Integral: This term is derived from the previous term and represents the historical trend in the error value. A typical way to generate it is to average the last X seconds of error values, although there are other ways. The goal of this term is to identify and eliminate error caused by bias in between the input versus the output. For example, if the error over the past 5 seconds has been "stuck" at 0.1, then we need to add a bit more output to get "unstuck."
  • Derivative: This term is also derived from the proportional term and represents the expected future value of the output. A typical way of generating this is to find the difference between the current input value and the input value of the previous iteration and divide by the amount of time that passed between the two samples -- a literal "rate of change." The goal of this term is to identify trends, and in particular, to ensure that the rate at which the input value changes does not get excessively large or excessively small.

So, with the definition out of the way, onto the code!

But wait, Vizzy does not expose a "Roll Angle" at all. It does expose a "Bank Angle," but... Bank Angle range is [-X...+X] where X varies based on current pitch (if pitch = ± 90° then X is also 90°, but otherwise the maximum and minimum values of bank angle are something less). It turns out that there is a formula that uses bank angle and pitch angle to produce roll angle (source[physics.stackexchange.com]):


This works, but is not ideal. The problem is that it produces a roll angle that varies between [-90°...+90°]. There is no way to distinguish between "Craft is upside down and wing level" and "Craft is right side up and wing level." It would be possible to modify the above formula to add this information, but there is a better way.


This formula (which I came up with on my own, so no source) simply measures the angle between the craft's yaw axis unit vector (which is a PCI vector that points in the +X direction in the local reference frame) and the position vector on the plane defined by the craft unit roll vector (which is a PCI Vector that points in the +Y direction in the local reference frame). This produces a roll angle in the range of [-180°...+180°], as expected. This is the formula that the reminder of the code in this guide expects to be used.

Regardless of which formula you use, roll is not defined at all when pitch is ± 90°. Both formulas prevent the roll angle from being calculated at this value, but nearby values (say, 89°) are going to be extremely sensitive to minor variations in input.

One other thing before we get to the autopilot itself. When I was testing the autopilot, I noted that the was being reset to 0 at the start of each physics tick. I do not know why this was happening, and later events make me wonder if this was actually happening, but it certainly seemed like any physics tick that I did not explicitly set the roll input to a value it would be treated as zero. In order to fix this, I wrote a dedicated thread that monitors a variable and sets the roll value to the value of the variable:


This is intentionally a very simple thread -- all it does is sets the roll input to whatever the variable "Alternate_Autopilot_Roll_Output_ is set to.
If you do not have the "Set [Roll] to [0]" at the end, then the craft will start spinning when the autopilot is off. This indicates that my belief that the game was automatically setting Roll to 0 if Vizzy code did not set it to another value on that particular physics tick is wrong.

But...

Unquestionably, when I placed the set [Roll] commands directly in my autopilot script it did have problems with the roll value being reset. So, I don't know -- but the dedicated thread works, and I am not going to fiddle with it further.
Advanced Autopilot: Roll (2/2)
Finally, we can get to the autopilot itself:


Unlike the pitch and heading autopilots, we will go through this in detail.


This is the same opening as all the other threads – the only interesting thing here is we block execution of the loop if the absolute value of the pitch is > 85°. Due to the way roll is defined, it becomes very flaky as pitch approaches +/- 90° and is not defined at all at 90°. Disabling the autopilot at very high and very low pitch values makes the autopilot more reliable.


This allows the first iteration of the loop to run without the need for special case code. This means that the first iteration of the autopilot will believe the roll rate is 0° per second rather than the correct value, but… We cannot calculate the proper value and 0° is a reasonable guess.


This is also to allow the first iteration of the loop to run without special case code. In this case, we are avoiding a division by zero when calculating the rate. Since we know that the rate will always be 0° we could set this to any arbitrary value, but this is a reasonable guess as to what it should be.


The inner loop, just like all the other threads.


This code determines how long it has been since the last time that the roll autopilot was executed. This is used later to normalize the rate calculation which allows various constants to, well, be constant even if the code is not able to execute every physics tick.


We capture the current roll angle. We store the roll angle in a variable to minimize the risk of a physics tick occurring during the execution of the code.
There is a slight risk of a physics tick occurring between this statement and the "set [Alternate_Autopilot_Roll_Tick_Length]" statements. When this occurs, the roll rate values calculated later will be incorrect which interferes with the operation of the autopilot.

I attempted replacing these two statements with one, setting both values in one instruction via the "Vec([],[],[])" command. While this worked, it significantly slowed down the execution of the code. Due to the slow down, and because it made the code harder to follow, I removed it and accepted the risk of an unwanted physics tick.


This could just as easily be called “Angular Velocity Roll,” because that is what it is.


We store the current value of roll for the next iteration, which is required to calculate the Roll delta.


We capture the error (in degrees) between the current roll value and the target roll value.


This defines a deadband – if the roll angle error is less than 0.25° we do not attempt to further reduce the error.


We do however, need to damp out the angular roll rotation rate, as otherwise we will drift right on past our target value. This code performs this task, damping roll down until the rate is less than 0.05°.
The "-0.25" value is less than ideal, although it does work almost always with my test craft. The problem is that this can produce very large roll outputs (which is necessary to stop high rates before it rolls out of the deadband altogether) but when roll authority is particularly high (high TWR generated by multiple engines) the high values returned will not only cancel the roll rate in a single physics tick it will reverse it and increase it to a very large number. If the large number is high enough (> 180° per physics tick) then the code can no longer calculate an accurate roll rate and therefore cannot cancel the roll. This results in the craft spinning with increasing rates until the autopilot is disengaged in physicless time warp is engaged (physicless time warp cancels all rates when engaged).

If you make the number smaller, it cannot cancel out rates when the roll control authority is not as large as it is in the worst case scenario, so... Between a rock and a hard place here. The ideal solution would be to calculate how large the roll control authority is on a per stage basis, but I cannot think of a way to do this.


Otherwise, the error in roll exceeds the specified deadband, so we need to try to zero out the error. This code limits the roll rate to 5° / second and only uses 2.5% of the maximum available roll authority to try to increase the rate. In practice, this results the roll control alternating rapidly between 0 and 2.5% as it seeks to maintain a constant rate of rotation.


If absolute value of the rate is greater than or equal to 5° then we are moving in the correct direction but we may be going "too fast." This block of code does this, using increasing amounts of control authority to try to halt excessive rotation rates.


Otherwise, we just set the roll output to 0. This will only happen with the roll rate is between greater than 5° and less than 7°. When all is well, the actual roll rate tends to be close to 5° / second.


This checks to see if a physics tick has occurred while the loop was being executed. If not, then it uses the standard “wait [0] seconds” to wait until one occurs. Doing it this way, rather than a simple “wait [0] seconds” significantly improve the update frequency when the “Instructions per second” control is set to a low value (25 or 50). However, it does run a risk of creating a “rolling shutter” effect, where a physics tick occurs during the execution of the first three instructions of the loop. As mentioned earlier, the extraction of the data into an atomic operation using a temporary vector would prevent this from occurring.


Finally, when we exit the inner loop (which means that either the autopilot was turned off or the absolute value of the pitch angle exceeds 85°, we set the roll value to zero.
Autolaunch Version 2 (1/3)
With all of that done, we rewrite the original launch script to take advantage of all the new functionality, in addition to some other minor improvements.


First, much more setup, so we do all of that.

In addition to setting all the various flags that direct other threads to turn off or on, we also have a new flag, "Autolaunch_Active." This will allow another layer of threads to determine the autolaunch script is executing and to alter their behavior if it is.

We also invoke "Calculate Launch Azimuth" here and set the heading autopilot to track its output.
The initial takeoff script is still very simple, although we do reduce the minimum altitude requirement from 1000 to 50, as this works better with the craft used for testing.
It turns out that 50 is not really enough if you are launching from the DSC small pad, due to the way in which "surface" is defined in the game. Structures do not count for purposes of calculating "Distance From Surface" and the DSC Small Pad extends beyond an actual cliff. Thus, ASL is greater than 50 when it is sitting on the pad, so it immediately starts to pitch over.

It does work (most of the time -- there is some risk of running into the lighting rods, depending on which inclination you are launching to) so I left it alone.


We make use of the “Relative_Pitch” mode in the autopilot to simplify the gravity turn autopilot. In addition, we change the target apoapsis to “Specified value – 1000”. This produces a more circular orbit than targeting “Target_Friendly_Apoapsis” directly.
This is because most engines in Juno do not respond to changes in throttle instantly. One effect of this is that you cannot switch to a physics-less time warp immediately after setting the throttle to zero but another effect is that the engines will continue to produce thrust for a time after they have been commanded to stop. This extra thrust will, inevitably, result in an increase in apoapsis after the loop has exited. By setting the exit condition a bit lower than the actual target, we reduce the overshoot significantly.


The coast phase is relatively unchanged – for reasons that will not be obvious until later, once we exit the atmosphere we turn back on the custom autopilot even if this means no longer tracking the velocity vector in pitch. Doing it this way allows us to ensure the heading is correct when we start the constant time to AP phase, which is more important.
An alternative here is to add a "Track orbital velocity vector in pitch only" mode to the pitch autopiliot.
Autolaunch Version 2 (2/3)


This is a major rewrite. Staring from the top:


This sets the pitch component of the custom autopilot to the current pitch of the orbital velocity vector.


This will be used to calculate “Delta_Time_To_Apoapsis”, which will make it far easier to control time to apoapsis.


We need to store the commanded throttle setting, because there is otherwise no way to get it. You can get (and we will) the actual throttle setting, but if you want to know the value you have requested the craft to achieve then you need to store it in a variable.


This supports yet another autopilot control mode – like the “Relative Pitch” mode discussed during the autopilot this also drives the autopilot based on an offset to a known pitch value – but in this case, the known pitch value is the pitch of the velocity prograde vector. Why implement it here, instead of adding it to the autopilot? Personal preference, honestly.



We set the throttle to 100%, set the time compression mode to 2x (from 10x), then wait for some amount of thrust to be produced.


The exit condition is unchanged.
Due to the nature of orbital mechanics and the way that this script works, the throttle setting when the loop exits will be very low (almost certain 0.5%, which is as low as this script will command). The amount of additional thrust produced switching from 0.5% -> 0% is very low and therefore it can be neglected here.


We calculate the pitch of the velocity vector.


We calculate the throttle setting that corresponds with the amount of thrust being produced.


We calculate how much time to apoapsis has changed since the last time the loop ran.


If delta time to apoapsis is less than 0, then we need to increase power.


If we are not producing the maximum amount of thrust then we increase the amount of thrust we are producing by 0.5%.


Otherwise, we pitch up (by 0.5° more than we were pitched up before), but only up to 60°. If we are pitched up to 60° and still cannot hold time to apoapsis constant then we just do not have a high enough TWR to make it to orbit.
Pitching up allows some of the thrust being produced to directly counter gravity, which will delay time to apoapsis. However, thrust being used in this way is "wasted" -- it is not increasing tangential velocity and therefore is not assisting in achieving orbit.

Thus, if you need to point the engine straight up (pitch = 90°) you are hovering, not going to orbit. That's why the code will not pitch above 60° trying to hold time to apoapsis to a constant value.


On the other hand, if delta time to apoapsis is greater than zero, we are moving further away from apoapsis and we need to reduce power.


If we have previously pitched up, we pitch back down (to a minimum of 0° relative to the pitch of the velocity vector.
Technically, you can pitch down to 0° (absolute) and this is slightly more efficient. You can even pitch further down, although this is wasteful in the same way that pitching up is inefficient. Real-world rockets sometimes do pitch below the horizon during their circulation burns, but this is because real-world engines generally cannot be throttled, so if your engine is producing more thrust than you can need then thrusting down may be the "best" solution.


Otherwise, we reduce the commanded throttle setting to by 0.5%, but not below 0.5%.


Finally, we set the desired throttle and pitch values, and update “Prev_Time_To_Apoapsis” for the next iteration.

The main change in this block is to add pitch control. The final stage on my testing craft has a relatively low TWR (0.87) and if the rocket is not very close to achieving orbit it cannot maintain a constant time to apoapsis when pointed prograde, even at 100% throttle. When the launch is due East the added velocity from the planet’s rotation boosts the orbital velocity at staging enough that the upper stage can do the job – but if you try polar or retrograde orbits… Well, the stage just is not quite enough. However, with a few degrees of up pitch (while fuel inefficient) it can do the trick.

Finally, we turn everything off before returning control to the player.
Autolaunch Version 2 (3/3)
There is one odd omission in all of the above code -- nothing in there interacts with the roll autopilot. That is because it is easiest to deal with the roll via a separate thread, like this:


The autolaunch script will automatically set the roll to 0° during stage 1, so this script waits for stage 2 to start and set the roll to 90°, then waits for stage 3 and set the roll back to 0°.

The correct value of roll (and what stages those roll values are associated with) is craft specific, so it makes sense to separate it from the craft agnostic auto launching script.
As mentioned during the roll autopilot description, the 3rd stage of the testing craft has an absurd amount of control authority in the roll axis (while the 3rd stage engines are firing, at least). As a consequence, the autopilot malfunctions in a variety of ways during the 3rd stage. In the vast majority of cases, it simply becomes very jerky, but every once and a while the craft will spin out of control. When it does spin out of control, most of the time it manages to maintain heading and pitch control and once the engine is turned off and 10x time compression is enabled the roll rate is killed. While this leave the craft in a random roll attitude, there is rarely much fuel left in the third stage, and the autopilot can control roll with no problem during stage 4 (with only a single engine, roll control is limited to the relatively small gyros built into the command pod, so control authority is much diminished).
Testing / Troubleshooting Launch to Inclination (1/2)
If you setup the heading autopilot like this:


You can try out the naive approach of "Launch Azimuth" corresponds with "Heading" used by the built-in autopilot. It does not work very well, however -- with a target inclination of 115° (launching from the Juno Pad) the final inclination is 113.51°, which is not even slightly acceptable. As mentioned earlier, this is caused by the fact that what the autopilot refers to as “Heading” is measured relative to the current position of the craft, while the launch azimuth calculation produces a value that needs to remain constant (relative to the distance stars) indefinitely.

So, if we change the autopilot to use the PCI heading we calculated, we get this:


This produces much, much better results – with a target of 115° it achieves an inclination of 115.20°. However, given that this is an automated solution, the expectation is that it should be 100% accurate, so what gives?

To troubleshoot this, a status thread is helpful:


This is a very simple thread – it just spams the display with a range of values that are interesting for troubleshooting. However, it is extremely expensive in terms of Vizzy instructions. A much, much, more efficient way to implement this is via a single very long format statement with a very, very long list of parameters. When implemented in this way, the roll autopilot updates 15 or 30 times when “Instructions Per Second” is set to 25. If all the string statements are included as separate instructions (as shown) then the update frequency drops (at 25 Vizzy instructions per second for the roll autopilot) to 7.5 (and sometimes lower than that). On the other hand, splitting it up into multiple “Set variable [] to []” statements is much more readable and maintainable. And makes for much better screenshots, cannot forget that. 😊

Which you prefer is a matter of personal preference as you can simply increase the instructions per frame to achieve 30 updates per frame and use the strings.

This thread introduces two new user defined expressions:


Modifies the input angle so that it falls within the range [0°…360°]. This significantly improves readability when outputting angles – most of the time.


This does two very useful things:
  • It allows you to display a vector without showing 4 numbers each with 20+ digits of precision.
  • It normalizes the components of the vector, which makes it much easier to compare vectors with one another.
A brief discussion of what is shown
Assuming that you have been following along, I would expect this to be very self-explanatory, but just to be safe:
  • "Current / Target Inclination": Two numbers in degrees, separated by a "/". The first number is the game reported current inclination (converted to degrees) and the second is the player specified target value.
  • "Initial NEU Velocity": This is the current orbital (or inertial) velocity of the craft with the radial component excluded. The X and Y components have been normalized and so only report on the relative magnitudes of velocity in the "North" (X) and "East" (Y) directions. If the number reported is (0.5, 0.5, 0) then the velocity is split evenly between North and East, for example.
  • "Final NEU Velocity": This is this the tangential velocity that "Calculate Launch Azimuth" expects the craft to have when it reaches orbit in the desired inclination. This will only be 100% accurate if the final actual orbit is perfectly circular, but it is very close for typical orbits created by the autolaunching code. This is shown using the same reference frame as the previous value, so the vector components should match exactly if the craft is traveling in the right direction.
  • "Delta NEU Velocity": This is the result of subtracting "Initial NEU Velocity" from "Final NEU Velocity" with vector subtraction. If the craft travels in this direction, then it should cancel out both the direction difference and the magnitude difference between the two vectors at the same time.
  • Launch Azimuth (Inertial)": This also shows two angles -- the first is the direction of "Delta NEU Velocity" relative to the NEU North unit vector (so "North" = 0°, which matches how Juno defines headings). The second number is the same but using the "Final NEU Velocity" as input. These two numbers should be converging throughout the flight, and will be the same if the rocket is traveling in the correct direction.
  • "Commanded / Actual Heading": This shows what heading the built-in autopilot has been told to fly and the actual direction the craft is facing, both in degrees. The difference between these two number is also shown, and ideally should be zero.
  • "Roll (Current / Target / Error / Rate / Freq)": This shows a wide range of data about the roll autopilot.
    1. The first number is the current roll, as calculated by "Current Roll Angle."
    2. The next number is the target roll value according to the roll autopilot.
    3. The third number is the calculate error (the difference between the two previous values).
    4. Next is the roll rate as calculated by the roll autopilot. This is in ° / second.
    5. Finally is the refresh rate of the roll autopilot, shown in hertz.
If we run the auto launch code with this new thread active, we can monitor the heading difference between the commanded heading values (which are generated by our custom autopilot based on the PCI vector generated by “Calculate Launch Azimuth”) and the actual heading that the craft is pointing and we can see that the error (Δ) spends far more time at a “small positive number” than it does as a “small negative number.” In short, the steering provided by the built-in autopilot is slightly biased – and that is enough to throw off the launch azimuth calculation.
When I am talking about “Error in the commanded versus actual heading” I am talking about numbers ± 0.01°. That error is far smaller than what a normal player would ever notice, much less care about. If you are trying to get a specified inclination to 4 significant digits, on the other hand, a persistent error of even ± 0.001° is unacceptable.
This hopefully should lead you to an obvious question – what if we recalculate the launch azimuth with each physics tick? After all, rather than attempting to calculate how fast the craft should be moving due to the rotation of the planet, we just asked the game “How fast is the craft currently moving?” and it should not matter if the craft is in flight or sitting on the ground. This turns out to be mostly correct (and I am not sure why it is not correct), but it is better than just calculating the azimuth once and sticking to it.
Testing and Troubleshooting (2/2)
Being lazy, we just add a call to "Calculate Launch Azimuth" to this thread like this:


The important part is this:


First, the launch azimuth is recalculated, assuming that the current location is the launch site. This mostly works when the craft is in-flight – but there are two issues:
  1. There is a problem with northerly launches in the northern hemisphere and southerly launches in the southern hemisphere. To get this guide out the door I have patched it to always perform southerly launches (if it starts in the northerly hemisphere) or northerly launches (if the initial launch site is in the southern hemisphere). See the “Calculate Launch Azimuth” section for more on this.
  2. When the radius of the targeted orbit is large enough, the tangential velocity during the initial boost phase will exceed (by a considerable amount, potentially) the tangential velocity of a circular orbit at the stated radius. This requires a very high radius – 1000 km exhibits the problem – so… We just check to see if the magnitude of “Initial Velocity” (really, “Current Velocity”) plus 50 is greater than the magnitude of the expected final velocity vector. If it is, we assume that the “Delta Velocity PCI” vector is unreliable and simply burn in the direction provided by “Launch Final Velocity.” Handily, this is the direction the velocity vector would point if we were in an orbit with the desired velocity, and thus burning in this direction tends to produce a very slow change of inclination towards the desired value.
Implementing this dramatically improves the accuracy of the inclination autopilot:
    These are one off tests – if you repeat these trials, I would expect significant differences (± 0.01°) in the error rate. The sources of error here are all highly chaotic.
  1. Target = 115° / Radius = 100 km / DSC Small Pad: 114.9945°
  2. Target = 26.0983° (Latitude +0.5°) / Radius = 100 km / Juno Pad: 26.0978°
  3. Target = 0° (Latitude +0°) / Radius = 100 km / DSC Small Pad: 0.1507° Better than it appears.
    The naive “Always travel due East” strategy from the DSC Small Pad produces an inclination of 0.4045°, so while the autopilot is off by quite a it, it outperforms the simple strategy.
    The problem here is that there exactly one latitude value where the craft can access a 0° orbit -- 0° (on the equator). As soon as the craft drifts off the equator due to autopilot heading error then it cannot get back to the correct latitude. Thus, you get the results here
  4. Target = 25.5983° (Latitude + 0°) / Radius = 100 km / Juno Pad: 25.5981°
    This is much, much, much better than the “Always head East” strategy. This is because the logic will automatically perform a “dog-leg” maneuver to achieve the requested inclination if the trajectory allows it. This is more likely to occur when the launch site’s latitude is large, resulting in the initial minimum inclination being relatively large. Both of these conditions are met with the Juno pad, thus the very high accuracy.
    Since we are launching from a high initial latitude, the minimum inclination is large. This means that we move towards the equator fairly rapidly with anything close to our targeted inclination, which means that even if the autopilot introduces an error that results in the craft moving north of the desired trajectory it is almost certainly that we will move far enough South to access the targeted trajectory before we achieve orbit.
  5. Target = 90° / radius = 100 km / Juno Pad: 89.9953°

I do not believe it is possible to further improve these results. The fundamental issue is the inaccuracy of the built-in autopilot, and that inaccuracy is not a result of bad design but the limits of PID controllers. I tweaked the settings of the built-in autopilot to this:


The most important change here is adding the "I"ntegral component (it defaults to zero). Adding this in causes the autopilot to notice when persistent error exists and "push" to eliminate it. The downside is that it adds a high risk of oscillations, so the other parameters were adjusted to compensate for this.

Further increasing the "I" component leads to significant oscillations, especially in the pitch axis, which make it difficult to get to orbit at all, much less in the correct inclination.
Areas for potential improvements
One particular improvement to this script is to further exploit the scripts capability to perform dog-leg maneuvers to reach lower inclination orbits to allow reaching very low inclination orbits from high latitude launch sites. This would require:
  1. Split the launch into three phases:
    1. The first has a goal of raising apoapsis out of the atmosphere
    2. The second has a goal achieving the targeted apoapsis while maintaining a constant time to apoapsis. Rather than the current logic (which uses the throttle first, then pitch) during this phase the goal is to maintain 100% throttle and pitch down to control time to apoapsis.
    3. The third goal remains the same – maintain constant time to apoapsis until periapsis equals the targeted value.
  2. You would need to ensure that the targeted radius is “large enough” for the craft to reach the required latitude before starting the second phase (or, at least, achieve the required latitude early during the second phase).
  3. For very low inclinations, you will need to ensure that the upper stage TWR is high enough to complete phase 2 and 3 before the latitude drifted beyond the desired inclination.
The advantage is that that the plane change maneuver (performed in phase 2 and 3) will be much, much, much less expensive than waiting until you reached orbit then performing a plane change. This is because the cost of a plane change scales with linear velocity at the point where the maneuver is performed and “not having yet achieved orbital velocity” is obviously lower than “has achieved orbital velocity.” In addition, you also gain additional efficiency because you are “cutting the corner” – that is, you are not wasting delta-v fully establishing orbital velocity in one direction, only to cancel some of that velocity out in a plane change maneuver.

The downside is that it will take a long time to reach low latitudes from a high latitude launch sites, especially if you have relatively low tangential velocity, which is desirable – minimizing tangential velocity before reaching a point where the targeted inclination is accessible is the whole point. It is highly likely this will only work for low inclinations (compared with the launch site’s latitude) when the targeted radius is quite large (500 km?) and quite a bit of finicky playing around with numbers will be required.

But if you read through this guide instead of writing your own launch script, that is a good project for you to work on. 😊

The other obvious place for improvement is the roll autopilot. While the autopilot works adequately for my test craft, it is very tuned for this specific craft, and I'm dubious as to how well it would work with another craft. Testing it on a wider range of craft and coming up with a formula (rather than the if statement ladder) for controlling roll rate would be worthwhile. If you could come up with a way of measuring control authority after staging so that you could mute (or amplify) the roll command outputs that would make it much more reliable.
Concluding remarks
I have been avoiding writing this guide for quite some time. Why didn't I write it before? That's simple -- I firmly believe and still do believe, that people who are interested in Vizzy programming should write their own autolaunch script. It is both the easiest task to automate in Vizzy and one of the most rewarding. If people just want a "black box" script, well, there are tons of those available on the Juno craft download site -- no guide is needed, nor is one particularly helpful.

Why write one now? Two reasons:

First, I needed something to distract me from my failure to implement automated docking. Yes, I'm writing a guide for that as well, but... Like I said, I'm stuck.

Second, the "How do I make use of the launch azimuth calculation" has been bugging me for close to a year. A successful implementation of that logic would be worthy of a guide and it would obviously be necessary to write an autolaunch script to go along with it.

When I actually sat down to implement it, I decided to also implement an autopilot for roll, which gave me another non-trivial and useful thing to include, which further reinforced my belief that this was worthwhile.

One last remark: This guide (as with any guide) presents the finished product as if it "sprang fully formed from my head" like Athena. This is very incorrect -- 20+ hours of time went into designing the code that this guide documents, most of it spent watching the exact same launch over and over and over again, because that's how debugging goes. Some of these travails are documented in the guide, but there is a lot more blood, sweat, and tears in the programming portion of this guide than is immediately apparent. Programming is not easy under the best of circumstances and the Vizzy editor makes it harder still.

With that said, hope you found the guide useful!
10 則留言
benchenbw 9 月 21 日 上午 7:02 
Here is a more complete(but not all) list of HTML-like tags used in game:
http://digitalnativestudios.com/textmeshpro/docs/rich-text/
I found it in stock craft SimpleBeast
HLSA300 6 月 9 日 下午 3:33 
ooo yess very ty, its work, i completed the bad mision finally
mreed2  [作者] 6 月 9 日 下午 2:29 
If you are only looking for the final minutes, one of my other guides cover this (the one covering "Hover and Land"). All you need to do is get close (within, say, 5 km) and the provided code will get you there.

Beyond that, I have posted a craft file that specifically completes this contract:
https://www.simplerockets.com/c/D4a278/Ali-To-DSC-Drone-Ship-Suborbital-Hopper

It uses a very different (earlier) revision of my code, but if you just want to clear out the mission it will do the trick.

I may end up publishing a "Suborbital Hopper" craft file that uses my current techniques to complete this task, but I'm unlikely to write a guide on the topic.
HLSA300 6 月 9 日 下午 1:55 
you can create**
HLSA300 6 月 9 日 下午 1:54 
can create guide automatic to land in the mission to barge in the oceanic? only for final minutes
mreed2  [作者] 6 月 6 日 上午 12:00 
Actually, I figured out what you are talking about, and it does appear in career -- but only if you include a manned capsule in the craft.

This being Juno, I'm very dubious to call this a "bug," but... I feel that this cannot be intended behavior.
Stinger 5 月 26 日 下午 2:57 
Ah... I guess it's a sandbox-only thing. Damn :/
mreed2  [作者] 5 月 24 日 下午 9:46 
Not all parts include the "Add Flight Program" button -- fuel tanks do not, for example.

All the cockpits and capsules do, as does the command chair and Droods themselves. In short, any part that allows you to control the craft also can have a Vizzy program attached.
Stinger 5 月 24 日 下午 4:58 
Little note: you do not need more command pods for more vizzy programs, the same "add flight program" button can be used for any part you wish :)