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.
 
 
 

285 lines
10 KiB

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text;
using TMPro;
using UnityEngine;
namespace UltraCombos
{
/// <summary>
/// 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.
/// </summary>
[ExecuteAlways]
public class DebugMonitorHUD : MonoBehaviour
{
// ── Data ──────────────────────────────────────────────────────────────────
[Serializable]
public class Entry
{
public UnityEngine.Object target;
public string groupLabel = string.Empty;
/// <summary>Field/property names written by the custom Editor.</summary>
public List<string> 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<Entry> 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("<b><color=").Append(colHeader).Append('>')
.Append(entry.groupLabel)
.AppendLine("</color></b>");
}
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("<color=").Append(colLabel).Append('>')
.Append(label)
.Append("</color> ");
AppendValue(val, ftype);
stringBuilder.AppendLine();
}
private void AppendValue(object val, Type ftype)
{
if (ftype == typeof(bool))
{
var b = (bool)val;
stringBuilder
.Append("<color=").Append(b ? colBoolTrue : colBoolFalse).Append('>')
.Append(b ? "true" : "false")
.Append("</color>");
return;
}
if (ftype == typeof(float))
{
stringBuilder
.Append("<color=").Append(colFloat).Append('>')
.Append(((float)val).ToString("G5", CultureInfo.InvariantCulture))
.Append("</color>");
return;
}
if (ftype == typeof(int))
{
stringBuilder
.Append("<color=").Append(colInt).Append('>')
.Append(((int)val).ToString(CultureInfo.InvariantCulture))
.Append("</color>");
return;
}
if (ftype == typeof(string))
{
stringBuilder
.Append("<color=").Append(colString).Append(">\"")
.Append(val as string ?? string.Empty)
.Append("\"</color>");
return;
}
if (ftype == typeof(Vector2))
{
var v = (Vector2)val;
stringBuilder
.Append("<color=").Append(colVec).Append(">(")
.Append(v.x.ToString("G4", CultureInfo.InvariantCulture)).Append(", ")
.Append(v.y.ToString("G4", CultureInfo.InvariantCulture))
.Append(")</color>");
return;
}
if (ftype == typeof(Vector3))
{
var v = (Vector3)val;
stringBuilder
.Append("<color=").Append(colVec).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(")</color>");
return;
}
if (ftype == typeof(Color))
{
var c = (Color)val;
var hex = ColorUtility.ToHtmlStringRGBA(c);
// Coloured swatch character followed by the hex value.
stringBuilder
.Append("<color=#").Append(hex).Append(">■</color> ")
.Append("<color=").Append(colColor).Append(">#").Append(hex).Append("</color>");
return;
}
if (ftype.IsEnum)
{
stringBuilder
.Append("<color=").Append(colEnum).Append('>')
.Append(val?.ToString() ?? "?")
.Append("</color>");
return;
}
// Fallback: display the ToString() value as read-only italic text.
stringBuilder
.Append("<color=").Append(colReadonly).Append("><i>")
.Append(val?.ToString() ?? "(null)")
.Append("</i></color>");
}
// ── 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);
/// <summary>Converts a camelCase or _prefixed field name to a readable label.</summary>
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();
}
}
}