using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Text; using TMPro; using UnityEngine; namespace UltraCombos { /// /// Read-only runtime monitor that renders selected component fields and properties /// into a single TextMeshProUGUI component using TMP rich-text colour tags. /// Drag the target TextMeshProUGUI into the Display Text field in the Inspector. /// [ExecuteAlways] public class DebugMonitorHUD : MonoBehaviour { // ── Data ────────────────────────────────────────────────────────────────── [Serializable] public class Entry { public UnityEngine.Object target; public string groupLabel = string.Empty; /// Field/property names written by the custom Editor. public List fields = new(); } [Header("Display")] [SerializeField] private TextMeshProUGUI displayText; [Header("General")] [Tooltip("How many times per second the text is refreshed. 0 = every frame.")] [Range(0f, 60f)] public float refreshRate = 20f; [Header("Entries")] public List entries = new(); // ── Runtime state ───────────────────────────────────────────────────────── private float nextRefreshTime; private readonly StringBuilder stringBuilder = new(); // ── TMP colour tokens ───────────────────────────────────────────────────── private const string colHeader = "#FFC940"; private const string colLabel = "#AAAAAA"; private const string colFloat = "#7EC8E3"; private const string colInt = "#7EC8E3"; private const string colBoolTrue = "#7EE37E"; private const string colBoolFalse = "#E37E7E"; private const string colString = "#E3D47E"; private const string colEnum = "#C8A0E3"; private const string colVec = "#E3C87E"; private const string colColor = "#E3E3E3"; private const string colReadonly = "#666666"; // ── Unity callbacks ─────────────────────────────────────────────────────── private void Update() { if (displayText == null) { return; } if (refreshRate > 0f && Time.unscaledTime < nextRefreshTime) { return; } nextRefreshTime = Time.unscaledTime + (refreshRate > 0f ? 1f / refreshRate : 0f); RebuildText(); } // ── Text building ───────────────────────────────────────────────────────── private void RebuildText() { stringBuilder.Clear(); var first = true; foreach (var entry in entries) { if (entry == null || entry.target == null || entry.fields.Count == 0) { continue; } if (!first) { stringBuilder.AppendLine(); } first = false; AppendEntry(entry); } displayText.SetText(stringBuilder); } private void AppendEntry(Entry entry) { if (!string.IsNullOrEmpty(entry.groupLabel)) { stringBuilder .Append("') .Append(entry.groupLabel) .AppendLine(""); } var type = entry.target.GetType(); const BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; const BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance; foreach (var name in entry.fields) { // Resolve by name — check fields first, then properties. var mi = (MemberInfo)type.GetField(name, fieldFlags) ?? type.GetProperty(name, propFlags); if (mi == null) { continue; } try { AppendMember(entry.target, mi); } catch { // Skip members that throw during reflection. } } } private void AppendMember(object obj, MemberInfo mi) { var label = NicifyName(mi.Name); var val = GetMemberValue(mi, obj); var ftype = GetMemberType(mi); stringBuilder .Append("') .Append(label) .Append(" "); AppendValue(val, ftype); stringBuilder.AppendLine(); } private void AppendValue(object val, Type ftype) { if (ftype == typeof(bool)) { var b = (bool)val; stringBuilder .Append("') .Append(b ? "true" : "false") .Append(""); return; } if (ftype == typeof(float)) { stringBuilder .Append("') .Append(((float)val).ToString("G5", CultureInfo.InvariantCulture)) .Append(""); return; } if (ftype == typeof(int)) { stringBuilder .Append("') .Append(((int)val).ToString(CultureInfo.InvariantCulture)) .Append(""); return; } if (ftype == typeof(string)) { stringBuilder .Append("\"") .Append(val as string ?? string.Empty) .Append("\""); return; } if (ftype == typeof(Vector2)) { var v = (Vector2)val; stringBuilder .Append("(") .Append(v.x.ToString("G4", CultureInfo.InvariantCulture)).Append(", ") .Append(v.y.ToString("G4", CultureInfo.InvariantCulture)) .Append(")"); return; } if (ftype == typeof(Vector3)) { var v = (Vector3)val; stringBuilder .Append("(") .Append(v.x.ToString("G4", CultureInfo.InvariantCulture)).Append(", ") .Append(v.y.ToString("G4", CultureInfo.InvariantCulture)).Append(", ") .Append(v.z.ToString("G4", CultureInfo.InvariantCulture)) .Append(")"); return; } if (ftype == typeof(Color)) { var c = (Color)val; var hex = ColorUtility.ToHtmlStringRGBA(c); // Coloured swatch character followed by the hex value. stringBuilder .Append("■ ") .Append("#").Append(hex).Append(""); return; } if (ftype.IsEnum) { stringBuilder .Append("') .Append(val?.ToString() ?? "?") .Append(""); return; } // Fallback: display the ToString() value as read-only italic text. stringBuilder .Append("") .Append(val?.ToString() ?? "(null)") .Append(""); } // ── Reflection helpers ──────────────────────────────────────────────────── private static Type GetMemberType(MemberInfo mi) => mi is FieldInfo fi ? fi.FieldType : ((PropertyInfo)mi).PropertyType; private static object GetMemberValue(MemberInfo mi, object obj) => mi is FieldInfo fi ? fi.GetValue(obj) : ((PropertyInfo)mi).GetValue(obj); /// Converts a camelCase or _prefixed field name to a readable label. private static string NicifyName(string s) { 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(); } } }