Need help making an inspector plugin for a custom class, I can't find the right keywords

Godot Version

Godot 4.5.1

Question

I’m following this guide from the docs: Inspector plugins — Godot Engine (4.5) documentation in English

I’ve written a PIDController class in C#, and it needs a PIDSettings class to configure it:

PIDController + PIDSettings Script
using Godot;
using System;
using Mf = Godot.Mathf;


namespace OctopusController
{
	/// <summary>
	/// A PID controller measures an input signal, compares it to a desired setting, and modulates an output control signal to alter future input signals.
	/// </summary>
	public class PIDController
	{
		public PIDController (PIDSettings settings)
		{
			Settings = settings;

			Initialised = false;
		}
		
		/// <summary>
		/// The settings for this controller.
		/// </summary>
		public PIDSettings Settings { get; private set; }

		/// <summary>
		/// The input signal this controller received in the previous frame.
		/// </summary>
		public float Input { get; private set; }

		/// <summary>
		/// The control output signal this controller sent in the previous frame.
		/// </summary>
		public float Control { get; private set; }

		/// <summary>
		/// The raw signal deviation measured, used internally.
		/// </summary>
		public float Deviation { get; private set; }

		/// <summary>
		/// The most recent proportional term calculated by this controller.
		/// </summary>
		/// <value></value>
		public float Porportional { get; private set; }

		/// <summary>
		/// The most recent derivative term calculated by this controller.
		/// </summary>
		public float Derivative { get; private set; }

		/// <summary>
		/// The most recent integral term calculated by this controller.
		/// </summary>
		public float Integral { get; private set; }

		public bool Initialised { get; private set; }

		public void UpdateSettings (PIDSettings newSettings)
		{
			Settings = newSettings;

			Initialised = false;
		}

		public void Deinitialise()
		{
			Initialised = false;
		}
		
		/// <summary>
		/// Call this on the first frame to initialise and reset values.
		/// Can also be called to reset the controller on a settings adjustment or if it has been idle for some time.
		/// </summary>
		/// <param name="initialInput">The state of the input signal on resuming, should also be sent to Process() before the next frame.</param>
		public void Reset(float initialInput)
		{
			Input = initialInput;
			Integral = Settings.IPreload;
			Initialised = true;
		}

		/// <summary>
		/// Call this each frame, pass it an input signal and pass the result to the control process.
		/// Must be called every frame, or else the integral and delta terms will be invalid.
		/// If this will not be called for some time, call reset just before resuming.
		/// </summary>
		/// <param name="newInput">The latest input signal.</param>
		/// <returns>A control output signal.</returns>
		public float Process(float newInput)
		{
			if(!Initialised) { Reset( newInput); }
			
			Deviation = Settings.SetSignal - newInput;
			
			Porportional = Deviation * Settings.P;

			if(Settings.ClampI)
			{
				if(Settings.SplitMaxI)
				{
					Integral = Mf.Clamp(Integral + Deviation * Settings.I, -Settings.MaxI, Settings.MaxI) ;
				}
				else
				{
					Integral = Mf.Clamp(Integral + Deviation * Settings.I, Settings.MinI, Settings.MaxI) ;
				}
			}
			else
			{
				Integral += Deviation * Settings.I;
			}
			
			Derivative = (Input - newInput) * Settings.D;

			Input = newInput;

			return Control = (Porportional + Derivative + Integral) * Settings.ControlGain;
		}


		
	}

	/// <summary>
	/// Holds all of the settings for a PIDController.
	/// </summary>
	public partial class PIDSettings : GodotObject
    {
        /// <summary>
        /// Proportional factor.
        /// </summary>
        public float P {set;get;}

        /// <summary>
        /// Integral factor.
        /// </summary>
        public float I {set;get;}

        /// <summary>
        /// Derivative factor.
        /// </summary>
        public float D {set;get;}
        
        /// <summary>
        /// The input signal this controller is attempting to achieve by modulating its control output.
        /// </summary>
        public float SetSignal {set;get;}

        /// <summary>
        /// Whether to clamp the integral term to prevent runaway.
        /// </summary>
        public bool ClampI {set;get;}

        /// <summary>
        /// If true, separate MaxI and MinI values will be used, otherwise MaxI will be treated as an absolute value.
        /// </summary>
        public bool SplitMaxI {set;get;}

        /// <summary>
        /// The maximum size that the integral term is allowed to be, used only if ClampI is true.
        /// </summary>
        public float MaxI {set;get;}

        /// <summary>
        /// The minimum size that the integral term is allowed to me, used only if SplitMaxI is true.
        /// </summary>
        public float MinI {set;get;}

		/// <summary>
		/// Allows the I term to be "preloaded" in anticipation of a given load.  This is useful for a standing controller that typically deals with a set gravity.
		/// </summary>
		public float IPreload {set;get;}

        /// <summary>
        /// How much to multiply the control signal by, default 1.0.
        /// </summary>
        public float ControlGain {get;set;} = 1.0f;
    }
}

