champions-of-luneraria/Assets/Luneraria/View/LuneBenchmark.cs
Martin Stickelbroeck 2b86c0d3b5 U3B/U3C Zwischenstand: HUD-Bereiche, Eingabe und Befehle
Das G3-HUD steht im Spiel; die Abnahme (U3D) folgt.

- U3B: alle Bereiche und Komponenten aus Godot übertragen (LuneHud,
  Lanes, Bossgrotte, nächste Welle, Kaufkarten, Armee, Held mit Items und
  Talenten, Angriffschampions, Team, Ereignisse, Minimap, Tooltip,
  Optionen, Partieende). Porträts aus den echten Modellen, einmal gerendert.
- Offene G3-Punkte umgesetzt: „1 WELLE“ als Band am Porträtrand,
  Schadens- und Rüstungssymbole auf den Karten, Armeewert, Heldenporträts
  auf der Minimap. Befehlsplatz für die Ultimates der geplanten Helden
  reserviert (verborgen).
- U3C: LuneGame verdrahtet Befehle (gleiche Schnittstelle wie die Bots),
  Bereit mit 1-s-Sperre nach dem Kampf, Tempo und Pause, Tasten wie in G3,
  Kamera-Ziele, Auswahl im Feld, Planungsvorschau und Aufstellungsflächen.
  Ersetzt die U2-Debug-Anzeige. Bot-Partien und Benchmark unverändert.
- Szenen- und Bibliotheksbauer idempotent: Animator-Controller und
  Post-Processing-Profil werden an Ort und Stelle aktualisiert. Die Szene
  bekommt bei jedem Bau neue Objekt-IDs, der Inhalt ist gleich.
- Prüfung (Port von verify_g3, Teile A–H): 699 Prüfungen ohne Fehler.
  EditMode 23/23, PlayMode 12/12, Fingerabdrücke der Bot-Partien
  unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:45:02 +02:00

