feat: polish game HUD scoring and results

This commit is contained in:
2026-05-29 00:32:21 +09:00
parent c4330aa544
commit b46ccddbdb
14 changed files with 768 additions and 646 deletions
+172 -42
View File
@@ -29,12 +29,13 @@ namespace VRBeats
[SerializeField] private bool createMissingHudLabels = true;
[SerializeField] private bool applyHudPlacement = true;
[SerializeField] private Vector2 hudAnchoredPosition = new Vector2(0.0f, 1.65f);
[SerializeField] private float perfectWindow = 0.08f;
[SerializeField] private float greatWindow = 0.15f;
[SerializeField] private float goodWindow = 0.25f;
[SerializeField] private float perfectWindow = 0.11f;
[SerializeField] private float greatWindow = 0.20f;
[SerializeField] private float goodWindow = 0.32f;
private int maxMultiplier = 0;
private const int MaxCourseScore = 1000000;
private const float ProgressBarWidth = 150.0f;
private float currentMultiplier = 1.0f;
private int acumulateErrors = 0;
private int errorLimit = 0;
@@ -54,6 +55,8 @@ namespace VRBeats
private float judgementTimer = 0.0f;
private Text progressLabel = null;
private Text rankLabel = null;
private Image progressBarBackground = null;
private Image progressBarFill = null;
private Vector3 comboBaseScale = Vector3.one;
private float songCurrentTime = 0.0f;
private float songDuration = 0.0f;
@@ -62,8 +65,20 @@ namespace VRBeats
private static bool hasPendingSliceTiming = false;
private static float pendingSliceTiming = 0.0f;
private static Font hudFont = null;
private Image ringBackground = null;
public int CurrentScore => Mathf.RoundToInt(MaxCourseScore * AccuracyPercent / 100.0f);
public int CurrentScore
{
get
{
float accuracyRatio = AccuracyPercent / 100.0f;
float comboRatio = totalNoteCount > 0
? maxCombo / (float)totalNoteCount
: 0.0f;
return Mathf.RoundToInt(800000.0f * accuracyRatio + 200000.0f * comboRatio);
}
}
public float AccuracyPercent
{
get
@@ -80,6 +95,8 @@ namespace VRBeats
{
get
{
if (CurrentScore >= MaxCourseScore) return "M";
float accuracy = AccuracyPercent;
if (accuracy >= 98.0f) return "S+";
if (accuracy >= 95.0f) return "S";
@@ -91,6 +108,9 @@ namespace VRBeats
}
}
public int MaxCombo => maxCombo;
public string RankColorHex => GetRankColorHex();
private void Awake()
{
maxMultiplier = VR_BeatManager.instance.GameSettings.MaxMultiplier;
@@ -99,6 +119,7 @@ namespace VRBeats
if (multiplierLoader != null)
multiplierLoader.fillAmount = 0.0f;
canvasGroup ??= GetComponent<CanvasGroup>() ?? gameObject.AddComponent<CanvasGroup>();
PrepareHud();
}
@@ -142,15 +163,23 @@ namespace VRBeats
public void SetSongProgress(float currentTime, float duration)
{
songCurrentTime = Mathf.Max(0.0f, currentTime);
songDuration = Mathf.Max(0.0f, duration);
songCurrentTime = songDuration > 0.0f
? Mathf.Clamp(currentTime, 0.0f, songDuration)
: Mathf.Max(0.0f, currentTime);
}
public void OnGameOver()
{
gameObject.CancelAllTweens();
if (canvasGroup != null)
canvasGroup.Fade(0.0f, 0.5f).SetEase(Ease.EaseOutExpo).SetOwner(gameObject);
{
canvasGroup.alpha = 0.0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
SetSaberVisibility(false);
}
public void OnGameRestart()
@@ -158,7 +187,13 @@ namespace VRBeats
ResetThisComponent();
gameObject.CancelAllTweens();
if (canvasGroup != null)
canvasGroup.Fade(1.0f, 0.5f).SetEase(Ease.EaseOutExpo).SetOwner(gameObject);
{
canvasGroup.alpha = 1.0f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = false;
}
SetSaberVisibility(true);
}
public void ResetThisComponent()
@@ -220,17 +255,11 @@ namespace VRBeats
public string BuildResultSummary(int minScoreLength)
{
string score = CurrentScore.ToString();
score = new string('0', Mathf.Max(minScoreLength - score.Length, 0)) + score;
string badge = missCount == 0
? (greatCount == 0 && goodCount == 0 ? "PERFECT PLAY" : "FULL COMBO")
: "TRY AGAIN";
string score = CurrentScore.ToString("N0");
return $"<size=150%><color=#41F2FF>{Rank}</color></size>\n" +
$"<size=118%>{score}</size>\n" +
$"<size=56%><color=#D7F7FF>ACC {AccuracyPercent:0.0}% {badge}</color></size>\n" +
$"<size=50%>MAX COMBO {maxCombo}</size>\n" +
$"<size=42%><color=#A9B7C0>P {perfectCount} G {greatCount} GOOD {goodCount} MISS {missCount}</color></size>";
return $"<line-height=76%><size=300%><color={GetRankColorHex()}>{Rank}</color></size>" +
$"<pos=255><voffset=0.48em><size=92%>{score}</size></voffset>\n" +
$"<pos=255><size=72%><color=#D7F7FF>MAX COMBO {maxCombo}</color></size>";
}
private void CancelTweenById(int id)
@@ -277,8 +306,8 @@ namespace VRBeats
scoreLabel.text = $"{Mathf.CeilToInt(visualScore):N0}";
if (comboLabel != null)
comboLabel.text = currentCombo > 0
? $"<size=42%><color=#E6F8FF>COMBO</color></size>\n<size=125%>{currentCombo}</size>"
: "<size=42%><color=#E6F8FF>COMBO</color></size>\n<size=125%>0</size>";
? $"<size=18><color=#E6F8FF>COMBO</color></size>\n<size=42>{currentCombo}</size>"
: "<size=18><color=#E6F8FF>COMBO</color></size>\n<size=42>0</size>";
if (accuracyLabel != null)
accuracyLabel.text = $"{AccuracyPercent:0.0}%";
if (rankLabel != null)
@@ -287,6 +316,10 @@ namespace VRBeats
progressLabel.text = songDuration > 0.0f
? $"{FormatTime(songCurrentTime)} / {FormatTime(songDuration)}"
: "";
if (progressBarFill != null)
SetProgressBarFill(songDuration > 0.0f
? Mathf.Clamp01(songCurrentTime / songDuration)
: 0.0f);
if (judgementLabel == null)
return;
@@ -326,13 +359,13 @@ namespace VRBeats
else if (judgement == BeatJudgement.Great)
{
greatCount++;
earnedAccuracyPoints += 700;
earnedAccuracyPoints += 900;
currentCombo++;
}
else if (judgement == BeatJudgement.Good)
{
goodCount++;
earnedAccuracyPoints += 400;
earnedAccuracyPoints += 700;
currentCombo++;
}
else
@@ -351,10 +384,10 @@ namespace VRBeats
private static float GetComboMultiplier(int combo)
{
if (combo >= 200) return 1.5f;
if (combo >= 100) return 1.35f;
if (combo >= 50) return 1.2f;
if (combo >= 20) return 1.1f;
if (combo >= 50) return 1.5f;
if (combo >= 30) return 1.35f;
if (combo >= 15) return 1.2f;
if (combo >= 5) return 1.1f;
return 1.0f;
}
@@ -383,11 +416,11 @@ namespace VRBeats
private float GetComboTierProgress()
{
int lower = 0;
int upper = 20;
if (currentCombo >= 200) return 1.0f;
if (currentCombo >= 100) { lower = 100; upper = 200; }
else if (currentCombo >= 50) { lower = 50; upper = 100; }
else if (currentCombo >= 20) { lower = 20; upper = 50; }
int upper = 5;
if (currentCombo >= 50) return 1.0f;
if (currentCombo >= 30) { lower = 30; upper = 50; }
else if (currentCombo >= 15) { lower = 15; upper = 30; }
else if (currentCombo >= 5) { lower = 5; upper = 15; }
return Mathf.InverseLerp(lower, upper, currentCombo);
}
@@ -398,22 +431,54 @@ namespace VRBeats
if (applyHudPlacement && rect != null)
rect.anchoredPosition = hudAnchoredPosition;
ConfigureText(scoreLabel, new Vector2(-255.0f, -18.0f), new Vector2(220.0f, 40.0f), 26, Color.white, TextAnchor.MiddleLeft);
ConfigureText(multiplierLabel, new Vector2(255.0f, 36.0f), new Vector2(100.0f, 68.0f), 34, Color.white, TextAnchor.MiddleCenter);
ConfigureImage(multiplierLoader, new Vector2(255.0f, 36.0f), new Vector2(104.0f, 104.0f));
comboLabel = comboLabel != null ? comboLabel : FindHudText("Combo");
accuracyLabel = accuracyLabel != null ? accuracyLabel : FindHudText("Accuracy");
rankLabel = rankLabel != null ? rankLabel : FindHudText("Rank");
judgementLabel = judgementLabel != null ? judgementLabel : FindHudText("Judgement");
progressLabel = progressLabel != null ? progressLabel : FindHudText("SongProgress");
progressBarBackground = progressBarBackground != null ? progressBarBackground : FindHudImage("SongProgressBarBackground");
progressBarFill = progressBarFill != null ? progressBarFill : FindHudImage("SongProgressBarFill");
if (!createMissingHudLabels)
return;
comboLabel ??= CreateHudText("Combo", new Vector2(-255.0f, 74.0f), new Vector2(220.0f, 112.0f), 36, Color.white, TextAnchor.MiddleLeft);
accuracyLabel ??= CreateHudText("Accuracy", new Vector2(-255.0f, -58.0f), new Vector2(220.0f, 34.0f), 20, new Color(0.88f, 0.95f, 1.0f, 1.0f), TextAnchor.MiddleLeft);
rankLabel ??= CreateHudText("Rank", new Vector2(-255.0f, -105.0f), new Vector2(220.0f, 76.0f), 48, Color.white, TextAnchor.MiddleLeft);
judgementLabel ??= CreateHudText("Judgement", new Vector2(0.0f, 112.0f), new Vector2(260.0f, 54.0f), 28, new Color(0.25f, 0.95f, 1.0f, 1.0f), TextAnchor.MiddleCenter);
progressLabel ??= CreateHudText("SongProgress", new Vector2(255.0f, -62.0f), new Vector2(180.0f, 34.0f), 18, Color.white, TextAnchor.MiddleCenter);
comboLabel ??= CreateHudText("Combo");
accuracyLabel ??= CreateHudText("Accuracy");
rankLabel ??= CreateHudText("Rank");
judgementLabel ??= CreateHudText("Judgement");
progressLabel ??= CreateHudText("SongProgress");
progressBarBackground ??= CreateHudImage("SongProgressBarBackground");
progressBarFill ??= CreateHudImage("SongProgressBarFill");
ringBackground = ringBackground != null ? ringBackground : FindHudImage("MultiplierRingBg");
ringBackground ??= CreateHudImage("MultiplierRingBg");
ConfigureText(comboLabel, new Vector2(-335.0f, 92.0f), new Vector2(190.0f, 118.0f), 34, Color.white, TextAnchor.MiddleCenter);
ConfigureText(scoreLabel, new Vector2(-335.0f, 10.0f), new Vector2(190.0f, 42.0f), 22, Color.white, TextAnchor.MiddleCenter);
ConfigureText(accuracyLabel, new Vector2(-335.0f, -34.0f), new Vector2(190.0f, 32.0f), 18, new Color(0.84f, 0.94f, 1.0f, 1.0f), TextAnchor.MiddleCenter);
ConfigureText(rankLabel, new Vector2(-335.0f, -92.0f), new Vector2(190.0f, 72.0f), 48, Color.white, TextAnchor.MiddleCenter);
ConfigureText(judgementLabel, new Vector2(0.0f, 118.0f), new Vector2(280.0f, 56.0f), 28, new Color(0.25f, 0.95f, 1.0f, 1.0f), TextAnchor.MiddleCenter);
ConfigureText(multiplierLabel, new Vector2(335.0f, 38.0f), new Vector2(118.0f, 76.0f), 34, Color.white, TextAnchor.MiddleCenter);
ConfigureText(progressLabel, new Vector2(335.0f, -75.0f), new Vector2(180.0f, 30.0f), 17, Color.white, TextAnchor.MiddleCenter);
ConfigureImage(multiplierLoader, new Vector2(335.0f, 38.0f), new Vector2(112.0f, 112.0f), new Color(1.0f, 1.0f, 1.0f, 0.78f));
ConfigureImage(ringBackground, new Vector2(335.0f, 38.0f), new Vector2(112.0f, 112.0f), new Color(1.0f, 1.0f, 1.0f, 0.15f));
if (ringBackground != null && multiplierLoader != null)
{
ringBackground.sprite = multiplierLoader.sprite;
ringBackground.type = Image.Type.Simple;
ringBackground.transform.SetSiblingIndex(multiplierLoader.transform.GetSiblingIndex());
}
ConfigureImage(progressBarBackground, new Vector2(335.0f, -48.0f), new Vector2(ProgressBarWidth, 5.0f), new Color(1.0f, 1.0f, 1.0f, 0.22f));
ConfigureImage(progressBarFill, new Vector2(335.0f - ProgressBarWidth * 0.5f, -48.0f), new Vector2(ProgressBarWidth, 5.0f), Color.white);
if (progressBarFill != null)
{
RectTransform fillRect = progressBarFill.rectTransform;
fillRect.pivot = new Vector2(0.0f, 0.5f);
SetProgressBarFill(0.0f);
}
comboBaseScale = comboLabel != null ? comboLabel.transform.localScale : Vector3.one;
}
private Text CreateHudText(string name, Vector2 anchoredPosition, Vector2 size, int fontSize, Color color, TextAnchor alignment)
private Text CreateHudText(string name)
{
GameObject textObject = new GameObject(name);
textObject.layer = gameObject.layer;
@@ -422,15 +487,27 @@ namespace VRBeats
RectTransform rect = textObject.AddComponent<RectTransform>();
textObject.AddComponent<CanvasRenderer>();
Text text = textObject.AddComponent<Text>();
ConfigureText(text, anchoredPosition, size, fontSize, color, alignment);
return text;
}
private Image CreateHudImage(string name)
{
GameObject imageObject = new GameObject(name);
imageObject.layer = gameObject.layer;
imageObject.transform.SetParent(transform, false);
imageObject.AddComponent<CanvasRenderer>();
return imageObject.AddComponent<Image>();
}
private static void ConfigureText(Text text, Vector2 anchoredPosition, Vector2 size, int fontSize, Color color, TextAnchor alignment)
{
if (text == null)
return;
if (text.font == null)
text.font = HudFont;
RectTransform rect = text.rectTransform;
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
@@ -452,6 +529,11 @@ namespace VRBeats
}
private static void ConfigureImage(Image image, Vector2 anchoredPosition, Vector2 size)
{
ConfigureImage(image, anchoredPosition, size, new Color(1.0f, 1.0f, 1.0f, 0.85f));
}
private static void ConfigureImage(Image image, Vector2 anchoredPosition, Vector2 size, Color color)
{
if (image == null)
return;
@@ -461,16 +543,49 @@ namespace VRBeats
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = size;
image.color = new Color(1.0f, 1.0f, 1.0f, 0.85f);
image.color = color;
image.raycastTarget = false;
}
private void SetProgressBarFill(float progress)
{
if (progressBarFill == null)
return;
RectTransform rect = progressBarFill.rectTransform;
rect.sizeDelta = new Vector2(ProgressBarWidth * Mathf.Clamp01(progress), rect.sizeDelta.y);
}
private static Font HudFont
{
get
{
if (hudFont == null)
hudFont = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
return hudFont;
}
}
private Text FindHudText(string objectName)
{
Transform child = transform.Find(objectName);
return child != null ? child.GetComponent<Text>() : null;
}
private Image FindHudImage(string objectName)
{
Transform child = transform.Find(objectName);
return child != null ? child.GetComponent<Image>() : null;
}
private string GetRankColorHex()
{
switch (Rank)
{
case "M": return "#E8B7FF";
case "S+": return "#41F2FF";
case "S": return "#69FFD1";
case "S": return "#FFD95C";
case "A": return "#B9FF72";
case "B": return "#FFE06A";
case "C": return "#FFB15C";
@@ -496,5 +611,20 @@ namespace VRBeats
int remainingSeconds = wholeSeconds % 60;
return $"{minutes}:{remainingSeconds:00}";
}
private static void SetSaberVisibility(bool visible)
{
VR_Saber[] sabers = FindObjectsByType<VR_Saber>(FindObjectsSortMode.None);
for (int i = 0; i < sabers.Length; i++)
{
if (sabers[i] == null)
continue;
if (visible)
sabers[i].MakeVisible();
else
sabers[i].MakeInvisible();
}
}
}
}