├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── addons └── DebugGUI │ ├── Attributes │ ├── DebugGUIGraphAttribute.cs │ └── DebugGUIPrintAttribute.cs │ ├── DebugGUI.cs │ ├── DebugGUISettingsInitializer.cs │ ├── Examples │ ├── DebugGUIExamples.cs │ ├── DebugGUIExamples.tscn │ └── debugGUI_examples.gd │ ├── Windows │ ├── DebugGUIWindow.cs │ ├── GraphWindow.cs │ └── LogWindow.cs │ └── plugin.cfg └── project.godot /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | 7 | .vs/ 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | Godot Projects.csproj.old* 17 | *.csproj* 18 | *.sln 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Isar Arason (WeaverDev) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Debug GUI Graph (C#)


3 |

4 | DebugGUI Icon 5 |

6 | 7 |

8 | Simple and easy to use graphing debug utility. 9 |

10 | 11 |
12 | 13 | ## Description 14 | This is an upgraded port of the [DebugGUI Graph](https://assetstore.unity.com/packages/tools/gui/debuggui-graph-139275) asset for [Godot 4.x](https://godotengine.org/ "Godot") .NET (C#). 15 | 16 | DebugGUI Graph provides a way to debug continuous systems by providing an inspectable graphing GUI and logging overlay. It also provides an optional attribute-based abstraction for a one line injection into your existing code. 17 | 18 |
19 | 20 | ```csharp 21 | // Works with regular fields 22 | [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 0, autoScale: true)] 23 | float SinField; 24 | 25 | // As well as properties 26 | [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 1, autoScale: true)] 27 | float CosProperty { get { return Mathf.Cos(time * 6); } } 28 | 29 | // Also works for expression-bodied properties 30 | [DebugGUIGraph(min: -1, max: 1, r: 1, g: 0.3f, b: 1)] 31 | float SinProperty => Mathf.Sin((time + Mathf.Pi / 2) * 6); 32 | 33 | // User inputs, print and graph in one! 34 | [DebugGUIPrint, DebugGUIGraph(group: 1, r: 1, g: 0.3f, b: 0.3f)] 35 | float mouseX; 36 | [DebugGUIPrint, DebugGUIGraph(group: 1, r: 0, g: 1, b: 0)] 37 | float mouseY; 38 | ``` 39 | 40 |

41 | DebugGUI Screenshot 42 |

43 | 44 | ## Installation 45 | 1. Download the latest commit or the stable Release version. 46 | 2. Place the `addons` folder into your project root. 47 | 3. **!! Build the project solution !!** under `MSBuild > Build > Build Project` on the bottom of the editor. 48 | 49 | * *Failing to do this will prevent the plugin settings from being generated and won't auto-add DebugGUI to AutoLoad* 50 | 4. Enable the plugin by going to `Project > Project Settings > Plugins` and checking `Enable` next to `DebugGUI` 51 | 52 | ## Overview 53 | 54 | ### UI Interaction 55 | 56 | - **Dragging** - Windows can be dragged with middle mouse button. 57 | - **Scrubbing** - You can check the values at any point on the graph by hovering over it with the mouse. 58 | - **Freezing** - Holding left mouse button on the graph window stops graph updates for easier scrubbing. 59 | - **Disabling** - Graphs can be toggled on/off by clicking their names. 60 | 61 | Settings for changing colors and graph sizes are available in the project settings under `DebugGui > Settings`. 62 | 63 | ### Graphing 64 | 65 | A graph can be generated by adding the `DebugGUIGraph` attribute above any property or variable castable to `float`. Here you can specify the range of the graph (i.e. values at the top and bottom), the RGB color, and whether it should expand its range if a variable goes outside the range. 66 | 67 | ```cs 68 | [DebugGUIGraph(group: 2, min: -200, max: 200, r: 0, g: 1, b: 0, autoScale: true)] 69 | float playerXVelocity => LinearVelocity.X; 70 | ``` 71 | 72 |
73 | 74 | Alternatively, for more control, you can define and manage a graph manually. Graphs are referenced via an `object` key, which you then use to push new values to the graph. 75 | 76 | ```cs 77 | object myGraphKey = new object(); 78 | 79 | public override void _Ready() 80 | { 81 | // Graph using this node as the key 82 | DebugGUI.SetGraphProperties(this, "My Graph", 0, 1, 0, Colors.Red, false); 83 | 84 | // Another graph with an arbitrary object key 85 | DebugGUI.SetGraphProperties(myGraphKey, "My Other Graph", 0, 1, 0, Colors.Blue, false); 86 | 87 | // Strings also work as keys 88 | DebugGUI.SetGraphProperties("my graph", "My Other Graph", 0, 1, 0, Colors.Green, false); 89 | } 90 | 91 | public override void _Process(double delta) 92 | { 93 | DebugGUI.Graph(this, (float)delta); 94 | DebugGUI.Graph(myGraphKey, (float)delta); 95 | DebugGUI.Graph("my graph", (float)delta); 96 | } 97 | ``` 98 | 99 |
100 | 101 | Graphs can be exported to json via `DebugGUI.ExportGraphs()`. 102 | 103 | ### Logging 104 | 105 | Similar to the graphs, a persistent logged value can be generated by adding the `DebugGUIPrint` attribute above any property or variable. This attribute has no configurable settings. Attribute-derived logs will include the object and field name in its output. 106 | 107 | ```cs 108 | [DebugGUIPrint] 109 | float playerXVelocity => LinearVelocity.X; 110 | ``` 111 | 112 |
113 | 114 | Persistent logs may also be managed manually in the same way as graphs using arbitrary keys. 115 | 116 | ```cs 117 | public override void _Process(double delta) 118 | { 119 | DebugGUI.LogPersistent(this, $"Velocity: {LinearVelocity}"); 120 | DebugGUI.LogPersistent("deltatime", $"Last delta: {delta}"); 121 | DebugGUI.Log("This is a temporary log entry!"); 122 | } 123 | ``` 124 | 125 | ## GDScript Support 126 | 127 | Using DebugGUI Graph from GDScript requires Godot 4.2. GDScript does not support C#-style attribues, however the static methods of `DebugGUI` can be used manually from GDScript. 128 | 129 | ```python 130 | func _ready(): 131 | DebugGUI.SetGraphProperties(self, "MyObject", 0.0, 10.0, 1, Color.WHITE, true) 132 | 133 | func _process(_delta): 134 | DebugGUI.Graph(self, self.linear_velocity.length()) 135 | ``` 136 | 137 | ## Contribution 138 | If you spot a bug while using this, please create an [Issue](https://github.com/WeaverDev/DebugGUIGraph/issues). 139 | 140 | ## License 141 | 142 | [MIT License](LICENSE) 143 | -------------------------------------------------------------------------------- /addons/DebugGUI/Attributes/DebugGUIGraphAttribute.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | 4 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 5 | public class DebugGUIGraphAttribute : Attribute 6 | { 7 | public float min { get; private set; } 8 | public float max { get; private set; } 9 | public Color color { get; private set; } 10 | public int group { get; private set; } 11 | public bool autoScale { get; private set; } 12 | 13 | public DebugGUIGraphAttribute( 14 | // Line color 15 | float r = 1, 16 | float g = 1, 17 | float b = 1, 18 | // Values at top/bottom of graph 19 | float min = 0, 20 | float max = 1, 21 | // Offset position on screen 22 | int group = 0, 23 | // Auto-adjust min/max to fit the values 24 | bool autoScale = true 25 | ) 26 | { 27 | color = new Color(r, g, b, 0.9f); 28 | this.min = min; 29 | this.max = max; 30 | this.group = group; 31 | this.autoScale = autoScale; 32 | } 33 | } -------------------------------------------------------------------------------- /addons/DebugGUI/Attributes/DebugGUIPrintAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 4 | public class DebugGUIPrintAttribute : Attribute { } -------------------------------------------------------------------------------- /addons/DebugGUI/DebugGUI.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System.IO; 3 | using WeavUtils; 4 | 5 | public partial class DebugGUI : Control 6 | { 7 | // Other scripts may use us right off the bat, so we make sure we initialize first 8 | public DebugGUI() 9 | { 10 | ProcessPhysicsPriority = int.MinValue; 11 | } 12 | 13 | static DebugGUI Instance; 14 | 15 | #region Settings 16 | 17 | public static class Settings 18 | { 19 | const string DEBUGGUI_SETTINGS_DIR = "DebugGUI/Settings/"; 20 | 21 | public static void Init() 22 | { 23 | if (!Engine.IsEditorHint()) return; 24 | 25 | // Inits defaults or load current if present 26 | Load(); 27 | 28 | ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", enableGraphs); 29 | ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", enableLogs); 30 | 31 | ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", backgroundColor); 32 | ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", scrubberColor); 33 | ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", graphWidth); 34 | ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", graphHeight); 35 | ProjectSettings.SetSetting($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", temporaryLogLifetime); 36 | 37 | ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", true); 38 | ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", true); 39 | 40 | ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", true); 41 | ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", true); 42 | ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", true); 43 | ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", true); 44 | ProjectSettings.SetAsBasic($"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", true); 45 | 46 | var err = ProjectSettings.Save(); 47 | if(err != Error.Ok) 48 | { 49 | GD.PrintErr(err); 50 | } 51 | } 52 | 53 | public static void Load() 54 | { 55 | textFont = ThemeDB.FallbackFont; 56 | 57 | enableGraphs = ProjectSettings.GetSetting( 58 | $"{DEBUGGUI_SETTINGS_DIR}{nameof(enableGraphs)}", 59 | true 60 | ).AsBool(); 61 | enableLogs = ProjectSettings.GetSetting( 62 | $"{DEBUGGUI_SETTINGS_DIR}{nameof(enableLogs)}", 63 | true 64 | ).AsBool(); 65 | 66 | backgroundColor = ProjectSettings.GetSetting( 67 | $"{DEBUGGUI_SETTINGS_DIR}{nameof(backgroundColor)}", 68 | new Color(0f, 0f, 0f, 0.7f) 69 | ).AsColor(); 70 | scrubberColor = ProjectSettings.GetSetting( 71 | $"{DEBUGGUI_SETTINGS_DIR}{nameof(scrubberColor)}", 72 | new Color(1f, 1f, 0f, 0.7f) 73 | ).AsColor(); 74 | graphWidth = ProjectSettings.GetSetting( 75 | $"{DEBUGGUI_SETTINGS_DIR}{nameof(graphWidth)}", 76 | 300 77 | ).AsInt32(); 78 | graphHeight = ProjectSettings.GetSetting( 79 | $"{DEBUGGUI_SETTINGS_DIR}{nameof(graphHeight)}", 80 | 100 81 | ).AsInt32(); 82 | temporaryLogLifetime = ProjectSettings.GetSetting( 83 | $"{DEBUGGUI_SETTINGS_DIR}{nameof(temporaryLogLifetime)}", 84 | 5 85 | ).AsDouble(); 86 | } 87 | 88 | public static bool enableGraphs; 89 | public static bool enableLogs; 90 | 91 | public static Color backgroundColor; 92 | public static Color scrubberColor; 93 | public static int graphWidth; 94 | public static int graphHeight; 95 | public static double temporaryLogLifetime; 96 | 97 | public static Font textFont; 98 | } 99 | 100 | #endregion 101 | 102 | #region Graph 103 | 104 | /// 105 | /// Set the properties of a graph. 106 | /// 107 | /// The graph's key 108 | /// The graph's label 109 | /// Value at the bottom of the graph box 110 | /// Value at the top of the graph box 111 | /// The graph's ordinal position on screen 112 | /// The graph's color 113 | public static void SetGraphProperties(object key, string label, float min, float max, int group, Color color, bool autoScale) 114 | { 115 | if (Settings.enableGraphs) 116 | Instance?.graphWindow.SetGraphProperties(key, label, min, max, group, color, autoScale); 117 | } 118 | 119 | /// 120 | /// Set the properties of a graph. 121 | /// 122 | /// The graph's key 123 | /// The graph's label 124 | /// Value at the bottom of the graph box 125 | /// Value at the top of the graph box 126 | /// The graph's ordinal position on screen 127 | /// The graph's color 128 | public static void SetGraphProperties(GodotObject key, string label, float min, float max, int group, Color color, bool autoScale) 129 | { 130 | SetGraphProperties((object)key, label, min, max, group, color, autoScale); 131 | } 132 | 133 | /// 134 | /// Add a data point to a graph. 135 | /// 136 | /// The graph's key 137 | /// Value to be added 138 | public static void Graph(object key, float val) 139 | { 140 | if (Settings.enableGraphs) 141 | Instance?.graphWindow.Graph(key, val); 142 | } 143 | 144 | /// 145 | /// Add a data point to a graph. 146 | /// 147 | /// The graph's key 148 | /// Value to be added 149 | public static void Graph(GodotObject key, float val) 150 | { 151 | Graph((object)key, val); 152 | } 153 | 154 | /// 155 | /// Remove an existing graph. 156 | /// 157 | /// The graph's key 158 | public static void RemoveGraph(object key) 159 | { 160 | if (Settings.enableGraphs) 161 | Instance?.graphWindow.RemoveGraph(key); 162 | } 163 | 164 | /// 165 | /// Remove an existing graph. 166 | /// 167 | /// The graph's key 168 | public static void RemoveGraph(GodotObject key) 169 | { 170 | RemoveGraph((object)key); 171 | } 172 | 173 | /// 174 | /// Resets a graph's data. 175 | /// 176 | /// The graph's key 177 | public static void ClearGraph(object key) 178 | { 179 | if (Settings.enableGraphs) 180 | Instance?.graphWindow.ClearGraph(key); 181 | } 182 | 183 | /// 184 | /// Resets a graph's data. 185 | /// 186 | /// The graph's key 187 | public static void ClearGraph(GodotObject key) 188 | { 189 | ClearGraph((object)key); 190 | } 191 | 192 | /// 193 | /// Export graphs to a json file. See path in log. 194 | /// 195 | public static void ExportGraphs() 196 | { 197 | if (Instance == null || !Settings.enableGraphs) 198 | return; 199 | 200 | string dateTimeStr = Time.GetDatetimeStringFromSystem().Replace(':', '-'); 201 | string filename = $"debuggui_graph_export_{dateTimeStr}.json"; 202 | 203 | using var file = Godot.FileAccess.Open( 204 | "user://" + filename, 205 | Godot.FileAccess.ModeFlags.Write 206 | ); 207 | 208 | if (file == null) 209 | { 210 | GD.Print("DebugGUI graph export failed: " + Godot.FileAccess.GetOpenError()); 211 | } 212 | else 213 | { 214 | file.StoreString(Instance.graphWindow.ToJson()); 215 | GD.Print($"Wrote graph data to {Path.Combine(OS.GetUserDataDir(), filename)}"); 216 | } 217 | } 218 | 219 | #endregion 220 | 221 | #region Log 222 | 223 | /// 224 | /// Create or update an existing message with the same key. 225 | /// 226 | public static void LogPersistent(object key, string message) 227 | { 228 | if (Settings.enableLogs) 229 | Instance?.logWindow.LogPersistent(key, message); 230 | } 231 | 232 | /// 233 | /// Create or update an existing message with the same key. 234 | /// 235 | public static void LogPersistent(GodotObject key, string message) 236 | { 237 | LogPersistent((object)key, message); 238 | } 239 | 240 | /// 241 | /// Remove an existing persistent message. 242 | /// 243 | public static void RemovePersistent(object key) 244 | { 245 | if (Settings.enableLogs) 246 | Instance?.logWindow.RemovePersistent(key); 247 | } 248 | 249 | /// 250 | /// Remove an existing persistent message. 251 | /// 252 | public static void RemovePersistent(GodotObject key) 253 | { 254 | RemovePersistent((object)key); 255 | } 256 | 257 | /// 258 | /// Clears all persistent logs. 259 | /// 260 | public static void ClearPersistent() 261 | { 262 | if (Settings.enableLogs) 263 | Instance?.logWindow.ClearPersistent(); 264 | } 265 | 266 | /// 267 | /// Print a temporary message. 268 | /// 269 | public static void Log(object message) 270 | { 271 | Log(message.ToString()); 272 | } 273 | 274 | /// 275 | /// Print a temporary message. 276 | /// 277 | public static void Log(string message) 278 | { 279 | if (Settings.enableLogs) 280 | Instance?.logWindow.Log(message); 281 | } 282 | 283 | #endregion 284 | 285 | /// 286 | /// Re-scans for DebugGUI attribute holders (i.e. [DebugGUIGraph] and [DebugGUIPrint]) 287 | /// 288 | public static void ForceReinitializeAttributes() 289 | { 290 | if (Instance == null) return; 291 | 292 | Instance.graphWindow.ReinitializeAttributes(); 293 | Instance.logWindow.ReinitializeAttributes(); 294 | } 295 | 296 | GraphWindow graphWindow; 297 | LogWindow logWindow; 298 | 299 | public override void _Ready() 300 | { 301 | Instance = this; 302 | Settings.Load(); 303 | 304 | if (Settings.enableGraphs) 305 | { 306 | AddChild(graphWindow = new()); 307 | } 308 | if (Settings.enableGraphs) 309 | { 310 | AddChild(logWindow = new()); 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /addons/DebugGUI/DebugGUISettingsInitializer.cs: -------------------------------------------------------------------------------- 1 | #if TOOLS 2 | using Godot; 3 | 4 | [Tool] 5 | public partial class DebugGUISettingsInitializer : EditorPlugin 6 | { 7 | const string DEBUGGUI_RES_PATH = "res://addons/DebugGUI/DebugGUI.cs"; 8 | 9 | public override void _EnterTree() 10 | { 11 | DebugGUI.Settings.Init(); 12 | AddAutoloadSingleton(nameof(DebugGUI), DEBUGGUI_RES_PATH); 13 | } 14 | 15 | public override void _ExitTree() 16 | { 17 | RemoveAutoloadSingleton(nameof(DebugGUI)); 18 | } 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /addons/DebugGUI/Examples/DebugGUIExamples.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | public partial class DebugGUIExamples : Node 6 | { 7 | /* * * * 8 | * 9 | * [DebugGUIGraph] 10 | * Renders the variable in a graph on-screen. Attribute based graphs will updates every _Process. 11 | * Lets you optionally define: 12 | * max, min - The range of displayed values 13 | * r, g, b - The RGB color of the graph (0~1) 14 | * group - Graphs can be grouped into the same window and overlaid 15 | * autoScale - If true the graph will readjust min/max to fit the data 16 | * 17 | * [DebugGUIPrint] 18 | * Draws the current variable continuously on-screen as 19 | * $"{GameObject name} {variable name}: {value}" 20 | * 21 | * For more control, these features can be accessed manually. 22 | * DebugGUI.SetGraphProperties(key, ...) - Set the properties of the graph with the provided key 23 | * DebugGUI.Graph(key, value) - Push a value to the graph 24 | * DebugGUI.LogPersistent(key, value) - Print a persistent log entry on screen 25 | * DebugGUI.Log(value) - Print a temporary log entry on screen 26 | * 27 | * See DebugGUI.cs for more info 28 | * 29 | * * * */ 30 | 31 | // Disable Field Unused warning 32 | #pragma warning disable 0414 33 | 34 | // Works with regular fields 35 | [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 0, autoScale: true)] 36 | float SinField; 37 | 38 | // As well as properties 39 | [DebugGUIGraph(min: -1, max: 1, r: 0, g: 1, b: 1, autoScale: true)] 40 | float CosProperty { get { return Mathf.Cos(time * 6); } } 41 | 42 | // Also works for expression-bodied properties 43 | [DebugGUIGraph(min: -1, max: 1, r: 1, g: 0.3f, b: 1)] 44 | float SinProperty => Mathf.Sin((time + Mathf.Pi / 2) * 6); 45 | 46 | // User inputs, print and graph in one! 47 | [DebugGUIPrint, DebugGUIGraph(group: 1, r: 1, g: 0.3f, b: 0.3f)] 48 | float mouseX; 49 | [DebugGUIPrint, DebugGUIGraph(group: 1, r: 0, g: 1, b: 0)] 50 | float mouseY; 51 | 52 | Queue deltaTimeBuffer = new(); 53 | double smoothDeltaTime => deltaTimeBuffer.Sum() / deltaTimeBuffer.Count; 54 | float time; 55 | float physicsTime; 56 | bool wasMouseDown; 57 | 58 | public override void _Ready() 59 | { 60 | 61 | // Init smooth DT 62 | for (int i = 0; i < 10; i++) 63 | { 64 | deltaTimeBuffer.Enqueue(0); 65 | } 66 | 67 | // Log (as opposed to LogPersistent) will disappear automatically after some time. 68 | DebugGUI.Log("Hello! I will disappear after some time!"); 69 | 70 | // Set up graph properties using our graph keys 71 | DebugGUI.SetGraphProperties("smoothFrameRate", "SmoothFPS", 0, 200, 2, new Color(0, 1, 1), false); 72 | DebugGUI.SetGraphProperties("frameRate", "FPS", 0, 200, 2, new Color(1, 0.5f, 1), false); 73 | DebugGUI.SetGraphProperties("fixedFrameRateSin", "FixedSin", -1, 1, 3, new Color(1, 1, 0), true); 74 | } 75 | 76 | public override void _Process(double delta) 77 | { 78 | time += (float)delta; 79 | 80 | // Update smooth delta time queue 81 | deltaTimeBuffer.Dequeue(); 82 | deltaTimeBuffer.Enqueue(delta); 83 | 84 | // Update the fields our attributes are graphing 85 | SinField = Mathf.Sin(time * 6); 86 | 87 | // Update graphed mouse XY values 88 | var mousePos = GetViewport().GetMousePosition(); 89 | var viewportRect = GetViewport().GetVisibleRect(); 90 | mouseX = Mathf.Clamp(mousePos.X, 0, viewportRect.Size.X); 91 | mouseY = Mathf.Clamp(mousePos.Y, 0, viewportRect.Size.Y); 92 | 93 | // Manual persistent logging 94 | DebugGUI.LogPersistent("smoothFrameRate", "SmoothFPS: " + (1 / smoothDeltaTime).ToString("F3")); 95 | DebugGUI.LogPersistent("frameRate", "FPS: " + (1 / delta).ToString("F3")); 96 | 97 | // Manual logging of mouse clicks 98 | if (Input.IsMouseButtonPressed(MouseButton.Left)) 99 | { 100 | if (!wasMouseDown) 101 | { 102 | wasMouseDown = true; 103 | DebugGUI.Log(string.Format( 104 | "Mouse down ({0}, {1})", 105 | mouseX.ToString("F3"), 106 | mouseY.ToString("F3") 107 | )); 108 | } 109 | } 110 | else 111 | { 112 | wasMouseDown = false; 113 | } 114 | 115 | if (smoothDeltaTime != 0) 116 | { 117 | DebugGUI.Graph("smoothFrameRate", 1 / (float)smoothDeltaTime); 118 | } 119 | if (delta != 0) 120 | { 121 | DebugGUI.Graph("frameRate", 1 / (float)delta); 122 | } 123 | 124 | if (Input.IsKeyPressed(Key.Space)) 125 | { 126 | QueueFree(); 127 | } 128 | } 129 | 130 | public override void _PhysicsProcess(double delta) 131 | { 132 | physicsTime += (float)delta; 133 | 134 | // Manual graphing 135 | DebugGUI.Graph("fixedFrameRateSin", Mathf.Sin(physicsTime * 6)); 136 | } 137 | 138 | public override void _ExitTree() 139 | { 140 | // Clean up our logs and graphs when this object leaves tree 141 | DebugGUI.RemoveGraph("frameRate"); 142 | DebugGUI.RemoveGraph("fixedFrameRateSin"); 143 | DebugGUI.RemoveGraph("smoothFrameRate"); 144 | 145 | DebugGUI.RemovePersistent("frameRate"); 146 | DebugGUI.RemovePersistent("smoothFrameRate"); 147 | } 148 | } -------------------------------------------------------------------------------- /addons/DebugGUI/Examples/DebugGUIExamples.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://cwxrep5n7yml0"] 2 | 3 | [ext_resource type="Script" path="res://addons/DebugGUI/Examples/DebugGUIExamples.cs" id="1_herxx"] 4 | [ext_resource type="Script" path="res://addons/DebugGUI/Examples/debugGUI_examples.gd" id="2_acgyp"] 5 | 6 | [node name="C# Example" type="Control"] 7 | layout_mode = 3 8 | anchors_preset = 15 9 | anchor_right = 1.0 10 | anchor_bottom = 1.0 11 | grow_horizontal = 2 12 | grow_vertical = 2 13 | script = ExtResource("1_herxx") 14 | 15 | [node name="gdscript example" type="Node" parent="."] 16 | script = ExtResource("2_acgyp") 17 | -------------------------------------------------------------------------------- /addons/DebugGUI/Examples/debugGUI_examples.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | func _ready(): 4 | DebugGUI.SetGraphProperties(self, "from gdscript", 0.0, 10.0, 1, Color.WHITE, true) 5 | DebugGUI.Log(self) 6 | DebugGUI.Log("This can be done from gdscript too!") 7 | 8 | func _process(_delta): 9 | DebugGUI.Graph(self, sin(Time.get_ticks_msec() / 100.0)) 10 | -------------------------------------------------------------------------------- /addons/DebugGUI/Windows/DebugGUIWindow.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | 4 | namespace WeavUtils 5 | { 6 | // Draggable window clamped to the corners 7 | public abstract partial class DebugGUIWindow : Control 8 | { 9 | protected const int outOfScreenClampPadding = 30; 10 | 11 | static bool dragInProgress; 12 | bool dragged; 13 | 14 | new public virtual Rect2 GetRect() 15 | { 16 | return base.GetRect(); 17 | } 18 | 19 | public override void _Input(InputEvent @event) 20 | { 21 | if (@event is InputEventMouseButton mb) 22 | { 23 | if (!dragInProgress && mb.Pressed && mb.ButtonIndex == MouseButton.Middle) 24 | { 25 | if (GetRect().HasPoint(mb.Position)) 26 | { 27 | dragged = true; 28 | dragInProgress = true; 29 | } 30 | } 31 | if (mb.IsReleased() && mb.ButtonIndex == MouseButton.Middle) 32 | { 33 | if (dragged) dragInProgress = false; 34 | dragged = false; 35 | } 36 | } 37 | 38 | if (@event is InputEventMouseMotion motion) 39 | { 40 | if (dragged) 41 | { 42 | Move(motion.Relative); 43 | } 44 | } 45 | } 46 | 47 | protected void Move(Vector2 delta = default) 48 | { 49 | Position += delta; 50 | 51 | var viewportRect = GetViewportRect(); 52 | 53 | // Limit graph window offset so we can't get lost off screen 54 | Position = Position.Clamp( 55 | -GetRect().Size + Vector2.One * outOfScreenClampPadding, 56 | viewportRect.Size - Vector2.One * outOfScreenClampPadding 57 | ); 58 | } 59 | 60 | } 61 | } -------------------------------------------------------------------------------- /addons/DebugGUI/Windows/GraphWindow.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using static DebugGUI.Settings; 6 | 7 | namespace WeavUtils 8 | { 9 | public partial class GraphWindow : DebugGUIWindow 10 | { 11 | const int graphLabelFontSize = 12; 12 | const int graphLabelPadding = 5; 13 | const int graphBlockPadding = 3; 14 | const int scrubberBackgroundWidth = 55; 15 | const int windowOutOfScreenPadding = 30; 16 | 17 | List graphs = new(); 18 | HashSet attributeContainers = new(); 19 | Dictionary typeInstanceCounts = new(); 20 | Dictionary graphDictionary = new(); 21 | Dictionary> attributeKeys = new(); 22 | Dictionary> debugGUIGraphFields = new(); 23 | Dictionary> debugGUIGraphProperties = new(); 24 | SortedDictionary> graphGroups = new(); 25 | 26 | bool freezeGraphs; 27 | float graphLabelBoxWidth; 28 | 29 | public override void _Ready() 30 | { 31 | Name = nameof(GraphWindow); 32 | 33 | // Register attributes of all nodes present at start 34 | // See also: ReinitializeAttributes() 35 | RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root); 36 | 37 | // Default to top right 38 | Position = new Vector2(GetViewportRect().Size.X - GetRect().Size.X, 0); 39 | } 40 | 41 | public override void _Process(double delta) 42 | { 43 | if (!Input.IsMouseButtonPressed(MouseButton.Left)) 44 | { 45 | freezeGraphs = false; 46 | } 47 | 48 | if (!freezeGraphs) 49 | { 50 | CallDeferred(nameof(PollGraphAttributes)); 51 | } 52 | 53 | QueueRedraw(); 54 | 55 | // Clean up any nodes queued to free 56 | CallDeferred(nameof(CleanUpDeletedAttributes)); 57 | } 58 | 59 | public override void _Draw() 60 | { 61 | int groupNum = 0; 62 | foreach (var group in graphGroups.Values) 63 | { 64 | DrawGraphGroup(group, groupNum); 65 | groupNum++; 66 | } 67 | } 68 | 69 | public void Graph(object key, float val) 70 | { 71 | if (!graphDictionary.ContainsKey(key)) 72 | { 73 | CreateGraph(key); 74 | } 75 | 76 | if (freezeGraphs) return; 77 | 78 | graphDictionary[key].Push(val); 79 | // Todo: optimize away? 80 | RecalculateGraphLabelWidth(); 81 | } 82 | 83 | public void CreateGraph(object key) 84 | { 85 | AddGraph(key, new GraphContainer(graphWidth)); 86 | RecalculateGraphLabelWidth(); 87 | } 88 | 89 | public void ClearGraph(object key) 90 | { 91 | if (graphDictionary.ContainsKey(key)) 92 | graphDictionary[key].Clear(); 93 | } 94 | 95 | public void RemoveGraph(object key) 96 | { 97 | if (graphDictionary.ContainsKey(key)) 98 | { 99 | var graph = graphDictionary[key]; 100 | graphs.Remove(graph); 101 | graphDictionary.Remove(key); 102 | graphGroups[graph.group].Remove(graph); 103 | if (graphGroups[graph.group].Count == 0) 104 | { 105 | graphGroups.Remove(graph.group); 106 | } 107 | RecalculateGraphLabelWidth(); 108 | } 109 | } 110 | 111 | public void SetGraphProperties(object key, string label, float min, float max, int group, Color color, bool autoScale) 112 | { 113 | if (graphDictionary.ContainsKey(key)) 114 | { 115 | RemoveGraph(key); 116 | } 117 | 118 | var graph = new GraphContainer(graphWidth, group); 119 | AddGraph(key, graph); 120 | 121 | graph.name = label; 122 | graph.SetMinMax(min, max); 123 | graph.color = color; 124 | graph.autoScale = autoScale; 125 | } 126 | 127 | public void ReinitializeAttributes() 128 | { 129 | // Clean up graphs 130 | List toRemove = new List(); 131 | foreach (var key in graphDictionary.Keys) 132 | { 133 | if (key is GraphAttributeKey) 134 | toRemove.Add(key); 135 | } 136 | foreach (var key in toRemove) 137 | { 138 | RemoveGraph(key); 139 | } 140 | 141 | attributeContainers = new(); 142 | debugGUIGraphFields = new(); 143 | debugGUIGraphProperties = new(); 144 | typeInstanceCounts = new(); 145 | attributeKeys = new(); 146 | 147 | RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root); 148 | } 149 | 150 | public string ToJson() 151 | { 152 | var data = new Godot.Collections.Array(); 153 | 154 | foreach (var node in graphs) 155 | { 156 | data.Add(node.ToDataVariant()); 157 | } 158 | 159 | return data.ToString(); 160 | } 161 | 162 | public override Rect2 GetRect() 163 | { 164 | RefreshRect(); 165 | return base.GetRect(); 166 | } 167 | 168 | private void AddGraph(object key, GraphContainer graph) 169 | { 170 | graph.OnLabelSizeChange += RefreshRect; 171 | 172 | graphDictionary.Add(key, graph); 173 | graphs.Add(graph); 174 | 175 | if (!graphGroups.ContainsKey(graph.group)) 176 | { 177 | graphGroups.Add(graph.group, new List()); 178 | } 179 | 180 | graphGroups[graph.group].Add(graph); 181 | 182 | RecalculateGraphLabelWidth(); 183 | } 184 | 185 | private void PollGraphAttributes() 186 | { 187 | foreach (var node in attributeContainers) 188 | { 189 | if (node != null && attributeKeys.ContainsKey(node)) 190 | { 191 | foreach (var key in attributeKeys[node]) 192 | { 193 | if (key.memberInfo is FieldInfo fieldInfo) 194 | { 195 | float? val = fieldInfo.GetValue(node) as float?; 196 | if (val != null) 197 | graphDictionary[key].Push(val.Value); 198 | } 199 | else if (key.memberInfo is PropertyInfo propertyInfo) 200 | { 201 | float? val = propertyInfo.GetValue(node, null) as float?; 202 | if (val != null) 203 | graphDictionary[key].Push(val.Value); 204 | } 205 | } 206 | } 207 | } 208 | } 209 | 210 | GraphContainer lastPressedGraphLabel; 211 | private void DrawGraphGroup(List group, int groupNum) 212 | { 213 | var mousePos = GetLocalMousePosition(); 214 | 215 | Vector2 graphBlockSize = new Vector2(graphWidth + graphBlockPadding, graphHeight + graphBlockPadding); 216 | 217 | var groupOrigin = new Vector2(0, graphBlockSize.Y * groupNum); 218 | var groupGraphRect = new Rect2( 219 | groupOrigin.X + graphLabelBoxWidth + graphBlockPadding, 220 | groupOrigin.Y, 221 | graphWidth, 222 | graphHeight 223 | ); 224 | 225 | // Label background 226 | DrawRect(new Rect2( 227 | groupOrigin.X, 228 | groupOrigin.Y, 229 | graphLabelBoxWidth, 230 | graphHeight), 231 | backgroundColor); 232 | 233 | // Graph background 234 | DrawRect(new Rect2( 235 | groupOrigin.X + graphBlockPadding + graphLabelBoxWidth, 236 | groupOrigin.Y, 237 | graphBlockSize.X, 238 | graphHeight), 239 | backgroundColor); 240 | 241 | // Magic padding offsets 242 | Vector2 textOrigin = groupOrigin + new Vector2(0, 14); 243 | Vector2 minMaxOrigin = groupOrigin + new Vector2(graphLabelBoxWidth - 10, 16); 244 | foreach (var graph in group) 245 | { 246 | var textSize = textFont.GetStringSize(graph.name, fontSize: graphLabelFontSize); 247 | textOrigin.Y += textSize.Y; 248 | var maxWidthOfMinMaxStrings = Mathf.Max( 249 | textFont.GetStringSize(graph.minString, fontSize: graphLabelFontSize).X, 250 | textFont.GetStringSize(graph.maxString, fontSize: graphLabelFontSize).X 251 | ); 252 | minMaxOrigin += Vector2.Left * (maxWidthOfMinMaxStrings + graphLabelPadding); 253 | 254 | // Label button logic 255 | var labelRect = new Rect2(textOrigin - textSize + new Vector2(graphLabelBoxWidth - (graphLabelPadding * 2), graphLabelPadding), textSize); 256 | // Enable disable 257 | var isHovered = labelRect.HasPoint(mousePos); 258 | var isPressed = isHovered && Input.IsMouseButtonPressed(MouseButton.Left); 259 | 260 | // Button click 261 | if (lastPressedGraphLabel == graph && !isPressed && isHovered) 262 | { 263 | graph.visible = !graph.visible; 264 | } 265 | 266 | if (isPressed) 267 | { 268 | lastPressedGraphLabel = graph; 269 | } 270 | else if (lastPressedGraphLabel == graph) 271 | { 272 | lastPressedGraphLabel = null; 273 | } 274 | 275 | var graphColor = graph.GetModifiedColor(isHovered); 276 | 277 | // Name 278 | DrawString( 279 | textFont, 280 | textOrigin - new Vector2(textSize.X + 10 - graphLabelBoxWidth, 0), 281 | graph.name, 282 | fontSize: graphLabelFontSize, 283 | modulate: graphColor, 284 | alignment: HorizontalAlignment.Right 285 | ); 286 | 287 | // Max 288 | DrawString( 289 | textFont, 290 | minMaxOrigin, 291 | graph.maxString, 292 | modulate: graphColor, 293 | fontSize: graphLabelFontSize, 294 | alignment: HorizontalAlignment.Right 295 | ); 296 | 297 | // Min 298 | DrawString( 299 | textFont, 300 | minMaxOrigin + new Vector2(0, graphHeight - 20), 301 | graph.minString, 302 | modulate: graphColor, 303 | fontSize: graphLabelFontSize, 304 | alignment: HorizontalAlignment.Right 305 | ); 306 | 307 | // Graph 308 | if (graph.visible) 309 | { 310 | graph.Draw(groupGraphRect, this); 311 | } 312 | } 313 | 314 | // Scrubber 315 | if (groupGraphRect.HasPoint(mousePos)) 316 | { 317 | if (Input.IsMouseButtonPressed(MouseButton.Left)) 318 | { 319 | freezeGraphs = true; 320 | } 321 | 322 | // Background 323 | Vector2 scrubberOrigin = new Vector2(mousePos.X, groupOrigin.Y); 324 | if (mousePos.X > groupGraphRect.End.X - scrubberBackgroundWidth) 325 | { 326 | scrubberOrigin.X -= scrubberBackgroundWidth; 327 | } 328 | 329 | var rect = new Rect2( 330 | scrubberOrigin, 331 | scrubberBackgroundWidth, 332 | graphHeight 333 | ); 334 | DrawRect(rect, backgroundColor); 335 | 336 | DrawLine( 337 | new Vector2( 338 | mousePos.X, 339 | groupOrigin.Y 340 | ), new Vector2( 341 | mousePos.X, 342 | groupOrigin.Y + graphHeight 343 | ), 344 | scrubberColor 345 | ); 346 | 347 | // Scrubber labels 348 | Vector2 textPos = scrubberOrigin + new Vector2(graphLabelPadding, graphLabelPadding * 3); 349 | var groupMousePosX = (mousePos.X - groupOrigin.X); 350 | int sampleIndex = (int)(groupGraphRect.Size.X - groupMousePosX + graphLabelBoxWidth + graphBlockPadding); 351 | foreach (GraphContainer graph in group) 352 | { 353 | var text = graph.GetValue(sampleIndex).ToString("F3"); 354 | DrawString( 355 | textFont, 356 | textPos, 357 | text, 358 | modulate: graph.color, 359 | fontSize: graphLabelFontSize 360 | ); 361 | textPos.Y += textFont.GetHeight(graphLabelFontSize); 362 | } 363 | } 364 | } 365 | 366 | private Rect2 GetGraphWindowRect() 367 | { 368 | return new Rect2( 369 | new Vector2(-graphLabelBoxWidth, 0) + Position, 370 | graphWidth + graphLabelBoxWidth + graphBlockPadding, 371 | (graphHeight + graphBlockPadding) * graphGroups.Count 372 | ); 373 | } 374 | 375 | private void RegisterAttributes(Node node) 376 | { 377 | foreach (Node child in node.GetChildren()) 378 | { 379 | GD.Print(child); 380 | RegisterAttributes(child); 381 | } 382 | 383 | Type nodeType = node.GetType(); 384 | 385 | HashSet uniqueAttributeContainers = new(); 386 | // Fields 387 | { 388 | // Retreive the fields from the mono instance 389 | FieldInfo[] objectFields = nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 390 | 391 | // search all fields/properties for the [DebugGUIVar] attribute 392 | for (int i = 0; i < objectFields.Length; i++) 393 | { 394 | DebugGUIGraphAttribute graphAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(DebugGUIGraphAttribute)) as DebugGUIGraphAttribute; 395 | 396 | if (graphAttribute != null) 397 | { 398 | // Can't cast to float so we don't bother registering it 399 | if (objectFields[i].GetValue(node) as float? == null) 400 | { 401 | GD.PrintErr(string.Format("Cannot cast {0}.{1} to float. This member will be ignored.", nodeType.Name, objectFields[i].Name)); 402 | continue; 403 | } 404 | 405 | uniqueAttributeContainers.Add(node); 406 | if (!debugGUIGraphFields.ContainsKey(nodeType)) 407 | debugGUIGraphFields.Add(nodeType, new HashSet()); 408 | if (!debugGUIGraphProperties.ContainsKey(nodeType)) 409 | debugGUIGraphProperties.Add(nodeType, new HashSet()); 410 | 411 | debugGUIGraphFields[nodeType].Add(objectFields[i]); 412 | GraphContainer graph = 413 | new GraphContainer(graphWidth, graphAttribute.group) 414 | { 415 | name = objectFields[i].Name, 416 | max = graphAttribute.max, 417 | min = graphAttribute.min, 418 | autoScale = graphAttribute.autoScale 419 | }; 420 | graph.OnLabelSizeChange += RefreshRect; 421 | if (!graphAttribute.color.Equals(default(Color))) 422 | graph.color = graphAttribute.color; 423 | 424 | var key = new GraphAttributeKey(objectFields[i]); 425 | if (!attributeKeys.ContainsKey(node)) 426 | attributeKeys.Add(node, new List()); 427 | attributeKeys[node].Add(key); 428 | 429 | AddGraph(key, graph); 430 | } 431 | } 432 | } 433 | 434 | // Properties 435 | { 436 | PropertyInfo[] objectProperties = nodeType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 437 | 438 | for (int i = 0; i < objectProperties.Length; i++) 439 | { 440 | if (Attribute.GetCustomAttribute(objectProperties[i], typeof(DebugGUIGraphAttribute)) is DebugGUIGraphAttribute graphAttribute) 441 | { 442 | // Can't cast to float so we don't bother registering it 443 | if (objectProperties[i].GetValue(node, null) as float? == null) 444 | { 445 | GD.PrintErr("Cannot cast " + objectProperties[i].Name + " to float. This member will be ignored."); 446 | continue; 447 | } 448 | 449 | uniqueAttributeContainers.Add(node); 450 | 451 | if (!debugGUIGraphFields.ContainsKey(nodeType)) 452 | debugGUIGraphFields.Add(nodeType, new HashSet()); 453 | if (!debugGUIGraphProperties.ContainsKey(nodeType)) 454 | debugGUIGraphProperties.Add(nodeType, new HashSet()); 455 | 456 | debugGUIGraphProperties[nodeType].Add(objectProperties[i]); 457 | 458 | GraphContainer graph = 459 | new GraphContainer(graphWidth, graphAttribute.group) 460 | { 461 | name = objectProperties[i].Name, 462 | max = graphAttribute.max, 463 | min = graphAttribute.min, 464 | autoScale = graphAttribute.autoScale 465 | }; 466 | graph.OnLabelSizeChange += RefreshRect; 467 | if (!graphAttribute.color.Equals(default(Color))) 468 | graph.color = graphAttribute.color; 469 | 470 | var key = new GraphAttributeKey(objectProperties[i]); 471 | if (!attributeKeys.ContainsKey(node)) 472 | attributeKeys.Add(node, new List()); 473 | attributeKeys[node].Add(key); 474 | 475 | AddGraph(key, graph); 476 | } 477 | } 478 | } 479 | 480 | foreach (var attributeContainer in uniqueAttributeContainers) 481 | { 482 | attributeContainers.Add(attributeContainer); 483 | Type type = attributeContainer.GetType(); 484 | if (!typeInstanceCounts.ContainsKey(type)) 485 | typeInstanceCounts.Add(type, 0); 486 | typeInstanceCounts[type]++; 487 | } 488 | } 489 | 490 | private void CleanUpDeletedAttributes() 491 | { 492 | // Clear out associated keys 493 | foreach (var node in attributeContainers) 494 | { 495 | if (node.IsQueuedForDeletion()) 496 | { 497 | var keys = attributeKeys[node]; 498 | foreach (var key in keys) 499 | { 500 | RemoveGraph(key); 501 | } 502 | attributeKeys.Remove(node); 503 | 504 | Type type = node.GetType(); 505 | typeInstanceCounts[type]--; 506 | if (typeInstanceCounts[type] == 0) 507 | { 508 | if (debugGUIGraphFields.ContainsKey(type)) 509 | debugGUIGraphFields.Remove(type); 510 | if (debugGUIGraphProperties.ContainsKey(type)) 511 | debugGUIGraphProperties.Remove(type); 512 | } 513 | } 514 | } 515 | 516 | // Finally clear out removed nodes 517 | attributeContainers.RemoveWhere(node => node.IsQueuedForDeletion()); 518 | } 519 | 520 | void RefreshRect() 521 | { 522 | var lastWidth = Size.X; 523 | RecalculateGraphLabelWidth(); 524 | Size = new Vector2( 525 | graphWidth + graphLabelBoxWidth + graphBlockPadding, 526 | (graphHeight + graphBlockPadding) * graphGroups.Count); 527 | // Grow to the left instead of right 528 | Position += new Vector2(lastWidth - Size.X, 0); 529 | } 530 | 531 | void RecalculateGraphLabelWidth() 532 | { 533 | float width = 0; 534 | foreach (var group in graphGroups.Values) 535 | { 536 | float minMaxWidth = graphLabelPadding; 537 | foreach (var graph in group) 538 | { 539 | // Names 540 | width = Mathf.Max(textFont.GetStringSize(graph.name, fontSize: graphLabelFontSize).X, width); 541 | 542 | // Minmax labels per group 543 | var maxWidthOfMinMaxStrings = Mathf.Max( 544 | textFont.GetStringSize(graph.minString, fontSize: graphLabelFontSize).X, 545 | textFont.GetStringSize(graph.maxString, fontSize: graphLabelFontSize).X 546 | ); 547 | minMaxWidth += maxWidthOfMinMaxStrings + graphLabelPadding; 548 | } 549 | width = Mathf.Max(minMaxWidth, width); 550 | } 551 | graphLabelBoxWidth = width + graphLabelPadding * 2; 552 | } 553 | 554 | private class GraphContainer 555 | { 556 | public Action OnLabelSizeChange; 557 | 558 | public string name; 559 | 560 | // Value at the top of the graph 561 | public float max = 1; 562 | // Value at the bottom of the graph 563 | public float min = 0; 564 | public bool autoScale; 565 | public Color color; 566 | // Graph order on screen 567 | public readonly int group; 568 | 569 | private int currentIndex; 570 | private readonly float[] values; 571 | private readonly Vector2[] graphPoints; 572 | 573 | public string minString = null; 574 | public string maxString = null; 575 | public bool visible = true; 576 | 577 | public Color GetModifiedColor(bool highlighted) 578 | { 579 | if (!highlighted && visible) return color; 580 | color.ToHsv(out float h, out float s, out float v); 581 | 582 | if (!visible) v *= 0.3f; 583 | if (highlighted) v *= (v > 0.9f ? 0.7f : 1.2f); 584 | return Color.FromHsv(h, s, v); 585 | } 586 | 587 | public void SetMinMax(float min, float max) 588 | { 589 | OnLabelSizeChange?.Invoke(); 590 | this.min = min; 591 | this.max = max; 592 | 593 | minString = min.ToString("F2"); 594 | maxString = max.ToString("F2"); 595 | } 596 | 597 | public GraphContainer(int width, int group = 0) 598 | { 599 | this.group = group; 600 | values = new float[width]; 601 | graphPoints = new Vector2[width]; 602 | SetMinMax(min, max); 603 | } 604 | 605 | // Add a data point to the beginning of the graph 606 | public void Push(float val) 607 | { 608 | if (autoScale && (val > max || val < min)) 609 | { 610 | SetMinMax(Mathf.Min(val, min), Mathf.Max(val, max)); 611 | } 612 | else 613 | { 614 | // Prevent drawing outside frame 615 | val = Mathf.Clamp(val, min, max); 616 | } 617 | 618 | values[currentIndex] = val; 619 | currentIndex = (currentIndex + 1) % values.Length; 620 | } 621 | 622 | public void Clear() 623 | { 624 | for (int i = 0; i < values.Length; i++) 625 | { 626 | values[i] = 0; 627 | } 628 | } 629 | 630 | public void Draw(Rect2 rect, CanvasItem canvasItem) 631 | { 632 | int num = values.Length; 633 | for (int i = 0; i < num; i++) 634 | { 635 | float value = values[Mod(currentIndex - i - 1, values.Length)]; 636 | // Note flipped inverse lerp min max to account for y = down in godot 637 | graphPoints[i] = new Vector2( 638 | rect.Position.X + (rect.Size.X * ((float)i / num)), 639 | rect.Position.Y + (Mathf.InverseLerp(max, min, value) * graphHeight) 640 | ); 641 | } 642 | 643 | canvasItem.DrawPolyline(graphPoints, color); 644 | } 645 | 646 | public float GetValue(int index) 647 | { 648 | return values[Mod(currentIndex + index, values.Length)]; 649 | } 650 | 651 | class DataExport 652 | { 653 | public string name; 654 | public float[] values; 655 | 656 | public DataExport(string name, float[] values) 657 | { 658 | this.name = name; 659 | this.values = values; 660 | } 661 | } 662 | 663 | public Variant ToDataVariant() 664 | { 665 | var vals = new float[values.Length]; 666 | for (int i = 0; i < vals.Length; i++) 667 | { 668 | vals[i] = values[Mod(currentIndex + i, values.Length)]; 669 | } 670 | 671 | var dict = new Godot.Collections.Dictionary(); 672 | dict.Add("name", name); 673 | dict.Add("values", vals); 674 | 675 | return dict; 676 | } 677 | 678 | private static int Mod(int n, int m) 679 | { 680 | return ((n % m) + m) % m; 681 | } 682 | } 683 | 684 | public class GraphAttributeKey 685 | { 686 | public MemberInfo memberInfo; 687 | public GraphAttributeKey(MemberInfo memberInfo) 688 | { 689 | this.memberInfo = memberInfo; 690 | } 691 | } 692 | } 693 | } -------------------------------------------------------------------------------- /addons/DebugGUI/Windows/LogWindow.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using System.Reflection; 6 | using static DebugGUI.Settings; 7 | 8 | namespace WeavUtils 9 | { 10 | public partial class LogWindow : DebugGUIWindow 11 | { 12 | List transientLogs = new(); 13 | HashSet attributeContainers = new(); 14 | Dictionary typeCache = new(); 15 | Dictionary typeInstanceCounts = new(); 16 | Dictionary persistentLogs = new(); 17 | Dictionary> attributeKeys = new(); 18 | Dictionary> debugGUIPrintFields = new(); 19 | Dictionary> debugGUIPrintProperties = new(); 20 | 21 | StringBuilder persistentLogStringBuilder = new(); 22 | 23 | double time; 24 | 25 | public override void _Ready() 26 | { 27 | Name = nameof(LogWindow); 28 | 29 | // Register attributes of all nodes present at start 30 | // See also: ReinitializeAttributes() 31 | RegisterAttributes(((SceneTree)Engine.GetMainLoop()).Root); 32 | } 33 | 34 | public override void _Process(double delta) 35 | { 36 | time += delta; 37 | 38 | // Clean up expired logs 39 | int expiredCt = 0; 40 | for (int i = 0; i < transientLogs.Count; i++) 41 | { 42 | if (transientLogs[i].expiryTime <= time) 43 | { 44 | expiredCt++; 45 | } 46 | } 47 | transientLogs.RemoveRange(0, expiredCt); 48 | 49 | if (debugGUIPrintFields.Count + debugGUIPrintProperties.Count + persistentLogs.Count + transientLogs.Count > 0) 50 | { 51 | QueueRedraw(); 52 | } 53 | 54 | CallDeferred(nameof(CleanUpDeletedAttributes)); 55 | } 56 | 57 | public override void _Draw() 58 | { 59 | persistentLogStringBuilder.Clear(); 60 | 61 | var viewportRect = GetViewportRect(); 62 | var lineHeight = textFont.GetHeight(); 63 | 64 | foreach (var node in attributeContainers) 65 | { 66 | Type type = typeCache[node]; 67 | if (debugGUIPrintFields.ContainsKey(type)) 68 | { 69 | foreach (var field in debugGUIPrintFields[type]) 70 | { 71 | persistentLogStringBuilder.AppendLine($"{node.Name} {field.Name}: {field.GetValue(node)}"); 72 | } 73 | } 74 | if (debugGUIPrintProperties.ContainsKey(type)) 75 | { 76 | foreach (var property in debugGUIPrintProperties[type]) 77 | { 78 | persistentLogStringBuilder.AppendLine($"{node.Name} {property.Name}: {property.GetValue(node, null)}"); 79 | } 80 | } 81 | } 82 | 83 | foreach (var log in persistentLogs.Values) 84 | { 85 | persistentLogStringBuilder.AppendLine(log); 86 | } 87 | 88 | if (persistentLogStringBuilder.Length > 0 && transientLogs.Count != 0) 89 | { 90 | persistentLogStringBuilder.AppendLine(); 91 | } 92 | 93 | var persistentLogStr = persistentLogStringBuilder.ToString(); 94 | var textSize = textFont.GetMultilineStringSize(persistentLogStr); 95 | Size = textSize + Vector2.One * 10; 96 | 97 | float transientLogY = textSize.Y; 98 | 99 | foreach (var log in transientLogs) 100 | { 101 | var size = textFont.GetStringSize(log.text); 102 | textSize = new Vector2( 103 | Mathf.Max(size.X, textSize.X), 104 | textSize.Y + size.Y 105 | ); 106 | } 107 | 108 | 109 | var backgroundRect = new Rect2(Vector2.Zero, textSize.X + 10, textSize.Y + 10); 110 | DrawRect(backgroundRect, backgroundColor); 111 | // Draw a little bit extra for the draggable area 112 | DrawRect(new Rect2(0, 0, GetRect().Size), new Color(1, 1, 1, 0.05f)); 113 | 114 | // Draw persistent logs 115 | DrawMultilineString(textFont, new Vector2(0, textFont.GetHeight()), persistentLogStr); 116 | 117 | // Draw separator 118 | if (persistentLogStringBuilder.Length > 0 && transientLogs.Count != 0) 119 | { 120 | DrawDashedLine( 121 | new Vector2(0, transientLogY), 122 | new Vector2(textSize.X, transientLogY), 123 | Colors.White, 124 | 2 125 | ); 126 | } 127 | 128 | // Draw transient logs 129 | for (int i = transientLogs.Count - 1; i >= 0; i--) 130 | { 131 | transientLogY += lineHeight; 132 | // Clear up transient logs going off screen 133 | if (transientLogY > viewportRect.Size.Y) 134 | { 135 | transientLogs.RemoveRange(0, i + 1); 136 | break; 137 | } 138 | 139 | var log = transientLogs[i]; 140 | DrawString(textFont, new Vector2(0, transientLogY), log.text); 141 | } 142 | } 143 | 144 | public void Log(string str) 145 | { 146 | transientLogs.Add(new TransientLog(str, time + temporaryLogLifetime)); 147 | } 148 | 149 | public void LogPersistent(object key, string message) 150 | { 151 | if (persistentLogs.ContainsKey(key)) 152 | persistentLogs[key] = message; 153 | else 154 | persistentLogs.Add(key, message); 155 | } 156 | 157 | public void RemovePersistent(object key) 158 | { 159 | if (persistentLogs.ContainsKey(key)) 160 | { 161 | persistentLogs.Remove(key); 162 | } 163 | } 164 | 165 | public void ClearPersistent() 166 | { 167 | persistentLogs.Clear(); 168 | } 169 | 170 | public void ReinitializeAttributes() 171 | { 172 | // Clean up graphs 173 | List toRemove = new List(); 174 | foreach (var key in persistentLogs.Keys) 175 | { 176 | if (key is PersistentLogAttributeKey) 177 | toRemove.Add(key); 178 | } 179 | foreach (var key in toRemove) 180 | { 181 | persistentLogs.Remove(key); 182 | } 183 | 184 | attributeContainers = new(); 185 | debugGUIPrintFields = new(); 186 | debugGUIPrintProperties = new(); 187 | typeInstanceCounts = new(); 188 | attributeKeys = new(); 189 | } 190 | 191 | private void RegisterAttributes(Node node) 192 | { 193 | foreach (Node child in node.GetChildren()) 194 | { 195 | GD.Print(child); 196 | RegisterAttributes(child); 197 | } 198 | 199 | Type nodeType = node.GetType(); 200 | 201 | 202 | HashSet uniqueAttributeContainers = new(); 203 | 204 | // Fields 205 | { 206 | // Retreive the fields from the mono instance 207 | FieldInfo[] objectFields = nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 208 | 209 | // search all fields/properties for the [DebugGUIVar] attribute 210 | for (int i = 0; i < objectFields.Length; i++) 211 | { 212 | DebugGUIPrintAttribute printAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(DebugGUIPrintAttribute)) as DebugGUIPrintAttribute; 213 | 214 | if (printAttribute != null) 215 | { 216 | uniqueAttributeContainers.Add(node); 217 | typeCache[node] = node.GetType(); 218 | if (!debugGUIPrintFields.ContainsKey(nodeType)) 219 | { 220 | debugGUIPrintFields.Add(nodeType, new HashSet()); 221 | } 222 | 223 | GD.Print("Found field " + objectFields[i].Name + " on " + node); 224 | debugGUIPrintFields[nodeType].Add(objectFields[i]); 225 | } 226 | } 227 | } 228 | 229 | // Properties 230 | { 231 | PropertyInfo[] objectProperties = nodeType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 232 | 233 | for (int i = 0; i < objectProperties.Length; i++) 234 | { 235 | if (Attribute.GetCustomAttribute(objectProperties[i], typeof(DebugGUIPrintAttribute)) is DebugGUIPrintAttribute) 236 | { 237 | uniqueAttributeContainers.Add(node); 238 | typeCache[node] = node.GetType(); 239 | 240 | if (!debugGUIPrintProperties.ContainsKey(nodeType)) 241 | { 242 | debugGUIPrintProperties.Add(nodeType, new HashSet()); 243 | } 244 | debugGUIPrintProperties[nodeType].Add(objectProperties[i]); 245 | } 246 | } 247 | } 248 | 249 | foreach (var attributeContainer in uniqueAttributeContainers) 250 | { 251 | attributeContainers.Add(attributeContainer); 252 | Type type = attributeContainer.GetType(); 253 | if (!typeInstanceCounts.ContainsKey(type)) 254 | typeInstanceCounts.Add(type, 0); 255 | typeInstanceCounts[type]++; 256 | } 257 | } 258 | 259 | private void CleanUpDeletedAttributes() 260 | { 261 | // Clear out associated keys 262 | foreach (var node in attributeContainers) 263 | { 264 | if (node.IsQueuedForDeletion()) 265 | { 266 | attributeKeys.Remove(node); 267 | typeCache.Remove(node); 268 | 269 | Type type = node.GetType(); 270 | typeInstanceCounts[type]--; 271 | if (typeInstanceCounts[type] == 0) 272 | { 273 | if (debugGUIPrintFields.ContainsKey(type)) 274 | debugGUIPrintFields.Remove(type); 275 | if (debugGUIPrintProperties.ContainsKey(type)) 276 | debugGUIPrintProperties.Remove(type); 277 | } 278 | } 279 | } 280 | 281 | // Finally clear out removed nodes 282 | attributeContainers.RemoveWhere(node => node.IsQueuedForDeletion()); 283 | } 284 | 285 | private struct TransientLog 286 | { 287 | public string text; 288 | public double expiryTime; 289 | 290 | public TransientLog(string text, double expiryTime) 291 | { 292 | this.text = text; 293 | this.expiryTime = expiryTime; 294 | } 295 | } 296 | 297 | // Wrapper to differentiate attributes from 298 | // manually created logs 299 | public class PersistentLogAttributeKey 300 | { 301 | public MemberInfo memberInfo; 302 | public PersistentLogAttributeKey(MemberInfo memberInfo) 303 | { 304 | this.memberInfo = memberInfo; 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /addons/DebugGUI/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="DebugGUI" 4 | description="" 5 | author="WeaverDev" 6 | version="1.0" 7 | script="DebugGUISettingsInitializer.cs" 8 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [DebugGUI] 12 | 13 | Settings/enableGraphs=true 14 | Settings/enableLogs=true 15 | Settings/backgroundColor=Color(0, 0, 0, 0.7) 16 | Settings/scrubberColor=Color(1, 1, 0, 0.7) 17 | Settings/graphWidth=300 18 | Settings/graphHeight=100 19 | Settings/temporaryLogLifetime=5.0 20 | 21 | [application] 22 | 23 | config/name="DebugGUIGraph" 24 | run/main_scene="res://addons/DebugGUI/Examples/DebugGUIExamples.tscn" 25 | config/features=PackedStringArray("4.1", "C#", "Mobile") 26 | 27 | [autoload] 28 | 29 | DebugGUI="*res://addons/DebugGUI/DebugGUI.cs" 30 | 31 | [dotnet] 32 | 33 | project/assembly_name="DebugGUIGraph" 34 | 35 | [editor_plugins] 36 | 37 | enabled=PackedStringArray("res://addons/DebugGUI/plugin.cfg") 38 | 39 | [rendering] 40 | 41 | renderer/rendering_method="mobile" 42 | --------------------------------------------------------------------------------