182 lines
8.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Luneraria.Core;
using UnityEngine;
namespace Luneraria.View
{
/// <summary>
/// Frame-time benchmark of a player build (U2D). Active only with the command-line argument
/// "-luneBenchmark &lt;out.json&gt;": runs the match of seed 9 from round 21 (the golden fixture
/// high_wave_21, the most units) at normal speed, VSync off, and measures two windows once the
/// armies meet: the whole map (20 s) and a close view of the Mid lane (10 s). Then it quits.
/// </summary>
[DefaultExecutionOrder(-100)]
public sealed class LuneBenchmark : MonoBehaviour
{
public LuneMatchDriver Driver;
public LuneMatchView View;
public LuneCameraRig Rig;
sealed class Window
{
public string Label;
public float Seconds;
public float Zoom;
public Vec2 Focus;
public readonly List<double> FrameMs = new List<double>();
public double Simulation;
public double Views;
public int Units;
public int GcStart;
public int GcEnd;
public int SlowFrames;
public int SlowWithStep;
public readonly List<double> MainThreadMs = new List<double>();
public readonly List<double> RenderThreadMs = new List<double>();
public readonly List<double> GpuMs = new List<double>();
public readonly List<double> PresentWaitMs = new List<double>();
}
readonly FrameTiming[] _timings = new FrameTiming[1];
string _out;
readonly List<Window> _windows = new List<Window>();
int _index = -1;
float _elapsed;
void Awake()
{
string[] args = Environment.GetCommandLineArgs();
int at = Array.IndexOf(args, "-luneBenchmark");
if (at < 0 || at + 1 >= args.Length)
{
enabled = false;
return;
}
_out = args[at + 1];
Running = true;
// The benchmark runs unattended; it must not pause when its window loses focus.
Application.runInBackground = true;
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = -1;
_windows.Add(new Window { Label = "overview (zoom 64)", Seconds = 20f, Zoom = 64f, Focus = new Vec2(0, 0) });
_windows.Add(new Window { Label = "mid lane close (zoom 22)", Seconds = 10f, Zoom = 22f, Focus = new Vec2(0, 0) });
}
/// <summary>True in a benchmark run: the game controller keeps the bot match (no human seat).</summary>
public static bool Running { get; private set; }
void Start()
{
Driver.HumanSlot = -1;
Driver.Seed = 9;
Driver.StartRound = 21;
Driver.ExtraGold = 4000;
Driver.Speed = 1f;
Driver.Restart();
Rig.Interactive = false;
Rig.Overview(true);
}
void Update()
{
LuneMatch match = Driver.Runner.Match;
if (_index < 0)
{
if (Driver.Runner.Current == LuneMatchRunner.Stage.Combat && match.Combat.Elapsed >= 10.0)
Begin(0);
return;
}
if (_index >= _windows.Count)
return;
Window window = _windows[_index];
double ms = Time.unscaledDeltaTime * 1000.0;
window.FrameMs.Add(ms);
// Where the time goes (needs "Frame Timing Stats" in the player settings).
FrameTimingManager.CaptureFrameTimings();
if (FrameTimingManager.GetLatestTimings(1, _timings) > 0)
{
window.MainThreadMs.Add(_timings[0].cpuMainThreadFrameTime);
window.RenderThreadMs.Add(_timings[0].cpuRenderThreadFrameTime);
window.GpuMs.Add(_timings[0].gpuFrameTime);
window.PresentWaitMs.Add(_timings[0].cpuMainThreadPresentWaitTime);
}
// Frames above 1/60 s, and how many of them also ran a simulation step.
if (ms > 1000.0 / 60.0)
{
window.SlowFrames++;
if (Driver.Runner.StepsThisFrame > 0)
window.SlowWithStep++;
}
window.Simulation += Driver.LastAdvanceMs;
window.Views += View.LastSyncMs;
if (match.Combat != null && window.FrameMs.Count % 30 == 1)
window.Units = Math.Max(window.Units, CountAlive(match.Combat));
_elapsed += Time.unscaledDeltaTime;
if (_elapsed >= window.Seconds)
{
window.GcEnd = GC.CollectionCount(0);
if (_index + 1 < _windows.Count)
Begin(_index + 1);
else
Finish();
}
}
static string Describe(List<double> values)
{
if (values.Count == 0)
return "n/a";
double[] sorted = values.OrderBy(v => v).ToArray();
return string.Format(CultureInfo.InvariantCulture, "avg {0:0.00} / median {1:0.00} / p95 {2:0.00} ms", sorted.Average(), sorted[sorted.Length / 2], sorted[(int)(sorted.Length * .95)]);
}
static int CountAlive(LuneCombat combat)
{
int count = 0;
foreach (LuneUnit unit in combat.Units)
if (unit.Alive && unit.IsMobile())
count++;
return count;
}
void Begin(int index)
{
_index = index;
_elapsed = 0f;
Window window = _windows[index];
window.GcStart = GC.CollectionCount(0);
Rig.LookAt(window.Focus, window.Zoom, true);
}
void Finish()
{
_index = _windows.Count;
var text = new System.Text.StringBuilder("{\n");
text.AppendFormat(" \"build\": \"{0} player, {1}, {2}\",\n", Application.platform, Application.unityVersion, Debug.isDebugBuild ? "development" : "release");
text.AppendFormat(" \"device\": \"{0}; {1}; {2}\",\n", SystemInfo.deviceModel, SystemInfo.processorType, SystemInfo.graphicsDeviceName);
text.AppendFormat(" \"graphics_api\": \"{0}\",\n \"resolution\": \"{1}x{2}\",\n \"vsync\": {3},\n", SystemInfo.graphicsDeviceType, Screen.width, Screen.height, QualitySettings.vSyncCount);
text.Append(" \"match\": \"seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat\",\n \"windows\": [\n");
for (int i = 0; i < _windows.Count; i++)
{
Window window = _windows[i];
double[] sorted = window.FrameMs.OrderBy(v => v).ToArray();
double total = window.FrameMs.Sum();
text.AppendFormat(CultureInfo.InvariantCulture,
" {{\"view\": \"{0}\", \"frames\": {1}, \"fps\": {2:0.0}, \"frame_ms_avg\": {3:0.00}, \"frame_ms_median\": {4:0.00}, \"frame_ms_p95\": {5:0.00}, \"frame_ms_p99\": {6:0.00}, \"frame_ms_max\": {7:0.00}, \"frames_over_16_7_ms\": {8}, \"of_them_with_simulation_step\": {9}, \"gc_collections\": {10}, \"simulation_ms_per_frame\": {11:0.000}, \"views_ms_per_frame\": {12:0.000}, \"units_alive_max\": {13}}}{14}\n",
window.Label, sorted.Length, sorted.Length / (total / 1000.0), total / sorted.Length, sorted[sorted.Length / 2], sorted[(int)(sorted.Length * .95)], sorted[(int)(sorted.Length * .99)], sorted[sorted.Length - 1],
window.SlowFrames, window.SlowWithStep, window.GcEnd - window.GcStart, window.Simulation / sorted.Length, window.Views / sorted.Length, window.Units, i < _windows.Count - 1 ? "," : "");
text.AppendFormat(CultureInfo.InvariantCulture, " // timings {0}: main thread {1}, render thread {2}, gpu {3}, present wait {4}\n", window.Label,
Describe(window.MainThreadMs), Describe(window.RenderThreadMs), Describe(window.GpuMs), Describe(window.PresentWaitMs));
}
text.Append(" ]\n}\n");
Directory.CreateDirectory(Path.GetDirectoryName(_out));
File.WriteAllText(_out, text.ToString());
Application.Quit();
}
}
}