Posted May 28, 2023 by CookieBadger
#C# #Godot #plugin
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.
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:
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; }
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:
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.
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.
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!
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 all. Even 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:
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! :)