In this mod tutorial, we will cover the basics of customizing and working with the game's inspector panels. This tutorial assumes you have completed the Getting Started tutorial and you are familiar with the basics of creating mods in Juno: New Origins. This tutorial was last updated with version 1.0.9 and Unity 2021.3.19f1.

Inspector panels are specific user interface windows in the game that are generated and updated via scripts. These include the flight view, map view, part inspector and performance analyzer windows. This tutorial will cover how to add entries to the flight view window.


Getting Started

First we create a new mod project. We will name this mod "Inspector Panels Tutorial". See the Getting Started tutorial for more info on this. At this point, the mod should be in a state that it can be built and loaded into the game, even though it does nothing yet.


Customizing the Flight View Window

To demonstrate how the inspector panels can be customized and used, we are going to create a new section in the flight view window that can adjust ambient light settings without pausing the game and jumping into the game's settings dialog. The game allows scripts to receive a callback when an inspector panel is being built. Inside this callback, a script can modify the set of items displayed in the panel. We are going to start by registering a callback that triggers when the flight view panel is built. Lets do this in the OnModInitialized method of our Mod.cs script.

protected override void OnModInitialized()
{
    base.OnModInitialized();

    Game.Instance.UserInterface.AddBuildInspectorPanelAction(
        ModApi.Ui.Inspector.InspectorIds.FlightView,
        OnBuildFlightViewInspectorPanel);
}

Now we need to implement the callback. This callback takes a BuildInspectorPanelRequest object.

private void OnBuildFlightViewInspectorPanel(BuildInspectorPanelRequest request)
{
}

Next, lets create a new collapsible group to put our new panel entries in.

var g = new GroupModel("Ambient Light");

This group can then be added to the panel by accessing the Model property on the request and calling the AddGroup method.

request.Model.AddGroup(g);

At this point we have a new group but it doesn't include any items. We can add items to the group by calling its Add method. It also includes methods for removing items, changing the collapsed state, and changing its visibility. The type of items that can be added are objects inheriting from the ModApi.Ui.Inspector.ItemModel class. There are numerous ItemModel classes in the ModApi.Ui.Inspector namespace.

We are going to add 3 entries to control ambient light settings. Lets start by grabbing some references to these settings.

var fs = Game.Instance.Settings.Game.Flight;
var atten = fs.AmbientLightAttenuation;
var night = fs.AmbientLightAtNightStrength;
var space = fs.AmbientLightInSpaceStrength;

Now lets create an item model for toggling the ambient light attenuation. We specify the label, the function to retrieve the current value, and an action that fires when the toggle value changes, which in this case simply updates the setting and commits the value.

var attentuationModel = new ToggleModel(
    "Attenuation", () => atten.Value, value => atten.UpdateAndCommit(value));

Finally, we need to add the item model to our new group.

g.Add(attentuationModel);

Next, we will add a slider model for controlling the ambient light at night. We specify the label, the function to get the current value, the on changed function (updates and commits the setting), and the min/max values. We can now add this slider to the group as well.

var nightModel = new SliderModel(
    "Night", () => night.Value, s => night.UpdateAndCommit(s), night.Min, night.Max, false);
g.Add(nightModel);

The last slider is just like the previous one, only this one controls the ambient light in space.

var spaceModel = new SliderModel(
    "Space", () => space.Value, s => space.UpdateAndCommit(s), space.Min, space.Max, false);
g.Add(spaceModel);

We can now save the mod, enable it in the game, and test it out. Go to the flight scene with the mod enabled. The Flight Info window should have a new Ambient Light category. If we are on the launchpad at Droo and fast forward until night time, we can slide the Night slider and toggle the Attenuation slider to see the immediate effect on the game's lighting.

AmbientLightTest.png


Additional Inspector Models

We just demonstrated the toggle button model and the slider model, but there are a few others we have available to work with. We will quickly demonstrate some of them now. This is purely for the sake of example, as these examples will not do anything useful.

First, lets grab some references to the flight scene UI and time manager, which we will use a bit later.

var ui = Game.Instance.FlightScene.FlightSceneUI;
var tm = Game.Instance.FlightScene.TimeManager;

Lets create a new inspector panel group for these example items.

g = new GroupModel("Test Group");
request.Model.AddGroup(g);

The TextModel can be used to display some text.

var textModel = new TextModel(
    "Text", () => Time.realtimeSinceStartup.ToString("F3"));
g.Add(textModel);

The TextButtonModel can display a button. The second parameter is the action that is fired when the button is clicked.

var textButtonModel = new TextButtonModel(
    "Text Button", b => ui.ShowMessage("Text Button Clicked"));
g.Add(textButtonModel);

The ProgresBarModel can display a progress bar.

var progressBarModel = new ProgressBarModel(
    "Progress Bar", () => Time.realtimeSinceStartup % 10f / 10f);
g.Add(progressBarModel);

The LabelButtonModel can display a button with a label in front of it.

var labelButtonModel = new LabelButtonModel(
    "Label", b => ui.ShowMessage("Label Button Clicked"));
