UI tuner: fix stale readback and World-Space label lever

Two defects in the tuner, both visible in the in-game logs:

1. AFTER lines read Canvas.scaleFactor immediately after writing, but that is
   recomputed in CanvasScaler.Update(), so it reported last frame's value
   (showed 1.3333 after ref was set to 1350). Now prints the mathematically
   expected scale instead.

2. WORLD_SCALE set CanvasScaler.scaleFactor on WorldSpace canvases, which
   HandleWorldCanvas() overwrites with dynamicPixelsPerUnit every frame - the
   log showed 'APPLIED scaleFactor 1 -> 0.8' followed by effectiveScale=1.0.
   World-space label size is driven by the canvas transform localScale, so it
   now scales that, stashing a baseline child so repeated runs don't compound.

Also documents that MATCH must stay 1.0: match .8 + ref 1350 yields 1.281 vs
1.067 at match 1, largely cancelling the shrink.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 16:04:56 -04:00
co-authored by Claude Opus 4.8
parent ae994c27e6
commit b9e68fd284
+56 -37
View File
@@ -1,19 +1,29 @@
// LIVE UI TUNER — paste into UnityExplorer's C# Console. Edit knobs, Run, look, repeat. // LIVE UI TUNER — paste into UnityExplorer's C# Console. Edit knobs, Run, look, repeat.
// Nothing persists; a scene change or relaunch reverts everything. // Nothing persists across a relaunch.
//
// TWO canvas families, each needing a DIFFERENT lever:
// //
// This game has TWO canvas families, tuned by DIFFERENT properties:
// 1. ScreenSpaceOverlay + ScaleWithScreenSize ("Canvas", "Canvas in front") // 1. ScreenSpaceOverlay + ScaleWithScreenSize ("Canvas", "Canvas in front")
// -> the HUD. Controlled by matchWidthOrHeight + referenceResolution. // The HUD. Lever = matchWidthOrHeight + referenceResolution.
// 2. WorldSpace + ConstantPixelSize ("NamePlate", etc.) // KEEP MATCH AT 1.0 — lowering it re-introduces width-based scaling and
// -> in-world labels. Unity IGNORES referenceResolution here; // undoes the shrink (match .8 + ref 1350 => 1.281, vs match 1 => 1.067).
// ONLY scaleFactor does anything.
// //
// The tuner prints the knob values it actually used, so you can confirm your edit took. // 2. WorldSpace canvases ("NamePlate", etc.)
// In-world labels. CanvasScaler.scaleFactor DOES NOT WORK here: for a
// WorldSpace canvas, CanvasScaler.HandleWorldCanvas() overwrites
// scaleFactor with dynamicPixelsPerUnit every frame. The real lever is
// the canvas transform's localScale.
//
// NOTE: Canvas.scaleFactor is recomputed in CanvasScaler.Update(), so reading it
// right after writing returns LAST frame's value. We print the mathematically
// expected scale instead, and echo BEFORE state (which is accurate) each run.
// ---- KNOBS ----------------------------------------------------------------- // ---- KNOBS -----------------------------------------------------------------
float MATCH = 1.0f; // ScaleWithScreenSize only. 0=scale by width, 1=by height float MATCH = 1.0f; // HUD. Leave at 1.0.
float HUD_SCALE = 0.85f; // ScaleWithScreenSize only. <1 = smaller HUD + more room float HUD_SCALE = 0.85f; // HUD. <1 = smaller HUD + more layout room.
float WORLD_SCALE = 0.8f; // ConstantPixelSize only. <1 = smaller in-world labels float WORLD_SCALE = 0f; // In-world labels. 0 = don't touch.
// Absolute multiplier vs the BASELINE printed below.
// Run once with 0 to read baseline, then e.g. 0.8f.
bool LIST_ONLY = false; // true = report only, change nothing bool LIST_ONLY = false; // true = report only, change nothing
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -25,39 +35,48 @@ log.AppendLine($"SCREEN {UnityEngine.Screen.width}x{UnityEngine.Screen.height} "
foreach (var cs in UnityEngine.Object.FindObjectsOfType<UnityEngine.UI.CanvasScaler>()) foreach (var cs in UnityEngine.Object.FindObjectsOfType<UnityEngine.UI.CanvasScaler>())
{ {
var cv = cs.GetComponent<UnityEngine.Canvas>(); var cv = cs.GetComponent<UnityEngine.Canvas>();
string render = cv ? cv.renderMode.ToString() : "?"; if (cv == null) continue;
log.AppendLine($"BEFORE {cs.name} [{cs.uiScaleMode}/{render}] ref={cs.referenceResolution} " +
$"match={cs.matchWidthOrHeight} scaleFactor={cs.scaleFactor}");
if (!LIST_ONLY) if (cv.renderMode == UnityEngine.RenderMode.WorldSpace)
{ {
if (cs.uiScaleMode == UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize) var ls = cv.transform.localScale;
log.AppendLine($"WORLD {cs.name}: localScale={ls} (dynPPU={cs.dynamicPixelsPerUnit})");
if (!LIST_ONLY && WORLD_SCALE > 0f)
{
// Store the pristine scale on first touch so repeated runs don't compound.
var marker = cv.transform.Find("__uwp_baseline");
float bx;
if (marker == null)
{
var go = new UnityEngine.GameObject("__uwp_baseline");
go.transform.SetParent(cv.transform, false);
go.transform.localScale = ls; // stash baseline here
bx = ls.x;
}
else bx = marker.localScale.x;
cv.transform.localScale = new UnityEngine.Vector3(bx, bx, bx) * WORLD_SCALE;
log.AppendLine($" APPLIED localScale baseline={bx:F5} -> {cv.transform.localScale}");
}
continue;
}
log.AppendLine($"BEFORE {cs.name} [{cs.uiScaleMode}/{cv.renderMode}] ref={cs.referenceResolution} " +
$"match={cs.matchWidthOrHeight}");
if (!LIST_ONLY && cs.uiScaleMode == UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize)
{ {
cs.matchWidthOrHeight = MATCH; cs.matchWidthOrHeight = MATCH;
// effective scale = screenH / refH => refH = 1080 / HUD_SCALE
if (HUD_SCALE > 0f && System.Math.Abs(HUD_SCALE - 1f) > 0.0001f) if (HUD_SCALE > 0f && System.Math.Abs(HUD_SCALE - 1f) > 0.0001f)
{ cs.referenceResolution = new UnityEngine.Vector2(cs.referenceResolution.x, 1080f / HUD_SCALE);
var r = cs.referenceResolution;
cs.referenceResolution = new UnityEngine.Vector2(r.x, 1080f / HUD_SCALE);
log.AppendLine($" APPLIED ref {r} -> {cs.referenceResolution}");
}
}
else if (cs.uiScaleMode == UnityEngine.UI.CanvasScaler.ScaleMode.ConstantPixelSize)
{
// referenceResolution is ignored in this mode; scaleFactor is the only lever
if (WORLD_SCALE > 0f && System.Math.Abs(WORLD_SCALE - 1f) > 0.0001f)
{
float old = cs.scaleFactor;
cs.scaleFactor = WORLD_SCALE;
log.AppendLine($" APPLIED scaleFactor {old} -> {cs.scaleFactor}");
}
}
}
float sf = cv ? cv.scaleFactor : 0f; // expected scale (accurate NOW; Canvas.scaleFactor lags by a frame)
if (sf > 0) var r = cs.referenceResolution;
log.AppendLine($"AFTER {cs.name}: effectiveScale={sf:F4} " + double exp = System.Math.Pow(UnityEngine.Screen.width / r.x, 1.0 - MATCH) *
$"logicalCanvas={UnityEngine.Screen.width / sf:F0}x{UnityEngine.Screen.height / sf:F0}"); System.Math.Pow(UnityEngine.Screen.height / r.y, MATCH);
log.AppendLine($" APPLIED ref={r} match={MATCH} => expectedScale={exp:F4} " +
$"logicalCanvas={UnityEngine.Screen.width / exp:F0}x{UnityEngine.Screen.height / exp:F0}");
}
} }
UnityExplorer.ExplorerCore.Log(log.ToString()); UnityExplorer.ExplorerCore.Log(log.ToString());