I want to be able to adjust the PIDSettings class in the inspector, so I want to write an editor inspector plugin for that class since I’m potentially using this class a lot, and obviously I don’t want to duplicate all 10 exports everywhere I do that, and these classes aren’t node objects, they’re just instantiated in regular code and I’d like to keep it that way if possible.

So that’s why PIDSettings inherits from GodotObject, since that’s the only way I can get the custom inspector to understand it. The editor plugin and editor inspector plugin code looks like this:

Editor Plugin Scripts
#if TOOLS
using Godot;
using System;

namespace OctopusController.EditorPlugins
{
	[Tool]
	public partial class PIDSettingsEditorPlugin : EditorPlugin
	{
		private PIDSettingsEditorInspectorPlugin inspectorPlugin;

		public override void _EnterTree()
		{
			// Initialization of the plugin goes here.
			inspectorPlugin = new PIDSettingsEditorInspectorPlugin();
			AddInspectorPlugin(inspectorPlugin);
		}

		public override void _ExitTree()
		{
			// Clean-up of the plugin goes here.
			RemoveInspectorPlugin(inspectorPlugin);
		}
	}
}

#endif

#if TOOLS
using Godot;
using System;


namespace OctopusController.EditorPlugins
{
    public partial class PIDSettingsEditorInspectorPlugin : EditorInspectorPlugin
    {
        public override bool _CanHandle(GodotObject @object)
        {
            return @object is PIDSettings;
        }

        public override bool _ParseProperty(GodotObject @object, Variant.Type type, string name, PropertyHint hintType, string hintString, PropertyUsageFlags usageFlags, bool wide)
        {
            if(type == Variant.Type.Float)
            {
                // I don't know what code goes here
                return true;
            }
            else if (type == Variant.Type.Bool)
            {
                // and I also don't know what code goes here
                return true;
            }

            return false;
        }
    }
}

#endif

So far I haven’t noticed any problems from making PIDSettings inherit from GodotObject, so hopefully that will remain the case.

I want to handle the floats and the bools in the inspector automatically, and they’re just standard values, so I assume these exporters exist already, I just don’t know how to access them in the API.

Does anyone know how to make this work, or if I’m going about it the right way?

Why not use a resource class to store settings. You’ll be able to edit that in the inspector without inspector plugins.

1 Like

Thanks so much, that was it. I had attempted that earlier but it spooked me, because I had incorrectly assigned the script itself to the resouce field in the inspector, which then, without alerting me, wiped the script empty, which I found a bit rude at the time, personally.

But I’ve spent a bit more time with this guide: Resources — Godot Engine (4.5) documentation in English

And now the script looks like this:

PIDSettings script
using Godot;
using System;

namespace OctopusController
{
    
    /// <summary>
	/// Holds all of the settings for a PIDController.
	/// </summary>
    [GlobalClass]
    public partial class PIDSettings : Resource
    {	
		
        public PIDSettings() {}
        
        /// <summary>
        /// Proportional factor.
        /// </summary>
        [Export]
        public float P {set;get;}

        /// <summary>
        /// Integral factor.
        /// </summary>
        [Export]
        public float I {set;get;}

        /// <summary>
        /// Derivative factor.
        /// </summary>
        [Export]
        public float D {set;get;}
        
        /// <summary>
        /// The input signal this controller is attempting to achieve by modulating its control output.
        /// </summary>
        [Export]
        public float SetSignal {set;get;}

        /// <summary>
        /// Whether to clamp the integral term to prevent runaway.
        /// </summary>
        [Export]
        public bool ClampI {set;get;}

        /// <summary>
        /// If true, separate MaxI and MinI values will be used, otherwise MaxI will be treated as an absolute value.
        /// </summary>
        [Export]
        public bool SplitMaxI {set;get;}

        /// <summary>
        /// The maximum size that the integral term is allowed to be, used only if ClampI is true.
        /// </summary>
        [Export]
        public float MaxI {set;get;}

        /// <summary>
        /// The minimum size that the integral term is allowed to me, used only if SplitMaxI is true.
        /// </summary>
        [Export]
        public float MinI {set;get;}

		/// <summary>
		/// Allows the I term to be "preloaded" in anticipation of a given load.  This is useful for a standing controller that typically deals with a set gravity.
		/// </summary>
        [Export]
		public float IPreload {set;get;}

        /// <summary>
        /// How much to multiply the control signal by, default 1.0.
        /// </summary>
        [Export]
        public float ControlGain {get;set;} = 1.0f;
    }
}

The key thing that I missed from this the first time is that the resource class absolutely needs to have the attribute [GlobalClass] above it. Once that’s there, I can go to the filesystem part of the editor, right click and go to Create New → Resource and then I can find PIDSettings in that list, and that’s the thing I should drag into the inspector to assign it to the script’s exported property.

Thanks again, I just needed that nudge in the right direction to find this. This is going to make my workflow so much easier, I just discovered I need copies of this resource for lots of different things, so this will make a big difference.