Godot Plugins - What nobody tells you


This Devlog is about the secrets of making GodotEngine plugins, that I used for the AssetPlacer. Read my first devlog, if you are interested in how the AssetPlacer came to be, and a gentle introduction. This devlog gives you more advanced information about the secrets behind my plugin, none of which are documented anywhere! I even have a small gift for you at the end.

Context-Free Godot Plugins in depth

In my first devlog, I described what a Context-Free plugin is. In short, most Godot plugins are used for modifying specific Node or Resource types. Context-Free plugins work regardless of what resource or node you currently select. There are 2 main problems when making Context-Free plugins:

  •  _forward_canvas_gui_input() and forward_3d_gui_input() don't work
  •  _forward_canvas_force_draw_over_viewport() and _forward_3d_draw_over_viewport() don't work

Viewport workarounds

This might or might not be new to you, but you can technically get a reference to any part of the Godot Editor, the same way you would get it in a game. The Godot Editor is a Godot game, in the sense that it is entirely composed of  Godot nodes. Hence, if you know the path to some node, you can get a reference, move it, remove it, add child nodes, etc. Thus, the problem that you cannot get the viewport by a built-in method of EditorPlugin can be worked around by getting all the viewports the hard way. In 4.0, you can do it like this:

private IEnumerable<SubViewport> Get3DViewports()
{
    var mainScreen = GetEditorInterface().GetEditorMainScreen();
    
    // MainScreen -> Node3DEditor -> HSplitContainer -> HSplitContainer -> VSplitContainer -> Node3DEditorViewportContainer
    
    var viewportContainer = mainScreen.GetChild(1).GetChild(1).GetChild(0).GetChild(0).GetChild(0);
    var node3DEditorViewports = viewportContainer.GetChildren();
    
    // Node3DEditorViewport -> SubViewportContainer -> SubViewport
    
    var viewports = node3DEditorViewports.Select((vp) => vp.GetChild(0).GetChild(0) as SubViewport);
    
    return viewports;
}

Getting the viewport camera

Once you get the 3D viewport, you can also receive a reference to the viewport camera by viewport.get_camera_3d(). However, even this comes with a problem: when you select a camera in the Editor, you can click a checkbox, that the viewport should show a preview of that camera. And guess what, in this state, the viewport.get_camera_3d() method then returns the camera as it was before you clicked that checkbox. This led to a weird bug in my program that the placement would not follow the mouse properly until I found out and disabled placement in the preview mode:

Camera Preview Bug

The approach of getting viewport and cameras through their node paths work, as long as the Editor structure does not change. However, once an update just slightly changes them, the code breaks. Hence, I do not endorse doing it this way, but since there is no other way at getting the viewport at the moment, you might as well use the workaround.

By the way, you can use the same technique to read UI information, press buttons from code, or detect or fake input. I'll explain how to read input from the viewport in the next section.

Detecting Viewport Input

You might think, that you can use the viewport we retrieved for input checking. However, since the input is not actually sent to the viewport node itself, but rather to a control node exactly on top of it, we read input this way:

public override void _UnhandledInput(InputEvent @event)
{
    if (!Engine.IsEditorHint()) return;
    var viewport = GetFocused3DViewport();
    if (viewport != null)
    {
        var stopInput = _Forward3DViewportUnhandledInput(viewport, @event);
        if (stopInput)
        {
            GetTree().Root.SetInputAsHandled();
        }
    }
}
protected virtual bool _Forward3DViewportUnhandledInput(Viewport vp, InputEvent e)
{
    return false;
}
protected SubViewport GetFocused3DViewport()
{
    var viewports = Get3DViewports();
    foreach(SubViewport vp in viewports)
    {
        if (IsEditorViewportFocused(vp)) return vp;
    }
    return null;
}
public static bool IsEditorViewportFocused(Viewport viewport)
{
    var editorVp = viewport.GetParent().GetParent<Control>();
    var vpControl = editorVp.GetChild(1) as Control;
    return editorVp.Visible && (vpControl?.HasFocus() is true);
}

You can then override the _Forward3DViewportUnhandledInput() method in your plugin, the same way that you would override the _forward_3d_gui_input() method. The Get3DViewports() method was explained earlier.

Drawing over the viewport

The asset placer uses tooltips to display some extra information. EditorPlugin would have a built-in method to do so, but as we found out, it doesn't work for Context-free plugins.

The way we can now circumvent this, is by adding a control node ourselves and calling draw methods on it. Here's how you can do that:

public override void _EnterTree()
{
    if (!Engine.IsEditorHint()) return;
    drawPanel = new EditorDrawPanel();
    GetEditorInterface().GetBaseControl().AddChild(drawPanel);
}
public sealed override void _ExitTree()
{
    if (!Engine.IsEditorHint()) return;
    drawPanel?.QueueFree();
    _Cleanup();
}
public override void _Process(double delta)
{
    if (!Engine.IsEditorHint()) return;
    drawPanel.QueueRedraw();
}
public partial class EditorDrawPanel : Control
{
    public override void _EnterTree()
    {
        MouseFilter = MouseFilterEnum.Ignore;
    }
    public override void _Process(double delta)
    {
        if (!Engine.IsEditorHint()) return;
        if (GetParent() is Control control)
        {
            Size = control.Size;
        }
    }
    public override void _Draw() {
        // draw tooltips and other stuff here
    }
}

You could technically add the control node wherever in the scene tree you need it, but I just added it on top of the base control, so I can draw tooltips anywhere.

