some updates
This commit is contained in:
@@ -53,6 +53,28 @@ public class ScoreManager : MonoBehaviour
|
||||
private readonly TextMeshProUGUI[] slotTmpFallback = new TextMeshProUGUI[5];
|
||||
private readonly Text[] slotLegacyFallback = new Text[5];
|
||||
private readonly bool[] slotFallbackResolved = new bool[5];
|
||||
|
||||
// 性能:缓存 currentTotalScore 的 TMP/Text 组件,避免每次判定 GetComponent。
|
||||
private TextMeshProUGUI _cachedTotalTmp;
|
||||
private Text _cachedTotalLegacy;
|
||||
private GameObject _cachedTotalGo;
|
||||
// 性能:记录上次写入 UI 的字符串,值未变则跳过 .text 赋值(避免 TMP 无谓网格重建)。
|
||||
private string _lastTotalText;
|
||||
// 性能:per-track pm/idol 合计上次写入值(12 项),值未变跳过 .text,避免每判定重写全部槽位触发网格重建。
|
||||
// 索引:0-5 = pm(red,green,yellow,purple,blue,all),6-11 = idol(同序)。int.MinValue 表示尚未写入。
|
||||
private readonly int[] _lastSumWritten = new int[12] {
|
||||
int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue,
|
||||
int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue
|
||||
};
|
||||
|
||||
// 仅当数值变化时写 TMP.text(避免相同字符串触发无谓网格重建)。
|
||||
private void SetSumTextIfChanged(TextMeshProUGUI field, int value, int cacheIndex)
|
||||
{
|
||||
if (field == null) return;
|
||||
if (_lastSumWritten[cacheIndex] == value) return;
|
||||
_lastSumWritten[cacheIndex] = value;
|
||||
field.text = value.ToString();
|
||||
}
|
||||
private JudgeManager hookedJudgeManager;
|
||||
private bool perfectBonusHooked = false;
|
||||
private int perfectClearBonusPm = 0;
|
||||
@@ -449,34 +471,22 @@ public class ScoreManager : MonoBehaviour
|
||||
if (ui != null)
|
||||
{
|
||||
// Legacy Text fields on teamUIController
|
||||
// 性能:仅在数值变化时写(SetSumTextIfChanged),避免每判定重写 12 个槽位触发 TMP 网格重建。
|
||||
try
|
||||
{
|
||||
string redPmText = red_pmScore_sum.ToString();
|
||||
string greenPmText = green_pmScore_sum.ToString();
|
||||
string yellowPmText = yellow_pmScore_sum.ToString();
|
||||
string purplePmText = purple_pmScore_sum.ToString();
|
||||
string bluePmText = blue_pmScore_sum.ToString();
|
||||
string allPmText = allSum_pmScore.ToString();
|
||||
string redIdolText = red_idolScore_sum.ToString();
|
||||
string greenIdolText = green_idolScore_sum.ToString();
|
||||
string yellowIdolText = yellow_idolScore_sum.ToString();
|
||||
string purpleIdolText = purple_idolScore_sum.ToString();
|
||||
string blueIdolText = blue_idolScore_sum.ToString();
|
||||
string allIdolText = allSum_idolScore.ToString();
|
||||
SetSumTextIfChanged(ui.red_pmScore_sum, red_pmScore_sum, 0);
|
||||
SetSumTextIfChanged(ui.green_pmScore_sum, green_pmScore_sum, 1);
|
||||
SetSumTextIfChanged(ui.yellow_pmScore_sum, yellow_pmScore_sum, 2);
|
||||
SetSumTextIfChanged(ui.purple_pmScore_sum, purple_pmScore_sum, 3);
|
||||
SetSumTextIfChanged(ui.blue_pmScore_sum, blue_pmScore_sum, 4);
|
||||
SetSumTextIfChanged(ui.allSum_pmScore, allSum_pmScore, 5);
|
||||
|
||||
if (ui.red_pmScore_sum != null) ui.red_pmScore_sum.text = redPmText;
|
||||
if (ui.green_pmScore_sum != null) ui.green_pmScore_sum.text = greenPmText;
|
||||
if (ui.yellow_pmScore_sum != null) ui.yellow_pmScore_sum.text = yellowPmText;
|
||||
if (ui.purple_pmScore_sum != null) ui.purple_pmScore_sum.text = purplePmText;
|
||||
if (ui.blue_pmScore_sum != null) ui.blue_pmScore_sum.text = bluePmText;
|
||||
if (ui.allSum_pmScore != null) ui.allSum_pmScore.text = allPmText;
|
||||
|
||||
if (ui.red_idolScore_sum != null) ui.red_idolScore_sum.text = redIdolText;
|
||||
if (ui.green_idolScore_sum != null) ui.green_idolScore_sum.text = greenIdolText;
|
||||
if (ui.yellow_idolScore_sum != null) ui.yellow_idolScore_sum.text = yellowIdolText;
|
||||
if (ui.purple_idolScore_sum != null) ui.purple_idolScore_sum.text = purpleIdolText;
|
||||
if (ui.blue_idolScore_sum != null) ui.blue_idolScore_sum.text = blueIdolText;
|
||||
if (ui.allSum_idolScore != null) ui.allSum_idolScore.text = allIdolText;
|
||||
SetSumTextIfChanged(ui.red_idolScore_sum, red_idolScore_sum, 6);
|
||||
SetSumTextIfChanged(ui.green_idolScore_sum, green_idolScore_sum, 7);
|
||||
SetSumTextIfChanged(ui.yellow_idolScore_sum, yellow_idolScore_sum, 8);
|
||||
SetSumTextIfChanged(ui.purple_idolScore_sum, purple_idolScore_sum, 9);
|
||||
SetSumTextIfChanged(ui.blue_idolScore_sum, blue_idolScore_sum, 10);
|
||||
SetSumTextIfChanged(ui.allSum_idolScore, allSum_idolScore, 11);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
@@ -681,23 +691,33 @@ public class ScoreManager : MonoBehaviour
|
||||
var go = ui.currentTotalScore.gameObject;
|
||||
if (go != null)
|
||||
{
|
||||
var tmpComp = go.GetComponent<TextMeshProUGUI>();
|
||||
if (tmpComp != null)
|
||||
// 性能:仅在目标物体变化时 GetComponent(缓存),避免每次判定重复反射查找。
|
||||
if (_cachedTotalGo != go)
|
||||
{
|
||||
tmpComp.text = totalScore.ToString();
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to TMP: {totalScore} -> {tmpComp.gameObject.name}");
|
||||
_cachedTotalGo = go;
|
||||
_cachedTotalTmp = go.GetComponent<TextMeshProUGUI>();
|
||||
_cachedTotalLegacy = _cachedTotalTmp == null ? go.GetComponent<Text>() : null;
|
||||
_lastTotalText = null; // 目标物体变了(如新一局/换场景),强制写一次
|
||||
}
|
||||
else
|
||||
|
||||
string totalText = totalScore.ToString();
|
||||
// 性能:值未变则跳过 .text 赋值,避免 TMP 无谓网格重建。
|
||||
if (totalText != _lastTotalText)
|
||||
{
|
||||
var legacyComp = go.GetComponent<Text>();
|
||||
if (legacyComp != null)
|
||||
_lastTotalText = totalText;
|
||||
if (_cachedTotalTmp != null)
|
||||
{
|
||||
legacyComp.text = totalScore.ToString();
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to legacy Text: {totalScore} -> {legacyComp.gameObject.name}");
|
||||
_cachedTotalTmp.text = totalText;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to TMP: {totalScore} -> {_cachedTotalTmp.gameObject.name}");
|
||||
}
|
||||
else if (_cachedTotalLegacy != null)
|
||||
{
|
||||
_cachedTotalLegacy.text = totalText;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to legacy Text: {totalScore} -> {_cachedTotalLegacy.gameObject.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ui.currentTotalScore.text = totalScore.ToString(); // fallback
|
||||
ui.currentTotalScore.text = totalText; // fallback
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to currentTotalScore field fallback: {totalScore}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public class SpriteNumberDisplay : MonoBehaviour
|
||||
{
|
||||
[Header("Sprites")]
|
||||
public Sprite[] digitSprites = new Sprite[10];
|
||||
public Sprite decimalPointSprite;
|
||||
public Sprite percentSprite;
|
||||
public Sprite plusSprite;
|
||||
public Sprite minusSprite;
|
||||
|
||||
[Header("Layout")]
|
||||
public Vector2 digitSize = new Vector2(28f, 42f);
|
||||
public Vector2 decimalPointSize = new Vector2(12f, 42f);
|
||||
public Vector2 percentSize = new Vector2(28f, 42f);
|
||||
public float spacing = 2f;
|
||||
public bool centerAlign = true;
|
||||
public bool preserveAspect = true;
|
||||
public Color color = Color.white;
|
||||
[Tooltip("Canvas sorting order used by generated character sprites.")]
|
||||
public int characterSortingOrder = 0;
|
||||
|
||||
private readonly List<Image> glyphImages = new List<Image>();
|
||||
private string currentText = string.Empty;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
digitSize.x = Mathf.Max(0f, digitSize.x);
|
||||
digitSize.y = Mathf.Max(0f, digitSize.y);
|
||||
decimalPointSize.x = Mathf.Max(0f, decimalPointSize.x);
|
||||
decimalPointSize.y = Mathf.Max(0f, decimalPointSize.y);
|
||||
percentSize.x = Mathf.Max(0f, percentSize.x);
|
||||
percentSize.y = Mathf.Max(0f, percentSize.y);
|
||||
}
|
||||
|
||||
public void SetNumber(int value)
|
||||
{
|
||||
SetText(value.ToString());
|
||||
}
|
||||
|
||||
public void SetText(string value)
|
||||
{
|
||||
string next = value ?? string.Empty;
|
||||
if (currentText == next && glyphImages.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
currentText = next;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SetText(string.Empty);
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
EnsureImageCount(currentText.Length);
|
||||
|
||||
float totalWidth = CalculateTotalWidth(currentText);
|
||||
float x = centerAlign ? -totalWidth * 0.5f : 0f;
|
||||
|
||||
for (int i = 0; i < glyphImages.Count; i++)
|
||||
{
|
||||
Image image = glyphImages[i];
|
||||
if (image == null) continue;
|
||||
|
||||
if (i >= currentText.Length)
|
||||
{
|
||||
image.gameObject.SetActive(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
char c = currentText[i];
|
||||
Sprite sprite = ResolveSprite(c);
|
||||
Vector2 size = ResolveSize(c);
|
||||
|
||||
if (sprite == null)
|
||||
{
|
||||
image.gameObject.SetActive(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
RectTransform rect = image.rectTransform;
|
||||
image.gameObject.SetActive(true);
|
||||
image.sprite = sprite;
|
||||
image.color = color;
|
||||
image.preserveAspect = preserveAspect;
|
||||
ApplyCharacterSortingOrder(image);
|
||||
rect.sizeDelta = size;
|
||||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
rect.pivot = new Vector2(0.5f, 0.5f);
|
||||
rect.anchoredPosition = new Vector2(x + size.x * 0.5f, 0f);
|
||||
|
||||
x += size.x + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyCharacterSortingOrder(Image image)
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Canvas canvas = image.GetComponent<Canvas>();
|
||||
if (canvas == null)
|
||||
{
|
||||
canvas = image.gameObject.AddComponent<Canvas>();
|
||||
}
|
||||
|
||||
canvas.overrideSorting = true;
|
||||
canvas.sortingOrder = characterSortingOrder;
|
||||
}
|
||||
|
||||
private void EnsureImageCount(int count)
|
||||
{
|
||||
while (glyphImages.Count < count)
|
||||
{
|
||||
GameObject glyph = new GameObject("digit_" + glyphImages.Count, typeof(RectTransform), typeof(Canvas), typeof(Image));
|
||||
RectTransform rect = glyph.transform as RectTransform;
|
||||
rect.SetParent(transform, false);
|
||||
|
||||
Image image = glyph.GetComponent<Image>();
|
||||
image.raycastTarget = false;
|
||||
glyphImages.Add(image);
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculateTotalWidth(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float total = 0f;
|
||||
int visibleCount = 0;
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
if (ResolveSprite(text[i]) == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
total += ResolveSize(text[i]).x;
|
||||
visibleCount++;
|
||||
}
|
||||
|
||||
if (visibleCount > 1)
|
||||
{
|
||||
total += spacing * (visibleCount - 1);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private Sprite ResolveSprite(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9')
|
||||
{
|
||||
int index = c - '0';
|
||||
return digitSprites != null && index < digitSprites.Length ? digitSprites[index] : null;
|
||||
}
|
||||
|
||||
if (c == '.') return decimalPointSprite;
|
||||
if (c == '%') return percentSprite;
|
||||
if (c == '+') return plusSprite;
|
||||
if (c == '-') return minusSprite;
|
||||
return null;
|
||||
}
|
||||
|
||||
private Vector2 ResolveSize(char c)
|
||||
{
|
||||
if (c == '.') return decimalPointSize;
|
||||
if (c == '%') return percentSize;
|
||||
return digitSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6449b17c0aa73cd488eb1730ae930361
|
||||
@@ -85,6 +85,14 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
public float trackSkillUiScaleMultiplier = 0.01f;
|
||||
public int trackSkillSortingOrder = 9000;
|
||||
|
||||
[Header("Track Skill Burst Stagger")]
|
||||
[Tooltip("If multiple track-skill popups spawn within this time window, increase later popups' Y launch speed to reduce overlap.")]
|
||||
public float trackSkillBurstWindow = 0.08f;
|
||||
[Tooltip("Extra jump force added per near-simultaneous popup.")]
|
||||
public float trackSkillBurstJumpForceStep = 0.65f;
|
||||
[Tooltip("Maximum burst step applied to later popups.")]
|
||||
public int trackSkillBurstMaxStep = 3;
|
||||
|
||||
private Transform redSpawn;
|
||||
private Transform greenSpawn;
|
||||
private Transform yellowSpawn;
|
||||
@@ -95,6 +103,8 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
private Coroutine prewarmJudgeCoroutine;
|
||||
private readonly Dictionary<GameObject, Stack<GameObject>> judgePools = new Dictionary<GameObject, Stack<GameObject>>();
|
||||
private readonly HashSet<GameObject> rentedJudges = new HashSet<GameObject>();
|
||||
private float lastTrackSkillSpawnTime = -999f;
|
||||
private int trackSkillBurstCount = 0;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -230,7 +240,33 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
ApplyTrackSkillScale(instance, finalScale);
|
||||
instance.SetActive(true);
|
||||
|
||||
StartCoroutine(AnimatePopup(instance, trackSkillPopup, true));
|
||||
PopupMotionSettings popupSettings = CreateTrackSkillPopupSettings();
|
||||
StartCoroutine(AnimatePopup(instance, popupSettings, true));
|
||||
}
|
||||
|
||||
private PopupMotionSettings CreateTrackSkillPopupSettings()
|
||||
{
|
||||
PopupMotionSettings popupSettings = new PopupMotionSettings
|
||||
{
|
||||
jumpForce = trackSkillPopup.jumpForce,
|
||||
gravity = trackSkillPopup.gravity,
|
||||
horizontalDriftRange = trackSkillPopup.horizontalDriftRange,
|
||||
rotationRange = trackSkillPopup.rotationRange,
|
||||
scalePunch = trackSkillPopup.scalePunch,
|
||||
scalePunchDuration = trackSkillPopup.scalePunchDuration,
|
||||
fadeInTime = trackSkillPopup.fadeInTime,
|
||||
fadeOutStartTime = trackSkillPopup.fadeOutStartTime,
|
||||
fadeOutDuration = trackSkillPopup.fadeOutDuration
|
||||
};
|
||||
|
||||
float now = Time.unscaledTime;
|
||||
float window = Mathf.Max(0f, trackSkillBurstWindow);
|
||||
trackSkillBurstCount = now - lastTrackSkillSpawnTime <= window ? trackSkillBurstCount + 1 : 0;
|
||||
lastTrackSkillSpawnTime = now;
|
||||
|
||||
int appliedStep = Mathf.Clamp(trackSkillBurstCount, 0, Mathf.Max(0, trackSkillBurstMaxStep));
|
||||
popupSettings.jumpForce += Mathf.Max(0f, trackSkillBurstJumpForceStep) * appliedStep;
|
||||
return popupSettings;
|
||||
}
|
||||
|
||||
public void PrewarmJudgePrefabs(int perPrefab = 1)
|
||||
|
||||
@@ -29,6 +29,8 @@ public sealed class CameraResolutionTuner : MonoBehaviour
|
||||
|
||||
[Header("21:9 时相机本体(Main Camera)的目标 local 位姿(最大偏移)")]
|
||||
[Tooltip("21:9 时相机本体的目标 local Z 高度。用户指定 2.5(相机自身 z=2.5)。")]
|
||||
public float targetCameraY = 0f;
|
||||
|
||||
public float targetCameraZ = 2.5f;
|
||||
|
||||
[Tooltip("21:9 时相机本体的目标 local X 旋转角度。用户指定 5(相机自身 X=5°)。")]
|
||||
@@ -67,7 +69,7 @@ public sealed class CameraResolutionTuner : MonoBehaviour
|
||||
if (!enableTuning)
|
||||
{
|
||||
// 关闭:t=0 → 相机本体回到 local 设计基准(运行时为 0/identity)。
|
||||
_applied = eff.SetResolutionCameraSelf(0f, targetCameraRotX, targetCameraZ);
|
||||
_applied = eff.SetResolutionCameraSelf(0f, targetCameraRotX, targetCameraZ, targetCameraY);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,6 +92,6 @@ public sealed class CameraResolutionTuner : MonoBehaviour
|
||||
}
|
||||
// aspect <= designAspect:t=0(无微调)。
|
||||
|
||||
_applied = eff.SetResolutionCameraSelf(t, targetCameraRotX, targetCameraZ);
|
||||
_applied = eff.SetResolutionCameraSelf(t, targetCameraRotX, targetCameraZ, targetCameraY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,6 +674,19 @@ public class HoldNote : BaseNote
|
||||
&& jm != null && jm.IsStartJudged(noteID)
|
||||
&& !jm.HasNoteReleased(noteID))
|
||||
{
|
||||
if (keyHeld && now >= scheduledEndTime)
|
||||
{
|
||||
if (debugEnabled) Debug.Log($"[HoldNote] END auto-complete on hold: {noteColor}, noteID={noteID}, now={now:F3}, scheduledEnd={scheduledEndTime:F3}");
|
||||
releaseTime = scheduledEndTime;
|
||||
hasReleased = true;
|
||||
jm.RegisterNoteReleased(noteID, true);
|
||||
isHoldActive = false;
|
||||
StopHoldFxLoop();
|
||||
EvaluateHoldEnd(scheduledEndTime, false, true);
|
||||
isJudged = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyUp)
|
||||
{
|
||||
// protect end handling with track lock so a single release doesn't trigger multiple ends
|
||||
@@ -834,11 +847,11 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] End entered while note not yet released: {noteColor}, auto-finishing hold as Perfect");
|
||||
// 记录松手时间为当前时刻(玩家按到尾部视为完美松手)
|
||||
releaseTime = GameplayClock.NowSongTime;
|
||||
releaseTime = scheduledEndTime;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
// 判定并生成 prefab——force Perfect(按到尾部=完美完成)
|
||||
EvaluateHoldEnd(releaseTime, false, true);
|
||||
EvaluateHoldEnd(scheduledEndTime, false, true);
|
||||
isJudged = true;
|
||||
// 终止输入:标记键已松开,停止持续特效
|
||||
isHoldActive = false;
|
||||
@@ -1299,6 +1312,8 @@ public class HoldNote : BaseNote
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
}
|
||||
|
||||
teamUIController.Instance?.RefreshRealtimeJudgeStatsText();
|
||||
|
||||
if (result != "Miss")
|
||||
{
|
||||
TrackJudgeHitEffectController.PlayTrackHitFx(trackIndex);
|
||||
|
||||
@@ -143,6 +143,7 @@ public class InputManager : MonoBehaviour
|
||||
/// Begin holding a track (0-4). Called by keyboard Update on key-down and by touch
|
||||
/// regions on pointer-down. Sets the held-state table, raises OnKeyPressed with the
|
||||
/// track's bound key so event-driven tap notes keep working, and updates lane visuals.
|
||||
/// Always fires OnKeyPressed to allow rapid same-track taps even when held.
|
||||
/// </summary>
|
||||
public void PressTrack(int index)
|
||||
{
|
||||
@@ -150,12 +151,14 @@ public class InputManager : MonoBehaviour
|
||||
bool wasHeld = trackHeldCount[index] > 0;
|
||||
trackHeldCount[index]++;
|
||||
|
||||
if (wasHeld) return; // already held; avoid duplicate press events
|
||||
|
||||
// Always fire tap judgment pulse, even if already held (fixes rapid same-track taps)
|
||||
KeyCode key = cachedKeys[index];
|
||||
if (key != KeyCode.None)
|
||||
OnKeyPressed?.Invoke(key);
|
||||
|
||||
// Only update visuals on the first press (0→1 transition)
|
||||
if (wasHeld) return;
|
||||
|
||||
if (laneSprites != null && index < laneSprites.Length)
|
||||
SetSpriteAlpha(laneSprites[index], pressedAlpha);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 088ed1ed6b6731f43ad3166781e3e931, type: 3}
|
||||
m_Name: NoteJudgeConfig
|
||||
m_EditorClassIdentifier:
|
||||
perfectRange: 0.1
|
||||
greatRange: 0.15
|
||||
goodRange: 0.2
|
||||
missRange: 0.25
|
||||
perfectRange: 0.075
|
||||
greatRange: 0.1
|
||||
goodRange: 0.125
|
||||
missRange: 0.15
|
||||
|
||||
@@ -254,7 +254,7 @@ public class Note : BaseNote
|
||||
{
|
||||
if (GameplayClock.NowSongTime >= missDeadlineTime)
|
||||
{
|
||||
Debug.Log($"[Note.Update] Forcing miss for {noteColor} on track {TrackIndex}: now={GameplayClock.NowSongTime:F3}, deadline={missDeadlineTime:F3}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.Update] Forcing miss for {noteColor} on track {TrackIndex}: now={GameplayClock.NowSongTime:F3}, deadline={missDeadlineTime:F3}");
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
@@ -563,12 +563,12 @@ public class Note : BaseNote
|
||||
|
||||
public void JudgeMiss()
|
||||
{
|
||||
Debug.Log($"[Note.JudgeMiss] Called for {noteColor} on track {TrackIndex}, isJudged={isJudged}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.JudgeMiss] Called for {noteColor} on track {TrackIndex}, isJudged={isJudged}");
|
||||
|
||||
// idempotent: this can be called from multiple paths
|
||||
if (isJudged)
|
||||
{
|
||||
Debug.Log($"[Note.JudgeMiss] Already judged, skipping");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.JudgeMiss] Already judged, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -588,20 +588,20 @@ public class Note : BaseNote
|
||||
|
||||
// 生成 Miss 判定 prefab(调试日志无条件输出,便于排查问题)
|
||||
string colorForPrefab = GetColorForPrefab();
|
||||
Debug.Log($"[Note.JudgeMiss] Attempting to spawn miss prefab: noteColor={noteColor}, TrackIndex={TrackIndex}, derivedColor={colorForPrefab}, Instance={(Animation_GenerateJudgementSituationPrefab.Instance != null ? "EXISTS" : "NULL")}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.JudgeMiss] Attempting to spawn miss prefab: noteColor={noteColor}, TrackIndex={TrackIndex}, derivedColor={colorForPrefab}, Instance={(Animation_GenerateJudgementSituationPrefab.Instance != null ? "EXISTS" : "NULL")}");
|
||||
|
||||
if (string.IsNullOrEmpty(colorForPrefab))
|
||||
{
|
||||
Debug.LogError($"[Note.JudgeMiss] Cannot spawn miss prefab: colorForPrefab is null! noteColor={noteColor}, TrackIndex={TrackIndex}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogError($"[Note.JudgeMiss] Cannot spawn miss prefab: colorForPrefab is null! noteColor={noteColor}, TrackIndex={TrackIndex}");
|
||||
}
|
||||
else if (Animation_GenerateJudgementSituationPrefab.Instance == null)
|
||||
{
|
||||
Debug.LogError($"[Note.JudgeMiss] Cannot spawn miss prefab: Animation_GenerateJudgementSituationPrefab.Instance is NULL!");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogError($"[Note.JudgeMiss] Cannot spawn miss prefab: Animation_GenerateJudgementSituationPrefab.Instance is NULL!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Animation_GenerateJudgementSituationPrefab.Instance.SpawnJudgePrefab(colorForPrefab, "Miss");
|
||||
Debug.Log($"[Note.JudgeMiss] SpawnJudgePrefab called successfully for {colorForPrefab}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.JudgeMiss] SpawnJudgePrefab called successfully for {colorForPrefab}");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -674,7 +674,7 @@ public class Note : BaseNote
|
||||
/// </summary>
|
||||
public void SetJudgeZone(bool inZone)
|
||||
{
|
||||
Debug.Log($"[Note.SetJudgeZone] {noteColor} on track {TrackIndex}: inZone={inZone}, isJudged={isJudged}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.SetJudgeZone] {noteColor} on track {TrackIndex}: inZone={inZone}, isJudged={isJudged}");
|
||||
|
||||
// leaving judge zone
|
||||
if (!inZone)
|
||||
@@ -687,7 +687,7 @@ public class Note : BaseNote
|
||||
// Calling ReturnToPool (which deactivates the object) inside a physics callback
|
||||
// causes "GameObject is already being activated or deactivated" errors.
|
||||
// Instead, mark for miss and let Update handle it next frame.
|
||||
Debug.Log($"[Note.SetJudgeZone] Note {noteColor} left judge zone on track {TrackIndex}, setting deadline to now");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.SetJudgeZone] Note {noteColor} left judge zone on track {TrackIndex}, setting deadline to now");
|
||||
missDeadlineTime = GameplayClock.NowSongTime; // Force deadline to now so Update will handle it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,124 +7,111 @@ using ETouch = UnityEngine.InputSystem.EnhancedTouch.Touch;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 手游触屏输入分发器:每帧遍历所有触摸点(及编辑器鼠标),用 Physics2D.OverlapPoint
|
||||
/// 命中带 TrackTouchZone 的透明 Collider2D,把按下/抬起转发给 InputManager。
|
||||
///
|
||||
/// 【多点触控根因】项目 activeInputHandler = Both(同时启用新旧输入系统)。
|
||||
/// 在这种配置下,旧版 UnityEngine.Input.touches 走的是兼容 shim,同时按多指时
|
||||
/// 常常只上报一个触点 → 只能触发一个轨道。所以这里优先用新输入系统的
|
||||
/// EnhancedTouch(Touch.activeTouches),它能可靠上报所有并发手指;
|
||||
/// 仅在没有新输入系统时回退到旧版 Input.touches。判定逻辑完全不变。
|
||||
///
|
||||
/// 为什么用物理命中而不是 UI 的 IPointerDownHandler:
|
||||
/// - 轨道会平移+旋转晃动,点击区必须跟随晃动的 Collider(OverlapPoint 支持旋转过的碰撞体)。
|
||||
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 touchId 配对按下/抬起)。
|
||||
///
|
||||
/// 判定时间戳仍由 Note.HandlePress 读取 GameplayClock.NowSongTime(dspTime)。
|
||||
///
|
||||
/// 用法:场景里放一个空物体挂本脚本,把主相机拖到 gameplayCamera(留空则自动取 Camera.main)。
|
||||
/// 5 块透明轨道点击区各挂 TrackTouchZone 并设置 trackIndex。
|
||||
/// Mobile touch dispatcher for the five gameplay tracks.
|
||||
/// PC builds disable this component immediately and use keyboard input only.
|
||||
/// </summary>
|
||||
public class TrackTouchInput : MonoBehaviour
|
||||
{
|
||||
[Tooltip("渲染 gameplay 的相机。留空则自动使用 Camera.main。")]
|
||||
[SerializeField] private Camera gameplayCamera;
|
||||
|
||||
[Tooltip("是否在编辑器/PC 上用鼠标模拟单点触摸(方便调试)。")]
|
||||
[SerializeField] private bool enableMouseFallback = true;
|
||||
|
||||
[Tooltip("命中检测使用的层。默认 Everything;如需只检测点击区层可在此限制。")]
|
||||
[SerializeField] private bool enableMouseFallback = false;
|
||||
[Tooltip("推荐:给 5 个 TrackTouchZone 物体单独设一个 layer(比如 layer 10 TouchZone),这里只勾该 layer,避免音符/背景碰撞体挤爆缓冲导致间歇漏点")]
|
||||
[SerializeField] private LayerMask hitLayers = ~0;
|
||||
|
||||
[Tooltip("透视相机必填:轨道点击区所在平面的世界 Z 坐标(轨道 Collider 的 Z)。\n" +
|
||||
"透视相机下屏幕点转世界点依赖深度,用它把射线交到轨道平面,相机怎么晃/推拉都精确。\n" +
|
||||
"正交相机可忽略此项。")]
|
||||
[SerializeField] private float trackPlaneWorldZ = 0f;
|
||||
|
||||
[Tooltip("相机是否为正交投影。正交相机忽略深度,命中转换更简单。\n" +
|
||||
"留空自动按相机 orthographic 判断;一般不用手动改。")]
|
||||
[SerializeField] private bool forceOrthographicMode = false;
|
||||
|
||||
// 触点 id -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
|
||||
private readonly Dictionary<int, int> _activeTouches = new Dictionary<int, int>();
|
||||
private int _mouseHeldTrack = -1;
|
||||
private readonly Dictionary<int, int> activeTouches = new Dictionary<int, int>();
|
||||
private int mouseHeldTrack = -1;
|
||||
|
||||
// 缓冲从 16 扩到 64:密集音符流量下防止 TrackTouchZone 被挤出 OverlapPoint 结果
|
||||
private static readonly Collider2D[] overlapResults = new Collider2D[64];
|
||||
private ContactFilter2D overlapFilter;
|
||||
private bool filterReady;
|
||||
|
||||
private Camera Cam
|
||||
{
|
||||
get
|
||||
{
|
||||
if (gameplayCamera == null) gameplayCamera = Camera.main;
|
||||
if (gameplayCamera == null)
|
||||
gameplayCamera = Camera.main;
|
||||
return gameplayCamera;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldProcessTouchInput()
|
||||
{
|
||||
#if UNITY_ANDROID || UNITY_IOS
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (!ShouldProcessTouchInput())
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
private void OnEnable()
|
||||
{
|
||||
// 开启增强触摸,Touch.activeTouches 才会被填充。
|
||||
if (!ShouldProcessTouchInput())
|
||||
return;
|
||||
|
||||
EnhancedTouchSupport.Enable();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void Update()
|
||||
{
|
||||
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
|
||||
// Windows 平台:禁用轨道点击/触摸轮询,只依赖按键输入。
|
||||
// 编辑器(Win)也一并禁用,便于在 PC 上以键盘方式测试真实 Windows 行为。
|
||||
return;
|
||||
#else
|
||||
var im = InputManager.Instance;
|
||||
if (im == null) return;
|
||||
if (!ShouldProcessTouchInput())
|
||||
return;
|
||||
|
||||
InputManager inputManager = InputManager.Instance;
|
||||
Camera cam = Cam;
|
||||
if (cam == null) return;
|
||||
|
||||
int touchCount = ProcessTouches(im, cam);
|
||||
if (inputManager == null || cam == null)
|
||||
return;
|
||||
|
||||
int touchCount = ProcessTouches(inputManager, cam);
|
||||
if (enableMouseFallback && touchCount == 0)
|
||||
{
|
||||
ProcessMouse(im, cam);
|
||||
ProcessMouse(inputManager, cam);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>处理所有并发触点,返回本帧触点数量。</summary>
|
||||
private int ProcessTouches(InputManager im, Camera cam)
|
||||
private int ProcessTouches(InputManager inputManager, Camera cam)
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
// 优先走新输入系统的增强触摸:可靠上报所有并发手指。
|
||||
var touches = ETouch.activeTouches;
|
||||
int count = touches.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ETouch t = touches[i];
|
||||
switch (t.phase)
|
||||
ETouch touch = touches[i];
|
||||
switch (touch.phase)
|
||||
{
|
||||
case UnityEngine.InputSystem.TouchPhase.Began:
|
||||
HandleTouchBegan(im, cam, t.touchId, t.screenPosition);
|
||||
HandleTouchBegan(inputManager, cam, touch.touchId, touch.screenPosition);
|
||||
break;
|
||||
case UnityEngine.InputSystem.TouchPhase.Ended:
|
||||
case UnityEngine.InputSystem.TouchPhase.Canceled:
|
||||
HandleTouchEnded(im, t.touchId);
|
||||
HandleTouchEnded(inputManager, touch.touchId);
|
||||
break;
|
||||
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属。
|
||||
}
|
||||
}
|
||||
return count;
|
||||
#else
|
||||
// 回退:旧版 Input.touches(仅在未启用新输入系统时使用)。
|
||||
int count = Input.touchCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Touch t = Input.GetTouch(i);
|
||||
switch (t.phase)
|
||||
Touch touch = Input.GetTouch(i);
|
||||
switch (touch.phase)
|
||||
{
|
||||
case TouchPhase.Began:
|
||||
HandleTouchBegan(im, cam, t.fingerId, t.position);
|
||||
HandleTouchBegan(inputManager, cam, touch.fingerId, touch.position);
|
||||
break;
|
||||
case TouchPhase.Ended:
|
||||
case TouchPhase.Canceled:
|
||||
HandleTouchEnded(im, t.fingerId);
|
||||
HandleTouchEnded(inputManager, touch.fingerId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -132,47 +119,45 @@ public class TrackTouchInput : MonoBehaviour
|
||||
#endif
|
||||
}
|
||||
|
||||
private void HandleTouchBegan(InputManager im, Camera cam, int touchId, Vector2 screenPos)
|
||||
private void HandleTouchBegan(InputManager inputManager, Camera cam, int touchId, Vector2 screenPosition)
|
||||
{
|
||||
int track = ResolveTrack(cam, screenPos);
|
||||
if (track >= 0)
|
||||
{
|
||||
_activeTouches[touchId] = track;
|
||||
im.PressTrack(track);
|
||||
}
|
||||
int track = ResolveTrack(cam, screenPosition);
|
||||
if (track < 0)
|
||||
return;
|
||||
|
||||
activeTouches[touchId] = track;
|
||||
inputManager.PressTrack(track);
|
||||
}
|
||||
|
||||
private void HandleTouchEnded(InputManager im, int touchId)
|
||||
private void HandleTouchEnded(InputManager inputManager, int touchId)
|
||||
{
|
||||
if (_activeTouches.TryGetValue(touchId, out int track))
|
||||
{
|
||||
_activeTouches.Remove(touchId);
|
||||
im.ReleaseTrack(track);
|
||||
}
|
||||
if (!activeTouches.TryGetValue(touchId, out int track))
|
||||
return;
|
||||
|
||||
activeTouches.Remove(touchId);
|
||||
inputManager.ReleaseTrack(track);
|
||||
}
|
||||
|
||||
private void ProcessMouse(InputManager im, Camera cam)
|
||||
private void ProcessMouse(InputManager inputManager, Camera cam)
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
var mouse = Mouse.current;
|
||||
if (mouse == null) return;
|
||||
Mouse mouse = Mouse.current;
|
||||
if (mouse == null)
|
||||
return;
|
||||
|
||||
if (mouse.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
int track = ResolveTrack(cam, mouse.position.ReadValue());
|
||||
if (track >= 0)
|
||||
{
|
||||
_mouseHeldTrack = track;
|
||||
im.PressTrack(track);
|
||||
mouseHeldTrack = track;
|
||||
inputManager.PressTrack(track);
|
||||
}
|
||||
}
|
||||
else if (mouse.leftButton.wasReleasedThisFrame)
|
||||
else if (mouse.leftButton.wasReleasedThisFrame && mouseHeldTrack >= 0)
|
||||
{
|
||||
if (_mouseHeldTrack >= 0)
|
||||
{
|
||||
im.ReleaseTrack(_mouseHeldTrack);
|
||||
_mouseHeldTrack = -1;
|
||||
}
|
||||
inputManager.ReleaseTrack(mouseHeldTrack);
|
||||
mouseHeldTrack = -1;
|
||||
}
|
||||
#else
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
@@ -180,123 +165,118 @@ public class TrackTouchInput : MonoBehaviour
|
||||
int track = ResolveTrack(cam, Input.mousePosition);
|
||||
if (track >= 0)
|
||||
{
|
||||
_mouseHeldTrack = track;
|
||||
im.PressTrack(track);
|
||||
mouseHeldTrack = track;
|
||||
inputManager.PressTrack(track);
|
||||
}
|
||||
}
|
||||
else if (Input.GetMouseButtonUp(0))
|
||||
else if (Input.GetMouseButtonUp(0) && mouseHeldTrack >= 0)
|
||||
{
|
||||
if (_mouseHeldTrack >= 0)
|
||||
{
|
||||
im.ReleaseTrack(_mouseHeldTrack);
|
||||
_mouseHeldTrack = -1;
|
||||
}
|
||||
inputManager.ReleaseTrack(mouseHeldTrack);
|
||||
mouseHeldTrack = -1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把屏幕坐标转换为轨道平面上的世界点,用 OverlapPoint 命中轨道点击区,返回轨道索引;无命中返回 -1。
|
||||
///
|
||||
/// 相机移动/晃动/推拉时,每帧都用当前相机状态转换,所以命中自动跟随相机。
|
||||
/// - 正交相机:ScreenToWorldPoint 忽略深度,直接取 x/y。
|
||||
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点。
|
||||
/// </summary>
|
||||
private int ResolveTrack(Camera cam, Vector2 screenPos)
|
||||
private int ResolveTrack(Camera cam, Vector2 screenPosition)
|
||||
{
|
||||
EnsureOverlapFilter();
|
||||
Vector2 point;
|
||||
|
||||
Vector2 worldPoint;
|
||||
if (forceOrthographicMode || cam.orthographic)
|
||||
{
|
||||
Vector3 world = cam.ScreenToWorldPoint(screenPos);
|
||||
point = new Vector2(world.x, world.y);
|
||||
Vector3 world = cam.ScreenToWorldPoint(screenPosition);
|
||||
worldPoint = new Vector2(world.x, world.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
Ray ray = cam.ScreenPointToRay(screenPos);
|
||||
Ray ray = cam.ScreenPointToRay(screenPosition);
|
||||
float denom = ray.direction.z;
|
||||
if (Mathf.Abs(denom) < 1e-6f)
|
||||
{
|
||||
return -1; // 射线与平面近乎平行,无有效交点
|
||||
}
|
||||
return -1;
|
||||
|
||||
float t = (trackPlaneWorldZ - ray.origin.z) / denom;
|
||||
if (t < 0f)
|
||||
{
|
||||
return -1; // 交点在相机背后
|
||||
}
|
||||
return -1;
|
||||
|
||||
Vector3 world = ray.origin + ray.direction * t;
|
||||
point = new Vector2(world.x, world.y);
|
||||
worldPoint = new Vector2(world.x, world.y);
|
||||
}
|
||||
|
||||
// 用 OverlapPointAll 遍历所有命中的碰撞体,挑出带 TrackTouchZone 的那个。
|
||||
// 关键修复:判定线附近同一屏幕点常密集着 note 的 Collider2D 与 judgeline 触发器,
|
||||
// 单值版 OverlapPoint 只返回其中一个,若不是 tap zone 就丢判定(=点了轨道却没反应)。
|
||||
// 遍历全部命中即可穿透这些遮挡,可靠找到轨道点击区。
|
||||
int count = Physics2D.OverlapPoint(point, _overlapFilter, _overlapResults);
|
||||
int count = Physics2D.OverlapPoint(worldPoint, overlapFilter, overlapResults);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Collider2D hit = _overlapResults[i];
|
||||
if (hit == null) continue;
|
||||
Collider2D hit = overlapResults[i];
|
||||
if (hit == null)
|
||||
continue;
|
||||
|
||||
TrackTouchZone zone = hit.GetComponent<TrackTouchZone>();
|
||||
if (zone == null) zone = hit.GetComponentInParent<TrackTouchZone>();
|
||||
if (zone != null) return zone.trackIndex;
|
||||
if (zone == null)
|
||||
zone = hit.GetComponentInParent<TrackTouchZone>();
|
||||
if (zone != null)
|
||||
return zone.trackIndex;
|
||||
}
|
||||
|
||||
// 兜底:没有精确命中任何 zone(点落在轨道间缝隙或紧邻边缘)。
|
||||
// 取横向最近的 zone,只要触点在其 Collider 的纵向范围内即算命中——
|
||||
// 等效于把相邻轨道点击区在横向补满、消除缝隙,且不误触远离轨道区的点击。
|
||||
return ResolveNearestZone(point);
|
||||
return ResolveNearestZone(worldPoint);
|
||||
}
|
||||
|
||||
// 横向最近轨道兜底:在所有 zone 中找触点纵向落在其包围盒内、且横向距离最近的一个。
|
||||
private int ResolveNearestZone(Vector2 point)
|
||||
private int ResolveNearestZone(Vector2 worldPoint)
|
||||
{
|
||||
var zones = TrackTouchZone.All;
|
||||
int best = -1;
|
||||
float bestDist = float.MaxValue;
|
||||
IReadOnlyList<TrackTouchZone> zones = TrackTouchZone.All;
|
||||
int bestTrack = -1;
|
||||
float bestDistance = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < zones.Count; i++)
|
||||
{
|
||||
TrackTouchZone z = zones[i];
|
||||
if (z == null || z.Collider == null) continue;
|
||||
Bounds b = z.Collider.bounds;
|
||||
// 纵向必须落在该点击区范围内(留一点容差),避免点屏幕别处也触发轨道。
|
||||
if (point.y < b.min.y - 0.1f || point.y > b.max.y + 0.1f) continue;
|
||||
float dx = Mathf.Abs(point.x - b.center.x);
|
||||
if (dx < bestDist) { bestDist = dx; best = z.trackIndex; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
TrackTouchZone zone = zones[i];
|
||||
if (zone == null || zone.Collider == null)
|
||||
continue;
|
||||
|
||||
// OverlapPoint 的复用缓冲与过滤器(避免每次触摸分配 GC)。
|
||||
private static readonly Collider2D[] _overlapResults = new Collider2D[16];
|
||||
private ContactFilter2D _overlapFilter;
|
||||
private bool _filterReady;
|
||||
Bounds bounds = zone.Collider.bounds;
|
||||
if (worldPoint.y < bounds.min.y - 0.1f || worldPoint.y > bounds.max.y + 0.1f)
|
||||
continue;
|
||||
|
||||
float distance = Mathf.Abs(worldPoint.x - bounds.center.x);
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
bestTrack = zone.trackIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return bestTrack;
|
||||
}
|
||||
|
||||
private void EnsureOverlapFilter()
|
||||
{
|
||||
if (_filterReady) return;
|
||||
_overlapFilter = new ContactFilter2D();
|
||||
_overlapFilter.useLayerMask = true;
|
||||
_overlapFilter.SetLayerMask(hitLayers);
|
||||
_overlapFilter.useTriggers = true; // tap zone 可能是触发器,需包含
|
||||
_filterReady = true;
|
||||
if (filterReady)
|
||||
return;
|
||||
|
||||
overlapFilter = new ContactFilter2D();
|
||||
overlapFilter.useLayerMask = true;
|
||||
overlapFilter.SetLayerMask(hitLayers);
|
||||
overlapFilter.useTriggers = true;
|
||||
filterReady = true;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// 组件被关闭/切场景时,释放所有还按住的轨道,避免音符卡在 held 状态。
|
||||
var im = InputManager.Instance;
|
||||
if (im != null)
|
||||
if (!ShouldProcessTouchInput())
|
||||
return;
|
||||
|
||||
InputManager inputManager = InputManager.Instance;
|
||||
if (inputManager != null)
|
||||
{
|
||||
foreach (var kv in _activeTouches)
|
||||
foreach (KeyValuePair<int, int> touch in activeTouches)
|
||||
{
|
||||
im.ReleaseTrack(kv.Value);
|
||||
inputManager.ReleaseTrack(touch.Value);
|
||||
}
|
||||
if (_mouseHeldTrack >= 0) im.ReleaseTrack(_mouseHeldTrack);
|
||||
|
||||
if (mouseHeldTrack >= 0)
|
||||
inputManager.ReleaseTrack(mouseHeldTrack);
|
||||
}
|
||||
_activeTouches.Clear();
|
||||
_mouseHeldTrack = -1;
|
||||
|
||||
activeTouches.Clear();
|
||||
mouseHeldTrack = -1;
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
EnhancedTouchSupport.Disable();
|
||||
|
||||
@@ -25,8 +25,24 @@ public class TrackTouchRegion : MonoBehaviour, IPointerDownHandler, IPointerUpHa
|
||||
private int activePointerId = int.MinValue;
|
||||
private bool pressed = false;
|
||||
|
||||
private static bool ShouldProcessTouchInput()
|
||||
{
|
||||
#if UNITY_ANDROID || UNITY_IOS
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (!ShouldProcessTouchInput())
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
if (!ShouldProcessTouchInput()) return;
|
||||
if (pressed) return; // already driven by another finger; ignore extras
|
||||
var im = InputManager.Instance;
|
||||
if (im == null) return;
|
||||
@@ -38,6 +54,7 @@ public class TrackTouchRegion : MonoBehaviour, IPointerDownHandler, IPointerUpHa
|
||||
|
||||
public void OnPointerUp(PointerEventData eventData)
|
||||
{
|
||||
if (!ShouldProcessTouchInput()) return;
|
||||
if (!pressed || eventData.pointerId != activePointerId) return;
|
||||
var im = InputManager.Instance;
|
||||
|
||||
@@ -48,6 +65,8 @@ public class TrackTouchRegion : MonoBehaviour, IPointerDownHandler, IPointerUpHa
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!ShouldProcessTouchInput()) return;
|
||||
|
||||
// If the region is hidden/destroyed mid-hold, make sure the track is released
|
||||
// so a note is not left stuck in the held state.
|
||||
if (pressed)
|
||||
|
||||
@@ -50,6 +50,7 @@ public class TrackTouchRegionLayoutController : MonoBehaviour
|
||||
|
||||
if (mobileOnly && !Application.isMobilePlatform)
|
||||
{
|
||||
DisableTouchRegionsForCurrentPlatform();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,4 +116,30 @@ public class TrackTouchRegionLayoutController : MonoBehaviour
|
||||
|
||||
touchRegion.trackIndex = trackIndex;
|
||||
}
|
||||
|
||||
private void DisableTouchRegionsForCurrentPlatform()
|
||||
{
|
||||
EnsureDefaultBindings();
|
||||
|
||||
for (int i = 0; i < regions.Length; i++)
|
||||
{
|
||||
RegionBinding binding = regions[i];
|
||||
if (binding == null || binding.regionObject == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Image image = binding.regionObject.GetComponent<Image>();
|
||||
if (image != null)
|
||||
{
|
||||
image.raycastTarget = false;
|
||||
}
|
||||
|
||||
TrackTouchRegion touchRegion = binding.regionObject.GetComponent<TrackTouchRegion>();
|
||||
if (touchRegion != null)
|
||||
{
|
||||
touchRegion.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ public class effectEventController : MonoBehaviour
|
||||
// local 位姿被清零(见 EnsureCameraDriftPivot/EnsureCameraSkillPivot),故基准 z≈0、X≈0。
|
||||
// 首次调用 SetResolutionCameraSelf 时惰性捕获,供插值 t=0 回到该基准。
|
||||
private bool _cameraSelfBaseCaptured;
|
||||
private float _cameraSelfBaseLocalY;
|
||||
private float _cameraSelfBaseLocalZ;
|
||||
private float _cameraSelfBaseLocalRotX;
|
||||
|
||||
@@ -168,7 +169,7 @@ public class effectEventController : MonoBehaviour
|
||||
/// 故结束后回到本方法设定的值。为兼容"抖动进行中改分辨率"的罕见情形,
|
||||
/// 若相机上已有 SmoothShake 且正在抖动,同步其 startPosition/startRotation 基准。
|
||||
/// </summary>
|
||||
public bool SetResolutionCameraSelf(float t, float targetLocalRotX, float targetLocalZ)
|
||||
public bool SetResolutionCameraSelf(float t, float targetLocalRotX, float targetLocalZ, float targetLocalY = 0f)
|
||||
{
|
||||
if (cachedShakeTarget == null)
|
||||
{
|
||||
@@ -179,15 +180,18 @@ public class effectEventController : MonoBehaviour
|
||||
|
||||
if (!_cameraSelfBaseCaptured)
|
||||
{
|
||||
_cameraSelfBaseLocalY = cachedShakeTarget.localPosition.y;
|
||||
_cameraSelfBaseLocalZ = cachedShakeTarget.localPosition.z;
|
||||
_cameraSelfBaseLocalRotX = NormalizeSignedAngle(cachedShakeTarget.localEulerAngles.x);
|
||||
_cameraSelfBaseCaptured = true;
|
||||
}
|
||||
|
||||
float y = Mathf.Lerp(_cameraSelfBaseLocalY, targetLocalY, t);
|
||||
float z = Mathf.Lerp(_cameraSelfBaseLocalZ, targetLocalZ, t);
|
||||
float rotX = Mathf.Lerp(_cameraSelfBaseLocalRotX, targetLocalRotX, t);
|
||||
|
||||
Vector3 lp = cachedShakeTarget.localPosition;
|
||||
lp.y = y;
|
||||
lp.z = z;
|
||||
cachedShakeTarget.localPosition = lp;
|
||||
|
||||
|
||||
@@ -99,14 +99,33 @@ public class groundParticularController : MonoBehaviour
|
||||
private static readonly int _headColorId = Shader.PropertyToID("_HeadColor");
|
||||
private static readonly int _tailColorId = Shader.PropertyToID("_TailColor");
|
||||
private static readonly int _moveDirId = Shader.PropertyToID("_MoveDir");
|
||||
|
||||
|
||||
// 材质均衡分配逻辑
|
||||
private List<MaterialConfig> _cachedEnabledConfigs = new List<MaterialConfig>();
|
||||
private int _materialIndex = 0;
|
||||
|
||||
|
||||
private IObjectPool<GameObject> _pool;
|
||||
private Vector3 _originalStartScale;
|
||||
|
||||
// 【GC 优化】去掉 per-particle 协程,改为中心化移动更新:活跃粒子列表 + 统一 Update 遍历
|
||||
private struct ActiveParticle
|
||||
{
|
||||
public GameObject gameObject;
|
||||
public Vector3 moveDir;
|
||||
public float totalDist;
|
||||
public float traveledDist;
|
||||
public MaterialConfig? config;
|
||||
public Renderer renderer;
|
||||
}
|
||||
private readonly List<ActiveParticle> _activeParticles = new List<ActiveParticle>(64);
|
||||
|
||||
// 【GC 优化】延迟发射队列(替代 per-emit 协程 + WaitForSeconds 分配)
|
||||
private struct DelayedEmit
|
||||
{
|
||||
public float fireTime;
|
||||
}
|
||||
private readonly List<DelayedEmit> _delayedEmits = new List<DelayedEmit>(32);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (startPoint != null) _originalStartScale = startPoint.localScale;
|
||||
@@ -141,7 +160,11 @@ public class groundParticularController : MonoBehaviour
|
||||
// 持续发射逻辑
|
||||
UpdateContinuousEmit();
|
||||
|
||||
// 【GC 优化】处理延迟发射队列(替代 per-emit 协程)
|
||||
ProcessDelayedEmits();
|
||||
|
||||
// 【GC 优化】统一更新所有活跃粒子(替代 per-particle MoveRoutine 协程)
|
||||
UpdateActiveParticles();
|
||||
|
||||
// 起点缩放抖动
|
||||
if (startPoint != null)
|
||||
@@ -166,15 +189,14 @@ public class groundParticularController : MonoBehaviour
|
||||
if (_emitTimer >= interval)
|
||||
{
|
||||
_emitTimer = 0;
|
||||
|
||||
// 批量生成
|
||||
|
||||
// 批量生成:引入混乱度用延迟队列(无 GC),立即发射直接调 Emit
|
||||
for (int i = 0; i < emitBatchSize; i++)
|
||||
{
|
||||
// 引入混乱度:随机延迟发射。用协程替代 Invoke(nameof)——
|
||||
// 后者每次做字符串反射+分配;协程延迟无字符串开销。
|
||||
if (chaosRandomDelay > 0)
|
||||
{
|
||||
StartCoroutine(DelayedEmitRoutine(Random.Range(0f, chaosRandomDelay)));
|
||||
// 【GC 优化】入队延迟发射,替代 StartCoroutine + WaitForSeconds 分配
|
||||
_delayedEmits.Add(new DelayedEmit { fireTime = Time.time + Random.Range(0f, chaosRandomDelay) });
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -184,10 +206,18 @@ public class groundParticularController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator DelayedEmitRoutine(float delay)
|
||||
// 【GC 优化】处理延迟发射队列(替代 DelayedEmitRoutine 协程)
|
||||
private void ProcessDelayedEmits()
|
||||
{
|
||||
if (delay > 0f) yield return new WaitForSeconds(delay);
|
||||
Emit();
|
||||
float now = Time.time;
|
||||
for (int i = _delayedEmits.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (now >= _delayedEmits[i].fireTime)
|
||||
{
|
||||
Emit();
|
||||
_delayedEmits.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取粒子缓存的 Renderer;未缓存(异常情况)则补取一次。
|
||||
@@ -230,10 +260,10 @@ public class groundParticularController : MonoBehaviour
|
||||
|
||||
// 1. 获取粒子
|
||||
GameObject particle = _pool.Get();
|
||||
|
||||
|
||||
// 2. 设置层级
|
||||
particle.transform.SetParent(particleParent != null ? particleParent : null);
|
||||
|
||||
|
||||
// 3. 计算缩放
|
||||
float randomScale = Random.Range(minParticleScale, maxParticleScale);
|
||||
float audioBoost = useAudioAnalysis ? (1f + _currentAverageVolume) : 1f;
|
||||
@@ -247,12 +277,13 @@ public class groundParticularController : MonoBehaviour
|
||||
0f,
|
||||
Random.Range(-spawnOffsetY, spawnOffsetY)
|
||||
);
|
||||
particle.transform.position = startPoint.TransformPoint(finalLocalPos);
|
||||
Vector3 spawnWorldPos = startPoint.TransformPoint(finalLocalPos);
|
||||
particle.transform.position = spawnWorldPos;
|
||||
particle.transform.rotation = GetSpawnRotation();
|
||||
|
||||
// 5. 材质均衡选取逻辑
|
||||
MaterialConfig? selectedConfig = GetBalancedMaterialConfig();
|
||||
|
||||
|
||||
// 设置物理层级
|
||||
int layer = LayerMask.NameToLayer(physicsLayerName);
|
||||
if (layer != -1)
|
||||
@@ -284,8 +315,21 @@ public class groundParticularController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 开始移动
|
||||
StartCoroutine(MoveRoutine(particle, finalLocalPos, selectedConfig));
|
||||
// 6. 【GC 优化】注册到活跃粒子列表(替代 StartCoroutine MoveRoutine)
|
||||
Vector3 currentStartPos = startPoint.position;
|
||||
Vector3 currentEndPos = endPoint.position;
|
||||
Vector3 moveDir = (currentEndPos - currentStartPos).normalized;
|
||||
float totalDist = Vector3.Distance(currentStartPos, currentEndPos);
|
||||
|
||||
_activeParticles.Add(new ActiveParticle
|
||||
{
|
||||
gameObject = particle,
|
||||
moveDir = moveDir,
|
||||
totalDist = totalDist,
|
||||
traveledDist = 0f,
|
||||
config = selectedConfig,
|
||||
renderer = renderer
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -328,72 +372,65 @@ public class groundParticularController : MonoBehaviour
|
||||
return _cachedEnabledConfigs[_materialIndex];
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator MoveRoutine(GameObject particle, Vector3 initialLocalPos, MaterialConfig? config)
|
||||
// 【GC 优化】统一更新所有活跃粒子(替代 per-particle MoveRoutine 协程,消除协程开销)
|
||||
private void UpdateActiveParticles()
|
||||
{
|
||||
if (startPoint == null || endPoint == null) yield break;
|
||||
if (startPoint == null || endPoint == null) return;
|
||||
|
||||
// 【关键修复】在协程开始时,缓存起点的世界坐标
|
||||
Vector3 spawnWorldPos = startPoint.TransformPoint(initialLocalPos);
|
||||
|
||||
Vector3 currentStartPos = startPoint.position;
|
||||
Vector3 currentEndPos = endPoint.position;
|
||||
Vector3 moveDir = (currentEndPos - currentStartPos).normalized;
|
||||
float totalDist = Vector3.Distance(currentStartPos, currentEndPos);
|
||||
float traveledDist = 0f;
|
||||
// 暂停检查:暂停期间不更新粒子
|
||||
if (PauseManager.Instance != null && PauseManager.Instance.IsPaused) return;
|
||||
|
||||
particle.transform.position = spawnWorldPos;
|
||||
|
||||
// 复用共享属性块(不 new,避免每颗粒子 GC)与缓存的 Renderer。
|
||||
MaterialPropertyBlock propBlock = _sharedPropBlock;
|
||||
Renderer renderer = GetCachedRenderer(particle);
|
||||
|
||||
while (particle != null && particle.activeInHierarchy)
|
||||
float currentBPM = GetCurrentBPM();
|
||||
float noteSpawnerSpeedMultiplier = 1.0f;
|
||||
if (beatmapManager != null && beatmapManager.noteSpawner != null)
|
||||
{
|
||||
// 如果游戏暂停,则等待直到取消暂停
|
||||
if (PauseManager.Instance != null && PauseManager.Instance.IsPaused)
|
||||
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.EffectiveSpeedMultiplier;
|
||||
currentBPM = beatmapManager.noteSpawner.EffectiveBpm;
|
||||
}
|
||||
|
||||
float effectiveBPM = Mathf.Max(currentBPM, 60f);
|
||||
MaterialPropertyBlock propBlock = _sharedPropBlock;
|
||||
|
||||
// 倒序遍历便于移除
|
||||
for (int i = _activeParticles.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var p = _activeParticles[i];
|
||||
if (p.gameObject == null || !p.gameObject.activeInHierarchy)
|
||||
{
|
||||
yield return new WaitUntil(() => !PauseManager.Instance.IsPaused);
|
||||
_activeParticles.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
float currentBPM = GetCurrentBPM();
|
||||
float noteSpawnerSpeedMultiplier = 1.0f;
|
||||
// 粒子速度须与音符下落同速:跟随 NoteSpawner 的“有效 BPM”(开关决定用谱面 bpm 还是固定参考 bpm)。
|
||||
if (beatmapManager != null && beatmapManager.noteSpawner != null)
|
||||
{
|
||||
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.EffectiveSpeedMultiplier;
|
||||
currentBPM = beatmapManager.noteSpawner.EffectiveBpm;
|
||||
}
|
||||
|
||||
float effectiveBPM = Mathf.Max(currentBPM, 60f);
|
||||
float noteSpeed = (totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f;
|
||||
|
||||
float noteSpeed = (p.totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f;
|
||||
float step = noteSpeed * speedMultiplier * emissionSpeedMultiplier * perFrameMoveMultiplier * Time.deltaTime;
|
||||
if (step <= 0) step = 0.01f;
|
||||
|
||||
traveledDist += step;
|
||||
particle.transform.position += moveDir * step;
|
||||
p.traveledDist += step;
|
||||
p.gameObject.transform.position += p.moveDir * step;
|
||||
|
||||
// 【动态空间渐变色逻辑】
|
||||
if (config != null && config.Value.useGradientOverride && renderer != null)
|
||||
// 动态空间渐变色
|
||||
if (p.config != null && p.config.Value.useGradientOverride && p.renderer != null)
|
||||
{
|
||||
// 获取渐变的首尾颜色
|
||||
Color headColor = config.Value.colorGradient.Evaluate(1f);
|
||||
Color tailColor = config.Value.colorGradient.Evaluate(0f);
|
||||
|
||||
renderer.GetPropertyBlock(propBlock);
|
||||
// 用缓存的属性 ID(避免每帧字符串查找)
|
||||
Color headColor = p.config.Value.colorGradient.Evaluate(1f);
|
||||
Color tailColor = p.config.Value.colorGradient.Evaluate(0f);
|
||||
|
||||
p.renderer.GetPropertyBlock(propBlock);
|
||||
propBlock.SetColor(_headColorId, headColor);
|
||||
propBlock.SetColor(_tailColorId, tailColor);
|
||||
propBlock.SetVector(_moveDirId, moveDir);
|
||||
renderer.SetPropertyBlock(propBlock);
|
||||
propBlock.SetVector(_moveDirId, p.moveDir);
|
||||
p.renderer.SetPropertyBlock(propBlock);
|
||||
}
|
||||
|
||||
if (traveledDist >= totalDist)
|
||||
// 到达终点:回收
|
||||
if (p.traveledDist >= p.totalDist)
|
||||
{
|
||||
if (particle.activeInHierarchy) _pool.Release(particle);
|
||||
yield break;
|
||||
if (p.gameObject.activeInHierarchy) _pool.Release(p.gameObject);
|
||||
_activeParticles.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
yield return null;
|
||||
|
||||
// 写回更新的 traveledDist(struct 需回写)
|
||||
_activeParticles[i] = p;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -835,6 +835,11 @@ public class teamUIController : MonoBehaviour
|
||||
public TextMeshProUGUI constructionName;
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public Text comboCounter;
|
||||
[Header("Realtime Judge Stats")]
|
||||
public Text currentComboText;
|
||||
public Text currentAccuracyText;
|
||||
public SpriteNumberDisplay currentComboSpriteDisplay;
|
||||
public SpriteNumberDisplay currentAccuracySpriteDisplay;
|
||||
[Tooltip("Documentation text normalized.")]
|
||||
public TextMeshProUGUI enemyCounter;
|
||||
[SerializeField] private int enemyCounterMax; // Documentation text normalized.
|
||||
@@ -885,6 +890,10 @@ public class teamUIController : MonoBehaviour
|
||||
private int combo = 0;
|
||||
public int CurrentCombo => combo;
|
||||
private Tween recentPlusAmountFadeTween;
|
||||
private const float RealtimePerfectWeight = 1f;
|
||||
private const float RealtimeGreatWeight = 0.6666667f;
|
||||
private const float RealtimeGoodWeight = 0.333333f;
|
||||
private const float RealtimeMissWeight = 0f;
|
||||
|
||||
public enum ComboJudgeType
|
||||
{
|
||||
@@ -1201,6 +1210,8 @@ public class teamUIController : MonoBehaviour
|
||||
recentPlusAmountText.color = color;
|
||||
}
|
||||
|
||||
RefreshRealtimeJudgeStatsText();
|
||||
|
||||
if (ScoreManager.Instance != null)
|
||||
{
|
||||
ScoreManager.Instance.RefreshAllScoreUi();
|
||||
@@ -1279,6 +1290,8 @@ public class teamUIController : MonoBehaviour
|
||||
comboCounter.text = _sb.ToString();
|
||||
}
|
||||
|
||||
RefreshRealtimeJudgeStatsText();
|
||||
|
||||
// --- Statistics: Update achievement tracking for combo ---
|
||||
if (InGamePerformanceManager.Instance != null)
|
||||
{
|
||||
@@ -1294,6 +1307,65 @@ public class teamUIController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshRealtimeJudgeStatsText()
|
||||
{
|
||||
if (currentComboText != null)
|
||||
{
|
||||
currentComboText.text = combo.ToString();
|
||||
}
|
||||
|
||||
if (currentComboSpriteDisplay != null)
|
||||
{
|
||||
currentComboSpriteDisplay.SetNumber(combo);
|
||||
}
|
||||
|
||||
bool hasAccuracyText = currentAccuracyText != null;
|
||||
bool hasAccuracySprite = currentAccuracySpriteDisplay != null;
|
||||
if (!hasAccuracyText && !hasAccuracySprite)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ScoreManager scoreManager = ScoreManager.Instance;
|
||||
if (scoreManager == null)
|
||||
{
|
||||
SetRealtimeAccuracyDisplay("0.00%");
|
||||
return;
|
||||
}
|
||||
|
||||
int perfect = Mathf.Max(0, scoreManager.countPerfect);
|
||||
int great = Mathf.Max(0, scoreManager.countGreat);
|
||||
int good = Mathf.Max(0, scoreManager.countGood);
|
||||
int miss = Mathf.Max(0, scoreManager.countMiss);
|
||||
int total = perfect + great + good + miss;
|
||||
if (total <= 0)
|
||||
{
|
||||
SetRealtimeAccuracyDisplay("0.00%");
|
||||
return;
|
||||
}
|
||||
|
||||
float accuracy =
|
||||
(perfect * RealtimePerfectWeight +
|
||||
great * RealtimeGreatWeight +
|
||||
good * RealtimeGoodWeight +
|
||||
miss * RealtimeMissWeight) / total * 100f;
|
||||
|
||||
SetRealtimeAccuracyDisplay(accuracy.ToString("F2") + "%");
|
||||
}
|
||||
|
||||
private void SetRealtimeAccuracyDisplay(string value)
|
||||
{
|
||||
if (currentAccuracyText != null)
|
||||
{
|
||||
currentAccuracyText.text = value;
|
||||
}
|
||||
|
||||
if (currentAccuracySpriteDisplay != null)
|
||||
{
|
||||
currentAccuracySpriteDisplay.SetText(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetRecentPlusAmountVisual()
|
||||
{
|
||||
if (recentPlusAmountText == null)
|
||||
|
||||
Reference in New Issue
Block a user