feat(ui): debug overlay + refined input controls
Adds the first on-screen HUD for the dev client plus today's mouse-control refinements. Also lands yesterday's scenery-alignment changes that were left uncommitted in the working tree. Overlay: - BitmapFont rasterizes a system TTF via StbTrueTypeSharp into a 512x512 R8 atlas at startup (Consolas on Windows, DejaVu/Menlo fallbacks) - TextRenderer batches 2D quads in screen-space with ortho projection; one shader + two draw calls (rect then text) for panel backgrounds under glyphs - DebugOverlay composes info / stats / compass / help panels on top of the 3D scene; toggles via F1/F4/F5/F6; transient toasts for key events - DebugLineRenderer and its shaders (carried over from the scenery work) are properly committed in this commit Controls: - Per-mode mouse sensitivity (Chase 0.15, Fly 1.0, Orbit 1.0); F8/F9 to adjust the active mode multiplicatively (x1.2) - Hold RMB to free-orbit the chase camera around the player; release stays at the new angle (no snap-back) - Mouse-wheel zooms chase distance between 2m and 40m - Chase pitch widened to [-0.7, 1.4] so mouse-Y tilts both ways from the default neutral angle Scenery alignment (carried from yesterday's session): - ShadowObjectRegistry AllEntriesForDebug + Scale field - SceneryGenerator uses ACViewer's OnRoad polygon test + baseLoc + set_heading rotation - BSPQuery dispatchers accept localToWorld so normals/offsets transform correctly per part - TransitionTypes.CylinderCollision rewritten with wall-slide + push-out - PhysicsDataCache caches visual-mesh AABB for scenery that lacks physics Setup bounds
This commit is contained in:
parent
6b4e7569a3
commit
ff325abd7b
20 changed files with 2734 additions and 268 deletions
330
src/AcDream.App/Rendering/DebugOverlay.cs
Normal file
330
src/AcDream.App/Rendering/DebugOverlay.cs
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Screen-space debug HUD. Composes panels on top of the 3D scene using a
|
||||
/// <see cref="TextRenderer"/> + <see cref="BitmapFont"/>. Panels can be
|
||||
/// toggled independently (info / stats / controls-help / compass).
|
||||
///
|
||||
/// The overlay is stateless w.r.t. game state — callers populate a
|
||||
/// <see cref="Snapshot"/> each frame and pass it to <see cref="Draw"/>.
|
||||
/// </summary>
|
||||
public sealed class DebugOverlay
|
||||
{
|
||||
private readonly TextRenderer _text;
|
||||
private readonly BitmapFont _font;
|
||||
|
||||
public bool ShowInfoPanel { get; set; } = true;
|
||||
public bool ShowStatsPanel { get; set; } = true;
|
||||
public bool ShowHelpPanel { get; set; } = false;
|
||||
public bool ShowCompass { get; set; } = true;
|
||||
|
||||
// Toast state for transient notifications (e.g. "wireframes off").
|
||||
private string? _toastText;
|
||||
private Vector4 _toastColor = White;
|
||||
private float _toastTimeLeft;
|
||||
|
||||
private static readonly Vector4 White = new(1f, 1f, 1f, 1f);
|
||||
private static readonly Vector4 Green = new(0.4f, 0.95f, 0.4f, 1f);
|
||||
private static readonly Vector4 Yellow = new(1f, 0.9f, 0.3f, 1f);
|
||||
private static readonly Vector4 Red = new(1f, 0.4f, 0.35f, 1f);
|
||||
private static readonly Vector4 Cyan = new(0.4f, 0.85f, 1f, 1f);
|
||||
private static readonly Vector4 Grey = new(0.7f, 0.7f, 0.75f, 1f);
|
||||
private static readonly Vector4 PanelBg = new(0f, 0f, 0f, 0.55f);
|
||||
private static readonly Vector4 PanelBorder = new(0.15f, 0.15f, 0.2f, 0.8f);
|
||||
|
||||
/// <summary>Per-frame state snapshot from the caller. See <see cref="Draw"/>.</summary>
|
||||
public readonly record struct Snapshot(
|
||||
float Fps,
|
||||
float FrameTimeMs,
|
||||
Vector3 PlayerPos,
|
||||
float HeadingDeg,
|
||||
uint CellId,
|
||||
bool OnGround,
|
||||
bool InPlayerMode,
|
||||
bool InFlyMode,
|
||||
float VerticalVelocity,
|
||||
int EntityCount,
|
||||
int AnimatedCount,
|
||||
int LandblocksVisible,
|
||||
int LandblocksTotal,
|
||||
int ShadowObjectCount,
|
||||
float NearestObjDist,
|
||||
string NearestObjLabel,
|
||||
bool Colliding,
|
||||
bool DebugWireframes,
|
||||
int StreamingRadius,
|
||||
float MouseSensitivity,
|
||||
float ChaseDistance,
|
||||
bool RmbOrbit);
|
||||
|
||||
public DebugOverlay(TextRenderer text, BitmapFont font)
|
||||
{
|
||||
_text = text;
|
||||
_font = font;
|
||||
}
|
||||
|
||||
/// <summary>Show a short message in the center-top for <paramref name="durationSec"/> seconds.</summary>
|
||||
public void Toast(string message, float durationSec = 1.5f, Vector4? color = null)
|
||||
{
|
||||
_toastText = message;
|
||||
_toastColor = color ?? Yellow;
|
||||
_toastTimeLeft = durationSec;
|
||||
}
|
||||
|
||||
/// <summary>Advance toast timer. Call once per frame with dt in seconds.</summary>
|
||||
public void Update(float dt)
|
||||
{
|
||||
if (_toastTimeLeft > 0f)
|
||||
{
|
||||
_toastTimeLeft -= dt;
|
||||
if (_toastTimeLeft <= 0f)
|
||||
_toastText = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Draw(Snapshot s, Vector2 screenSize)
|
||||
{
|
||||
_text.Begin(screenSize);
|
||||
|
||||
if (ShowInfoPanel) DrawInfoPanel(s);
|
||||
if (ShowStatsPanel) DrawStatsPanel(s, screenSize);
|
||||
if (ShowCompass) DrawCompass(s, screenSize);
|
||||
if (ShowHelpPanel) DrawHelpPanel(screenSize);
|
||||
DrawHintBar(screenSize);
|
||||
DrawToast(screenSize);
|
||||
|
||||
_text.Flush(_font);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Info panel — top-left: mode, position, heading, ground, nearest-obj.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawInfoPanel(Snapshot s)
|
||||
{
|
||||
var lines = new List<(string text, Vector4 color)>();
|
||||
|
||||
string modeLabel = s.InPlayerMode ? "PLAYER" : (s.InFlyMode ? "FLY " : "ORBIT ");
|
||||
var modeColor = s.InPlayerMode ? Yellow : (s.InFlyMode ? Cyan : Grey);
|
||||
lines.Add(($"[{modeLabel}] wireframes {(s.DebugWireframes ? "ON " : "OFF")}",
|
||||
modeColor));
|
||||
|
||||
lines.Add(($"Pos {s.PlayerPos.X,8:F1} {s.PlayerPos.Y,8:F1} {s.PlayerPos.Z,8:F2}", White));
|
||||
lines.Add(($"Head {s.HeadingDeg,5:F0} deg Cell 0x{s.CellId:X8}", White));
|
||||
|
||||
var gColor = s.OnGround ? Green : Yellow;
|
||||
string vzStr = s.VerticalVelocity >= 0
|
||||
? $"+{s.VerticalVelocity,4:F2}"
|
||||
: $"{s.VerticalVelocity,5:F2}";
|
||||
lines.Add(($"Grnd {(s.OnGround ? "yes" : "NO ")} vZ {vzStr}", gColor));
|
||||
|
||||
var nColor = s.Colliding ? Red : White;
|
||||
string nearDist = float.IsPositiveInfinity(s.NearestObjDist) ? " --- " : $"{s.NearestObjDist,4:F1}m";
|
||||
lines.Add(($"Near {nearDist} {s.NearestObjLabel}", nColor));
|
||||
|
||||
var cColor = s.Colliding ? Red : Green;
|
||||
lines.Add(($"Coll {(s.Colliding ? "BLOCKED" : "free ")}", cColor));
|
||||
|
||||
if (s.InPlayerMode)
|
||||
{
|
||||
string orbitTag = s.RmbOrbit ? " [RMB orbit]" : "";
|
||||
lines.Add(($"Cam dist {s.ChaseDistance,4:F1}m{orbitTag}",
|
||||
s.RmbOrbit ? Cyan : White));
|
||||
}
|
||||
lines.Add(($"Sens {s.MouseSensitivity:F3}x (F8 slower / F9 faster)", Grey));
|
||||
|
||||
DrawPanel(10f, 10f, lines);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Stats panel — top-right: fps, frame time, landblock/entity counters.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawStatsPanel(Snapshot s, Vector2 screenSize)
|
||||
{
|
||||
var lines = new List<(string text, Vector4 color)>
|
||||
{
|
||||
($"{s.Fps,5:F0} fps {s.FrameTimeMs,5:F1} ms", Green),
|
||||
($"lb {s.LandblocksVisible,3}/{s.LandblocksTotal,3} visible", White),
|
||||
($"ent {s.EntityCount,4} anim {s.AnimatedCount,3}", White),
|
||||
($"coll {s.ShadowObjectCount,5} radius {s.StreamingRadius}", White),
|
||||
};
|
||||
|
||||
float pad = 10f;
|
||||
float panelW = MeasureMax(lines) + 2 * InnerPad;
|
||||
DrawPanel(screenSize.X - panelW - pad, pad, lines, panelW);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Compass — bottom-center: arrow indicating heading, cardinal labels.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawCompass(Snapshot s, Vector2 screenSize)
|
||||
{
|
||||
// Simple linear compass strip across 180° of horizon at the top-center.
|
||||
const float stripW = 360f;
|
||||
const float stripH = 20f;
|
||||
float cx = screenSize.X * 0.5f;
|
||||
float top = 8f;
|
||||
float left = cx - stripW * 0.5f;
|
||||
|
||||
_text.DrawRect(left, top, stripW, stripH, PanelBg);
|
||||
_text.DrawRectOutline(left, top, stripW, stripH, PanelBorder);
|
||||
|
||||
// Mark every 30° of yaw, labelled with cardinal letters at N/E/S/W.
|
||||
// Heading 0 = +X (east). We show 180° wide, ±90° from current heading.
|
||||
float h = NormalizeDeg(s.HeadingDeg);
|
||||
for (int d = 0; d < 360; d += 15)
|
||||
{
|
||||
float rel = NormalizeDegSigned(d - h);
|
||||
if (rel < -90 || rel > 90) continue;
|
||||
float x = cx + rel / 90f * (stripW * 0.5f - 6f);
|
||||
bool bold = (d % 90) == 0;
|
||||
float tickH = bold ? stripH * 0.9f : stripH * 0.4f;
|
||||
_text.DrawRect(x - 0.5f, top + stripH - tickH, 1f, tickH, bold ? White : Grey);
|
||||
|
||||
if (bold)
|
||||
{
|
||||
string lab = d switch
|
||||
{
|
||||
0 => "E",
|
||||
90 => "N",
|
||||
180 => "W",
|
||||
270 => "S",
|
||||
_ => ""
|
||||
};
|
||||
if (lab.Length > 0)
|
||||
{
|
||||
float w = _font.MeasureWidth(lab);
|
||||
_text.DrawString(_font, lab, x - w * 0.5f, top - _font.LineHeight + 4f, White);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current heading indicator arrow below the strip.
|
||||
_text.DrawRect(cx - 1.5f, top + stripH + 2f, 3f, 6f, Yellow);
|
||||
string hText = $"{h,3:F0}°";
|
||||
float hw = _font.MeasureWidth(hText);
|
||||
_text.DrawString(_font, hText, cx - hw * 0.5f, top + stripH + 10f, Yellow);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Help panel — center: full keybind cheat-sheet, shown when F1 is pressed.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly (string key, string desc)[] Keybinds =
|
||||
{
|
||||
("F1", "toggle this help"),
|
||||
("F2", "toggle collision wireframes"),
|
||||
("F3", "console dump (pos + nearby objects)"),
|
||||
("F4", "toggle debug HUD info panel"),
|
||||
("F5", "toggle stats panel"),
|
||||
("F6", "toggle compass"),
|
||||
("F", "toggle fly camera"),
|
||||
("Tab", "toggle player mode (requires login)"),
|
||||
("W A S D", "move (player mode) / fly"),
|
||||
("Mouse", "turn character / look (fly)"),
|
||||
("Hold RMB", "free orbit camera around player"),
|
||||
("Wheel", "zoom chase camera in / out"),
|
||||
("F8 / F9", "mouse sensitivity slower / faster"),
|
||||
("Space", "jump (hold to charge)"),
|
||||
("Shift", "run"),
|
||||
("Escape", "exit fly / player / close window"),
|
||||
};
|
||||
|
||||
private void DrawHelpPanel(Vector2 screenSize)
|
||||
{
|
||||
var lines = new List<(string text, Vector4 color)>();
|
||||
lines.Add(("CONTROLS", Yellow));
|
||||
lines.Add(("", White));
|
||||
foreach (var (k, d) in Keybinds)
|
||||
lines.Add(($" {k,-9} {d}", White));
|
||||
lines.Add(("", White));
|
||||
lines.Add(("Press F1 to close", Grey));
|
||||
|
||||
float panelW = MeasureMax(lines) + 2 * InnerPad;
|
||||
float panelH = lines.Count * _font.LineHeight + 2 * InnerPad;
|
||||
float x = (screenSize.X - panelW) * 0.5f;
|
||||
float y = (screenSize.Y - panelH) * 0.5f;
|
||||
DrawPanel(x, y, lines, panelW);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Hint bar — bottom-left: always-visible "F1 for help" reminder.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void DrawHintBar(Vector2 screenSize)
|
||||
{
|
||||
string hint = "F1 help F2 wireframes F3 dump F4/F5/F6 panels F8/F9 sens Tab player Hold RMB orbit Wheel zoom";
|
||||
float w = _font.MeasureWidth(hint);
|
||||
float pad = 10f;
|
||||
float y = screenSize.Y - _font.LineHeight - pad;
|
||||
_text.DrawRect(pad - 4, y - 3, w + 8, _font.LineHeight + 6, PanelBg);
|
||||
_text.DrawString(_font, hint, pad, y, Grey);
|
||||
}
|
||||
|
||||
private void DrawToast(Vector2 screenSize)
|
||||
{
|
||||
if (_toastText is null || _toastTimeLeft <= 0f) return;
|
||||
float w = _font.MeasureWidth(_toastText);
|
||||
float x = (screenSize.X - w) * 0.5f;
|
||||
float y = 60f;
|
||||
var c = _toastColor;
|
||||
float alpha = MathF.Min(1f, _toastTimeLeft / 0.5f);
|
||||
c.W *= alpha;
|
||||
var bg = PanelBg;
|
||||
bg.W *= alpha;
|
||||
_text.DrawRect(x - 10, y - 4, w + 20, _font.LineHeight + 8, bg);
|
||||
_text.DrawString(_font, _toastText, x, y, c);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Panel helpers.
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
private const float InnerPad = 8f;
|
||||
|
||||
private float MeasureMax(IReadOnlyList<(string text, Vector4 color)> lines)
|
||||
{
|
||||
float m = 0;
|
||||
foreach (var (text, _) in lines)
|
||||
m = MathF.Max(m, _font.MeasureWidth(text));
|
||||
return m;
|
||||
}
|
||||
|
||||
private void DrawPanel(float x, float y, IReadOnlyList<(string text, Vector4 color)> lines, float? widthOverride = null)
|
||||
{
|
||||
float maxW = MeasureMax(lines);
|
||||
float panelW = widthOverride ?? (maxW + 2 * InnerPad);
|
||||
float panelH = lines.Count * _font.LineHeight + 2 * InnerPad;
|
||||
|
||||
_text.DrawRect(x, y, panelW, panelH, PanelBg);
|
||||
_text.DrawRectOutline(x, y, panelW, panelH, PanelBorder);
|
||||
|
||||
float cy = y + InnerPad;
|
||||
foreach (var (text, color) in lines)
|
||||
{
|
||||
_text.DrawString(_font, text, x + InnerPad, cy, color);
|
||||
cy += _font.LineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private static float NormalizeDeg(float deg)
|
||||
{
|
||||
deg %= 360f;
|
||||
if (deg < 0) deg += 360f;
|
||||
return deg;
|
||||
}
|
||||
|
||||
private static float NormalizeDegSigned(float deg)
|
||||
{
|
||||
deg %= 360f;
|
||||
if (deg > 180) deg -= 360f;
|
||||
if (deg < -180) deg += 360f;
|
||||
return deg;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue