备份,准备做双端

This commit is contained in:
FloatGaming
2026-07-30 23:15:58 +08:00
parent add675e45d
commit ec4ec53545
88 changed files with 4050 additions and 872 deletions
+111 -19
View File
@@ -18,6 +18,9 @@ public class ScoreManager : MonoBehaviour
private readonly float[] _idolscoreLastTargetY = new float[5];
private readonly float[] _idolscorePulseAmplitude = new float[5];
private readonly float[] _idolscorePulseSeed = new float[5];
// True once a key has fully converged to its target (no wobble, displayY == targetY).
// While settled we skip the per-frame noise + localScale write; cleared when target changes.
private readonly bool[] _idolscoreSettled = new bool[5];
// per-track pm score sums (red, green, yellow, purple, blue)
public int red_pmScore_sum = 0;
@@ -69,6 +72,12 @@ public class ScoreManager : MonoBehaviour
int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue
};
// 性能:RecalculateTotal 里 5 个分槽 "cur/max" 上次写入的字符串及其目标 Text 对象。
// 仅当"数值(cur/max)变化"或"目标对象变化(换局/换场景导致 UI 重建)"时才写 .text,
// 避免每判定对 5 个槽位无条件赋值触发 TMP 网格重建。写入逻辑与原先完全一致,只是跳过重复写。
private readonly string[] _lastSlotScoreText = new string[5];
private readonly UnityEngine.Object[] _lastSlotScoreTarget = new UnityEngine.Object[5];
// 仅当数值变化时写 TMP.text(避免相同字符串触发无谓网格重建)。
private void SetSumTextIfChanged(TextMeshProUGUI field, int value, int cacheIndex)
{
@@ -105,13 +114,21 @@ public class ScoreManager : MonoBehaviour
public int countLate = 0;
public float totalOffsetMs = 0f;
public int offsetCount = 0;
// Raw per-hit samples (ms) for median/suggestion. Miss (0) not included.
private readonly System.Collections.Generic.List<float> offsetSamples =
new System.Collections.Generic.List<float>();
/// <summary>
/// 只读访问判定偏移样本列表(供直方图等外部组件读取)。
/// </summary>
public System.Collections.Generic.IReadOnlyList<float> OffsetSamples => offsetSamples;
public void ResetStatistics()
{
countPerfect = countGreat = countGood = countMiss = 0;
perfectClearBonusPm = 0;
perfectClearBonusApplied = false;
for (int i = 0; i < 5; i++)
{
trackPerfectCounts[i] = 0;
@@ -123,11 +140,17 @@ public class ScoreManager : MonoBehaviour
countEarly = countLate = 0;
totalOffsetMs = 0f;
offsetCount = 0;
offsetSamples.Clear();
}
/// <summary>
/// Records a timing offset in milliseconds.
/// Positive = Early, Negative = Late.
/// Fired whenever a timing offset is recorded (Positive = Early, Negative = Late).
/// Display-only hook (e.g. realtime offset bar); does not affect scoring/judgement.
/// </summary>
public static event System.Action<float> OnOffsetRecorded;
/// <summary>
/// Records a timing offset in milliseconds. Positive = Early, Negative = Late.
/// </summary>
public void RecordOffset(float offsetMs)
{
@@ -135,9 +158,26 @@ public class ScoreManager : MonoBehaviour
totalOffsetMs += offsetMs;
offsetCount++;
offsetSamples.Add(offsetMs);
if (offsetMs > 0) countEarly++;
else if (offsetMs < 0) countLate++;
OnOffsetRecorded?.Invoke(offsetMs);
}
/// <summary>
/// Returns median offset (ms) across all recorded hits. More robust than mean for
/// calibration suggestions — single outlier taps don't skew it.
/// </summary>
public float GetMedianOffsetMs()
{
int n = offsetSamples.Count;
if (n == 0) return 0f;
var sorted = new System.Collections.Generic.List<float>(offsetSamples);
sorted.Sort();
int mid = n / 2;
return (n % 2 == 1) ? sorted[mid] : 0.5f * (sorted[mid - 1] + sorted[mid]);
}
private void Awake()
@@ -200,6 +240,10 @@ public class ScoreManager : MonoBehaviour
Transform t = _idolscoreKeys[i];
if (t == null) continue;
// Settled: display converged to target and wobble decayed. Nothing changes frame to
// frame, so skip the noise sample and localScale write until the target moves again.
if (_idolscoreSettled[i]) continue;
Vector3 baseScale = _idolscoreKeyBaseScaleCached[i] ? _idolscoreKeyBaseScales[i] : t.localScale;
float targetY = Mathf.Max(0f, _idolscoreTargetY[i]);
@@ -213,6 +257,16 @@ public class ScoreManager : MonoBehaviour
float y = Mathf.Max(0f, _idolscoreDisplayY[i] + noise * _idolscorePulseAmplitude[i]);
t.localScale = new Vector3(baseScale.x, y, baseScale.z);
// Mark settled once the smoothed value has essentially reached the target and the
// wobble amplitude has decayed to zero, so the next frame can early-out.
if (Mathf.Abs(_idolscoreDisplayY[i] - targetY) < 0.0005f && _idolscorePulseAmplitude[i] < 0.0005f)
{
_idolscoreDisplayY[i] = targetY;
_idolscorePulseAmplitude[i] = 0f;
t.localScale = new Vector3(baseScale.x, targetY, baseScale.z);
_idolscoreSettled[i] = true;
}
}
}
@@ -382,6 +436,8 @@ public class ScoreManager : MonoBehaviour
{
_idolscoreLastTargetY[i] = targetY;
_idolscorePulseAmplitude[i] = UnityEngine.Random.Range(0.08f, 0.24f);
// Target moved (and/or wobble re-seeded): resume per-frame animation.
_idolscoreSettled[i] = false;
}
if (targetY <= 0.001f)
@@ -638,13 +694,37 @@ public class ScoreManager : MonoBehaviour
var ui = teamUIController.Instance;
// helper lambda to set text on TMP or legacy Text under a parent fallback
void SetScoreText(TextMeshProUGUI assignedTmp, GameObject parent, int cur, int max, string slotName)
void SetScoreText(TextMeshProUGUI assignedTmp, GameObject parent, int cur, int max, string slotName, int slotIdx)
{
string value = cur + "/" + max;
// 【性能】值变门控:仅当 (目标 Text 对象, value) 与该槽位上次写入不同才赋值 .text。
// 目标对象或数值任一变化都会写(换局/换场景使 UI 重建时目标对象变,必写一次),
// 因此可见文本与原先逐次无条件赋值完全一致,只是跳过"同对象同值"的重复写。
// 用纯判断函数(非闭包 lambda)避免额外 GC:返回 true 表示需要写并已更新缓存。
bool ShouldWriteSlot(UnityEngine.Object target)
{
if (slotIdx >= 0 && slotIdx < 5 &&
ReferenceEquals(_lastSlotScoreTarget[slotIdx], target) &&
_lastSlotScoreText[slotIdx] == value)
{
return false; // 同对象同值,已是该文本,跳过写入(等效)。
}
if (slotIdx >= 0 && slotIdx < 5)
{
_lastSlotScoreTarget[slotIdx] = target;
_lastSlotScoreText[slotIdx] = value;
}
return true;
}
if (assignedTmp != null)
{
assignedTmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to assigned TMP for {slotName}: '{value}' -> {assignedTmp.gameObject.name}");
if (ShouldWriteSlot(assignedTmp))
{
assignedTmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to assigned TMP for {slotName}: '{value}' -> {assignedTmp.gameObject.name}");
}
return;
}
if (parent != null)
@@ -661,15 +741,21 @@ public class ScoreManager : MonoBehaviour
var tmp = slotTmpFallback[idx];
if (tmp != null)
{
tmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
if (ShouldWriteSlot(tmp))
{
tmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
}
return;
}
var legacy = slotLegacyFallback[idx];
if (legacy != null)
{
legacy.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
if (ShouldWriteSlot(legacy))
{
legacy.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
}
return;
}
}
@@ -678,15 +764,21 @@ public class ScoreManager : MonoBehaviour
var tmp = parent.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
{
tmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
if (ShouldWriteSlot(tmp))
{
tmp.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
}
return;
}
var legacy = parent.GetComponentInChildren<Text>(true);
if (legacy != null)
{
legacy.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
if (ShouldWriteSlot(legacy))
{
legacy.text = value;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
}
return;
}
}
@@ -695,11 +787,11 @@ public class ScoreManager : MonoBehaviour
}
// Update each slot: use the explicit TMP fields if set; otherwise try parent object fields
SetScoreText(ui.teammate01_current_scoreText, ui.objectFather_ally01, tmpCurrents[0], tmpMaxes[0], "teammate01");
SetScoreText(ui.teammate02_current_scoreText, ui.objectFather_ally02, tmpCurrents[1], tmpMaxes[1], "teammate02");
SetScoreText(ui.teammate03_current_scoreText, ui.objectFather_ally03, tmpCurrents[2], tmpMaxes[2], "teammate03");
SetScoreText(ui.teammate04_current_scoreText, ui.objectFather_ally04, tmpCurrents[3], tmpMaxes[3], "teammate04");
SetScoreText(ui.teammate05_current_scoreText, ui.objectFather_ally05, tmpCurrents[4], tmpMaxes[4], "teammate05");
SetScoreText(ui.teammate01_current_scoreText, ui.objectFather_ally01, tmpCurrents[0], tmpMaxes[0], "teammate01", 0);
SetScoreText(ui.teammate02_current_scoreText, ui.objectFather_ally02, tmpCurrents[1], tmpMaxes[1], "teammate02", 1);
SetScoreText(ui.teammate03_current_scoreText, ui.objectFather_ally03, tmpCurrents[2], tmpMaxes[2], "teammate03", 2);
SetScoreText(ui.teammate04_current_scoreText, ui.objectFather_ally04, tmpCurrents[3], tmpMaxes[3], "teammate04", 3);
SetScoreText(ui.teammate05_current_scoreText, ui.objectFather_ally05, tmpCurrents[4], tmpMaxes[4], "teammate05", 4);
// update total score (try TMP first, then legacy Text)
if (ui.currentTotalScore != null)