Tip: EditorDrawPanel can now be used to display debug information as well, instead of printing to the console output. Very handy!

Plugins in C#

Finally, I might mention my struggles due to the plugin being written in C#. I previously almost only worked in C# and not in GDScript, and also found large architectures way easier to handle with a conventional strong-typed language and a powerful IDE, rather than the weak-typed GDScript. I had many learnings on the way, of how to do this properly and what you should avoid. If you are using C#, this information might be extremely valuable to you. But also mind, that if you too decide to make a large plugin with a wide userbase in C#, some people will hate you for not using GDScript.

First of all, C# is a compiled rather than interpreted language. Hence, every time you make changes, you need to press the build button. This is a minor inconvenience compared to GDScript, but note that every time you recompile, Godot serializes the state of all GodotObjects of all enabled plugins, removes their scripts, reattaches the (possibly newly compiled) scripts, and deserializes the information. However, not all information is serialized correctly, and some information is not serialized at allEven worse, non-GodotObject objects don't get serialized at all, and just become null. So simply put, every time you press build,  the state of your variables might get lost. This is especially annoying, since it leads to NullpoinerExceptions, and ObjectDisposedExceptions, that only resolve once you restart the editor. Because of this behavior, my workspace frequently looked like this:

I partially lost my sanity over this stuff (if you don't believe me, ask my flatmates) and created several issues about this behavior, but if you want to keep your sanity intact, don't hope for a fix of these, but rather follow these recommendations:

  1. All classes should extend GodotObject and be marked as partial
    Anything that should be persisted when you recompile should be a descendent of GodotObject. For serialization to work, you also have to mark it as partial, even though the compilation does not require you to! Use plain C# objects only if you need them just within a method. Better just don't.
  2. Don't use C# collections as fields - all fields should be Variant types
    C# collections don't get serialized. When you rebuild, they are empty. I created an issue about this, but I'd guess it won't be fixed soon. Actually, I think nothing that does not fit in a Godot Variant is serialized. So all your fields should be assignable to a variant (references to GodotObjects, Godot collections, or primitive types are fine). Also, if you use Godot Collections, make sure that their generic type is compatible with Variant as well!
  3. Don't make fields readonly
    Even though the IDE might really want you to, serialization will not work if you have a field marked as readonly.
  4. Don't, and I repeat, don't ever use C# event Actions
    They lead to really helpful single-line error messages like 'modules/mono/managed_callable.cpp:92 - Condition "delegate_handle.value == nullptr" is true'. You'll never know where they come from. Use signals. They are annoying to work with, but they serialize without issues
  5. Avoid bound variables in anonymous functions
    They don't get reconnected properly when you recompile. This is freaking annoying since it also gives you useless error messages if you ignore it, and workarounds usually require you to make a new class. I really hope that this gets fixed, since it's an issue both in C# and also in GDScript. If using data in an anonymous function can’t be avoided, try to use constant bindings instead of captured variables. If it needs to be dynamic, encapsulate the signal, by giving the thrower a script, that has another signal with the bound parameters. If you can’t do that either… well… things will get difficult. Here's the issue, if you think you can fix this, please please give it a try.
  6. Make sure you always QueueFree() your nodes
    If you add some node when your plugin gets enabled, make sure you free it when the plugin gets disabled or exits the tree. This holds for GDScript plugins as well. If you don't you might get annoying errors that only go away when you restart the editor.

If you follow these steps, you should avoid a great deal of trauma. Note that Godot and C# IDEs sometimes work against each other. Not everything the IDE says is gold, so don't just convert stuff to anonymous functions or events as the IDE tells you to.

Finally, as promised, here is my gift: The ContextlessPlugin C# source files, which you can use and extend as you wish. It provides these useful methods to detect input and use viewports in plugins, regardless of context, that I described here. 

I hope that it helps you in your projects and that this devlog gave you some valuable information, even though it was probably really really boring. Next devlog will be more digestible, promise. If you liked it  nevertheless, let me know with a comment or by pressing the like button on the top of the page!   :)

Get AssetPlacer

Buy Now$17.99 USD or more

Comments

Log in with itch.io to leave a comment.

I've been using gdscript for a while and I don't hate the language it would have been nice to use some of my OO experience in Java towards something more familiar especially around object typing and use c#.  Sadly it just feels really clunky and can get into a state, even in simple projects where godot an vscode/compiler are fighting each other.  I've had 2 tiny projects go bad where breakpoints wouldn't be hit, where the game running didn't seem to have the code changes and the extremely cryptic compile/runtime crashes in vscode were of no help on how to fix the problem.  Shame really but I suspect it's back to gdscript for me.  I just don't want to fight with not "trusting" the build environment and would rather enjoy writing code that I'm confident will work.  (or at least if it doesn't work I can trust that it was my shitty code not because the dev tools hate each other)

I’ve also run into a lot of issues with C# Godot addons. I’ve found the mono build of Godot to be much more prone to random crashes even when no mono scripts exist in the project. Because of that I’ve moved away from using the mono version entirely.

Are there any plans to create a GDScript equivalent of the addon for those not using the mono build?

Random crashes with no scripts? I think I only ever experienced that in the alpha/beta builds, so I really hope they are fixed by now, or that remaining issues get fixed.


As to the GDScript equivalent, no plans exist right now, since I simply don't have the capacity to do so, maybe someday in the future I will though.

(+2)

Thank you cookie for your service, your experience of being a pioneer and spreading around of what you have learned made this an interesting read.