labelButtonModel.ButtonLabel = "Button Label";
g.Add(labelButtonModel);

The EnumDropdownModel displays a dropdown with a list of enumeration options. In this example, it lists the fuel transfer modes. A class field is created to hold the selected value. We need to subscribe to the ValueChanged event to update the selected value.

private ModApi.Craft.Parts.FuelTransferMode _enumDropdownSelected; // class field

var enumDropdownModel = new EnumDropdownModel<ModApi.Craft.Parts.FuelTransferMode>(
    "Enum Dropdown", () => this._enumDropdownSelected);
enumDropdownModel.ValueChanged += (newValue, oldValue) => this._enumDropdownSelected = newValue;
g.Add(enumDropdownModel);

The DropdownModel displays a string based collection of items in a dropdown element. We create a couple class level fields to hold the available values and selected value. We specify the label, the function to get the current value, the callback that sets the selected item, and the full list of options.

private string _dropdownModelSelection = "Value 1";                           // class field
private string[] _dropdownModelOptions = { "Value 1", "Value 2", "Value 3" }; // class field

var dropdownModel = new DropdownModel(
    "Dropdown", 
    () => this._dropdownModelSelection, 
    value => this._dropdownModelSelection = value, 
    this._dropdownModelOptions);
g.Add(dropdownModel);

The SpinnerModel provides left/right buttons to cycle through a collection of options. Actions can be set to fire when either of these buttons is clicked. The buttons can also be hidden.

var spinnerModel = new SpinnerModel(() => tm.CurrentMode.Name);
spinnerModel.PrevClicked = s => tm.DecreaseTimeMultiplier();
spinnerModel.NextClicked = s => tm.IncreaseTimeMultiplier();
tm.TimeMultiplierModeChanged += e =>
{
    spinnerModel.Update();
    spinnerModel.PrevButtonVisible = tm.CurrentMode != tm.Modes.First();
    spinnerModel.NextButtonVisible = tm.CurrentMode != tm.Modes.Last();
};
g.Add(spinnerModel);

Its worth noting that you can also toggle the visibility of individual items and groups.

g.Add(new TextButtonModel(
    "Toggle ProgressBar", 
    b => progressBarModel.Visible = !progressBarModel.Visible));

Feel free to save the mod and give it a test.

AdditionalModelsTest.png

The final content of our Mod.cs file is below.

namespace Assets.Scripts
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using ModApi;
    using ModApi.Common;
    using ModApi.Mods;
    using ModApi.Ui.Inspector;
    using UnityEngine;

    /// <summary>
    /// A singleton object representing this mod that is instantiated and initialize when the mod is loaded.
    /// </summary>
    public class Mod : ModApi.Mods.GameMod
    {
        /// <summary>
        /// Prevents a default instance of the <see cref="Mod"/> class from being created.
        /// </summary>
        private Mod() : base()
        {
        }

        /// <summary>
        /// Gets the singleton instance of the mod object.
        /// </summary>
        /// <value>The singleton instance of the mod object.</value>
        public static Mod Instance { get; } = GetModInstance<Mod>();

        protected override void OnModInitialized()
        {
            base.OnModInitialized();

            Game.Instance.UserInterface.AddBuildInspectorPanelAction(
                ModApi.Ui.Inspector.InspectorIds.FlightView, 
                OnBuildFlightViewInspectorPanel);
        }

        private void OnBuildFlightViewInspectorPanel(BuildInspectorPanelRequest request)
        {
            var g = new GroupModel("Ambient Light");
            request.Model.AddGroup(g);

            var fs = Game.Instance.Settings.Game.Flight;
            var atten = fs.AmbientLightAttenuation;
            var night = fs.AmbientLightAtNightStrength;
            var space = fs.AmbientLightInSpaceStrength;

            var attenuationModel = new ToggleModel(
                "Attenuation", () => atten.Value, value => atten.UpdateAndCommit(value));
            g.Add(attenuationModel);

            var nightModel = new SliderModel(
                "Night", () => night.Value, s => night.UpdateAndCommit(s), night.Min, night.Max, false);
            g.Add(nightModel);

            var spaceModel = new SliderModel(
                "Space", () => space.Value, s => space.UpdateAndCommit(s), space.Min, space.Max, false);
            g.Add(spaceModel);

            var ui = Game.Instance.FlightScene.FlightSceneUI;
            var tm = Game.Instance.FlightScene.TimeManager;

            g = new GroupModel("Test Group");
            request.Model.AddGroup(g);

            var textModel = new TextModel(
                "Text", () => Time.realtimeSinceStartup.ToString("F3"));
            g.Add(textModel);

            var textButtonModel = new TextButtonModel(
                "Text Button", b => ui.ShowMessage("Text Button Clicked"));
            g.Add(textButtonModel);

            var progressBarModel = new ProgressBarModel(
                "Progress Bar", () => Time.realtimeSinceStartup % 10f / 10f);
            g.Add(progressBarModel);

            var labelButtonModel = new LabelButtonModel(
                "Label", b => ui.ShowMessage("Label Button Clicked"));
            labelButtonModel.ButtonLabel = "Button Label";
            g.Add(labelButtonModel);

            var enumDropdownModel = new EnumDropdownModel<ModApi.Craft.Parts.FuelTransferMode>(
                "Enum Dropdown", () => this._enumDropdownSelected);
            enumDropdownModel.ValueChanged += (newValue, oldValue) => this._enumDropdownSelected = newValue;
            g.Add(enumDropdownModel);

            var dropdownModel = new DropdownModel(
                "Dropdown",
                () => this._dropdownModelSelection,
                value => this._dropdownModelSelection = value,
                this._dropdownModelOptions);
            g.Add(dropdownModel);

            var spinnerModel = new SpinnerModel(() => tm.CurrentMode.Name);
            spinnerModel.PrevClicked = s => tm.DecreaseTimeMultiplier();
            spinnerModel.NextClicked= s => tm.IncreaseTimeMultiplier();
            tm.TimeMultiplierModeChanged += e =>
            {
                spinnerModel.Update();
                spinnerModel.PrevButtonVisible = tm.CurrentMode != tm.Modes.First();
                spinnerModel.NextButtonVisible = tm.CurrentMode != tm.Modes.Last();
            };
            g.Add(spinnerModel);

            g.Add(new TextButtonModel(
                "Toggle ProgressBar",
                b => progressBarModel.Visible = !progressBarModel.Visible));
        }

        private string _dropdownModelSelection = "Value 1";
        private string[] _dropdownModelOptions = { "Value 1", "Value 2", "Value 3" };

        private ModApi.Craft.Parts.FuelTransferMode _enumDropdownSelected;
    }
}


