DMagic Posted November 2, 2016 Share Posted November 2, 2016 (edited) Newer versions of KSP support Unity’s updated UI system (technically available since KSP 1.0, but only really practical since 1.1), which is a major improvement over the old OnGUI system. Rather than rebuilding the UI every frame using the esoteric GUILayout and GUI system, the new Unity UI is built using standard Unity objects and hierarchies. This significantly improves performance, reduces the garbage created, and allows for some fancy effects that weren’t practical with OnGUI. The only problem is that building the new UI entirely through code is extremely tricky, to the point of being impractical in all but the simplest cases. So there are basically two options for using the new UI. One is built into KSP and is perfect for creating relatively simple windows that don’t require much customization. This is the PopupDialog system, it uses notations similar to the old GUILayout system, but generates a new Unity UI object. There is a thread with more details on PopupDialogs and some examples. The other option is to build your UI in the Unity editor, export it as an AssetBundle, and load it into KSP. There are also two options for how to handle this. You can create the UI in Unity without importing any assemblies into the Unity editor. When you load this UI into KSP you will need to manually hook up all of the elements through code. Things like the function called by a button, or the string of a text element will all have to be manually assigned. This is OK for simpler UIs, but can become prohibitively tedious for more complex UIs. Sarbian’s GC Monitor is an example of this type of UI. For a more complex UI it can be simpler to import an assembly directly into the Unity editor, allowing you to set button, or toggle listeners, and to store references to UI elements where needed. The only problem here is that you cannot import an assembly that refers to KSP’s Assembly-CSharp.dll, the primary KSP assembly. This means that any mod using a Unity UI will need two assemblies, one to handle the KSP side of things, and another that can be imported into Unity and will handle the UI side of things. The KSP assembly can keep a reference to the UI assembly, but the UI assembly can’t refer to the KSP assembly, since that will refer to Assembly-CSharp (this would create a kind of nested Unity project in the code, which Unity won't accept). This means that communication between the two assemblies will be difficult. It's worth noting here, that while you can't import any references to the KSP assembly in your Unity project, nothing is preventing you from adding that reference to your code outside of Unity. If you are careful you can setup your code in such a way that only a single assembly is needed. This would require either doing all of the Unity UI work before hooking things up to KSP, or just walling off anything that refers to KSP code into separate classes that could be disabled when you need to import the assembly into Unity. This tutorial doesn't cover this option, but this is another option that you can use and could get around some of the complicated interface-based techniques discussed below. But before we get to that we can go over some of the basics of Unity UI creation (generic UI creation is also broadly similar to KSPedia creation, which is covered in its own tutorial). I won’t go into too much detail, since there are a number of very thorough tutorials and examples available. Check out some of these; pay particular attention to the RectTransform as that is the core positioning component of all UI elements, and it can be quite tricky to get a handle on. Using Basic Orbit as an example project we’ll go over several different areas of UI creation, starting with making a simple, static window, one that can’t be re-sized and has a fixed amount of UI elements. I’ll be using the same formatting as in the KSPedia tutorial, with Unity Editor screen elements Bolded and UI objects and elements Bolded and Italicized. The code for Basic Orbit is available on GitHub. Much of the methods used here come from Kerbal Engineer Redux (which uses the Unity UI only for its toolbar button), its source and Unity project can be found on its GitHub page. This is the window we’ll be creating and hooking up to KSP today. It controls the visibility and some options for other screen objects. Section 1: Software and Tools: Required: Unity – Current KSP version (1.9.1) is using Unity 2019.2.2f1 KSP Part Tools – The current package includes what is needed for generating Asset Bundles and the legacy Part Tools files It should be noted that anyone familiar with Unity who can write their own asset bundle script doesn't need to worry about the Part Tools, just write the script for building an asset bundle and you can use the version of Unity that matches KSP If you do so make sure that any scripts you add to the project are placed in a folder called Editor somewhere within your Unity project's Assets folder To start with we need a new Unity project (I’m using Unity 5.2.4f1, since this is the version supported by the KSP Part Tools - KSP 1.4.x uses Unity 2017, but earlier versions should still work; KSP 1.8 and above uses Unity 2019.2). I would suggest making a separate project folder for each UI, you can create a new project in the Unity startup window, you can then copy the KSP Part Tools files into that project’s Asset folder, or just import them into Unity in the normal way. We only need the Part Tools for the Asset Compiler function, which we’ll get to later. Unity Project Setup: The first step is to install Unity and add the Part Tools package. Once Unity is installed open it and create a project for KSP (there are probably other tutorials that cover setting up Unity in more detail; that isn’t really covered here). Go to the Assets Tab along the top -> Import Package -> Custom Package -> Select the PartTools_AssetBundles.unitypackage file This will load all of the bits necessary for KSPedia creation Now you want a new scene; the Hierarchy Window should only have the Camera, which we want because it allows us to view the UI as it will be in-game In the Main Window you’ll want to be in the #Scene Tab, in 2D mode, using regular Shaded mode Section 2: Creating the UI: All Unity UI windows must be the child of a Canvas element, so we’ll need to add one here. In general, we don’t need to export this Canvas, since our UI can be added directly to one of KSP’s Canvases, but we need it in the editor to actually see anything we create. Add a Canvas: GameObject Tab -> UI -> Canvas This adds the Canvas element and the EventSystem The default properties should be fine, you’ll want to set Pixel Perfect on, since most KSP canvas’ seem to use this, it simply makes sure that all UI elements are snapped to pixel edges Render Mode should be on Screen Space – Overlay, this is for basic UI styles, a simple window on the screen To this Canvas we then add a Panel: Right-click on the Canvas -> UI sub-folder -> Panel The Panel is the basic window object, this is where we add buttons, labels, images, and so on The Panel has a RectTransform, a Canvas Renderer, and an Image element The RectTransform controls the window size and position The Canvas Renderer is a required component for any UI element that will actually be drawn on-screen The Image is the window background image By default, the Panel is the same size as the Canvas and uses the Stretch Anchor, meaning it will always stretch to the size of the Canvas, we obviously don’t want this Click on the Anchor image (the blue arrows, or red lines) in the RectTransform element and select the Middle-Center Option, with red lines crossing in the center You may end up wanting the Anchor to be Top-Left and the Pivot to be 0, 1, but you can leave them as they are for now This will set the Anchor to the center of the Panel, and will change the available RectTransform Fields, we can now directly set the size of the Panel, and they will be fixed Once we set the Panel size it will look like this: The Pivot (blue circle) and Anchor (white arrows) are in the center and the borders are marked in the corners with the blue dots Adding Elements to the Window: We’ll go over the background images used for the window, and more complex anchoring and pivot functions later, but for now, we can start adding the UI elements to this simple window. Basic UI elements can be added by right-clicking on any object in the Hierarchy Window and selecting a UI object The Add Component button can be used to add those same elements, or any other Unity object or script To start with we’ll add a few Text labels and some Toggles that look like buttons For Text elements just right-click the Panel and select a Text object under the UI tab To position the element we can drag it to the desired location, it should snap to the center line The width and height can be changed by dragging the edges, or by changing their values in the RectTransform properties Dragging the edges will make the element un-centered, whereas editing the height and width in the RectTransform will adjust the element’s size based on its pivot position (centered by default) Since this window is fixed we don’t need to worry about the anchor position, but if you want to make sure that an element stays at the top of the window you can change the anchor to Top-Center, or Top-Right if you want it stay in that corner, this can help when making a UI if you are frequently adjusting the window size For fixed Text elements we can just type in the Text Field whatever we want, for dynamic elements (which are set by something in-game) we’ll cover them later The Text properties: size, style, alignment, color, etc… can all be adjusted in the Text element’s properties A quick note about hierarchy and draw order here: Elements lower in the hierarchy are always drawn above higher elements This means that child elements are drawn over their parents, and siblings are drawn in order from last to first, with last on top Now to add some Toggle Buttons, since this a group of three related Toggles we can put them all under one parent object Right-Click on the Panel and select Create Empty, this will add a simple RectTransform with no UI elements We can adjust its position, size and anchor so that it can hold the Toggles and be fixed relative to the top of the window Now add a Toggle element directly to the new empty object (all Unity objects can be renamed by double-clicking on the in the Hierarchy window) Unity doesn’t have native Toggle Buttons, so the element created here is a standard toggle with a label and checkbox We can adjust these elements to work as a Toggle Button just by changing their size and positions (and some code that we’ll get to later) We need to go from this: To this (yes it looks odd, but we’ll get to specifics in a bit): Creating a Toggle Button and Adjusting the RectTransform: To make what we need we basically only have to adjust the RectTransform component for each UI element in the Toggle element. The Toggle itself is made up of a Toggle Script, which controls actually activating and deactivating it, the Background Image, which by default is the empty checkbox, the Checkmark Image, which is only shown when the Toggle is put into the “on” state, and a Text Label. We want this Toggle to look like a standard KSP button, so the images should fill the entire object and the text should be centered The Background Image will be set to standard KSP button styles (the regular button, a brighter button for when the mouse is over the element, and a darker button for when the mouse is actually clicking on it) and the Checkmark Image will be set to the darker, pressed KSP button; since the Checkmark Image is drawn over the Background Image, it will hide the standard button images when the Toggle is on To do this we need to adjust the Background and Checkmark Image RectTransforms to fill the entire object We set the Anchor to Stretch-Stretch (four blue arrows), this will make the element stretch to fit the size of its parent, it also replaces the size and position fields with offsets, so if we want an element to stretch with its parent, but always have 10 pixel borders around the edge, we can set each field to 10, here we want them all set to 0, the same size as its parent We do the same with the Checkmark Image For the Label, we generally want some padding around the edges, and the text should be changed to centered alignment We’ll get into actually assigning images to these elements later on, for now these are all using the default Unity UI element sprites (which can be seen in KSP in the debug window and a few other places) We can also replace the Text label with an Image element, this will simply draw an icon over the Toggle Button rather that a label Images can be imported into Unity by simply copying them into a folder in the Unity Project’s Asset folder, or by dragging them directly into the Unity Editor’s Project Window Images should be imported as Sprite (2D and UI) and the Generate Mip Maps toggle should be off Filling in the other UI elements: I’m not going to go over how to add all of the different UI elements. There are numerous UI tutorials that thoroughly cover different element types; sliders, standard buttons, and text labels are all fairly straightforward to add. For static windows it isn’t too complicated, the RectTransform can get quite complicated, but the best way to understand it is to simply play around with it and change values to see what happens. More complicated UI layouts, and variable size windows will be covered later on. Exporting the Asset Bundle: To load anything into KSP we’ll need to actually export all of our prefabs as an AssetBundle using the Asset Compiler from the KSP Part Tools. The method for this is similar to that described in the KSPedia tutorial, just without the KSPedia Database steps. Drag any prefabs into the Assets folder in your Project Window (this would be the Panel that we added to the Canvas at the start for our window) Set the AssetBundle name in the Properties Panel in the lower right Then open or go to the Asset Compiler window -> Create (for your bundle) -> Update -> Build This will export a file into the AssetBundles folder in your Unity Project, it should have the .ksp file extension Section 3: Hooking up the UI: Now to get to the fun part. By making use of the Unity Editor we can assign methods to all of the toggles and buttons, store references to text elements so they can be updated later, spawn new windows, and much more. To do this we need to make a new assembly that can be imported into the Unity Editor. Any MonoBehaviours defined in this assembly can then be added as scripts to our UI objects. These can be scripts that control specific behaviors, like switching text colors when mousing-over an element, or that control the various window functions. I’ll be referring to the assembly that is imported into the Unity editor as the Unity Assembly, and the assembly that uses KSP code as the KSP Assembly. Unity Assembly: We create our Unity Assembly the same as any other, it should use .Net 3.5, but it should only add references to UnityEngine and UnityEngine.UI, it should not have any references to KSP assemblies. For our KSP Assembly we create that as always, and we add a reference to our new Unity Assembly. This means that the KSP Assembly can call any public methods from the Unity Assembly, modify any public fields, and implement interfaces. But the Unity Assembly can’t directly communicate with the upstream KSP Assembly, or directly use any KSP code. There are a few ways around this, we could use some sort of listener and events system to trigger methods in the KSP Assembly or use interfaces in the Unity Assembly. We’ll be using the interface method. To simplify importing your assembly into Unity you might want to add a post-build event to your VS project that copies the assembly into the Unity folder: copy /y "$(TargetPath)" "C:\YourUnityProjectFolder\Assets\Plugins\" Creating Scripts for Unity: Any class that inherits from a Unity MonoBehaviour can be imported into Unity and added as a component to any other Unity object. To import an assembly into the Unity Editor just drag the .dll into your Unity project’s Asset folder, there should be a separate Plugins folder Once it is imported you can add the script to any Unity object: Add Component -> Scripts -> Your.Assembly.NameSpace -> YourScript Now the script is added to that object and a new instance will be instantiated whenever that object, or its parent object is created These scripts can accomplish several things They can store references to elements of the UI that need to modified by the script They can add behaviors to elements, such as controlling text color, or for replacing standard Unity Text elements with KSP’s new vector font Text Mesh Pro elements They can be used to assign listeners to buttons, toggles, sliders, etc… For the simple window that we’ve already created we have a Text element that needs to be updated in-game (the little mod version label), and several toggles. Because we want to set the initial state of some of these toggles (one controls whether a separate window is open or closed, so if it’s already open that toggle should be set to the on state), and because we want some of the toggles to affect others, we need to store references to the Toggle elements. And we need to assign listeners to the Toggle scripts. Any field with Primitive Types, or Unity Object Types can be set in the script then assigned to in the Unity Editor Storing these fields allows for easy access in the script using UnityEngine; using UnityEngine.UI; namespace BasicOrbit.Unity.Unity { [RequireComponent(typeof(RectTransform))] public class BasicOrbit_Example : MonoBehaviour { [SerializeField] private Toggle m_OrbitToggle = null; [SerializeField] private Toggle m_OrbitDragToggle = null; [SerializeField] private Toggle m_OrbitSettingsToggle = null; [SerializeField] private Text m_VersionText = null; } } Note the RequireComponent attribute at the top, this simply means that the specified types must also be present on the same GameObject All UI elements that actually draw something on the screen (images, text, etc…) require a CanvasRenderer, for example If that component isn’t present on the object it will be added when you add the script Public fields will automatically be added to the script’s Inspector tab You can set attach the [NonSerialized] attribute to public fields to prevent them from being shown in the editor or serialized Private fields can be added by attaching the [SerializeField] attribute (unity chops off the m_ part of the field's name in the Inspector window) These fields can be filled in by simply dragging the desired elements into their respective fields, or by selecting the little circle to right of the field and selecting the element from a list of all valid elements in the project, or by filling in the desired value for primitive types Now we can access these fields from any instance of the script, though it is still a good idea to check if they are null, in case of errors made when setting up the UI, or exporting your prefabs To access or update these properties we just use these references public void updateVersionText(string newText) { if (m_VersionText != null) m_VersionText.text = newText; } public void setInitialToggle(bool isOn) { if (m_OrbitToggle != null) m_OrbitToggle.isOn = isOn; } One thing to note about setting Toggle states like this, whenever you change a Toggle’s isOn field, it will trigger that Toggle’s Listener Events, so anything you or anyone else has attached to this Toggle will be triggered. One way of getting around this is to have a Bool set to False while you are doing the initial setup. Then set your Toggle Listener to not do anything when the Loaded Flag is False, after the setup is complete you can set the Flag to True. Now to add listeners to Toggles and Buttons. The Unity UI attaches listeners to Unity Events triggered by a Button, Toggle, Slider, etc… Any public method that meets the requirement of that particular event can be added as a listener. public void OrbitPanelToggle(bool isOn) { if (!loaded) return; if (m_OrbitToggle == null) return; //Turn on Orbit Panel } public void OrbitDragToggle(bool isOn) { if (m_OrbitDragToggle == null) return; //Turn on Orbit Panel drag state } public void OrbitSettingsToggle(bool isOn) { if (m_OrbitSettingsToggle == null) return; //Spawn Orbit Panel settings window } public void myButtonListener() { //Methods with no arguments can be added to any button or to any other element if the argument does not need to be specified } public void mySliderAlpha(float alpha) { if (!loaded) return; if (m_AlphaText != null) m_AlphaText.text = alpha.ToString("P0"); //Change panel background alpha } Inside the Inspector tab for any Unity element with a Unity Event there is a section for adding listeners, you can add more by selecting the plus button on the bottom. Add a listener by first selecting an object for the little box below the “Runtime Only” box This will be the object that contains the script which has your listener You can either drag the object into the box, or select it from the list using the little circle In this case we select the parent Panel object, which has our example script The specific method is then selected in the box on the right This box has a list of all components attached to the selected object Select the Example script which will then show a list of all public methods that can be chosen Now we can just basically repeat these steps wherever needed. If you need access to an object somewhere, just add a reference to it in your script and assign it in the editor. If you need more listeners, just add them. More complicated behaviors will be explained later. One thing to note about Buttons, Toggles, etc, is the Transition and Navigation elements in their Inspector tabs. Transition refers to how the element behaves in its different states Sprite Swap transitions mean that different sprites are used for the normal state, when the mouse is over the object, or for when the mouse is clicking on the object Color Tint just adjusts the color tint for the attached Image element in those same states Animation uses Unity Animations to design more complex behaviors This is something that will be covered more later, but KSP generally uses Sprite Swap transitions, and for this example these states will all be setup in-game Navigation refers to keyboard navigation and is generally something that should be deactivated When you click on an object it will become the “active” object until you click somewhere else unless Navigation is disabled This means that the element will remain in the Highlighted state Section 4: Assembly Communication: Now that we have our UI hooked up to the Unity Assembly we need to get it communicating with the KSP assembly. There are probably several ways of handling this, but I’ve been using Interfaces in the Unity Assembly to handle it. The basic idea is to create one or more interfaces with the methods and fields needed to send information between the two assemblies, then we add those interfaces to objects in our KSP Assembly. This basically serves two purposes, the interface us used to set the initial state of the UI when it is created, using information from KSP, this could be persistent data, or just anything that can be altered at run time, like the name of a vessel. And it allows for the UI elements to make changes on the KSP side, by setting persistent data, or triggering a KSP-related function. In our last example we had a window with several Toggle elements, a Text field, and a method for the alpha Slider, so the interface contains what is needed to setup those elements, and to transfer data to the KSP Assembly for persistent storage. namespace BasicOrbit.Unity.Interface { public interface IExample { string Version { get; } bool ShowOrbit { get; set; } float Alpha { get; set; } } } The Version string is read-only, since the UI does nothing to alter it The Show Orbit bool is read to set the initial state, but can also be set by the UI when clicking on the Toggle The Alpha float is also used to set the initial state, and needs to be updated when changing the Slider value So now we can set the UI’s initial state by implementing our interface on an object in the KSP Assembly and calling the Setup method. private IExample exampleInterface; public void setInitialState(IExample example) { if (example == null) return; exampleInterface = example; if (m_VersionText != null) m_VersionText.text = example.Version; if (m_OrbitToggle != null) m_OrbitToggle.isOn = example.ShowOrbit; if (m_AlphaSlider != null) m_AlphaSlider.value = example.Alpha; if (m_AlphaText != null) m_AlphaText.text = example.Alpha.ToString("P0"); loaded = true; } public void OrbitPanelToggle(bool isOn) { if (!loaded) return; if (m_OrbitToggle == null) return; if (exampleInterface != null) exampleInterface.ShowOrbit = isOn; //Turn on Orbit Panel } public void mySliderAlpha(float alpha) { if (!loaded) return; if (exampleInterface != null) exampleInterface.Alpha = alpha; if (m_AlphaText != null) m_AlphaText.text = alpha.ToString("P0"); //Set panel background transparency } Note that we store a reference to the interface for use by the listener methods Make sure to set the Loaded Flag to true if needed The code for actually turning on the separate panel, or changing the background image’s alpha channel can all be handled within the Unity Assembly Section 5: Turning it On: Now we have to be able to actually turn on the UI. To do this we need a reference to the UI Prefab and a button somewhere to trigger the UI. We can let KSP load the AssetBundle that was exported from Unity, anything with a .ksp file extension should be loaded, or we can load it ourselves (KSP won’t load it twice, so there is no duplication of resources doing it this way; you can also just remove the .ksp extension to hide it from KSP’s asset loader). I’ve been loading it myself, it works find, and it allows me to open it immediately upon starting KSP. If we need to process or update all of the prefabs it can be useful to load in all of the prefabs, but to generate a window all you really need is the primary prefab (anything that will be created directly by the KSP Assembly). We can store prefabs as references in our Unity scripts by adding a serializable GameObject. A simple KSPAddon can be used to manually load and store a reference to the prefab: [KSPAddon(KSPAddon.Startup.Instantly, true)] public class BasicOrbitLoader : MonoBehaviour { private static GameObject panelPrefab; public static GameObject PanelPrefab { get { return panelPrefab; } } private void Awake() { string path = KSPUtil.ApplicationRootPath + "GameData/YourMod/PrefabFolder"; AssetBundle prefabs = AssetBundle.LoadFromFile(path + "/your_bundle_name"); panelPrefab = prefabs.LoadAsset("Your_Prefab_Name") as GameObject; } } Then we need an object that implements our interface; this can get its data from a persistent settings file, or a scenario module, from in-game data, or any other suitable source: [KSPAddon(KSPAddon.Startup.Flight, false)] public class BasicExample : MonoBehaviour, IExample { private string _version; private static BasicExample instance = null; public static BasicExample Instance { get { return instance; } } private void Awake() { instance = this; _version = "Assembly Info Version"; } public string Version { get { return _version; } } public bool ShowOrbit { get { return BasicSettings.Instance.showOrbitPanel; } set { BasicSettings.Instance.showOrbitPanel = value; } } public float Alpha { get { return BasicSettings.Instance.panelAlpha; } set { BasicSettings.Instance.panelAlpha = value; } } } To open the window we just need to setup a toolbar button as normal and use the Open function to start the UI: BasicOrbit_Example window; private void Open() { if (BasicOrbitLoader.PanelPrefab == null) return; GameObject obj = Instantiate(BasicOrbitLoader.ToolbarPrefab, GetAnchor(), Quaternion.identity) as GameObject; if (obj == null) return; obj.transform.SetParent(MainCanvasUtil.MainCanvas.transform); window = obj.GetComponent<BasicOrbit_Example>(); if (window == null) return; window.setInitialState(BasicExample.Instance); } The arguments in the Instantiate method are used to set the window’s position and rotation The rotation is set to zero Here the position is set using the Toolbar Button’s GetAnchor method There are several canvases that could be used, but in general the MainCanvas will probably be best We can get a reference to the UI script since it is attached to the newly instantiated object The window can be closed by either hiding it or destroying it You can hide the window by setting its gameObject.SetActive(false) This can be used if the window has a complicated initial setup and you don’t want to keep repeating that This should be enough to get a basic window into KSP. Future sections will go over dynamic UI generation and UI layouts elements, specific UI features, KSP-style UI elements, and using TextMeshPro for all text elements. They should also, hopefully, be much shorter, since they won’t have to cover so much information. If you want to setup your UI with the legacy Unity GUI style elements, they are available for free on the Asset Store. You will need to import them into Unity and use the Sprite Editor to set the Splicing lines properly (so that the images stretch to fit whatever size is needed). After that you can simply drag the sprites into your Image elements wherever needed. Edited June 22, 2020 by DMagic Quote Link to comment Share on other sites More sharing options...
DMagic Posted November 21, 2016 Author Share Posted November 21, 2016 (edited) In this section we’ll go over how to use layout groups, how to make your window adjust its size to the content within, and how to attach a variable number of elements using prefabs. In principle Unity UI layout groups are simple, there are Horizontal and Vertical Layout groups. By adding child elements, with Layout Elements attached, to a GameObject with a Layout Group the children will be organized according to their order, with their size adjusted according to the Layout Element settings. Section 1: Layout Groups and Elements: Basic Orbit has two panels that use Layout Groups to create a variable-sized window. These two windows (the background image transparency is turned down very low) use the same prefab, but a different number of readout modules. There are two components that drive windows like this; the Layout Group and the Content Size Fitter. Layout Groups: Vertical and Horizontal Layout Groups are standard components that can be added to any UI element through the Add Component button in the Unity Editor Inspector Panel. Only one Layout Group can be placed on a GameObject, but as many as is necessary can be placed on child objects. For this example we are only using Vertical Layout Groups, Horizontal Layout Groups work essentially the same way. To make a window like those shown above we need to create a new Panel and add a Vertical Layout Group element The script has several properties Padding allows for a border to be placed around the edges of all child Layout Elements Padding is applied after all child elements are placed Spacing places the specified number of pixels between each child Layout Element Child Alignment doesn’t seem to do anything; I think it may be used for cases where your Layout Elements don’t fill out all of the available space Child Force Expand can be used to make the child elements expand to fill out available space in the window; this would usually be used for Width with a Vertical Layout Group By itself the Layout Group won’t do very much, so we need to add child Layout Elements In this case, each element will need an empty GameObject and two child Text elements We could use the Text elements as Layout Elements (perhaps within a parent Horizontal Layout Group), but it is generally best to minimize the number of Layout Elements The Layout Element has several fields to specify its size and flexibility Min Width and Height are used to set the minimum allowed size, they also set the baseline for the Preferred Size Preferred Width and Height can be used for some more advanced behavior, such as variable sized objects based on the length of the text content, which we’ll get to later This is also used frequently used in conjunction with the Content Size Fitter, which we’ll also get to later Flexible Width and Height can be used to limit how much the size of the element can be changed A value of 0 means no change is allowed; I believe 0 is implied if the option is unchecked Note how most of the RectTransform values cannot be adjusted, since they are being controlled by the parent Layout Group Now we have a window with several Layout Elements under a parent Vertical Layout Group. To get the window to adjust automatically to these, and any additional elements, we’ll need to tell the window how to fit everything. Content Size Fitter: At the very top-level Layout Group we need to add a Content Size Fitter component. Only one should be added to window (which can sometimes make things tricky in Unity, since we are usually creating all the different UI elements as separate objects that will be combined later in-game) and it should be added alongside a Layout Group element. These are the scripts attached to the top-level object in this hierarchy. It’s important to note that the Image element is included here; this is used as the window’s background image and will be re-sized along the window according to how many Layout Elements are present (the Canvas Group is used to control the fade in and out behavior that we will get to much later). Also, note how the RectTransform’s Height field cannot be altered, since it is under the control of the Content Size Fitter. There is a Vertical Layout Group added here; this is the direct parent of the Vertical Layout Group described above Since this is a simple window none of its properties need to be changed, more complex windows may need adjustments to the Padding or Spacing The Content Size Fitter is added and its Vertical Fit field is set to Preferred Size The Horizontal Fit does not need to be changed, since the window is intended to be fixed-width This will force the window to adjust its size properties to fit the Preferred Height values of all of the child Layout Elements It also takes into account all of the Spacing and Padding specified in any included Layout Groups Each time a new Layout Element is added or removed the window will be re-sized to adjust This also happens when a child GameObject is set to active or inactive The Layout Groups and Content Size Fitter are simple in principle, but can get quite complicated to deal with when working with, and adjusting Layout Elements. Layout Element Prefabs: When we want to dynamically add Layout Elements to a Layout Group it is sometimes necessary to make separate prefabs that can be instantiated multiple times depending on your needs. Basic Orbit is constructed this way, with each readout module consisting of a Layout Element and two Text Elements. The prefab is then instantiated as many times as is necessary and added to the readout panel. For the parent panel we just need to store a reference to the Prefab GameObject, and to the Transform where we want to add the prefabs, this transform should have a Layout Group that will control the size and positioning of the elements. The picture with the Content Size Fitter above demonstrates this in its Basic Orbit_Panel script. Section 2: Adding Prefab Elements to a Layout Group: Now that we have our Layout Groups and Prefabs setup we need to actually add them to our window in-game. First off we need a script attached to the window with references to the Prefabs and to the Transform where they will be added (see part I for how to generate these scripts; prefabs can be dragged into a Serializable GameObject field the same as any other object). Generate a list of all the elements that need to be added to the window For Basic Orbit these are readout modules defined in the KSP Assembly This could be anything, for instance, a list of vessels with their names and locations Enumerate through the list and instantiate a new instance of the prefab for each element This process is generally the same as that defined in part I The only difference is that the new UI Element’s Transform parent is set to the Window Transform described above You can hide the new element, or use some logic for dynamically turning elements on and off if necessary Using gameObject.SetActive(true/false) will change the visibility of the element, and make the Layout Group readjust the window to compensate In general, you should try to limit SetActive calls Use gameObject.activeSelf or activeInHierarchy to check if an element is active or not before using SetActive Using a list of the layout elements from the KSP Assembly (using the interface method described in part I) we can instantiate and add our new objects to the Layout Group one at a time. Since not all elements will necessarily be active when they are first added we set their active status based on information from the KSP Assembly. It is sometimes useful to keep references to the UI elements that we are adding, that way they can be updated, removed, or monitored in some other way whenever we need them. [SerializeField] private GameObject m_ModulePrefab = null; [SerializeField] private Transform m_ModuleTransform = null; private List<BasicOrbit_Module> Modules = new List<BasicOrbit_Module>(); private IBasicPanel panelInterface; public void setPanel(IBasicPanel panel) { if (panel == null) return; panelInterface = panel; CreateModules(panel.GetModules); } private void CreateModules(IList<IBasicModule> modules) { if (modules == null) return; if (m_ModulePrefab == null || m_ModuleTransform == null) return; for (int i = modules.Count - 1; i >= 0; i--) { IBasicModule module = modules[i]; if (module == null) continue; CreateModule(module); } } private void CreateModule(IBasicModule module) { GameObject mod = Instantiate(m_ModulePrefab); if (mod == null) return; mod.transform.SetParent(m_ModuleTransform, false); BasicOrbit_Module bMod = mod.GetComponent<BasicOrbit_Module>(); if (bMod == null) return; bMod.setModule(module); bMod.gameObject.SetActive(module.IsVisible || module.AlwaysShow); Modules.Add(bMod); } Section 3: Text Content-Based Layout Element Size: There is a special case when dealing with Layout Elements where you may want an object to fit the size of its text content, for instance, you have a button that might have a long text string. Layout Elements and Layout Groups can be used to handle this. There is also a section in the Unity tutorials that deals specifically with this. A specific setup is required for this The Text Element should be a direct child of the element that needs its size to be adjusted For instance, you have a Button Script with an Image on the same GameObject, then the Text Element should be a child of that GameObject The parent object (the Button) should have a Layout Group Element (either Vertical or Horizontal should work) The Text Element should have a Layout Element Define a Min Width and Height, but leave the Preferred values unchecked The Pivot point (the blue circle, not to be confused with the anchor point) for the Text Element should be put at the origin of where you want it to expand from Top-center if you want the Button to have a fixed width, variable height and fixed top position Somewhere upstream needs to be a Content Size Fitter with its Vertical Fit set to Preferred Size When all of these requirements are met the UI element should adjust its size based on the length of the text content Note here the Layout Element on the Text Element (left), with its Preferred Height field left blank, the Pivot position, and the Layout Group on the parent object (right) with the Toggle Script and Image. The Contracts Window + source and Unity project files are also available on GitHub, though in general I would recommend not using it as a reference, it is an extremely complicated UI and has many elements that are adapted specifically to its needs. In the next section we’ll go over how to apply KSP style UI elements to our creations. Edited November 21, 2016 by DMagic Quote Link to comment Share on other sites More sharing options...
DMagic Posted November 29, 2016 Author Share Posted November 29, 2016 (edited) In this section we’ll go over how to implement style elements for your UI so that your windows, buttons, sliders, etc… can match the KSP style. KSP defines a UISkinDef that contains all of the sprites used for the different states of all the common UI elements. Using this we can assign those sprites to our UI. You could also, theoretically define your own UISkinDef, using the old Unity Smoke UI elements, for instance, and possibly allow for switching between the two styles (if you only want to use the Unity UI style it would probably be easier to just assign all of the sprites within Unity. The basic method used here, like much of the material covered in this tutorial comes from Kerbal Engineer Redux. Section 1: Tagging UI Elements: The first step is to create some kind of tag for all of the different UI elements that we are using, this will be used to identify those elements and to determine which KSP style to apply to them. Create a simple script in your Unity Assembly Define an Enum with options for each type of UI element Window, box, slider, button, toggle button, regular toggle, etc… Text will be covered in a later section Add a serializable field for that Enum Add the script to each UI Element in your Unity project and choose the matching option for the Enum Field You want the script to be attached to the same GameObject that needs styling applied The window or box option should be added to a GameObject with an Image element The button should be added to a GameObject with the Button script, etc… If you add in new Enum options later on make sure to them to the end of the list, otherwise you will need to update the selection for each existing GameObject that the script has been added to public class BasicStyle : MonoBehaviour { public enum ElementTypes { None, Window, Box, Button, Toggle, Slider } [SerializeField] private ElementTypes m_ElementType = ElementTypes.None; public ElementTypes ElementType { get { return m_ElementType; } } } Section 2: Finding the Tags: Once you’ve added the script to each UI Element that needs styling you’ll need to create a method for processing each UI Element from in-game. Engineer processes each element whenever it is created, but I think it’s better to work at the prefab level, loading and processing each prefab as soon as possible so that it only needs to be done once. Create a script in your KSP Assembly for loading and processing the prefabs The UISkinDefs can be accessed through UISkinManager defaultSkin contains the definitions that will be of interest in most cases These are only available once the Main Menu has been loaded, so prefab processing can’t occur during the initial loading scene I’ve been manually loading the assetbundles and then loading the prefabs from there It’s also possible to keep the .ksp file extension on the assetbundle and let KSP load it Make sure that all prefabs in your assetbundle use the same tag name Store a reference to the list of all prefabs so they can be processed when ready [KSPAddon(KSPAddon.Startup.MainMenu, false)] public class BasicOrbitLoader : MonoBehaviour { private static GameObject[] loadedPrefabs; private void Awake() { if (loadedPrefabs == null) { string path = KSPUtil.ApplicationRootPath + "GameData/BasicOrbit/Resources"; AssetBundle prefabs = AssetBundle.LoadFromFile(path + "/basic_orbit_prefabs"); if (prefabs != null) loadedPrefabs = prefabs.LoadAllAssets<GameObject>(); } if (loadedPrefabs != null) { if (UISkinManager.defaultSkin != null) processUIPrefabs(); } } } Once the prefabs are loaded we can use the Style script as a tag for finding which elements need to be processed Enumerate through each prefab, selecting the Style object from every child of that prefab Use the UISkinDef elements and the Style Enum to process each element according to its needs private void processUIPrefabs() { for (int i = loadedPrefabs.Length - 1; i >= 0; i--) { GameObject o = loadedPrefabs[i]; if (o != null) processUIComponents(o); } } private void processUIComponents(GameObject obj) { BasicStyle[] styles = obj.GetComponentsInChildren<BasicStyle>(true); if (styles == null) return; for (int i = 0; i < styles.Length; i++) processComponents(styles[i]); } private void processComponents(BasicStyle style) { if (style == null) return; UISkinDef skin = UISkinManager.defaultSkin; if (skin == null) return; switch (style.ElementType) { case BasicStyle.ElementTypes.Window: style.setImage(skin.window.normal.background, Image.Type.Sliced); break; case BasicStyle.ElementTypes.Box: style.setImage(skin.box.normal.background, Image.Type.Sliced); break; case BasicStyle.ElementTypes.Button: style.setButton(skin.button.normal.background, skin.button.highlight.background, skin.button.active.background, skin.button.disabled.background); break; case BasicStyle.ElementTypes.Toggle: style.setToggle(skin.button.normal.background, skin.button.highlight.background, skin.button.active.background, skin.button.disabled.background); break; case BasicStyle.ElementTypes.Slider: style.setSlider(skin.horizontalSlider.normal.background, skin.horizontalSliderThumb.normal.background, skin.horizontalSliderThumb.highlight.background, skin.horizontalSliderThumb.active.background, skin.horizontalSliderThumb.disabled.background); break; default: break; } } Here we can see how each component of the UISkinDef has multiple UIStyleStates, each with a corresponding sprite Section 3: Applying the Style: Now that we have the KSP style elements we can apply those to our UI elements. Since the Style script is attached to the same GameObject as the UI Element that needs processing the method is simple. Create a method for each different UI element type Use the sprites from the KSP Assembly to make your UI elements look how you want them to Most interactable UI Elements share a common base, Selectable, which is what we actually want to access to apply our new style private void setSelectable(Sprite normal, Sprite highlight, Sprite active, Sprite inactive) { Selectable select = GetComponent<Selectable>(); if (select == null) return; select.image.sprite = normal; select.image.type = Image.Type.Sliced; select.transition = Selectable.Transition.SpriteSwap; SpriteState spriteState = select.spriteState; spriteState.highlightedSprite = highlight; spriteState.pressedSprite = active; spriteState.disabledSprite = inactive; select.spriteState = spriteState; } public void setImage(Sprite sprite, Image.Type type) { Image image = GetComponent<Image>(); if (image == null) return; image.sprite = sprite; image.type = type; } public void setButton(Sprite normal, Sprite highlight, Sprite active, Sprite inactive) { setSelectable(normal, highlight, active, inactive); } public void setToggle(Sprite normal, Sprite highlight, Sprite active, Sprite inactive) { setSelectable(normal, highlight, active, inactive); Toggle toggle = GetComponent<Toggle>(); if (toggle == null) return; //The "checkmark" sprite is replaced with the "active" sprite; this is only displayed when the toggle is in the true state Image toggleImage = toggle.graphic as Image; if (toggleImage == null) return; toggleImage.sprite = active; toggleImage.type = Image.Type.Sliced; } public void setSlider(Sprite background, Sprite thumb, Sprite thumbHighlight, Sprite thumbActive, Sprite thumbInactive) { //The slider thumb is the selectable component setSelectable(thumb, thumbHighlight, thumbActive, thumbInactive); if (background == null) return; Slider slider = GetComponent<Slider>(); if (slider == null) return; Image back = slider.GetComponentInChildren<Image>(); if (back == null) return; back.sprite = background; back.type = Image.Type.Sliced; } Simple UI Elements like the Window background, or the content Box are only an Image Only the sprite, and possibly the sprite color and Image.Type need to be accessed Image.Type is an Enum that defines the different Sprite types possible, in most cases UI elements use Sliced sprites, where the edges are defined and the center is stretched to fill in the space Many KSP UI elements (though not generally the ones found in the default UISkinDef) are actually white and use a color overlay to give the final result For Selectable UI Elements we need the sprites for the different states, normal, highlight (mouse over the element) active or pressed (mouse clicking on the element), inactive or disabled (when the element can’t be interacted with) The Selectable’s Image Component is used to define the normal state The SpriteState component is used to define the transition style (SpriteSwap) for standard KSP UI Elements and the sprites for each state The Toggle UI Elements have an additional component, the Checkmark Image, which is activated only when the Toggle is in the On state This could be an actual check mark, or a circle to fill in the Toggle radio button, or a Toggle Button sprite The Toggle.graphic component is the Checkmark Image For Toggle Buttons we can use the pressed/active sprite so that it appears as a button that has been pressed down when active, this basically draws the pressed sprite over the standard sprite Other components can be handled in the same manner Some don’t directly store their image components, so you’ll need to make note of how the UI Element is arranged in Unity and manually find each Image The same process can applied to non-standard UI components by picking apart their prefab Surface Experiment Pack uses the sliders from the experiment results dialog Maneuver Node Evolved uses most of the components from the part context menus In the next section we’ll go over how to use TextMeshPro elements in your UI. These are the new vector font text elements used by KSP that make text much cleaner and more readable. The overall method is similar to how UI style elements are applied, but there are some special concerns that need to be covered to make it work properly. Edited November 29, 2016 by DMagic Quote Link to comment Share on other sites More sharing options...
DMagic Posted December 15, 2016 Author Share Posted December 15, 2016 (edited) This section will cover how to replace the standard Unity Text elements with the fancy new TextMeshPro elements included with the latest version of KSP. Since we can’t add the KSP assemblies to Unity we won’t be able to directly add TMP elements to out Unity project, unless you happen to own the $95 TMP Asset. So instead we’ll have to use standard Text elements as placeholders then do all of the replacements from in-game. This turns out to be fairly simple, the only real issue is that we need some method for dynamically updating the text, or other properties from in-game. Section 1: Tagging Text Elements: Much like in the last part of the tutorial, the first step is to create a simple script that can be used to tag all of the Text elements that we wish to replace. Create a script in your Unity Assembly This script will also be used for updating the Text Field of the element whenever it needs changing A simple UnityEvent can be used to trigger then text change when needed Listeners can be added to this event to watch for text changes and update the Text Field public class TextHandler : MonoBehaviour { public class OnTextEvent : UnityEvent<string> { } private OnTextEvent _onTextUpdate = new OnTextEvent(); public UnityEvent<string> OnTextUpdate { get { return _onTextUpdate; } } } Section 2: Listening for Updates: To handle text updates the simplest method is to create an extension of the standard TMP Text Element with a few added methods. Create an extension class from the TextMeshProUGUI, this is the UI element used for text in KSP This element will be added to the same GameObject that our script is attached to We can grab a reference to our script and add a listener to its UnityEvent A simple method takes the argument from that UnityEvent and uses it to update the Text Field of the TMP Element public class BasicOrbitTextMeshProHolder : TextMeshProUGUI { private TextHandler _handler; new private void Awake() { base.Awake(); _handler = GetComponent<TextHandler>(); if (_handler == null) return; _handler.OnTextUpdate.AddListener(new UnityAction<string>(UpdateText)); } private void UpdateText(string t) { text = t; } } Store a reference to the TextHandler script in the Unity Assembly Invoke the UnityEvent whenever needed public class BasicOrbit_Module : MonoBehaviour { [SerializeField] private TextHandler m_Title = null; private IBasicModule moduleInterface; public void setModule(IBasicModule module) { if (module == null || m_Title == null) return; moduleInterface = module; m_Title.OnTextUpdate.Invoke(module.ModuleTitle + ":"); } } Section 3: Replacing Text Elements: The only tricky part to all of this is how we take the placeholder Text Element and replace it with the Text Mesh Pro Element. The method for this works similar to assigning style elements, covered in the last part. See the last part for how to load the Asset Bundle and process the prefabs during loading Once we have the prefabs we use the TextHandler tag to search for all of the Text Elements that need replacing We cache the properties from the placeholder Text Element to be used for creating the TMP Element Since a GameObject can only contain one UI element at a time we need to immediately destroy the Text Element after caching its properties Then create the new TMP Element and add it to the GameObject private void processTMP(GameObject obj) { TextHandler[] handlers = obj.GetComponentsInChildren<TextHandler>(true); if (handlers == null) return; for (int i = 0; i < handlers.Length; i++) TMProFromText(handlers[i]); } private void TMProFromText(TextHandler handler) { if (handler == null) return; //The TextHandler element should be attached only to objects with a Unity Text element //Note that the "[RequireComponent(typeof(Text))]" attribute cannot be attached to TextHandler since Unity will not allow the Text element to be removed Text text = handler.GetComponent<Text>(); if (text == null) return; //Cache all of the relevent information from the Text element string t = text.text; Color c = text.color; int i = text.fontSize; bool r = text.raycastTarget; FontStyles sty = getStyle(text.fontStyle); TextAlignmentOptions align = getAnchor(text.alignment); float spacing = text.lineSpacing; GameObject obj = text.gameObject; //The existing Text element must by destroyed since Unity will not allow two UI elements to be placed on the same GameObject MonoBehaviour.DestroyImmediate(text); BasicOrbitTextMeshProHolder tmp = obj.AddComponent<BasicOrbitTextMeshProHolder>(); //Populate the TextMeshPro fields with the cached data from the old Text element tmp.text = t; tmp.color = c; tmp.fontSize = i; tmp.raycastTarget = r; tmp.alignment = align; tmp.fontStyle = sty; tmp.lineSpacing = spacing; //Load the TMP Font from disk tmp.font = Resources.Load("Fonts/Calibri SDF", typeof(TMP_FontAsset)) as TMP_FontAsset; tmp.fontSharedMaterial = Resources.Load("Fonts/Materials/Calibri Dropshadow", typeof(Material)) as Material; tmp.enableWordWrapping = true; tmp.isOverlay = false; tmp.richText = true; } Different fonts are available and there is a TMP Font defined in the UISkinManager described in the last part There are two properties of the Text Element that don’t translate directly to Text Mesh Pro The Font Style and Text Alignment TMP does not have the bold - italic style and has more alignment options For these there are simple methods to convert to TMP private FontStyles getStyle(FontStyle style) { switch (style) { case FontStyle.Normal: return FontStyles.Normal; case FontStyle.Bold: return FontStyles.Bold; case FontStyle.Italic: return FontStyles.Italic; case FontStyle.BoldAndItalic: return FontStyles.Bold; default: return FontStyles.Normal; } } private TextAlignmentOptions getAnchor(TextAnchor anchor) { switch (anchor) { case TextAnchor.UpperLeft: return TextAlignmentOptions.TopLeft; case TextAnchor.UpperCenter: return TextAlignmentOptions.Top; case TextAnchor.UpperRight: return TextAlignmentOptions.TopRight; case TextAnchor.MiddleLeft: return TextAlignmentOptions.MidlineLeft; case TextAnchor.MiddleCenter: return TextAlignmentOptions.Midline; case TextAnchor.MiddleRight: return TextAlignmentOptions.MidlineRight; case TextAnchor.LowerLeft: return TextAlignmentOptions.BottomLeft; case TextAnchor.LowerCenter: return TextAlignmentOptions.Bottom; case TextAnchor.LowerRight: return TextAlignmentOptions.BottomRight; default: return TextAlignmentOptions.Center; } } That’s basically all there is to it. The result is much cleaner looking text that scales well and is readable in much smaller fonts. The only real problem areas are UI elements that require a standard Text Element, such as a Text Input Field. A custom input field will be required to handle these, or you can just leave them as standard Text, since they generally aren’t used as much. The full source code for the most complete version of the TextHandler and Text Mesh Pro extension (from Contracts Window +) are included below. Feel free to use the code directly. TMP Source Code: Spoiler Text Handler: using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.Events; namespace ContractsWindow.Unity { public class TextHandler : MonoBehaviour { public class OnTextEvent : UnityEvent<string> { } public class OnColorEvent : UnityEvent<Color> { } public class OnFontEvent : UnityEvent<int> { } private OnTextEvent _onTextUpdate = new OnTextEvent(); private OnColorEvent _onColorUpdate = new OnColorEvent(); private OnFontEvent _onFontChange = new OnFontEvent(); private Vector2 _preferredSize = new Vector2(); public Vector2 PreferredSize { get { return _preferredSize; } set { _preferredSize = value; } } public UnityEvent<string> OnTextUpdate { get { return _onTextUpdate; } } public UnityEvent<Color> OnColorUpdate { get { return _onColorUpdate; } } public UnityEvent<int> OnFontChange { get { return _onFontChange; } } } } Text Mesh Pro Extension Class: using ContractsWindow.Unity; using UnityEngine; using UnityEngine.UI; using UnityEngine.Events; using TMPro; namespace ContractsWindow { public class CWTextMeshProHolder : TextMeshProUGUI { private TextHandler _handler; new private void Awake() { base.Awake(); _handler = GetComponent<TextHandler>(); if (_handler == null) return; _handler.OnColorUpdate.AddListener(new UnityAction<Color>(UpdateColor)); _handler.OnTextUpdate.AddListener(new UnityAction<string>(UpdateText)); _handler.OnFontChange.AddListener(new UnityAction<int>(UpdateFontSize)); } public void Setup(TextHandler h) { _handler = h; _handler.OnColorUpdate.AddListener(new UnityAction<Color>(UpdateColor)); _handler.OnTextUpdate.AddListener(new UnityAction<string>(UpdateText)); _handler.OnFontChange.AddListener(new UnityAction<int>(UpdateFontSize)); } private void UpdateColor(Color c) { color = c; } private void UpdateText(string t) { text = t; _handler.PreferredSize = new Vector2(preferredWidth, preferredHeight); } private void UpdateFontSize(int i) { fontSize += i; } } } Replacement Code: private void processPrefab(GameObject obj) { if (obj == null) return; TextHandler[] handlers = obj.GetComponentsInChildren<TextHandler>(true); if (handlers == null) return; for (int i = 0; i < handlers.Length; i++) TMProFromText(handlers[i]); } private void TMProFromText(TextHandler handler) { if (handler == null) return; Text text = handler.GetComponent<Text>(); if (text == null) return; string t = text.text; Color c = text.color; int i = text.fontSize; bool r = text.raycastTarget; FontStyles sty = getStyle(text.fontStyle); TextAlignmentOptions align = getAnchor(text.alignment); float spacing = text.lineSpacing; GameObject obj = text.gameObject; MonoBehaviour.DestroyImmediate(text); CWTextMeshProHolder tmp = obj.AddComponent<CWTextMeshProHolder>(); tmp.text = t; tmp.color = c; tmp.fontSize = i; tmp.raycastTarget = r; tmp.alignment = align; tmp.fontStyle = sty; tmp.lineSpacing = spacing; tmp.font = Resources.Load("Fonts/Calibri SDF", typeof(TMP_FontAsset)) as TMP_FontAsset; tmp.fontSharedMaterial = Resources.Load("Fonts/Materials/Calibri Dropshadow", typeof(Material)) as Material; tmp.enableWordWrapping = true; tmp.isOverlay = false; tmp.richText = true; tmp.Setup(handler); } private FontStyles getStyle(FontStyle style) { switch (style) { case FontStyle.Normal: return FontStyles.Normal; case FontStyle.Bold: return FontStyles.Bold; case FontStyle.Italic: return FontStyles.Italic; case FontStyle.BoldAndItalic: return FontStyles.Bold; default: return FontStyles.Normal; } } private TextAlignmentOptions getAnchor(TextAnchor anchor) { switch (anchor) { case TextAnchor.UpperLeft: return TextAlignmentOptions.TopLeft; case TextAnchor.UpperCenter: return TextAlignmentOptions.Top; case TextAnchor.UpperRight: return TextAlignmentOptions.TopRight; case TextAnchor.MiddleLeft: return TextAlignmentOptions.MidlineLeft; case TextAnchor.MiddleCenter: return TextAlignmentOptions.Midline; case TextAnchor.MiddleRight: return TextAlignmentOptions.MidlineRight; case TextAnchor.LowerLeft: return TextAlignmentOptions.BottomLeft; case TextAnchor.LowerCenter: return TextAlignmentOptions.Bottom; case TextAnchor.LowerRight: return TextAlignmentOptions.BottomRight; default: return TextAlignmentOptions.Center; } } Edited December 15, 2016 by DMagic Quote Link to comment Share on other sites More sharing options...
DuoDex Posted December 16, 2016 Share Posted December 16, 2016 A most excellent topic. Pinned for the foreseeable future. Quote Link to comment Share on other sites More sharing options...
DMagic Posted March 31, 2017 Author Share Posted March 31, 2017 (edited) I have several updates made for SCANsat that I'll add here eventually, but for now I'll just add a section on how to get a Text Mesh Pro Input Field into KSP using a method similar to that used for the TextMeshProUGUI. This method follows the same basic idea as the previous entry: tag any standard Unity Input Fields that you want to replace with a small script, create a TMP_InputField extension class in the KSP Assembly, then convert the Unity Input Field into a TMP_InputField when processing the UI components. Section 1: Tagging Input Fields: Make a script for tagging of the Unity Input Fields that need replacing and to handle a few things. public class InputHandler : MonoBehaviour { private string _text; private bool _isFocused; public class OnTextEvent : UnityEvent<string> { } public class OnValueChanged: UnityEvent<string> { } private OnTextEvent _onTextUpdate = new OnTextEvent(); private OnValueChanged _onValueChanged = new OnValueChanged(); public string Text { get { return _text; } set { _text = value; } } public bool IsFocused { get { return _isFocused; } set { _isFocused = value; } } public UnityEvent<string> OnTextUpdate { get { return _onTextUpdate; } } public UnityEvent<string> OnValueChange { get { return _onValueChanged; } } } This script has two UnityEvents and a few fields. The OnTextEvent can be used to update the text for the input field; this is separate from the PlaceHolder text, which could, if needed, be updated from another similar event. The OnValueChanged event is fired whenever anything is typed into the input field You could add a listener to this event to perform some action as the text is typed in The _text string is used to store the current text entered into the input field This would be used when you have a separate button to perform an action after text is entered The _isFocused field is used to indicate when the input field is active and can be typed in This is used when locking out other KSP controls Section 2: Listening for Input Updates: A TMP_InputField extension class is used to listen for text input and a few other things. public class SCAN_TMP_InputField : TMP_InputField { private InputHandler _handler; new private void Awake() { base.Awake(); _handler = GetComponent<InputHandler>(); onValueChanged.AddListener(new UnityAction<string>(valueChanged)); _handler.OnTextUpdate.AddListener(new UnityAction<string>(UpdateText)); } private void Update() { if (_handler != null) _handler.IsFocused = isFocused; } private void valueChanged(string s) { if (_handler == null) return; _handler.Text = s; _handler.OnValueChange.Invoke(s); } private void UpdateText(string t) { text = t; } } The standard TMP_InputField onValueChanged event is used to update the text in the InputHandler and to trigger its onValueChanged event It also updates the InputHandler IsFocused field It adds a small method to the InputHandler's OnTextUpdate event to update the input field's text value. Section 3: Unity setup: The Input Field needs to be setup in Unity in a certain way to properly replace it with a TMP_InputField. First we have to make sure to add the Text-to-TextMeshProUGUI tag to the Text and the Placeholder text components And make sure to process the Text fields before processing any Input Fields; the TMP_InputField will only work with TMP_Text components Text Mesh Pro uses a Rect Mask to handle masking text that overflows the text box, rather than the string methods used by the standard Unity Input Field Add a Rect Mask 2D as a direct child of the Input Field, then add the two Text components to that object I'm using an Event Trigger and the OnInputClick event to handle control locks whenever an Input Field is clicked on, as you can see in the bottom of the Inspector Tab. Section 4: Replacing Input Field Elements: As before, we process the TMP_InputField during loading to replace all standard Input Fields. We do this by caching a few values, generating a new TMP_InputField, and assigning the required values, and assigning the TextMeshProUGUI components. private static void TMPInputFromInput(InputHandler handler) { if (handler == null) return; InputField input = handler.GetComponent<InputField>(); if (input == null) return; int limit = input.characterLimit; TMP_InputField.ContentType content = GetTMPContentType(input.contentType); float caretBlinkRate = input.caretBlinkRate; int caretWidth = input.caretWidth; Color selectionColor = input.selectionColor; GameObject obj = input.gameObject; RectTransform viewport = handler.GetComponentInChildren<RectMask2D>().rectTransform; SCAN_TextMeshPro placholder = handler.GetComponentsInChildren<SCAN_TextMeshPro>()[0]; SCAN_TextMeshPro textComponent = handler.GetComponentsInChildren<SCAN_TextMeshPro>()[1]; if (viewport == null || placholder == null || textComponent == null) return; MonoBehaviour.DestroyImmediate(input); SCAN_TMP_InputField tmp = obj.AddComponent<SCAN_TMP_InputField>(); tmp.textViewport = viewport; tmp.placeholder = placholder; tmp.textComponent = textComponent; tmp.characterLimit = limit; tmp.contentType = content; tmp.caretBlinkRate = caretBlinkRate; tmp.caretWidth = caretWidth; tmp.selectionColor = selectionColor; tmp.readOnly = false; tmp.shouldHideMobileInput = false; tmp.fontAsset = UISkinManager.TMPFont; } The first section handles caching a few values A conversion method is required for the ContentType field, but the available types for Input Fields and TMP_InputFields are the same Then we have to find the Rect Mask and the two TextMeshProUGUI components and cache these Then we Destroy the old Input Field and create a new TMP_InputField Then assign the cached values It is important to assign the TextMeshProUGUI components first, since assigning some of the others values relies on these After all of this you'll end up with Input Fields that use the much nicer Text Mesh Pro text, providing better scaling and better support for other input languages. I'll add links to the full code for this once it is online. Edited March 31, 2017 by DMagic Quote Link to comment Share on other sites More sharing options...
DMagic Posted April 27, 2017 Author Share Posted April 27, 2017 Apparently this managed to slide by without notice. It would seem the Text Mesh Pro is now free and will eventually be integrated into Unity: You can download it from the Asset Store for free, so I guess my tedious workarounds for not having access to TMP in Unity are no longer necessary. Quote Link to comment Share on other sites More sharing options...
JPLRepo Posted April 27, 2017 Share Posted April 27, 2017 2 hours ago, DMagic said: Apparently this managed to slide by without notice. It would seem the Text Mesh Pro is now free and will eventually be integrated into Unity: You can download it from the Asset Store for free, so I guess my tedious workarounds for not having access to TMP in Unity are no longer necessary. Different versions though until KSP is upgraded to the version of Unity that has the free TMPro embedded in it. Just for you to be aware of. Quote Link to comment Share on other sites More sharing options...
sarbian Posted April 27, 2017 Share Posted April 27, 2017 3 hours ago, DMagic said: Apparently this managed to slide by without notice. It would seem the Text Mesh Pro is now free and will eventually be integrated into Unity I did not mention it for one simple reason: the free version does not uses the same script id as the paid one. So you should not be able to import Asset made with the free version in KSP (feel free to prove me wrong, it would be better for the modding community). And this will be a mess to migrate into when it is integrated (unless they add a tool later, or the KSP's dev write a script to do it) Quote Link to comment Share on other sites More sharing options...
EricL Posted September 13, 2017 Share Posted September 13, 2017 @DMagic Nice tutorial, but it's a little confusing. For example, how do you make the scripts? Do you make them in one big cs file or create them separately? (in either situation, Unity complains about not being able to find the IExample interface) And some feedback: I prefer the tutorials that provide base code, which you can modify later to your needs Quote Link to comment Share on other sites More sharing options...
Fengist Posted September 16, 2017 Share Posted September 16, 2017 Nice tutorial. Definitely. And I got 90 percent of the way through setting up a GUI and decided my forehead couldn't take one more smash against my desk. Quote Link to comment Share on other sites More sharing options...
HebaruSan Posted September 16, 2017 Share Posted September 16, 2017 (edited) 4 hours ago, Fengist said: And I got 90 percent of the way through setting up a GUI and decided my forehead couldn't take one more smash against my desk. Personally, I made it to the first screenshot of Unity. Have you seen the other API? Edited September 16, 2017 by HebaruSan Quote Link to comment Share on other sites More sharing options...
Fengist Posted September 16, 2017 Share Posted September 16, 2017 7 hours ago, HebaruSan said: Personally, I made it to the first screenshot of Unity. Have you seen the other API? Yes, thanks, that's exactly what I did. It's not what I wanted but in 10 lines of code I achieved what was taking me hours and costing me money in pain killers. I was hoping for a set of 4 buttons with arrow icons to direct movement. After seeing exactly what it was going to take to achieve that... I mean I honestly tried. I got to the point I was adding the code into the KSP Asset as he calls it, and ended up throwing my hands up in disgust because it made no sense... all the references and callbacks. All this work for 4 buttons with arrows? It should not be this difficult. My end users will get stupid looking text buttons. Quote Link to comment Share on other sites More sharing options...
DMagic Posted September 18, 2017 Author Share Posted September 18, 2017 (edited) @EricL A comprehensive tutorial would be nice, but it would take far more time than I'm willing to put into it. This tutorial assumes some experience with C# (you can put all of your classes and scripts into a single file, I think, maybe Unity requires separate files, but in general that just makes it very difficult to keep track of things), and Unity. There are a million resources, tutorials and answers to specific questions available for both C# and Unity (in particular Unity has some really good video series available for UI content), so there's no need to go over all of that here. For complete code you can check the source for Basic Orbit (it has changed a bit since this tutorial has been made, but most of the added complexity is in picking apart the stock KSP UI assets to make a matching style). There is also some more complex code for SCANsat's UI, and some simpler code for Maneuver Node Evolved. The Unity project files are also included in those GitHub repos. Overall I don't think the Unity UI system is that complicated. It is based on standard Unity objects, so most of the UI related work also relates to how most things work in Unity. The Rect Transform and Layout Elements are probably the trickiest aspects to figure out, there are lots of tutorials covering them, but the best thing to do is probably just play around with different values and see what they do, being able to work within the Unity Editor makes this simple since you don't have to launch KSP to test things. Hooking things up with KSP is tedious, but not really difficult. You either have to just manually assign functions and listeners, or go through interfaces or some kind of event system. It takes some time to get your head around how to set things up, but once you manage that it just becomes tedious. Edited September 18, 2017 by DMagic Quote Link to comment Share on other sites More sharing options...
nightstalker101s Posted March 14, 2018 Share Posted March 14, 2018 (edited) I'm having difficulties understanding how the two assemblies are speaking with each other. I'm missing something in the initial post and its confusing me. I assume that we have the KSP assembly and the Unity assembly. The Unity assembly does not need to discuss jack with the KSP assembly. The KSP assembly has an include of the Unity assembly? Which allows it to access the classes and variables? If this is so, then are we dealing with .dll assemblies or simply .cs files contained within the same project or folder? If the Unity assembly is turning into a .dll assembly... how is that possible? Please ignore me. I found the exact line which says exactly what I was asking. Sorry. Edited March 14, 2018 by nightstalker101s Revision Quote Link to comment Share on other sites More sharing options...
nightstalker101s Posted March 15, 2018 Share Posted March 15, 2018 (edited) I'm working on designing a UI and when I update and build using the part tool assembly compiler I can't find A) Asset Bundle file or B) anything with a .ksp extension. I know I'm doing something wrong, but I don't know what it is. I'm running Unity3D 2017.3.1f1 Edited March 15, 2018 by nightstalker101s Quote Link to comment Share on other sites More sharing options...
DMagic Posted March 16, 2018 Author Share Posted March 16, 2018 @nightstalker101s KSP uses Unity 2017.1. It might not matter, but it's usually a good idea to stick to the same version. I don't know what's going on with Part Tools, maybe we'll get an update soon. But you don't need them unless you are doing KSPedia or parts. For these simple asset bundles I just write my own script: using UnityEditor; public class Bundler { const string dir = "AssetBundles"; const string extension = ".btk"; [MenuItem("BetterTracking/Build Bundles")] static void BuildAllAssetBundles() { BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.ForceRebuildAssetBundle, BuildTarget.StandaloneWindows); FileUtil.ReplaceFile(dir + "/better_tracking_prefabs", dir + "/better_tracking_prefabs" + extension); FileUtil.DeleteFileOrDirectory(dir + "/better_tracking_prefabs"); } } You can make a C# script in Unity (this is not something to add to your plugin, it is just for the Unity editor), you just have to make sure it's in a folder called "Editor" (so Unity project folder/Assets/Editor/script.cs). This adds a toolbar button along the top row in the editor, then it builds any asset bundles you have defined, and in this case, attaches a custom file extension. The target directory appears alongside your Assets folder. .ksp files will be loaded automatically by KSP during loading. But I just handle the asset loading myself, so you can use any extension, or none, for that: https://github.com/DMagic1/KSP_BetterTracking/blob/master/Source/BetterTracking/Util/Tracking_Loader.cs#L84-L91 Quote Link to comment Share on other sites More sharing options...
nightstalker101s Posted March 16, 2018 Share Posted March 16, 2018 I understand your example code. I'm not tracking how you are loading them yourself into KSP. I'm assuming KSP loads all it's assets, goes to main menu, then when appropriate your plugin starts calling on the private asset bundle? I really don't "need" to know this, but it is very interesting as I may decide to use this later for some unknown reason. The newest Unity made several functions obsolete which are used by the parts tool. I installed the version you listed in your OP and had no problems building the bundle. It took me a minute to understand that literally I am building a UI bundle (I don't even need textures or anything actually in the Unity project) and then using Visual Studio to build two separate libraries (.dll). I had a serious oh duh moment when I realized what you meant in the OP. You did a great job at explaining it, but it really doesn't make sense (no fault of yours) until you are actually doing it. Even worse, if someone isn't familiar with Unity or other game engines it makes it even more difficult to understand. It seems silly to have to build a library (I understand why) in VS instead of just having the part tool asset bundler compile a .dll for any scripts attached to the asset bundle, but again I get why. Thank you for your assistance. Side Note: I am working on what I'm calling K.O.P.C.O. ("K.A.S.A. Operation Chief Campaign Overhaul"). I'm going to try to build more of a story line or story line feel without causing restrictions to gameplay into KSP. The player starts out being selected as the Operational Chief Director by 11 chairmen of the Kerbal Aeronautics and Space Administration. This was step one because the player will need to participate in interviews later and I needed a UI which is easier than trying to figure out how to program and animate a whole scene (maybe some day). Thank you again. Quote Link to comment Share on other sites More sharing options...
DMagic Posted March 16, 2018 Author Share Posted March 16, 2018 KSP's loading system loads all of its own resources first (I assume before it even gets to the loading screen) then goes through the GameData folder looking for certain file types. .ksp files are treated as asset bundles. I only use .ksp for KSPedia entries, because I don't really know what KSP does with them and I've never really needed to find out. For most of my mods I just wait until the main menu screen then manually load the asset bundle (which is really just a collection of GameObjects) and doing whatever needs to be done from there, in most cases that is either adding stock KSP UI style sprites to the UI, saving prefabs for use later on, or replacing standard Unity Text components with TextMeshPro components. And yes, you don't really have to group things together for export in Unity, just label the objects you want with the asset bundle name and it will package together any sprites, textures, or whatever that those objects refer to. Which reminds me, now that KSP is using Unity 2017 I need to add another section here about using the new sprite atlas system. Quote Link to comment Share on other sites More sharing options...
stelarfox Posted August 20, 2018 Share Posted August 20, 2018 I am sorry but I am following by the letter your instructions and it just does not work. You are explaining it as if everyone here knows exactly what it should do, and thats not the case, I am an expert on c# and know my deal in Unity, but was unable to follow all you said, even reading it 10 times and doing it at the same time. Mostly because some stuff is totally out of context. And some parts are not explained at all. of course this is my first mod so i do not know much of ksp, and thats mostly the problem. Is there any truly basic UI to see this?, or can you say in which file to set each step My main problem is, after have done the Unity part, doing the rest (the hook). because from the ksp section It is unable to see what it should. Also, I need this as soon as possible. will be really gratefull for a fast answer. Quote Link to comment Share on other sites More sharing options...
stelarfox Posted August 20, 2018 Share Posted August 20, 2018 my main issue is, how do you do that, if you are in ksp and m_versionText.text, does not exist there. And if you are not in ksp, how you reference it? truly this has no head or tail. if (m_VersionText != null) m_VersionText.text = example.Version; Quote Link to comment Share on other sites More sharing options...
DMagic Posted August 20, 2018 Author Share Posted August 20, 2018 4 hours ago, stelarfox said: because from the ksp section It is unable to see what it should. 3 hours ago, stelarfox said: my main issue is, how do you do that, if you are in ksp and m_versionText.text, does not exist there. And if you are not in ksp, how you reference it? This is the relevant section covering what I think is the problem. Your KSP Assembly needs to have a reference to your Unity Assembly. Then your KSP Assembly can call any method and use any interface defined in the Unity Assembly. The reverse isn't true because you wouldn't be able to import your Unity Assembly into Unity if it had a reference to any KSP code. On 11/2/2016 at 3:08 PM, DMagic said: For our KSP Assembly we create that as always, and we add a reference to our new Unity Assembly. This means that the KSP Assembly can call any public methods from the Unity Assembly, modify any public fields, and implement interfaces. But the Unity Assembly can’t directly communicate with the upstream KSP Assembly, or directly use any KSP code. Quote Link to comment Share on other sites More sharing options...
stupid_chris Posted October 12, 2018 Share Posted October 12, 2018 On 8/20/2018 at 4:53 PM, DMagic said: Your KSP Assembly needs to have a reference to your Unity Assembly. Then your KSP Assembly can call any method and use any interface defined in the Unity Assembly. I'm currently taking a look at this too and also had some confusion at this part. Should I reference the generated Asembly-CSharp generated by the Unity project? Do I also need to bundle this with my mode afterwards? Or should I force all this code into an external .csproj to the Unity generated project and bundle this? Quote Link to comment Share on other sites More sharing options...
DMagic Posted October 12, 2018 Author Share Posted October 12, 2018 9 hours ago, stupid_chris said: I'm currently taking a look at this too and also had some confusion at this part. Should I reference the generated Asembly-CSharp generated by the Unity project? Do I also need to bundle this with my mode afterwards? Or should I force all this code into an external .csproj to the Unity generated project and bundle this? To be clear, the Unity Assembly is one that can be imported into a Unity project and used by assets within that project. It only required references to the UnityEngine.dll and (probably) the UnityEngine.UI.dll. This is imported into Unity as a compiled dll, not as un-compiled .cs files, then all of its classes are accessible by Game Objects within Unity. Whenever you create a Unity project it also generates an Assembly-CSharp.dll (and several others). This is not related to KSP's Assembly-CSharp.dll (well, technically this is how KSP's .dll is generated, through Squad's original KSP Unity Project). The new Assembly-CSharp.dll is the default assembly file where any code added through Unity is compiled. This happens when you create a new .cs script somewhere in your project's Asset folder. That code will be compiled into the Assembly-CSharp.dll. In general you should never have to do anything or worry about the Assembly-CSharp.dll generated by your Unity project. The Unity Assembly that you create is an already compiled .dll file that you import directly into Unity. So the answer is no, you should not reference the Assembly-CSharp.dll generated by the Unity project, and you should not include that file with your mod. When you make the KSP Assembly you do so basically the same way as you would for any other mod. You add references to the UnityEngine.dll and UnityEngine.UI.dll, and to KSP's Assembly-CSharp.dll. Then, in this case, you would also add a reference to your Unity Assembly .dll, so that it can be accessed directly from the KSP Assembly. UnityUIAssembly.dll -> this is the Unity Assembly that is imported into Unity | | ---> UnityEngine.dll ---> UnityEngine.UI.dll KSPAssembly.dll -> this is the KSP Assembly | | ---> Assembly-CSharp.dll -> this is from the KSP folder, not from your Unity project folder ---> UnityEngine.dll ---> UnityEngine.UI.dll ---> UnityUIAssembly.dll -> this is your Unity Assembly file Both the UnityUIAssembly.dll and the KSPAssembly.dll files are placed in a folder in KSP's GameData folder and loaded into KSP. The basic problem, and the reason for this whole run-around method, is that you can't import an assembly into Unity that has a reference to KSP's Assembly-CSharp.dll. Unity gets confused, or thinks there is some loop of references, or something, and it will fail to import. So if you want code that you can access from within the Unity Editor, then you have to make a separate assembly and go through some indirect method of tying that back into KSP's code. It isn't absolutely necessary to do this, Fengist has a great tutorial for how to make a UI with a single, standard KSP Assembly. It works well for simpler UI's, but I would have a hard time doing many of the things that I have in my UI's without having code that is directly accessible from within Unity. You can also use this same method for doing other interesting things, like adding custom behavior into KSPedia entries. Quote Link to comment Share on other sites More sharing options...
stupid_chris Posted October 13, 2018 Share Posted October 13, 2018 7 hours ago, DMagic said: To be clear, the Unity Assembly is one that can be imported into a Unity project and used by assets within that project. It only required references to the UnityEngine.dll and (probably) the UnityEngine.UI.dll. This is imported into Unity as a compiled dll, not as un-compiled .cs files, then all of its classes are accessible by Game Objects within Unity. Okay, that's the part I wasn't too clear about. That makes a lot more sense actually. I've been working a lot with the new object based UI lately on personal projects and yeah I do agree it's probably gonna be a lot more handy to have an assembly directly into Unity. Thanks! Other than that, is there any particular reason it seems to be recommended to anchor in the top left? Just as a convenience or does KSP handle it's Canvas in a specific way that makes this more convenient? Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.