技能主要更新,修复卡顿并加入动画,以及各种其他更新。

This commit is contained in:
2026-02-07 21:05:17 +08:00
parent abeca51be5
commit 1ac3cd0104
1349 changed files with 1526749 additions and 24850 deletions
@@ -1,6 +1,6 @@
using UnityEngine;
using TMPro;
using System.Collections; // 保持对 Coroutine 的支持
using System.Collections; // ֶ Coroutine ֧
using System.Collections.Generic; // For List
public class AnimationController : MonoBehaviour
@@ -9,11 +9,11 @@ public class AnimationController : MonoBehaviour
public static AnimationController Global;
public GameObject redEffect; // 红色击打动画对象
public GameObject greenEffect; // 绿色击打动画对象
public GameObject yellowEffect; // 黄色击打动画对象
public GameObject purpleEffect; // 紫色击打动画对象
public GameObject blueEffect; // 蓝色击打动画对象
public GameObject redEffect; // ɫ򶯻
public GameObject greenEffect; // ɫ򶯻
public GameObject yellowEffect; // ɫ򶯻
public GameObject purpleEffect; // ɫ򶯻
public GameObject blueEffect; // ɫ򶯻
private Animator redAnimator;
private Animator greenAnimator;
@@ -25,35 +25,33 @@ public class AnimationController : MonoBehaviour
public GameObject hit_particular_object;
public GameObject hit_ring_object;
[Header("轨道颜色 (Hex Color Codes)")]
// F86F64 (浅红色/珊瑚色)
public string redColorHex = "#F86F64";
// 40EC90 (亮绿色)
public string greenColorHex = "#40EC90";
// F7CB6C (金黄色)
public string yellowColorHex = "#F7CB6C";
// F888E7 (亮粉色/洋红色)
public string purpleColorHex = "#F888E7";
// 4FACFE (天蓝色)
public string blueColorHex = "#4FACFE";
[Header("Hold 粒子配置")]
[Header("ɫ (Hex Color Codes)")]
public string redColorHex = "#FF9390";
public string greenColorHex = "#38F6CA";
public string yellowColorHex = "#FFE1A2";
public string purpleColorHex = "#F083E4";
public string blueColorHex = "#7AF9FF";
[Header("Hold ")]
public float holdParticleInterval = 0.2f;
private Coroutine holdParticleCoroutine;
private bool holdActive = false;
private string currentHoldColor;
private bool isHolding = false;
private WaitForSeconds cachedHoldWait;
private float cachedHoldWaitSeconds = -1f;
private static bool loggedMissingHitParticleSource;
// Track active particle instances for immediate cleanup
private List<GameObject> activeParticles = new List<GameObject>();
[Header("判定飘字prefabs")]
[Header("жƮprefabs")]
public GameObject perfect_judge_prefab;
public GameObject great_judge_prefab;
public GameObject good_judge_prefab;
public GameObject miss_judge_prefab;
[Header("轨道关联的判定文字 (Legacy Text)")]
// 请在 Inspector 中将对应的 UI Text 拖入
[Header("ж (Legacy Text)")]
// Inspector нӦ UI Text
public TextMeshProUGUI red_track_judgementText;
public TextMeshProUGUI green_track_judgementText;
public TextMeshProUGUI yellow_track_judgementText;
@@ -124,7 +122,13 @@ public class AnimationController : MonoBehaviour
{
while (holdActive)
{
yield return new WaitForSeconds(Mathf.Max(0.01f, holdParticleInterval));
float waitSeconds = Mathf.Max(0.01f, holdParticleInterval);
if (cachedHoldWait == null || !Mathf.Approximately(cachedHoldWaitSeconds, waitSeconds))
{
cachedHoldWaitSeconds = waitSeconds;
cachedHoldWait = new WaitForSeconds(waitSeconds);
}
yield return cachedHoldWait;
if (!holdActive) break;
PlayDestroyAnimation(color);
}
@@ -143,7 +147,7 @@ public class AnimationController : MonoBehaviour
Global = this;
DontDestroyOnLoad(gameObject);
Debug.Log("[AnimationController] Global AnimationController registered");
if (JudgeManager.IsDebugEnabled) Debug.Log("[AnimationController] Global AnimationController registered");
}
// Animator cache
@@ -164,7 +168,7 @@ public class AnimationController : MonoBehaviour
if (byTag != null)
{
hit_particular_object = byTag;
Debug.Log("AnimationController: assigned hit_particular_object via tag 'HitParticleSource'.");
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: assigned hit_particular_object via tag 'HitParticleSource'.");
}
}
catch { }
@@ -176,15 +180,16 @@ public class AnimationController : MonoBehaviour
if (byName != null)
{
hit_particular_object = byName;
Debug.Log("AnimationController: assigned hit_particular_object via GameObject.Find by name.");
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: assigned hit_particular_object via GameObject.Find by name.");
}
}
if (hit_particular_object == null)
{
Debug.LogWarning("AnimationController: hit_particular_object not assigned in Inspector and automatic lookup failed.\n" +
"You can either assign a scene object to 'hit_particular_object' on the prefab instance in the scene,\n" +
"or tag the scene particle source with 'HitParticleSource', or place a GameObject named 'HitParticleSource' in the scene.");
if (JudgeManager.IsDebugEnabled)
Debug.LogWarning("AnimationController: hit_particular_object not assigned in Inspector and automatic lookup failed.\n" +
"You can either assign a scene object to 'hit_particular_object' on the prefab instance in the scene,\n" +
"or tag the scene particle source with 'HitParticleSource', or place a GameObject named 'HitParticleSource' in the scene.");
}
}
}
@@ -194,7 +199,11 @@ public class AnimationController : MonoBehaviour
// Only use the scene object as the template. No prefab fallback.
if (hit_particular_object == null)
{
Debug.LogError("hit_particular_object is null. Assign a scene particle source to AnimationController.hit_particular_object or tag a GameObject 'HitParticleSource'.");
if (!loggedMissingHitParticleSource)
{
loggedMissingHitParticleSource = true;
Debug.LogError("hit_particular_object is null. Assign a scene particle source to AnimationController.hit_particular_object or tag a GameObject 'HitParticleSource'.");
}
// As a fallback, trigger the Animator-based effects
TriggerAnimatorEffect(color);
return;
@@ -214,15 +223,15 @@ public class AnimationController : MonoBehaviour
case "blue": if (blueEffect != null) { spawnPos = blueEffect.transform.position; parentTransform = blueEffect.transform; } break;
}
// 获取并解析颜色
// ȡɫ
Color trackColor;
string hexCode = GetHexCodeForColor(color);
// 尝试解析 16 进制颜色代码。如果失败,使用白色作为默认值。
// Խ 16 ɫʧܣʹðɫΪĬֵ
if (!ColorUtility.TryParseHtmlString(hexCode, out trackColor))
{
trackColor = Color.white;
Debug.LogWarning($"Failed to parse hex color for '{color}' ({hexCode}). Using white.");
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"Failed to parse hex color for '{color}' ({hexCode}). Using white.");
}
SpawnJudgePrefabByTrackText(color);
@@ -281,7 +290,7 @@ public class AnimationController : MonoBehaviour
GameObject ringInstance = Instantiate(hit_ring_object, spawnPos, Quaternion.identity);
if (ringInstance != null)
{
Debug.Log($"AnimationController: instantiated hit_ring_object '{ringInstance.name}' at {spawnPos}. parentTransform={(parentTransform != null ? parentTransform.name : "null")} ");
if (JudgeManager.IsDebugEnabled) Debug.Log($"AnimationController: instantiated hit_ring_object '{ringInstance.name}' at {spawnPos}. parentTransform={(parentTransform != null ? parentTransform.name : "null")} ");
// Parent before activation to avoid transform surprises when Simulation Space = Local
if (parentTransform != null)
@@ -289,14 +298,14 @@ public class AnimationController : MonoBehaviour
ringInstance.transform.SetParent(parentTransform, false);
// keep world position at spawnPos
ringInstance.transform.position = spawnPos;
Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name);
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name);
}
// Activate after parenting
ringInstance.SetActive(true);
var ringSystems = ringInstance.GetComponentsInChildren<ParticleSystem>(true);
Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0));
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0));
if (ringSystems != null && ringSystems.Length > 0)
{
float ringMaxLife = 0f;
@@ -304,7 +313,7 @@ public class AnimationController : MonoBehaviour
{
var rmain = rps.main;
// log important runtime properties for debugging
Debug.Log($"ring PS: {rps.gameObject.name} simulationSpace={rmain.simulationSpace} startLifetime={rmain.startLifetime.constant} startColor={rmain.startColor.color}");
if (JudgeManager.IsDebugEnabled) Debug.Log($"ring PS: {rps.gameObject.name} simulationSpace={rmain.simulationSpace} startLifetime={rmain.startLifetime.constant} startColor={rmain.startColor.color}");
// reuse same track color
rmain.startColor = trackColor;
@@ -319,7 +328,7 @@ public class AnimationController : MonoBehaviour
{
var emission = rps.emission;
var rate = emission.rateOverTime;
Debug.Log($"ring PS emission rateOverTime.constant (approx) = {rate.constant}");
if (JudgeManager.IsDebugEnabled) Debug.Log($"ring PS emission rateOverTime.constant (approx) = {rate.constant}");
}
catch { }
}
@@ -331,7 +340,7 @@ public class AnimationController : MonoBehaviour
}
else
{
Debug.LogWarning("hit_ring_object has no ParticleSystem components");
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("hit_ring_object has no ParticleSystem components");
Destroy(ringInstance);
}
}
@@ -342,7 +351,7 @@ public class AnimationController : MonoBehaviour
}
}
// 用于根据颜色名称获取对应的 16 进制代码
// ڸɫƻȡӦ 16 ƴ
private string GetHexCodeForColor(string color)
{
switch (color)
@@ -352,7 +361,7 @@ public class AnimationController : MonoBehaviour
case "yellow": return yellowColorHex;
case "purple": return purpleColorHex;
case "blue": return blueColorHex;
default: return "#FFFFFF"; // 默认返回白色
default: return "#FFFFFF"; // ĬϷذɫ
}
}
@@ -399,14 +408,14 @@ public class AnimationController : MonoBehaviour
}
}
/// <summary>
/// 根据传入的颜色频道,定位到对应的 UI Text 并读取其内容
/// ݴɫƵλӦ UI Text ȡ
/// </summary>
private void SpawnJudgePrefabByTrackText(string color)
{
TextMeshProUGUI targetText = null;
Transform spawnPoint = null;
// 1. 匹配对应的文本组件和位置
// 1. ƥӦıλ
switch (color)
{
case "red": targetText = red_track_judgementText; spawnPoint = redEffect != null ? redEffect.transform : null; break;
@@ -417,11 +426,11 @@ public class AnimationController : MonoBehaviour
}
if (targetText != null && spawnPoint != null)
{
// 打印调试信息,看程序到底读到了什么文字
Debug.LogError($"轨道 {color} 当前读到的文字: [{targetText.text}]");
// 打印调试信息,确认判定到底读取到的是什么文字
if (JudgeManager.IsDebugEnabled) Debug.Log($"判定 {color} 当前读到的文字: [{targetText.text}]");
}
// 2. 如果找到了文本且文本不为空,则执行喷出逻辑
// 2. ҵııΪգִ߼
if (targetText != null && spawnPoint != null && !string.IsNullOrEmpty(targetText.text))
{
DoExecutePrefabSpawn(targetText.text, spawnPoint);
@@ -429,13 +438,13 @@ public class AnimationController : MonoBehaviour
}
/// <summary>
/// 最终执行实例化的函数
/// ִʵĺ
/// </summary>
private void DoExecutePrefabSpawn(string judgeResult, Transform spawnTransform)
{
GameObject prefabToUse = null;
// 字符串匹配(需确保与 InputManager 中传入的字符串一致)
// ַƥȷ InputManager дַһ£
if (judgeResult.Contains("Perfect")) prefabToUse = perfect_judge_prefab;
else if (judgeResult.Contains("Great")) prefabToUse = great_judge_prefab;
else if (judgeResult.Contains("Good")) prefabToUse = good_judge_prefab;
@@ -443,10 +452,10 @@ public class AnimationController : MonoBehaviour
if (prefabToUse != null)
{
// 在对应特效点生成判定 Prefab
// ڶӦЧж Prefab
GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity);
// 自动销毁,防止堆积
// Զ٣ֹѻ
Destroy(instance, 1.0f);
}
}
@@ -524,4 +533,4 @@ public class AnimationController : MonoBehaviour
{
yield return StartCoroutine(PrewarmCoroutine(perTemplate));
}
}
}
@@ -5,33 +5,33 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
{
public static Animation_GenerateJudgementSituationPrefab Instance;
[Header("生成位置 (五个轨道)")]
[Header("λ ()")]
public GameObject redEffect;
public GameObject greenEffect;
public GameObject yellowEffect;
public GameObject purpleEffect;
public GameObject blueEffect;
[Header("判定飘字 Prefabs")]
[Header("жƮ Prefabs")]
public GameObject perfect_judge_prefab;
public GameObject great_judge_prefab;
public GameObject good_judge_prefab;
public GameObject miss_judge_prefab;
[Header("缩放与随机")]
[Header("")]
public float baseScale = 1.0f;
public bool useRandomScale = true;
public float minScaleMultiplier = 0.8f;
public float maxScaleMultiplier = 1.2f;
[Header("物理跳跃设置")]
public float jumpForce = 5.0f; // 向上弹起的初始速度
public float gravity = -12.0f; // 重力
[Header("Ծ")]
public float jumpForce = 5.0f; // ϵijʼٶ
public float gravity = -12.0f; //
[Header("时间与透明度控制 (秒)")]
public float fadeInTime = 0.1f; // 生成后多少秒完成渐显 (0->1)
public float fadeOutStartTime = 0.6f; // 生成后第几秒开始渐隐
public float fadeOutDuration = 0.3f; // 渐隐动画持续时长
[Header("ʱ͸ȿ ()")]
public float fadeInTime = 0.1f; // ɺɽ (0->1)
public float fadeOutStartTime = 0.6f; // ɺڼ뿪ʼ
public float fadeOutDuration = 0.3f; // ʱ
// Cache spawn transforms to avoid accessing destroyed GameObject references.
private Transform redSpawn;
@@ -48,8 +48,8 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
if (Instance == null)
{
Instance = this;
// 注意:仅当第一次创建时保留,允许重新加载
// DontDestroyOnLoad(gameObject); // ← 移除此行以允许场景重新加载时的清理
// עһδʱ¼
// DontDestroyOnLoad(gameObject); // Ƴ¼ʱ
}
else if (Instance != this)
{
@@ -73,7 +73,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
int currentSceneIndex = UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex;
if (currentSceneIndex != lastSceneIndex)
{
if (GameConfig.verboseLogs) Debug.Log($"[Animation_Generate] Scene changed detected (from {lastSceneIndex} to {currentSceneIndex}), refreshing spawn point cache");
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Animation_Generate] Scene changed detected (from {lastSceneIndex} to {currentSceneIndex}), refreshing spawn point cache");
lastSceneIndex = currentSceneIndex;
CacheSpawnPoints();
}
@@ -111,7 +111,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
purpleSpawn = purpleEffect != null ? purpleEffect.transform : null;
blueSpawn = blueEffect != null ? blueEffect.transform : null;
if (GameConfig.verboseLogs)
if (JudgeManager.IsDebugEnabled)
{
Debug.Log($"[Animation_Generate] Cached spawn points: red={redSpawn!=null}, green={greenSpawn!=null}, yellow={yellowSpawn!=null}, purple={purpleSpawn!=null}, blue={blueSpawn!=null}");
}
@@ -145,17 +145,17 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
return;
}
if (GameConfig.verboseLogs) Debug.Log($"[Animation_Generate] Spawning judge prefab: color={color}, result={judgeResult}, prefab={prefabToUse.name}");
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Animation_Generate] Spawning judge prefab: color={color}, result={judgeResult}, prefab={prefabToUse.name}");
GameObject instance = Instantiate(prefabToUse, spawnPoint);
instance.transform.localPosition = Vector3.zero;
// 处理缩放
//
float finalScale = baseScale;
if (useRandomScale) finalScale *= Random.Range(minScaleMultiplier, maxScaleMultiplier);
instance.transform.localScale = new Vector3(finalScale, finalScale, 1f);
// 开启运动逻辑
// ˶߼
StartCoroutine(AnimateSprite(instance));
}
@@ -168,10 +168,10 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
float vVelocity = jumpForce;
Vector3 currentLocalPos = Vector3.zero;
// 计算总生命周期:开始渐隐的时间 + 渐隐持续的时长
// ڣʼʱ + ʱ
float totalLifeTime = fadeOutStartTime + fadeOutDuration;
// 初始透明度
// ʼ͸
if (sr != null)
{
Color c = sr.color;
@@ -179,7 +179,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
sr.color = c;
}
// 只要没到总寿命,就持续执行
// ֻҪûͳִ
while (elapsed < totalLifeTime)
{
if (obj == null) yield break;
@@ -187,28 +187,28 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
float dt = Time.deltaTime;
elapsed += dt;
// --- 1. 物理位移 ---
// --- 1. λ ---
vVelocity += gravity * dt;
currentLocalPos.y += vVelocity * dt;
obj.transform.localPosition = currentLocalPos;
// --- 2. 透明度逻辑 (基于秒数) ---
// --- 2. ͸߼ () ---
if (sr != null)
{
float alpha;
// 渐显阶段:当前时间 < fadeInTime
// Խ׶Σǰʱ < fadeInTime
if (elapsed < fadeInTime)
{
alpha = Mathf.InverseLerp(0f, fadeInTime, elapsed);
}
// 渐隐阶段:当前时间 > fadeOutStartTime
// ׶Σǰʱ > fadeOutStartTime
else if (elapsed > fadeOutStartTime)
{
// fadeOutStartTime totalLifeTime 之间从 1 变到 0
// fadeOutStartTime totalLifeTime ֮ 1 0
alpha = Mathf.InverseLerp(totalLifeTime, fadeOutStartTime, elapsed);
}
// 中间完全显示阶段
// мȫʾ׶
else
{
alpha = 1f;
@@ -222,7 +222,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
yield return null;
}
// --- 确保最后一帧是完全透明的 ---
// --- ȷһ֡ȫ͸ ---
if (obj != null && sr != null)
{
Color c = sr.color;
@@ -230,7 +230,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
sr.color = c;
}
// 等待渲染完成,防止闪删
// ȴȾɣֹɾ
yield return new WaitForEndOfFrame();
if (obj != null) Destroy(obj);
@@ -240,7 +240,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
{
if (string.IsNullOrEmpty(color)) return null;
// 在每次调用时检查是否需要重新缓存
// ÿεʱǷҪ»
CacheSpawnPointsIfNeeded();
Transform point = null;
+4 -2
View File
@@ -5,8 +5,10 @@ public abstract class BaseNote : MonoBehaviour
public int TrackIndex { get; protected set; }
public string NoteColor { get; protected set; }
public float Speed { get; protected set; }
protected float hitTime;
protected float hitTime;
public float HitTime => hitTime;
public virtual void Initialize(int trackIndex, string noteColor, float speed, float hitTime)
{
TrackIndex = trackIndex;
@@ -210,6 +210,8 @@ public class BeatmapManager : MonoBehaviour
// ͳһĵ˴߼ HP ã
SetupEnemiesAndHP();
ApplyTrackScoreCaps(beatmap);
ApplyTrackScoreCaps(beatmap);
return true;
}
@@ -221,6 +223,7 @@ public class BeatmapManager : MonoBehaviour
Debug.Log("Ѽأ Beatmap 󣩣" + (beatmap != null ? beatmap.title : "null"));
// No globalDelaySeconds available in this path. Call NoteSpawner directly.
noteSpawner.LoadBeatmap(beatmap);
ApplyTrackScoreCaps(beatmap);
}
// ȡصǰã
@@ -299,7 +302,7 @@ public class BeatmapManager : MonoBehaviour
Debug.LogWarning("BeatmapManager.Start: chart parsed from SongData, attempting to assign audio and pause system");
// try to assign audio to GameManager.musicSource
var gm = FindObjectOfType<GameManager>();
var gm = FindAnyObjectByType<GameManager>();
if (gm != null && gm.musicSource != null)
{
if (assignedSongData != null && assignedSongData.audioFile != null)
@@ -369,7 +372,7 @@ public class BeatmapManager : MonoBehaviour
}
// Pause the system to show pause overlay (PauseManager will enable overlayRoot)
var pauseMgr = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
var pauseMgr = PauseManager.Instance ?? FindAnyObjectByType<PauseManager>();
if (pauseMgr != null)
{
pauseMgr.Pause(true);
@@ -575,7 +578,7 @@ public class BeatmapManager : MonoBehaviour
if (parsedNoteAmount <= 0)
{
parsedNoteAmount = parsed.notes.Length;
if (GameConfig.verboseLogs) Debug.Log($"[BeatmapManager] Calculated missing noteAmount: {parsedNoteAmount}");
if (JudgeManager.IsDebugEnabled) Debug.Log($"[BeatmapManager] Calculated missing noteAmount: {parsedNoteAmount}");
}
// Ensure note statistics (counts by color) are populated
@@ -596,10 +599,72 @@ public class BeatmapManager : MonoBehaviour
{
parsedNoteStatistics[i++] = new NoteStatistic { colorType = kvp.Key, count = kvp.Value };
}
if (GameConfig.verboseLogs) Debug.Log($"[BeatmapManager] Calculated missing noteStatistics for {counts.Count} colors");
if (JudgeManager.IsDebugEnabled) Debug.Log($"[BeatmapManager] Calculated missing noteStatistics for {counts.Count} colors");
}
}
// Compute per-track note counts and apply max score caps to allies
private void ApplyTrackScoreCaps(Beatmap parsed)
{
if (parsed == null || parsed.notes == null) return;
const int trackCount = 5;
var counts = new int[trackCount];
for (int i = 0; i < parsed.notes.Length; i++)
{
var n = parsed.notes[i];
int idx = n != null ? n.trackIndex : -1;
if (idx < 0 || idx >= trackCount) continue;
counts[idx]++;
}
int per = perNoteScore;
if (per <= 0 && parsedNoteAmount > 0)
per = totalChartScore / parsedNoteAmount;
var ui = teamUIController.Instance;
for (int i = 0; i < trackCount; i++)
{
int maxScore = Mathf.Max(0, per * counts[i]);
var ally = ResolveAllyCombatant(i, ui);
if (ally != null)
{
ally.SetMaxTrackScore(maxScore, true);
continue;
}
// Fallback: update UI text directly if combatant not found
string value = $"0/{maxScore}";
if (ui != null)
{
switch (i)
{
case 0: if (ui.teammate01_current_scoreText != null) ui.teammate01_current_scoreText.text = value; break;
case 1: if (ui.teammate02_current_scoreText != null) ui.teammate02_current_scoreText.text = value; break;
case 2: if (ui.teammate03_current_scoreText != null) ui.teammate03_current_scoreText.text = value; break;
case 3: if (ui.teammate04_current_scoreText != null) ui.teammate04_current_scoreText.text = value; break;
case 4: if (ui.teammate05_current_scoreText != null) ui.teammate05_current_scoreText.text = value; break;
}
}
}
}
private AllyCombatant ResolveAllyCombatant(int slotIndex, teamUIController ui)
{
GameObject go = null;
if (ui != null)
go = ui.GetAllyObjectBySlot(slotIndex);
if (go == null)
{
var byName = GameObject.Find($"ally_0{slotIndex + 1}");
if (byName != null) go = byName;
}
if (go == null) return null;
var ally = go.GetComponent<AllyCombatant>();
if (ally != null) return ally;
return go.GetComponentInChildren<AllyCombatant>(true);
}
[System.Serializable]
private class BeatmapExtra
{
@@ -633,4 +698,4 @@ public class BeatmapManager : MonoBehaviour
public class TrackStates { public bool trackFading_active; public TrackState red; public TrackState green; public TrackState yellow; public TrackState purple; public TrackState blue; }
[System.Serializable]
public class TrackState { public bool isOn; public float fadeTime; }
}
}
@@ -3,6 +3,9 @@ public static class GameConfig
// Toggle detailed logging for debugging. Keep false in production for performance.
public static bool verboseLogs = false;
// Toggle skill effect logging.
public static bool skillDebugMode = true;
// When true, gameplay will not automatically read JSON beatmaps on Start.
public static bool testMode = false;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -15,7 +15,7 @@ public static class HoldNoteJudgePool
public object noteData; // store as object to avoid type dependency
}
private static readonly Dictionary<string, HoldInfo> pool = new Dictionary<string, HoldInfo>();
private static Dictionary<string, HoldInfo> pool = new Dictionary<string, HoldInfo>();
public static void RegisterStart(string noteID, float pressTime, float hitTime, float scheduledEnd, string color, int trackIndex, object noteData)
{
@@ -32,7 +32,7 @@ public static class HoldNoteJudgePool
noteData = noteData
};
pool[noteID] = info;
Debug.Log($"[HoldNoteJudgePool] Registered start for {noteID} press={pressTime:F3} hit={hitTime:F3} end={scheduledEnd:F3}");
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNoteJudgePool] Registered start for {noteID} press={pressTime:F3} hit={hitTime:F3} end={scheduledEnd:F3}");
}
public static bool TryGet(string noteID, out HoldInfo info)
+190 -176
View File
@@ -1,176 +1,190 @@
using UnityEngine;
using TMPro;
using System;
using UnityEngine.UI;
public class InputManager : MonoBehaviour
{
public static InputManager Instance { get; private set; }
public static event Action<KeyCode> OnKeyPressed;
public static event Action<KeyCode> OnKeyReleased;
[Header("轨道判定显示TMP对象(请在Inspector中连接)")]
public TextMeshProUGUI[] trackJudgeTexts = new TextMeshProUGUI[5];
[Header("按键指示文本(独立于判定文本,五个分别对应按键:D,F,Space,J,K")]
public Text[] trackKeyTexts = new Text[5];
[Header("是否显示判定文字")]
public bool showJudgeText = true;
// 按键激活/非激活颜色
[Header("按键高亮颜色设置")]
[Tooltip("按键被按下时的颜色")]
public Color keyActiveColor = Color.yellow;
[Tooltip("按键未按下时的颜色(默认黑色)")]
public Color keyInactiveColor = Color.black;
// 判定结果对应颜色
[Header("判定结果对应颜色")]
public Color perfectColor = Color.yellow;
public Color greatColor = Color.green;
public Color goodColor = Color.cyan;
public Color missColor = Color.red;
private void Awake()
{
if (Instance == null)
Instance = this;
else
Destroy(gameObject);
// 初始化按键文本为非激活颜色
if (trackKeyTexts != null)
{
foreach (var t in trackKeyTexts)
{
if (t != null) t.color = keyInactiveColor;
}
}
}
private void Start()
{
// Ensure UI shows current bindings from KeyBindingManager/PlayerPrefs
RefreshKeyLabels();
}
private void Update()
{
foreach (string color in new string[] { "red", "green", "yellow", "purple", "blue" })
{
KeyCode key = KeyBindingManager.GetKeyForColor(color);
if (key == KeyCode.None) continue;
int index = GetIndexForColor(color);
if (Input.GetKeyDown(key))
{
OnKeyPressed?.Invoke(key);
// 调用 JudgeManager 统一判断最早的音符
JudgeManager.Instance.JudgeEarliestNote(key);
// 按下时设置对应的按键文本颜色为激活色(如果已设置)
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
txt.color = keyActiveColor;
}
}
if (Input.GetKeyUp(key))
{
OnKeyReleased?.Invoke(key);
// 松开时恢复为非激活颜色
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
txt.color = keyInactiveColor;
}
}
}
}
/// <summary>
/// Force a release event for all bound keys and reset UI indicators.
/// Useful for external "clear input" operations where the system should treat
/// all keys as released regardless of physical state.
/// </summary>
public void ForceReleaseAllKeys()
{
string[] colors = new string[] { "red", "green", "yellow", "purple", "blue" };
foreach (var color in colors)
{
KeyCode key = KeyBindingManager.GetKeyForColor(color);
if (key == KeyCode.None) continue;
try { OnKeyReleased?.Invoke(key); } catch { }
int index = GetIndexForColor(color);
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null) txt.color = keyInactiveColor;
}
}
}
// Refresh displayed labels for key bindings (called after rebind)
public void RefreshKeyLabels()
{
string[] colors = new string[] { "red", "green", "yellow", "purple", "blue" };
if (trackKeyTexts == null) return;
for (int i = 0; i < colors.Length && i < trackKeyTexts.Length; i++)
{
var txt = trackKeyTexts[i];
if (txt == null) continue;
KeyCode k = KeyBindingManager.GetKeyForColor(colors[i]);
txt.text = KeyBindingManager.GetDisplayName(k);
}
}
private int GetIndexForColor(string color)
{
// 保持与 trackJudgeTexts 相同的索引映射
switch (color)
{
case "red": return 0;
case "green": return 1;
case "yellow": return 2;
case "purple": return 3;
case "blue": return 4;
default: return -1;
}
}
/// <summary>
/// 更新指定轨道的判定文本和颜色
/// </summary>
public void ShowJudgeResult(int trackIndex, string result)
{
if (!showJudgeText) return;
if (trackJudgeTexts == null || trackIndex < 0 || trackIndex >= trackJudgeTexts.Length) return;
var textObj = trackJudgeTexts[trackIndex];
if (textObj == null) return;
textObj.text = result;
switch (result)
{
case "Perfect":
textObj.color = perfectColor;
break;
case "Great":
textObj.color = greatColor;
break;
case "Good":
textObj.color = goodColor;
break;
case "Miss":
textObj.color = missColor;
break;
default:
textObj.color = Color.white;
break;
}
}
}
using UnityEngine;
using TMPro;
using System;
using UnityEngine.UI;
public class InputManager : MonoBehaviour
{
public static InputManager Instance { get; private set; }
public static event Action<KeyCode> OnKeyPressed;
public static event Action<KeyCode> OnKeyReleased;
private static readonly string[] TrackColors = { "red", "green", "yellow", "purple", "blue" };
[Header("жʾTMPInspectorӣ")]
public TextMeshProUGUI[] trackJudgeTexts = new TextMeshProUGUI[5];
[Header("ָʾıжıֱӦD,F,Space,J,K")]
public Text[] trackKeyTexts = new Text[5];
[Header("Ƿʾж")]
public bool showJudgeText = true;
// /Ǽɫ
[Header("ɫ")]
[Tooltip("ʱɫ")]
public Color keyActiveColor = Color.yellow;
[Tooltip("δʱɫĬϺɫ")]
public Color keyInactiveColor = Color.black;
// жӦɫ
[Header("жӦɫ")]
public Color perfectColor = Color.yellow;
public Color greatColor = Color.green;
public Color goodColor = Color.cyan;
public Color missColor = Color.red;
private KeyCode[] cachedKeys = new KeyCode[5];
private void Awake()
{
if (Instance == null)
Instance = this;
else
Destroy(gameObject);
// 初始化按键缓存
RefreshKeyCache();
// ʼıΪǼɫ
if (trackKeyTexts != null)
{
foreach (var t in trackKeyTexts)
{
if (t != null) t.color = keyInactiveColor;
}
}
}
private void RefreshKeyCache()
{
for (int i = 0; i < TrackColors.Length; i++)
{
cachedKeys[i] = KeyBindingManager.GetKeyForColor(TrackColors[i]);
}
}
private void Start()
{
// Ensure UI shows current bindings from KeyBindingManager/PlayerPrefs
RefreshKeyLabels();
}
private void Update()
{
for (int i = 0; i < TrackColors.Length; i++)
{
KeyCode key = cachedKeys[i];
if (key == KeyCode.None) continue;
int index = i; // TrackColors array index matches track index logic here
if (Input.GetKeyDown(key))
{
OnKeyPressed?.Invoke(key);
// JudgeManager ͳһж
JudgeManager.Instance.JudgeEarliestNote(key);
// ʱöӦİıɫΪɫã
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
txt.color = keyActiveColor;
}
}
if (Input.GetKeyUp(key))
{
OnKeyReleased?.Invoke(key);
// ɿʱָΪǼɫ
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
txt.color = keyInactiveColor;
}
}
}
}
/// <summary>
/// Force a release event for all bound keys and reset UI indicators.
/// Useful for external "clear input" operations where the system should treat
/// all keys as released regardless of physical state.
/// </summary>
public void ForceReleaseAllKeys()
{
for (int i = 0; i < TrackColors.Length; i++)
{
string color = TrackColors[i];
KeyCode key = KeyBindingManager.GetKeyForColor(color);
if (key == KeyCode.None) continue;
try { OnKeyReleased?.Invoke(key); } catch { }
int index = GetIndexForColor(color);
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null) txt.color = keyInactiveColor;
}
}
}
// Refresh displayed labels for key bindings (called after rebind)
public void RefreshKeyLabels()
{
if (trackKeyTexts == null) return;
for (int i = 0; i < TrackColors.Length && i < trackKeyTexts.Length; i++)
{
var txt = trackKeyTexts[i];
if (txt == null) continue;
KeyCode k = KeyBindingManager.GetKeyForColor(TrackColors[i]);
txt.text = KeyBindingManager.GetDisplayName(k);
}
}
private int GetIndexForColor(string color)
{
// trackJudgeTexts ͬӳ
switch (color)
{
case "red": return 0;
case "green": return 1;
case "yellow": return 2;
case "purple": return 3;
case "blue": return 4;
default: return -1;
}
}
/// <summary>
/// ָжıɫ
/// </summary>
public void ShowJudgeResult(int trackIndex, string result)
{
if (!showJudgeText) return;
if (trackJudgeTexts == null || trackIndex < 0 || trackIndex >= trackJudgeTexts.Length) return;
var textObj = trackJudgeTexts[trackIndex];
if (textObj == null) return;
textObj.text = result;
switch (result)
{
case "Perfect":
textObj.color = perfectColor;
break;
case "Great":
textObj.color = greatColor;
break;
case "Good":
textObj.color = goodColor;
break;
case "Miss":
textObj.color = missColor;
break;
default:
textObj.color = Color.white;
break;
}
}
}
@@ -10,6 +10,23 @@ public class JudgeManager : MonoBehaviour
public settlementController sc;
public NoteSpawner ns;
private bool _cachedIsDebugEnabled;
private void UpdateDebugCache()
{
_cachedIsDebugEnabled = (_enableDebugLogs || GameConfig.verboseLogs);
}
[Header("Debug Settings")]
[SerializeField] private bool _enableDebugLogs = false;
public bool EnableDebugLogs => _enableDebugLogs;
public static bool IsDebugEnabled => (Instance != null && Instance._cachedIsDebugEnabled);
[Header("Global Note Settings")]
[Tooltip("Global material applied to hold notes when they are judged.")]
public Material globalJudgedMaterial;
[Tooltip("Whether to enable material switching after a hold note is judged.")]
public bool enableJudgedMaterial = false;
public static JudgeManager Instance { get; private set; }
// total/remaining notes tracking for end-of-song detection
@@ -35,9 +52,12 @@ public class JudgeManager : MonoBehaviour
private HoldJudgeState GetHoldState(string noteID)
{
if (!holdStates.ContainsKey(noteID))
holdStates[noteID] = new HoldJudgeState();
return holdStates[noteID];
if (!holdStates.TryGetValue(noteID, out var state))
{
state = new HoldJudgeState();
holdStates[noteID] = state;
}
return state;
}
// Public event and hook invoked when the entire beatmap's last note has been judged.
@@ -52,7 +72,7 @@ public class JudgeManager : MonoBehaviour
{
if (settlement_go == null)
{
Debug.LogError("结算页面不存在!");
if (IsDebugEnabled) Debug.LogError("结算页面不存在!");
return;
}
@@ -63,7 +83,7 @@ public class JudgeManager : MonoBehaviour
}
catch (Exception ex)
{
Debug.LogWarning($"[JudgeManager] Failed to activate settlement_go: {ex}");
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Failed to activate settlement_go: {ex}");
}
// Try to resolve settlementController reference if missing
@@ -87,12 +107,12 @@ public class JudgeManager : MonoBehaviour
}
catch (Exception ex)
{
Debug.LogWarning($"[JudgeManager] Exception calling startSettlement_uiUpdate: {ex}");
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Exception calling startSettlement_uiUpdate: {ex}");
}
}
else
{
Debug.LogWarning("[JudgeManager] settlementController (sc) not found on settlement_go; cannot call startSettlement_uiUpdate().");
if (IsDebugEnabled) Debug.LogWarning("[JudgeManager] settlementController (sc) not found on settlement_go; cannot call startSettlement_uiUpdate().");
}
}
@@ -108,7 +128,7 @@ public class JudgeManager : MonoBehaviour
}
catch (Exception ex)
{
Debug.LogWarning($"[JudgeManager] Exception while invoking AllNotesJudged event: {ex}");
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Exception while invoking AllNotesJudged event: {ex}");
}
try
{
@@ -116,7 +136,7 @@ public class JudgeManager : MonoBehaviour
}
catch (Exception ex)
{
Debug.LogWarning($"[JudgeManager] Exception in OnAllNotesJudged hook: {ex}");
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] Exception in OnAllNotesJudged hook: {ex}");
}
}
@@ -125,6 +145,7 @@ public class JudgeManager : MonoBehaviour
if (Instance == null) Instance = this;
else Destroy(gameObject);
UpdateDebugCache();
if (settlement_go != null) settlement_go.SetActive(false);
}
@@ -136,7 +157,7 @@ public class JudgeManager : MonoBehaviour
{
totalNotes = Mathf.Max(0, total);
remainingNotes = totalNotes;
Debug.Log($"[JudgeManager] SetTotalNotes: total={totalNotes}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] SetTotalNotes: total={totalNotes}");
}
/// <summary>
@@ -147,10 +168,10 @@ public class JudgeManager : MonoBehaviour
{
if (remainingNotes <= 0) return;
remainingNotes = Mathf.Max(0, remainingNotes - 1);
Debug.Log($"[JudgeManager] NotifyNoteJudged: remaining={remainingNotes}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] NotifyNoteJudged: remaining={remainingNotes}");
if (remainingNotes == 0)
{
Debug.Log("[JudgeManager] All notes judged -> Triggering AllNotesJudged");
if (IsDebugEnabled) Debug.Log("[JudgeManager] All notes judged -> Triggering AllNotesJudged");
TriggerAllNotesJudged();
}
}
@@ -161,11 +182,11 @@ public class JudgeManager : MonoBehaviour
var s = GetHoldState(noteID);
if (s.startResolved)
{
Debug.LogWarning($"[JudgeManager] TryResolveStart: already resolved start for {noteID}");
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] TryResolveStart: already resolved start for {noteID}");
return false;
}
s.startResolved = true;
Debug.Log($"[JudgeManager] TryResolveStart: start resolved for {noteID} at time={Time.time:F3}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveStart: start resolved for {noteID} at time={Time.time:F3}");
return true;
}
@@ -174,11 +195,11 @@ public class JudgeManager : MonoBehaviour
var s = GetHoldState(noteID);
if (s.endResolved)
{
Debug.LogWarning($"[JudgeManager] TryResolveEnd: already resolved end for {noteID}");
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] TryResolveEnd: already resolved end for {noteID}");
return false;
}
s.endResolved = true;
Debug.Log($"[JudgeManager] TryResolveEnd: end resolved for {noteID} at time={Time.time:F3}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveEnd: end resolved for {noteID} at time={Time.time:F3}");
return true;
}
@@ -187,11 +208,11 @@ public class JudgeManager : MonoBehaviour
var s = GetHoldState(noteID);
if (s.skillTriggered)
{
Debug.LogWarning($"[JudgeManager] TryTriggerSkill: already triggered for {noteID}");
if (IsDebugEnabled) Debug.LogWarning($"[JudgeManager] TryTriggerSkill: already triggered for {noteID}");
return false;
}
s.skillTriggered = true;
Debug.Log($"[JudgeManager] TryTriggerSkill: skill triggered for {noteID} at time={Time.time:F3}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryTriggerSkill: skill triggered for {noteID} at time={Time.time:F3}");
return true;
}
@@ -200,7 +221,7 @@ public class JudgeManager : MonoBehaviour
if (holdStates.ContainsKey(noteID))
{
holdStates.Remove(noteID);
Debug.Log($"[JudgeManager] ClearHoldState: cleared state for {noteID}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] ClearHoldState: cleared state for {noteID}");
}
}
@@ -209,34 +230,34 @@ public class JudgeManager : MonoBehaviour
public void RegisterScheduledEndTime(string noteID, float endTime)
{
noteEndTimes[noteID] = endTime;
Debug.Log($"[JudgeManager] RegisterScheduledEndTime: {noteID} -> {endTime:F3}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterScheduledEndTime: {noteID} -> {endTime:F3}");
}
public float GetScheduledEndTime(string noteID)
{
return noteEndTimes.ContainsKey(noteID) ? noteEndTimes[noteID] : 0f;
return noteEndTimes.TryGetValue(noteID, out var endTime) ? endTime : 0f;
}
public void RegisterStartJudged(string noteID, bool state)
{
startJudgedNotes[noteID] = state;
Debug.Log($"[JudgeManager] RegisterStartJudged: {noteID} = {state} at time={Time.time:F3}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterStartJudged: {noteID} = {state} at time={Time.time:F3}");
}
public bool IsStartJudged(string noteID)
{
return startJudgedNotes.ContainsKey(noteID) && startJudgedNotes[noteID];
return startJudgedNotes.TryGetValue(noteID, out var state) && state;
}
public void RegisterNoteReleased(string noteID, bool state)
{
releasedNotes[noteID] = state;
Debug.Log($"[JudgeManager] RegisterNoteReleased: {noteID} = {state}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterNoteReleased: {noteID} = {state}");
}
public bool HasNoteReleased(string noteID)
{
return releasedNotes.ContainsKey(noteID) && releasedNotes[noteID];
return releasedNotes.TryGetValue(noteID, out var state) && state;
}
public void RegisterNote(KeyCode key, Note note)
@@ -246,7 +267,7 @@ public class JudgeManager : MonoBehaviour
if (!judgeQueues[key].Contains(note))
judgeQueues[key].Enqueue(note);
Debug.Log($"[JudgeManager] RegisterNote: key={key} note={note.name} time={Time.time:F3} queueSize={judgeQueues[key].Count}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterNote: key={key} note={note.name} time={Time.time:F3} queueSize={judgeQueues[key].Count}");
}
public void UnregisterNote(KeyCode key, Note note)
@@ -263,7 +284,7 @@ public class JudgeManager : MonoBehaviour
n.JudgeMiss();
}
judgeQueues[key] = newQueue;
Debug.Log($"[JudgeManager] UnregisterNote: key={key} removed {note.name}, newQueueSize={judgeQueues[key].Count}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] UnregisterNote: key={key} removed {note.name}, newQueueSize={judgeQueues[key].Count}");
}
public void JudgeEarliestNote(KeyCode key)
@@ -273,7 +294,7 @@ public class JudgeManager : MonoBehaviour
Note note = judgeQueues[key].Dequeue();
if (note != null && !note.IsJudged())
{
Debug.Log($"[JudgeManager] JudgeEarliestNote: judging {note.name} for key={key} at time={Time.time:F3}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] JudgeEarliestNote: judging {note.name} for key={key} at time={Time.time:F3}");
note.Judge();
}
}
@@ -281,15 +302,28 @@ public class JudgeManager : MonoBehaviour
public void RegisterMiddlePassed(string noteID)
{
if (!middlePassedCounts.ContainsKey(noteID))
middlePassedCounts[noteID] = 0;
middlePassedCounts[noteID]++;
Debug.Log($"[JudgeManager] Middle passed for {noteID}, total={middlePassedCounts[noteID]}");
if (middlePassedCounts.TryGetValue(noteID, out int count))
{
middlePassedCounts[noteID] = count + 1;
}
else
{
middlePassedCounts[noteID] = 1;
}
if (IsDebugEnabled) Debug.Log($"[JudgeManager] Middle passed for {noteID}, total={middlePassedCounts[noteID]}");
}
// Event for syncing alpha across segments of a hold note
public event Action<string, float> OnHoldAlphaSync;
public void SyncHoldAlpha(string noteID, float alpha)
{
OnHoldAlphaSync?.Invoke(noteID, alpha);
}
public int GetMiddlePassedCount(string noteID)
{
return middlePassedCounts.ContainsKey(noteID) ? middlePassedCounts[noteID] : 0;
return middlePassedCounts.TryGetValue(noteID, out var count) ? count : 0;
}
public void ClearNoteRecord(string noteID)
@@ -299,6 +333,6 @@ public class JudgeManager : MonoBehaviour
noteEndTimes.Remove(noteID);
middlePassedCounts.Remove(noteID);
ClearHoldState(noteID);
Debug.Log($"[JudgeManager] ClearNoteRecord: cleared records for {noteID}");
if (IsDebugEnabled) Debug.Log($"[JudgeManager] ClearNoteRecord: cleared records for {noteID}");
}
}
@@ -2,14 +2,14 @@ using UnityEngine;
public class Notes : MonoBehaviour
{
public int trackIndex; // 轨道索引
public float hitTime; // 该音符应该被击中的时间
private bool canBeJudged = false; // 是否进入判定区域
private bool isHit = false; // 是否已被击中
public int trackIndex; //
public float hitTime; // Ӧñеʱ
private bool canBeJudged = false; // Ƿж
private bool isHit = false; // Ƿѱ
private void Update()
{
// 只有进入判定区后,才允许自动miss
// ֻнж󣬲Զmiss
if (canBeJudged && !isHit && Time.timeSinceLevelLoad > hitTime + 0.3f)
{
JudgeMiss();
@@ -34,7 +34,7 @@ public class Notes : MonoBehaviour
public void JudgeNote()
{
if (!canBeJudged || isHit) return; // 仅在进入判定区后才能被判定
if (!canBeJudged || isHit) return; // ڽжܱж
float currentTime = Time.timeSinceLevelLoad;
float timeDifference = Mathf.Abs(currentTime - hitTime);
@@ -59,10 +59,10 @@ public class Notes : MonoBehaviour
private void Recycle()
{
// 如果存在 NotePool,优先归还池,否则销毁物体
// NotePoolȹ黹أ
if (NotePool.Instance != null)
{
NotePool.Instance.ReturnNote(gameObject, "red"); // color 未保存时默认 red,建议上层调用时传入
NotePool.Instance.ReturnNote(gameObject, "red"); // color δʱĬ redϲʱ
}
else
{
+390 -385
View File
@@ -1,385 +1,390 @@
using UnityEngine;
public class Note : BaseNote
{
private KeyCode keyToPress;
private string noteColor;
private AnimationController anim;
private NoteController controller;
private bool isJudged = false;
private bool hasLock = false; // track whether we hold the track lock
private NoteData noteData;
[Header("判定配置")]
public NoteJudgeConfig judgeConfig; // 判定窗口配置,包含判定时间范围
// Cache allies per track to avoid GameObject.Find on every judge.
private static AllyCombatant[] allyCache;
// Timeout handling - force miss if note isn't judged by this time
private float missDeadlineTime = -1f;
private static AllyCombatant GetAllyForTrackCached(int trackIndex)
{
if (trackIndex < 0) return null;
if (allyCache == null || allyCache.Length < 10) allyCache = new AllyCombatant[10];
if (allyCache[trackIndex] != null) return allyCache[trackIndex];
var allyGo = GameObject.Find($"ally_0{trackIndex + 1}");
if (allyGo != null)
{
allyCache[trackIndex] = allyGo.GetComponent<AllyCombatant>();
}
return allyCache[trackIndex];
}
private void Awake()
{
anim = GetComponent<AnimationController>();
controller = GetComponent<NoteController>();
}
public void Setup(KeyCode key, int trackIndex, float speed, float hitTime, string color, NoteJudgeConfig judgeConfig, NoteData data)
{
keyToPress = key;
noteColor = color;
TrackIndex = trackIndex;
Speed = speed;
this.hitTime = hitTime;
isJudged = false;
hasLock = false;
this.judgeConfig = judgeConfig;
this.noteData = data;
// Set miss deadline: hitTime + missRange (the latest time to judge before auto-miss)
float missRange = (judgeConfig?.missRange ?? 0.5f);
this.missDeadlineTime = hitTime + missRange;
if (controller != null)
{
controller.SetSpeed(speed);
}
InputManager.OnKeyPressed += HandlePress;
// 判定配置检查
if (judgeConfig == null)
{
if (GameConfig.verboseLogs) Debug.LogError($"NoteJudgeConfig is null! 判定配置未赋值!track={trackIndex}");
}
else
{
if (GameConfig.verboseLogs) Debug.Log($"NoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
}
}
private void OnDestroy()
{
InputManager.OnKeyPressed -= HandlePress;
// Release lock if we still hold it
if (hasLock)
{
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, gameObject.GetInstanceID().ToString());
hasLock = false;
}
}
private void Update()
{
// Force miss if we've passed the deadline without being judged
if (!isJudged && Time.time > missDeadlineTime && gameObject.activeSelf)
{
if (GameConfig.verboseLogs) Debug.Log($"[Note] {noteColor} on track {TrackIndex} exceeded miss deadline at time {Time.time:F3}, forcing Miss");
JudgeMiss();
}
}
private void HandlePress(KeyCode key)
{
// only proceed for matching key and not already judged
if (isJudged || key != keyToPress)
return;
string myId = gameObject.GetInstanceID().ToString();
// Ensure only one note per track consumes this physical press per frame
if (TrackKeyManager.Instance != null && !TrackKeyManager.Instance.TryConsumeTrackForFrame(TrackIndex))
{
if (GameConfig.verboseLogs) Debug.Log($"[Note] Press ignored due to frame consumption on track {TrackIndex}");
return;
}
// Ensure we are the front-most note on this track. If not, ignore this press so later press can hit the next note.
var headId = TrackKeyManager.Instance?.GetCurrentNoteId(TrackIndex);
if (headId != null && headId != myId)
{
if (GameConfig.verboseLogs) Debug.Log($"[Note] Press ignored because another note is ahead on track {TrackIndex}: head={headId} me={myId}");
return;
}
// Try to acquire lock for this note (prevents multiple notes on same track being judged at once)
if (TrackKeyManager.Instance != null && !TrackKeyManager.Instance.TryLockTrackForJudge(TrackIndex, myId))
return;
hasLock = true;
float pressTime = Time.time;
float maxWindow = (judgeConfig?.missRange ?? 0.5f);
// If there is a controller and the note is not inside judge zone, only allow judgment
// if the press time is within the allowed window. Otherwise ignore the press.
if (controller != null && !controller.IsInJudgeZone())
{
if (Mathf.Abs(pressTime - hitTime) > maxWindow)
{
// outside allowed window and not in judge zone -> release lock and ignore the press
ReleaseTrackLock(myId);
if (GameConfig.verboseLogs) Debug.Log($"[Note] {noteColor} press rejected: outside timing window and not in judge zone");
return;
}
// otherwise within window: fall through to normal judging
}
float timeDifference = Mathf.Abs(pressTime - hitTime);
// raw offset ms: positive = note was early (hitTime > pressTime)
float rawOffsetMs = (hitTime - pressTime) * 1000f;
string judgeResult = null;
if (judgeConfig != null)
{
if (timeDifference <= judgeConfig.perfectRange)
{
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Perfect");
judgeResult = "Perfect";
ScoreManager.Instance.countPerfect += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackPerfectCounts[TrackIndex]++;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
ScoreManager.Instance.RecordOffset(rawOffsetMs);
}
else if (timeDifference <= judgeConfig.greatRange)
{
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Great");
judgeResult = "Great";
ScoreManager.Instance.countGreat += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackGreatCounts[TrackIndex]++;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
ScoreManager.Instance.RecordOffset(rawOffsetMs);
}
else if (timeDifference <= judgeConfig.goodRange)
{
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Good");
judgeResult = "Good";
ScoreManager.Instance.countGood += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackGoodCounts[TrackIndex]++;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
ScoreManager.Instance.RecordOffset(rawOffsetMs);
}
else
{
JudgeMiss();
return;
}
}
else
{
// fallback timing
if (timeDifference <= 0.08f)
{
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Perfect");
judgeResult = "Perfect";
ScoreManager.Instance.countPerfect += 1;
// Add missing offset record for fallback path too if possible
ScoreManager.Instance.RecordOffset(0);
}
else if (timeDifference <= 0.15f)
{
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Great");
judgeResult = "Great";
ScoreManager.Instance.countGreat += 1;
ScoreManager.Instance.RecordOffset(0);
}
else
{
JudgeMiss();
return;
}
}
if (!string.IsNullOrEmpty(judgeResult))
{
InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult);
JudgeSoundManager.Instance?.PlayJudgeSound(judgeResult);
teamUIController.Instance?.OnJudgeResult(judgeResult);
// Spawn judgment prefab - ensure noteColor is passed correctly
if (GameConfig.verboseLogs) Debug.Log($"[Note] Spawning judge prefab: color={noteColor}, result={judgeResult}");
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, judgeResult);
try
{
var ally = GetAllyForTrackCached(TrackIndex);
if (ally != null)
{
int added = ally.AddScoreForJudge(judgeResult);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[Note] Failed to add per-track score: {ex}");
}
try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, judgeResult, SkillDefinition.NoteTypeTrigger.Tap); } catch { }
}
Judge();
}
public bool IsJudged()
{
return isJudged;
}
public void Judge()
{
if (isJudged)
return;
isJudged = true;
if (controller != null)
{
controller.StopMovement();
controller.PlayHitEffect();
}
// Remove from per-track queue immediately so following notes become head
string myId = gameObject.GetInstanceID().ToString();
TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId);
// Release lock if we held it
if (hasLock)
{
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId);
hasLock = false;
}
ReturnToPool();
if (anim != null)
{
anim.PlayDestroyAnimation(noteColor);
}
// notify global judge manager that this short note has been finally judged (hit)
JudgeManager.Instance?.NotifyNoteJudged();
}
public void JudgeMiss()
{
// idempotent: this can be called from multiple paths
if (isJudged) return;
isJudged = true;
ScoreManager.Instance.countMiss += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackMissCounts[TrackIndex]++;
// Record a 0 offset for Miss to ensure total Offset Count matches note count
ScoreManager.Instance?.RecordOffset(0);
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress} Miss");
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
teamUIController.Instance?.OnJudgeResult("Miss");
// Spawn judgment prefab for Miss - ensure noteColor is passed correctly
if (GameConfig.verboseLogs) Debug.Log($"[Note.JudgeMiss] Spawning miss judge prefab: color={noteColor}");
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
try
{
var ally = GetAllyForTrackCached(TrackIndex);
if (ally != null)
{
int added = ally.AddScoreForJudge("Miss");
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[Note] Failed to add per-track score for Miss: {ex}");
}
try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, "Miss", SkillDefinition.NoteTypeTrigger.Tap); } catch { }
// notify global judge manager that this note has been finally judged (miss)
JudgeManager.Instance?.NotifyNoteJudged();
ReturnToPool();
}
private void ReleaseTrackLock(string myId)
{
if (hasLock)
{
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId);
hasLock = false;
}
}
private void ReturnToPool()
{
InputManager.OnKeyPressed -= HandlePress;
// CRITICAL: Always release lock and unregister before deactivating
string myId = gameObject.GetInstanceID().ToString();
// Release lock if we still hold it
ReleaseTrackLock(myId);
// Remove from per-track queue to avoid stale entries
TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId);
if (controller != null)
{
controller.ResetState();
}
gameObject.SetActive(false);
NotePool.Instance.ReturnNote(gameObject, noteColor);
}
public int GetTrackIndex()
{
return TrackIndex;
}
public KeyCode GetKey()
{
return keyToPress;
}
/// <summary>
/// From Controller notify Note whether it's inside judge zone
/// </summary>
public void SetJudgeZone(bool inZone)
{
// leaving judge zone
if (!inZone)
{
// Only process if we haven't been judged yet
if (!isJudged)
{
// IMPORTANT: Don't call JudgeMiss directly here because SetJudgeZone is called
// from OnTriggerExit2D, which is in a physics callback.
// 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.
if (GameConfig.verboseLogs) Debug.Log($"[Note.SetJudgeZone] Note {noteColor} left judge zone on track {TrackIndex}, will force Miss next frame");
missDeadlineTime = Time.time; // Force deadline to now so Update will handle it
}
}
}
}
using UnityEngine;
public class Note : BaseNote
{
private KeyCode keyToPress;
private string noteColor;
private AnimationController anim;
private NoteController controller;
private bool isJudged = false;
private bool hasLock = false; // track whether we hold the track lock
private NoteData noteData;
[Header("判定配置")]
public NoteJudgeConfig judgeConfig; // 判定窗口配置,包含判定时间范围
// Cache allies per track to avoid GameObject.Find on every judge.
private static AllyCombatant[] allyCache;
// Timeout handling - force miss if note isn't judged by this time
private float missDeadlineTime = -1f;
private static AllyCombatant GetAllyForTrackCached(int trackIndex)
{
if (trackIndex < 0) return null;
if (allyCache == null || allyCache.Length < 10) allyCache = new AllyCombatant[10];
if (allyCache[trackIndex] != null) return allyCache[trackIndex];
var allyGo = GameObject.Find($"ally_0{trackIndex + 1}");
if (allyGo != null)
{
allyCache[trackIndex] = allyGo.GetComponent<AllyCombatant>();
}
return allyCache[trackIndex];
}
private void Awake()
{
anim = GetComponent<AnimationController>();
controller = GetComponent<NoteController>();
}
public void Setup(KeyCode key, int trackIndex, float speed, float hitTime, string color, NoteJudgeConfig judgeConfig, NoteData data)
{
keyToPress = key;
noteColor = color;
TrackIndex = trackIndex;
Speed = speed;
this.hitTime = hitTime;
isJudged = false;
hasLock = false;
this.judgeConfig = judgeConfig;
this.noteData = data;
// Set miss deadline: hitTime + missRange (the latest time to judge before auto-miss)
float missRange = (judgeConfig?.missRange ?? 0.5f);
this.missDeadlineTime = hitTime + missRange;
if (controller != null)
{
controller.SetSpeed(speed);
}
InputManager.OnKeyPressed += HandlePress;
// 判定配置检查
if (judgeConfig == null)
{
if (JudgeManager.IsDebugEnabled) Debug.LogError($"NoteJudgeConfig is null! 判定配置未赋值!track={trackIndex}");
}
else
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"NoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
}
}
private void OnDestroy()
{
InputManager.OnKeyPressed -= HandlePress;
// Release lock if we still hold it
if (hasLock)
{
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, gameObject.GetInstanceID().ToString());
hasLock = false;
}
}
private void Update()
{
// Force miss if we've passed the deadline without being judged
if (!isJudged && Time.time > missDeadlineTime && gameObject.activeSelf)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] {noteColor} on track {TrackIndex} exceeded miss deadline at time {Time.time:F3}, forcing Miss");
JudgeMiss();
}
}
private void HandlePress(KeyCode key)
{
// only proceed for matching key and not already judged
if (isJudged || key != keyToPress)
return;
string myId = gameObject.GetInstanceID().ToString();
// Try to acquire lock for this note (prevents multiple notes on same track being judged at once)
if (TrackKeyManager.Instance != null && !TrackKeyManager.Instance.TryLockTrackForJudge(TrackIndex, myId))
return;
hasLock = true;
float pressTime = Time.time;
float maxWindow = (judgeConfig?.missRange ?? 0.5f);
// If there is a controller and the note is not inside judge zone, only allow judgment
// if the press time is within the allowed window. Otherwise ignore the press.
if (controller != null && !controller.IsInJudgeZone())
{
if (Mathf.Abs(pressTime - hitTime) > maxWindow)
{
// outside allowed window and not in judge zone -> release lock and ignore the press
ReleaseTrackLock(myId);
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] {noteColor} press rejected: outside timing window and not in judge zone");
return;
}
// otherwise within window: fall through to normal judging
}
// Prefer the closest candidate in the judge zone for this press
if (TrackKeyManager.Instance != null)
{
if (!TrackKeyManager.Instance.IsBestCandidate(TrackIndex, myId, pressTime, maxWindow))
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] Press ignored because another note is a better candidate on track {TrackIndex}");
ReleaseTrackLock(myId);
return;
}
// Ensure only one note per track consumes this physical press per frame,
// but only after we've confirmed this note is the best candidate.
if (!TrackKeyManager.Instance.TryConsumeTrackForFrame(TrackIndex))
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] Press ignored due to frame consumption on track {TrackIndex}");
ReleaseTrackLock(myId);
return;
}
}
float timeDifference = Mathf.Abs(pressTime - hitTime);
// raw offset ms: positive = note was early (hitTime > pressTime)
float rawOffsetMs = (hitTime - pressTime) * 1000f;
string judgeResult = null;
if (judgeConfig != null)
{
if (timeDifference <= judgeConfig.perfectRange)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"{keyToPress}: Perfect");
judgeResult = "Perfect";
ScoreManager.Instance.countPerfect += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackPerfectCounts[TrackIndex]++;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
ScoreManager.Instance.RecordOffset(rawOffsetMs);
}
else if (timeDifference <= judgeConfig.greatRange)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"{keyToPress}: Great");
judgeResult = "Great";
ScoreManager.Instance.countGreat += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackGreatCounts[TrackIndex]++;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
ScoreManager.Instance.RecordOffset(rawOffsetMs);
}
else if (timeDifference <= judgeConfig.goodRange)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"{keyToPress}: Good");
judgeResult = "Good";
ScoreManager.Instance.countGood += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackGoodCounts[TrackIndex]++;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
ScoreManager.Instance.RecordOffset(rawOffsetMs);
}
else
{
JudgeMiss();
return;
}
}
else
{
// fallback timing
if (timeDifference <= 0.08f)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"{keyToPress}: Perfect");
judgeResult = "Perfect";
ScoreManager.Instance.countPerfect += 1;
// Add missing offset record for fallback path too if possible
ScoreManager.Instance.RecordOffset(0);
}
else if (timeDifference <= 0.15f)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"{keyToPress}: Great");
judgeResult = "Great";
ScoreManager.Instance.countGreat += 1;
ScoreManager.Instance.RecordOffset(0);
}
else
{
JudgeMiss();
return;
}
}
if (!string.IsNullOrEmpty(judgeResult))
{
InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult);
JudgeSoundManager.Instance?.PlayJudgeSound(judgeResult);
teamUIController.Instance?.OnJudgeResult(judgeResult);
// Spawn judgment prefab - ensure noteColor is passed correctly
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] Spawning judge prefab: color={noteColor}, result={judgeResult}");
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, judgeResult);
try
{
var ally = GetAllyForTrackCached(TrackIndex);
if (ally != null)
{
int added = ally.AddScoreForJudge(judgeResult);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
catch (System.Exception ex)
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[Note] Failed to add per-track score: {ex}");
}
try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, judgeResult, SkillDefinition.NoteTypeTrigger.Tap); } catch { }
}
Judge();
}
public bool IsJudged()
{
return isJudged;
}
public void Judge()
{
if (isJudged)
return;
isJudged = true;
if (controller != null)
{
controller.StopMovement();
controller.PlayHitEffect();
}
// Remove from per-track queue immediately so following notes become head
string myId = gameObject.GetInstanceID().ToString();
TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId);
// Release lock if we held it
if (hasLock)
{
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId);
hasLock = false;
}
ReturnToPool();
if (anim != null)
{
anim.PlayDestroyAnimation(noteColor);
}
// notify global judge manager that this short note has been finally judged (hit)
JudgeManager.Instance?.NotifyNoteJudged();
}
public void JudgeMiss()
{
// idempotent: this can be called from multiple paths
if (isJudged) return;
isJudged = true;
ScoreManager.Instance.countMiss += 1;
if (TrackIndex >= 0 && TrackIndex < 5) ScoreManager.Instance.trackMissCounts[TrackIndex]++;
// Record a 0 offset for Miss to ensure total Offset Count matches note count
ScoreManager.Instance?.RecordOffset(0);
if (JudgeManager.IsDebugEnabled) Debug.Log($"{keyToPress} Miss");
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
teamUIController.Instance?.OnJudgeResult("Miss");
// Spawn judgment prefab for Miss - ensure noteColor is passed correctly
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.JudgeMiss] Spawning miss judge prefab: color={noteColor}");
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
try
{
var ally = GetAllyForTrackCached(TrackIndex);
if (ally != null)
{
int added = ally.AddScoreForJudge("Miss");
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
catch (System.Exception ex)
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[Note] Failed to add per-track score for Miss: {ex}");
}
try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, "Miss", SkillDefinition.NoteTypeTrigger.Tap); } catch { }
// notify global judge manager that this note has been finally judged (miss)
JudgeManager.Instance?.NotifyNoteJudged();
ReturnToPool();
}
private void ReleaseTrackLock(string myId)
{
if (hasLock)
{
TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId);
hasLock = false;
}
}
private void ReturnToPool()
{
InputManager.OnKeyPressed -= HandlePress;
// CRITICAL: Always release lock and unregister before deactivating
string myId = gameObject.GetInstanceID().ToString();
// Release lock if we still hold it
ReleaseTrackLock(myId);
// Remove from per-track queue to avoid stale entries
TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId);
if (controller != null)
{
controller.ResetState();
}
gameObject.SetActive(false);
NotePool.Instance.ReturnNote(gameObject, noteColor);
}
public int GetTrackIndex()
{
return TrackIndex;
}
public KeyCode GetKey()
{
return keyToPress;
}
/// <summary>
/// From Controller notify Note whether it's inside judge zone
/// </summary>
public void SetJudgeZone(bool inZone)
{
// leaving judge zone
if (!inZone)
{
// Only process if we haven't been judged yet
if (!isJudged)
{
// IMPORTANT: Don't call JudgeMiss directly here because SetJudgeZone is called
// from OnTriggerExit2D, which is in a physics callback.
// 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.
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.SetJudgeZone] Note {noteColor} left judge zone on track {TrackIndex}, will force Miss next frame");
missDeadlineTime = Time.time; // Force deadline to now so Update will handle it
}
}
}
}
@@ -65,7 +65,7 @@ public class NoteController : MonoBehaviour
activationTime = hitTime - travelTime;
baseSpawnYOffset = initialYOffset;
if (GameConfig.verboseLogs)
if (JudgeManager.IsDebugEnabled)
Debug.Log($"[NoteController] Configured: spawnPos={spawnPosition}, activationTime={activationTime:F3}, initialYOffset={initialYOffset:F4}");
}
@@ -94,7 +94,7 @@ public class NoteController : MonoBehaviour
isInJudgeZone = true;
// register note by instance id so TrackKeyManager can prioritize
// tap notes are short notes without hold
TrackKeyManager.Instance.RegisterKey(linkedNote.GetTrackIndex(), linkedNote.gameObject.GetInstanceID().ToString(), "tap");
TrackKeyManager.Instance.RegisterKey(linkedNote.GetTrackIndex(), linkedNote.gameObject.GetInstanceID().ToString(), "tap", linkedNote.HitTime);
}
}
}
+1 -1
View File
@@ -113,7 +113,7 @@ public class NotePool : MonoBehaviour
}
// Also ensure particle and judge prefab warm-up if AnimationController exists in scene.
var anim = AnimationController.Global ?? FindObjectOfType<AnimationController>();
var anim = AnimationController.Global ?? FindAnyObjectByType<AnimationController>();
if (anim != null)
{
anim.PrewarmParticles(2);
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,11 @@ public class TrackKeyManager : MonoBehaviour
private Dictionary<int, Queue<(string noteId, string noteType)>> trackKeyMappings =
new Dictionary<int, Queue<(string, string)>>();
// Active notes currently inside judge zone per track
private Dictionary<int, List<string>> trackActiveNotes = new Dictionary<int, List<string>>();
// Hit times for notes (used to pick best candidate when multiple notes overlap)
private readonly Dictionary<string, float> noteHitTimes = new Dictionary<string, float>();
// Track notes currently being judged to prevent double-judging the same note
// Format: trackIndex -> HashSet of note IDs being judged
private Dictionary<int, HashSet<string>> trackNotesBeingJudged = new Dictionary<int, HashSet<string>>();
@@ -38,6 +43,12 @@ public class TrackKeyManager : MonoBehaviour
public int pruneBatchSize = 5;
private Coroutine cleanupCoroutine;
private readonly WaitForSecondsRealtime cleanupInterval = new WaitForSecondsRealtime(1f);
private readonly List<int> trackKeysBuffer = new List<int>(16);
private readonly List<int> trackLockKeysBuffer = new List<int>(16);
private readonly List<string> noteIdsToRemoveBuffer = new List<string>(32);
private readonly HashSet<int> allTracksBuffer = new HashSet<int>();
private readonly HashSet<string> allQueuedNoteIdsBuffer = new HashSet<string>();
private void Awake()
{
@@ -80,25 +91,40 @@ public class TrackKeyManager : MonoBehaviour
while (true)
{
// run once per second (unscaled so it runs during pause)
yield return new WaitForSecondsRealtime(1f);
yield return cleanupInterval;
float now = Time.unscaledTime;
// 1) Prune head entries for all tracks (defensive)
var trackKeys = new List<int>(trackKeyMappings.Keys);
foreach (var ti in trackKeys)
trackKeysBuffer.Clear();
foreach (var ti in trackKeyMappings.Keys) trackKeysBuffer.Add(ti);
for (int i = 0; i < trackKeysBuffer.Count; i++)
{
PruneStaleHeads(ti);
PruneStaleHeads(trackKeysBuffer[i]);
}
allQueuedNoteIdsBuffer.Clear();
foreach (var qpair in trackKeyMappings)
{
var q = qpair.Value;
if (q == null || q.Count == 0) continue;
foreach (var item in q)
{
if (!string.IsNullOrEmpty(item.noteId))
allQueuedNoteIdsBuffer.Add(item.noteId);
}
}
// 2) Inspect locks and release any that reference ids that are expired or not present in any queue
var trackLockKeys = new List<int>(trackNotesBeingJudged.Keys);
foreach (var t in trackLockKeys)
trackLockKeysBuffer.Clear();
foreach (var t in trackNotesBeingJudged.Keys) trackLockKeysBuffer.Add(t);
for (int i = 0; i < trackLockKeysBuffer.Count; i++)
{
int t = trackLockKeysBuffer[i];
var locks = trackNotesBeingJudged[t];
if (locks == null || locks.Count == 0) continue;
var toRemove = new List<string>();
noteIdsToRemoveBuffer.Clear();
foreach (var noteId in locks)
{
@@ -108,35 +134,23 @@ public class TrackKeyManager : MonoBehaviour
// If expiry missing or expired, consider the id stale and remove lock
if (!hasExpiry || expired)
{
toRemove.Add(noteId);
noteIdsToRemoveBuffer.Add(noteId);
continue;
}
// Also if the queues do not contain this id anywhere, it is likely stale
bool foundInQueues = false;
foreach (var qpair in trackKeyMappings)
if (!allQueuedNoteIdsBuffer.Contains(noteId))
{
foreach (var item in qpair.Value)
{
if (!string.IsNullOrEmpty(item.noteId) && item.noteId == noteId)
{
foundInQueues = true;
break;
}
}
if (foundInQueues) break;
}
if (!foundInQueues)
{
toRemove.Add(noteId);
noteIdsToRemoveBuffer.Add(noteId);
}
}
foreach (var id in toRemove)
for (int r = 0; r < noteIdsToRemoveBuffer.Count; r++)
{
var id = noteIdsToRemoveBuffer[r];
locks.Remove(id);
noteIdExpiry.Remove(id);
if (GameConfig.verboseLogs) Debug.LogWarning($"[TrackKeyManager] Cleanup: removed stale lock id={id} on track {t}");
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[TrackKeyManager] Cleanup: removed stale lock id={id} on track {t}");
}
// If locks become empty, remove dictionary entry to keep structure clean
@@ -147,12 +161,12 @@ public class TrackKeyManager : MonoBehaviour
}
// 3) Defensive: if a track has no queued ids but has consumed-frame or locks, clear them
var allTracks = new HashSet<int>();
foreach (var k in trackKeyMappings.Keys) allTracks.Add(k);
foreach (var k in trackNotesBeingJudged.Keys) allTracks.Add(k);
foreach (var k in trackConsumedFrame.Keys) allTracks.Add(k);
allTracksBuffer.Clear();
foreach (var k in trackKeyMappings.Keys) allTracksBuffer.Add(k);
foreach (var k in trackNotesBeingJudged.Keys) allTracksBuffer.Add(k);
foreach (var k in trackConsumedFrame.Keys) allTracksBuffer.Add(k);
foreach (var track in allTracks)
foreach (var track in allTracksBuffer)
{
bool hasQueue = trackKeyMappings.ContainsKey(track) && trackKeyMappings[track].Count > 0;
bool hasLocks = trackNotesBeingJudged.ContainsKey(track) && trackNotesBeingJudged[track].Count > 0;
@@ -160,7 +174,7 @@ public class TrackKeyManager : MonoBehaviour
{
// clear locks for this track
trackNotesBeingJudged.Remove(track);
if (GameConfig.verboseLogs) Debug.LogWarning($"[TrackKeyManager] Cleanup: cleared locks for empty track {track}");
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[TrackKeyManager] Cleanup: cleared locks for empty track {track}");
}
}
}
@@ -173,14 +187,18 @@ public class TrackKeyManager : MonoBehaviour
bool isFocused = Application.isFocused;
// If we've moved to a new frame or focus state changed, clear consumption tracking
if (lastCheckedFrame != currentFrame || (isFocused && !lastFocusedState))
if (lastCheckedFrame != currentFrame)
{
// Reset per-frame tracking for the new frame
// In high-performance scenarios, we might avoid Clear() if the dict is small,
// but for safety with multiple keys, we keep it.
}
if (isFocused && !lastFocusedState)
{
// On focus regain, clear the consumed frame dictionary to allow input again
if (isFocused && !lastFocusedState)
{
trackConsumedFrame.Clear();
if (GameConfig.verboseLogs) Debug.Log("[TrackKeyManager] Focus regained, cleared track consumption");
}
trackConsumedFrame.Clear();
if (JudgeManager.IsDebugEnabled) Debug.Log("[TrackKeyManager] Focus regained, cleared track consumption");
}
lastCheckedFrame = currentFrame;
@@ -214,8 +232,11 @@ public class TrackKeyManager : MonoBehaviour
{
q.Dequeue();
noteIdExpiry.Remove(head.noteId);
noteHitTimes.Remove(head.noteId);
if (trackActiveNotes.ContainsKey(trackIndex))
trackActiveNotes[trackIndex].Remove(head.noteId);
pruned++;
if (GameConfig.verboseLogs)
if (JudgeManager.IsDebugEnabled)
Debug.LogWarning($"[TrackKeyManager] Pruned stale head on track {trackIndex}: id={head.noteId} type={head.noteType} now={now:F2} expiry={expiry:F2}");
continue;
}
@@ -226,16 +247,24 @@ public class TrackKeyManager : MonoBehaviour
}
/// <summary>
/// 当音符进入判定区域时,添加音符 id 到队列(包含note类型信息)
/// noteType: "tap" "hold"
/// жʱ id УnoteϢ
/// noteType: "tap" "hold"
/// </summary>
public void RegisterKey(int trackIndex, string noteInstanceId, string noteType = "tap")
public void RegisterKey(int trackIndex, string noteInstanceId, string noteType = "tap", float hitTime = float.NaN)
{
if (!trackKeyMappings.ContainsKey(trackIndex))
trackKeyMappings[trackIndex] = new Queue<(string, string)>();
trackKeyMappings[trackIndex].Enqueue((noteInstanceId, noteType));
if (!trackActiveNotes.ContainsKey(trackIndex))
trackActiveNotes[trackIndex] = new List<string>();
if (!string.IsNullOrEmpty(noteInstanceId) && !trackActiveNotes[trackIndex].Contains(noteInstanceId))
trackActiveNotes[trackIndex].Add(noteInstanceId);
if (!string.IsNullOrEmpty(noteInstanceId) && !float.IsNaN(hitTime))
noteHitTimes[noteInstanceId] = hitTime;
// record/refresh TTL so the id can't block forever if exit/unregister is missed
if (!string.IsNullOrEmpty(noteInstanceId))
{
@@ -244,14 +273,20 @@ public class TrackKeyManager : MonoBehaviour
}
/// <summary>
/// 当音符离开判定区域时,移除音符 id
/// жʱƳ id
/// </summary>
public void UnregisterKey(int trackIndex, string noteInstanceId)
{
if (!trackKeyMappings.ContainsKey(trackIndex) || trackKeyMappings[trackIndex].Count == 0)
{
// still clear TTL record
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
if (!string.IsNullOrEmpty(noteInstanceId))
{
noteIdExpiry.Remove(noteInstanceId);
noteHitTimes.Remove(noteInstanceId);
}
if (trackActiveNotes.ContainsKey(trackIndex))
trackActiveNotes[trackIndex].Remove(noteInstanceId);
return;
}
@@ -259,7 +294,13 @@ public class TrackKeyManager : MonoBehaviour
if (trackKeyMappings[trackIndex].Count > 0 && trackKeyMappings[trackIndex].Peek().noteId == noteInstanceId)
{
trackKeyMappings[trackIndex].Dequeue();
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
if (!string.IsNullOrEmpty(noteInstanceId))
{
noteIdExpiry.Remove(noteInstanceId);
noteHitTimes.Remove(noteInstanceId);
}
if (trackActiveNotes.ContainsKey(trackIndex))
trackActiveNotes[trackIndex].Remove(noteInstanceId);
return;
}
@@ -276,11 +317,17 @@ public class TrackKeyManager : MonoBehaviour
}
trackKeyMappings[trackIndex] = temp;
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
if (!string.IsNullOrEmpty(noteInstanceId))
{
noteIdExpiry.Remove(noteInstanceId);
noteHitTimes.Remove(noteInstanceId);
}
if (trackActiveNotes.ContainsKey(trackIndex))
trackActiveNotes[trackIndex].Remove(noteInstanceId);
}
/// <summary>
/// 获取轨道当前排在队首的音符实例 id(队列头)
/// ȡǰڶ׵ʵ idͷ
/// </summary>
public string GetCurrentNoteId(int trackIndex)
{
@@ -290,11 +337,11 @@ public class TrackKeyManager : MonoBehaviour
{
return trackKeyMappings[trackIndex].Peek().noteId;
}
return null; // 无音符
return null; //
}
/// <summary>
/// 获取当前队头note的类型("tap" "hold"
/// ȡǰͷnoteͣ"tap" "hold"
/// </summary>
public string GetCurrentNoteType(int trackIndex)
{
@@ -385,6 +432,44 @@ public class TrackKeyManager : MonoBehaviour
{
trackConsumedFrame.Clear();
}
public bool IsBestCandidate(int trackIndex, string noteId, float pressTime, float maxWindow)
{
if (trackIndex < 0 || string.IsNullOrEmpty(noteId)) return true;
if (!trackActiveNotes.TryGetValue(trackIndex, out var list) || list == null || list.Count == 0)
return true;
string bestId = null;
float bestDelta = float.MaxValue;
float bestHit = float.MaxValue;
for (int i = 0; i < list.Count; i++)
{
var id = list[i];
if (string.IsNullOrEmpty(id)) continue;
if (!noteHitTimes.TryGetValue(id, out var ht)) continue;
float delta = Mathf.Abs(pressTime - ht);
if (delta > maxWindow) continue;
if (delta < bestDelta - 0.0001f)
{
bestDelta = delta;
bestId = id;
bestHit = ht;
}
else if (Mathf.Abs(delta - bestDelta) <= 0.0001f)
{
// tie-breaker: earlier hit time wins
if (ht < bestHit)
{
bestId = id;
bestHit = ht;
}
}
}
if (bestId == null) return true;
return bestId == noteId;
}
/// <summary>
/// Force clear all track locks and per-frame consumption state.
@@ -395,6 +480,16 @@ public class TrackKeyManager : MonoBehaviour
trackNotesBeingJudged.Clear();
trackConsumedFrame.Clear();
noteIdExpiry.Clear();
if (GameConfig.verboseLogs) Debug.Log("[TrackKeyManager] ClearAllLocks called: cleared locks and consumption state");
noteHitTimes.Clear();
trackActiveNotes.Clear();
if (JudgeManager.IsDebugEnabled) Debug.Log("[TrackKeyManager] ClearAllLocks called: cleared locks and consumption state");
}
}
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using UnityEngine.Serialization;
@@ -9,22 +9,22 @@ using UnityEditor;
public class settlementController : MonoBehaviour
{
[Header("֮")]
[Header("管理器与基础引用")]
[SerializeField] private BeatmapManager bmm;
[SerializeField] private ScoreManager sm;
public GameManager gm;
private SongData thisSong_so;
private int maxScore_sum = 2000000;
[SerializeField] private Image thisSong_backPic;
[Header("ת֮ť")]
[Header("跳转控制按钮")]
public Button exit_toSelectSongs;
public Button replay_thisGame;
public Button display_rankList;
public Button share_toSocialMedia;
[Header("ıͽ")]
[Header("文本与进度显示")]
public Text songName_Text;
[Tooltip("ǰؿĽȰٷֱ")]
[Tooltip("当前关卡的进度百分比")]
public Text thisLevel_currentPercentage_Text;
public Text finalScore_Text;
public Text pmScoreSum_Text;
@@ -33,26 +33,26 @@ public class settlementController : MonoBehaviour
public Image thisLevel_progressBar_Image;
[Header("׼ȷ")]
[Header("准确度权重算法")]
public float perfect_weight = 1f;
public float great_weight = 0.6666667f;
public float good_weight = 0.333333f;
public float miss_weight = 0;
[Header("Ϣ")]
[Header("结算奖励信息")]
public Text reward_playerEXP_Text;
public Text reward_money_Text;
public Text reward_idolEXP_bottle_Text;
[Header("Ŷӽ")]
[Header("队伍展示")]
public loadSettlementTeamPrefab settlementTeamLoader;
public Image mvp_hero_hd_image;
[Header("Ǯ")]
[Header("结算获得金钱")]
[SerializeField] private long moneyToGive_thisLevel;
// õͳ
[Header("ͳ")]
// 得到评分统计
[Header("音符判定统计")]
public Text perfectHitCount_Text;
public Text perfectHitPercent_Text;
public Image perfect_barFill_Image;
@@ -76,27 +76,27 @@ public class settlementController : MonoBehaviour
public Text lateHitCount_Text;
public Text avgOffset_Text;
[Header("ϵͳ")]
[Tooltip("ֲ½")]
[Header("结算音频控制")]
[Tooltip("结算界面背景音乐,播放结算流程")]
public AudioSource settlementAudioSource;
[Tooltip("ϷֲԭϷ֣")]
[Tooltip("游戏背景音乐(通常是原游戏曲目)")]
public AudioSource oriGameMusicSource;
[Tooltip("Ƶڵͨ˲")]
[Tooltip("音频混音器,用于低通滤波等效果")]
public AudioMixer gameMusicMixer;
[Tooltip("Ƶеͨ˲")]
[Tooltip("音频混音器中的低通滤波参数名")]
public string lowpassParamName = "inGameMusic_lowpass";
[Tooltip("ͨ˲ʱ")]
[Tooltip("低通滤波器渐变持续时间(秒)")]
public float lowpassFadeDuration = 2f;
[Tooltip(" CanvasGroupڵЧ")]
[Tooltip("结算界面 CanvasGroup,用于淡入效果")]
public CanvasGroup settlementCanvasGroup;
[Header("")]
[Tooltip("")]
[Header("结算控制组件")]
[Tooltip("结算控制管理器")]
public GameObject cdm;
private Coroutine musicTransitionCoroutine;
@@ -104,7 +104,7 @@ public class settlementController : MonoBehaviour
private void Awake()
{
// Setup button listeners
// 设置按钮监听
if (replay_thisGame != null)
{
replay_thisGame.onClick.AddListener(OnReplayButtonClicked);
@@ -115,11 +115,11 @@ public class settlementController : MonoBehaviour
exit_toSelectSongs.onClick.AddListener(OnExitButtonClicked);
}
// CDM ڿʼʱֽ״̬
// 确保 CDM 对象在开始时处于关闭状态
if (cdm != null)
{
cdm.SetActive(false);
if (GameConfig.verboseLogs) Debug.Log("[SettlementController] CDM object disabled at startup");
if (JudgeManager.IsDebugEnabled) Debug.Log("[SettlementController] CDM object disabled at startup");
}
else
{
@@ -149,14 +149,14 @@ public class settlementController : MonoBehaviour
exit_toSelectSongs.onClick.RemoveListener(OnExitButtonClicked);
}
// ֹֹͣЭ
// 停止音乐过渡协程
if (musicTransitionCoroutine != null)
{
StopCoroutine(musicTransitionCoroutine);
musicTransitionCoroutine = null;
}
// ֹͣ CanvasGroup Э
// 停止 CanvasGroup 渐变协程
if (canvasFadeCoroutine != null)
{
StopCoroutine(canvasFadeCoroutine);
@@ -175,10 +175,10 @@ public class settlementController : MonoBehaviour
public void startSettlement_uiUpdate()
{
// --- ڽ̿ʼʱ CanvasGroup Ϊ͸ ---
// --- 确保结算界面开始时 CanvasGroup 为透明 ---
InitializeSettlementCanvas();
// --- ý ---
// --- 启用结算控制管理器 ---
if (cdm != null)
{
cdm.SetActive(true);
@@ -189,9 +189,9 @@ public class settlementController : MonoBehaviour
// --- Statistics: Record total play time at settlement ---
if (gm != null) gm.RecordTotalPlayTime();
else { var activeGM = FindObjectOfType<GameManager>(); if (activeGM != null) activeGM.RecordTotalPlayTime(); }
else { var activeGM = FindAnyObjectByType<GameManager>(); if (activeGM != null) activeGM.RecordTotalPlayTime(); }
// ׼֣UI֮ǰ
// 准备结算音乐(在UI更新之前)
PrepareSettlementMusic();
if(sm != null)
@@ -277,6 +277,7 @@ public class settlementController : MonoBehaviour
InGamePerformanceManager.Instance.UpdateTotalCure(totalCure);
InGamePerformanceManager.Instance.UpdateTotalDamage(totalDamage);
InGamePerformanceManager.Instance.UpdateTotalManaRestored(totalMana);
if (sm != null) InGamePerformanceManager.Instance.UpdateTotalScore(sm.allSum_pmScore + sm.allSum_idolScore);
// Load and display achievement prefabs
InGamePerformanceManager.Instance.LoadArchievePrefab();
@@ -326,7 +327,7 @@ public class settlementController : MonoBehaviour
if (settlementTeamLoader == null)
{
// try to auto-locate loader in scene if not assigned in inspector
settlementTeamLoader = FindObjectOfType<loadSettlementTeamPrefab>();
settlementTeamLoader = FindAnyObjectByType<loadSettlementTeamPrefab>();
}
if (settlementTeamLoader != null)
@@ -334,22 +335,22 @@ public class settlementController : MonoBehaviour
// ensure loader has key references
if (settlementTeamLoader.sm == null) settlementTeamLoader.sm = sm ?? ScoreManager.Instance;
if (settlementTeamLoader.tuic == null) settlementTeamLoader.tuic = teamUIController.Instance;
if (settlementTeamLoader.gm == null) settlementTeamLoader.gm = gm ?? FindObjectOfType<GameManager>();
if (settlementTeamLoader.gm == null) settlementTeamLoader.gm = gm ?? FindAnyObjectByType<GameManager>();
try { settlementTeamLoader.PopulateSettlementCards(); }
catch (System.Exception ex) { Debug.LogWarning("Failed to PopulateSettlementCards: " + ex); }
}
// ʼֹɣڽUIʾ
// ѡ MVP ͼƬ
// 初始化结算完成,在界面UI显示后
// 尝试选择 MVP 英雄的大图
SetMvpHeroImageFromTopScorer();
StartMusicTransition();
// --- ޸ģ߼ĩβʼ CanvasGroup ---
// --- 修改:在所有逻辑末尾开始 CanvasGroup 渐变 ---
StartCanvasFadeIn();
}
/// <summary>
/// ʼ CanvasGroup Ϊ͸״̬
/// 初始化结算界面的 CanvasGroup 为透明状态
/// </summary>
private void InitializeSettlementCanvas()
{
@@ -366,12 +367,15 @@ public class settlementController : MonoBehaviour
}
}
// 缓存 AllyHero_SO 数组,避免在结算界面多次调用昂贵的 Resources.LoadAll
private static AllyHero_SO[] _cachedAllyHeroSOs;
/// <summary>
/// dlcDataзҰǰDLC׼
/// 从所有dlcData中查找并准备当前歌曲所属DLC的结算音乐
/// </summary>
private void PrepareSettlementMusic()
{
// --- ޸ģƳ CanvasGroup ãڽ߼ĩβŵ ---
// --- 修改:移除 CanvasGroup 相关设置,已移动到结算逻辑末尾调用 ---
if (thisSong_so == null)
{
@@ -385,13 +389,20 @@ public class settlementController : MonoBehaviour
return;
}
// dlcData ScriptableObjects
// 使用异步或分帧逻辑来查找 DLC
StartCoroutine(PrepareSettlementMusicRoutine());
}
private IEnumerator PrepareSettlementMusicRoutine()
{
// 加载 dlcData ScriptableObjects
dlcData[] allDlcs = Resources.LoadAll<dlcData>("");
dlcData foundDlc = null;
// DLCҰǰDLC
foreach (var dlc in allDlcs)
// 遍历所有DLC数据以找到包含当前歌曲的DLC
for (int i = 0; i < allDlcs.Length; i++)
{
var dlc = allDlcs[i];
if (dlc == null || dlc.songList == null) continue;
if (dlc.songList.Contains(thisSong_so))
@@ -399,37 +410,37 @@ public class settlementController : MonoBehaviour
foundDlc = dlc;
break;
}
// 每处理 20 个 DLC 等待一帧
if (i > 0 && i % 20 == 0) yield return null;
}
if (foundDlc == null)
{
Debug.LogWarning($"[SettlementController] No DLC found containing song '{thisSong_so.songName}'");
return;
yield break;
}
if (foundDlc.settlementMusic == null)
{
Debug.LogWarning($"[SettlementController] DLC '{foundDlc.dlcName}' does not have settlement music assigned");
return;
yield break;
}
// ýֵAudioSource
// 设置结算音乐的AudioSource
settlementAudioSource.clip = foundDlc.settlementMusic;
settlementAudioSource.loop = true;
settlementAudioSource.playOnAwake = false;
// ͣȴ
// 初始音量并静音等待播放
settlementAudioSource.volume = 1f;
settlementAudioSource.mute = true;
settlementAudioSource.Stop();
Debug.Log($"[SettlementController] Prepared settlement music from DLC '{foundDlc.dlcName}': {foundDlc.settlementMusic.name}");
// --- ޸ģƳ CanvasGroup ãڽ߼ĩβŵ ---
}
/// <summary>
/// ʼֹɣϷ֣ͨͨ˲Ȼ󲥷Ž
/// 开始音乐过渡,将原游戏背景音乐淡出,然后播放结算音乐
/// </summary>
private void StartMusicTransition()
{
@@ -441,9 +452,9 @@ public class settlementController : MonoBehaviour
}
/// <summary>
/// ֹЭ̣
/// 1. Ϸֵĵͨ˲22000Hz𽥽0Hzͬʱ0
/// 2. ֹͣԭֲʼŽ
/// 音乐过渡协程:
/// 1. 将游戏音乐的低通滤波器从 22000Hz 逐渐降至 0Hz,同时音量降至 0
/// 2. 渐变完成后停止原音轨并开始播放结算音乐
/// </summary>
private IEnumerator MusicTransitionRoutine()
{
@@ -451,10 +462,10 @@ public class settlementController : MonoBehaviour
float startLowpass = 22000f;
float endLowpass = 0f;
// --- ¼ʼ ---
// --- 获取当前音量作为起始点 ---
float startVolume = (oriGameMusicSource != null) ? oriGameMusicSource.volume : 1f;
// ԭϷԴͻڣеͨ˲
// 如果原游戏音轨和混音器参数存在,执行低通滤波和音量淡出
if (oriGameMusicSource != null && gameMusicMixer != null && !string.IsNullOrEmpty(lowpassParamName))
{
Debug.Log($"[SettlementController] Starting lowpass and volume fade over {lowpassFadeDuration}s");
@@ -464,23 +475,23 @@ public class settlementController : MonoBehaviour
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / Mathf.Max(0.0001f, lowpassFadeDuration));
// 1. õͨ˲
// 1. 设置低通滤波器
float currentLowpass = Mathf.Lerp(startLowpass, endLowpass, t);
try { gameMusicMixer.SetFloat(lowpassParamName, currentLowpass); }
catch (System.Exception ex) { Debug.LogWarning($"Mixer error: {ex.Message}"); }
// 2. --- ޸ģƽ ---
// 2. --- 修改:平滑降低音量 ---
oriGameMusicSource.volume = Mathf.Lerp(startVolume, 0f, t);
yield return null;
}
// ȷ״̬
// 确保最终状态
try { gameMusicMixer.SetFloat(lowpassParamName, endLowpass); } catch { }
// --- ޸ģͣ/ֹͣ ---
// --- 修改:音量归零并暂停/停止 ---
oriGameMusicSource.volume = 0f;
oriGameMusicSource.Pause(); // ʹ .Stop()
oriGameMusicSource.Pause(); // 也可以使用 .Stop()
Debug.Log("[SettlementController] Lowpass and volume fade complete, game music paused.");
}
@@ -489,7 +500,7 @@ public class settlementController : MonoBehaviour
Debug.LogWarning("[SettlementController] oriGameMusicSource or gameMusicMixer not assigned, skipping fade");
}
// ɺ󣬿ʼŽ
// 过渡完成后,开始播放结算音乐
if (settlementAudioSource != null && settlementAudioSource.clip != null)
{
try
@@ -515,7 +526,7 @@ public class settlementController : MonoBehaviour
}
/// <summary>
/// ʼ CanvasGroup Ч
/// 开始 CanvasGroup 渐入效果
/// </summary>
private void StartCanvasFadeIn()
{
@@ -527,7 +538,7 @@ public class settlementController : MonoBehaviour
}
/// <summary>
/// CanvasGroup Э̣0.25ڴ alpha=0 alpha=1
/// CanvasGroup 渐入协程:0.25秒内从 alpha=0 渐变到 alpha=1
/// </summary>
private IEnumerator CanvasFadeInRoutine()
{
@@ -563,13 +574,13 @@ public class settlementController : MonoBehaviour
}
/// <summary>
/// ǰϷ - ¼ֺͳʼϷ״̬
/// 重新开始当前游戏 - 重新加载场景并重置游戏状态
/// </summary>
private void OnReplayButtonClicked()
{
Debug.Log("[SettlementController] Replay button clicked - restarting current game");
// ֤Ҫ
// 验证必要引用
if (bmm == null)
{
Debug.LogError("[SettlementController] BeatmapManager is null, cannot replay");
@@ -582,7 +593,7 @@ public class settlementController : MonoBehaviour
return;
}
// 浱ǰؿϢ¼أ
// 保存当前关卡信息用于重新加载
SongData songToReplay = bmm.assignedSongData;
int difficultyToReplay = bmm.assignedDifficulty;
@@ -594,33 +605,42 @@ public class settlementController : MonoBehaviour
Debug.Log($"[SettlementController] Reloading song: {songToReplay.songName}, difficulty: {difficultyToReplay}");
// ֹͣ
// 停止结算音乐
if (settlementAudioSource != null && settlementAudioSource.isPlaying)
{
settlementAudioSource.Stop();
}
// BeatmapManager Ϊһ׼
// 设置 BeatmapManager 为下一次加载准备
BeatmapManager.SetPendingSong(songToReplay, difficultyToReplay);
// ¼صǰGamePlay_gamePlay
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
// 重新加载当前场景
StartCoroutine(LoadSceneAsync(SceneManager.GetActiveScene().name));
}
private IEnumerator LoadSceneAsync(string sceneName)
{
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
while (!asyncLoad.isDone)
{
yield return null;
}
}
/// <summary>
/// ѡ
/// 返回选曲界面
/// </summary>
private void OnExitButtonClicked()
{
Debug.Log("[SettlementController] Exit button clicked - returning to song selection");
// ֹͣ
// 停止结算音乐
if (settlementAudioSource != null && settlementAudioSource.isPlaying)
{
settlementAudioSource.Stop();
}
// κδ
// 清除任何待处理的歌曲信息
BeatmapManager.pendingSongData = null;
BeatmapManager.pendingDifficulty = -1;
@@ -634,7 +654,7 @@ public class settlementController : MonoBehaviour
}
else
{
SceneManager.LoadScene("selectYourSongFirst");
StartCoroutine(LoadSceneAsync("selectYourSongFirst"));
}
}
@@ -643,7 +663,8 @@ public class settlementController : MonoBehaviour
{
if (gm == null || gm.blackMaskImage == null)
{
SceneManager.LoadScene(sceneName);
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
while (!asyncLoad.isDone) yield return null;
yield break;
}
@@ -673,12 +694,16 @@ public class settlementController : MonoBehaviour
blackMask.color = fc;
// load target scene
SceneManager.LoadScene(sceneName);
AsyncOperation asyncOp = SceneManager.LoadSceneAsync(sceneName);
while (!asyncOp.isDone)
{
yield return null;
}
}
/// <summary>
/// ݸɫѡ߷ֵĽɫ AllyHero_SO е ally_hero_HD_image ֵ mvp_hero_hd_image
/// ʹ ScoreManager per-track pm sums˵ AllyCombatant.currentScore
/// 根据各角色(按槽位选出最高分的角色),设置 AllyHero_SO 中的 ally_hero_HD_image 赋值给 mvp_hero_hd_image
/// 这里使用 ScoreManager per-track pm sums,如果没有则从场景中的 AllyCombatant.currentScore 读取
/// </summary>
private void SetMvpHeroImageFromTopScorer()
{
@@ -750,8 +775,11 @@ public class settlementController : MonoBehaviour
allyId = teamUIController.Instance.allySlotIds[topIndex];
if (allyId > 0)
{
var arr = Resources.LoadAll<AllyHero_SO>("");
foreach (var a in arr)
if (_cachedAllyHeroSOs == null || _cachedAllyHeroSOs.Length == 0)
{
_cachedAllyHeroSOs = Resources.LoadAll<AllyHero_SO>("");
}
foreach (var a in _cachedAllyHeroSOs)
{
if (a != null && a.ally_heroID == allyId) { heroSO = a; break; }
}
@@ -761,11 +789,14 @@ public class settlementController : MonoBehaviour
// final fallback: try direct Resources lookup by scanning all and picking first non-null for slot
if (heroSO == null)
{
var arr = Resources.LoadAll<AllyHero_SO>("");
if (arr != null && arr.Length > 0)
if (_cachedAllyHeroSOs == null || _cachedAllyHeroSOs.Length == 0)
{
_cachedAllyHeroSOs = Resources.LoadAll<AllyHero_SO>("");
}
if (_cachedAllyHeroSOs != null && _cachedAllyHeroSOs.Length > 0)
{
// attempt to match by name or just pick first
heroSO = arr[0] as AllyHero_SO;
heroSO = _cachedAllyHeroSOs[0] as AllyHero_SO;
}
}