mirror of
https://gitlab.com/MaddoScientisto/cirnogodot.git
synced 2026-06-01 10:05:34 +00:00
Debug addon and removed old scaling
This commit is contained in:
parent
ed502eb061
commit
ca85765ca2
16 changed files with 1637 additions and 11 deletions
|
|
@ -1,7 +1,6 @@
|
|||
[gd_scene load_steps=19 format=4 uid="uid://bv451a8wgty4u"]
|
||||
[gd_scene load_steps=18 format=4 uid="uid://bv451a8wgty4u"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bghghp5ep4w2j" path="res://Scenes/player.tscn" id="2_8mh54"]
|
||||
[ext_resource type="PackedScene" uid="uid://cxmcqehjjy82j" path="res://Scenes/reisen.tscn" id="3_8k37m"]
|
||||
[ext_resource type="PackedScene" uid="uid://rp4jhx0tuh24" path="res://Scenes/fragola.tscn" id="4_s7wq6"]
|
||||
[ext_resource type="PackedScene" uid="uid://bj28qiai2x2ar" path="res://Scenes/Barrel.tscn" id="5_3uba3"]
|
||||
[ext_resource type="PackedScene" uid="uid://uaf5r6cd71hu" path="res://Scenes/Furniture/LargeTank.tscn" id="6_nkauc"]
|
||||
|
|
@ -280,6 +279,7 @@ position = Vector2(-766, -74)
|
|||
|
||||
[node name="CameraController" type="Camera2D" parent="."]
|
||||
script = ExtResource("6_t8ide")
|
||||
pixel_snap = false
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("2_8mh54")]
|
||||
position = Vector2(-790, -165)
|
||||
|
|
@ -301,8 +301,5 @@ polygon = PackedVector2Array(95, 57, 46, 58, -83, 61, -91, -37, 88, -37, 114, -1
|
|||
[node name="Fragola" parent="." instance=ExtResource("4_s7wq6")]
|
||||
position = Vector2(-743, -117)
|
||||
|
||||
[node name="CharacterBody2D" parent="." instance=ExtResource("3_8k37m")]
|
||||
position = Vector2(78, -15)
|
||||
|
||||
[node name="Enemy" parent="." instance=ExtResource("18_ixcwn")]
|
||||
position = Vector2(-687, -10)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public partial class Enemy : Area2D, IDestructible
|
|||
|
||||
private Timer _cooldownTimer;
|
||||
|
||||
[DebugGUIPrint]
|
||||
private int _ammo = 0;
|
||||
|
||||
// Called when the node enters the scene tree for the first time.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public partial class MainMenu : Control
|
|||
|
||||
[Export]
|
||||
public string MainMenuScene { get; set; }
|
||||
|
||||
|
||||
// Called when the node enters the scene tree for the first time.
|
||||
public override void _Ready()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible
|
|||
|
||||
[Export] public float Health = 4f;
|
||||
|
||||
[DebugGUIPrint]
|
||||
private float _currentHealth = 0f;
|
||||
|
||||
[Export] public double RateOfFire = 0.4f;
|
||||
|
|
|
|||
33
addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs
Normal file
33
addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using Godot;
|
||||
using System;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class DebugGUIGraphAttribute : Attribute
|
||||
{
|
||||
public float min { get; private set; }
|
||||
public float max { get; private set; }
|
||||
public Color color { get; private set; }
|
||||
public int group { get; private set; }
|
||||
public bool autoScale { get; private set; }
|
||||
|
||||
public DebugGUIGraphAttribute(
|
||||
// Line color
|
||||
float r = 1,
|
||||
float g = 1,
|
||||
float b = 1,
|
||||
// Values at top/bottom of graph
|
||||
float min = 0,
|
||||
float max = 1,
|
||||
// Offset position on screen
|
||||
int group = 0,
|
||||
// Auto-adjust min/max to fit the values
|
||||
bool autoScale = true
|
||||
)
|
||||
{
|
||||
color = new Color(r, g, b, 0.9f);
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.group = group;
|
||||
this.autoScale = autoScale;
|
||||
}
|
||||
}
|
||||
4
addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs
Normal file
4
addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
using System;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class DebugGUIPrintAttribute : Attribute { }
|
||||
313
addons/DebugGUI/DebugGUI.cs
Normal file
313
addons/DebugGUI/DebugGUI.cs
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
using Godot;
|
||||
using System.IO;
|
||||
using WeavUtils;
|
||||
|
||||
public partial class DebugGUI : Control
|
||||
{
|
||||
// Other scripts may use us right off the bat, so we make sure we initialize first
|
||||
public DebugGUI()
|
||||
{
|
||||
ProcessPhysicsPriority = int.MinValue;
|
||||
}
|
||||
|
||||
static DebugGUI Instance;
|
||||
|
||||
#region Settings
|
||||
|
||||
public static class Settings
|
||||
{
|
||||
const string DEBUGGUI_SETTINGS_DIR = "DebugGUI/Settings/";
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
if (!Engine.IsEditorHint()) return;
|
||||
|
||||
// Inits defaults or load current if present
|
||||
Load();
|
||||
|
||||
ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", enableGraphs);
|
||||
ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", enableLogs);
|
||||
|
||||
ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", backgroundColor);
|
||||
ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", scrubberColor);
|
||||
ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", graphWidth);
|
||||
ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", graphHeight);
|
||||
ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", temporaryLogLifetime);
|
||||
|
||||
ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", true);
|
||||
ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", true);
|
||||
|
||||
ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", true);
|
||||
ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", true);
|
||||
ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", true);
|
||||
ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", true);
|
||||
ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", true);
|
||||
|
||||
var err = ProjectSettings.Save();
|
||||
if(err != Error.Ok)
|
||||
{
|
||||
GD.PrintErr(err);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
textFont = ThemeDB.FallbackFont;
|
||||
|
||||
enableGraphs = ProjectSettings.GetSetting(
|
||||
$"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}",
|
||||
true
|
||||
).AsBool();
|
||||
enableLogs = ProjectSettings.GetSetting(
|
||||
$"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}",
|
||||
true
|
||||
).AsBool();
|
||||
|
||||
backgroundColor = ProjectSettings.GetSetting(
|
||||
$"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}",
|
||||
new Color(0f, 0f, 0f, 0.7f)
|
||||
).AsColor();
|
||||
scrubberColor = ProjectSettings.GetSetting(
|
||||
$"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}",
|
||||
new Color(1f, 1f, 0f, 0.7f)
|
||||
).AsColor();
|
||||
graphWidth = ProjectSettings.GetSetting(
|
||||
$"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}",
|
||||
300
|
||||
).AsInt32();
|
||||
graphHeight = ProjectSettings.GetSetting(
|
||||
$"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}",
|
||||
100
|
||||
).AsInt32();
|
||||
temporaryLogLifetime = ProjectSettings.GetSetting(
|
||||
$"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}",
|
||||
5
|
||||
).AsDouble();
|
||||
}
|
||||
|
||||
public static bool enableGraphs;
|
||||
public static bool enableLogs;
|
||||
|
||||
public static Color backgroundColor;
|
||||
public static Color scrubberColor;
|
||||
public static int graphWidth;
|
||||
public static int graphHeight;
|
||||
public static double temporaryLogLifetime;
|
||||
|
||||
public static Font textFont;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Graph
|
||||
|
||||
/// <summary>
|
||||
/// Set the properties of a graph.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
/// <param name="label">The graph's label</param>
|
||||
/// <param name="min">Value at the bottom of the graph box</param>
|
||||
/// <param name="max">Value at the top of the graph box</param>
|
||||
/// <param name="group">The graph's ordinal position on screen</param>
|
||||
/// <param name="color">The graph's color</param>
|
||||
public static void SetGraphProperties(object key, string label, float min, float max, int group, Color color, bool autoScale)
|
||||
{
|
||||
if (Settings.enableGraphs)
|
||||
Instance?.graphWindow.SetGraphProperties(key, label, min, max, group, color, autoScale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the properties of a graph.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
/// <param name="label">The graph's label</param>
|
||||
/// <param name="min">Value at the bottom of the graph box</param>
|
||||
/// <param name="max">Value at the top of the graph box</param>
|
||||
/// <param name="group">The graph's ordinal position on screen</param>
|
||||
/// <param name="color">The graph's color</param>
|
||||
public static void SetGraphProperties(GodotObject key, string label, float min, float max, int group, Color color, bool autoScale)
|
||||
{
|
||||
SetGraphProperties((object)key, label, min, max, group, color, autoScale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a data point to a graph.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
/// <param name="val">Value to be added</param>
|
||||
public static void Graph(object key, float val)
|
||||
{
|
||||
if (Settings.enableGraphs)
|
||||
Instance?.graphWindow.Graph(key, val);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a data point to a graph.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
/// <param name="val">Value to be added</param>
|
||||
public static void Graph(GodotObject key, float val)
|
||||
{
|
||||
Graph((object)key, val);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an existing graph.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
public static void RemoveGraph(object key)
|
||||
{
|
||||
if (Settings.enableGraphs)
|
||||
Instance?.graphWindow.RemoveGraph(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an existing graph.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
public static void RemoveGraph(GodotObject key)
|
||||
{
|
||||
RemoveGraph((object)key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets a graph's data.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
public static void ClearGraph(object key)
|
||||
{
|
||||
if (Settings.enableGraphs)
|
||||
Instance?.graphWindow.ClearGraph(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets a graph's data.
|
||||
/// </summary>
|
||||
/// <param name="key">The graph's key</param>
|
||||
public static void ClearGraph(GodotObject key)
|
||||
{
|
||||
ClearGraph((object)key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export graphs to a json file. See path in log.
|
||||
/// </summary>
|
||||
public static void ExportGraphs()
|
||||
{
|
||||
if (Instance == null || !Settings.enableGraphs)
|
||||
return;
|
||||
|
||||
string dateTimeStr = Time.GetDatetimeStringFromSystem().Replace(':', '-');
|
||||
string filename = $"debuggui_graph_export_{dateTimeStr}.json";
|
||||
|
||||
using var file = Godot.FileAccess.Open(
|
||||
"user://" + filename,
|
||||
Godot.FileAccess.ModeFlags.Write
|
||||
);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
GD.Print("DebugGUI graph export failed: " + Godot.FileAccess.GetOpenError());
|
||||
}
|
||||
else
|
||||
{
|
||||
file.StoreString(Instance.graphWindow.ToJson());
|
||||
GD.Print($"Wrote graph data to {Path.Combine(OS.GetUserDataDir(), filename)}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Log
|
||||
|
||||
/// <summary>
|
||||
/// Create or update an existing message with the same key.
|
||||
/// </summary>
|
||||
public static void LogPersistent(object key, string message)
|
||||
{
|
||||
if (Settings.enableLogs)
|
||||
Instance?.logWindow.LogPersistent(key, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create or update an existing message with the same key.
|
||||
/// </summary>
|
||||
public static void LogPersistent(GodotObject key, string message)
|
||||
{
|
||||
LogPersistent((object)key, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an existing persistent message.
|
||||
/// </summary>
|
||||
public static void RemovePersistent(object key)
|
||||
{
|
||||
if (Settings.enableLogs)
|
||||
Instance?.logWindow.RemovePersistent(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an existing persistent message.
|
||||
/// </summary>
|
||||
public static void RemovePersistent(GodotObject key)
|
||||
{
|
||||
RemovePersistent((object)key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all persistent logs.
|
||||
/// </summary>
|
||||
public static void ClearPersistent()
|
||||
{
|
||||
if (Settings.enableLogs)
|
||||
Instance?.logWindow.ClearPersistent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print a temporary message.
|
||||
/// </summary>
|
||||
public static void Log(object message)
|
||||
{
|
||||
Log(message.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print a temporary message.
|
||||
/// </summary>
|
||||
public static void Log(string message)
|
||||
{
|
||||
if (Settings.enableLogs)
|
||||
Instance?.logWindow.Log(message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Re-scans for DebugGUI attribute holders (i.e. [DebugGUIGraph] and [DebugGUIPrint])
|
||||
/// </summary>
|
||||
public static void ForceReinitializeAttributes()
|
||||
{
|
||||
if (Instance == null) return;
|
||||
|
||||
Instance.graphWindow.ReinitializeAttributes();
|
||||
Instance.logWindow.ReinitializeAttributes();
|
||||
}
|
||||
|
||||
GraphWindow graphWindow;
|
||||
LogWindow logWindow;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Instance = this;
|
||||
Settings.Load();
|
||||
|
||||
if (Settings.enableGraphs)
|
||||
{
|
||||
AddChild(graphWindow = new());
|
||||
}
|
||||
if (Settings.enableGraphs)
|
||||
{
|
||||
AddChild(logWindow = new());
|
||||
}
|
||||
}
|
||||
}
|
||||
20
addons/DebugGUI/DebugGUISettingsInitializer.cs
Normal file
20
addons/DebugGUI/DebugGUISettingsInitializer.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#if TOOLS
|
||||
using Godot;
|
||||
|
||||
[Tool]
|
||||
public partial class DebugGUISettingsInitializer : EditorPlugin
|
||||
{
|
||||
const string DEBUGGUI_RES_PATH = "res://addons/DebugGUI/DebugGUI.cs";
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
DebugGUI.Settings.Init();
|
||||
AddAutoloadSingleton(nameof(DebugGUI), DEBUGGUI_RES_PATH);
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
RemoveAutoloadSingleton(nameof(DebugGUI));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
148
addons/DebugGUI/Examples/DebugGUIExamples.cs
Normal file
148
addons/DebugGUI/Examples/DebugGUIExamples.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
public partial class DebugGUIExamples : Node
|
||||
{
|
||||
/* * * *
|
||||
*
|
||||
* [DebugGUIGraph]
|
||||
* Renders the variable in a graph on-screen. Attribute based graphs will updates every _Process.
|
||||
* Lets you optionally define:
|
||||
* max, min - The range of displayed values
|
||||
* r, g, b - The RGB color of the graph (0~1)
|
||||
* group - Graphs can be grouped into the same window and overlaid
|
||||
* autoScale - If true the graph will readjust min/max to fit the data
|
||||
*
|
||||
* [DebugGUIPrint]
|
||||
* Draws the current variable continuously on-screen as
|
||||
* $"{GameObject name} {variable name}: {value}"
|
||||
*
|
||||
* For more control, these features can be accessed manually.
|
||||
* DebugGUI.SetGraphProperties(key, ...) - Set the properties of the graph with the provided key
|
||||
* DebugGUI.Graph(key, value) - Push a value to the graph
|
||||
* DebugGUI.LogPersistent(key, value) - Print a persistent log entry on screen
|
||||
* DebugGUI.Log(value) - Print a temporary log entry on screen
|
||||
*
|
||||
* See DebugGUI.cs for more info
|
||||
*
|
||||
* * * */
|
||||
|
||||
// Disable Field Unused warning
|
||||
#pragma warning disable 0414
|
||||
|
||||
// Works with regular fields
|
||||
[DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 0, autoScale: true)]
|
||||
float SinField;
|
||||
|
||||
// As well as properties
|
||||
[DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 1, autoScale: true)]
|
||||
float CosProperty { get { return Mathf.Cos(time * 6); } }
|
||||
|
||||
// Also works for expression-bodied properties
|
||||
[DebugGUIGraph(min: -1, max: 1, r: 1, g: 0.3f, b: 1)]
|
||||
float SinProperty => Mathf.Sin((time + Mathf.Pi / 2) * 6);
|
||||
|
||||
// User inputs, print and graph in one!
|
||||
[DebugGUIPrint, DebugGUIGraph(group: 1, r: 1, g: 0.3f, b: 0.3f)]
|
||||
float mouseX;
|
||||
[DebugGUIPrint, DebugGUIGraph(group: 1, r: 0, g: 1, b: 0)]
|
||||
float mouseY;
|
||||
|
||||
Queue<double> deltaTimeBuffer = new();
|
||||
double smoothDeltaTime => deltaTimeBuffer.Sum() / deltaTimeBuffer.Count;
|
||||
float time;
|
||||
float physicsTime;
|
||||
bool wasMouseDown;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
|
||||
// Init smooth DT
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
deltaTimeBuffer.Enqueue(0);
|
||||
}
|
||||
|
||||
// Log (as opposed to LogPersistent) will disappear automatically after some time.
|
||||
DebugGUI.Log("Hello! I will disappear after some time!");
|
||||
|
||||
// Set up graph properties using our graph keys
|
||||
DebugGUI.SetGraphProperties("smoothFrameRate", "SmoothFPS", 0, 200, 2, new Color(0, 1, 1), false);
|
||||
DebugGUI.SetGraphProperties("frameRate", "FPS", 0, 200, 2, new Color(1, 0.5f, 1), false);
|
||||
DebugGUI.SetGraphProperties("fixedFrameRateSin", "FixedSin", -1, 1, 3, new Color(1, 1, 0), true);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
time += (float)delta;
|
||||
|
||||
// Update smooth delta time queue
|
||||
deltaTimeBuffer.Dequeue();
|
||||
deltaTimeBuffer.Enqueue(delta);
|
||||
|
||||
// Update the fields our attributes are graphing
|
||||
SinField = Mathf.Sin(time * 6);
|
||||
|
||||
// Update graphed mouse XY values
|
||||
var mousePos = GetViewport().GetMousePosition();
|
||||
var viewportRect = GetViewport().GetVisibleRect();
|
||||
mouseX = Mathf.Clamp(mousePos.X, 0, viewportRect.Size.X);
|
||||
mouseY = Mathf.Clamp(mousePos.Y, 0, viewportRect.Size.Y);
|
||||
|
||||
// Manual persistent logging
|
||||
DebugGUI.LogPersistent("smoothFrameRate", "SmoothFPS: " + (1 / smoothDeltaTime).ToString("F3"));
|
||||
DebugGUI.LogPersistent("frameRate", "FPS: " + (1 / delta).ToString("F3"));
|
||||
|
||||
// Manual logging of mouse clicks
|
||||
if (Input.IsMouseButtonPressed(MouseButton.Left))
|
||||
{
|
||||
if (!wasMouseDown)
|
||||
{
|
||||
wasMouseDown = true;
|
||||
DebugGUI.Log(string.Format(
|
||||
"Mouse down ({0}, {1})",
|
||||
mouseX.ToString("F3"),
|
||||
mouseY.ToString("F3")
|
||||
));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
wasMouseDown = false;
|
||||
}
|
||||
|
||||
if (smoothDeltaTime != 0)
|
||||
{
|
||||
DebugGUI.Graph("smoothFrameRate", 1 / (float)smoothDeltaTime);
|
||||
}
|
||||
if (delta != 0)
|
||||
{
|
||||
DebugGUI.Graph("frameRate", 1 / (float)delta);
|
||||
}
|
||||
|
||||
if (Input.IsKeyPressed(Key.Space))
|
||||
{
|
||||
QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
physicsTime += (float)delta;
|
||||
|
||||
// Manual graphing
|
||||
DebugGUI.Graph("fixedFrameRateSin", Mathf.Sin(physicsTime * 6));
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
// Clean up our logs and graphs when this object leaves tree
|
||||
DebugGUI.RemoveGraph("frameRate");
|
||||
DebugGUI.RemoveGraph("fixedFrameRateSin");
|
||||
DebugGUI.RemoveGraph("smoothFrameRate");
|
||||
|
||||
DebugGUI.RemovePersistent("frameRate");
|
||||
DebugGUI.RemovePersistent("smoothFrameRate");
|
||||
}
|
||||
}
|
||||
16
addons/DebugGUI/Examples/DebugGUIExamples.tscn
Normal file
16
addons/DebugGUI/Examples/DebugGUIExamples.tscn
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[gd_scene load_steps=3 format=3 uid="uid://cwxrep5n7yml0"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/DebugGUI/Examples/DebugGUIExamples.cs" id="1_herxx"]
|
||||
[ext_resource type="Script" path="res://addons/DebugGUI/Examples/debugGUI_examples.gd" id="2_acgyp"]
|
||||
|
||||
[node name="C# Example" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_herxx")
|
||||
|
||||
[node name="gdscript example" type="Node" parent="."]
|
||||
script = ExtResource("2_acgyp")
|
||||
9
addons/DebugGUI/Examples/debugGUI_examples.gd
Normal file
9
addons/DebugGUI/Examples/debugGUI_examples.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
extends Node
|
||||
|
||||
func _ready():
|
||||
DebugGUI.SetGraphProperties(self, "from gdscript", 0.0, 10.0, 1, Color.WHITE, true)
|
||||
DebugGUI.Log(self)
|
||||
DebugGUI.Log("This can be done from gdscript too!")
|
||||
|
||||
func _process(_delta):
|
||||
DebugGUI.Graph(self, sin(Time.get_ticks_msec() / 100.0))
|
||||
61
addons/DebugGUI/Windows/DebugGUIWindow.cs
Normal file
61
addons/DebugGUI/Windows/DebugGUIWindow.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using Godot;
|
||||
using System;
|
||||
|
||||
namespace WeavUtils
|
||||
{
|
||||
// Draggable window clamped to the corners
|
||||
public abstract partial class DebugGUIWindow : Control
|
||||
{
|
||||
protected const int outOfScreenClampPadding = 30;
|
||||
|
||||
static bool dragInProgress;
|
||||
bool dragged;
|
||||
|
||||
new public virtual Rect2 GetRect()
|
||||
{
|
||||
return base.GetRect();
|
||||
}
|
||||
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventMouseButton mb)
|
||||
{
|
||||
if (!dragInProgress && mb.Pressed && mb.ButtonIndex == MouseButton.Middle)
|
||||
{
|
||||
if (GetRect().HasPoint(mb.Position))
|
||||
{
|
||||
dragged = true;
|
||||
dragInProgress = true;
|
||||
}
|
||||
}
|
||||
if (mb.IsReleased() && mb.ButtonIndex == MouseButton.Middle)
|
||||
{
|
||||
if (dragged) dragInProgress = false;
|
||||
dragged = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (@event is InputEventMouseMotion motion)
|
||||
{
|
||||
if (dragged)
|
||||
{
|
||||
Move(motion.Relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void Move(Vector2 delta = default)
|
||||
{
|
||||
Position += delta;
|
||||
|
||||
var viewportRect = GetViewportRect();
|
||||
|
||||
// Limit graph window offset so we can't get lost off screen
|
||||
Position = Position.Clamp(
|
||||
-GetRect().Size + Vector2.One * outOfScreenClampPadding,
|
||||
viewportRect.Size - Vector2.One * outOfScreenClampPadding
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
693
addons/DebugGUI/Windows/GraphWindow.cs
Normal file
693
addons/DebugGUI/Windows/GraphWindow.cs
Normal file
|
|
@ -0,0 +1,693 @@
|
|||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using static DebugGUI.Settings;
|
||||
|
||||
namespace WeavUtils
|
||||
{
|
||||
public partial class GraphWindow : DebugGUIWindow
|
||||
{
|
||||
const int graphLabelFontSize = 12;
|
||||
const int graphLabelPadding = 5;
|
||||
const int graphBlockPadding = 3;
|
||||
const int scrubberBackgroundWidth = 55;
|
||||
const int windowOutOfScreenPadding = 30;
|
||||
|
||||
List<GraphContainer> graphs = new();
|
||||
HashSet<Node> attributeContainers = new();
|
||||
Dictionary<Type, int> typeInstanceCounts = new();
|
||||
Dictionary<object, GraphContainer> graphDictionary = new();
|
||||
Dictionary<Node, List<GraphAttributeKey>> attributeKeys = new();
|
||||
Dictionary<Type, HashSet<FieldInfo>> debugGUIGraphFields = new();
|
||||
Dictionary<Type, HashSet<PropertyInfo>> debugGUIGraphProperties = new();
|
||||
SortedDictionary<int, List<GraphContainer>> graphGroups = new();
|
||||
|
||||
bool freezeGraphs;
|
||||
float graphLabelBoxWidth;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Name = nameof(GraphWindow);
|
||||
|
||||
// Register attributes of all nodes present at start
|
||||
// See also: ReinitializeAttributes()
|
||||
RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root);
|
||||
|
||||
// Default to top right
|
||||
Position = new Vector2(GetViewportRect().Size.X - GetRect().Size.X, 0);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (!Input.IsMouseButtonPressed(MouseButton.Left))
|
||||
{
|
||||
freezeGraphs = false;
|
||||
}
|
||||
|
||||
if (!freezeGraphs)
|
||||
{
|
||||
CallDeferred(nameof(PollGraphAttributes));
|
||||
}
|
||||
|
||||
QueueRedraw();
|
||||
|
||||
// Clean up any nodes queued to free
|
||||
CallDeferred(nameof(CleanUpDeletedAttributes));
|
||||
}
|
||||
|
||||
public override void _Draw()
|
||||
{
|
||||
int groupNum = 0;
|
||||
foreach (var group in graphGroups.Values)
|
||||
{
|
||||
DrawGraphGroup(group, groupNum);
|
||||
groupNum++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Graph(object key, float val)
|
||||
{
|
||||
if (!graphDictionary.ContainsKey(key))
|
||||
{
|
||||
CreateGraph(key);
|
||||
}
|
||||
|
||||
if (freezeGraphs) return;
|
||||
|
||||
graphDictionary[key].Push(val);
|
||||
// Todo: optimize away?
|
||||
RecalculateGraphLabelWidth();
|
||||
}
|
||||
|
||||
public void CreateGraph(object key)
|
||||
{
|
||||
AddGraph(key, new GraphContainer(graphWidth));
|
||||
RecalculateGraphLabelWidth();
|
||||
}
|
||||
|
||||
public void ClearGraph(object key)
|
||||
{
|
||||
if (graphDictionary.ContainsKey(key))
|
||||
graphDictionary[key].Clear();
|
||||
}
|
||||
|
||||
public void RemoveGraph(object key)
|
||||
{
|
||||
if (graphDictionary.ContainsKey(key))
|
||||
{
|
||||
var graph = graphDictionary[key];
|
||||
graphs.Remove(graph);
|
||||
graphDictionary.Remove(key);
|
||||
graphGroups[graph.group].Remove(graph);
|
||||
if (graphGroups[graph.group].Count == 0)
|
||||
{
|
||||
graphGroups.Remove(graph.group);
|
||||
}
|
||||
RecalculateGraphLabelWidth();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGraphProperties(object key, string label, float min, float max, int group, Color color, bool autoScale)
|
||||
{
|
||||
if (graphDictionary.ContainsKey(key))
|
||||
{
|
||||
RemoveGraph(key);
|
||||
}
|
||||
|
||||
var graph = new GraphContainer(graphWidth, group);
|
||||
AddGraph(key, graph);
|
||||
|
||||
graph.name = label;
|
||||
graph.SetMinMax(min, max);
|
||||
graph.color = color;
|
||||
graph.autoScale = autoScale;
|
||||
}
|
||||
|
||||
public void ReinitializeAttributes()
|
||||
{
|
||||
// Clean up graphs
|
||||
List<object> toRemove = new List<object>();
|
||||
foreach (var key in graphDictionary.Keys)
|
||||
{
|
||||
if (key is GraphAttributeKey)
|
||||
toRemove.Add(key);
|
||||
}
|
||||
foreach (var key in toRemove)
|
||||
{
|
||||
RemoveGraph(key);
|
||||
}
|
||||
|
||||
attributeContainers = new();
|
||||
debugGUIGraphFields = new();
|
||||
debugGUIGraphProperties = new();
|
||||
typeInstanceCounts = new();
|
||||
attributeKeys = new();
|
||||
|
||||
RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root);
|
||||
}
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
var data = new Godot.Collections.Array<Variant>();
|
||||
|
||||
foreach (var node in graphs)
|
||||
{
|
||||
data.Add(node.ToDataVariant());
|
||||
}
|
||||
|
||||
return data.ToString();
|
||||
}
|
||||
|
||||
public override Rect2 GetRect()
|
||||
{
|
||||
RefreshRect();
|
||||
return base.GetRect();
|
||||
}
|
||||
|
||||
private void AddGraph(object key, GraphContainer graph)
|
||||
{
|
||||
graph.OnLabelSizeChange += RefreshRect;
|
||||
|
||||
graphDictionary.Add(key, graph);
|
||||
graphs.Add(graph);
|
||||
|
||||
if (!graphGroups.ContainsKey(graph.group))
|
||||
{
|
||||
graphGroups.Add(graph.group, new List<GraphContainer>());
|
||||
}
|
||||
|
||||
graphGroups[graph.group].Add(graph);
|
||||
|
||||
RecalculateGraphLabelWidth();
|
||||
}
|
||||
|
||||
private void PollGraphAttributes()
|
||||
{
|
||||
foreach (var node in attributeContainers)
|
||||
{
|
||||
if (node != null && attributeKeys.ContainsKey(node))
|
||||
{
|
||||
foreach (var key in attributeKeys[node])
|
||||
{
|
||||
if (key.memberInfo is FieldInfo fieldInfo)
|
||||
{
|
||||
float? val = fieldInfo.GetValue(node) as float?;
|
||||
if (val != null)
|
||||
graphDictionary[key].Push(val.Value);
|
||||
}
|
||||
else if (key.memberInfo is PropertyInfo propertyInfo)
|
||||
{
|
||||
float? val = propertyInfo.GetValue(node, null) as float?;
|
||||
if (val != null)
|
||||
graphDictionary[key].Push(val.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GraphContainer lastPressedGraphLabel;
|
||||
private void DrawGraphGroup(List<GraphContainer> group, int groupNum)
|
||||
{
|
||||
var mousePos = GetLocalMousePosition();
|
||||
|
||||
Vector2 graphBlockSize = new Vector2(graphWidth + graphBlockPadding, graphHeight + graphBlockPadding);
|
||||
|
||||
var groupOrigin = new Vector2(0, graphBlockSize.Y * groupNum);
|
||||
var groupGraphRect = new Rect2(
|
||||
groupOrigin.X + graphLabelBoxWidth + graphBlockPadding,
|
||||
groupOrigin.Y,
|
||||
graphWidth,
|
||||
graphHeight
|
||||
);
|
||||
|
||||
// Label background
|
||||
DrawRect(new Rect2(
|
||||
groupOrigin.X,
|
||||
groupOrigin.Y,
|
||||
graphLabelBoxWidth,
|
||||
graphHeight),
|
||||
backgroundColor);
|
||||
|
||||
// Graph background
|
||||
DrawRect(new Rect2(
|
||||
groupOrigin.X + graphBlockPadding + graphLabelBoxWidth,
|
||||
groupOrigin.Y,
|
||||
graphBlockSize.X,
|
||||
graphHeight),
|
||||
backgroundColor);
|
||||
|
||||
// Magic padding offsets
|
||||
Vector2 textOrigin = groupOrigin + new Vector2(0, 14);
|
||||
Vector2 minMaxOrigin = groupOrigin + new Vector2(graphLabelBoxWidth - 10, 16);
|
||||
foreach (var graph in group)
|
||||
{
|
||||
var textSize = textFont.GetStringSize(graph.name, fontSize: graphLabelFontSize);
|
||||
textOrigin.Y += textSize.Y;
|
||||
var maxWidthOfMinMaxStrings = Mathf.Max(
|
||||
textFont.GetStringSize(graph.minString, fontSize: graphLabelFontSize).X,
|
||||
textFont.GetStringSize(graph.maxString, fontSize: graphLabelFontSize).X
|
||||
);
|
||||
minMaxOrigin += Vector2.Left * (maxWidthOfMinMaxStrings + graphLabelPadding);
|
||||
|
||||
// Label button logic
|
||||
var labelRect = new Rect2(textOrigin - textSize + new Vector2(graphLabelBoxWidth - (graphLabelPadding * 2), graphLabelPadding), textSize);
|
||||
// Enable disable
|
||||
var isHovered = labelRect.HasPoint(mousePos);
|
||||
var isPressed = isHovered && Input.IsMouseButtonPressed(MouseButton.Left);
|
||||
|
||||
// Button click
|
||||
if (lastPressedGraphLabel == graph && !isPressed && isHovered)
|
||||
{
|
||||
graph.visible = !graph.visible;
|
||||
}
|
||||
|
||||
if (isPressed)
|
||||
{
|
||||
lastPressedGraphLabel = graph;
|
||||
}
|
||||
else if (lastPressedGraphLabel == graph)
|
||||
{
|
||||
lastPressedGraphLabel = null;
|
||||
}
|
||||
|
||||
var graphColor = graph.GetModifiedColor(isHovered);
|
||||
|
||||
// Name
|
||||
DrawString(
|
||||
textFont,
|
||||
textOrigin - new Vector2(textSize.X + 10 - graphLabelBoxWidth, 0),
|
||||
graph.name,
|
||||
fontSize: graphLabelFontSize,
|
||||
modulate: graphColor,
|
||||
alignment: HorizontalAlignment.Right
|
||||
);
|
||||
|
||||
// Max
|
||||
DrawString(
|
||||
textFont,
|
||||
minMaxOrigin,
|
||||
graph.maxString,
|
||||
modulate: graphColor,
|
||||
fontSize: graphLabelFontSize,
|
||||
alignment: HorizontalAlignment.Right
|
||||
);
|
||||
|
||||
// Min
|
||||
DrawString(
|
||||
textFont,
|
||||
minMaxOrigin + new Vector2(0, graphHeight - 20),
|
||||
graph.minString,
|
||||
modulate: graphColor,
|
||||
fontSize: graphLabelFontSize,
|
||||
alignment: HorizontalAlignment.Right
|
||||
);
|
||||
|
||||
// Graph
|
||||
if (graph.visible)
|
||||
{
|
||||
graph.Draw(groupGraphRect, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Scrubber
|
||||
if (groupGraphRect.HasPoint(mousePos))
|
||||
{
|
||||
if (Input.IsMouseButtonPressed(MouseButton.Left))
|
||||
{
|
||||
freezeGraphs = true;
|
||||
}
|
||||
|
||||
// Background
|
||||
Vector2 scrubberOrigin = new Vector2(mousePos.X, groupOrigin.Y);
|
||||
if (mousePos.X > groupGraphRect.End.X - scrubberBackgroundWidth)
|
||||
{
|
||||
scrubberOrigin.X -= scrubberBackgroundWidth;
|
||||
}
|
||||
|
||||
var rect = new Rect2(
|
||||
scrubberOrigin,
|
||||
scrubberBackgroundWidth,
|
||||
graphHeight
|
||||
);
|
||||
DrawRect(rect, backgroundColor);
|
||||
|
||||
DrawLine(
|
||||
new Vector2(
|
||||
mousePos.X,
|
||||
groupOrigin.Y
|
||||
), new Vector2(
|
||||
mousePos.X,
|
||||
groupOrigin.Y + graphHeight
|
||||
),
|
||||
scrubberColor
|
||||
);
|
||||
|
||||
// Scrubber labels
|
||||
Vector2 textPos = scrubberOrigin + new Vector2(graphLabelPadding, graphLabelPadding * 3);
|
||||
var groupMousePosX = (mousePos.X - groupOrigin.X);
|
||||
int sampleIndex = (int)(groupGraphRect.Size.X - groupMousePosX + graphLabelBoxWidth + graphBlockPadding);
|
||||
foreach (GraphContainer graph in group)
|
||||
{
|
||||
var text = graph.GetValue(sampleIndex).ToString("F3");
|
||||
DrawString(
|
||||
textFont,
|
||||
textPos,
|
||||
text,
|
||||
modulate: graph.color,
|
||||
fontSize: graphLabelFontSize
|
||||
);
|
||||
textPos.Y += textFont.GetHeight(graphLabelFontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Rect2 GetGraphWindowRect()
|
||||
{
|
||||
return new Rect2(
|
||||
new Vector2(-graphLabelBoxWidth, 0) + Position,
|
||||
graphWidth + graphLabelBoxWidth + graphBlockPadding,
|
||||
(graphHeight + graphBlockPadding) * graphGroups.Count
|
||||
);
|
||||
}
|
||||
|
||||
private void RegisterAttributes(Node node)
|
||||
{
|
||||
foreach (Node child in node.GetChildren())
|
||||
{
|
||||
GD.Print(child);
|
||||
RegisterAttributes(child);
|
||||
}
|
||||
|
||||
Type nodeType = node.GetType();
|
||||
|
||||
HashSet<Node> uniqueAttributeContainers = new();
|
||||
// Fields
|
||||
{
|
||||
// Retreive the fields from the mono instance
|
||||
FieldInfo[] objectFields = nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
// search all fields/properties for the [DebugGUIVar] attribute
|
||||
for (int i = 0; i < objectFields.Length; i++)
|
||||
{
|
||||
DebugGUIGraphAttribute graphAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(DebugGUIGraphAttribute)) as DebugGUIGraphAttribute;
|
||||
|
||||
if (graphAttribute != null)
|
||||
{
|
||||
// Can't cast to float so we don't bother registering it
|
||||
if (objectFields[i].GetValue(node) as float? == null)
|
||||
{
|
||||
GD.PrintErr(string.Format("Cannot cast {0}.{1} to float. This member will be ignored.", nodeType.Name, objectFields[i].Name));
|
||||
continue;
|
||||
}
|
||||
|
||||
uniqueAttributeContainers.Add(node);
|
||||
if (!debugGUIGraphFields.ContainsKey(nodeType))
|
||||
debugGUIGraphFields.Add(nodeType, new HashSet<FieldInfo>());
|
||||
if (!debugGUIGraphProperties.ContainsKey(nodeType))
|
||||
debugGUIGraphProperties.Add(nodeType, new HashSet<PropertyInfo>());
|
||||
|
||||
debugGUIGraphFields[nodeType].Add(objectFields[i]);
|
||||
GraphContainer graph =
|
||||
new GraphContainer(graphWidth, graphAttribute.group)
|
||||
{
|
||||
name = objectFields[i].Name,
|
||||
max = graphAttribute.max,
|
||||
min = graphAttribute.min,
|
||||
autoScale = graphAttribute.autoScale
|
||||
};
|
||||
graph.OnLabelSizeChange += RefreshRect;
|
||||
if (!graphAttribute.color.Equals(default(Color)))
|
||||
graph.color = graphAttribute.color;
|
||||
|
||||
var key = new GraphAttributeKey(objectFields[i]);
|
||||
if (!attributeKeys.ContainsKey(node))
|
||||
attributeKeys.Add(node, new List<GraphAttributeKey>());
|
||||
attributeKeys[node].Add(key);
|
||||
|
||||
AddGraph(key, graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties
|
||||
{
|
||||
PropertyInfo[] objectProperties = nodeType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
for (int i = 0; i < objectProperties.Length; i++)
|
||||
{
|
||||
if (Attribute.GetCustomAttribute(objectProperties[i], typeof(DebugGUIGraphAttribute)) is DebugGUIGraphAttribute graphAttribute)
|
||||
{
|
||||
// Can't cast to float so we don't bother registering it
|
||||
if (objectProperties[i].GetValue(node, null) as float? == null)
|
||||
{
|
||||
GD.PrintErr("Cannot cast " + objectProperties[i].Name + " to float. This member will be ignored.");
|
||||
continue;
|
||||
}
|
||||
|
||||
uniqueAttributeContainers.Add(node);
|
||||
|
||||
if (!debugGUIGraphFields.ContainsKey(nodeType))
|
||||
debugGUIGraphFields.Add(nodeType, new HashSet<FieldInfo>());
|
||||
if (!debugGUIGraphProperties.ContainsKey(nodeType))
|
||||
debugGUIGraphProperties.Add(nodeType, new HashSet<PropertyInfo>());
|
||||
|
||||
debugGUIGraphProperties[nodeType].Add(objectProperties[i]);
|
||||
|
||||
GraphContainer graph =
|
||||
new GraphContainer(graphWidth, graphAttribute.group)
|
||||
{
|
||||
name = objectProperties[i].Name,
|
||||
max = graphAttribute.max,
|
||||
min = graphAttribute.min,
|
||||
autoScale = graphAttribute.autoScale
|
||||
};
|
||||
graph.OnLabelSizeChange += RefreshRect;
|
||||
if (!graphAttribute.color.Equals(default(Color)))
|
||||
graph.color = graphAttribute.color;
|
||||
|
||||
var key = new GraphAttributeKey(objectProperties[i]);
|
||||
if (!attributeKeys.ContainsKey(node))
|
||||
attributeKeys.Add(node, new List<GraphAttributeKey>());
|
||||
attributeKeys[node].Add(key);
|
||||
|
||||
AddGraph(key, graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var attributeContainer in uniqueAttributeContainers)
|
||||
{
|
||||
attributeContainers.Add(attributeContainer);
|
||||
Type type = attributeContainer.GetType();
|
||||
if (!typeInstanceCounts.ContainsKey(type))
|
||||
typeInstanceCounts.Add(type, 0);
|
||||
typeInstanceCounts[type]++;
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpDeletedAttributes()
|
||||
{
|
||||
// Clear out associated keys
|
||||
foreach (var node in attributeContainers)
|
||||
{
|
||||
if (node.IsQueuedForDeletion())
|
||||
{
|
||||
var keys = attributeKeys[node];
|
||||
foreach (var key in keys)
|
||||
{
|
||||
RemoveGraph(key);
|
||||
}
|
||||
attributeKeys.Remove(node);
|
||||
|
||||
Type type = node.GetType();
|
||||
typeInstanceCounts[type]--;
|
||||
if (typeInstanceCounts[type] == 0)
|
||||
{
|
||||
if (debugGUIGraphFields.ContainsKey(type))
|
||||
debugGUIGraphFields.Remove(type);
|
||||
if (debugGUIGraphProperties.ContainsKey(type))
|
||||
debugGUIGraphProperties.Remove(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally clear out removed nodes
|
||||
attributeContainers.RemoveWhere(node => node.IsQueuedForDeletion());
|
||||
}
|
||||
|
||||
void RefreshRect()
|
||||
{
|
||||
var lastWidth = Size.X;
|
||||
RecalculateGraphLabelWidth();
|
||||
Size = new Vector2(
|
||||
graphWidth + graphLabelBoxWidth + graphBlockPadding,
|
||||
(graphHeight + graphBlockPadding) * graphGroups.Count);
|
||||
// Grow to the left instead of right
|
||||
Position += new Vector2(lastWidth - Size.X, 0);
|
||||
}
|
||||
|
||||
void RecalculateGraphLabelWidth()
|
||||
{
|
||||
float width = 0;
|
||||
foreach (var group in graphGroups.Values)
|
||||
{
|
||||
float minMaxWidth = graphLabelPadding;
|
||||
foreach (var graph in group)
|
||||
{
|
||||
// Names
|
||||
width = Mathf.Max(textFont.GetStringSize(graph.name, fontSize: graphLabelFontSize).X, width);
|
||||
|
||||
// Minmax labels per group
|
||||
var maxWidthOfMinMaxStrings = Mathf.Max(
|
||||
textFont.GetStringSize(graph.minString, fontSize: graphLabelFontSize).X,
|
||||
textFont.GetStringSize(graph.maxString, fontSize: graphLabelFontSize).X
|
||||
);
|
||||
minMaxWidth += maxWidthOfMinMaxStrings + graphLabelPadding;
|
||||
}
|
||||
width = Mathf.Max(minMaxWidth, width);
|
||||
}
|
||||
graphLabelBoxWidth = width + graphLabelPadding * 2;
|
||||
}
|
||||
|
||||
private class GraphContainer
|
||||
{
|
||||
public Action OnLabelSizeChange;
|
||||
|
||||
public string name;
|
||||
|
||||
// Value at the top of the graph
|
||||
public float max = 1;
|
||||
// Value at the bottom of the graph
|
||||
public float min = 0;
|
||||
public bool autoScale;
|
||||
public Color color;
|
||||
// Graph order on screen
|
||||
public readonly int group;
|
||||
|
||||
private int currentIndex;
|
||||
private readonly float[] values;
|
||||
private readonly Vector2[] graphPoints;
|
||||
|
||||
public string minString = null;
|
||||
public string maxString = null;
|
||||
public bool visible = true;
|
||||
|
||||
public Color GetModifiedColor(bool highlighted)
|
||||
{
|
||||
if (!highlighted && visible) return color;
|
||||
color.ToHsv(out float h, out float s, out float v);
|
||||
|
||||
if (!visible) v *= 0.3f;
|
||||
if (highlighted) v *= (v > 0.9f ? 0.7f : 1.2f);
|
||||
return Color.FromHsv(h, s, v);
|
||||
}
|
||||
|
||||
public void SetMinMax(float min, float max)
|
||||
{
|
||||
OnLabelSizeChange?.Invoke();
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
|
||||
minString = min.ToString("F2");
|
||||
maxString = max.ToString("F2");
|
||||
}
|
||||
|
||||
public GraphContainer(int width, int group = 0)
|
||||
{
|
||||
this.group = group;
|
||||
values = new float[width];
|
||||
graphPoints = new Vector2[width];
|
||||
SetMinMax(min, max);
|
||||
}
|
||||
|
||||
// Add a data point to the beginning of the graph
|
||||
public void Push(float val)
|
||||
{
|
||||
if (autoScale && (val > max || val < min))
|
||||
{
|
||||
SetMinMax(Mathf.Min(val, min), Mathf.Max(val, max));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prevent drawing outside frame
|
||||
val = Mathf.Clamp(val, min, max);
|
||||
}
|
||||
|
||||
values[currentIndex] = val;
|
||||
currentIndex = (currentIndex + 1) % values.Length;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Draw(Rect2 rect, CanvasItem canvasItem)
|
||||
{
|
||||
int num = values.Length;
|
||||
for (int i = 0; i < num; i++)
|
||||
{
|
||||
float value = values[Mod(currentIndex - i - 1, values.Length)];
|
||||
// Note flipped inverse lerp min max to account for y = down in godot
|
||||
graphPoints[i] = new Vector2(
|
||||
rect.Position.X + (rect.Size.X * ((float)i / num)),
|
||||
rect.Position.Y + (Mathf.InverseLerp(max, min, value) * graphHeight)
|
||||
);
|
||||
}
|
||||
|
||||
canvasItem.DrawPolyline(graphPoints, color);
|
||||
}
|
||||
|
||||
public float GetValue(int index)
|
||||
{
|
||||
return values[Mod(currentIndex + index, values.Length)];
|
||||
}
|
||||
|
||||
class DataExport
|
||||
{
|
||||
public string name;
|
||||
public float[] values;
|
||||
|
||||
public DataExport(string name, float[] values)
|
||||
{
|
||||
this.name = name;
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
public Variant ToDataVariant()
|
||||
{
|
||||
var vals = new float[values.Length];
|
||||
for (int i = 0; i < vals.Length; i++)
|
||||
{
|
||||
vals[i] = values[Mod(currentIndex + i, values.Length)];
|
||||
}
|
||||
|
||||
var dict = new Godot.Collections.Dictionary<string, Variant>();
|
||||
dict.Add("name", name);
|
||||
dict.Add("values", vals);
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
private static int Mod(int n, int m)
|
||||
{
|
||||
return ((n % m) + m) % m;
|
||||
}
|
||||
}
|
||||
|
||||
public class GraphAttributeKey
|
||||
{
|
||||
public MemberInfo memberInfo;
|
||||
public GraphAttributeKey(MemberInfo memberInfo)
|
||||
{
|
||||
this.memberInfo = memberInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
308
addons/DebugGUI/Windows/LogWindow.cs
Normal file
308
addons/DebugGUI/Windows/LogWindow.cs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Reflection;
|
||||
using static DebugGUI.Settings;
|
||||
|
||||
namespace WeavUtils
|
||||
{
|
||||
public partial class LogWindow : DebugGUIWindow
|
||||
{
|
||||
List<TransientLog> transientLogs = new();
|
||||
HashSet<Node> attributeContainers = new();
|
||||
Dictionary<Node, Type> typeCache = new();
|
||||
Dictionary<Type, int> typeInstanceCounts = new();
|
||||
Dictionary<object, string> persistentLogs = new();
|
||||
Dictionary<Node, List<PersistentLogAttributeKey>> attributeKeys = new();
|
||||
Dictionary<Type, HashSet<FieldInfo>> debugGUIPrintFields = new();
|
||||
Dictionary<Type, HashSet<PropertyInfo>> debugGUIPrintProperties = new();
|
||||
|
||||
StringBuilder persistentLogStringBuilder = new();
|
||||
|
||||
double time;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Name = nameof(LogWindow);
|
||||
|
||||
// Register attributes of all nodes present at start
|
||||
// See also: ReinitializeAttributes()
|
||||
RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
time += delta;
|
||||
|
||||
// Clean up expired logs
|
||||
int expiredCt = 0;
|
||||
for (int i = 0; i < transientLogs.Count; i++)
|
||||
{
|
||||
if (transientLogs[i].expiryTime <= time)
|
||||
{
|
||||
expiredCt++;
|
||||
}
|
||||
}
|
||||
transientLogs.RemoveRange(0, expiredCt);
|
||||
|
||||
if (debugGUIPrintFields.Count + debugGUIPrintProperties.Count + persistentLogs.Count + transientLogs.Count > 0)
|
||||
{
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
CallDeferred(nameof(CleanUpDeletedAttributes));
|
||||
}
|
||||
|
||||
public override void _Draw()
|
||||
{
|
||||
persistentLogStringBuilder.Clear();
|
||||
|
||||
var viewportRect = GetViewportRect();
|
||||
var lineHeight = textFont.GetHeight();
|
||||
|
||||
foreach (var node in attributeContainers)
|
||||
{
|
||||
Type type = typeCache[node];
|
||||
if (debugGUIPrintFields.ContainsKey(type))
|
||||
{
|
||||
foreach (var field in debugGUIPrintFields[type])
|
||||
{
|
||||
persistentLogStringBuilder.AppendLine($"{node.Name} {field.Name}: {field.GetValue(node)}");
|
||||
}
|
||||
}
|
||||
if (debugGUIPrintProperties.ContainsKey(type))
|
||||
{
|
||||
foreach (var property in debugGUIPrintProperties[type])
|
||||
{
|
||||
persistentLogStringBuilder.AppendLine($"{node.Name} {property.Name}: {property.GetValue(node, null)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var log in persistentLogs.Values)
|
||||
{
|
||||
persistentLogStringBuilder.AppendLine(log);
|
||||
}
|
||||
|
||||
if (persistentLogStringBuilder.Length > 0 && transientLogs.Count != 0)
|
||||
{
|
||||
persistentLogStringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
var persistentLogStr = persistentLogStringBuilder.ToString();
|
||||
var textSize = textFont.GetMultilineStringSize(persistentLogStr);
|
||||
Size = textSize + Vector2.One * 10;
|
||||
|
||||
float transientLogY = textSize.Y;
|
||||
|
||||
foreach (var log in transientLogs)
|
||||
{
|
||||
var size = textFont.GetStringSize(log.text);
|
||||
textSize = new Vector2(
|
||||
Mathf.Max(size.X, textSize.X),
|
||||
textSize.Y + size.Y
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var backgroundRect = new Rect2(Vector2.Zero, textSize.X + 10, textSize.Y + 10);
|
||||
DrawRect(backgroundRect, backgroundColor);
|
||||
// Draw a little bit extra for the draggable area
|
||||
DrawRect(new Rect2(0, 0, GetRect().Size), new Color(1, 1, 1, 0.05f));
|
||||
|
||||
// Draw persistent logs
|
||||
DrawMultilineString(textFont, new Vector2(0, textFont.GetHeight()), persistentLogStr);
|
||||
|
||||
// Draw separator
|
||||
if (persistentLogStringBuilder.Length > 0 && transientLogs.Count != 0)
|
||||
{
|
||||
DrawDashedLine(
|
||||
new Vector2(0, transientLogY),
|
||||
new Vector2(textSize.X, transientLogY),
|
||||
Colors.White,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
// Draw transient logs
|
||||
for (int i = transientLogs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
transientLogY += lineHeight;
|
||||
// Clear up transient logs going off screen
|
||||
if (transientLogY > viewportRect.Size.Y)
|
||||
{
|
||||
transientLogs.RemoveRange(0, i + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
var log = transientLogs[i];
|
||||
DrawString(textFont, new Vector2(0, transientLogY), log.text);
|
||||
}
|
||||
}
|
||||
|
||||
public void Log(string str)
|
||||
{
|
||||
transientLogs.Add(new TransientLog(str, time + temporaryLogLifetime));
|
||||
}
|
||||
|
||||
public void LogPersistent(object key, string message)
|
||||
{
|
||||
if (persistentLogs.ContainsKey(key))
|
||||
persistentLogs[key] = message;
|
||||
else
|
||||
persistentLogs.Add(key, message);
|
||||
}
|
||||
|
||||
public void RemovePersistent(object key)
|
||||
{
|
||||
if (persistentLogs.ContainsKey(key))
|
||||
{
|
||||
persistentLogs.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearPersistent()
|
||||
{
|
||||
persistentLogs.Clear();
|
||||
}
|
||||
|
||||
public void ReinitializeAttributes()
|
||||
{
|
||||
// Clean up graphs
|
||||
List<object> toRemove = new List<object>();
|
||||
foreach (var key in persistentLogs.Keys)
|
||||
{
|
||||
if (key is PersistentLogAttributeKey)
|
||||
toRemove.Add(key);
|
||||
}
|
||||
foreach (var key in toRemove)
|
||||
{
|
||||
persistentLogs.Remove(key);
|
||||
}
|
||||
|
||||
attributeContainers = new();
|
||||
debugGUIPrintFields = new();
|
||||
debugGUIPrintProperties = new();
|
||||
typeInstanceCounts = new();
|
||||
attributeKeys = new();
|
||||
}
|
||||
|
||||
private void RegisterAttributes(Node node)
|
||||
{
|
||||
foreach (Node child in node.GetChildren())
|
||||
{
|
||||
GD.Print(child);
|
||||
RegisterAttributes(child);
|
||||
}
|
||||
|
||||
Type nodeType = node.GetType();
|
||||
|
||||
|
||||
HashSet<Node> uniqueAttributeContainers = new();
|
||||
|
||||
// Fields
|
||||
{
|
||||
// Retreive the fields from the mono instance
|
||||
FieldInfo[] objectFields = nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
// search all fields/properties for the [DebugGUIVar] attribute
|
||||
for (int i = 0; i < objectFields.Length; i++)
|
||||
{
|
||||
DebugGUIPrintAttribute printAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(DebugGUIPrintAttribute)) as DebugGUIPrintAttribute;
|
||||
|
||||
if (printAttribute != null)
|
||||
{
|
||||
uniqueAttributeContainers.Add(node);
|
||||
typeCache[node] = node.GetType();
|
||||
if (!debugGUIPrintFields.ContainsKey(nodeType))
|
||||
{
|
||||
debugGUIPrintFields.Add(nodeType, new HashSet<FieldInfo>());
|
||||
}
|
||||
|
||||
GD.Print("Found field " + objectFields[i].Name + " on " + node);
|
||||
debugGUIPrintFields[nodeType].Add(objectFields[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties
|
||||
{
|
||||
PropertyInfo[] objectProperties = nodeType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
for (int i = 0; i < objectProperties.Length; i++)
|
||||
{
|
||||
if (Attribute.GetCustomAttribute(objectProperties[i], typeof(DebugGUIPrintAttribute)) is DebugGUIPrintAttribute)
|
||||
{
|
||||
uniqueAttributeContainers.Add(node);
|
||||
typeCache[node] = node.GetType();
|
||||
|
||||
if (!debugGUIPrintProperties.ContainsKey(nodeType))
|
||||
{
|
||||
debugGUIPrintProperties.Add(nodeType, new HashSet<PropertyInfo>());
|
||||
}
|
||||
debugGUIPrintProperties[nodeType].Add(objectProperties[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var attributeContainer in uniqueAttributeContainers)
|
||||
{
|
||||
attributeContainers.Add(attributeContainer);
|
||||
Type type = attributeContainer.GetType();
|
||||
if (!typeInstanceCounts.ContainsKey(type))
|
||||
typeInstanceCounts.Add(type, 0);
|
||||
typeInstanceCounts[type]++;
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpDeletedAttributes()
|
||||
{
|
||||
// Clear out associated keys
|
||||
foreach (var node in attributeContainers)
|
||||
{
|
||||
if (node.IsQueuedForDeletion())
|
||||
{
|
||||
attributeKeys.Remove(node);
|
||||
typeCache.Remove(node);
|
||||
|
||||
Type type = node.GetType();
|
||||
typeInstanceCounts[type]--;
|
||||
if (typeInstanceCounts[type] == 0)
|
||||
{
|
||||
if (debugGUIPrintFields.ContainsKey(type))
|
||||
debugGUIPrintFields.Remove(type);
|
||||
if (debugGUIPrintProperties.ContainsKey(type))
|
||||
debugGUIPrintProperties.Remove(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally clear out removed nodes
|
||||
attributeContainers.RemoveWhere(node => node.IsQueuedForDeletion());
|
||||
}
|
||||
|
||||
private struct TransientLog
|
||||
{
|
||||
public string text;
|
||||
public double expiryTime;
|
||||
|
||||
public TransientLog(string text, double expiryTime)
|
||||
{
|
||||
this.text = text;
|
||||
this.expiryTime = expiryTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper to differentiate attributes from
|
||||
// manually created logs
|
||||
public class PersistentLogAttributeKey
|
||||
{
|
||||
public MemberInfo memberInfo;
|
||||
public PersistentLogAttributeKey(MemberInfo memberInfo)
|
||||
{
|
||||
this.memberInfo = memberInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
addons/DebugGUI/plugin.cfg
Normal file
7
addons/DebugGUI/plugin.cfg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="DebugGUI"
|
||||
description=""
|
||||
author="WeaverDev"
|
||||
version="1.0"
|
||||
script="DebugGUISettingsInitializer.cs"
|
||||
|
|
@ -8,21 +8,36 @@
|
|||
|
||||
config_version=5
|
||||
|
||||
[DebugGUI]
|
||||
|
||||
Settings/enableGraphs=true
|
||||
Settings/enableLogs=true
|
||||
Settings/backgroundColor=Color(0, 0, 0, 0.7)
|
||||
Settings/scrubberColor=Color(1, 1, 0, 0.7)
|
||||
Settings/graphWidth=300
|
||||
Settings/graphHeight=100
|
||||
Settings/temporaryLogLifetime=5.0
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Cirno"
|
||||
run/main_scene="res://Scenes/game.tscn"
|
||||
run/main_scene="res://Scenes/test.tscn"
|
||||
config/features=PackedStringArray("4.3", "C#", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
DebugStats="*res://Scenes/debug_stats.tscn"
|
||||
DebugStats="res://Scenes/debug_stats.tscn"
|
||||
DebugGUI="*res://addons/DebugGUI/DebugGUI.cs"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
window/size/viewport_width=320
|
||||
window/size/viewport_height=160
|
||||
window/size/window_width_override=1920
|
||||
window/size/window_height_override=1080
|
||||
window/stretch/mode="canvas_items"
|
||||
window/stretch/scale_mode="integer"
|
||||
|
||||
[dotnet]
|
||||
|
||||
|
|
@ -30,7 +45,7 @@ project/assembly_name="Cirno"
|
|||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/smoothing/plugin.cfg")
|
||||
enabled=PackedStringArray("res://addons/DebugGUI/plugin.cfg", "res://addons/smoothing/plugin.cfg")
|
||||
|
||||
[input]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue