class_name Debugger extends CanvasLayer ## Handles displaying debug info. signal mode_changed(mode: Mode) enum Mode { DISABLED, PERFORMANCE, TEXT, FULL } const LINE_WIDTH: float = 2 const MARKER_RADIUS: float = 0.2 const CIRCLE_RADIUS: float = 3 const DEFAULT_COLOR: Color = Color.RED static var mode: Mode = Mode.PERFORMANCE static var instance: Debugger @export_group("References") @export var _control: Control @export var _label1: RichTextLabel @export var _label2: RichTextLabel @export var _label3: RichTextLabel @export var _label4: RichTextLabel var _vectors_to_draw: Dictionary = {} var _lines_to_draw: Dictionary = {} var _markers_to_draw: Dictionary = {} var _circles_to_draw: Dictionary = {} var _text_to_draw: Dictionary = {} var _events_to_draw: Dictionary = {} var _label1_text: String = "" var _label2_text: String = "" var _label3_text: String = "" func _ready() -> void: assert(_control, str(self) + ": _control missing!") assert(_label1, str(self) + ": _label1 missing!") assert(_label2, str(self) + ": _label2 missing!") assert(_label3, str(self) + ": _label2 missing!") assert(_label4, str(self) + ": _label2 missing!") instance = self _control.draw.connect(_on_control_draw) Inputer.mode_changed.connect(_on_inputer_mode_changed) # enabled = OS.has_feature("editor") _update_visibility() _update_controls_label() mode_changed.emit(mode) func _process(_delta: float) -> void: if mode == Mode.DISABLED: return _control.queue_redraw() func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("toggle_debug"): mode = wrapi(mode + 1, 0, Mode.size()) as Mode _update_visibility() mode_changed.emit(mode) static func show_debug() -> bool: return mode == Mode.FULL or mode == Mode.TEXT static func text(key: String, value: Variant, label_index: int = 1) -> void: if not show_debug(): return instance._text_to_draw[key] = {"value": value, "label_index": label_index} static func add_event(key: String) -> void: instance._events_to_draw[key] = {"frame": -9999, "args": []} static func event_emitted(key: String, args: Array[Variant] = []) -> void: if not show_debug(): return instance._events_to_draw[key] = {"frame": Engine.get_physics_frames(), "args": args} static func vector( key: String, from: Vector3, to: Vector3, color: Color = DEFAULT_COLOR ) -> void: if not show_debug(): return instance._vectors_to_draw[key] = { "from": from, "to": to, "color": color, "on": true } static func line( key: String, from: Vector3, to: Vector3, color: Color = DEFAULT_COLOR ) -> void: if not show_debug(): return instance._lines_to_draw[key] = {"from": from, "to": to, "color": color, "on": true} static func marker( key: String, pos: Vector3, radius: float = MARKER_RADIUS, color: Color = DEFAULT_COLOR ) -> void: if not show_debug(): return instance._markers_to_draw[key] = ({ "pos": pos, "radius": radius, "color": color, "on": true }) static func circle(key: String, pos: Vector3, color: Color = DEFAULT_COLOR) -> void: if not show_debug(): return instance._circles_to_draw[key] = {"pos": pos, "color": color, "on": true} func _update_visibility() -> void: visible = mode != Mode.DISABLED func _unproject(pos: Vector3) -> Vector2: return MainCamera.instance.unproject_position(pos) func _update_controls_label() -> void: var label_text: String = ( "DEBUG CONTROLS\ntoggle debug: %s" % [ Inputer.get_action_prompt("toggle_debug"), ] ) _label4.text = label_text func _append_text(key: String, value: Variant, label_index: int) -> void: var line_text := str(value) if value is int: line_text = (" " if value >= 0 else "") + line_text if (value as int) > 0: line_text = "[color=sky_blue]%s[/color]" % line_text elif (value as int) < 0: line_text = "[color=salmon]%s[/color]" % line_text elif value is float: line_text = (" " if value >= 0 else "") + "%.6f" % value if value > 0: line_text = "[color=sky_blue]%s[/color]" % line_text elif value < 0: line_text = "[color=salmon]%s[/color]" % line_text elif value is bool: if value: line_text = "[color=sky_blue]%s[/color]" % line_text else: line_text = "[color=salmon]%s[/color]" % line_text elif value is Vector3: line_text = ( "(%s, %s, %s)" % [ (" " if value.x >= 0 else "") + ("%.6f" % value.x), (" " if value.y >= 0 else "") + ("%.6f" % value.y), (" " if value.z >= 0 else "") + ("%.6f" % value.z), ] ) elif value is Vector2: line_text = ( "(%s, %s)" % [ (" " if value.x >= 0 else "") + ("%.6f" % value.x), (" " if value.y >= 0 else "") + ("%.6f" % value.y), ] ) line_text = "%s: %s\n" % [key, line_text] if label_index == 2: _label2_text += line_text else: _label1_text += line_text func _append_event(key: String, frame: int, args: Array[Variant]) -> void: var line_text := key if args.size() > 0: line_text += "(%s)" % ", ".join(args.map(str)) var physics_frame := Engine.get_physics_frames() var color := Color.SALMON.lerp( Color.WHITE, clampf(float(physics_frame - frame) / 30.0, 0, 1) ) if physics_frame - frame < 5: color = Color.SKY_BLUE line_text = "[color=#%s]%s[/color]\n" % [color.to_html(), line_text] _label3_text += line_text func _draw_vector(from: Vector3, to: Vector3, color: Color) -> void: if ( not MainCamera.instance.is_position_in_frustum(from) and not MainCamera.instance.is_position_in_frustum(to) ): return var start := _unproject(from) var end := _unproject(to) if (start - end).length() > 0: _control.draw_line(start, end, color, LINE_WIDTH) if get_viewport().get_visible_rect().has_point(end): _draw_triangle(end, start.direction_to(end), 5, color) func _draw_line(from: Vector3, to: Vector3, color: Color) -> void: if ( not MainCamera.instance.is_position_in_frustum(from) and not MainCamera.instance.is_position_in_frustum(to) ): return var start := _unproject(from) var end := _unproject(to) if (start - end).length() > 0: _control.draw_line(start, end, color, LINE_WIDTH) func _draw_triangle( pos: Vector2, dir: Vector2, size: float, color: Color, ) -> void: 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 := PackedVector2Array([a, b, c]) _control.draw_polygon(points, PackedColorArray([color])) func _draw_marker(pos: Vector3, radius: float, color: Color) -> void: if not MainCamera.instance.is_position_in_frustum(pos): return var x_start := _unproject(pos + (Vector3.LEFT * radius)) var x_end := _unproject(pos + (Vector3.RIGHT * radius)) _control.draw_line(x_start, x_end, color, LINE_WIDTH) var y_start := _unproject(pos + (Vector3.UP * radius)) var y_end := _unproject(pos + (Vector3.DOWN * radius)) _control.draw_line(y_start, y_end, color, LINE_WIDTH) var z_start := _unproject(pos + (Vector3.FORWARD * radius)) var z_end := _unproject(pos + (Vector3.BACK * radius)) _control.draw_line(z_start, z_end, color, LINE_WIDTH) func _draw_circle(pos: Vector3, color: Color) -> void: if not MainCamera.instance.is_position_in_frustum(pos): return var point := _unproject(pos) _control.draw_circle(point, CIRCLE_RADIUS, color) func _set_label_texts() -> void: _label1.text = _label1_text _label2.text = _label2_text _label3.text = _label3_text func _on_control_draw() -> void: if mode != Mode.DISABLED: _label1_text = "" _label2_text = "" _label3_text = "" _append_text("fps", Engine.get_frames_per_second() as int, 0) _append_text("gpu", RenderingServer.get_video_adapter_name(), 0) _append_text( "resolution", Vector2i(get_viewport().size * get_viewport().scaling_3d_scale as Vector2), 0 ) if mode == Mode.PERFORMANCE: _set_label_texts() return if mode == Mode.TEXT or mode == Mode.FULL: for k: String in _text_to_draw.keys(): var v: Dictionary = _text_to_draw[k] _append_text(k, v["value"] as Variant, v["label_index"] as int) for k: String in _events_to_draw.keys(): var v: Dictionary = _events_to_draw[k] _append_event(k, v["frame"] as int, v["args"] as Array[Variant]) _set_label_texts() if !MainCamera.instance or mode != Mode.FULL: return for v: Dictionary in _vectors_to_draw.values(): if v["on"]: _draw_vector( v["from"] as Vector3, v["to"] as Vector3, v["color"] as Color, ) # v["on"] = false for v: Dictionary in _lines_to_draw.values(): if v["on"]: _draw_line( v["from"] as Vector3, v["to"] as Vector3, v["color"] as Color, ) # v["on"] = false for v: Dictionary in _markers_to_draw.values(): if v["on"]: _draw_marker( v["pos"] as Vector3, v["radius"] as float, v["color"] as Color, ) # v["on"] = false for v: Dictionary in _circles_to_draw.values(): if v["on"]: _draw_circle(v["pos"] as Vector3, v["color"] as Color) # v["on"] = false func _on_inputer_mode_changed(_input_mode: Inputer.Mode) -> void: _update_controls_label()