You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
430 lines
15 KiB
430 lines
15 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace UltraCombos
|
|
{
|
|
/// <summary>
|
|
/// Runtime settings panel. Press the configured toggle key (default F1) to show or hide.
|
|
/// Drag any Component into the Entries list via the Inspector, select the fields to expose,
|
|
/// and the panel lets you tweak them live at runtime.
|
|
/// </summary>
|
|
public class DebugSettingsGUI : MonoBehaviour
|
|
{
|
|
// ── Data ──────────────────────────────────────────────────────────────────
|
|
|
|
[Serializable]
|
|
public class Entry
|
|
{
|
|
public UnityEngine.Object target;
|
|
public string groupLabel = string.Empty;
|
|
|
|
/// <summary>Field names written by the custom Editor.</summary>
|
|
public List<string> fields = new();
|
|
}
|
|
|
|
[Header("General")]
|
|
public KeyCode toggleKey = KeyCode.F1;
|
|
public Rect initialRect = new Rect(10, 10, 360, 520);
|
|
|
|
[Header("Entries")]
|
|
public List<Entry> entries = new();
|
|
|
|
// ── Runtime state ─────────────────────────────────────────────────────────
|
|
|
|
private bool isVisible;
|
|
private Rect windowRect;
|
|
private Vector2 scrollPos;
|
|
|
|
// Styles — lazy-initialised inside OnGUI so GUI.skin is available.
|
|
private bool stylesInitialized;
|
|
private GUIStyle headerStyle;
|
|
private GUIStyle subBoxStyle;
|
|
private GUIStyle labelStyle;
|
|
private GUIStyle readonlyStyle;
|
|
|
|
// ── Unity callbacks ───────────────────────────────────────────────────────
|
|
|
|
private void Awake()
|
|
{
|
|
windowRect = initialRect;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (Input.GetKeyDown(toggleKey))
|
|
{
|
|
isVisible = !isVisible;
|
|
}
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
if (!isVisible)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureStyles();
|
|
windowRect = GUILayout.Window(
|
|
GetInstanceID(), windowRect, DrawWindow,
|
|
$" Debug Settings [{toggleKey}: toggle]");
|
|
}
|
|
|
|
// ── Window ────────────────────────────────────────────────────────────────
|
|
|
|
private void DrawWindow(int id)
|
|
{
|
|
scrollPos = GUILayout.BeginScrollView(scrollPos);
|
|
|
|
var first = true;
|
|
foreach (var e in entries)
|
|
{
|
|
if (e == null || e.target == null || e.fields.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
if (!first)
|
|
{
|
|
GUILayout.Space(6);
|
|
}
|
|
first = false;
|
|
DrawEntry(e);
|
|
}
|
|
|
|
GUILayout.EndScrollView();
|
|
GUI.DragWindow(new Rect(0, 0, 10000, 22));
|
|
}
|
|
|
|
private void DrawEntry(Entry entry)
|
|
{
|
|
if (!string.IsNullOrEmpty(entry.groupLabel))
|
|
{
|
|
GUILayout.Label(entry.groupLabel, headerStyle);
|
|
}
|
|
|
|
var type = entry.target.GetType();
|
|
const BindingFlags flags =
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
|
|
|
foreach (var name in entry.fields)
|
|
{
|
|
var fi = type.GetField(name, flags);
|
|
if (fi == null)
|
|
{
|
|
continue;
|
|
}
|
|
try
|
|
{
|
|
DrawField(entry.target, fi);
|
|
}
|
|
catch
|
|
{
|
|
// Skip fields that throw during reflection.
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Per-type drawing ──────────────────────────────────────────────────────
|
|
|
|
private void DrawField(object obj, FieldInfo fi)
|
|
{
|
|
var val = fi.GetValue(obj);
|
|
var ftype = fi.FieldType;
|
|
var label = NicifyName(fi.Name);
|
|
|
|
if (ftype == typeof(bool))
|
|
{
|
|
DrawBool(obj, fi, (bool)val, label);
|
|
}
|
|
else if (ftype == typeof(float))
|
|
{
|
|
DrawFloat(obj, fi, (float)val, label);
|
|
}
|
|
else if (ftype == typeof(int))
|
|
{
|
|
DrawInt(obj, fi, (int)val, label);
|
|
}
|
|
else if (ftype == typeof(string))
|
|
{
|
|
DrawString(obj, fi, (string)val, label);
|
|
}
|
|
else if (ftype == typeof(Vector2))
|
|
{
|
|
DrawVector2(obj, fi, (Vector2)val, label);
|
|
}
|
|
else if (ftype == typeof(Vector3))
|
|
{
|
|
DrawVector3(obj, fi, (Vector3)val, label);
|
|
}
|
|
else if (ftype == typeof(Color))
|
|
{
|
|
DrawColor(obj, fi, (Color)val, label);
|
|
}
|
|
else if (ftype.IsEnum)
|
|
{
|
|
DrawEnum(obj, fi, val, ftype, label);
|
|
}
|
|
else
|
|
{
|
|
// Unsupported type — read-only display.
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
GUILayout.Label(val?.ToString() ?? "(null)", readonlyStyle);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawBool(object obj, FieldInfo fi, bool val, string label)
|
|
{
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
var next = GUILayout.Toggle(val, val ? "✓" : string.Empty);
|
|
if (next != val)
|
|
{
|
|
fi.SetValue(obj, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawFloat(object obj, FieldInfo fi, float val, string label)
|
|
{
|
|
var range = fi.GetCustomAttribute<RangeAttribute>();
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
|
|
float next;
|
|
if (range != null)
|
|
{
|
|
next = GUILayout.HorizontalSlider(val, range.min, range.max);
|
|
GUILayout.Label(next.ToString("F2"), GUILayout.Width(44));
|
|
}
|
|
else
|
|
{
|
|
var s = GUILayout.TextField(val.ToString("G5"));
|
|
next = float.TryParse(s, out var p) ? p : val;
|
|
}
|
|
|
|
if (!Mathf.Approximately(next, val))
|
|
{
|
|
fi.SetValue(obj, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawInt(object obj, FieldInfo fi, int val, string label)
|
|
{
|
|
var range = fi.GetCustomAttribute<RangeAttribute>();
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
|
|
int next;
|
|
if (range != null)
|
|
{
|
|
next = Mathf.RoundToInt(GUILayout.HorizontalSlider(val, range.min, range.max));
|
|
GUILayout.Label(next.ToString(), GUILayout.Width(32));
|
|
}
|
|
else
|
|
{
|
|
var s = GUILayout.TextField(val.ToString());
|
|
next = int.TryParse(s, out var p) ? p : val;
|
|
}
|
|
|
|
if (next != val)
|
|
{
|
|
fi.SetValue(obj, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawString(object obj, FieldInfo fi, string val, string label)
|
|
{
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
var next = GUILayout.TextField(val ?? string.Empty);
|
|
if (next != val)
|
|
{
|
|
fi.SetValue(obj, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawVector2(object obj, FieldInfo fi, Vector2 val, string label)
|
|
{
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
GUILayout.Label("X", GUILayout.Width(14));
|
|
var x = ParseFloat(GUILayout.TextField(val.x.ToString("G4"), GUILayout.Width(52)), val.x);
|
|
GUILayout.Label("Y", GUILayout.Width(14));
|
|
var y = ParseFloat(GUILayout.TextField(val.y.ToString("G4"), GUILayout.Width(52)), val.y);
|
|
var next = new Vector2(x, y);
|
|
if (next != val)
|
|
{
|
|
fi.SetValue(obj, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawVector3(object obj, FieldInfo fi, Vector3 val, string label)
|
|
{
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
GUILayout.Label("X", GUILayout.Width(14));
|
|
var x = ParseFloat(GUILayout.TextField(val.x.ToString("G4"), GUILayout.Width(40)), val.x);
|
|
GUILayout.Label("Y", GUILayout.Width(14));
|
|
var y = ParseFloat(GUILayout.TextField(val.y.ToString("G4"), GUILayout.Width(40)), val.y);
|
|
GUILayout.Label("Z", GUILayout.Width(14));
|
|
var z = ParseFloat(GUILayout.TextField(val.z.ToString("G4"), GUILayout.Width(40)), val.z);
|
|
var next = new Vector3(x, y, z);
|
|
if (next != val)
|
|
{
|
|
fi.SetValue(obj, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawColor(object obj, FieldInfo fi, Color val, string label)
|
|
{
|
|
GUILayout.Label(label, headerStyle);
|
|
using (new GUILayout.VerticalScope(subBoxStyle))
|
|
{
|
|
var r = SliderChannel("R", val.r);
|
|
var g = SliderChannel("G", val.g);
|
|
var b = SliderChannel("B", val.b);
|
|
var a = SliderChannel("A", val.a);
|
|
var next = new Color(r, g, b, a);
|
|
if (next != val)
|
|
{
|
|
fi.SetValue(obj, next);
|
|
}
|
|
}
|
|
}
|
|
|
|
private float SliderChannel(string ch, float val)
|
|
{
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(ch, GUILayout.Width(16));
|
|
var next = GUILayout.HorizontalSlider(val, 0f, 1f);
|
|
GUILayout.Label(next.ToString("F2"), GUILayout.Width(36));
|
|
return next;
|
|
}
|
|
}
|
|
|
|
private void DrawEnum(object obj, FieldInfo fi, object val, Type type, string label)
|
|
{
|
|
var names = Enum.GetNames(type);
|
|
var values = Enum.GetValues(type);
|
|
var cur = Array.IndexOf(values, val);
|
|
|
|
using (new GUILayout.HorizontalScope())
|
|
{
|
|
GUILayout.Label(label, labelStyle, GUILayout.Width(150));
|
|
|
|
if (names.Length <= 4)
|
|
{
|
|
var next = GUILayout.SelectionGrid(cur, names, names.Length);
|
|
if (next != cur)
|
|
{
|
|
fi.SetValue(obj, values.GetValue(next));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Cycle through with prev / next buttons for long enums.
|
|
if (GUILayout.Button("◀", GUILayout.Width(24)))
|
|
{
|
|
cur = (cur - 1 + names.Length) % names.Length;
|
|
}
|
|
GUILayout.Label(names[Mathf.Clamp(cur, 0, names.Length - 1)]);
|
|
if (GUILayout.Button("▶", GUILayout.Width(24)))
|
|
{
|
|
cur = (cur + 1) % names.Length;
|
|
}
|
|
fi.SetValue(obj, values.GetValue(Mathf.Clamp(cur, 0, values.Length - 1)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
private static float ParseFloat(string s, float fallback)
|
|
=> float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : fallback;
|
|
|
|
/// <summary>Converts a camelCase or _prefixed field name to a readable label.</summary>
|
|
private static string NicifyName(string s)
|
|
{
|
|
// Strip leading underscores and "m_" prefix.
|
|
var start = 0;
|
|
while (start < s.Length && s[start] == '_')
|
|
{
|
|
start++;
|
|
}
|
|
if (s.Length > start + 1 && s[start] == 'm' && s[start + 1] == '_')
|
|
{
|
|
start += 2;
|
|
}
|
|
|
|
var sb = new StringBuilder();
|
|
for (var i = start; i < s.Length; i++)
|
|
{
|
|
var c = s[i];
|
|
if (i == start)
|
|
{
|
|
sb.Append(char.ToUpper(c));
|
|
continue;
|
|
}
|
|
if (char.IsUpper(c) && !char.IsUpper(s[i - 1]))
|
|
{
|
|
sb.Append(' ');
|
|
}
|
|
sb.Append(c);
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
// ── Styles — OnGUI-safe lazy initialisation ────────────────────────────────
|
|
|
|
private void EnsureStyles()
|
|
{
|
|
if (stylesInitialized)
|
|
{
|
|
return;
|
|
}
|
|
stylesInitialized = true;
|
|
|
|
headerStyle = new GUIStyle(GUI.skin.label)
|
|
{
|
|
fontStyle = FontStyle.Bold,
|
|
normal = { textColor = new Color(1f, 0.85f, 0.3f) }
|
|
};
|
|
|
|
subBoxStyle = new GUIStyle(GUI.skin.box)
|
|
{
|
|
padding = new RectOffset(6, 6, 3, 3),
|
|
margin = new RectOffset(2, 2, 0, 0)
|
|
};
|
|
|
|
labelStyle = new GUIStyle(GUI.skin.label)
|
|
{
|
|
normal = { textColor = new Color(0.85f, 0.85f, 0.85f) }
|
|
};
|
|
|
|
readonlyStyle = new GUIStyle(GUI.skin.label)
|
|
{
|
|
normal = { textColor = new Color(0.55f, 0.55f, 0.55f) },
|
|
fontStyle = FontStyle.Italic
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|