Wrapping up

That should cover the basics of how to work with inspector panels in JNO mod projects. Let me know if you have any questions. The full Unity project for this tutorial can be found here.


13 Comments

  • Log in to leave a comment
  • Profile image

    Can anyone please tell me how to add another button next to this ToggleMenu button (in Flight/ViewPanel.xml)? I tried everything, but the button doesn't seem to appear.

    <ContentButton name="ToggleMenu" class="view-button audio-btn-click" tooltip="Open the menu" onClick="OnToggleMenuButtonClicked();">

    I found the solution:
    Because ModApi.Ui didn't declare the constant ViewPanel (which contains the Menu button in the flight view), let's extend it and declare:

    using ModApi.Ui;
    public static class ExtendedUserInterfaceIds
    {
        public static class Flight
        {
                   public const string ViewPanel = "Ui/Xml/Flight/ViewPanel";
        }
    }

    Then initialize it:

    public static void Initialize()
     {
         var userInterface = Game.Instance.UserInterface;
         userInterface.AddBuildUserInterfaceXmlAction(ExtendedUserInterfaceIds.Flight.ViewPanel, OnBuildTimePanel);
     }

    Now we can modify the Flight/ViewPanel UI.

    4 months ago
  • Profile image
    Dev Pedro

    @SatelliteTorifune untrue

    8 months ago
  • Profile image

    @HaoHanxinghe

    NEIN

    8 months ago
  • Profile image

    能不能让手机关闭UI也能控制火箭!

    1.4 years ago
  • Profile image

    @TiagoFINO

    No

    +1 4.6 years ago
  • Profile image

    Is there some way to create a mod for mobile?

    4.6 years ago
  • Profile image

    @NathanMikeska others。So. Is it impossible to use mod on mobile phones in the future? Doesn't your team have this plan?thanks

    5.2 years ago
  • Profile image

    @NathanMikeska ok。thanks your answer

    5.2 years ago
  • Profile image

    @GongJiTianZhuZhuYiLianMengDang No mod support on mobile unfortunately, sorry.

    5.2 years ago
  • Profile image

    Hello. Excuse me. Can I use mod on my mobile phone or make mod on my own when the future mobile version comes online?

    5.2 years ago
  • Profile image

    @Hypr Those are not magic strings, its just part of the API for the game. Stuff like altitude data can be found on the FlightData class of the player's CraftScript. For example...

    Game.Instance.FlightScene.CraftNode.CraftScript.FlightData.AltitudeAboveSeaLevel

    +1 5.3 years ago
  • Profile image
    0 Hypr

    Hi I see you use Game.Instance for tons of stuff like you did here for ambient light attenuation, for this you used Game.Instance.Settings.Game.Flight.AmbientLightAttenuation I saw similar strings as options instead of sliders in game but, how would I find different ones like the ships altitude for instance? Is there a text file similar to StockUserInterfaceResourceManifest.txt for stuff like this? Or am I missing something?

    edit: if you ignored it, please "update" to .net 4.7.1 framework, now at least I have auto fill in visual studio, nice to know something exists before you compile it :/

    5.3 years ago
  • Profile image
    Dev Pedro

    This tutorial posts are a confirmation of the necessity of moving the upvote button to the top hahahaha

    +2 5.3 years ago

18 Upvotes

Log in in to upvote this post.