-->
Debugging is a really fun activity, isn't it? Often we require some extra tools to aid in this process. Maybe by dumping a bunch of text into the screen or drawing some extra lines. This addon is meant to bring some little scripts to help with those.
Activating the "Debug Helpers" addon will automatically add the scripts into the auto-load list. The default name to reference them will be mentioned in the specific "sub-addon" topics. Nevertheless, if you prefer you can manually add those scripts to the singleton list instead of activating the plugin.
This script is meant to be part of the auto-load list, meaning that its functionality will be accessible from wherever in the code. Basically it provides a few functions to add/remove text into/from the screen using the Label
Control widget. Those are added into a panel that expands/shrinks according to the contents. This panel is, by default, black with some transparency set just so text is more easily readable. This color (and transparency) can be changed.
By default this script will be named OverlayDebugInfo
and it's the name that will be used throughout the rest of this topic.
Once the script is in the auto-load list, you can then easily use the functionality. Note that by default the panel is set to be shown, which will result in a tiny black square on the top left corner of the screen. From code you can control the visibility of the entire panel using set_visibility()
function, which requires a single parameter, true
to show and false
to hide. So, if you don't want the panel to be visible in you main menu (or anywhere else, actually), you can add this into the _ready()
script:
OverlayDebugInfo.set_visibility(false)
There is also a "shortcut" function that you can use to toggle the visibility. This can be useful to be called from a key press event:
# Toggle the visibility of the overlay debug info panel
OverlayDebugInfo.toggle_visibility()
When you want to add text to the screen, there are two options:
set_label()
: adds a label to the screen, which must be associated with some ID, so you can then manipulate it or even remove.add_timed_label()
: add a label that will remain on screen during some time then will be deleted. Those don't require any ID.Let see the set_label()
first. It requires two arguments, the label ID and the text that will be set into that label. That said, suppose you want a label that will display the position of a character, which changes during the game. Assume we have a variable named character_position
holding this information. Then we can call the function during the _physics_process()
:
OverlayDebugInfo.set_label("char_pos", character_position)
Note that you don't have to first create a label with that name (char_pos
in this example). The script will take care of creating the Label object if it doesn't exist.
Now suppose you want to see some information being shown on the screen for a short amount of time, mostly because the value you want to check doesn't change too frequently (if at all), but you still want to verify if the value is correct. For that, there is the add_timed_label()
function, which also requires two arguments, the text and the amount of seconds the information will remain on screen.
For the next script, suppose you have one plant object that holds a grow_stage
property which isn't updated too frequently. You then want to check this value when you press a key and keep this information for only 2 seconds:
OverlayDebugInfo.add_timed_label("Grow Stage: %s" % grow_stage, 2.0)
With this, the label will be removed from the panel after 2 seconds.
After you have set a "permanent" label you can remove it, with the remove_label()
function. It requires a single argument, the ID of the label. So, going back to the character position example, we can remove that information with this:
OverlayDebugInfo.remove_label("char_pos")
You can clear all non timed labels at once like this:
OverlayDebugInfo.clear()
The position of the panel can be changed, into "9 spots" by setting the vertical and horizontal alignment:
# Put the panel on the left horizontal alignment
OverlayDebugInfo.set_horizontal_align_left()
# Put the panel on the center horizontal alignment
OverlayDebugInfo.set_horizontal_align_center()
# Put the panel on the right horizontal alignment
OverlayDebugInfo.set_horizontal_align_right()
# Put the panel on the top vertical alignment
OverlayDebugInfo.set_vertical_align_top()
# Put the panel on the center vertical alignment
OverlayDebugInfo.set_vertical_align_center()
# Put the panel on the bottom vertical alignment
OverlayDebugInfo.set_vertical_align_bottom()
With those, suppose you want the panel to be placed on the bottom-right corner of the screen. Those two can be called in sequence:
OverlayDebugInfo.set_horizontal_align_right()
OverlayDebugInfo.set_vertical_align_bottom()
Finally, to change the color of the panel, there is the function set_panel_color()
, which requires a single argument, the color itself. Suppose we wanted to change the panel to white with 50% transparency:
OverlayDebugInfo.set_panel_color(Color(1.0, 1.0, 1.0, 0.5))
And that's all for this script!
The first thing to note in this script is the containers. There is the _haligner
(which is one HBoxContainer
) and inside of it the _valigner
(which is one VBoxContainer
). The _haligner
is used to help position the panel horizontally, while the _valigner
is used to position the panel vertically. Check the set_*_align_*()
functions to see exactly which property is changed.
The anchor
property of the _haligner
is set in a way so this container uses the entire available space.
Inside the _valigner
there is the _background
, which is one PanelContainer
widget. When it is created, the self_modulate
property is used to change the panel's color (defaulting to black with some transparency). Also, the size_flags_vertical
is completely cleared as the default one expands the panel to use the entire available height every time a new label is added (which is not exactly the desired behavior).
Lastly, inside the _background
there is yet another VBoxContainer
widget, named _label_box
. This time it is used to place the labels one bellow the other.
Note that all four containers have been configured to ignore the mouse (mouse filter is set to MOUSE_FILTER_IGNORE
). If that is not done (specially on the _valigner
) it becomes impossible to interact with HUD/Widgets when the panel is visible.
There is one script property named _label_node
, which is a dictionary holding the created non-timed labels. When the set_label()
function is called, it first checks if the label exists within this dictionary. If so, only update the text
property, otherwise, create the label, add into the _label_box
container and the dictionary, then set the text
property.
The timed label function (add_timed_label()
) contains a bit more code, but it's not that complicated. Since labels added in this way are not meant to have ID's, it immediately create a new Label
object and add into the _label_box
container. After that a timer is created and setup. The function connected to its timeout
signal receives two payload arguments, the timer itself and the newly created label object.
The _on_timeout()
function is connected to the timeout
Timer signal. As mentioned, it receives, as payload, two arguments. Those are used to perform the necessary cleanup, which is basically call the queue_free()
of both the Timer and the Label nodes.
And that's it! This script is simple, really!
As with the overlayinfo.gd
script, once this one is in the auto-load list, the functionality can be accessed.
By default this script will be named DebugLine3D
and it's the name that will be used throughout the rest of this topic.
To draw a line, there is the add_line()
function, which requires 2 arguments plus one optional. The first two are the global positions of the end points. Perhaps this should have been named add_line_segment()
but, really, this would become really tedious, right? The optional argument specifies the color of the line, which defaults to white. That said, suppose you have two positions stored in Vector3
variables (p1
and p2
) and you want to draw a line connecting those two points using the red color:
DebugLine3D.add_line(p1, p2, Color.red)
Doing so will create a line that will be drawn during a single physics iteration frame. In other words, on the next update the line will be removed. This is done in this way because the idea is to show lines for things that do change frequently. With that in mind, if you want the line connecting those two points to remain more time on screen you either keep calling the add_line()
function as part of the _physics_process()
function or call the add_timed_line()
function.
The add_timed_line()
requires 3 arguments plus the optional color. The first two arguments are the end points of the line segment while the third one is the amount of time (in seconds) the line will remain on screen. So, let's add a green line that will disappear after 2.5 seconds:
DebugLine3D.add_timed_line(p1, p2, Color.green, 2.5)
There is one detail, this function returns a value, which is the "ID" of the created line. This ID can be used to manually remove a timed line. The thing is, if you specify a 0 (or negative) value as the amount of time, the line will remain there and will not be automatically removed. To have more control over when the line is to be removed, you can use remove_timed_line()
, which requires the ID returned from the add_timed_line()
function. So, you can use something like this:
var timed_line: int = DebugLine3D.add_timed_line(p1, p2, some_color, 0)
# ... later, maybe even somewhere in the code, remove the line
DebugLine3D.remove_timed_line(timed_line)
Godot will probably warn you about a return value that was ignored if you used the add_timed_line()
function without using what was returned. It's possible to append a comment to tell Godot you are aware of that and you don't want to be warned, like this:
# warning-ignore:return_value_discarded
DebugLine3D.add_timed_line(p1, p2, some_color, some_time)
It's possible to disable the functionality of the addon (basically its processing) while still being able to access it. The idea here is to not clash too much with the game logic code (having to remove debug code then adding it back) all the time but also allowing the script to do nothing when disabled. Basically, you can the set_enabled()
function, which requires a single boolean argument:
# Disable the line3d processing
DebugLine3D.set_enabled(false)
# Or enable it
DebugLine3D.set_enabled(true)
This can be called somewhere on the main menu script or during initialization of another auto-load script prior to exporting the project. Then any call to add debug lines will basically do nothing. You can check its state by calling is_enabled()
:
if (DebugLine3D.is_enabled()):
# Do something if the functionality is enabled.
And that's it for this sub-addon!
The core of this script is to extend the ImmediateGeometry
. You can ready about it in the documentation. Basically, every time the add_line()
or the add_timed_line()
is called, an instance of the inner class Line3D
is created. This object holds the end points of the line segment as well as the color.
During initialization (_enter_tree()
), a material override is set, otherwise the line color will not work. In this case it's configured to be unshaded (so lighting will not affect the line) and to use the vertex color as albedo (so the specified color works).
In the case of the common line, it is added into the internal _onef_line
array. As for the timed lines, the instance is added into the _timed_line
dictionary. In this case, the entry is keyed by one internal "ID" (which is just an incrementing integer, really) and contains both the Line3D
and a Timer
object.
When the Timer object is created, the _on_timeout()
function is connected to its timeout
signal, giving the line ID and a container as payloads. While writing this I realize that the container payload is not necessary, but was used because I did try to add other drawing shapes. That ended up being too much work for something that is meant to be "quick and dirty" so I abandoned the idea while forgetting to update the code. Nevertheless, within this function the remove_timed_line()
is called, providing the ID of the line.
The remove_timed_line()
function first retrieves a line from the _timed_line
dictionary and, if valid, takes the Timer object, stops it, remove it from the tree and then erase the entry from the dictionary.
Finally, from within the _physics_process()
function, the previous drawing operation is cleared then the begin()
function is used to start the line drawing process. This is done by iterating through the entries in the line containers and adding the vertices using add_vertex()
function (inherited from ImmediateGeometry
) for each of the end points of the line segment. Once that iteration is done, the end()
function is called to end the process of adding vertices (and possibly other properties - please see the above linked documentation). Finally, the _onef_line
array is cleared just so the lines in it are not drawn in the next loop iteration.
And that's it for this script!