Creating a simple mod with UI

In this tutorial you will create a custom panel with Unity that shows the current amount of trains, and create a new toolbar button to toggle that panel. This tutorial will be assuming that you're using Windows for this tutorial, so if you're not you may have to setup some things differently. It is recommended that you try Making your first mod first if you haven't yet.

## Creating a simple panel

You will first need to install Unity 2020.3 by following the steps on the Unity website (https://unity.com/download#how-get-started - I have 2020.3.14f1 but the current game version runs on 2020.3.33f1, so I think any recent 2020.3 release will work). If you want to release your mod for platforms other than Windows, you will also need Linux and Mac build support (Mono) modules. Start Unity and create a new 2D project called Traincounter UI. You will be greeted by something like this:

The scene viewport in the middle is where you will see your UI. The inspector on the right will show you properties of selected objects, your project's assets folder is visible in the bottom and a hierarchy of all objects is visible on the left. Let's first create a Resources folder and add the PNG files from https://github.com/asdfCYBER/Heatmap/tree/master/Assets/Resources. These will be used to recreate the looks of the game UI. I made these in Inkscape a while ago if you were wondering, it's a great tool.

We now have all the things we need to make a basic UI. In the hierarchy, right-click on the bold SampleScene to add a panel:

Rename the panel to 'Traincounter panel'. We would also like to rename the scene because 'SampleScene' looks stupid, for that we first need to save (ctrl+s) the scene so that it no longer has a `*` after it (this indicates unsaved changes). Now we can rename the scene file in the Scenes folder to 'Traincounter UI'. It'll ask you to reload the scene, that's fine.

Select the panel. It is currently streched so that it fills the entire Canvas object it is a child of. We don't want our panel to fill the entire game screen but have it somewhere in the top-left corner next to the toolbar instead. For that we must change the pivot and anchor points of the panel, which we can do by clicking the image in the Rect Transform component in the inspector, then clicking the top-left option while holding ctrl and alt:

Note how the blue circle (the pivot) and the four white arrows (the anchors) changed location.

Change the width to 500 pixels and the height to 300 pixels. We now want our panel to look more like the panels already present in Rail Route so our mod will blend in more. We first need to *slice* the images in the Resources folder in nine parts, so Unity knows how to apply the image to our panel without having it look weird. Click one of the images, then click the Sprite Editor button in the inspector. Slice the image something like this so that there are four sections with corners, four with sides, and one center:

Save it and repeat for the other images except the dropdown arrow (I find slicing the large images at 72/72/72/72 and the smaller ones at 38/38/38/38 to work fine, but feel free to experiment).

Select the panel again. In the Image component below the Rect Transform we want to change the Source image field and the Color field. Drag the fill image from the Resources folder to the first field, then click the color in the second field to change it to `#021323` (a very dark blue). Also change the alpha value to max, or your UI panel will be partially transparent. Set the 'Pixels Per Unit Multiplier' field to 2.

Now we want to add a border around the panel. Right-click the panel in the hierarchy view and add another UI > panel, name it 'Border'. This time we want to keep it stretched so it is just as large as the panel, and give it the border image with color `#3FDCE0` (again, fully opaque). The multiplier should be set to 2 for this as well.

If you did it right, you should now have something that looks like this:

Although a gradient shader is used for the backgrounds in Rail Route, I find a simple image to work fine as well.

## Adding elements to the panel

Now that we have a panel, we can add things to it. Let's start by adding a simple header that says 'Train counter'. Right-click the panel in the hierarchy window and add a `UI > Text - TextMeshPro`. If there are two options you can choose from (in this case, `UI > Text` and `UI > Text - TextMeshPro`) you always want to use the TMP version, because its text looks far better than the non-TMP version at basically no additional performance cost.

A TMP Importer window will pop up asking you to import some essential stuff. Let it do so, you can ignore the additional examples though. Call the new object something like 'Header' or 'Title', set its anchor and pivot to the upper left corner at Pos X = 35 and Pos Y = -30. The width should be at 250 pixels. Set the text in the `TextMeshPro - Text (UI)` component to 'Train counter' at font size 40 and make it the same blue color as the border. The default font is fine, it doesn't look a lot like the fonts Rail Route uses but it doesn't distract either.

Here's what the panel looks like now:

Let's say we want to be able to select what type of train we are counting. First add a text element that has position (35, -120), a width of 220 and a height of 50. Make the text white, left-middle aligned and font size 32, let it say something like 'Selected type:'. Add a dropdown element and place it next to the label at (260, -120), make the width 260 and the height 50. Replace the UISprite image by the border\_small image, set the multiplier to 4 but leave the color white - we will adjust the color from the Dropdown component. Set the normal, highlighted, pressed, selected and disabled colors to `#3FDCE0`, `#FDDC00`, `#FD5700`, `#FDDC00` and `#063865` respectively, these are the colors used in Rail Route for all buttons/toggles/input fields/dropdowns. In the options section of the dropdown component, add 'All', 'Commuter', 'Intercity', 'Freight' and 'Urban'. It's barely legible, so fold out the dropdown element in the hierarchy window, select the label and make the text white and font size 32. You should now have something like this:

Let's click the 'play' button above the scene window to see the UI in action. Click the dropdown (notice how it changes color). The dropdown options don't look like the rest of the UI at all and the small arrow in the dropdown is barely visible, let's change that. Click the play button again to go back to the scene view, and select the arrow object (a child of the dropdown object). Replace the DropdownArrow with our own dropdownarrow from the Resources directory, check the 'preserve aspect' option and make it 8 pixels high. Go to the 'Template' object and check the box next to the name in the inspector to make it visible. Set the color of the image component to `#3FDCE0`. It looks quite ugly now, but don't worry. Select the Item object (located at Dropdown/Template/Viewport/Content/Item) and set the toggle colors to `#031B31`, `#0C3357`, `#02111F`, `#0C3357` and the last one at pure black (`#000000`, you won't see that color anyway). Make the Item object 45 pixels high, and the Content object 50. Also set the Item y-scale to 0.95. Delete the Item Checkmark object, and adjust the Item Label object to have 7 pixels of space on the left and 8 on the right. Make the text white, and check the 'auto size' font option (set it to a minimum of 24 and maximum of 32). This is what the scene should look like now:

One last thing with the dropdown is to change the scrollbar. Select the scrollbar object and set the background color to `#021323`, and the scrollbar colors to `#0C355B`, `#134B80`, `#134B80`, `#134B80` and `#021323` again. The dropdown is now (finally) done, so uncheck the Template object again in the inspector and test your UI by clicking the play button again. If you're just like me and think that the default scroll speed is way to low, change the scroll sensitivity in the Template object's Scroll Rect component from 1 to 10.

All we need now are two simple labels: one that says 'Amount of trains:' and one that acts as a placeholder. I will assume you know how to create text objects by now, so try to recreate this:

You can also right-align the text if you want, but I think this looks better.

Finally, we need to add a Graphic Raycaster component to the Traincounter panel object to make it visible in the game later: select the object and click the 'Add Component' button. Search for the component we need and add it. It will also add a Canvas component, that's fine.

## Setting up the Visual Studio solution

To get our new UI to do things, we need to feed it values we get from the game. For simple UIs we could manually hook up everything after exporting the panel from Unity, but the more complex the UI gets the more tedious and annoying it becomes. What we would like to do is have the UI store references and do simple UI-things from the editor. However, there's a problem: we can't reference `RailRoute.dll` from inside our Unity project. That means we will need to make two separate assemblies: a `Traincounter.dll` which can reference everything, and a `Traincounter.Unity.dll` which can reference everything except `RailRoute.dll` or `Traincounter.dll`. This is what we'll do here. First, create a new .NET Framework 4.8 (or 4.7.2) Class Library project in Visual Studio. **Make sure you *uncheck* the option 'place solution and project in the same directory', or you will have to manually edit the .sln file later to get a proper file structure.** You should now have a folder in your repositories directory that contains the .sln file and another Traincounter subfolder with the project files. In VS, rename the automatically created `Class1.cs` file to `Traincounter.cs` and add references to `RailRoute.dll`, `UnityEngine.dll` and `UnityEngine.CoreModule.dll`. Add the usual title and description:

```
using Game.Mod;
using Utils;

namespace Traincounter
{
 public class Traincounter : AbstractMod
 {
 public override CachedLocalizedString Title => "Traincounter";

 public override CachedLocalizedString Description => "Shows the amount of trains on the board";
 }
}

```

We'll come back to this assembly later. First we want to make the Unity assembly. Right-click the solution item in the solution explorer and add a new .NET Framework Class Library project in the Traincounter directory, call it `Traincounter.Unity`. Add references to `UnityEngine.dll`, `UnityEngine.CoreModule.dll`. `UnityEngine.UI.dll` and `Unity.TextMeshPro.dll`, and rename the `Class1.cs` file to something like `TraincounterPanelManager.cs`.

## The Unity assembly

The TraincounterPanelManager class will be our own component we will add to the panel object in Unity. To be able to do so, we need to inherit from `UnityEngine.MonoBehaviour`. Any public fields we add to our component will also show up in Unity where we can assign values to them, as long as the types of those fields inherit (directly or indirectly) from `UnityEngine.Object`. Since the only things we need to interface with in our UI are the dropdown and the label with the amount of trains, this will suffice:

```
using UnityEngine;
using TMPro;

namespace Traincounter.Unity
{
 public class TraincounterPanelManager : MonoBehaviour
 {
 public TMP\_Dropdown TrainType;

 public TMP\_Text TrainAmount;
 }
}

```

For more complex UIs you can initialize various things or attach listeners in an `Awake` method, which is called by Unity. In fact, there are a whole range of methods that are automatically called at various points which are described in the Unity documentation. For example, in `Heatmap.Unity.GradientItem` I use the Awake method to initialize an item's background color and to add various listeners.

Compile the project, and drag the resulting DLL in a new `Plugins` directory in Unity (you can also set a post-build event in Visual Studio to copy it to that directory every time you build it). From there, click the triangle on the file to show the classes that are present in the DLL. Now select the panel and drag the `TraincounterPanelManager` class from the assets window to the inspector. You should now see this:

There is a 'Train Type' and 'Train Amount' field in the component we just added. Drag the Dropdown GameObject from the hierarchy window to the first field, and the Amount Placeholder GameObject to the second. That's it, the UI is now done!

## Creating the AssetBundle

Next, create another new folder called 'Editor' in Assets. Add a new C# script in this folder, name it 'BuildAssetBundles.cs'. Doubleclick to open it (this will probably open another Visual Studio window) or open it with Notepad/Notepad++ from the Windows file explorer and replace the contents with this:

```
using UnityEditor;
using System.IO;

public class CreateAssetBundles
{
 [MenuItem("Assets/Build AssetBundles")]
 public static void BuildAllAssetBundles()
 {
 string assetBundleDirectory = "Assets/AssetBundles";
 EnsureDirectory(assetBundleDirectory);

 string windowsPath = Path.Combine(assetBundleDirectory, "windows");
 EnsureDirectory(windowsPath);

 BuildPipeline.BuildAssetBundles(
 windowsPath,
 BuildAssetBundleOptions.None,
 BuildTarget.StandaloneWindows64);
 }

 private static void EnsureDirectory(string path)
 {
 if (!Directory.Exists(path))
 Directory.CreateDirectory(path);
 }
}

```

If you want to build your UI for Linux or Mac as well, this is where you would do that. You can also set this up so that it automatically copies one of the assetbundles to the mod folder for testing. You can find a full version of this file for that in the Logviewer github repository. Save the edited file and close whatever was opened to edit it. This script will add a menu item to Unity so we can build the AssetBundle containing our UI and the necessary resources later.

To export our UI we need to make a prefab. Create a Prefabs folder in the Assets one and drag the panel GameObject from the hierarchy window to the new folder. The panel will turn blue in the hierarchy now:

If you want to add or remove components or objects to the prefab you will need to `right-click > prefab > unpack completely` in the hierarchy window, and after you are done drag it on top of the original prefab file to update it.

With the prefab selected, you can see a AssetBundle dropdown all the way in the bottom right corner of the screen (it is visible in the screenshot above). Click this dropdown, click 'New...' and enter `TraincounterUIassets` as the assetbundle name. Now right-click the prefab and select 'Build AssetBundles' - we added this button with the editor script we made earlier. It'll think for a while and then spit out the finished AssetBundle in `Assets/AssetBundles/windows/traincounteruiassets`. Create a new Traincounter mod folder in the Rail Route mods folder (`%USERPROFILE%/AppData/LocalLow/bitrich/Rail Route/mods`), and copy the traincounteruiassets file and Traincounter.Unity.dll there.

## The main assembly

We'll now return to the Traincounter class. Add a reference to the assembly and to `UnityEngine.AssetBundleModule.dll` so we can load the assetbundle:

```
using System.IO;
using System.Reflection;
using Game.Mod;
using Utils;
using UnityEngine;

namespace Traincounter
{
 public class Traincounter : AbstractMod
 {
 public override CachedLocalizedString Title => "Traincounter";

 public override CachedLocalizedString Description => "Shows the amount of trains on the board";

 public static AssetBundle TraincounterUIAssets { get; } = LoadAssets();

 internal static IControllers \_controller;

 private const string \_assetBundleName = "TraincounterUIassets";

 private static AssetBundle LoadAssets()
 {
 // Load assets from file
 string assembly = Assembly.GetExecutingAssembly().Location;
 string assetBundlePath = Path.Combine(Path.GetDirectoryName(assembly), \_assetBundleName);

 AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundlePath);

 // assetBundle is null if it couldn't be laoded
 if (assetBundle == null)
 {
 Debug.LogWarning("The TraincounterUIassets file could not be loaded! " +
 "Settings will not be visible.");
 return null;
 }

 return assetBundle;
 }
 }
}

```

Now that we have the assetbundle loaded, we have to get the prefab from there and hook it up to the game code. Create `TraincounterPanel.cs` (place it in a 'UI' folder if you want, but for a minimod such as this that won't be necessary), and add a project reference to `Traincounter.Unity.dll`, `UnityEngine.UI.dll`, `UnityEngine.UIModule.dll` and `Unity.TextMeshPro.dll`. Because we only want one panel to exist at a time, we will make this class a singleton. We will also hook up the dropdown and add a method for updating the text. To store the dropdown value and use it internally I added an enum, but it's not strictly necessary (just useful).

```
using Game;
using UnityEngine;
using Traincounter.Unity;

namespace Traincounter
{
 public enum TrainTypeSelection
 {
 All,
 Freight,
 Commuter,
 Intercity,
 Urban
 }

 public class TraincounterPanel
 {
 public static TraincounterPanel Instance { get; private set; }

 public TraincounterPanelManager PanelManager { get; }

 public TrainTypeSelection Mode { get; private set; } = TrainTypeSelection.All;

 private static readonly GameObject \_prefab = LoadPrefab();

 private readonly GameObject \_panel;

 private const string \_prefabName = "Traincounter panel";

 private TraincounterPanel(GameObject parent)
 {
 // Instantiate prefab and find the panel manager component
 \_panel = GameObject.Instantiate(\_prefab, parent.transform, worldPositionStays: false);
 PanelManager = \_panel.GetComponent<TraincounterPanelManager>();

 // Move the panel so it is next to the toolbar
 \_panel.GetComponent<RectTransform>().anchoredPosition = new Vector2(200, -180);

 // Hook up events
 PanelManager.TrainType.onValueChanged.AddListener(ChangeTrainType);
 }

 /// <summary>
 /// Set the mode based on the selected option
 /// </summary>
 public void ChangeTrainType(int index)
 {
 switch (PanelManager.TrainType.options[index].text)
 {
 case "Freight":
 Mode = TrainTypeSelection.Freight;
 break;
 case "Commuter":
 Mode = TrainTypeSelection.Commuter;
 break;
 case "Intercity":
 Mode = TrainTypeSelection.Intercity;
 break;
 case "Urban":
 Mode = TrainTypeSelection.Urban;
 break;
 default:
 Mode = TrainTypeSelection.All;
 break;
 }

 UpdateTraincounter();
 }

 /// <summary>
 /// Set the train amount in the UI to the current amount of selected trains
 /// </summary>
 public void UpdateTraincounter()
 {
 if (\_panel == null)
 return;

 int count = Traincounter.GetTraincount(Mode);
 PanelManager.TrainAmount.text = count.ToString();
 }

 public static void Show()
 {
 Debug.Log("Opening traincounter panel");
 Instance = new TraincounterPanel(GameController.Current.UiCanvas.gameObject);
 }

 public static void Destroy()
 {
 Debug.Log("Closing traincounter panel");
 GameObject.Destroy(Instance.\_panel);
 }

 public static void ToggleShow()
 {
 if (Instance?.\_panel == null)
 Show();
 else
 Destroy();
 }

 private static GameObject LoadPrefab()
 {
 // Load assets from assetbundle
 if (Traincounter.TraincounterUIAssets == null)
 return null;

 // Load prefab from assets and return
 return Traincounter.TraincounterUIAssets.LoadAsset<GameObject>(\_prefabName);
 }
 }
}

```

There are a couple of things to note here:

* First we need to load the assetbundle (this may happen only once), then we need to load the prefab from the loaded assetbundle, and only then can we instantiate the prefab
* To execute a method after the dropdown value is changed, we use the AddListener method: `TrainType.onValueChanged.AddListener(method)`
* We attach our UI to `GameController.Current.UiCanvas.gameObject`. This causes the UI to scale when the scale slider is used, which is very useful

If you're following along you may have noticed I used <code>Traincounter.GetTraincount(Mode)</code> here while that method doesn't exist yet. Let's fix that by adding these methods to `Traincounter.cs`:

```
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Game.Context;
using Game.Contract;
using Game.Mod;
using Utils;
using UnityEngine;

namespace Traincounter
{
 public class Traincounter
 {
 // [...]

 /// <summary>
 /// Return the amount of trains on the game board which
 /// are also selected by <paramref name="selection"/>
 /// </summary>
 public static int GetTraincount(TrainTypeSelection selection)
 {
 return \_controller.TrainRepository.Trains.Count(
 train => train.OnBoard && TrainTypeSelected(selection, train.TrainType));
 }

 /// <summary>
 /// Return whether <paramref name="type"/> is selected
 /// </summary>
 public static bool TrainTypeSelected(TrainTypeSelection typeSelection, TrainType type)
 {
 return typeSelection == TrainTypeSelection.All
 }

 // [...]
 }
}

```

Before we try to add a toolbar button we should see if the UI we have right now works. Let's make it so that the UI is shown when a map is started by adding these methods top the Traincounter class:

```
public override async Task OnContextChanged(IControllers controller)
{
 \_controller = controller;

 if (controller.CurrentMode == GameMode.Play)
 {
 // We will create a toolbar button from this place later

 TraincounterPanel.Show();
 controller.EventManager.StartListening(controller.EventManager.LevelStarted, UpdateTraincounter);

 controller.EventManager.StartListening(controller.EventManager.TrainSpawned, UpdateTraincounter);
 controller.EventManager.StartListening(controller.EventManager.TrainDisposed, UpdateTraincounter);
 }
 else
 {
 controller.EventManager.StopListening(controller.EventManager.TrainSpawned, UpdateTraincounter);
 controller.EventManager.StopListening(controller.EventManager.TrainDisposed, UpdateTraincounter);
 }

 await Task.Yield();
}

/// <summary>
/// Wrapper around <see cref="TraincounterPanel.UpdateTraincounter"/>
/// </summary>
private static void UpdateTraincounter(params object[] args)
{
 if (TraincounterPanel.Instance != null)
 TraincounterPanel.Instance.UpdateTraincounter();
}

```

I also added listeners for the in-game events LevelStarted, TrainSpawned and TrainDisposed, which are the only times the train count should change (if you're wondering why we can't immediately initialize the UI but have to wait for LevelStarted instead: when OnContextChanged runs no trains have been loaded yet so it'd always show 0). Note that subscribing to these events doesn't happen with AddListener but with StartListening this time. Compile the project and place the resulting assembly in your mod's folder (the Unity assembly and assets should already be there from the previous step). Run the game, start a map and let's see if it works:

Next we will add a button to the toolbar. For this we can copy my ToolbarButton.cs and modify it to our liking. Don't forget to add a reference to `UnityEngine.Localization.dll` as well.

```
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Components;
using UnityEngine.UI;
using Game.Hud;
using Game.Hud.Menu;
using TMPro;
using UIElements;

namespace Traincounter
{
 public class ToolbarButton
 {
 public static ToolbarButton Instance { get; private set; }

 /// <summary>
 /// True if the traincounter button is toggled, otherwise false
 /// </summary>
 public static bool Value { get; private set; } = false;

 private readonly MultiTargetButton \_button;

 private readonly GameButton \_buttonHolder;

 /// <summary>
 /// Add a traincounter toggle button to bottom of the left toolbar.
 /// Credit to Yarian for figuring out what to copy and how to edit it!
 /// </summary>
 public ToolbarButton()
 {
 // Find the button that will be copied, and the layoutgroup it is in
 ModalDialogUiController modalDialog = Traincounter.\_controller.GameController.GetModalDialog();
 GameButton sourceButton = modalDialog.GameButtons.ContractsButtonHolder;
 VerticalLayoutGroup buttonGroup = sourceButton.GetComponentInParent<VerticalLayoutGroup>();

 // Copy the button and add it to the layoutgroup
 \_buttonHolder = Object.Instantiate(sourceButton);
 Object.DontDestroyOnLoad(\_buttonHolder);
 \_buttonHolder.transform.SetParent(buttonGroup.transform, worldPositionStays: false);
 \_buttonHolder.gameObject.SetActive(value: true);
 \_button = \_buttonHolder.GetComponentInChildren<MultiTargetButton>();

 CustomiseButton();

 // Make the button a toggle
 \_button.onClick = new Button.ButtonClickedEvent();
 \_button.onClick.AddListener(ToggleValue);
 \_button.onRightClick.AddListener(TraincounterPanel.ToggleShow); // This is how we will show the custom UI panel

 // Increases the layoutgroup size so that buttons are no longer squished
 RectTransform rect = buttonGroup.gameObject.GetComponent<RectTransform>();
 rect.sizeDelta = new Vector2(rect.sizeDelta.x, rect.sizeDelta.y \* 1.5f);
 rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, rect.anchoredPosition.y \* 1.5f);
 }

 public static void Show()
 {
 Destroy(); // destroy existing button if there is one
 Instance = new ToolbarButton();
 }

 public static void Destroy()
 {
 if (Instance != null)
 Object.Destroy(Instance.\_buttonHolder);
 }

 private void ToggleValue()
 {
 Value = !Value;
 Debug.Log($"[Traincounter] Traincounter has been {(Value ? "enabled" : "disabled")}");
 TraincounterPanel.ToggleShow();
 TraincounterPanel.Instance.UpdateTraincounter();

 if (Value)
 \_button.Highlight();
 else
 \_button.ClearHighlight();
 }

 /// <summary>
 /// Remove localization events, change the tooltip and icon
 /// </summary>
 private void CustomiseButton()
 {
 // Remove existing LocalizeStringEvents
 foreach (LocalizeStringEvent localize in \_button.GetComponentsInChildren<LocalizeStringEvent>())
 Object.Destroy(localize);

 // Change the tooltip
 Tooltip tooltip = \_button.GetComponentInParent<Tooltip>();
 tooltip.TooltipText = "Toggle train counter";
 tooltip.Text = "Toggle train counter";
 tooltip.LocalizedText = new LocalizedString();

 // Change the icon
 TextMeshProUGUI tmpComponent = \_button.GetComponentInChildren<TextMeshProUGUI>();
 tmpComponent.text = "\uF238"; // train font awesome 5 icon
 }
 }
}

```

This bit of code finds the Contracts button, duplicates it and then edits it by replacing the mouseover text and the icon. If you're looking for a specific icon, you can find them here: https://fontawesome.com/v5/icons. Finally, we will change the Traincounter.OnContextChanged method so that it creates a toolbar button instead of immediately showing the panel:

```
public override async Task OnContextChanged(IControllers controller)
{
 \_controller = controller;

 if (controller.CurrentMode == GameMode.Play)
 {
 ToolbarButton.Show();

 controller.EventManager.StartListening(controller.EventManager.TrainSpawned, UpdateTraincounter);
 controller.EventManager.StartListening(controller.EventManager.TrainDisposed, UpdateTraincounter);
 }
 else
 {
 controller.EventManager.StopListening(controller.EventManager.TrainSpawned, UpdateTraincounter);
 controller.EventManager.StopListening(controller.EventManager.TrainDisposed, UpdateTraincounter);
 }

 await Task.Yield();
}

```

**Congratulations, you made your first Unity UI**!

## Notes

* I didn't explain the layout components, I feel like others can do that much better than I can. There's also the official Unity Scripting Reference
* I didn't really go into detail on how the information flow between the Unity assembly and the main assembly works. The tl;dr is that this uses events to make sure things happen at the right moment, and there are other possible ways of doing it. This excellent Kerbal Space Program thread by DMagic uses interfaces (and also explains more Unity things than I do, it's definitely worth a read)
* Full code may or may not be uploaded to a tutorials repository at some point. Speaking of which, if you want to add a UI to your mod's git repo you only have to copy (or link) the Assets folder to your repo folder. Don't forget the .meta files, they look useless but are actually necessary. The file structure will look like my heatmap and logviewer mod repos if you do this, I think it's neat
* There's actually a bug in this example: if you leave the level with the panel still open and start any level again, the toolbar button highlight and UI are out of phase. I could fix it of course (and it's not even difficult), but letting you think about that has more educational value :)
* I would make the panel a bit less high, but I don't want to redo all the screenshots
* Setting all dropdown options in Unity and then using the same strings in the Unity assembly is not the most safe way of doing it, you might want to consider setting the `options` field to the names of the enum options in the Awake method instead (this is what I do in Heatmap)