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