Merge branch 'master' of gitlab.com:MaddoScientisto/cirnogodot

This commit is contained in:
MaddoScientisto 2024-12-30 09:10:07 +01:00
commit ce2d4231bf
20 changed files with 1804 additions and 54 deletions

View file

@ -1,8 +1,9 @@
[gd_scene load_steps=8 format=3 uid="uid://v8s3kubgb2qg"]
[gd_scene load_steps=9 format=3 uid="uid://v8s3kubgb2qg"]
[ext_resource type="Texture2D" uid="uid://b4ynnb14mb4uq" path="res://Sprites/Reisen.png" id="1_4w8mj"]
[ext_resource type="Script" path="res://Scripts/Enemy.cs" id="1_lpwdj"]
[ext_resource type="PackedScene" uid="uid://cuixq5ex0j40h" path="res://Scenes/enemyBullet.tscn" id="2_ogldd"]
[ext_resource type="PackedScene" uid="uid://crry0rgk7a8sm" path="res://Scenes/Weapons/BaseWeapon.tscn" id="4_2k1dv"]
[sub_resource type="AtlasTexture" id="AtlasTexture_2brqc"]
atlas = ExtResource("1_4w8mj")
@ -17,12 +18,11 @@ radius = 4.0
[sub_resource type="CircleShape2D" id="CircleShape2D_v711r"]
radius = 85.0529
[node name="Enemy" type="Area2D" groups=["Destroyable"]]
[node name="Enemy" type="Area2D" node_paths=PackedStringArray("EquippedWeapon") groups=["Destroyable"]]
collision_layer = 16
collision_mask = 9
script = ExtResource("1_lpwdj")
BulletScene = ExtResource("2_ogldd")
BulletSpeed = 50.0
EquippedWeapon = NodePath("Weapon")
metadata/_edit_group_ = true
[node name="Sprite2D" type="Sprite2D" parent="."]
@ -48,6 +48,11 @@ shape = SubResource("CircleShape2D_v711r")
wait_time = 0.4
one_shot = true
[node name="Weapon" parent="." instance=ExtResource("4_2k1dv")]
BulletScene = ExtResource("2_ogldd")
BulletCapacity = 4
BulletSpeed = 50.0
[connection signal="area_entered" from="." to="." method="_on_area_entered"]
[connection signal="area_entered" from="PlayerDetection" to="." method="_on_player_detection_area_entered"]
[connection signal="area_exited" from="PlayerDetection" to="." method="_on_player_detection_area_exited"]

View file

@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3 uid="uid://crry0rgk7a8sm"]
[ext_resource type="Script" path="res://Scripts/Weapon.cs" id="1_f5iec"]
[node name="Weapon" type="Sprite2D"]
script = ExtResource("1_f5iec")
[node name="Muzzle" type="Marker2D" parent="."]
position = Vector2(5, 0)
[node name="ShootTimer" type="Timer" parent="."]
one_shot = true

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=27 format=3 uid="uid://bghghp5ep4w2j"]
[gd_scene load_steps=28 format=3 uid="uid://bghghp5ep4w2j"]
[ext_resource type="Script" path="res://Scripts/PlayerMovement.cs" id="1_m27vu"]
[ext_resource type="Texture2D" uid="uid://la06powu57hu" path="res://Sprites/Cirno_Big.png" id="2_bwf6x"]
@ -8,6 +8,7 @@
[ext_resource type="Script" path="res://Scenes/CameraTarget.gd" id="5_cxvyt"]
[ext_resource type="PackedScene" uid="uid://cfb3nsay84xdb" path="res://Scenes/crosshair.tscn" id="6_l43rf"]
[ext_resource type="Script" path="res://Scenes/InteractionController.cs" id="7_uvgjg"]
[ext_resource type="PackedScene" uid="uid://crry0rgk7a8sm" path="res://Scenes/Weapons/BaseWeapon.tscn" id="9_wblq0"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ai4rh"]
size = Vector2(6, 6)
@ -141,7 +142,7 @@ radius = 17.2627
[sub_resource type="CircleShape2D" id="CircleShape2D_e6woi"]
radius = 1.41421
[node name="Player" type="CharacterBody2D" node_paths=PackedStringArray("Muzzle") groups=["Destroyable", "player"]]
[node name="Player" type="CharacterBody2D" node_paths=PackedStringArray("Muzzle", "EquippedWeapon") groups=["Destroyable", "player"]]
collision_layer = 2
collision_mask = 99
script = ExtResource("1_m27vu")
@ -152,7 +153,7 @@ SelectorScene = ExtResource("3_8wt6s")
GameOverScene = "res://Scenes/GameOver.tscn"
Muzzle = NodePath("Muzzle")
Health = 32.0
RateOfFire = 0.1
EquippedWeapon = NodePath("Weapon")
metadata/_edit_group_ = true
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
@ -205,5 +206,12 @@ collision_mask = 128
shape = SubResource("CircleShape2D_e6woi")
debug_color = Color(1, 0.00817797, 0.0443347, 0.42)
[node name="Weapon" parent="." instance=ExtResource("9_wblq0")]
BulletScene = ExtResource("2_ov36d")
RateOfFire = 0.1
BulletCapacity = 100
ReloadTime = 0.4
BulletSpeed = 300.0
[connection signal="area_entered" from="InteractionController" to="." method="_on_interaction_controller_area_entered"]
[connection signal="area_entered" from="DamageHitBox" to="." method="_on_damage_hit_box_area_entered"]

View file

@ -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,9 +301,6 @@ 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)

