Die Figuren (Einheiten, Monster, Völker, Helden) bekommen den Materialweg der Welt; dazu Umgebungsverdeckung für die ganze Szene. - Surface-Materialien für alle 146 Figurenmaterialien (142 neu unter Materials/Units/, die LN_Boss_* gehörten schon zu den Gebäuden). Farbe, Rauigkeit und Emission bleiben die Blender-Werte; Metalle werden metallisch (vorher war keines der 414 Figurenmaterialien metallisch), ein Randlicht hebt die Silhouetten im Draufblick vom Boden ab. - Oberflächenstruktur je Materialfamilie im eigenen Raum der Figur, nur in den HUD-Porträts. Im Feld (30-60 px hohe Figuren) war sie nicht zu sehen und kostete rund 10 % Bildrate. - Ringborkes 4k-Satz liegt am Schnittholz (RB_Cutwood), wohin er gehört; U5.3 hatte ihn an die Rinde gebunden. Verwaistes RB_Bark.mat entfernt. - SSAO über LuneProjectSetup: nach den opaken Objekten, halbe Auflösung, Radius 0,35 m, Ausblendung ab 260 m. Standardmäßig an (Entscheidung des Projektinhabers), in den Optionen abschaltbar, -luneSsao 0 für eine Sitzung. Die Variante mit Tiefen-Normalen-Vorpass kostete 19-28 %. - Shader: Steinreliefkarte nur noch für Gelände, Stein, Pflaster, Erde. - Werkzeug: Übersichtsbilder mit LUNE_DRESS=1 eingekleidet wie im Spiel; das Materialset wird nach der leeren Vorschauszene geladen (sie entlädt unreferenzierte Assets). - Tests: EditMode 29/29, PlayMode 14/14, Verify 718/0, Player-Selbsttest 30/30 (4 neue zur SSAO-Option), Fingerabdrücke unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Leistung (A/B mit U5.3 im selben Lauf): Mid nah 177,7 -> 148,5 fps (-16 %), Karte 127,8 -> 111,5 fps (-13 %); ohne SSAO -3,5 / -4,5 %. - Doku: U5-VISUAL (U5.4), PORT-STATUS, PORT-QA; Bilder screenshots/u5.4/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
346 lines
14 KiB
C#
346 lines
14 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
using UnityEngine.Rendering.Universal;
|
||
using UnityEngine.UIElements;
|
||
|
||
namespace Luneraria.UI
|
||
{
|
||
/// <summary>
|
||
/// Options and controls (port of scripts/luneraria/ui/lune_settings.gd): volume, mute, full
|
||
/// screen, reduced motion and the HUD size; the key list is the one place where every shortcut
|
||
/// is shown. A running combat pauses offline while this window is open (the game controller
|
||
/// does that on Opened/Closed).
|
||
/// </summary>
|
||
public sealed class LuneSettingsPanel : VisualElement
|
||
{
|
||
public static readonly float[] Scales = { .9f, 1f, 1.15f, 1.3f };
|
||
public static readonly (string group, (string key, string action)[] pairs)[] Keys =
|
||
{
|
||
("Planung", new[] { ("1–5", "Truppe kaufen"), ("Leertaste", "Bereit"), ("Klick auf Einheit", "Details, Aufwerten, Einsatz"), ("Esc", "Auswahl oder Fenster schließen") }),
|
||
("Kampf", new[] { ("Leertaste / P", "Pause (offline)"), ("Tempo-Knopf", "1× · 2× · 4×") }),
|
||
("Kamera", new[] { ("F2 · F3 · F4", "Top · Mid · Bottom"), ("G", "Bossgrotte"), ("B", "eigener Ancient"), ("H", "eigener Held"), ("Tab", "nächste Lane"),
|
||
("O", "Übersicht"), ("R", "zurücksetzen"), ("Q / E", "drehen"), ("WASD · Pfeile · mittlere Maustaste", "verschieben"), ("Mausrad", "Zoom"), ("Minimap", "klicken oder ziehen") }),
|
||
("Allgemein", new[] { ("F1", "Optionen"), ("M", "Ton an/aus"), ("F11", "Vollbild") }),
|
||
};
|
||
|
||
public event Action Opened;
|
||
public event Action Closed;
|
||
readonly LuneSlider _volume;
|
||
readonly Dictionary<string, LuneSwitch> _checks = new Dictionary<string, LuneSwitch>();
|
||
public readonly List<LuneButton> ScaleButtons = new List<LuneButton>();
|
||
|
||
public LuneSettingsPanel()
|
||
{
|
||
style.position = Position.Absolute;
|
||
style.left = 0f;
|
||
style.top = 0f;
|
||
style.right = 0f;
|
||
style.bottom = 0f;
|
||
style.backgroundColor = new Color(.02f, .03f, .04f, .9f);
|
||
style.justifyContent = Justify.Center;
|
||
style.alignItems = Align.Center;
|
||
RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
var panel = new LunePanel(LunePanels.Menu);
|
||
panel.style.width = 900f;
|
||
Add(panel);
|
||
VisualElement column = LuneUi.Box(true, 10f, panel);
|
||
Label heading = LuneUi.Label("Optionen & Steuerung", "HeadingLabel", column);
|
||
heading.style.alignSelf = Align.Center;
|
||
LuneUi.Label("Ein laufender Kampf pausiert offline, solange dieses Fenster geöffnet ist.", "MutedLabel", column).style.alignSelf = Align.Center;
|
||
VisualElement top = LuneUi.Box(false, 30f, column);
|
||
top.style.alignItems = Align.FlexStart;
|
||
VisualElement left = LuneUi.Box(true, 8f, top);
|
||
left.style.flexGrow = 1f;
|
||
left.style.flexBasis = 0f;
|
||
LuneUi.Label("LAUTSTÄRKE", "SectionLabel", left);
|
||
_volume = new LuneSlider();
|
||
left.Add(_volume);
|
||
_volume.Changed += value =>
|
||
{
|
||
LunePreferences.Volume = value;
|
||
LunePreferences.Commit();
|
||
Apply();
|
||
};
|
||
foreach ((string key, string text) entry in new[] { ("muted", "Ton stumm [M]"), ("fullscreen", "Vollbild [F11]"), ("reduced_motion", "Bewegung reduzieren"), ("ambient_occlusion", "Umgebungsverdeckung (SSAO)") })
|
||
{
|
||
var check = new LuneSwitch(entry.text);
|
||
left.Add(check);
|
||
string key = entry.key;
|
||
check.Changed += value =>
|
||
{
|
||
if (key == "muted")
|
||
LunePreferences.Muted = value;
|
||
else if (key == "fullscreen")
|
||
LunePreferences.Fullscreen = value;
|
||
else if (key == "ambient_occlusion")
|
||
LunePreferences.AmbientOcclusion = value;
|
||
else
|
||
LunePreferences.ReducedMotion = value;
|
||
LunePreferences.Commit();
|
||
Apply();
|
||
};
|
||
_checks[key] = check;
|
||
}
|
||
VisualElement right = LuneUi.Box(true, 8f, top);
|
||
right.style.flexGrow = 1f;
|
||
right.style.flexBasis = 0f;
|
||
LuneUi.Label("UI-GRÖSSE IM SPIEL", "SectionLabel", right);
|
||
VisualElement scales = LuneUi.Box(false, 6f, right);
|
||
foreach (float value in Scales)
|
||
{
|
||
var button = new LuneButton(Mathf.RoundToInt(value * 100f) + " %", LuneButtons.HudToggle, scales);
|
||
button.style.flexGrow = 1f;
|
||
float captured = value;
|
||
button.Clicked += () =>
|
||
{
|
||
LunePreferences.UiScale = captured;
|
||
LunePreferences.Commit();
|
||
Sync();
|
||
};
|
||
ScaleButtons.Add(button);
|
||
}
|
||
LuneUi.Wrap(LuneUi.Label("Größer für hochauflösende oder weit entfernte Bildschirme. Das HUD begrenzt die Größe selbst, sodass nichts aus dem Fenster ragt.", "MutedLabel", right));
|
||
var line = new VisualElement { pickingMode = PickingMode.Ignore };
|
||
line.style.height = 1f;
|
||
line.style.marginTop = 7f;
|
||
line.style.marginBottom = 7f;
|
||
line.style.backgroundColor = LuneUiTheme.WithAlpha(LuneUiTheme.Bronze, .35f);
|
||
column.Add(line);
|
||
LuneUi.Label("STEUERUNG", "SectionLabel", column);
|
||
VisualElement keys = LuneUi.Box(false, 26f, column);
|
||
keys.style.alignItems = Align.FlexStart;
|
||
for (int index = 0; index < 2; index++)
|
||
{
|
||
VisualElement list = LuneUi.Box(true, 2f, keys);
|
||
list.style.flexGrow = 1f;
|
||
list.style.flexBasis = 0f;
|
||
var groups = index == 0 ? new[] { Keys[0], Keys[1], Keys[3] } : new[] { Keys[2] };
|
||
foreach (var group in groups)
|
||
{
|
||
LuneUi.Label(LuneUi.Upper(group.group), "EyebrowLabel", list, 14f).style.marginTop = 6f;
|
||
foreach (var pair in group.pairs)
|
||
{
|
||
VisualElement row = LuneUi.Box(false, 14f, list);
|
||
Label key = LuneUi.Label(pair.key, "CardFactLabel", row, 15f);
|
||
key.style.minWidth = 160f;
|
||
LuneUi.SetColor(key, LuneUiTheme.BronzeLight);
|
||
LuneUi.Wrap(LuneUi.Label(pair.action, "MutedLabel", row, 15f)).style.flexGrow = 1f;
|
||
}
|
||
}
|
||
}
|
||
var back = new LuneButton("ZURÜCK [Esc]", LuneButtons.Primary, column);
|
||
back.style.alignSelf = Align.Center;
|
||
back.Clicked += Close;
|
||
LuneUi.Show(this, false);
|
||
}
|
||
|
||
public bool Open { get { return style.display != DisplayStyle.None; } }
|
||
|
||
public void Show()
|
||
{
|
||
Sync();
|
||
LuneUi.Show(this, true);
|
||
BringToFront();
|
||
Opened?.Invoke();
|
||
}
|
||
|
||
public void Close()
|
||
{
|
||
if (!Open)
|
||
return;
|
||
LuneUi.Show(this, false);
|
||
Closed?.Invoke();
|
||
}
|
||
|
||
/// <summary>Reads the preferences into the controls (Godot sync()).</summary>
|
||
public void Sync()
|
||
{
|
||
_volume.SetValueWithoutNotify(LunePreferences.Volume);
|
||
_checks["muted"].SetValueWithoutNotify(LunePreferences.Muted);
|
||
_checks["fullscreen"].SetValueWithoutNotify(LunePreferences.Fullscreen);
|
||
_checks["reduced_motion"].SetValueWithoutNotify(LunePreferences.ReducedMotion);
|
||
_checks["ambient_occlusion"].SetValueWithoutNotify(LunePreferences.AmbientOcclusion);
|
||
int best = 1;
|
||
for (int index = 0; index < Scales.Length; index++)
|
||
{
|
||
if (Mathf.Abs(Scales[index] - LunePreferences.UiScale) < Mathf.Abs(Scales[best] - LunePreferences.UiScale))
|
||
best = index;
|
||
}
|
||
for (int index = 0; index < ScaleButtons.Count; index++)
|
||
ScaleButtons[index].On = index == best;
|
||
}
|
||
|
||
/// <summary>Volume, mute, full screen and ambient occlusion take effect at once.</summary>
|
||
public static void Apply()
|
||
{
|
||
AudioListener.volume = LunePreferences.Muted ? 0f : LunePreferences.Volume;
|
||
ApplyAmbientOcclusion(LunePreferences.AmbientOcclusion);
|
||
if (Application.isBatchMode)
|
||
return;
|
||
if (Screen.fullScreen != LunePreferences.Fullscreen)
|
||
Screen.fullScreen = LunePreferences.Fullscreen;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Switches the SSAO feature of the URP renderer (U5.4). Only in the player: in the editor
|
||
/// the renderer is a project asset, and switching it there would change the file.
|
||
/// </summary>
|
||
static void ApplyAmbientOcclusion(bool on)
|
||
{
|
||
if (Application.isEditor)
|
||
return;
|
||
ScriptableRendererFeature feature = AmbientOcclusionFeature();
|
||
if (feature != null && feature.isActive != on)
|
||
feature.SetActive(on);
|
||
}
|
||
|
||
/// <summary>The SSAO feature of the active URP renderer, or null (player self-test).</summary>
|
||
public static ScriptableRendererFeature AmbientOcclusionFeature()
|
||
{
|
||
if (!(GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset pipeline))
|
||
return null;
|
||
foreach (ScriptableRendererData data in pipeline.rendererDataList)
|
||
{
|
||
if (data == null)
|
||
continue;
|
||
foreach (ScriptableRendererFeature feature in data.rendererFeatures)
|
||
{
|
||
if (feature is ScreenSpaceAmbientOcclusion)
|
||
return feature;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>Horizontal slider in the HUD look (track, bronze fill, handle); value 0–1.</summary>
|
||
public sealed class LuneSlider : VisualElement
|
||
{
|
||
public event Action<float> Changed;
|
||
public float Value { get; private set; }
|
||
bool _drag;
|
||
|
||
public LuneSlider()
|
||
{
|
||
style.height = 28f;
|
||
pickingMode = PickingMode.Position;
|
||
generateVisualContent += Draw;
|
||
RegisterCallback<PointerDownEvent>(evt =>
|
||
{
|
||
if (evt.button != 0)
|
||
return;
|
||
_drag = true;
|
||
this.CapturePointer(evt.pointerId);
|
||
Set(evt.localPosition.x);
|
||
});
|
||
RegisterCallback<PointerMoveEvent>(evt =>
|
||
{
|
||
if (_drag)
|
||
Set(evt.localPosition.x);
|
||
});
|
||
RegisterCallback<PointerUpEvent>(evt =>
|
||
{
|
||
_drag = false;
|
||
this.ReleasePointer(evt.pointerId);
|
||
});
|
||
}
|
||
|
||
void Set(float x)
|
||
{
|
||
float value = Mathf.Clamp01((x - 9f) / Mathf.Max(1f, layout.width - 18f));
|
||
if (Mathf.Approximately(value, Value))
|
||
return;
|
||
Value = value;
|
||
MarkDirtyRepaint();
|
||
Changed?.Invoke(value);
|
||
}
|
||
|
||
public void SetValueWithoutNotify(float value)
|
||
{
|
||
Value = Mathf.Clamp01(value);
|
||
MarkDirtyRepaint();
|
||
}
|
||
|
||
void Draw(MeshGenerationContext context)
|
||
{
|
||
Painter2D p = context.painter2D;
|
||
float width = layout.width;
|
||
float y = layout.height * .5f;
|
||
LunePlates.Line(p, new Vector2(9f, y), new Vector2(width - 9f, y), new Color(0f, 0f, 0f, .55f), 6f);
|
||
float x = 9f + (width - 18f) * Value;
|
||
LunePlates.Line(p, new Vector2(9f, y), new Vector2(x, y), LuneUiTheme.Bronze, 6f);
|
||
p.fillColor = LuneUiTheme.BronzeLight;
|
||
p.BeginPath();
|
||
p.Arc(new Vector2(x, y), 8f, Angle.Degrees(0f), Angle.Degrees(360f));
|
||
p.ClosePath();
|
||
p.Fill();
|
||
}
|
||
}
|
||
|
||
/// <summary>On/off switch with a label (Godot CheckButton with the toggle icons of the menu theme).</summary>
|
||
public sealed class LuneSwitch : VisualElement
|
||
{
|
||
public event Action<bool> Changed;
|
||
public bool Value { get; private set; }
|
||
readonly VisualElement _knob;
|
||
|
||
public LuneSwitch(string text)
|
||
{
|
||
style.flexDirection = FlexDirection.Row;
|
||
style.alignItems = Align.Center;
|
||
style.height = 34f;
|
||
pickingMode = PickingMode.Position;
|
||
Label label = LuneUi.Label(text, "CardFactLabel", this);
|
||
label.style.flexGrow = 1f;
|
||
_knob = new VisualElement { pickingMode = PickingMode.Ignore };
|
||
_knob.style.width = 44f;
|
||
_knob.style.height = 22f;
|
||
_knob.generateVisualContent += Draw;
|
||
Add(_knob);
|
||
RegisterCallback<PointerDownEvent>(evt =>
|
||
{
|
||
if (evt.button == 0)
|
||
Click();
|
||
});
|
||
}
|
||
|
||
/// <summary>Text of the switch (self-test lookup).</summary>
|
||
public string Text { get { return this.Q<Label>()?.text ?? ""; } }
|
||
|
||
/// <summary>Flips the switch as a click does.</summary>
|
||
public void Click()
|
||
{
|
||
Value = !Value;
|
||
_knob.MarkDirtyRepaint();
|
||
Changed?.Invoke(Value);
|
||
}
|
||
|
||
public void SetValueWithoutNotify(bool value)
|
||
{
|
||
Value = value;
|
||
_knob.MarkDirtyRepaint();
|
||
}
|
||
|
||
void Draw(MeshGenerationContext context)
|
||
{
|
||
Painter2D p = context.painter2D;
|
||
float w = _knob.layout.width;
|
||
float h = _knob.layout.height;
|
||
float r = h * .5f;
|
||
p.fillColor = Value ? LuneUiTheme.WithAlpha(LuneUiTheme.Bronze, .85f) : new Color(0f, 0f, 0f, .5f);
|
||
p.BeginPath();
|
||
p.Arc(new Vector2(r, r), r, Angle.Degrees(90f), Angle.Degrees(270f));
|
||
p.LineTo(new Vector2(w - r, 0f));
|
||
p.Arc(new Vector2(w - r, r), r, Angle.Degrees(270f), Angle.Degrees(450f));
|
||
p.ClosePath();
|
||
p.Fill();
|
||
p.fillColor = Value ? LuneUiTheme.Parchment : LuneUiTheme.Muted;
|
||
p.BeginPath();
|
||
p.Arc(new Vector2(Value ? w - r : r, r), r - 3f, Angle.Degrees(0f), Angle.Degrees(360f));
|
||
p.ClosePath();
|
||
p.Fill();
|
||
}
|
||
}
|
||
}
|