• Content count

  • Joined

  • Last visited

Community Reputation

49 Excellent

1 Follower

About Mu.

  • Rank

Contact Methods

  • Website URL
  1. i7-6700K @ 4.2 32Gb GTX980 Mouse, keyboard, speakers, desk, chair, cup of tea
  2. Not a bad night's haul! Thanks for being with us on the KSP journey and, of course, voting!
  3. I think we have found and fixed this already. Was a bug whereby during a craft packing/unpacking and warping it would incinerate remote vessels. If you can post your save files so we can test, that would be a great help.
  4. Tis because the models n textures imported via the plugin are stored in the scene file itself.
  5. Good day to you all, Here is the link for the 0.23 PartTools package. Not much else has changed really but will now let you export .mu files with KSPParticleEmitters for use in your mods. KSPParticleEmitter was built because Unity particle emitters are notoriously hard to script and serialize. It was impossible to save/load them from files without a wrapper of some kind. Sadly it does lead to some limitations in the space orientation and spawning of the particles. However for simple thrust and rcs jets it should be fine. If Unity ever open the particle emitters up to be able to script for them properly then we will update KSPParticleEmitter to match. If you require more extreme particle emitters then you will need UnityPro and AssetBundles to export them, alternatively you can spawn our inbuilt ones via code.
  6. Hi Mu,

    Wondering whether you can help me, what technique (if any) do you use to generate the height map data for each of the planets? I've been searching for a good while now for information from yourselves at Squad, but it's either hard to come by or simply hearsay. I'm in need of a technique name from the source (yourselves at Squad) for my research project for University.

    I understand you're busy but I will be grateful for any information you could provide on KSPs height maps.


  7. PartTools 0.20 Firstly, here is the new PartTools package for 0.20. Please read the included ReadMe.txt! GameDatabase & PartLoader The system that KSP uses to load game resources has changed, its called GameDatabase (GDb). GDb iterates through the various game data directories creating a list of files. Each file is assigned a url and then the assets are loaded in a specific order; Assemblies (dlls), audio, textures, models and then configs. Once GDb has finished loading files it hands control over to PartLoader which compiles all of the game's configs into parts, internal props and internal spaces. GameDatabase URL system GDb assigns a unique url to every asset and config in its directory structure. No file extensions are stored in the url, this allows you to reference models, textures and audio without needing to know what the file type is. The legacy directories (parts, internals, etc) are all prefixed with the directory name however the new data directory, GameData, is not. Lets look at some examples... File: KSP\GameData\Squad\Parts\Command\landerCabinSmall\ URL: Squad\Parts\Command\landerCabinSmall\model File: KSP\Parts\cupola\model000.mbm URL: Parts\cupola\model000 GameDatabase 'Get' Methods All of the assets held in the GDb are retrieved via the Get methods. They are seperated into asset type. GetAudioClip - Gets audio clip from url GetTexture - Gets texture from url GetTextureIn - Gets a texture in the specified directory GetModel - Gets model by url GetModelIn - Gets model in specified directory GetConfigNode - Gets a config node by url GetConfigNodes - Gets all config nodes by type For detailed information load the dll into VisualStudio or MonoDevleop. KSPAddon & AddonLoader AddonLoader is a simple system which allows modders to instantiate Unity MonoBehaviours without need to messily overload Part, PartModule or UnitTests. To do this you tag a component in your assembly with the KSPAddon attribute. This attribute allows you to set which scene the component is instantiated in and also if it should be repeatedly instantiated upon transition into that scene. If you want the component to not be destroyed then you should set 'once' to true and manually call DontDestroyOnLoad. ATTACH Nodes Previously, part configs had to define attach nodes with messy position values in the base config. Now you can define an attach node as being linked to a transform on the model. To do this you define an ATTACH config node like this.. NODE { name = top transformName = myTopTransform } NODE { name = srfAttach transformName = mySrfAttachmentTransform } There are two key names of attach nodes; There is the 'top' node, this defines the traditional part heirarchy structure. If you only have one attach node then it should be called top. There is also the 'srfAttach' node, this defines the surface attachment point. The name 'attach' is also valid for this node, you can only have one surface attachment node. You can define two other values; size and method. 'size' allows you to change the node's sphere size in the editor and has no effect on gameplay. 'method' allows you to change the physic joint type. Its values can be; FIXED_JOINT, HINGE_JOINT, LOCKED_JOINT, MERGED_PHYSICS or NO_PHYSICS. Not sure what madness you can do with this value, will leave it to you all to play. NOTE: Attach nodes defined in this way are not animated with respect to physics and should remain static on the model. MODEL Nodes In the old system, models were loaded directly from a part config's directory. This is still the case however can be overridden using MODEL config nodes. MODEL nodes can be used to compile parts containing scaled, translated or rotated models. You can define multiple MODEL nodes to compile multiple models into one part. It also allows you to overload textures on the model. Parts, Props and InternalSpaces can all use MODEL nodes instead of the traditional same-driectory loading system. Here is a sample MODEL node showing all valid values MODEL { model = Squad/Parts/Command/cupola/model position = 0.25, 0.5, 1.0 scale = 2.0, 2.0, 4.0 rotation = 0, 90, 0 parent = anotherModelTransform texture = model000 , Squad/Parts/Command/landerCabinSmall/model000 texture = oldTextureName , newTextureURL } NOTES: The 'parent' value is only valid in the second or subsequent MODEL nodes as they refer to a transform which must already exist in the part model heirarchy. Texture overloading value is delimited by comma or semicolon just in case you have spaces in your new texture's URL. All materials using the old texture are swapped to the new texture.
  8. Sorry I didnt have my account enabled when you sent that one and only just noticed it.

    No idea why those things do that. Harv is your best bet for finicky engine gubbinz such as those.

  9. Can you explain why the new command pods for rockets: Mk1 and Mk1-2 does not take input from Part.propagateControlUpdate while in orbit? Speccifficly i can send a FlightCtrlState to this method that modifies the throttle but not yaw, pitch and roll. While in the atmosphere it seems to be working fine and i can set both throttle, yaw, pitch and roll.

    There is still a chance this is caused by a bug in my program but since throttle is working, i doubt it. I would really appreciate a reply on this one.

    Thanks in advance.


  10. Hi all, Here is the 0.16 PartTools package. Improvements include the ability to serialize wheel colliders and GameObject tags. Tags are important to create ladders and hatches for EVA and will be used to create hotspots for IVA. Mu
  11. PartResources The PartResources system is fairly simple. Its all fairly simple. Its designed to allow many addon devs to use a few core resource definitions, add them to parts easily and also retrieve them easily. Resource definitions ResourceDefinitions define a resource, in name and a few physical values. Resource definitions are currently defined in .cfg files in the /resources directory. It parses all .cfg files in resources at startup so filename is unimportant. As far as I know, no stock .cfg files come with KSP so lets look at mine. RESOURCE_DEFINITION { name = LiquidOxygen density = 1.5 } RESOURCE_DEFINITION { name = LiquidHydrogen density = 1.5 } RESOURCE_DEFINITION { name = LiquidHydrazine density = 1.5 } RESOURCE_DEFINITION { name = AtmosphereKerbin density = 0.1 } So we have four resource definitions there. All pretty simple. None of the densities are to scale, they\'re just for idle testing. The density should be in kg/m^3 but i\'m afraid KSP\'s mass units make no sence so they\'re in tonnes/m^3 I think (12 kilobanana per m^3). Going forward I would like to agree some standard units. Ok mass confusion (pun intended) over. Tanks for the resources To create a volume of resource on a part you can do it through the part.cfg file. Here is the cfg for a hydrazine fuel tank. RESOURCE { name = LiquidHydrazine amount = 2 maxAmount = 2 } Thus this part contains a node called RESOURCE which has a name, an amount value and a maxAmount value. The name should match the one in your resource definition and the amount/max are in units of volume. The mass increase on the part is calculated from the amount*density. Amount should be less than or equal to the max. Crossfeeding If a part contains a resource with a maxAmount of zero then this allows the part to act as a crossfeed for this resource. Alternatively, the standard 'fuelCrossFeed' acts as a master crossfeed switch. This was purely to ensure backward compatability with crossfeeding mods should we have upgraded our parts to use PartResource. RequestResource Parts have a method called RequestResource. This is the main method you will use to request or produce resources. public float RequestResource(string resourceName, float demand) Any part can request fuel in this way. A PartModule must use part.RequestResource but the operation is exactly the same. This method will return you an amount drawn through the system, if the amount is available. If the demand is negative then you are producing the resource and it will start to fill up reservoirs connected to the system. As the system is not balanced you must first create an amount somewhere in order to fill it up. Example #1 - Generic rocket module This is a rocket engine module that can run on any number of any propellants. Calling it a rocket is a bit of a misnomer, really its a generic reaction engine. The list of Propellant subclasses is loaded/saved manually in the OnLoad and OnSave. (I do hope you\'ve followed the PartModule tutorial above!) public class ModuleEngineRocketLiquid : PartModule { [System.Serializable] public class Propellant { public string name; public int id; public float requirementMax; public float currentRequirement; public float currentAmount; public float currentPotential; public Propellant() { } public void Load(ConfigNode node) { name = node.GetValue('name'); id = name.GetHashCode(); if (node.HasValue('requirementMax')) requirementMax = float.Parse(node.GetValue('requirementMax')); } public void Save(ConfigNode node) { node.AddValue('name', name); node.AddValue('requirementMax', requirementMax); } } [KSPField] public float thrustMax; [KSPField] public string thrustVectorTransformName; [KSPField] public float currentThrottle; public List<Propellant> propellants; public Transform thrustTransform; public float currentThrust; public float currentPower; public override void OnAwake() { if (propellants == null) propellants = new List<Propellant>(); } public override void OnStart(StartState state) { if (thrustTransform == null) { thrustTransform = part.FindModelTransform(thrustVectorTransformName); } } // calculate current output thrust as factor of fuel requirements met public override void OnUpdate() { //string str = ''; if (part.isControllable) { currentThrottle = FlightInputHandler.state.mainThrottle; } float throttle = currentThrottle; currentPower = throttle; foreach (Propellant p in propellants) { p.currentRequirement = (p.requirementMax * throttle * Time.deltaTime); p.currentAmount = part.RequestResource(, p.currentRequirement); p.currentPotential = p.currentAmount / p.currentRequirement; currentPower = Mathf.Min(currentPower, p.currentPotential); } currentThrust = thrustMax * currentPower; } public override void OnFixedUpdate() { if (thrustTransform != null && part.Rigidbody != null) { part.Rigidbody.AddForceAtPosition(-thrustTransform.forward * currentThrust, thrustTransform.position); } } public override void OnSave(ConfigNode node) { foreach (Propellant p in propellants) { p.Save(node.AddNode('PROPELLANT')); } } public override void OnLoad(ConfigNode node) { foreach (ConfigNode subNode in node.nodes) { switch ( { case 'PROPELLANT': if (!subNode.HasValue('name')) { Debug.Log('Propellant must have value \'name\''); continue; } Propellant newProp = new Propellant(); newProp.Load(subNode); propellants.Add(newProp); break; } } } } The key line in all that is 'p.currentAmount = part.RequestResource(, p.currentRequirement);' It requests a resource from its id (the GetHashCode of the resource name). Example #2 - Air intake module For the air-breathing engines to work properly here is a PartModule to do so. It creates an amount of resource per frame depending on forward velocity of the intake and also a mechanical suction effect. public class ModuleResourceIntake : PartModule { [KSPField] public string resourceName = 'AtmosphereKerbin'; [KSPField] public float area; public float airSpeed; [KSPField] public string intakeTransformName = 'Intake'; public Transform intakeTransform; [KSPField] public bool intakeEnabled = true; [KSPField] public float intakeSpeed; [KSPField] public float intakePower = 75; public override void OnStart(StartState state) { if (intakeTransform == null) { intakeTransform = part.FindModelTransform(intakeTransformName); } } [KSPEvent(guiActive = true, guiName = 'Deactivate')] public void Deactivate() { Events['Deactivate'].active = false; Events['Activate'].active = true; intakeEnabled = false; } [KSPEvent(guiActive = true, guiName = 'Activate')] public void Activate() { Events['Deactivate'].active = true; Events['Activate'].active = false; intakeEnabled = true; } public override void OnFixedUpdate() { if (intakeEnabled) { if (part.isControllable) { intakeSpeed = FlightInputHandler.state.mainThrottle * intakePower; } airSpeed = (float)vessel.srf_velocity.magnitude; float aoa = Vector3.Dot(vessel.srf_velocity.normalized, intakeTransform.forward.normalized); float airVolume = (aoa * area * (airSpeed + intakeSpeed) * Time.fixedDeltaTime); float airMass = (float)vessel.atmDensity * airVolume; airMass = Mathf.Max(0f, airMass); part.RequestResource(resourceName, -airMass); } } public override void OnLoad(ConfigNode node) { if (intakeEnabled) { Events['Deactivate'].active = false; Events['Activate'].active = true; } } } Note is uses a transform inside model to calculate relative velocities, this will take into account any mirroring that happens. A part.cfg using this module looks like this.. MODULE { name = ModuleResourceIntake resourceName = AtmosphereKerbin area = 2 intakeTransformName = Intake intakeEnabled = True intakePower = 75 } RESOURCE { name = AtmosphereKerbin maxAmount = 2 } We have created a resource tank on the part. This is akin to the compression chamber, it is constantly topped up with air (assuming engine is running or we have forward velocity). Conclusion So the resources system is incredibly simple at the moment. Really we need nothing more complicated however I would like to expand it to include flow, pressure, etc but have not had the time or the maths skills to do it. If anyone has any experience with iterative analysis of pipe networks and electrical circuits then give me a shout.
  12. So time to talk about models and the much hyped hierarchical model format, rather suspiciously named '.mu' The old DAE importer was a bit wrong. It was messy to use and did weird things like flatten the heirarchy out. Also it only imported static meshes. When you\'re dealing with a system like Unity we dont want you, or ourselves, limited. My aim was to create a WYSIWYG part exporter for our artists so that we could be certain what went in came out the same. With Unity as a backbone i\'ve created us simple exporter tool. It comes as a unitypackage and you can import it into any existing project. Should work on non-pro versions too. Overview To use it you would create a PartTools gameobject in a scene, add/setup your model as a child of the PartTools object, then click Write. It automatically writes the hierarchy and any textures into the given directory ready to be playtested. You can write each PartTools object individually or use the bulk exporter. This works on the entire project currently. Every PartTools prefab in the project will get written to its respective directory. Will possibly expand these features a little in the future. Currently the only supported filename for .mu files is ''. Just place it in the directory with the part.cfg and it will be looked for as priority over any other models. How-to 1. Create a new GameObject in a scene and add the KSP/PartTools component to it. This GameObject becomes the \'model\' parent when it is loaded into the game, its name is irrelevant. 2. Drag and drop your model into the scene and parent it to the PartTools GameObject. 3. Assign KSP materials to your model. This version of PartTools only supports the included shaders. 4. Set up PartTools making sure that the directory is set correctly. 5. If your materials use formats other than JPEG or PNG then you must convert the textures. 6. Press Write! Meshes Standard meshes and skinned meshes are supported along with tangents and vertex colours. You may also use meshcollider components tho must usually ensure isConvex is ticked. Submeshes are supported. Materials and shaders You may only use shaders from the list provided with PartTools. Shared material references are preserved. Multiple materials on a renderer are also supported. Textures Currently there are three types of texture format supported by KSP; JPEG, PNG and MBM. You may not use cube maps. I do want to ditch MBM in favour of something user-editable but we\'re stuck with it for a few weeks. At least you wont have to convert everything manually. If you tick 'Convert Textures' then all textures are converted to MBM format. It is often a safer route to leave convert ticked. Animations Animation components can be written along with the hierarchy so you can force an animation to play forever or use them from script (part.FindModelAnimator is useful). Transform/bone animations are supported along with Light and Material animations. Supported components MeshFilter, MeshRenderer, SkinnedMeshRenderer MeshCollider, SphereCollider, CapsuleCollider, BoxCollider Animation (Lights, Materials & Transforms) Light Supported shaders KSP/Diffuse KSP/Specular KSP/Bumped KSP/Bumped Specular KSP/Emissive/Diffuse KSP/Emissive/Specular KSP/Emissive/Bumped Specular KSP/Alpha/Cutoff KSP/Alpha/Cutoff Bumped
  13. Example #2 - IConfigNode and ConfigNode Ok so time for something more meaty. Here is my take on an aerodynamic lift module. It uses a class called FloatCurve to create the lift/drag vs angle of attack graphs for standard aerofoils. FloatCurve also implements IConfigNode so it can be used with KSPField. public class ModuleAerodynamicLift : PartModule { /// <summary> /// Planform area of lifting surface in m^2 /// </summary> [KSPField] public float planformArea = 10f; /// <summary> /// Overall lift factor for this wing /// </summary> [KSPField] public float liftFactor = 1f; /// <summary> /// Overall drag factor for this wing /// </summary> [KSPField] public float dragFactor = 1f; /// <summary> /// FloatCurve of lift vs angle of attack. Angle is abs cosine of angle (0 -> 1) /// </summary> [KSPField] public FloatCurve liftAoA; /// <summary> /// FloatCurve of drag vs angle of attack. Angle is abs cosine of angle (0 -> 1) /// </summary> [KSPField] public FloatCurve dragAoA; /// <summary> /// Model transform name for center of lift /// </summary> [KSPField] public string centerOfLiftTransformName; /// <summary> /// grabbed OnStart from the model transform named liftTransformName /// </summary> public Transform centerOfLiftTransform; /// <summary> /// Sets up the float curves if they\'re not already set up /// </summary> public override void OnAwake() { if (liftAoA == null) liftAoA = new FloatCurve(); if (dragAoA == null) dragAoA = new FloatCurve(); } /// <summary> /// Grabs center of lift transform from model /// </summary> /// <param name='state'></param> public override void OnStart(StartState state) { if (centerOfLiftTransform == null) { centerOfLiftTransform = part.FindModelTransform(centerOfLiftTransformName); if (centerOfLiftTransform == null) Debug.LogError('ModuleAerodynamicLift: liftTransform is null!'); } } /// <summary> /// Calculates and applied the lift/drag force from the aerofoil /// </summary> public override void OnFixedUpdate() { if (centerOfLiftTransform == null) return; Vector3 force = CalculateForce(); part.Rigidbody.AddForceAtPosition(force, centerOfLiftTransform.position, ForceMode.Force); } /// <summary> /// Calculates lift/drag according the simple aerofoil equations... /// Lift: L = (CL)(1/2)(dens)(V^2)(area) or L = (CL)(q)(S) q(dyn pressure) = (1/2)(dens)(V^2) /// Drag: D = (CD)(1/2)(dens)(V^2)(area) or D = (CD)(q)(S) /// </summary> /// <returns>Overall force vector</returns> private Vector3 CalculateForce() { // grab world point and relative velocity Vector3 worldVelocity = part.Rigidbody.GetPointVelocity(centerOfLiftTransform.position); // note we use centerOfLiftTransfrom from the model to calculate relative. This will take into account any part mirroring Vector3 relativeVelocity = centerOfLiftTransform.InverseTransformDirection(worldVelocity); Vector3 velocityNorm = relativeVelocity.normalized; // only need the speed squared - saves us a square root float speedSqr = relativeVelocity.sqrMagnitude; // calc the angle of attack float vDot = Vector3.Dot(velocityNorm, centerOfLiftTransform.up.normalized); float vDotNorm = (vDot + 1f) * 0.5f; float absVDot = Mathf.Abs(vDot); float abs1MVDot = 1f - absVDot; // dynamic pressure float dynPressure = 0.5f * (float)vessel.atmDensity * speedSqr; // calc coefficient of lift and drag from the factors and the float curves float cL = liftFactor * liftAoA.Evaluate(abs1MVDot); float cD = dragFactor * dragAoA.Evaluate(abs1MVDot); // calc lift, drag and add to get overall Vector3 lift = centerOfLiftTransform.up * (cL * dynPressure * planformArea); Vector3 drag = -(worldVelocity.normalized) * (cD * dynPressure * planformArea); Vector3 force = lift + drag; // some debug stuff string str = ''; str += 'AoA: ' + abs1MVDot; str += ' cL: ' + cL; str += ' cD: ' + cD; Debug.Log(str); Debug.DrawLine(centerOfLiftTransform.position, centerOfLiftTransform.position + lift * 100f,; Debug.DrawLine(centerOfLiftTransform.position, centerOfLiftTransform.position + drag * 100f, Color.cyan); Debug.DrawLine(centerOfLiftTransform.position, centerOfLiftTransform.position + worldVelocity * 100f, Color.magenta); // et voila return force; } } Really its a very simple module. All of its persistance is handled by its KSPFields and it basically adds lift/drag based on standard aerofoil model. The FloatCurve class is a Unity AnimationCurve wrapped up in an IConfigNode extending interface. Remember in order to use KSPField on a class successfully it needs to implement IConfigNode. Here is the IConfigNode interface. /// <summary> /// Can this item be saved using a KSPField persitance object. KSPField creates a subnode for this type /// </summary> public interface IConfigNode { void Load(ConfigNode node); void Save(ConfigNode node); } It requires two methods, Load(ConfigNode) and Save(ConfigNode). The implementing class must be able to be instantiated blind and then have OnLoad called to load its values in. ConfigNode consists of a recursive node/value list. In essence it looks like this.. public class ConfigNode { public string name; public List<ConfigNode> nodes; public List<ConfigNode.Value> values; public class Value { public string name; public string value; } } For loading use; HasValue, GetValue, GetValues, HasNode, GetNode & GetNodes For saving use; AddValue or AddNode Every value you add is a string and should be parsable to/from what you set it to. Now lets have a look in FloatCurve to see that in action.. [System.Serializable] public class FloatCurve : IConfigNode { [SerializeField] private AnimationCurve fCurve; public float minTime { get; private set; } public float maxTime { get; private set; } public FloatCurve() { fCurve = new AnimationCurve(); minTime = float.MaxValue; maxTime = float.MinValue; } public void Add(float time, float value) { fCurve.AddKey(time, value); minTime = Mathf.Min(minTime, time); maxTime = Mathf.Max(maxTime, time); } public float Evaluate(float time) { return fCurve.Evaluate(time); } private static char[] delimiters = new char[] { \' \', \',\', \';\', \'\t\' }; public void Load(ConfigNode node) { string[] values = node.GetValues('key'); int vCount = values.Length; string[] valueSplit; for (int i = 0; i < vCount; i++) { valueSplit = values[i].Split(delimiters, System.StringSplitOptions.RemoveEmptyEntries); if (valueSplit.Length < 2) { Debug.LogError('FloatCurve: Invalid line. Requires two values, \'time\' and \'value\''); } Add(float.Parse(valueSplit[0]), float.Parse(valueSplit[1])); } } public void Save(ConfigNode node) { for (int i = 0; i < fCurve.keys.Length; i++) { node.AddValue('key', fCurve.keys[i].time + ' ' + fCurve.keys[i].value); } } } As you can see it has a list of values called \'key\' and each value is made up of a space seperated time/value pair. I\'ve added extra delimiters into the code cuz you/I might forget and comma seperate them or something. Example #3 - part.cfg So now we get to the point of adding these things into a part\'s config file. You should know that every config file is parsed by ConfigNode now so can be considered in the node/value list paradigm. Here is ModuleAerodynamicLift module from example #2 added to the end of DeltaWing\'s part.cfg MODULE { name = ModuleAerodynamicLift liftFactor = 0.001 dragFactor = 0.001 liftTransformName = CenterOfLift liftAoA { key = 0.0 1 key = 0.2 3 key = 0.4 4 key = 0.6 1 key = 0.7 0 key = 1.0 -1 } dragAoA { key = 0.0 1 key = 0.2 3 key = 0.5 5 key = 0.7 6 key = 1.0 7 } } MODULE is a subnode of Part and you can have as many as you like. This particular module relys on you having the transform named 'CenterOfLift' somewhere in the DeltaWing heirarchy, which it may not when we go to press. Most of the KSPField values are in the usual style of valueName=value however you can see how the two IConfigNode implementing FloatCurves are represented as subnodes of the module node. KSPField will make any IConfigNode implementing class a subnode. Alternatively you can add/find your own subnodes in the OnLoad or OnSave methods. In this next example we\'ll add two modules, ModuleAnimatorLandingGear (from Example #1) and an as yet unknown module called ModuleAnimateHeat. MODULE { name = ModuleAnimatorLandingGear } MODULE { name = ModuleAnimateHeat ThermalAnim = HeatAnimationEmissive; } Here is ModuleAnimateHeat. This was written to handle all of emissive heat glowing by heated parts. The heat is represented in the models as animations going from 0->1 (none to full heat) public class ModuleAnimateHeat : PartModule { [KSPField] public string ThermalAnim = 'HeatAnimationEmissive'; public float draperPoint = 525f; // Draper point is when solid objects begin to emit heat. public AnimationState[] heatAnimStates; public override void OnStart(StartState state) { HeatEffectStartup(); } public void Update() { UpdateHeatEffect(); } private void HeatEffectStartup() { Animation[] heatAnims = part.FindModelAnimators(ThermalAnim); heatAnimStates = new AnimationState[heatAnims.Length]; int i = 0; foreach (Animation a in heatAnims) { AnimationState aState = a[ThermalAnim]; aState.speed = 0; aState.enabled = true; a.Play(ThermalAnim); heatAnimStates[i++] = aState; } } private void UpdateHeatEffect() { float temperatureValue = Mathf.Clamp01((part.temperature - draperPoint) / (part.maxTemp - draperPoint)); foreach (AnimationState a in heatAnimStates) { a.normalizedTime = temperatureValue; } } } Conclusion Well hopefully you learned vaguely how to use PartModule and ConfigNode from all that. It was all written to be really simple and thus I hope it turns out to be. I\'m always open to questions and feedback, send me a mail if you really need to. I will hold a few dev sessions over the coming weeks to get some feedback and answer more detailed questions. In part #2 i\'ll talk about resources, how to define them and how to use them.
  14. Welcome to the 0.15 patch. I want to outline a number of new bits that have come in and show you how to use them. These bits have filtered down from the new code which has been written to be easier to work with and easier to expand. Please note that modding functionality has not changed at all. You can continue to use whatever interface you were doing and everything currently should be backwards compatible. However we are moving away from functional code in Part for reasons outlined below. What and why Firstly however I want to talk about the reasons for the change so you can understand (and comment on) the direction moving to 0.16 and beyond. Originally KSP was designed as a single-vessel orbital physics game. As the success of the project increased people obviously wanted more and so more features were added into the code. Multiple vessels, etc. The community plugin development also wasnt originally planned and was tacked on at some point. All these factors combined means that the core code is not easy for us to expand upon or easy for you lot to write plugins for. No doubt we all have big plans for KSP and to implement them properly we need to expand the base and make it fit for purpose. So the plan has been to abstract what a Part and a Vessel actually are. A vessel is a collection of connected parts, but with some orbit stuff tacked on. A part is a physical object which can be connected to another, also has some code tacked on too. Therefore.. - Part gets split into Part (model & physical connection) and PartModule (functional code). - Vessel gets split into PartAssembly (a list of Parts) and Vessel (some orbit stuff). Having PartAssembly seperate from Vessel means that we can create other types of groups of parts. Internal spaces, virtual cockpits, kerbal personalities, buildings, etc. Thus one editor screen can function as an editor for all types of assembly. Having PartModule seperate from Part means we get to a smaller group of core parts which just define types of attachment logic and they can be have many (or none) code behaviours layered onto it. 'All very well and good', you say, 'but its a complicated horrible mess of interconnectedness how are we gonna deal with that? Eh? Eh?!'. A fine question, ignorance is bliss, data and structure should be on a need to know basis. Having to learn how a specific thing works is a chore. So to deal with this is an in built event messaging system. It deals, from your point of view, in strings. (It doesnt ofc, it uses pre-reflection and ints for lil extra performance). Basically a PartModule, Part or Vessel can send messages to things and recieve events from things. A code module ideally should be coded that it is ignorant of anything outside it. There are cases where you may want a group of modules to communicate with eachother, these can either be done with the messaging or ofc through direct references as before. Defining attributes in your module code of KSPEvent (on methods) or KSPField (on fields) exposes that event/field to the internal reflection. These attributes also contain data for linking the event/field to the part action gui. You can also alter the values for your event and field attributes at runtime to control the flow of your code. That last bit sounds confusing. When you see it in action later it\'ll sink in. Lastly we need a simple, easy and powerful way of defining configuration files. For this purpose ConfigNode was born. Its an incredibly simple recursive node/value list and can be used a few ways. It contains all the code for reading and writing all the config files for the game. PartModule So, the long awaited PartModule class. This is a piece of code which can be attached to any Part in its config file. You can add as many as you like and add multiple of the same type should you need to. They are added to the Part GameObject itself and you still have access to part and vessel directly from PartModule. PartModules are very simple and currently contain only 6 overrides (compared to Part\'s.. err.. 34). Here is a PartModule showing all of its overrides in place... public class ModuleTest : PartModule { /// <summary> /// Constructor style setup. /// Called in the Part\'s Awake method. /// The model may not be built by this point. /// </summary> public override void OnAwake() { } /// <summary> /// Called during the Part startup. /// StartState gives flag values of initial state /// </summary> public override void OnStart(StartState state) { } /// <summary> /// Per-frame update /// Called ONLY when Part is ACTIVE! /// </summary> public override void OnUpdate() { } /// <summary> /// Per-physx-frame update /// Called ONLY when Part is ACTIVE! /// </summary> public override void OnFixedUpdate() { } /// <summary> /// Called when PartModule is asked to save its values. /// Can save additional data here. /// </summary> /// <param name='node'>The node to save in to</param> public override void OnSave(ConfigNode node) { } /// <summary> /// Called when PartModule is asked to load its values. /// Can load additional data here. /// </summary> /// <param name='node'>The node to load from</param> public override void OnLoad(ConfigNode node) { } } Looks rather simple doesnt it and thats because it is. Its not set in stone and if we need more we can add them. In reality I hope you only have to use very few of those overrides for any given module. Most of the loading and saving will be taken care of KSPField attributes unless you want to save complicated stuff. You can use any Unity MonoBehaviour method apart from Awake. You can use OnDestroy as a destructor. Update and FixedUpdate are perfectly fine (you should check if the part is controllable first). There is no guarantee of having a rigidbody attached in the standard FixedUpdate. Example #1 - KSPEvents and KSPFields So lets look a more complicated example with communication between two modules and some KSPEvent/KSPField malarky. Here is ModuleCommand. It would sit on a command pod and scream orders to everything else. In this case it just screams one order, InputGearToggle. public class ModuleCommand : PartModule { public override void OnUpdate() { if (FlightInputHandler.state.gearDown || FlightInputHandler.state.gearUp) { part.SendEvent('InputGearToggle'); } } } The key feature of ModuleCommand is part.SendEvent(evtName) this tells its host part to send an event into the assembly. The Part sends the event to all of its modules and all of its attached neighbours, who in turn send it to their modules, ad infinitum. So we need something to respond to that order. Here is ModuleAnimatorLandingGear. Technically it doesnt animate anything. It just changes a float from 0 to 1 and renames its gui event to reflect its current state. public class ModuleAnimatorLandingGear : PartModule { [KSPField] public float gearExtension = 0f; [KSPEvent(guiActive = true, guiName = 'Toggle Gear')] public void InputGearToggle() { if (gearExtension == 0f) { gearExtension = 1f; Events['InputGearToggle'].guiName = 'Retract gear'; } else { gearExtension = 0f; Events['InputGearToggle'].guiName = 'Extend gear'; } } public override void OnLoad(ConfigNode node) { if (gearExtension == 0f) { Events['InputGearToggle'].guiName = 'Extend gear'; } else { Events['InputGearToggle'].guiName = 'Retract gear'; } } } Plenty new going on here! We have one field, gearExtension which has the KSPField attribute applied. This makes this field persistant and it will be written to/from any saves as required. After KSPFields are parsed by PartModule then the OnLoad method is fired. As we have nothing else to load, we use OnLoad as a method for working out what to do with our data. In this instance that means setting the event gui name to be correct. The limitation for adding the KSPField attribute is that it can only be applied to classes which implement the IConfigNode interface (more on this later) or one of the types; string, bool, int, float, Vector2, Vector3, Vector4 or Quaternion Here KSPField attribute from the game source.. /// <summary> /// Attribute applied to fields to make them persistant or available to the part action GUI /// /// Automatic persistance can only be applied to types which implement the IConfigNode interface or /// one of the following.. /// string, bool, int, float, Vector2, Vector3, Vector4 or Quaternion /// </summary> [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = false)] public class KSPField : System.Attribute { /// <summary> /// Is this field persistant? /// </summary> public bool isPersistant; /// <summary> /// Is this field active on gui /// </summary> public bool guiActive; /// <summary> /// Is this field active on gui /// </summary> public string guiName; /// <summary> /// Is this field active on gui /// </summary> public string guiUnits; /// <summary> /// The gui format string for this field (D4, F2, N0, etc). Blank if none /// </summary> public string guiFormat; /// <summary> /// string category id /// </summary> public string category; public KSPField() { this.isPersistant = true; this.guiName = ''; this.guiUnits = ''; this.guiFormat = ''; this.category = ''; } } The method InputGearToggle has the KSPEvent attribute applied. This makes this event able to be internally reflected and recieve events thus is the entry point for most functionality. It will be fired in response to the ModuleCommand\'s part.SendEvent. KSPEvent also makes this method available to the gui in form of a labelled button. You can change the guiActive, guiName or any other KSPEvent value at run time by using the Events list. Here is the KSPEvent attribute from the game source.. /// <summary> /// Tells the compiler that this method is an action and allows you to set up /// the KSP specific stuff. /// ** REQUIRED BY ALL ACTION METHODS ** /// </summary> [System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true)] public class KSPEvent : System.Attribute { /// <summary> /// The external name of this action /// </summary> public string name; /// <summary> /// Is this action assigned as the part\'s default? /// * Will override any previous default * /// </summary> public bool isDefault; /// <summary> /// Is this action initially active? /// </summary> public bool active; /// <summary> /// Is this action available to the user? /// </summary> public bool guiActive; /// <summary> /// The guiIcon name (guiAction must be true) /// </summary> public string guiIcon; /// <summary> /// The gui name for this action (userAction must be true) /// </summary> public string guiName; /// <summary> /// A string category id so can display all actions of certain types /// </summary> public string category; public KSPEvent() { = ''; this.isDefault = false; = true; this.allowStaging = false; this.autoStaging = false; this.guiActive = false; this.guiIcon = ''; this.guiName = ''; this.category = ''; } }
  15. Black terrain fix and some optimisations are coming in the next patch also will be able to force the game into SM2 mode to give you a bit more speed.