View file

@ -8,26 +8,19 @@ public partial class Enemy : Area2D, IDestructible
private InteractionController _cachedPlayer;
private EnemyState _currentState = EnemyState.Idle;
[Export]
public PackedScene BulletScene { get; set; }
[Export] public float Health = 4f;
[Export] public double RateOfFire = 0.4f;
[Export] public float BulletSpeed = 100f;
[Export] public Weapon EquippedWeapon;
private float _currentHealth = 0f;
private bool _isDestroyed = false;
private Timer _cooldownTimer;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
_currentHealth = Health;
_cooldownTimer = GetNode<Timer>("./ShootTimer");
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
@ -64,20 +57,42 @@ public partial class Enemy : Area2D, IDestructible
return;
}
if (_cooldownTimer.IsStopped() && IsPlayerInSight())
if (IsPlayerInSight())
{
// SHOOT
var bullet = this.CreateChild<Bullet>(BulletScene);
// var bullet = BulletScene.Instantiate<Bullet>();
// Owner.AddChild(bullet);
// bullet.Transform = this.GlobalTransform;
// bullet.Position = this.Position;
bullet.SetDirection((_cachedPlayer.GlobalPosition - this.GlobalPosition).Normalized());
bullet.Speed = BulletSpeed;
_cooldownTimer.Start(RateOfFire);
Shoot();
}
}
private void Shoot()
{
if (EquippedWeapon == null) return;
EquippedWeapon.ShootDirection = (_cachedPlayer.GlobalPosition - this.GlobalPosition).Normalized();
EquippedWeapon.Shoot();
// // SHOOT
// var bullet = this.CreateChild<Bullet>(BulletScene);
// // var bullet = BulletScene.Instantiate<Bullet>();
// // Owner.AddChild(bullet);
// // bullet.Transform = this.GlobalTransform;
// // bullet.Position = this.Position;
// bullet.SetDirection((_cachedPlayer.GlobalPosition - this.GlobalPosition).Normalized());
// bullet.Speed = BulletSpeed;
//
// _ammo -= 1;
//
// if (_ammo <= 0)
// {
// _ammo = BulletCount;
// _cooldownTimer.Start(ReloadTime);
// }
// else
// {
// _cooldownTimer.Start(RateOfFire);
// }
}
private bool IsPlayerInSight()
{
var spaceState = GetWorld2D().DirectSpaceState;

View file

@ -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()
{

View file

@ -11,9 +11,6 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible
[Export]
public float CrosshairDistance { get; set; } = 10f;
[Export]
public PackedScene BulletScene { get; set; }
[Export]
public PackedScene SelectorScene { get; set; }
@ -35,16 +32,13 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible
private Sprite2D _crosshair;
private Timer _cooldownTimer;
[Export] public float Health = 4f;
[Export] public Weapon EquippedWeapon;
[DebugGUIPrint]
private float _currentHealth = 0f;
[Export] public double RateOfFire = 0.4f;
[Export] public float BulletSpeed = 300f;
private bool _isDestroyed = false;
public override void _Ready()
@ -53,7 +47,6 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible
_animatedSprite = GetNode<AnimatedSprite2D>("./Smoothing2D/AnimatedSprite2D");
_crosshair = GetNode<Sprite2D>("./Smoothing2D/Crosshair");
_cooldownTimer = GetNode<Timer>("./ShootTimer");
_movementDirection = Vector2.Zero;
_facingDirection = Vector2.Zero;
@ -99,13 +92,18 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible
private void HandleShoot()
{
if (!Input.IsActionJustPressed("shoot")) return;
//Debug.WriteLine("Shoot");
var bullet = BulletScene.Instantiate<Bullet>();
Owner.AddChild(bullet);
bullet.Transform = Muzzle.GlobalTransform;
bullet.Position = this.Position;
bullet.SetDirection(this._facingDirection);
if (EquippedWeapon == null) return;
if (!Input.IsActionPressed("shoot")) return;
EquippedWeapon.ShootDirection = this._facingDirection;
EquippedWeapon.Shoot();
// //Debug.WriteLine("Shoot");
// var bullet = BulletScene.Instantiate<Bullet>();
// Owner.AddChild(bullet);
// bullet.Transform = Muzzle.GlobalTransform;
// bullet.Position = this.Position;
// bullet.SetDirection(this._facingDirection);
}
private void SetAnimation()

84
Scripts/Weapon.cs Normal file
View file

@ -0,0 +1,84 @@
using Godot;
using System;
using Cirno.Scripts;
public partial class Weapon : Node2D
{
[Export]
public PackedScene BulletScene { get; set; }
[Export]
public Marker2D Muzzle { get; set; }
[Export] public double RateOfFire = 0.4f;
[Export] public int BulletCapacity = 20;
[Export] public double ReloadTime = 1.0f;
[Export] public float BulletSpeed = 100f;
[Export] public bool AutoReload = true;
[Export] public bool InfiniteAmmo = true;
public int Ammo { get; set; } = 0;
public int LoadedAmmo { get; private set; }
public Vector2 ShootDirection { get; set; } = Vector2.Zero;
private Timer _cooldownTimer;
private Marker2D _muzzle;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
_muzzle = GetNode<Marker2D>("./Muzzle");
_cooldownTimer = GetNode<Timer>("./ShootTimer");
}
public void Reload()
{
_cooldownTimer.Start(ReloadTime);
if (InfiniteAmmo)
{
LoadedAmmo = BulletCapacity;
}
else
{
// TODO: Calculate subtraction, etc
LoadedAmmo = BulletCapacity;
}
}
public void Shoot()
{
// Waiting on reload or Rate of Fire cooldown?
if (!_cooldownTimer.IsStopped())
{
return;
}
// Out of ammo?
if (LoadedAmmo <= 0)
{
if (AutoReload)
{
Reload();
}
return;
}
// TODO: Shoot at muzzle position, need to provide a way to turn it, on a radius?
var bullet = this.CreateChild<Bullet>(BulletScene);
bullet.SetDirection(ShootDirection);
bullet.Speed = BulletSpeed;
LoadedAmmo -= 1;
_cooldownTimer.Start(RateOfFire);
}
}

View 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;
}
}

View file

@ -0,0 +1,4 @@
using System;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class DebugGUIPrintAttribute : Attribute { }

317
addons/DebugGUI/DebugGUI.cs Normal file
View file

@ -0,0 +1,317 @@
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)
{
CanvasLayer canvasLayer = new();
canvasLayer.AddChild(graphWindow = new());
AddChild(canvasLayer);
}
if (Settings.enableGraphs)
{
CanvasLayer canvasLayer = new();
canvasLayer.AddChild(logWindow = new());
AddChild(canvasLayer);
}
}
}

View 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

View 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");
}
}

View 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")

View 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))

View 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
);
}
}
}

View 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;
}
}
}
}

View 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;
}
}
}
}

View file

@ -0,0 +1,7 @@
[plugin]
name="DebugGUI"
description=""
author="WeaverDev"
version="1.0"
script="DebugGUISettingsInitializer.cs"

View file

@ -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]