Difference between revisions of "Module code examples"

From Kerbal Space Program Wiki
Jump to: navigation, search
(Added how to use the Unity Debugger class)
(Moving this to Category: Modding, as it's not really an API.)
 
(One intermediate revision by the same user not shown)
Line 14: Line 14:
  
 
=== Unity Engine Debugger ===
 
=== Unity Engine Debugger ===
 +
Messages printed using the Unity Debug class will appear in KSP's ALT-F12 debug menu and the KSP log.
 +
 
<syntaxhighlight>
 
<syntaxhighlight>
 
using UnityEngine;
 
using UnityEngine;
Line 436: Line 438:
  
  
[[Category:Community API Documentation]]
+
[[Category:Modding]]

Latest revision as of 22:51, 20 June 2015

Here is a collection of example code for various tasks you might want to perform in your module.


Debugging your code

The most important thing to be able to do!

Possibly outdated method

This will print in real time to the in-game error console, which you can bring up from anywhere in the game by pressing Alt-F2. It will also show up in KSP_Data/output_log.txt (or /Users/{username}/Library/Logs/Unity/Player.log in OS X, or ~/.config/unity3d/Squad/Kerbal Space Program/Player.log in Linux).

print("hello world!");

Unity Engine Debugger

Messages printed using the Unity Debug class will appear in KSP's ALT-F12 debug menu and the KSP log.

using UnityEngine;
.
.
.
void someFunction()
{
    Debug.Log("Bob is concerned. But he's always concerned.");
    Debug.LogWarning("Bob is screaming.");
    Debug.LogError("Jeb is still smiling.");
    Debug.LogException(new Exception("Jeb is screaming too now.")); // This line is not tested.
}

All methods can also accept an input of "Object context".

Protip: create a variable containing your mod name, then append it to all your log messages. That way you and users running into bugs know where they're coming from, and if you want to rename your mod, you don't have to hunt down every time you used the old name.

    public class MyMod
    {
        public static string MyModTag = "[MyMod] ";
 
        void someFunction()
        {
            Debug.LogWarning(MyModTag + "Oops, something went wrong!");
            // .
            // .
            // .
        }
    }
 
// Output: 
// [MyMod] Oops, something went wrong!

Visual Debugging

Sometimes you may wish to create a visual marker pointing in some direction, or a line connecting several objects. LineRenderer Unity object may help a lot.

    private LineRenderer line = null;
 
    protected override void onFlightStart() {
        base.onFlightStart();
 
        // First of all, create a GameObject to which LineRenderer will be attached.
        GameObject obj = new GameObject( "Line" );
 
        // Then create renderer itself...
        line = obj.AddComponent< LineRenderer >();
        line.transform.parent = transform; // ...child to our part...
	line.useWorldSpace = false; // ...and moving along with it (rather 
                                    // than staying in fixed world coordinates)
        line.transform.localPosition = Vector3.zero;
        line.transform.localEulerAngles = Vector3.zero; 
 
        // Make it render a red to yellow triangle, 1 meter wide and 2 meters long
        line.material = new Material( Shader.Find( "Particles/Additive" ) );
        line.SetColors( Color.red, Color.yellow );
        line.SetWidth( 1, 0 ); 
        line.SetVertexCount( 2 );
        line.SetPosition( 0, Vector3.zero );
        line.SetPosition( 1, Vector3.forward * 2 ); 
    }
 
    protected override void onPartFixedUpdate() {
        base.onPartFixedUpdate();
        // As our part moves make the line point downwards to planet's center.
        Vector3 gee = FlightGlobals.getGeeForceAtPosition( transform.position ); 
        line.transform.rotation = Quaternion.LookRotation( gee.normalized );
    }

Creating a GUI Window and Button

This module will create a new GUI window and button, on button press the part will explode, close the GUI, and be removed.

using UnityEngine;
 
/*
This module will create a new GUI window and button, on button press the part will explode, close the GUI, and be removed.
*/
	public class SelfDestruct : Part
	{
		protected Rect windowPos;
 
		private void WindowGUI(int windowID)
		{
        	GUIStyle mySty = new GUIStyle(GUI.skin.button); 
       		mySty.normal.textColor = mySty.focused.textColor = Color.white;
      		mySty.hover.textColor = mySty.active.textColor = Color.yellow;
      		mySty.onNormal.textColor = mySty.onFocused.textColor = mySty.onHover.textColor = mySty.onActive.textColor = Color.green;
      		mySty.padding = new RectOffset(8, 8, 8, 8);
 
                GUILayout.BeginVertical();
			if (GUILayout.Button("DESTROY",mySty,GUILayout.ExpandWidth(true)))//GUILayout.Button is "true" when clicked
			{	
			this.explode();
			this.onPartDestroy ();
			this.Die ();
			}
		GUILayout.EndVertical();
 
                //DragWindow makes the window draggable. The Rect specifies which part of the window it can by dragged by, and is 
                //clipped to the actual boundary of the window. You can also pass no argument at all and then the window can by
                //dragged by any part of it. Make sure the DragWindow command is AFTER all your other GUI input stuff, or else
                //it may "cover up" your controls and make them stop responding to the mouse.
                GUI.DragWindow(new Rect(0, 0, 10000, 20));
 
		}
		private void drawGUI()
		{
            GUI.skin = HighLogic.Skin;
            windowPos = GUILayout.Window(1, windowPos, WindowGUI, "Self Destruct", GUILayout.MinWidth(100));	 
		}
		protected override void onFlightStart()  //Called when vessel is placed on the launchpad
		{
		    RenderingManager.AddToPostDrawQueue(3, new Callback(drawGUI));//start the GUI
		}
		protected override void onPartStart()
		{
       		if ((windowPos.x == 0) && (windowPos.y == 0))//windowPos is used to position the GUI window, lets set it in the center of the screen
			{
          	  windowPos = new Rect(Screen.width / 2, Screen.height / 2, 10, 10);
   			}
		}
	 	protected override void onPartDestroy() 
		{
			RenderingManager.RemoveFromPostDrawQueue(3, new Callback(drawGUI)); //close the GUI
   		}
 
	}


Useful geometry stuff

All these vectors live in a "world" coordinate system. This coordinate system has an arbitrary orientation and an origin that is periodically shifted to keep it reasonably close to the active ship.

Vector3d has lots of useful vector manipulation methods that you should check out.

//position of the ship's center of mass:
Vector3d position = vessel.findWorldCenterOfMass();
 
//unit vectors in the up (normal to planet surface), east, and north (parallel to planet surface) directions
Vector3d eastUnit = vessel.mainBody.getRFrmVel(position).normalized; //uses the rotation of the body's frame to determine "east"
Vector3d upUnit = (position - vessel.mainBody.position).normalized;
Vector3d northUnit = Vector3d.Cross(upUnit, eastUnit); //north = up cross east
 
double altitude = vessel.mainBody.GetAltitude(position);
 
//ship velocity in an external, non-rotating frame:
Vector3d nonRotatingFrameVelocity = vessel.orbit.GetVel(); 
 
//velocity as seen in planet's rotating frame
Vector3d rotatingFrameVelocity = nonRotatingFrameVelocity - vessel.mainBody.getRFrmVel(position); 
 
//local acceleration vector due to gravity:
Vector3d geeAcceleration = FlightGlobals.getGeeForceAtPosition(position); 
 
//given currentVelocity in rotating frame, find coriolis acceleration:
//this first one becomes zero when the game switches out of the planet's reference frame
Vector3d coriolisAcceleration = FlightGlobals.getCoriolisAcc(currentVelocity, vessel.mainBody); 
//this doesn't:
Vector3d myCoriolisAcc = -2 * Vector3d.Cross(vessel.mainBody.angularVelocity, currentVelocity); 
 
//Moment of inertia of the ship around x, y, and z axes (I think y is always the roll axis):
Vector3d MOI = vessel.findLocalMOI(vessel.findWorldCenterOfMass());
 
//direction in which the ship currently points:
Vector3d heading = (Vector3d)vessel.transform.up;

It's often useful to transform a vector into a coordinate system affixed to the ship, which will in general be rotated with respect to the world coordinate system. To do this use vessel.transform.InverseTransformDirection( ):

//angular velocity of the ship around three axes,
//x-component = pitch rate, y = roll rate, z = yaw rate:
Vector3d angularVelocity = (Vector3d)vessel.transform.InverseTransformDirection(vessel.rigidbody.angularVelocity);
 
//to damp out roll (k is some gain):
roll_command = k * angularVelocity.y;
 
//to command the ship to slew towards a given direction, dir (k is some gain):
//error will be (0, 1, 0) if dir = heading:
Vector3d error = (Vector3d)vessel.transform.InverseTransformDirection((Vector3)dir);
yaw_command = k * error.x;
pitch_command = -k * error.z;

When you print vectors you may find it useful to first cast them to the Unity type Vector3, which by default only prints one digit past the decimal point, whereas Vector3d prints all the digits. Using Vector3's makes your debugging output much easier to read:

print("position = " + (Vector3)position);

Note on eulerAngles

While you may find transform.eulerAngles convenient for representing directions, be warned that eulerAngles representation is extremely imprecise and yields 1E-4 errors (i.e. values are rounded down to something like '180.5').

Avoid using eulerAngles in intermediate calculations.

Fly-by-wire

You can control the flight from code as if you were the player using w,a,s,d,q,e or a joystick. In addition to yaw, pitch, roll, and throttle as shown below, the FlightCtrlState class gives access to a few other controls that you can check out.

...
protected override void onFlightStart()  //Called when vessel is placed on the launchpad
{
    //at the beginning of the flight, register your fly-by-wire control function that will be called repeatedly during flight:
    vessel.OnFlyByWire += new FlightInputCallback(fly);
 
    ...
}
 
//remove the fly-by-wire function when we get destroyed:
protected override void onPartDestroy()
{
    vessel.OnFlyByWire -= new FlightInputCallback(fly);
 
    ...
}
 
//remove the fly-by-wire function when we get disconnected from the ship:
protected override void onDisconnect()
{
    vessel.OnFlyByWire -= new FlightInputCallback(fly);
 
    ...
}
 
 
//now this function gets called every frame or something and gives you access to the flight controls
private void fly(FlightCtrlState s) {
    s.yaw = -0.2F;  //set yaw input to 20% left
    s.pitch += 0.3F; //set pitch input to whatever the player has input + 30%
    s.roll = 0.5F;   //set roll to 50% (either clockwise or counterclockwise, try it and find out)
    s.mainThrottle = 0.8F; //set throttle to 80%
 
    //the range of yaw, pitch, and roll is -1.0F to 1.0F, and the throttle goes from 0.0F to 1.0F.
    //if your code might violate that it's probably a good idea to clamp the inputs, e.g.:
    s.roll = Mathf.Clamp(s.roll, -1.0F, +1.0F);
}
...

Setting the warp rate and modifying the available warp rates

using UnityEngine;
...
//to set the warp rate:
TimeWarp.SetRate(3); //activates the fourth warp rate
 
//to modify the available warp rates:
TimeWarp timeWarp = (TimeWarp)FindObjectOfType(typeof(TimeWarp)); //documentation claims this function is slow
                                                                  //so call it once and store the reference
timeWarp.warpRates[3] = 45; //set the fourth warp rate to be x45
timeWarp.altitudeLimits[3] = 0.5; //set the altitude limit for the fourth warp rate. Is this measured in planetary radii?


Automatically activating the next stage

Here is some code that can automatically activate the next stage when this one is done. It uses the usual logic you would use to determine when to activate the next stage, staging only when all the engines that would be decoupled by staging have burned out. However it doesn't have any other criteria for staging, so it may e.g. prematurely activate parachutes.

    //Determines whether any of the parts under this part in the ship are engines in the ACTIVE or IDLE
    //state. Engines are IDLE before they are fired, ACTIVE while firing, and DEACTIVATED after running
    //out of fuel. So if a decoupler has an ACTIVE or IDLE engine descendant we shouldn't blow the decoupler, or
    //we will shed burning or unused engines.
    private bool hasActiveOrIdleEngineDescendant(Part p)
    {
        //note that if someone makes a new engine that does not subclass LiquidEngine or SolidRocket
        //then this method will not account for it
        if ((p.State == PartStates.ACTIVE || p.State == PartStates.IDLE) && (p is SolidRocket || p is LiquidEngine)) return true;
        foreach (Part child in p.children)
        {
            if (hasActiveEngineDescendant(child)) return true;
        }
        return false;
    }
 
    //Parts store an "inverseStage" which is just the stage number that appears in the staging display
    //in the GUI, and which counts is counts downward to zero as you activate successive stages. This
    //method looks to see if any of the parts in the given inverseStage are decouplers that would
    //decouple an active or idle engine if we activated the given inverseStage.
    private bool inverseStageDecouplesActiveEngine(int inverseStage, Part root)
    {
        if (root.inverseStage == inverseStage && (root is Decoupler || root is RadialDecoupler) && hasActiveEngineDescendant(root)) return true;
        foreach (Part child in root.children)
        {
            if (inverseStageDecouplesActiveEngine(inverseStage, child)) return true;
        }
        return false;
    }
 
...
//Now we call this code regularly somewhere in the part logic:
//Staging.CurrentStage != Staging.StageCount is a way to detect whether liftoff has occurred yet.
//This condition is false until the first stage fires. The last stage is stage zero, so we should
//only try to fire the next stage if this is not the last stage.
if (Staging.CurrentStage != Staging.StageCount && Staging.CurrentStage > 0)
{
    if (!inverseStageDecouplesActiveEngine(Staging.CurrentStage - 1, vessel.rootPart))
    {
        Staging.ActivateNextStage();
    }
}


Drawing and displaying a 2D image

Disclaimer: I'm not sure if this code is the proper way to do this, but it works.

You can draw an image in your code and display it as part of a GUI interface. See for example the trajectory display in http://i.imgur.com/oEsoI.png

The relevant Unity class is Texture2D, which is a 2D image. Unity doesn't provide many useful functions for drawing on these, but you can always set the contents of the image pixel by pixel:

Texture2D image = new Texture2D(400, 100); //400 pixels wide by 100 pixels tall
 
void drawImage()
{
    //Set all the pixels to black. If you don't do this the image contains random junk.
    for (int y = 0; y < image.height; y++)
    {
        for (int x = 0; x < image.width; x++)
        {
            image.SetPixel(x, y, Color.black);
        }
    }
 
    //then we can get a single red pixel on a field of black:
    image.SetPixel(10, 20, Color.red);
 
    //That's not a very interesting image. Let's plot a function
    int lineWidth = 3; //draw a curve 3 pixels wide
    for (int x = 0; x < image.width; x++)
    {
        int fx = f(x);
        for (int y = fx; y < fx + lineWidth; y++)
        {
            image.SetPixel(x, y, Color.red);
        }
    }
 
    image.Apply();
 
    //now image contains the graph of f(x), with the curve draw red on a black background
}
 
//the function to plot:
int f(int x) 
{
    return 2*x;
}
 
//Now to display this we can include it in a GUI window. See the "Creating a Window GUI" example for how to bring up a window.
//In the window GUI code we just need this line to put our Texture2D on the screen:
void windowGUI(int windowID)
{
    GUILayout.Box(image);
}

Getting input from a text box

You can put a text box in your GUI window and get input from it.

String currentText = "10.0";
double currentValue = 10.0;
 
//see the "Creating a Window GUI" example for how to set this up as your window GUI handler
void windowGUI(int windowID) {
    ...    
 
    //try to parse the contents of currentText as a double and put the result in currentValue:
    if (!Double.TryParse(currentText, out currentValue)) 
    {
        //the text couldn't be parsed; you'll have to decide what you want to do here
        currentValue = 0;
    }
 
    //display the text field and put its current text in currentText
    currentText = GUILayout.TextField(currentText, GUILayout.MinWidth(30.0F)); //you can play with the width of the text box
 
    ...
}

Adding a Custom Icon

Only works for 0.17+!
First, use File:Icons.psd in either GIMP or Photoshop to make your desired custom icons; There's a helpful grid there to help you lay them out. The icon should be white with some slight airbrushed shadowing.
Example

Export as a PNG with an alpha layer (in GIMP, uncheck "save background color" and "save color values from transparent pixels") when you're done, and replace whatever you previously used to set your DefaultIcon with something like this:

    // In your Part class:
    protected override void onPartStart()
    {
        // Set our stackIcon to use the file icons.png in the same folder as part.cfg, and use the icon at position (0,0).
        // This overrides any DefaultIcon you've specified.
        base.stackIcon.SetIcon(this, "icons.png", 0, 0);
    }

Place icons.png in the same folder as your part.cfg, and you're ready to roll.

Update Notice

This will be changed to pull icons.png from your PluginData folder instead of the Part folder in 0.17.1, so parts packs only need to make one file. -- N3X15 (talk) 19:47, 27 September 2012 (UTC)