champions-of-luneraria/Assets/Luneraria/View/LuneMatchView.cs
Martin Stickelbroeck 3871b3e2e6 U2C Simulation/View Binding
Fester Simulationstakt und Ansichten, die nur lesen; Nachweis, dass die Partie
bei jeder Bildrate dieselbe ist.

- LuneClock: Akkumulator, ganze Schritte zu 0,05 s, Alpha zum Interpolieren.
- LuneMatchRunner: Host-Ablauf einer Bot-Partie (Planung über die
  Befehlsschnittstelle, Kampf im festen Takt, Abrechnung, nächste Runde);
  LuneMatchDriver reicht in Update nur Bildzeit weiter.
- LuneTrace: Fingerabdruck der Partie (jeder Schritt, jede Abrechnung) und der
  reine Kernlauf zum Vergleich.
- Ansichten: LuneMatchView, LuneUnitView (interpolierte Position, Blickrichtung,
  Clips aus Kampfereignissen, Teamring, Lebensbalken), LuneBuildingView mit
  Einsturz und Ruine aus den autoritativen HP, Turmblitze, LuneCameraRig wie
  lune_camera.gd, vorläufige Debug-Anzeige (kein HUD; U3 portiert das G3-HUD).
- Einheitenbibliothek: Kohlenbeißer aus 131 Teilen zu einem Mesh, Animator-
  Controller mit den Blender-Clips, Ring- und Balkenmeshes, Markermaterialien.
- Tests: EditMode (30/60/120 fps, unbegrenzt, Ruckler, 8x Tempo; Seed 2 und
  Seed 9 ab Runde 21) und PlayMode in der Szene bei echten Bildraten 30, 60,
  120 und unbegrenzt: überall derselbe Fingerabdruck wie der Kern.

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

145 lines
5.8 KiB
C#

using System.Collections.Generic;
using Luneraria.Core;
using UnityEngine;
namespace Luneraria.View
{
/// <summary>
/// Shows the driver's match (U2C): unit views per combat, building looks from the authoritative
/// HP, tower bolts from the towers' attack events. Runs after the driver in the same frame and
/// only reads the core.
/// </summary>
[DefaultExecutionOrder(100)]
public sealed class LuneMatchView : MonoBehaviour
{
public LuneMatchDriver Driver;
public LuneUnitLibrary Library;
public Camera ViewCamera;
readonly Dictionary<int, LuneUnitView> _units = new Dictionary<int, LuneUnitView>();
readonly Dictionary<string, LuneBuildingView> _buildings = new Dictionary<string, LuneBuildingView>();
readonly Dictionary<string, int> _towerSerials = new Dictionary<string, int>();
readonly List<Bolt> _bolts = new List<Bolt>();
LuneCombat _combat;
Transform _unitRoot;
bool _synced;
sealed class Bolt
{
public LineRenderer Line;
public float Age;
public LuneUnitView Target;
public Vector3 From;
}
public IEnumerable<LuneUnitView> Units { get { return _units.Values; } }
/// <summary>Main-thread cost of the last frame's view update (milliseconds).</summary>
public double LastSyncMs { get; private set; }
readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch();
public LuneBuildingView Building(string id)
{
_buildings.TryGetValue(id, out LuneBuildingView view);
return view;
}
void Start()
{
foreach (LuneBuildingView building in FindObjectsByType<LuneBuildingView>(FindObjectsSortMode.None))
_buildings[building.BuildingId] = building;
_unitRoot = new GameObject("Units").transform;
if (ViewCamera == null)
ViewCamera = Camera.main;
}
void LateUpdate()
{
if (Driver == null || Driver.Runner == null)
return;
_clock.Restart();
LuneMatch match = Driver.Runner.Match;
float dt = Time.deltaTime;
if (match.Combat != _combat)
{
foreach (LuneUnitView view in _units.Values)
Destroy(view.gameObject);
_units.Clear();
_towerSerials.Clear();
_combat = match.Combat;
}
bool showUnits = _combat != null && Driver.Runner.Current != LuneMatchRunner.Stage.Planning;
float alpha = Driver.Runner.Current == LuneMatchRunner.Stage.Combat ? (float)Driver.Runner.Clock.Alpha : 1f;
if (showUnits)
{
foreach (LuneUnit unit in _combat.Units)
{
if (!unit.IsMobile())
continue;
if (!_units.TryGetValue(unit.Id, out LuneUnitView view))
{
view = new GameObject().AddComponent<LuneUnitView>();
view.transform.SetParent(_unitRoot, false);
view.Bind(unit, Library);
_units[unit.Id] = view;
}
view.Sync(alpha, dt, ViewCamera != null ? ViewCamera.transform : null);
}
}
SyncBuildings(match);
SyncBolts(dt);
_synced = true;
LastSyncMs = _clock.Elapsed.TotalMilliseconds;
}
void SyncBuildings(LuneMatch match)
{
bool fighting = _combat != null && match.Phase == Phase.Combat;
foreach (BuildingDef definition in match.Map.Buildings)
{
if (!_buildings.TryGetValue(definition.Id, out LuneBuildingView view))
continue;
double hp = match.BuildingHp[definition.Id];
LuneUnit unit = null;
if (_combat != null && _combat.Buildings.TryGetValue(definition.Id, out unit) && fighting)
hp = unit.Hp;
view.SetHealth((float)hp, (float)definition.MaxHp, _synced);
if (unit != null && unit.Kind == "tower" && _combat != null)
TowerAttack(view, unit);
}
}
void TowerAttack(LuneBuildingView view, LuneUnit tower)
{
_towerSerials.TryGetValue(tower.BuildingId, out int last);
_towerSerials[tower.BuildingId] = tower.AttackSerial;
if (tower.AttackSerial == last || last == 0 && tower.AttackSerial == 0)
return;
if (tower.Target == null || !_units.TryGetValue(tower.Target.Id, out LuneUnitView target))
return;
var line = new GameObject("Bolt").AddComponent<LineRenderer>();
line.transform.SetParent(_unitRoot, false);
line.sharedMaterial = Library.Bolt[Mathf.Clamp(view.Team, 0, 2)];
line.widthMultiplier = view.Tier == "base" ? .22f : .16f;
line.positionCount = 2;
line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
_bolts.Add(new Bolt { Line = line, Target = target, From = view.Muzzle });
}
void SyncBolts(float dt)
{
for (int i = _bolts.Count - 1; i >= 0; i--)
{
Bolt bolt = _bolts[i];
bolt.Age += dt;
if (bolt.Age > .25f || bolt.Target == null)
{
Destroy(bolt.Line.gameObject);
_bolts.RemoveAt(i);
continue;
}
bolt.Line.SetPosition(0, bolt.From);
bolt.Line.SetPosition(1, bolt.Target.AimPoint);
}
}
}
}