using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text;
using UnityEngine;
namespace UltraCombos
{
///
/// 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.
///
public class DebugSettingsGUI : MonoBehaviour
{
// ── Data ──────────────────────────────────────────────────────────────────
[Serializable]
public class Entry
{
public UnityEngine.Object target;
public string groupLabel = string.Empty;
/// Field names written by the custom Editor.
public List fields = new();
}
[Header("General")]
public KeyCode toggleKey = KeyCode.F1;
public Rect initialRect = new Rect(10, 10, 360, 520);
[Header("Entries")]
public List 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();
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();
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;
/// Converts a camelCase or _prefixed field name to a readable label.
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
};
}
}
}