You’d like visual debug information in your 3D game: a way to see vectors representing velocity, position, etc.
Debug drawing in 2D is quite convenient.
CanvasItem provides a range of primitive drawing methods to use in the
_draw() callback. In 3D, things are not quite so simple. One solution is to use
ImmediateGeometry to manually create meshes, but this is very cumbersome and inconvenient for quick debugging.
A better solution is to stick with the
CanvasItem draw methods. To do so, we need to project the positions in 3D space onto the 2D viewport. Fortunately,
Camera can do this for us using its
For the display layer, add a
CanvasLayer containing a
Control to your 3D scene and add a script to the
As an example, let’s assume this drawing control has a reference to the player node, and we want to draw the node’s
velocity vector. We also have a reference to the
Camera node. More about how we’ll add those references later.
var player var camera func _draw(): var color = Color(0, 1, 0) var start = camera.unproject_position(player.global_transform.origin) var end = camera.unproject_position(player.global_transform.origin + player.velocity) node.draw_line(start, end, color, width) node.draw_triangle(end, start.direction_to(end), width*2, color) func draw_triangle(pos, dir, size, color): var a = pos + dir * size var b = pos + dir.rotated(2*PI/3) * size var c = pos + dir.rotated(4*PI/3) * size var points = PoolVector2Array([a, b, c]) draw_polygon(points, PoolColorArray([color]))
unproject_position() to find the start and end points of the vector we want to draw.
draw_triangle() is there to give us a nice pointed arrow appearance.
Now let’s make this more functional. Your game might have many objects you want to draw debug vectors for. An enemy’s facing direction, an acceleration vector, a destination, etc. We need an easy way to register any object to the debug drawing layer.
DebugOverlay as an autoload and set it as a singleton. This way we can access it from any node. Add this script to it:
extends CanvasLayer onready var draw = $DebugDraw3D func _ready(): if not InputMap.has_action("toggle_debug"): InputMap.add_action("toggle_debug") var ev = InputEventKey.new() ev.scancode = KEY_BACKSLASH InputMap.action_add_event("toggle_debug", ev) func _input(event): if event.is_action_pressed("toggle_debug"): for n in get_children(): n.visible = not n.visible
I’ve included the code to add an input action to toggle visibility. This makes it convenient to drop this into any project without needing to edit the Input Map. We can now reference the drawing layer with
You can add other debug layers here too. For example, one that displays properties as text.
We’ll start by defining a custom object to hold all the information for the debug value we want to display.
extends Control class Vector: var object # The node to follow var property # The property to draw var scale # Scale factor var width # Line width var color # Draw color func _init(_object, _property, _scale, _width, _color): object = _object value = _property scale = _scale width = _width color = _color func draw(node, camera): var start = camera.unproject_position(object.global_transform.origin) var end = camera.unproject_position(object.global_transform.origin + object.get(property) * scale) node.draw_line(start, end, color, width) node.draw_triangle(end, start.direction_to(end), width*2, color) var vectors =  # Array to hold all registered values.
This object encapsulates all the functionality for each vector we want to display, including the drawing code we saw earlier. In
_process(), we can then draw them, making sure to get the current active camera:
func _process(delta): if not visible: return update() func _draw(): var camera = get_viewport().get_camera() for vector in vectors: vector.draw(self, camera)
And finally, we can add a function to register a new vector to follow:
func add_vector(object, property, scale, width, color): vectors.append(Vector.new(object, property, scale, width, color))
Now any object in the game can add a debug vector with the following:
DebugOverlay.draw.add_vector(self, "velocity", 1, 4, Color(0,1,0, 0.5))
Here’s an example of an AI car displaying its raycasts and steering direction: