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