长音符再修复 加入了关卡连接 优化了一些默认加载
This commit is contained in:
@@ -451,4 +451,77 @@ public class AnimationController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public API to prewarm particle instances. Call during loading/pause time to avoid hitch on first use.
|
||||
/// This will instantiate the configured scene particle sources (hit_particular_object / hit_ring_object)
|
||||
/// and briefly play their ParticleSystems to force any internal setup.
|
||||
/// </summary>
|
||||
public void PrewarmParticles(int perTemplate = 2)
|
||||
{
|
||||
// start coroutine to avoid blocking main thread with many instantiations
|
||||
StartCoroutine(PrewarmCoroutine(perTemplate));
|
||||
}
|
||||
|
||||
private IEnumerator PrewarmCoroutine(int perTemplate)
|
||||
{
|
||||
// List of templates to warm
|
||||
var templates = new List<GameObject>();
|
||||
if (hit_particular_object != null) templates.Add(hit_particular_object);
|
||||
if (hit_ring_object != null) templates.Add(hit_ring_object);
|
||||
|
||||
for (int i = 0; i < templates.Count; i++)
|
||||
{
|
||||
var prefab = templates[i];
|
||||
for (int j = 0; j < perTemplate; j++)
|
||||
{
|
||||
GameObject inst = null;
|
||||
try
|
||||
{
|
||||
inst = Instantiate(prefab, this.transform);
|
||||
inst.SetActive(true);
|
||||
var systems = inst.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (systems != null)
|
||||
{
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
try { ps.Play(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// wait a frame to allow any internal initialization to run
|
||||
yield return null;
|
||||
|
||||
// stop and destroy the instance to free memory - the warmup work is done
|
||||
if (inst != null)
|
||||
{
|
||||
var systems = inst.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (systems != null)
|
||||
{
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
|
||||
}
|
||||
}
|
||||
Destroy(inst);
|
||||
}
|
||||
|
||||
// small yield to spread work across frames
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public routine variant that callers can yield on to wait until prewarm completes.
|
||||
/// Use this when you need to ensure particle templates are fully instantiated and torn down
|
||||
/// before proceeding (to avoid hiccups at first real use).
|
||||
/// </summary>
|
||||
public IEnumerator PrewarmParticlesRoutine(int perTemplate = 2)
|
||||
{
|
||||
yield return StartCoroutine(PrewarmCoroutine(perTemplate));
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,39 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
public float fadeOutStartTime = 0.6f; // 生成后第几秒开始渐隐
|
||||
public float fadeOutDuration = 0.3f; // 渐隐动画持续时长
|
||||
|
||||
// Cache spawn transforms to avoid accessing destroyed GameObject references.
|
||||
private Transform redSpawn;
|
||||
private Transform greenSpawn;
|
||||
private Transform yellowSpawn;
|
||||
private Transform purpleSpawn;
|
||||
private Transform blueSpawn;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
CacheSpawnPoints();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// In case this object survives scene changes / references are assigned late.
|
||||
if (redSpawn == null && redEffect != null) CacheSpawnPoints();
|
||||
}
|
||||
|
||||
private void CacheSpawnPoints()
|
||||
{
|
||||
// Important: check Unity "fake null" before accessing .transform
|
||||
redSpawn = redEffect != null ? redEffect.transform : null;
|
||||
greenSpawn = greenEffect != null ? greenEffect.transform : null;
|
||||
yellowSpawn = yellowEffect != null ? yellowEffect.transform : null;
|
||||
purpleSpawn = purpleEffect != null ? purpleEffect.transform : null;
|
||||
blueSpawn = blueEffect != null ? blueEffect.transform : null;
|
||||
}
|
||||
|
||||
public void SpawnJudgePrefab(string color, string judgeResult)
|
||||
@@ -95,7 +124,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
// --- 2. 透明度逻辑 (基于秒数) ---
|
||||
if (sr != null)
|
||||
{
|
||||
float alpha = 1f;
|
||||
float alpha;
|
||||
|
||||
// 渐显阶段:当前时间 < fadeInTime
|
||||
if (elapsed < fadeInTime)
|
||||
@@ -138,19 +167,28 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
|
||||
private Transform GetSpawnPoint(string color)
|
||||
{
|
||||
if (string.IsNullOrEmpty(color)) return null;
|
||||
|
||||
// If references were destroyed (Unity fake null) try to rebuild cache once.
|
||||
if (redSpawn == null && greenSpawn == null && yellowSpawn == null && purpleSpawn == null && blueSpawn == null)
|
||||
{
|
||||
CacheSpawnPoints();
|
||||
}
|
||||
|
||||
switch (color.ToLower())
|
||||
{
|
||||
case "red": return redEffect?.transform;
|
||||
case "green": return greenEffect?.transform;
|
||||
case "yellow": return yellowEffect?.transform;
|
||||
case "purple": return purpleEffect?.transform;
|
||||
case "blue": return blueEffect?.transform;
|
||||
case "red": return redSpawn;
|
||||
case "green": return greenSpawn;
|
||||
case "yellow": return yellowSpawn;
|
||||
case "purple": return purpleSpawn;
|
||||
case "blue": return blueSpawn;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject GetJudgePrefab(string result)
|
||||
{
|
||||
if (string.IsNullOrEmpty(result)) return null;
|
||||
switch (result.ToLower())
|
||||
{
|
||||
case "perfect": return perfect_judge_prefab;
|
||||
|
||||
@@ -5,6 +5,37 @@ using System.Collections.Generic;
|
||||
|
||||
public class BeatmapManager : MonoBehaviour
|
||||
{
|
||||
// static holder to accept SongData passed from previous scene before this manager exists
|
||||
public static SongData pendingSongData = null;
|
||||
public static int pendingDifficulty = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Called by previous scene to hand over a SongData SO and difficulty to this manager after scene load.
|
||||
/// This stores the pair in static fields so BeatmapManager.Start can pick them up.
|
||||
/// </summary>
|
||||
public static void SetPendingSong(SongData song, int difficulty)
|
||||
{
|
||||
pendingSongData = song;
|
||||
pendingDifficulty = difficulty;
|
||||
Debug.LogWarning($"BeatmapManager.SetPendingSong called: song={(song==null?"NULL":song.songName)}, difficulty={difficulty}");
|
||||
}
|
||||
|
||||
[Header("Assigned SongData (set at runtime from previous scene or via Inspector)")]
|
||||
public SongData assignedSongData; // visible in Inspector so you can see the SO passed from previous scene
|
||||
[Header("Assigned difficulty (for inspector visibility)")]
|
||||
public int assignedDifficulty = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Public API to accept a SongData at runtime (can be called by other scripts after scene load)
|
||||
/// Also sets inspector-exposed fields so you can visually confirm in Editor during play.
|
||||
/// </summary>
|
||||
public void AcceptSongData(SongData song, int difficulty)
|
||||
{
|
||||
assignedSongData = song;
|
||||
assignedDifficulty = difficulty;
|
||||
Debug.LogWarning($"BeatmapManager.AcceptSongData: accepted song={(song==null?"NULL":song.songName)}, difficulty={difficulty}");
|
||||
}
|
||||
|
||||
public Beatmap beatmap; // 当前谱面数据
|
||||
|
||||
// 音符生成器引用
|
||||
@@ -45,9 +76,6 @@ public class BeatmapManager : MonoBehaviour
|
||||
public float inMultiplier = 2f;
|
||||
public float imMultiplier = 3f;
|
||||
|
||||
// 新增:基础生命值缩放因子(Base HP Unit Scale)
|
||||
// 用于将 Note 数量(例如 709)缩放到 40000 左右的范围。
|
||||
// 计算公式:NoteAmount * DifficultyMult * BaseScale ≈ TotalHP
|
||||
[Header("HP Calculation Settings")]
|
||||
[Tooltip("Scales the note amount to enemy HP. Example: NoteAmount * MaxMultiplier * BaseScale ≈ TotalHP")]
|
||||
public float baseHpUnitScale = 1f;
|
||||
@@ -205,9 +233,119 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:从 TextAsset 解析并根据 parseOnly 决定是否只是解析或直接加载
|
||||
public bool LoadBeatmapFromTextAsset(TextAsset chartAsset, bool parseOnly = false)
|
||||
{
|
||||
if (chartAsset == null)
|
||||
{
|
||||
Debug.LogWarning("LoadBeatmapFromTextAsset: chartAsset is null");
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrEmpty(chartAsset.text))
|
||||
{
|
||||
Debug.LogWarning("LoadBeatmapFromTextAsset: chartAsset.text is null or empty");
|
||||
return false;
|
||||
}
|
||||
if (parseOnly)
|
||||
{
|
||||
return ParseJsonOnly(chartAsset.text);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessJsonAndLoad(chartAsset.text);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:从 SongData 的 chart TextAsset 加载谱面
|
||||
public bool LoadBeatmapFromSongData(SongData song, int difficulty, bool parseOnly = false)
|
||||
{
|
||||
if (song == null)
|
||||
{
|
||||
Debug.LogWarning("LoadBeatmapFromSongData: song is null");
|
||||
return false;
|
||||
}
|
||||
TextAsset ta = song.GetChartFile(difficulty);
|
||||
if (ta == null)
|
||||
{
|
||||
Debug.LogWarning($"LoadBeatmapFromSongData: chart TextAsset for difficulty {difficulty} is null on song {song.songName}");
|
||||
return false;
|
||||
}
|
||||
Debug.LogWarning($"LoadBeatmapFromSongData: loading chart for song {song.songName}, difficulty {difficulty}, parseOnly={parseOnly}, chartSize={(ta.text != null ? ta.text.Length : 0)}");
|
||||
return LoadBeatmapFromTextAsset(ta, parseOnly);
|
||||
}
|
||||
|
||||
// 在 Start() 方法中初始化
|
||||
void Start()
|
||||
{
|
||||
// 被动接收上一个场景传递过来的 SongData(通过 BeatmapManager.pendingSongData)
|
||||
if (pendingSongData != null)
|
||||
{
|
||||
Debug.LogWarning($"BeatmapManager.Start: pendingSongData detected: {pendingSongData.songName}, difficulty={pendingDifficulty}");
|
||||
// 将 pending 显示到 Inspector 字段,便于检查
|
||||
assignedSongData = pendingSongData;
|
||||
assignedDifficulty = pendingDifficulty;
|
||||
|
||||
// 尝试解析(只解析,不立即生成音符)
|
||||
bool ok = LoadBeatmapFromSongData(assignedSongData, assignedDifficulty, true);
|
||||
if (!ok) Debug.LogWarning("BeatmapManager.Start: LoadBeatmapFromSongData failed");
|
||||
else
|
||||
{
|
||||
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>();
|
||||
if (gm != null && gm.musicSource != null)
|
||||
{
|
||||
if (assignedSongData != null && assignedSongData.audioFile != null)
|
||||
{
|
||||
gm.musicSource.clip = assignedSongData.audioFile;
|
||||
gm.musicSource.loop = false;
|
||||
Debug.LogWarning("Assigned SongData.audioFile to GameManager.musicSource.clip (from SO)");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(parsedMusicFile))
|
||||
{
|
||||
Debug.LogWarning($"Attempting Resources.Load for audio: {parsedMusicFile}");
|
||||
var ac = Resources.Load<AudioClip>(parsedMusicFile);
|
||||
if (ac != null)
|
||||
{
|
||||
gm.musicSource.clip = ac;
|
||||
gm.musicSource.loop = false;
|
||||
Debug.LogWarning("Assigned audio via Resources.Load(parsedMusicFile)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Resources.Load failed for '{parsedMusicFile}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("No audio available on SongData and parsedMusicFile empty");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("GameManager or its musicSource not found; audio not assigned");
|
||||
}
|
||||
|
||||
// Pause the system to show pause overlay (PauseManager will enable overlayRoot)
|
||||
var pauseMgr = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
|
||||
if (pauseMgr != null)
|
||||
{
|
||||
pauseMgr.Pause(true);
|
||||
Debug.LogWarning("System paused after loading chart and audio to allow player to start");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("PauseManager not found in scene; cannot pause system automatically");
|
||||
}
|
||||
}
|
||||
|
||||
// clear pending so it doesn't run again
|
||||
pendingSongData = null;
|
||||
pendingDifficulty = -1;
|
||||
}
|
||||
|
||||
// 如果处于测试模式,则跳过自动加载谱面
|
||||
if (GameConfig.testMode)
|
||||
{
|
||||
@@ -215,20 +353,20 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
// 加载默认谱面
|
||||
LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
|
||||
// 原先默认加载的调试谱面已注释,改为仅在需要时手动恢复
|
||||
// LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
|
||||
Debug.LogWarning("Default automatic demo beatmap load is commented out to prioritize SO-provided charts.");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前难度乘数
|
||||
private float GetCurrentDifficultyMultiplier()
|
||||
{
|
||||
// 假设 1=EZ, 2=HD, 3=IN, 4=IM
|
||||
if (parsedDifficulty == 1) return ezMultiplier;
|
||||
if (parsedDifficulty == 2) return hdMultiplier;
|
||||
if (parsedDifficulty == 3) return inMultiplier;
|
||||
if (parsedDifficulty == 4) return imMultiplier;
|
||||
return 1f; // 默认值
|
||||
return 1f;
|
||||
}
|
||||
|
||||
// 统一处理敌人 ID 提取、发送给 UI 以及 HP 计算的逻辑
|
||||
@@ -242,7 +380,6 @@ public class BeatmapManager : MonoBehaviour
|
||||
|
||||
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
|
||||
{
|
||||
// 1. 提取敌人 IDs
|
||||
List<int> enemyIds = new List<int>();
|
||||
foreach (var segment in parsedColorSegments)
|
||||
{
|
||||
@@ -256,43 +393,29 @@ public class BeatmapManager : MonoBehaviour
|
||||
enemyIds.Add(enemyId);
|
||||
}
|
||||
|
||||
// 2. 补齐 5 个槽位
|
||||
while (enemyIds.Count < 5)
|
||||
{
|
||||
enemyIds.Add(0);
|
||||
}
|
||||
while (enemyIds.Count < 5) enemyIds.Add(0);
|
||||
|
||||
// A. 传递 ID 列表给 uiController
|
||||
Debug.Log($"Assigning enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
|
||||
uiController.enemySlotIds = enemyIds;
|
||||
|
||||
// B. 让 UI 控制器加载 SO (此时 SO 的 HP 仍是默认值)
|
||||
uiController.PopulateEnemySOsFromIds();
|
||||
|
||||
// C. 计算并应用 HP 逻辑
|
||||
if (parsedNoteAmount > 0)
|
||||
{
|
||||
float currentMultiplier = GetCurrentDifficultyMultiplier();
|
||||
|
||||
// 计算总 HP:音符数 * 难度乘数 * 基础缩放
|
||||
float totalCalculatedHP = parsedNoteAmount * currentMultiplier * baseHpUnitScale;
|
||||
|
||||
// 计算活跃敌人数量(非0 ID)
|
||||
int activeEnemyCount = enemyIds.FindAll(id => id != 0).Count;
|
||||
if (activeEnemyCount == 0) activeEnemyCount = 1; // 防止除零
|
||||
if (activeEnemyCount == 0) activeEnemyCount = 1;
|
||||
|
||||
// 计算单个敌人 HP (总血量 / 敌人数量,均匀分配)
|
||||
int individualMaxHP = Mathf.RoundToInt(totalCalculatedHP / activeEnemyCount);
|
||||
|
||||
Debug.Log($"[BeatmapManager] HP Calc -> Notes: {parsedNoteAmount}, Multiplier: {currentMultiplier}, Total: {totalCalculatedHP:F0}, ActiveEnemies: {activeEnemyCount}, Per Enemy: {individualMaxHP}");
|
||||
|
||||
// D. 【关键调用】将计算出的 HP 回填到 teamUIController 中的 SO 中
|
||||
uiController.ApplyCalculatedEnemyHP(individualMaxHP);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有敌人,也要通知 UI 控制器清空列表
|
||||
Debug.Log("Parsed enemy list is empty, clearing UI.");
|
||||
uiController.enemySlotIds = new List<int> { 0, 0, 0, 0, 0 };
|
||||
uiController.PopulateEnemySOsFromIds();
|
||||
@@ -308,7 +431,6 @@ public class BeatmapManager : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse main beatmap
|
||||
Beatmap parsed = null;
|
||||
try
|
||||
{
|
||||
@@ -326,17 +448,11 @@ public class BeatmapManager : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse extras using BeatmapExtra which contains the requested fields and types
|
||||
BeatmapExtra extra = null;
|
||||
try
|
||||
{
|
||||
extra = JsonUtility.FromJson<BeatmapExtra>(json);
|
||||
}
|
||||
catch { extra = null; }
|
||||
try { extra = JsonUtility.FromJson<BeatmapExtra>(json); } catch { extra = null; }
|
||||
|
||||
if (extra != null)
|
||||
{
|
||||
// store into manager fields
|
||||
parsedTitle = extra.title;
|
||||
parsedComposer = extra.composer;
|
||||
parsedIllustrator = extra.illustrator;
|
||||
@@ -354,24 +470,16 @@ public class BeatmapManager : MonoBehaviour
|
||||
parsedNoteAmount = extra.noteAmount;
|
||||
globalDelaySeconds = extra.globalDelaySeconds;
|
||||
|
||||
// If JSON contains absolute paths for music/background, copy into parsed object for later use
|
||||
if (!string.IsNullOrEmpty(extra.musicFile)) parsed.musicFile = extra.musicFile;
|
||||
if (!string.IsNullOrEmpty(extra.backgroundFile)) parsed.backgroundFile = extra.backgroundFile;
|
||||
|
||||
Debug.Log($"Beatmap extras parsed: title={parsedTitle}, composer={parsedComposer}, duration={parsedDuration}, bpm={parsedBpm}, difficulty={parsedDifficulty}, noteAmount={parsedNoteAmount}, globalDelaySeconds={globalDelaySeconds}");
|
||||
|
||||
// New fields
|
||||
parsedEnemyListIsEmpty = extra.enemyList_isEmpty;
|
||||
if (!parsedEnemyListIsEmpty)
|
||||
{
|
||||
parsedColorSegments = extra.colorSegments;
|
||||
}
|
||||
if (!parsedEnemyListIsEmpty) parsedColorSegments = extra.colorSegments;
|
||||
parsedNoteStatistics = extra.noteStatistics;
|
||||
parsedTrackStates = extra.trackStates;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to populate from parsed Beatmap where possible
|
||||
parsedTitle = parsed.title;
|
||||
parsedComposer = parsed.composer;
|
||||
parsedIllustrator = parsed.illustrator;
|
||||
@@ -382,7 +490,6 @@ public class BeatmapManager : MonoBehaviour
|
||||
parsedMusicFile = parsed.musicFile;
|
||||
parsedBackgroundFile = parsed.backgroundFile;
|
||||
|
||||
// convert types
|
||||
parsedDuration = Mathf.RoundToInt(parsed.duration);
|
||||
parsedBpm = Mathf.RoundToInt(parsed.bpm);
|
||||
parsedDifficulty = parsed.difficulty;
|
||||
@@ -390,29 +497,17 @@ public class BeatmapManager : MonoBehaviour
|
||||
Debug.LogWarning("BeatmapExtra not found in JSON; populated limited fields from Beatmap.");
|
||||
}
|
||||
|
||||
// Calculate per-note and leftover scores
|
||||
CalculateNoteScores();
|
||||
|
||||
// NOTE: Do NOT modify parsed.notes here. globalDelaySeconds will be applied to audio playback instead.
|
||||
|
||||
// assign and hand off to spawner
|
||||
beatmap = parsed;
|
||||
Debug.Log("谱面已加载:" + beatmap.title);
|
||||
|
||||
if (noteSpawner != null)
|
||||
{
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("NoteSpawner is null in BeatmapManager.");
|
||||
}
|
||||
if (noteSpawner != null) noteSpawner.LoadBeatmap(beatmap);
|
||||
else Debug.LogError("NoteSpawner is null in BeatmapManager.");
|
||||
|
||||
// 调用统一的敌人处理逻辑(包含 HP 计算和设置)
|
||||
SetupEnemiesAndHP();
|
||||
}
|
||||
|
||||
// Calculate per-note and leftover scores based on parsedNoteAmount
|
||||
private void CalculateNoteScores()
|
||||
{
|
||||
if (parsedNoteAmount > 0)
|
||||
@@ -429,7 +524,6 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Helper class used to parse extra fields from JSON that Beatmap doesn't define
|
||||
[System.Serializable]
|
||||
private class BeatmapExtra
|
||||
{
|
||||
@@ -449,43 +543,18 @@ public class BeatmapManager : MonoBehaviour
|
||||
public int noteAmount;
|
||||
public float globalDelaySeconds;
|
||||
|
||||
// New fields
|
||||
public bool enemyList_isEmpty;
|
||||
public ColorSegment[] colorSegments;
|
||||
public NoteStatistic[] noteStatistics;
|
||||
public TrackStates trackStates;
|
||||
}
|
||||
|
||||
// New serializable classes for the extra fields
|
||||
[System.Serializable]
|
||||
public class ColorSegment
|
||||
{
|
||||
public string enemyID;
|
||||
public float percentage;
|
||||
}
|
||||
|
||||
public class ColorSegment { public string enemyID; public float percentage; }
|
||||
[System.Serializable]
|
||||
public class NoteStatistic
|
||||
{
|
||||
public string colorType;
|
||||
public int count;
|
||||
}
|
||||
|
||||
public class NoteStatistic { public string colorType; public int count; }
|
||||
[System.Serializable]
|
||||
public class TrackStates
|
||||
{
|
||||
public bool trackFading_active;
|
||||
public TrackState red;
|
||||
public TrackState green;
|
||||
public TrackState yellow;
|
||||
public TrackState purple;
|
||||
public TrackState blue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public class TrackState { public bool isOn; public float fadeTime; }
|
||||
}
|
||||
@@ -2,14 +2,15 @@ using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
public class GameManager : MonoBehaviour
|
||||
{
|
||||
public BeatmapManager beatmapManager; // 负责加载和管理谱面
|
||||
public NoteSpawner noteSpawner; // 音符生成器
|
||||
public AudioSource musicSource; // 音乐播放器
|
||||
public BeatmapManager beatmapManager; // ������غ�������
|
||||
public NoteSpawner noteSpawner; // ����������
|
||||
public AudioSource musicSource; // ���ֲ�����
|
||||
[Header("paths")]
|
||||
// 变量声明不再从静态变量中读取
|
||||
// �����������ٴӾ�̬�����ж�ȡ
|
||||
public string JSONPath;
|
||||
public string audioPath;
|
||||
public string bgPicPath;
|
||||
@@ -21,7 +22,15 @@ public class GameManager : MonoBehaviour
|
||||
[Header("Debug / Test")]
|
||||
public Text banflagTextUI;
|
||||
|
||||
[Header("UI Overlays")]
|
||||
public UnityEngine.UI.Image blackMaskImage; // assign in inspector: full-screen black overlay
|
||||
|
||||
[Header("Playback")]
|
||||
[Tooltip("Delay in seconds after unpausing (press Space) before starting audio and spawning. Configure in inspector.")]
|
||||
public float playbackStartDelay = 3f;
|
||||
|
||||
private bool pauseSubscribed = false;
|
||||
private bool musicWasPlayingBeforePause = false;
|
||||
|
||||
private void SubscribeToPauseManager()
|
||||
{
|
||||
@@ -60,37 +69,148 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
if (musicSource != null)
|
||||
{
|
||||
if (isPaused)
|
||||
try
|
||||
{
|
||||
musicSource.Pause();
|
||||
if (isPaused)
|
||||
{
|
||||
// remember whether music was playing and pause it to stop timeline advancing
|
||||
musicWasPlayingBeforePause = musicSource.isPlaying;
|
||||
if (musicWasPlayingBeforePause)
|
||||
{
|
||||
musicSource.Pause();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Do not change mute state here. External startup flow will unmute when appropriate.
|
||||
// resume if it was playing before pause
|
||||
if (musicWasPlayingBeforePause)
|
||||
{
|
||||
try { musicSource.UnPause(); } catch { }
|
||||
}
|
||||
|
||||
// reset flag
|
||||
musicWasPlayingBeforePause = false;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fade out the black mask image over duration seconds (unscaled), then disable its raycast target so UI passes through.
|
||||
/// </summary>
|
||||
public IEnumerator FadeOutBlackMask(float duration)
|
||||
{
|
||||
if (blackMaskImage == null) yield break;
|
||||
|
||||
// ensure image active
|
||||
if (!blackMaskImage.gameObject.activeSelf) blackMaskImage.gameObject.SetActive(true);
|
||||
|
||||
float t = 0f;
|
||||
Color c = blackMaskImage.color;
|
||||
float startA = c.a;
|
||||
while (t < duration)
|
||||
{
|
||||
t += Time.unscaledDeltaTime;
|
||||
float frac = Mathf.Clamp01(t / Mathf.Max(0.0001f, duration));
|
||||
c.a = Mathf.Lerp(startA, 0f, frac);
|
||||
blackMaskImage.color = c;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// ensure fully transparent
|
||||
c.a = 0f;
|
||||
blackMaskImage.color = c;
|
||||
|
||||
// disable raycast so underlying UI can receive input
|
||||
try { blackMaskImage.raycastTarget = false; } catch { }
|
||||
|
||||
// optionally deactivate the overlay to save draw calls
|
||||
blackMaskImage.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// Public helper to reliably unmute and start music playback, respecting delay.
|
||||
public void PlayMusicWithDelay(float delaySeconds)
|
||||
{
|
||||
if (musicSource == null) return;
|
||||
try
|
||||
{
|
||||
// Ensure unmuted
|
||||
musicSource.mute = false;
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
// If a delay is specified, stop current and schedule playback
|
||||
if (delaySeconds > 0f)
|
||||
{
|
||||
// Stop any currently playing to avoid overlapping schedules
|
||||
try { musicSource.Stop(); } catch { }
|
||||
musicSource.PlayDelayed(delaySeconds);
|
||||
Debug.Log($"GameManager.PlayMusicWithDelay: PlayDelayed({delaySeconds}) called");
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.UnPause();
|
||||
// If already playing but paused, try UnPause; otherwise Play
|
||||
if (musicSource.isPlaying)
|
||||
{
|
||||
// already playing - nothing to do
|
||||
Debug.Log("GameManager.PlayMusicWithDelay: musicSource already playing");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try UnPause first (in case it was paused), otherwise Play
|
||||
try { musicSource.UnPause(); Debug.Log("GameManager.PlayMusicWithDelay: UnPause called"); }
|
||||
catch { musicSource.Play(); Debug.Log("GameManager.PlayMusicWithDelay: Play called"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"PlayMusicWithDelay failed: {ex}");
|
||||
try { musicSource.Play(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Debug.Log("GameManager Start() 被调用");
|
||||
Debug.Log("GameManager Start() ������");
|
||||
|
||||
// 检查关键组件是否为空
|
||||
if (beatmapManager == null) Debug.LogError("beatmapManager 未赋值!");
|
||||
if (noteSpawner == null) Debug.LogError("noteSpawner 未赋值!");
|
||||
if (musicSource == null) Debug.LogError("musicSource 未赋值!");
|
||||
// ���ؼ�����Ƿ�Ϊ��
|
||||
if (beatmapManager == null) Debug.LogError("beatmapManager δ��ֵ��");
|
||||
if (noteSpawner == null) Debug.LogError("noteSpawner δ��ֵ��");
|
||||
if (musicSource == null) Debug.LogError("musicSource δ��ֵ��");
|
||||
|
||||
// 如果 pressStart 已经读取了 banflag 内容,则把它显示在 UI(如果分配了)
|
||||
// preload audio fully and start muted playback to warm up audio decoding
|
||||
if (musicSource != null && musicSource.clip != null)
|
||||
{
|
||||
musicSource.mute = true;
|
||||
musicSource.loop = false;
|
||||
musicSource.playOnAwake = false;
|
||||
try
|
||||
{
|
||||
musicSource.Play();
|
||||
Debug.Log("GameManager: warmed up audio playback (muted)");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning("GameManager: failed to warm up audio: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// ��� pressStart �Ѿ���ȡ�� banflag ���ݣ��������ʾ�� UI����������ˣ�
|
||||
if (banflagTextUI != null && !string.IsNullOrEmpty(pressStart.banflagContent))
|
||||
{
|
||||
banflagTextUI.text = pressStart.banflagContent;
|
||||
Debug.Log("Displayed banflag content from pressStart into banflagTextUI.");
|
||||
}
|
||||
|
||||
// 在 test mode 下使用外部路径预加载资源并暂停系统,等待按空格继续
|
||||
// �� test mode ��ʹ���ⲿ·��Ԥ������Դ����ͣϵͳ���ȴ����ո����
|
||||
if (GameConfig.testMode)
|
||||
{
|
||||
// 延迟赋值:在 Start() 中获取静态路径,此时 pressStart.cs 已经运行并更新了它们
|
||||
// �ӳٸ�ֵ���� Start() �л�ȡ��̬·������ʱ pressStart.cs �Ѿ����в�����������
|
||||
JSONPath = pressStart.testmode_beatmapJSON_path;
|
||||
audioPath = pressStart.testmode_audioFile_path;
|
||||
bgPicPath = pressStart.testmode_bgPicFile_path;
|
||||
@@ -110,6 +230,22 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
StartCoroutine(HandleNormalModeStartup());
|
||||
}
|
||||
|
||||
// Ensure black mask blocks input and is fully opaque at scene start (will fade out after loading).
|
||||
if (blackMaskImage != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Color cc = blackMaskImage.color;
|
||||
cc.a = 1f;
|
||||
blackMaskImage.color = cc;
|
||||
blackMaskImage.gameObject.SetActive(true);
|
||||
blackMaskImage.raycastTarget = true;
|
||||
// start automatic fade from alpha=1 to 0 over 1 second after Start
|
||||
StartCoroutine(FadeOutBlackMask(1f));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator HandleTestModeStartup()
|
||||
@@ -123,42 +259,92 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
// 注意:这里仍然使用过时的 WWW,建议替换为 UnityWebRequest
|
||||
// Use UnityWebRequestMultimedia instead of WWW which is deprecated and can be unreliable for file:// audio
|
||||
string url = "file://" + audioPath;
|
||||
using (var www = new WWW(url))
|
||||
|
||||
// determine audio type from extension for best compatibility
|
||||
AudioType audioType = AudioType.UNKNOWN;
|
||||
string ext = Path.GetExtension(audioPath)?.ToLowerInvariant();
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
{
|
||||
yield return www;
|
||||
if (!string.IsNullOrEmpty(www.error))
|
||||
switch (ext)
|
||||
{
|
||||
Debug.LogError($"加载测试模式音频失败: {www.error}");
|
||||
case ".wav": audioType = AudioType.WAV; break;
|
||||
case ".ogg": audioType = AudioType.OGGVORBIS; break;
|
||||
case ".mp3": audioType = AudioType.MPEG; break;
|
||||
case ".aif":
|
||||
case ".aiff": audioType = AudioType.AIFF; break;
|
||||
default: audioType = AudioType.UNKNOWN; break;
|
||||
}
|
||||
}
|
||||
|
||||
using (var uwr = UnityWebRequestMultimedia.GetAudioClip(url, audioType))
|
||||
{
|
||||
yield return uwr.SendWebRequest();
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
if (uwr.result == UnityWebRequest.Result.ConnectionError || uwr.result == UnityWebRequest.Result.ProtocolError)
|
||||
#else
|
||||
if (uwr.isNetworkError || uwr.isHttpError)
|
||||
#endif
|
||||
{
|
||||
Debug.LogError($"TestMode audio load failed: {uwr.error}");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
AudioClip clip = DownloadHandlerAudioClip.GetContent(uwr);
|
||||
if (clip == null)
|
||||
{
|
||||
AudioClip clip = www.GetAudioClip(false, false);
|
||||
if (clip == null)
|
||||
Debug.LogError("Failed to obtain AudioClip from downloaded data");
|
||||
}
|
||||
else
|
||||
{
|
||||
// wait for audio data to be loaded if necessary
|
||||
float waitStart = Time.realtimeSinceStartup;
|
||||
while (clip.loadState == AudioDataLoadState.Loading && Time.realtimeSinceStartup - waitStart < 5f)
|
||||
{
|
||||
Debug.LogError("从文件获取 AudioClip 失败");
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (clip.loadState != AudioDataLoadState.Loaded)
|
||||
{
|
||||
Debug.LogWarning($"AudioClip loadState is {clip.loadState}. It may still stream or be incomplete.");
|
||||
}
|
||||
|
||||
if (musicSource == null)
|
||||
{
|
||||
Debug.LogError("musicSource is null - cannot assign TestMode audio");
|
||||
}
|
||||
else
|
||||
{
|
||||
clip.name = Path.GetFileName(audioPath);
|
||||
musicSource.clip = clip;
|
||||
musicSource.loop = false;
|
||||
musicSource.playOnAwake = false;
|
||||
// Warm up decoding by playing briefly muted; avoid try/catch around yield (not allowed).
|
||||
musicSource.mute = true;
|
||||
musicSource.Play();
|
||||
// allow a few frames for audio system to start
|
||||
yield return null;
|
||||
musicSource.Stop();
|
||||
|
||||
Debug.Log("TestMode audio loaded into AudioSource");
|
||||
|
||||
// After loading audio, enable overlay and pause via PauseManager
|
||||
var pm = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
|
||||
if (pm != null)
|
||||
{
|
||||
pm.Pause(true);
|
||||
Debug.Log("PauseManager.Pause(true) invoked after TestMode audio load");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"将音频设置到 AudioSource 时出错: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("TestMode audioPath 未配置。");
|
||||
Debug.LogWarning("TestMode audioPath not provided");
|
||||
}
|
||||
|
||||
// Preload background image into sprite renderer
|
||||
@@ -179,7 +365,7 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"读取背景图片文件出错: {ex}");
|
||||
Debug.LogError($"��ȡ����ͼƬ�ļ�����: {ex}");
|
||||
}
|
||||
|
||||
if (imgBytes != null)
|
||||
@@ -190,29 +376,29 @@ public class GameManager : MonoBehaviour
|
||||
Sprite s = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
|
||||
backgroundRenderer.sprite = s;
|
||||
|
||||
// 【修改点:设置 SpriteRenderer 透明度为 255 (1.0f)】
|
||||
// ���ĵ㣺���� SpriteRenderer ����Ϊ 255 (1.0f)��
|
||||
Color color = backgroundRenderer.color;
|
||||
color.a = 1.0f;
|
||||
backgroundRenderer.color = color;
|
||||
|
||||
// 保持比例不变的最大适应由场景布局决定,记录 assignment
|
||||
// ���ֱ�������������Ӧ�ɳ������־�������¼ assignment
|
||||
Debug.Log("Background sprite assigned (ensure your SpriteRenderer size fits scene)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"无法从字节创建 Texture2D: {bgPicPath}");
|
||||
Debug.LogError($"�����ֽڴ��� Texture2D: {bgPicPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("TestMode bgPicPath 未配置。");
|
||||
Debug.LogWarning("TestMode bgPicPath δ���á�");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("backgroundRenderer 未配置,跳过背景加载。");
|
||||
Debug.LogWarning("backgroundRenderer δ���ã������������ء�");
|
||||
}
|
||||
|
||||
// Read JSON from JSONPath and parse via BeatmapManager.ParseJsonOnly
|
||||
@@ -231,14 +417,14 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"读取 TestMode JSON 文件出错: {ex}");
|
||||
Debug.LogError($"��ȡ TestMode JSON �ļ�����: {ex}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
bool parsed = beatmapManager.ParseJsonOnly(jsonContent);
|
||||
if (!parsed)
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly 失败,停止启动测试模式。");
|
||||
Debug.LogError("ParseJsonOnly ʧ�ܣ�ֹͣ��������ģʽ��");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -247,7 +433,7 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("TestMode JSONPath 未配置。");
|
||||
Debug.LogError("TestMode JSONPath δ���á�");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -265,6 +451,8 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
// Resume using PauseManager
|
||||
PauseManager.Instance?.Pause(false);
|
||||
// small buffer to avoid hitching immediately after unpause: wait configured realtime delay
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0f, playbackStartDelay));
|
||||
|
||||
// Start spawning using the parsed beatmap
|
||||
if (beatmapManager.beatmap != null)
|
||||
@@ -273,40 +461,93 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("启动播放失败:beatmap 未解析或为空");
|
||||
Debug.LogError("��������ʧ�ܣ�beatmap δ������Ϊ��");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// If audio clip was loaded into musicSource.clip, play now with delay from parsed globalDelaySeconds
|
||||
float delaySeconds = beatmapManager != null ? beatmapManager.globalDelaySeconds : 0f;
|
||||
if (musicSource.clip != null)
|
||||
{
|
||||
if (delaySeconds > 0f)
|
||||
{
|
||||
musicSource.PlayDelayed(delaySeconds);
|
||||
Debug.Log($"Scheduled test-mode music to play after {delaySeconds} seconds.");
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.Play();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("TestMode: no audio clip loaded into AudioSource; skipping play.");
|
||||
}
|
||||
PlayMusicWithDelay(delaySeconds);
|
||||
|
||||
UpdateStatusOnConsole("Playback started");
|
||||
}
|
||||
|
||||
private IEnumerator HandleNormalModeStartup()
|
||||
{
|
||||
// 加载谱面但不生成音符
|
||||
// If a Beatmap was already provided by previous scene (pendingSongData path), skip loading default JSON
|
||||
if (beatmapManager != null && beatmapManager.beatmap != null)
|
||||
{
|
||||
Debug.Log("HandleNormalModeStartup: existing beatmap found on BeatmapManager; skipping default JSON load.");
|
||||
|
||||
// Pause the system until Space pressed
|
||||
SubscribeToPauseManager();
|
||||
PauseManager.Instance?.Pause(true);
|
||||
UpdateStatusOnConsole("Paused: press Space to start playback");
|
||||
|
||||
// Wait for space
|
||||
while (!Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Resume
|
||||
PauseManager.Instance?.Pause(false);
|
||||
// small buffer to avoid hitching immediately after unpause: wait configured realtime delay
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0f, playbackStartDelay));
|
||||
|
||||
// Start spawning using the existing parsed beatmap
|
||||
if (beatmapManager.beatmap != null)
|
||||
{
|
||||
noteSpawner.LoadBeatmap(beatmapManager.beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("HandleNormalModeStartup: beatmap was expected but is null");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Play background music if assigned
|
||||
if (musicSource != null && musicSource.clip != null)
|
||||
{
|
||||
float delay = beatmapManager != null ? beatmapManager.globalDelaySeconds : 0f;
|
||||
PlayMusicWithDelay(delay);
|
||||
}
|
||||
else
|
||||
{
|
||||
// try load via parsedMusicFile if available
|
||||
if (beatmapManager != null && !string.IsNullOrEmpty(beatmapManager.parsedMusicFile))
|
||||
{
|
||||
AudioClip musicClip = Resources.Load<AudioClip>(beatmapManager.parsedMusicFile);
|
||||
if (musicClip != null)
|
||||
{
|
||||
musicSource.clip = musicClip;
|
||||
// ensure we don't accidentally autoplay when assigning clips
|
||||
musicSource.playOnAwake = false;
|
||||
musicSource.Stop();
|
||||
float delay = beatmapManager.globalDelaySeconds;
|
||||
PlayMusicWithDelay(delay);
|
||||
|
||||
// ensure overlay shows and pause logic runs after loading the clip
|
||||
var pm2 = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
|
||||
if (pm2 != null)
|
||||
{
|
||||
pm2.Pause(true);
|
||||
Debug.Log("PauseManager.Pause(true) invoked after assigning musicClip in normal startup");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateStatusOnConsole("Playback started");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// ԭ�����̣��������浫����������
|
||||
string beatmapFilePath = Path.Combine(Application.streamingAssetsPath, "Emilia_demo.json");
|
||||
|
||||
if (!File.Exists(beatmapFilePath))
|
||||
{
|
||||
Debug.LogError($"谱面文件不存在: {beatmapFilePath}");
|
||||
Debug.LogError($"�����������: {beatmapFilePath}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -314,61 +555,66 @@ public class GameManager : MonoBehaviour
|
||||
bool parsed = beatmapManager.ParseJsonOnly(json);
|
||||
if (!parsed)
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly 失败");
|
||||
Debug.LogError("ParseJsonOnly ʧ��");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 暂停系统
|
||||
// ��ͣϵͳ
|
||||
SubscribeToPauseManager();
|
||||
PauseManager.Instance?.Pause(true);
|
||||
UpdateStatusOnConsole("Paused: press Space to start playback");
|
||||
|
||||
// 等待空格
|
||||
// �ȴ��ո�
|
||||
while (!Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 恢复
|
||||
// �ָ�
|
||||
PauseManager.Instance?.Pause(false);
|
||||
// small buffer to avoid hitching immediately after unpause: wait configured realtime delay
|
||||
yield return new WaitForSecondsRealtime(Mathf.Max(0f, playbackStartDelay));
|
||||
|
||||
// 生成音符
|
||||
// ��������
|
||||
if (beatmapManager.beatmap != null)
|
||||
{
|
||||
noteSpawner.LoadBeatmap(beatmapManager.beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("beatmap 未解析");
|
||||
Debug.LogError("beatmap ���");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 播放背景音乐(延迟播放由 beatmapManager.globalDelaySeconds 决定)
|
||||
// ���ű������֣��ӳٲ����� beatmapManager.globalDelaySeconds ������
|
||||
if (!string.IsNullOrEmpty(beatmapManager.parsedMusicFile))
|
||||
{
|
||||
AudioClip musicClip = Resources.Load<AudioClip>(beatmapManager.parsedMusicFile);
|
||||
if (musicClip == null)
|
||||
{
|
||||
Debug.LogError($"音乐文件加载失败: {beatmapManager.parsedMusicFile}");
|
||||
Debug.LogError($"�����ļ�����ʧ��: {beatmapManager.parsedMusicFile}");
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.clip = musicClip;
|
||||
// ensure we don't accidentally autoplay when assigning clips
|
||||
musicSource.playOnAwake = false;
|
||||
musicSource.Stop();
|
||||
float delay = beatmapManager.globalDelaySeconds;
|
||||
if (delay > 0f)
|
||||
PlayMusicWithDelay(delay);
|
||||
|
||||
// After assigning clip, enable overlay and pause
|
||||
var pm3 = PauseManager.Instance ?? FindObjectOfType<PauseManager>();
|
||||
if (pm3 != null)
|
||||
{
|
||||
musicSource.PlayDelayed(delay);
|
||||
Debug.Log($"Scheduled music to play after {delay} seconds.");
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.Play();
|
||||
pm3.Pause(true);
|
||||
Debug.Log("PauseManager.Pause(true) invoked after assigning parsedMusicFile clip");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("谱面中 musicFile 为空!");
|
||||
Debug.LogError("������ musicFile Ϊ�գ�");
|
||||
}
|
||||
|
||||
UpdateStatusOnConsole("Playback started");
|
||||
|
||||
@@ -18,6 +18,7 @@ public class HoldNote : BaseNote
|
||||
private bool hasEnteredLine = false;
|
||||
|
||||
private Coroutine autoReturnCoroutine;
|
||||
private Coroutine scheduledReturnCoroutine;
|
||||
private HoldNoteController controller;
|
||||
private AnimationController anim;
|
||||
|
||||
@@ -32,14 +33,15 @@ public class HoldNote : BaseNote
|
||||
public KeyCode key;
|
||||
public string type; // "start", "middle", or "end"
|
||||
|
||||
private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定
|
||||
private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开
|
||||
// Flags and runtime state
|
||||
private bool isJudged = false; // whether this segment has been judged (true = evaluation done)
|
||||
private bool isHoldActive = false; // whether the hold is currently active (player is holding)
|
||||
|
||||
// 新增变量
|
||||
private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定)
|
||||
// whether this middle segment was held from the start (used for logic checks)
|
||||
private bool hasBeenHeldFromStart = false;
|
||||
|
||||
[Header("判定区间配置")]
|
||||
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
|
||||
[Header("Judge configuration")]
|
||||
public NoteJudgeConfig judgeConfig; // judge windows configuration
|
||||
private NoteData noteData;
|
||||
|
||||
// store original transform scale so we can restore on reset
|
||||
@@ -48,13 +50,33 @@ public class HoldNote : BaseNote
|
||||
private Transform visualTransform = null;
|
||||
private Vector3 originalVisualLocalScale = Vector3.one;
|
||||
|
||||
[Header("判定调整(仅对长音符生效)")]
|
||||
[Header("Hold judgement adjustments")]
|
||||
[Tooltip("Multiplier applied to judgement windows for hold notes. >1 makes hold judgement more lenient (wider windows).")]
|
||||
[Range(1f, 2f)]
|
||||
public float holdWindowMultiplier = 1.3f;
|
||||
|
||||
public float visualSpeedMultiplier = 1f; // injected from NoteSpawner to adapt windows when visual speed changes
|
||||
|
||||
// track when the hold actually started (real-time) so we can compute held fraction
|
||||
private float holdStartTime = -1f;
|
||||
|
||||
// Cache allies per track to avoid GameObject.Find on first judgement.
|
||||
private static AllyCombatant[] allyCache;
|
||||
|
||||
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()
|
||||
{
|
||||
controller = GetComponent<HoldNoteController>();
|
||||
@@ -71,7 +93,7 @@ public class HoldNote : BaseNote
|
||||
|
||||
if (anim == null)
|
||||
{
|
||||
Debug.LogWarning("HoldNote: No AnimationController found (Global/local/scene). Particle effects will be unavailable.");
|
||||
if (GameConfig.verboseLogs) Debug.LogWarning("HoldNote: No AnimationController found (Global/local/scene). Particle effects will be unavailable.");
|
||||
}
|
||||
|
||||
if (controller != null)
|
||||
@@ -166,48 +188,90 @@ public class HoldNote : BaseNote
|
||||
this.hasEnteredLine = false;
|
||||
this.hasReleased = false;
|
||||
|
||||
// 判定区间配置检查
|
||||
if (judgeConfig == null)
|
||||
Debug.LogError($"HoldNoteJudgeConfig is null! 判定区间配置未传入!track={trackIndex}");
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.LogError($"HoldNoteJudgeConfig is null! judgeConfig not assigned. track={trackIndex}");
|
||||
}
|
||||
else
|
||||
Debug.Log($"HoldNoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"HoldNoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
|
||||
}
|
||||
|
||||
this.noteData = noteData;
|
||||
|
||||
// Debug info to help investigate end note visibility
|
||||
Debug.Log($"[HoldNote.Setup] id={noteID} segment={segment} type={type} hitTime={hitTime:F2} scheduledEnd={scheduledEndTime:F2} color={color} track={trackIndex}");
|
||||
if (GameConfig.verboseLogs)
|
||||
{
|
||||
Debug.Log($"[HoldNote.Setup] id={noteID} segment={segment} type={type} hitTime={hitTime:F2} scheduledEnd={scheduledEndTime:F2} color={color} track={trackIndex}");
|
||||
Debug.Log($"[HoldNote.Setup] Controller present={(controller != null)}, AnimationController global={(AnimationController.Global != null)}, localAnim={(anim != null)}");
|
||||
}
|
||||
|
||||
// 在 Setup 时注册初始状态
|
||||
// Register initial judge state in JudgeManager
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, false);
|
||||
if (segment == NoteSegment.Start)
|
||||
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定
|
||||
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // initially mark start as not judged
|
||||
|
||||
if (segment == NoteSegment.End)
|
||||
JudgeManager.Instance?.RegisterScheduledEndTime(noteID, scheduledEndTime);
|
||||
|
||||
controller?.SetSpeed(speed);
|
||||
controller?.SetSegmentDelay(delay);
|
||||
|
||||
// IMPORTANT: configure timing using the same timebase as hitTime.
|
||||
// NoteSpawner/Setup passes 'time' as the base realtime hit point already (startTime + note.time + globalHitDelay).
|
||||
// We need travelTime so the segment starts moving at (hitTime - travelTime) rather than (Time.time + delay).
|
||||
float travelTime = 0f;
|
||||
if (speed > 0.0001f)
|
||||
{
|
||||
// NoteSpawner.CalculateSpeed uses: speed = 10.75f / travelTime, so travelTime = 10.75f / speed
|
||||
travelTime = 10.75f / speed;
|
||||
}
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
controller.ConfigureTiming(time, delay, travelTime);
|
||||
|
||||
// If the activation time is already in the past at the moment of Setup,
|
||||
// the segment should have already traveled some distance. In that case
|
||||
// snap its position forward so it appears at the correct location.
|
||||
// Do this only when activation happened before now to avoid snapping
|
||||
// freshly spawned segments that haven't started moving yet.
|
||||
if (controller.ActivationTime < Time.time - 0.0001f)
|
||||
{
|
||||
// Use current transform.position as the spawn origin (NoteSpawner sets this before Setup).
|
||||
CalibratePosition(transform.position, 0f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback
|
||||
controller?.SetSegmentDelay(delay);
|
||||
}
|
||||
|
||||
// Removed: CalibratePosition for delay==0f was causing Start segments to snap to incorrect positions
|
||||
// if they started moving immediately after Setup (due to activationTime <= Time.time).
|
||||
// Start segments should begin at spawnPoint and move from there naturally.
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 如果当前片段已经判定过,直接返回
|
||||
// If already judged, skip heavy logic
|
||||
if (isJudged) return;
|
||||
|
||||
// 重新启动长按粒子协程,如果长按状态恢复且协程未运行
|
||||
// If start was judged and player is holding key, start hold effects
|
||||
if (!isHoldActive && JudgeManager.Instance.IsStartJudged(noteID) && !JudgeManager.Instance.HasNoteReleased(noteID) && Input.GetKey(keyToPress))
|
||||
{
|
||||
isHoldActive = true;
|
||||
AnimationController.Global?.StartHoldParticles(noteColor);
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Start particles started for {noteID} color={noteColor} at time={Time.time:F3}");
|
||||
}
|
||||
|
||||
// 自动 Miss 判定:当音符经过判定线且未被判定时
|
||||
// Auto-miss checks when inside judge line
|
||||
if (hasEnteredLine && !isJudged)
|
||||
{
|
||||
float missRangeScaled = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
|
||||
if(segment == NoteSegment.Start && Time.time > hitTime + missRangeScaled)
|
||||
if (segment == NoteSegment.Start && Time.time > hitTime + missRangeScaled)
|
||||
{
|
||||
Debug.Log($"[HoldNote] START段超时自动Miss(自动): {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START auto-Miss: {noteColor}");
|
||||
|
||||
// register as missed
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, false);
|
||||
@@ -216,64 +280,63 @@ public class HoldNote : BaseNote
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
|
||||
// Show judgement prefab for auto-miss as well (consistent with other notes)
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
|
||||
|
||||
isJudged = true;
|
||||
ReturnToPool();
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
|
||||
else if (segment == NoteSegment.End && Time.time > scheduledEndTime + missRangeScaled)
|
||||
{
|
||||
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END auto-Miss: {noteColor}");
|
||||
HandleEnd(true);
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// **通用长按状态更新:**
|
||||
// Handle key release while holding
|
||||
if (isHoldActive && Input.GetKeyUp(keyToPress))
|
||||
{
|
||||
isHoldActive = false;
|
||||
AnimationController.Global?.StopHoldParticles();
|
||||
Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] KeyUp {keyToPress} detected. NoteID: {noteID}, Segment: {segment}, Type: {type}");
|
||||
|
||||
// 当玩家松开按键时,向 JudgeManager 注册该长音符已被释放,确保尾部能正确检查释放状态
|
||||
if (!hasReleased)
|
||||
{
|
||||
hasReleased = true;
|
||||
releaseTime = Time.time;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
Debug.Log($"[HoldNote] RegisterNoteReleased on KeyUp. NoteID: {noteID}, releaseTime={releaseTime:F2}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] RegisterNoteReleased on KeyUp. NoteID: {noteID}, releaseTime={releaseTime:F2}");
|
||||
|
||||
EvaluateHoldEnd(releaseTime, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Start段判定
|
||||
// allow early keypress within judgement window even before collider entered (helps slow visual speeds)
|
||||
// Start judgement input handling
|
||||
if (segment == NoteSegment.Start)
|
||||
{
|
||||
if (Input.GetKeyDown(keyToPress))
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] KeyDown detected for {noteID} key={keyToPress} time={Time.time:F3} hitTime={hitTime:F3} hasEnteredLine={hasEnteredLine}");
|
||||
float pressTimeLocal = Time.time;
|
||||
float maxWindow = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
|
||||
if (Mathf.Abs(pressTimeLocal - hitTime) <= maxWindow)
|
||||
{
|
||||
HandleStart(false);
|
||||
isJudged = true; // mark judged to avoid duplicates
|
||||
}
|
||||
else
|
||||
{
|
||||
// key pressed but outside hold window -> ignore (do not mark judged)
|
||||
// do not mark isJudged here; Start remains active for hold
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (segment == NoteSegment.Start && hasEnteredLine)
|
||||
{
|
||||
// legacy fallback (shouldn't normally hit because above handles Start)
|
||||
// legacy fallback
|
||||
if (Input.GetKeyDown(keyToPress))
|
||||
{
|
||||
HandleStart(false);
|
||||
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
|
||||
else if (segment == NoteSegment.Middle
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID)
|
||||
&& isHoldActive
|
||||
@@ -283,16 +346,14 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (hasEnteredLine)
|
||||
{
|
||||
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Middle passed: {noteColor}");
|
||||
PlayHitAnimation();
|
||||
// 把通过事件上报给 JudgeManager(记录中段通过)
|
||||
JudgeManager.Instance?.RegisterMiddlePassed(noteID);
|
||||
ReturnToPool();
|
||||
ScheduleReturnToPool(0.2f);
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
|
||||
else if (segment == NoteSegment.End
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID)
|
||||
&& !JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
@@ -314,7 +375,7 @@ public class HoldNote : BaseNote
|
||||
if (segment == NoteSegment.Middle)
|
||||
{
|
||||
hasBeenHeldFromStart = Input.GetKey(keyToPress);
|
||||
Debug.Log($"[HoldNote] Middle段进入判定线: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Middle enter: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
|
||||
|
||||
if (autoReturnCoroutine != null)
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
@@ -324,13 +385,12 @@ public class HoldNote : BaseNote
|
||||
if (autoReturnCoroutine != null)
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
|
||||
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
|
||||
// If already released before reaching the line, handle end judgement immediately
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isJudged)
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] End entered but note already released: {noteColor}");
|
||||
HandleEnd();
|
||||
isJudged = true;
|
||||
// ReturnToPool handled inside HandleEnd
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,17 +406,16 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (!JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
|
||||
// 确保未判定的 Start 段在离开判定线时登记为 Miss
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Start left judge zone without being judged -> Miss: {noteColor}");
|
||||
HandleStart(true);
|
||||
isJudged = true;
|
||||
ReturnToPool();
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
}
|
||||
else if (segment == NoteSegment.Middle)
|
||||
{
|
||||
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
|
||||
ReturnToPool();
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Middle left judge zone: {noteColor}");
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
else if (segment == NoteSegment.End)
|
||||
{
|
||||
@@ -366,44 +425,36 @@ public class HoldNote : BaseNote
|
||||
autoReturnCoroutine = null;
|
||||
}
|
||||
|
||||
// 在尾部离开判定线时,按如下规则处理:
|
||||
// - 如果头部未判定:补偿 Miss 并回收
|
||||
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
|
||||
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
|
||||
if (!isJudged)
|
||||
{
|
||||
if (!JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] End left judge zone and start was not judged -> Miss: {noteColor}");
|
||||
if (!JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
HandleEnd(true);
|
||||
ReturnToPool();
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] End left judge zone and was released -> HandleEnd: {noteColor}");
|
||||
HandleEnd();
|
||||
isJudged = true;
|
||||
ReturnToPool();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定,等待玩家松手或超时处理
|
||||
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReturnToPool();
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DelayedAutoReturnCheck()
|
||||
{
|
||||
// yield one frame to ensure any state changes are settled
|
||||
yield return null;
|
||||
|
||||
if (segment == NoteSegment.End)
|
||||
@@ -411,35 +462,72 @@ public class HoldNote : BaseNote
|
||||
float timeToWait = Mathf.Max(0f, scheduledEndTime - Time.time);
|
||||
if (timeToWait > 0f)
|
||||
{
|
||||
yield return new WaitForSeconds(timeToWait);
|
||||
// use scaled time here so pause affects this wait
|
||||
float target = Time.time + timeToWait;
|
||||
while (Time.time < target)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gameObject.activeSelf)
|
||||
// If the object was deactivated while waiting, bail out
|
||||
if (!gameObject.activeInHierarchy)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
// wait a short buffer after arriving to judge line before returning to pool
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
// Schedules a return-to-pool after a short delay. Cancels any previously scheduled return.
|
||||
private void ScheduleReturnToPool(float delay)
|
||||
{
|
||||
if (scheduledReturnCoroutine != null)
|
||||
{
|
||||
StopCoroutine(scheduledReturnCoroutine);
|
||||
scheduledReturnCoroutine = null;
|
||||
}
|
||||
|
||||
if (!gameObject.activeInHierarchy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
scheduledReturnCoroutine = StartCoroutine(DelayedReturnToPool(delay));
|
||||
}
|
||||
|
||||
private IEnumerator DelayedReturnToPool(float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
ReturnToPool();
|
||||
}
|
||||
scheduledReturnCoroutine = null;
|
||||
}
|
||||
|
||||
private void HandleStart(bool forceMiss = false)
|
||||
{
|
||||
if (!JudgeManager.Instance.TryResolveStart(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] START 已被判定,跳过: {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START try-resolve failed: {noteColor}");
|
||||
return;
|
||||
}
|
||||
|
||||
// If forced miss (e.g. leaving judge zone without press), register miss immediately
|
||||
if (forceMiss)
|
||||
{
|
||||
Debug.Log($"[HoldNote] START 强制 Miss (未按下): {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START forced Miss: {noteColor}");
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, false);
|
||||
isHoldActive = false;
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
|
||||
// Also show judgement prefab for forced miss on start
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -456,32 +544,43 @@ public class HoldNote : BaseNote
|
||||
if (offset <= pRange)
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, true);
|
||||
isHoldActive = true;
|
||||
PlayHitAnimation();
|
||||
AnimationController.Global?.StartHoldParticles(noteColor);
|
||||
|
||||
// record when the hold started so we can compute held fraction later
|
||||
holdStartTime = pressTime;
|
||||
|
||||
// store into pool for use on end
|
||||
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
|
||||
}
|
||||
else if (offset <= gRange)
|
||||
{
|
||||
result = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, true);
|
||||
isHoldActive = true;
|
||||
PlayHitAnimation();
|
||||
AnimationController.Global?.StartHoldParticles(noteColor);
|
||||
|
||||
holdStartTime = pressTime;
|
||||
|
||||
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
|
||||
}
|
||||
else if (offset <= gdRange)
|
||||
{
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, true);
|
||||
isHoldActive = true;
|
||||
PlayHitAnimation();
|
||||
AnimationController.Global?.StartHoldParticles(noteColor);
|
||||
|
||||
holdStartTime = pressTime;
|
||||
|
||||
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -492,7 +591,7 @@ public class HoldNote : BaseNote
|
||||
try
|
||||
{
|
||||
bool triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false;
|
||||
Debug.Log($"[HoldNote] START Miss NotifyNoteHit fired for slot {trackIndex}: {triggered}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START Miss NotifyNoteHit fired for slot {trackIndex}: {triggered}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
@@ -500,30 +599,20 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
// show judge result and play sound / update combo like short notes
|
||||
// show judge result and update combo/UI like short notes
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound(result);
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result); // 是否启动头音符判定跳字
|
||||
|
||||
// --- Add scoring for Start like short notes ---
|
||||
try
|
||||
// Play judge sound only for START segment
|
||||
if (segment == NoteSegment.Start)
|
||||
{
|
||||
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
|
||||
if (allyGo != null)
|
||||
{
|
||||
var ally = allyGo.GetComponent<AllyCombatant>();
|
||||
if (ally != null)
|
||||
{
|
||||
int added = ally.AddScoreForJudge(result);
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
}
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound(result);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
|
||||
// Do not spawn the judgement animation prefab for Start (head) presses to avoid duplicated prefabs
|
||||
if (segment != NoteSegment.Start)
|
||||
{
|
||||
Debug.LogWarning($"[HoldNote] Failed to add per-track score for START: {ex}");
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
|
||||
}
|
||||
|
||||
if (segment != NoteSegment.Start)
|
||||
@@ -537,136 +626,120 @@ public class HoldNote : BaseNote
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] DelayedReturn: returning {noteID} to pool at time={Time.time:F3}");
|
||||
ReturnToPool();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEnd(bool forceMiss = false)
|
||||
// central evaluation for hold end, given an actual releaseTime (real-time) or forceMiss
|
||||
private void EvaluateHoldEnd(float actualReleaseTime, bool forceMiss)
|
||||
{
|
||||
if (isJudged) return; // already evaluated
|
||||
|
||||
// ensure we only resolve once per note
|
||||
if (!JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] END 已被判定,跳过: {noteColor}");
|
||||
return;
|
||||
}
|
||||
// 记录释放时间并在 JudgeManager 中登记已释放状态
|
||||
if (!hasReleased)
|
||||
{
|
||||
releaseTime = Time.time;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
}
|
||||
|
||||
string result;
|
||||
float rawOffsetMsEnd = 0f;
|
||||
|
||||
|
||||
// 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 Miss
|
||||
if (!hasEnteredLine && !forceMiss)
|
||||
{
|
||||
Debug.Log($"[HoldNote] HandleEnd: 提前松手,记录释放但等待尾部进入判定区再判定. NoteID={noteID}, color={noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] EvaluateHoldEnd: TryResolveEnd failed for {noteID}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 现在进行正常判定逻辑
|
||||
releaseTime = Time.time;
|
||||
releaseTime = actualReleaseTime;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance.RegisterNoteReleased(noteID, true);
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] EvaluateHoldEnd: noteID={noteID} releaseTime={releaseTime:F3} scheduledEnd={scheduledEndTime:F3} forceMiss={forceMiss}");
|
||||
|
||||
string result;
|
||||
|
||||
if (forceMiss || !JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
result = "Miss";
|
||||
// if (JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
if (result == "Perfect") ScoreManager.Instance.countPerfect += 1;
|
||||
else if (result == "Great") ScoreManager.Instance.countGreat += 1;
|
||||
else if (result == "Good") ScoreManager.Instance.countGood += 1;
|
||||
else ScoreManager.Instance.countMiss += 1;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[HoldNote] END判定失败({noteColor}):{(forceMiss ? "强制Miss" : "头部未判定")}");
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
if (GameConfig.verboseLogs) Debug.LogWarning($"[HoldNote] END judged as Miss: {noteColor} reason={(forceMiss ? "forced Miss" : "start not judged")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
float diff = Mathf.Abs(releaseTime - scheduledEndTime);
|
||||
rawOffsetMsEnd = (scheduledEndTime - releaseTime) * 1000f;
|
||||
if (judgeConfig != null)
|
||||
// try to use pool record for accurate press duration
|
||||
if (HoldNoteJudgePool.TryGet(noteID, out var info))
|
||||
{
|
||||
// apply hold-specific multiplier to end judgement windows as well
|
||||
float pEnd = judgeConfig.perfectRange * holdWindowMultiplier;
|
||||
float gEnd = judgeConfig.greatRange * holdWindowMultiplier;
|
||||
float gdEnd = judgeConfig.goodRange * holdWindowMultiplier;
|
||||
float mEnd = judgeConfig.missRange * holdWindowMultiplier;
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Found pool info for {noteID}: pressTime={info.pressTime:F3} length={info.length:F3} scheduledEnd={info.scheduledEnd:F3}");
|
||||
float playerHeld = Mathf.Clamp(releaseTime - info.pressTime, 0f, info.length);
|
||||
float frac = info.length <= 0f ? 0f : (playerHeld / info.length);
|
||||
|
||||
if (diff <= pEnd)
|
||||
if (frac > 0.8f)
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
}
|
||||
else if (diff <= gEnd)
|
||||
else if (frac > 0.5f)
|
||||
{
|
||||
result = "Great";
|
||||
}
|
||||
else if (diff <= gdEnd)
|
||||
{
|
||||
result = "Good";
|
||||
}
|
||||
else if (diff <= mEnd)
|
||||
{
|
||||
result = "Miss";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "Miss";
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
}
|
||||
|
||||
if (noteData != null && result != "Miss")
|
||||
{
|
||||
noteData.judgeOffsetMsEnd = (info.scheduledEnd - releaseTime) * 1000f;
|
||||
}
|
||||
|
||||
// cleanup pool
|
||||
HoldNoteJudgePool.Unregister(noteID);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (diff <= 0.1f * holdWindowMultiplier)
|
||||
if (GameConfig.verboseLogs) Debug.LogWarning($"[HoldNote] No pool info for {noteID}, falling back to local held fraction calculation");
|
||||
// fallback logic if no pool info
|
||||
float holdRequired = Mathf.Max(0.0001f, scheduledEndTime - hitTime);
|
||||
float held = Mathf.Clamp(actualReleaseTime - hitTime, 0f, holdRequired);
|
||||
float frac = holdRequired <= 0f ? 0f : (held / holdRequired);
|
||||
|
||||
if (frac > 0.8f)
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
}
|
||||
else if (diff < 0.2f * holdWindowMultiplier)
|
||||
else if (frac > 0.5f)
|
||||
{
|
||||
result = "Great";
|
||||
}
|
||||
else if (diff < 0.3f * holdWindowMultiplier)
|
||||
{
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "Bad";
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
}
|
||||
}
|
||||
// --- 新增:根据最终确定的 result 统计 ---
|
||||
if (result == "Perfect") ScoreManager.Instance.countPerfect += 1;
|
||||
else if (result == "Great") ScoreManager.Instance.countGreat += 1;
|
||||
else if (result == "Good") ScoreManager.Instance.countGood += 1;
|
||||
else ScoreManager.Instance.countMiss += 1;
|
||||
|
||||
// --- 新增:记录偏差数据到 NoteData ---
|
||||
if (noteData != null && result != "Miss")
|
||||
{
|
||||
noteData.judgeOffsetMsEnd = rawOffsetMsEnd;
|
||||
float rawOffsetMsEnd = (scheduledEndTime - actualReleaseTime) * 1000f;
|
||||
if (noteData != null && result != "Miss")
|
||||
{
|
||||
noteData.judgeOffsetMsEnd = rawOffsetMsEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END result for {noteID}: {result} (color={noteColor})");
|
||||
|
||||
// show UI/audio and combo
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
|
||||
teamUIController.Instance?.OnJudgeResult(result); // 新增:更新combo计数
|
||||
teamUIController.Instance?.OnJudgeResult(result);
|
||||
|
||||
// Ensure END always spawns judgement prefab (including Miss)
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
|
||||
|
||||
// --- Add scoring for End like short notes ---
|
||||
try
|
||||
{
|
||||
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
|
||||
if (allyGo != null)
|
||||
var ally = GetAllyForTrackCached(TrackIndex);
|
||||
if (ally != null)
|
||||
{
|
||||
var ally = allyGo.GetComponent<AllyCombatant>();
|
||||
if (ally != null)
|
||||
{
|
||||
int added = ally.AddScoreForJudge(result);
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
int added = ally.AddScoreForJudge(result);
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
@@ -677,26 +750,12 @@ public class HoldNote : BaseNote
|
||||
// Notify SkillBuilder so Hold notes can trigger skills configured for Hold
|
||||
if (JudgeManager.Instance.TryTriggerSkill(noteID))
|
||||
{
|
||||
var sb = SkillBuilder.Instance;
|
||||
if (sb == null)
|
||||
{
|
||||
Debug.LogWarning($"[HoldNote] SkillBuilder.Instance is null when trying to notify for track {TrackIndex}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var so = sb.GetAllyHeroSOBySlot(TrackIndex);
|
||||
var def = so?.GetPrimarySkill();
|
||||
Debug.Log($"[HoldNote] About to NotifyNoteHit: TrackIndex={TrackIndex} trackIndexField={trackIndex} so={(so != null ? so.name : "null")} skill={(def != null ? def.skillId : "null")} trigger={(def != null ? def.triggerCondition.ToString() : "-")}");
|
||||
}
|
||||
|
||||
bool triggeredHold = false;
|
||||
bool triggeredTap = false;
|
||||
|
||||
// Build candidate slot indices to try: prefer instance field, then BaseNote.TrackIndex, then try to match UI slots
|
||||
var candidates = new List<int>();
|
||||
candidates.Add(this.trackIndex);
|
||||
if (!candidates.Contains(this.TrackIndex)) candidates.Add(this.TrackIndex);
|
||||
// try to resolve by matching this object to UI slots
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null)
|
||||
{
|
||||
@@ -712,9 +771,6 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[HoldNote] NotifyNoteHit candidate slots: {string.Join(",", candidates)}");
|
||||
|
||||
// Try each candidate: prefer Hold notification. Stop on first successful trigger.
|
||||
foreach (var slot in candidates)
|
||||
{
|
||||
try
|
||||
@@ -725,14 +781,9 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
Debug.LogError($"[HoldNote] NotifyNoteHit(Hold) threw for slot {slot}: {ex}");
|
||||
}
|
||||
if (triggeredHold)
|
||||
{
|
||||
Debug.Log($"[HoldNote] NotifyNoteHit succeeded (Hold) for slot {slot}");
|
||||
break;
|
||||
}
|
||||
if (triggeredHold) break;
|
||||
}
|
||||
|
||||
// If no candidate triggered via Hold, try Tap fallback on same candidate list
|
||||
if (!triggeredHold)
|
||||
{
|
||||
foreach (var slot in candidates)
|
||||
@@ -745,68 +796,56 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
Debug.LogError($"[HoldNote] NotifyNoteHit(Tap) threw for slot {slot}: {ex}");
|
||||
}
|
||||
if (triggeredTap)
|
||||
{
|
||||
Debug.Log($"[HoldNote] NotifyNoteHit succeeded (Tap fallback) for slot {slot}");
|
||||
break;
|
||||
}
|
||||
if (triggeredTap) break;
|
||||
}
|
||||
}
|
||||
|
||||
// _hasTriggeredOnThisHold = true;
|
||||
Debug.Log($"[HoldNote] Hold end NotifyNoteHit called: TrackIndex={TrackIndex} trackIndexField={trackIndex} result={result} triggeredHold={triggeredHold} triggeredTap={triggeredTap}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Hold end NotifyNoteHit called: TrackIndex={TrackIndex} trackIndexField={trackIndex} result={result} triggeredHold={triggeredHold} triggeredTap={triggeredTap}");
|
||||
}
|
||||
Debug.Log($"[HoldNote] END判定结果: {noteColor} {result} (release={releaseTime:F2}, target={scheduledEndTime:F2})");
|
||||
|
||||
AnimationController.Global?.StopHoldParticles();
|
||||
ReturnToPool();
|
||||
|
||||
isJudged = true;
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
private void HandleEnd(bool forceMiss = false)
|
||||
{
|
||||
if (segment == NoteSegment.End)
|
||||
if (!JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
AnimationController.Global?.StopHoldParticles();
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END try-resolve failed: {noteColor}");
|
||||
return;
|
||||
}
|
||||
if (autoReturnCoroutine != null)
|
||||
// record release time and register release in JudgeManager
|
||||
if (!hasReleased)
|
||||
{
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
autoReturnCoroutine = null;
|
||||
}
|
||||
|
||||
if (segment == NoteSegment.End &&
|
||||
!isJudged && // 尚未判定
|
||||
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
|
||||
{
|
||||
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isHoldActive)
|
||||
if (Input.GetKey(keyToPress))
|
||||
{
|
||||
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
|
||||
if (JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
HandleEnd();
|
||||
}
|
||||
|
||||
isJudged = true;
|
||||
releaseTime = scheduledEndTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[HoldNote] OnDisable:End段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
|
||||
releaseTime = Time.time;
|
||||
}
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
}
|
||||
|
||||
controller?.StopMovement();
|
||||
// central evaluation
|
||||
EvaluateHoldEnd(releaseTime, forceMiss);
|
||||
}
|
||||
|
||||
private void ReturnToPool()
|
||||
{
|
||||
if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用
|
||||
if (!gameObject.activeSelf) return;
|
||||
|
||||
if (segment == NoteSegment.Start && !hasEnteredLine)
|
||||
{
|
||||
Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}");
|
||||
if (GameConfig.verboseLogs) Debug.LogWarning($"[HoldNote] Refusing to return a Start segment that never entered judge zone: {noteColor}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] ReturnToPool called for {noteID} segment={segment} time={Time.time:F3}");
|
||||
gameObject.SetActive(false);
|
||||
|
||||
if (segment == NoteSegment.Start)
|
||||
@@ -828,9 +867,10 @@ public class HoldNote : BaseNote
|
||||
scheduledEndTime = 0f;
|
||||
hasEnteredLine = false;
|
||||
isJudged = false;
|
||||
isHoldActive = false; // 重置长按标记
|
||||
hasBeenHeldFromStart = false; // 重置
|
||||
// _hasTriggeredOnThisHold = false; // 重置
|
||||
isHoldActive = false;
|
||||
hasBeenHeldFromStart = false;
|
||||
|
||||
holdStartTime = -1f;
|
||||
|
||||
if (autoReturnCoroutine != null)
|
||||
{
|
||||
@@ -846,7 +886,6 @@ public class HoldNote : BaseNote
|
||||
|
||||
public void ApplyVisualScale(float scaleY)
|
||||
{
|
||||
// clamp to reasonable range to avoid extreme distortion
|
||||
float s = Mathf.Clamp(scaleY, 0.1f, 4f);
|
||||
if (visualTransform != null)
|
||||
{
|
||||
@@ -869,4 +908,20 @@ public class HoldNote : BaseNote
|
||||
else
|
||||
transform.localScale = originalLocalScale;
|
||||
}
|
||||
|
||||
public void CalibratePosition(Vector3 spawnPointPosition, float tolerance = 0.02f)
|
||||
{
|
||||
if (controller == null) controller = GetComponent<HoldNoteController>();
|
||||
if (controller == null) return;
|
||||
|
||||
float activation = controller.ActivationTime;
|
||||
float s = controller.CurrentSpeed;
|
||||
float elapsed = Mathf.Max(0f, Time.time - activation);
|
||||
Vector3 expected = spawnPointPosition + Vector3.down * s * elapsed;
|
||||
|
||||
if (Vector3.Distance(transform.position, expected) > tolerance)
|
||||
{
|
||||
transform.position = expected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,12 @@ public class HoldNoteController : MonoBehaviour
|
||||
private bool isInsideJudgeZone = false;
|
||||
public event Action<bool> OnJudgeZoneChanged;
|
||||
private bool isMoving = false;
|
||||
private float speed = 0f; // 音符下落速度
|
||||
private float activationTime; // 用于延迟启用下落
|
||||
private float speed = 0f; // 运动速度
|
||||
private float activationTime; // 何时开始移动(Time.time 基准)
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// **控制音符下落**
|
||||
// 到达激活时间后开始移动
|
||||
if (!isMoving && Time.time >= activationTime)
|
||||
{
|
||||
isMoving = true;
|
||||
@@ -22,30 +22,40 @@ public class HoldNoteController : MonoBehaviour
|
||||
transform.Translate(Vector3.down * speed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置音符延迟下落的时间
|
||||
/// 设置该段的“到达判定线的目标时间”(hitTime) 与分段延迟(delay),并由此推导移动开始时间。
|
||||
/// 注意:这里必须使用与 HoldNote.hitTime 一致的时间基准(NoteSpawner 传入的谱面实时点),
|
||||
/// 否则用 Time.time + delay 会在首段/暂停/开歌延迟时产生系统性错位,导致永远 Miss。
|
||||
/// </summary>
|
||||
public void SetSegmentDelay(float segmentDelay)
|
||||
public void ConfigureTiming(float baseHitTime, float segmentDelay, float travelTime)
|
||||
{
|
||||
activationTime = Time.time + segmentDelay;
|
||||
// baseHitTime = startTime + noteData.time (+ globalHitDelay)
|
||||
float hitTime = baseHitTime + segmentDelay;
|
||||
activationTime = hitTime - travelTime;
|
||||
isMoving = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置音符的下落速度
|
||||
/// 兼容旧接口:不推荐。旧版用 Time.time 会导致时间基准不一致。
|
||||
/// </summary>
|
||||
public void SetSegmentDelay(float segmentDelay)
|
||||
{
|
||||
// Fallback to old behavior if caller didn't use ConfigureTiming.
|
||||
activationTime = Time.time + segmentDelay;
|
||||
isMoving = false;
|
||||
}
|
||||
|
||||
public void SetSpeed(float newSpeed)
|
||||
{
|
||||
speed = newSpeed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止音符下落(当音符被击中或Miss时)
|
||||
/// </summary>
|
||||
public void StopMovement()
|
||||
{
|
||||
isMoving = false;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D collision)
|
||||
{
|
||||
if (!collision.CompareTag("JudgmentLine")) return;
|
||||
@@ -66,4 +76,7 @@ public class HoldNoteController : MonoBehaviour
|
||||
{
|
||||
return isInsideJudgeZone;
|
||||
}
|
||||
|
||||
public float ActivationTime => activationTime;
|
||||
public float CurrentSpeed => speed;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public static class HoldNoteJudgePool
|
||||
{
|
||||
public class HoldInfo
|
||||
{
|
||||
public string noteID;
|
||||
public float pressTime; // real-time when player pressed start
|
||||
public float hitTime; // scheduled hit time (real-time)
|
||||
public float scheduledEnd; // scheduled end time (real-time)
|
||||
public float length; // scheduledEnd - hitTime
|
||||
public string color;
|
||||
public int trackIndex;
|
||||
public object noteData; // store as object to avoid type dependency
|
||||
}
|
||||
|
||||
private static readonly 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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(noteID)) return;
|
||||
HoldInfo info = new HoldInfo
|
||||
{
|
||||
noteID = noteID,
|
||||
pressTime = pressTime,
|
||||
hitTime = hitTime,
|
||||
scheduledEnd = scheduledEnd,
|
||||
length = Mathf.Max(0.0001f, scheduledEnd - hitTime),
|
||||
color = color,
|
||||
trackIndex = trackIndex,
|
||||
noteData = noteData
|
||||
};
|
||||
pool[noteID] = info;
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrEmpty(noteID)) { info = null; return false; }
|
||||
return pool.TryGetValue(noteID, out info);
|
||||
}
|
||||
|
||||
public static void Unregister(string noteID)
|
||||
{
|
||||
if (string.IsNullOrEmpty(noteID)) return;
|
||||
pool.Remove(noteID);
|
||||
}
|
||||
|
||||
public static void ClearAll()
|
||||
{
|
||||
pool.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f43e60cf3d44b294b8a5d786de110f9b
|
||||
@@ -1,158 +1,184 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class JudgeManager : MonoBehaviour
|
||||
{
|
||||
public static JudgeManager Instance { get; private set; }
|
||||
|
||||
// ===== 原有数据 =====
|
||||
private Dictionary<KeyCode, Queue<Note>> judgeQueues = new Dictionary<KeyCode, Queue<Note>>();
|
||||
private Dictionary<string, bool> startJudgedNotes = new Dictionary<string, bool>();
|
||||
private Dictionary<string, bool> releasedNotes = new Dictionary<string, bool>();
|
||||
private Dictionary<string, float> noteEndTimes = new Dictionary<string, float>();
|
||||
private Dictionary<string, int> middlePassedCounts = new Dictionary<string, int>();
|
||||
|
||||
// ===== 新增:长音符全局互斥状态 =====
|
||||
private class HoldJudgeState
|
||||
{
|
||||
public bool startResolved; // Start 是否已经判定过(成功 or 失败)
|
||||
public bool endResolved; // End 是否已经判定过
|
||||
public bool skillTriggered; // 技能是否已触发
|
||||
}
|
||||
|
||||
private Dictionary<string, HoldJudgeState> holdStates = new Dictionary<string, HoldJudgeState>();
|
||||
|
||||
private HoldJudgeState GetHoldState(string noteID)
|
||||
{
|
||||
if (!holdStates.ContainsKey(noteID))
|
||||
holdStates[noteID] = new HoldJudgeState();
|
||||
return holdStates[noteID];
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
// ===== 新增:判定互斥闸门 =====
|
||||
public bool TryResolveStart(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.startResolved) return false;
|
||||
s.startResolved = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryResolveEnd(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.endResolved) return false;
|
||||
s.endResolved = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTriggerSkill(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.skillTriggered) return false;
|
||||
s.skillTriggered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearHoldState(string noteID)
|
||||
{
|
||||
if (holdStates.ContainsKey(noteID))
|
||||
holdStates.Remove(noteID);
|
||||
}
|
||||
|
||||
// ===== 原有接口(保持不变) =====
|
||||
|
||||
public void RegisterScheduledEndTime(string noteID, float endTime)
|
||||
{
|
||||
noteEndTimes[noteID] = endTime;
|
||||
}
|
||||
|
||||
public float GetScheduledEndTime(string noteID)
|
||||
{
|
||||
return noteEndTimes.ContainsKey(noteID) ? noteEndTimes[noteID] : 0f;
|
||||
}
|
||||
|
||||
public void RegisterStartJudged(string noteID, bool state)
|
||||
{
|
||||
startJudgedNotes[noteID] = state;
|
||||
}
|
||||
|
||||
public bool IsStartJudged(string noteID)
|
||||
{
|
||||
return startJudgedNotes.ContainsKey(noteID) && startJudgedNotes[noteID];
|
||||
}
|
||||
|
||||
public void RegisterNoteReleased(string noteID, bool state)
|
||||
{
|
||||
releasedNotes[noteID] = state;
|
||||
Debug.Log($"[JudgeManager] RegisterNoteReleased: {noteID} = {state}");
|
||||
}
|
||||
|
||||
public bool HasNoteReleased(string noteID)
|
||||
{
|
||||
return releasedNotes.ContainsKey(noteID) && releasedNotes[noteID];
|
||||
}
|
||||
|
||||
public void RegisterNote(KeyCode key, Note note)
|
||||
{
|
||||
if (!judgeQueues.ContainsKey(key))
|
||||
judgeQueues[key] = new Queue<Note>();
|
||||
|
||||
if (!judgeQueues[key].Contains(note))
|
||||
judgeQueues[key].Enqueue(note);
|
||||
}
|
||||
|
||||
public void UnregisterNote(KeyCode key, Note note)
|
||||
{
|
||||
if (!judgeQueues.ContainsKey(key)) return;
|
||||
|
||||
Queue<Note> newQueue = new Queue<Note>();
|
||||
while (judgeQueues[key].Count > 0)
|
||||
{
|
||||
Note n = judgeQueues[key].Dequeue();
|
||||
if (n != note)
|
||||
newQueue.Enqueue(n);
|
||||
else if (!n.IsJudged())
|
||||
n.JudgeMiss();
|
||||
}
|
||||
judgeQueues[key] = newQueue;
|
||||
}
|
||||
|
||||
public void JudgeEarliestNote(KeyCode key)
|
||||
{
|
||||
if (judgeQueues.ContainsKey(key) && judgeQueues[key].Count > 0)
|
||||
{
|
||||
Note note = judgeQueues[key].Dequeue();
|
||||
if (note != null && !note.IsJudged())
|
||||
note.Judge();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterMiddlePassed(string noteID)
|
||||
{
|
||||
if (!middlePassedCounts.ContainsKey(noteID))
|
||||
middlePassedCounts[noteID] = 0;
|
||||
middlePassedCounts[noteID]++;
|
||||
Debug.Log($"[JudgeManager] Middle passed for {noteID}, total={middlePassedCounts[noteID]}");
|
||||
}
|
||||
|
||||
public int GetMiddlePassedCount(string noteID)
|
||||
{
|
||||
return middlePassedCounts.ContainsKey(noteID) ? middlePassedCounts[noteID] : 0;
|
||||
}
|
||||
|
||||
public void ClearNoteRecord(string noteID)
|
||||
{
|
||||
startJudgedNotes.Remove(noteID);
|
||||
releasedNotes.Remove(noteID);
|
||||
noteEndTimes.Remove(noteID);
|
||||
middlePassedCounts.Remove(noteID);
|
||||
ClearHoldState(noteID);
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class JudgeManager : MonoBehaviour
|
||||
{
|
||||
public static JudgeManager Instance { get; private set; }
|
||||
|
||||
// ===== ԭ������ =====
|
||||
private Dictionary<KeyCode, Queue<Note>> judgeQueues = new Dictionary<KeyCode, Queue<Note>>();
|
||||
private Dictionary<string, bool> startJudgedNotes = new Dictionary<string, bool>();
|
||||
private Dictionary<string, bool> releasedNotes = new Dictionary<string, bool>();
|
||||
private Dictionary<string, float> noteEndTimes = new Dictionary<string, float>();
|
||||
private Dictionary<string, int> middlePassedCounts = new Dictionary<string, int>();
|
||||
|
||||
// ===== ������������ȫ�ֻ���״̬ =====
|
||||
private class HoldJudgeState
|
||||
{
|
||||
public bool startResolved; // Start �Ƿ��Ѿ��ж������ɹ� or ʧ�ܣ�
|
||||
public bool endResolved; // End �Ƿ��Ѿ��ж���
|
||||
public bool skillTriggered; // �����Ƿ��Ѵ���
|
||||
}
|
||||
|
||||
private Dictionary<string, HoldJudgeState> holdStates = new Dictionary<string, HoldJudgeState>();
|
||||
|
||||
private HoldJudgeState GetHoldState(string noteID)
|
||||
{
|
||||
if (!holdStates.ContainsKey(noteID))
|
||||
holdStates[noteID] = new HoldJudgeState();
|
||||
return holdStates[noteID];
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
// ===== �������ж�����բ�� =====
|
||||
public bool TryResolveStart(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.startResolved)
|
||||
{
|
||||
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}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryResolveEnd(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.endResolved)
|
||||
{
|
||||
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}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTriggerSkill(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.skillTriggered)
|
||||
{
|
||||
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}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearHoldState(string noteID)
|
||||
{
|
||||
if (holdStates.ContainsKey(noteID))
|
||||
{
|
||||
holdStates.Remove(noteID);
|
||||
Debug.Log($"[JudgeManager] ClearHoldState: cleared state for {noteID}");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ԭ�нӿڣ����ֲ��䣩 =====
|
||||
|
||||
public void RegisterScheduledEndTime(string noteID, float endTime)
|
||||
{
|
||||
noteEndTimes[noteID] = endTime;
|
||||
Debug.Log($"[JudgeManager] RegisterScheduledEndTime: {noteID} -> {endTime:F3}");
|
||||
}
|
||||
|
||||
public float GetScheduledEndTime(string noteID)
|
||||
{
|
||||
return noteEndTimes.ContainsKey(noteID) ? noteEndTimes[noteID] : 0f;
|
||||
}
|
||||
|
||||
public void RegisterStartJudged(string noteID, bool state)
|
||||
{
|
||||
startJudgedNotes[noteID] = state;
|
||||
Debug.Log($"[JudgeManager] RegisterStartJudged: {noteID} = {state} at time={Time.time:F3}");
|
||||
}
|
||||
|
||||
public bool IsStartJudged(string noteID)
|
||||
{
|
||||
return startJudgedNotes.ContainsKey(noteID) && startJudgedNotes[noteID];
|
||||
}
|
||||
|
||||
public void RegisterNoteReleased(string noteID, bool state)
|
||||
{
|
||||
releasedNotes[noteID] = state;
|
||||
Debug.Log($"[JudgeManager] RegisterNoteReleased: {noteID} = {state}");
|
||||
}
|
||||
|
||||
public bool HasNoteReleased(string noteID)
|
||||
{
|
||||
return releasedNotes.ContainsKey(noteID) && releasedNotes[noteID];
|
||||
}
|
||||
|
||||
public void RegisterNote(KeyCode key, Note note)
|
||||
{
|
||||
if (!judgeQueues.ContainsKey(key))
|
||||
judgeQueues[key] = new Queue<Note>();
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
public void UnregisterNote(KeyCode key, Note note)
|
||||
{
|
||||
if (!judgeQueues.ContainsKey(key)) return;
|
||||
|
||||
Queue<Note> newQueue = new Queue<Note>();
|
||||
while (judgeQueues[key].Count > 0)
|
||||
{
|
||||
Note n = judgeQueues[key].Dequeue();
|
||||
if (n != note)
|
||||
newQueue.Enqueue(n);
|
||||
else if (!n.IsJudged())
|
||||
n.JudgeMiss();
|
||||
}
|
||||
judgeQueues[key] = newQueue;
|
||||
Debug.Log($"[JudgeManager] UnregisterNote: key={key} removed {note.name}, newQueueSize={judgeQueues[key].Count}");
|
||||
}
|
||||
|
||||
public void JudgeEarliestNote(KeyCode key)
|
||||
{
|
||||
if (judgeQueues.ContainsKey(key) && judgeQueues[key].Count > 0)
|
||||
{
|
||||
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}");
|
||||
note.Judge();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterMiddlePassed(string noteID)
|
||||
{
|
||||
if (!middlePassedCounts.ContainsKey(noteID))
|
||||
middlePassedCounts[noteID] = 0;
|
||||
middlePassedCounts[noteID]++;
|
||||
Debug.Log($"[JudgeManager] Middle passed for {noteID}, total={middlePassedCounts[noteID]}");
|
||||
}
|
||||
|
||||
public int GetMiddlePassedCount(string noteID)
|
||||
{
|
||||
return middlePassedCounts.ContainsKey(noteID) ? middlePassedCounts[noteID] : 0;
|
||||
}
|
||||
|
||||
public void ClearNoteRecord(string noteID)
|
||||
{
|
||||
startJudgedNotes.Remove(noteID);
|
||||
releasedNotes.Remove(noteID);
|
||||
noteEndTimes.Remove(noteID);
|
||||
middlePassedCounts.Remove(noteID);
|
||||
ClearHoldState(noteID);
|
||||
Debug.Log($"[JudgeManager] ClearNoteRecord: cleared records for {noteID}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,25 @@ public class Note : BaseNote
|
||||
|
||||
private NoteData noteData;
|
||||
|
||||
[Header("判定区间配置")]
|
||||
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
|
||||
[Header("���������")]
|
||||
public NoteJudgeConfig judgeConfig; // �ж��������ã�����Ԥ���������ʱ��ֵ
|
||||
|
||||
// Cache allies per track to avoid GameObject.Find on every judge.
|
||||
private static AllyCombatant[] allyCache;
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -37,11 +54,15 @@ public class Note : BaseNote
|
||||
|
||||
InputManager.OnKeyPressed += HandlePress;
|
||||
|
||||
// 判定区间配置检查
|
||||
// �ж��������ü��
|
||||
if (judgeConfig == null)
|
||||
Debug.LogError($"NoteJudgeConfig is null! 判定区间配置未传入!track={trackIndex}");
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.LogError($"NoteJudgeConfig is null! �ж���������δ���룡track={trackIndex}");
|
||||
}
|
||||
else
|
||||
Debug.Log($"NoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log($"NoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -51,12 +72,12 @@ public class Note : BaseNote
|
||||
|
||||
private void HandlePress(KeyCode key)
|
||||
{
|
||||
// 只判定正确轨道且在判定区
|
||||
// ֻ�ж���ȷ��������ж���
|
||||
if (isJudged || key != keyToPress || (controller != null && !controller.IsInJudgeZone()))
|
||||
return;
|
||||
|
||||
float timeDifference = Mathf.Abs(Time.time - hitTime);
|
||||
// 计算正负偏差:正值提前,负值落后
|
||||
// ��������ƫ���ֵ��ǰ����ֵ���
|
||||
float rawOffsetMs = (hitTime - Time.time) * 1000f;
|
||||
string judgeResult = null;
|
||||
|
||||
@@ -64,37 +85,29 @@ public class Note : BaseNote
|
||||
{
|
||||
if (timeDifference <= judgeConfig.perfectRange)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Perfect");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Perfect");
|
||||
judgeResult = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.greatRange)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Great");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Great");
|
||||
judgeResult = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.goodRange)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Good");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Good");
|
||||
judgeResult = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.missRange)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Miss");
|
||||
judgeResult = "Miss";
|
||||
/*ScoreManager.Instance.countMiss += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;*/
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Miss");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss");
|
||||
judgeResult = "Miss";
|
||||
// Do not return here - let shared post-judge logic run so NotifyNoteHit is invoked for Miss as well
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -102,43 +115,36 @@ public class Note : BaseNote
|
||||
// fallback timing
|
||||
if (timeDifference <= 0.08f)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Perfect");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Perfect");
|
||||
judgeResult = "Perfect";
|
||||
}
|
||||
else if (timeDifference <= 0.15f)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Great");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Great");
|
||||
judgeResult = "Great";
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Miss");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss");
|
||||
judgeResult = "Miss";
|
||||
// Do not return here - let shared post-judge logic run so NotifyNoteHit is invoked for Miss as well
|
||||
}
|
||||
}
|
||||
|
||||
// 显示判定信息
|
||||
if (!string.IsNullOrEmpty(judgeResult))
|
||||
{
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult);
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound(judgeResult);
|
||||
teamUIController.Instance?.OnJudgeResult(judgeResult); // 更新combo计数
|
||||
teamUIController.Instance?.OnJudgeResult(judgeResult);
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, judgeResult);
|
||||
|
||||
// Calculate score addition via AllyCombatant and add to per-track pm sums
|
||||
try
|
||||
{
|
||||
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
|
||||
if (allyGo != null)
|
||||
var ally = GetAllyForTrackCached(TrackIndex);
|
||||
if (ally != null)
|
||||
{
|
||||
var ally = allyGo.GetComponent<AllyCombatant>();
|
||||
if (ally != null)
|
||||
{
|
||||
int added = ally.AddScoreForJudge(judgeResult);
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
int added = ally.AddScoreForJudge(judgeResult);
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
@@ -146,9 +152,9 @@ public class Note : BaseNote
|
||||
Debug.LogWarning($"[Note] Failed to add per-track score: {ex}");
|
||||
}
|
||||
|
||||
// Notify SkillBuilder about this note hit so OnNoteHit skills may trigger (tap notes)
|
||||
try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, judgeResult, SkillDefinition.NoteTypeTrigger.Tap); } catch { }
|
||||
}
|
||||
|
||||
Judge();
|
||||
}
|
||||
|
||||
@@ -178,29 +184,26 @@ public class Note : BaseNote
|
||||
|
||||
public void JudgeMiss()
|
||||
{
|
||||
// idempotent: this can be called from multiple paths
|
||||
if (isJudged) return;
|
||||
|
||||
isJudged = true;
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
Debug.Log($"{keyToPress} Miss");
|
||||
// Ensure UI shows Miss and combo is updated when a note auto-misses
|
||||
if (GameConfig.verboseLogs) Debug.Log($"{keyToPress} Miss");
|
||||
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
|
||||
|
||||
// Add score for Miss as well (some systems may give 0)
|
||||
try
|
||||
{
|
||||
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
|
||||
if (allyGo != null)
|
||||
var ally = GetAllyForTrackCached(TrackIndex);
|
||||
if (ally != null)
|
||||
{
|
||||
var ally = allyGo.GetComponent<AllyCombatant>();
|
||||
if (ally != null)
|
||||
{
|
||||
int added = ally.AddScoreForJudge("Miss");
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
int added = ally.AddScoreForJudge("Miss");
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
@@ -208,9 +211,8 @@ public class Note : BaseNote
|
||||
Debug.LogWarning($"[Note] Failed to add per-track score for Miss: {ex}");
|
||||
}
|
||||
|
||||
// Notify SkillBuilder about Miss so OnNoteHit skills configured for Miss can trigger
|
||||
try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, "Miss", SkillDefinition.NoteTypeTrigger.Tap); } catch { }
|
||||
// InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss"); // 已在HandlePress中调用 for manual presses
|
||||
|
||||
ReturnToPool();
|
||||
}
|
||||
|
||||
@@ -236,17 +238,14 @@ public class Note : BaseNote
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由 Controller 通知 Note 是否进入判定区
|
||||
/// �� Controller ֪ͨ Note �Ƿ�����ж���
|
||||
/// </summary>
|
||||
public void SetJudgeZone(bool inZone)
|
||||
{
|
||||
// 离开判定区且未判定则判 Miss
|
||||
if (!inZone && !isJudged)
|
||||
// leaving judge zone without being judged -> Miss
|
||||
if (!inZone)
|
||||
{
|
||||
// Show Miss and update combo
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
// JudgeMiss already handles UI/sound and prevents double execution.
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ public class NoteController : MonoBehaviour
|
||||
{
|
||||
private float speed;
|
||||
private bool isMoving = true;
|
||||
private bool isInJudgeZone = false; // 判定区域标记
|
||||
private bool isInJudgeZone = false; // �������
|
||||
|
||||
private ParticleSystem hitEffect;
|
||||
private Note linkedNote;
|
||||
@@ -59,7 +59,7 @@ public class NoteController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 将 OnTriggerEnter2D 和 OnTriggerExit2D 挂载在 Controller 上
|
||||
// �� OnTriggerEnter2D �� OnTriggerExit2D ������ Controller ��
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D collision)
|
||||
{
|
||||
@@ -92,7 +92,7 @@ public class NoteController : MonoBehaviour
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 供 Note 查询是否处于判定区域
|
||||
/// �� Note ��ѯ�Ƿ����ж�����
|
||||
/// </summary>
|
||||
public bool IsInJudgeZone()
|
||||
{
|
||||
|
||||
@@ -1,309 +1,397 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using GamePlay; // for PoolItem
|
||||
|
||||
public class NotePool : MonoBehaviour
|
||||
{
|
||||
public static NotePool Instance { get; private set; }
|
||||
|
||||
public GameObject[] notePrefabs; // 短音符预制体(按颜色存储)
|
||||
public GameObject[] holdNotePrefabs; // 长音符片段预制体
|
||||
public GameObject[] holdNoteEndPrefabs; // 新增:尾部专用长音符预制体(可选)
|
||||
public GameObject[] startNotePrefabs; // 长音符start片段预制体
|
||||
private int poolSize = 12; // 每种类型的音符池大小
|
||||
private int maxPoolSize = 60; // 池的最大容量
|
||||
|
||||
// 颜色到索引的快速映射,避免每次 switch
|
||||
private Dictionary<string, int> colorIndexMap;
|
||||
|
||||
private Dictionary<int, Stack<GameObject>> notePools; // 短音符对象池
|
||||
private Dictionary<int, Stack<GameObject>> holdNotePools; // 长音符片段对象池
|
||||
private Dictionary<int, Stack<GameObject>> holdNoteEndPools; // 新增:尾部长音符对象池
|
||||
private Dictionary<int, Stack<GameObject>> startNotePools; // 长音符start片段对象池
|
||||
|
||||
// 容器物体用于在层级中组织池中的对象,便于调试和管理
|
||||
private Transform notePoolContainer;
|
||||
private Transform holdPoolContainer;
|
||||
private Transform startPoolContainer;
|
||||
|
||||
// 控制调试输出,默认关闭以提升性能
|
||||
public bool verboseLogging = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化颜色映射
|
||||
colorIndexMap = new Dictionary<string, int>
|
||||
{
|
||||
{ "red", 0 },
|
||||
{ "green", 1 },
|
||||
{ "yellow", 2 },
|
||||
{ "purple", 3 },
|
||||
{ "blue", 4 }
|
||||
};
|
||||
|
||||
// 创建容器
|
||||
notePoolContainer = new GameObject("_NotePool_Notes").transform;
|
||||
notePoolContainer.SetParent(transform, false);
|
||||
holdPoolContainer = new GameObject("_NotePool_HoldSegments").transform;
|
||||
holdPoolContainer.SetParent(transform, false);
|
||||
startPoolContainer = new GameObject("_NotePool_StartSegments").transform;
|
||||
startPoolContainer.SetParent(transform, false);
|
||||
|
||||
notePools = new Dictionary<int, Stack<GameObject>>();
|
||||
holdNotePools = new Dictionary<int, Stack<GameObject>>();
|
||||
holdNoteEndPools = new Dictionary<int, Stack<GameObject>>();
|
||||
startNotePools = new Dictionary<int, Stack<GameObject>>();
|
||||
|
||||
int colorCount = Mathf.Max(1, notePrefabs != null ? notePrefabs.Length : 0);
|
||||
|
||||
for (int i = 0; i < colorCount; i++)
|
||||
{
|
||||
notePools[i] = new Stack<GameObject>();
|
||||
holdNotePools[i] = new Stack<GameObject>();
|
||||
holdNoteEndPools[i] = new Stack<GameObject>();
|
||||
startNotePools[i] = new Stack<GameObject>();
|
||||
|
||||
for (int j = 0; j < poolSize; j++)
|
||||
{
|
||||
AddToPool(notePools[i], notePrefabs[i], notePoolContainer);
|
||||
AddToPool(startNotePools[i], startNotePrefabs[i], startPoolContainer);
|
||||
}
|
||||
|
||||
for (int j = 0; j < poolSize * 5; j++)
|
||||
{
|
||||
AddToPool(holdNotePools[i], holdNotePrefabs[i], holdPoolContainer);
|
||||
// 如果专用尾部预制体存在则初始化尾部池,否则使用中段预制体
|
||||
if (holdNoteEndPrefabs != null && holdNoteEndPrefabs.Length > i && holdNoteEndPrefabs[i] != null)
|
||||
AddToPool(holdNoteEndPools[i], holdNoteEndPrefabs[i], holdPoolContainer);
|
||||
else
|
||||
AddToPool(holdNoteEndPools[i], holdNotePrefabs[i], holdPoolContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject GetObjectFromPool(Stack<GameObject> pool, GameObject prefab, Transform container)
|
||||
{
|
||||
if (pool == null)
|
||||
return InstantiateAndPrepare(prefab);
|
||||
|
||||
if (pool.Count > 0)
|
||||
{
|
||||
GameObject obj = pool.Pop();
|
||||
if (obj == null)
|
||||
{
|
||||
// 如果池中对象已被销毁,创建新对象
|
||||
obj = InstantiateAndPrepare(prefab);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 优化:使用 PoolItem 标识来检查是否来自同一 prefab
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi == null || prefab == null || pi.prefabName != prefab.name)
|
||||
{
|
||||
// 销毁错误类型的对象并替换为正确的 prefab 实例
|
||||
if (pi == null && verboseLogging) Debug.LogWarning("PoolItem missing on pooled object, replacing.");
|
||||
Destroy(obj);
|
||||
obj = InstantiateAndPrepare(prefab);
|
||||
}
|
||||
}
|
||||
obj.transform.SetParent(null);
|
||||
obj.SetActive(true);
|
||||
|
||||
// mark as taken from pool
|
||||
GamePlay.PoolItem takenPi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (takenPi != null) takenPi.inPool = false;
|
||||
|
||||
return obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 池空时直接实例化新对象
|
||||
GameObject obj = InstantiateAndPrepare(prefab);
|
||||
obj.SetActive(true);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject InstantiateAndPrepare(GameObject prefab)
|
||||
{
|
||||
if (prefab == null) return null;
|
||||
GameObject obj = Instantiate(prefab);
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi == null) pi = obj.AddComponent<GamePlay.PoolItem>();
|
||||
pi.prefabName = prefab.name;
|
||||
pi.inPool = false;
|
||||
return obj;
|
||||
}
|
||||
|
||||
private void ReturnObjectToPool(Stack<GameObject> pool, GameObject obj, Transform container)
|
||||
{
|
||||
if (obj == null || pool == null) return; // 防止空引用
|
||||
|
||||
// 避免重复归还:如果对象已在池内,直接忽略
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi != null && pi.inPool) return;
|
||||
|
||||
// 将对象移动到池容器并重置变换
|
||||
obj.transform.SetParent(container, false);
|
||||
obj.transform.localPosition = Vector3.zero;
|
||||
obj.transform.localRotation = Quaternion.identity;
|
||||
obj.transform.localScale = Vector3.one;
|
||||
|
||||
// 设为非激活状态以便复用
|
||||
obj.SetActive(false);
|
||||
|
||||
// 标记为已在池内
|
||||
if (pi != null) pi.inPool = true;
|
||||
|
||||
if (pool.Count < maxPoolSize)
|
||||
{
|
||||
pool.Push(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果池已满,销毁多余对象以节省内存
|
||||
Destroy(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToPool(Stack<GameObject> pool, GameObject prefab, Transform container)
|
||||
{
|
||||
if (prefab == null) return;
|
||||
if (pool.Count < maxPoolSize)
|
||||
{
|
||||
GameObject obj = Instantiate(prefab, container);
|
||||
// ensure PoolItem exists and is marked in pool
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>() ?? obj.AddComponent<GamePlay.PoolItem>();
|
||||
pi.prefabName = prefab.name;
|
||||
pi.inPool = true;
|
||||
|
||||
obj.SetActive(false);
|
||||
pool.Push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject GetNote(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(notePools[colorIndex], notePrefabs[colorIndex], notePoolContainer);
|
||||
}
|
||||
|
||||
public GameObject GetStartNote(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(startNotePools[colorIndex], startNotePrefabs[colorIndex], startPoolContainer);
|
||||
}
|
||||
|
||||
public GameObject GetHoldNoteSegment(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(holdNotePools[colorIndex], holdNotePrefabs[colorIndex], holdPoolContainer);
|
||||
}
|
||||
|
||||
// 新增:从尾部专用池获取尾部片段(如果未配置尾部预制体则降级为中段)
|
||||
public GameObject GetHoldNoteEndSegment(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
GameObject prefab = (holdNoteEndPrefabs != null && holdNoteEndPrefabs.Length > colorIndex && holdNoteEndPrefabs[colorIndex] != null)
|
||||
? holdNoteEndPrefabs[colorIndex]
|
||||
: holdNotePrefabs[colorIndex];
|
||||
return GetObjectFromPool(holdNoteEndPools[colorIndex], prefab, holdPoolContainer);
|
||||
}
|
||||
|
||||
public void ReturnNote(GameObject note, string color)
|
||||
{
|
||||
if (note == null) return;
|
||||
NoteController noteScript = note.GetComponent<NoteController>();
|
||||
if (noteScript != null)
|
||||
{
|
||||
noteScript.ResetState();
|
||||
}
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(notePools[idx], note, notePoolContainer);
|
||||
}
|
||||
|
||||
public void ReturnStartNote(GameObject startNote, string color)
|
||||
{
|
||||
if (startNote == null) return;
|
||||
if (string.IsNullOrEmpty(color))
|
||||
{
|
||||
if (verboseLogging) Debug.LogWarning($" 归还 StartNote 时 color 为空!音符名: {startNote.name}");
|
||||
Destroy(startNote);
|
||||
return;
|
||||
}
|
||||
|
||||
HoldNote holdNote = startNote.GetComponent<HoldNote>();
|
||||
if (holdNote != null)
|
||||
{
|
||||
holdNote.ResetState(); // 确保重置音符状态
|
||||
}
|
||||
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(startNotePools[idx], startNote, startPoolContainer);
|
||||
}
|
||||
|
||||
// 新增:归还尾部片段
|
||||
public void ReturnHoldNoteEndSegment(GameObject holdNoteEnd, string color)
|
||||
{
|
||||
if (holdNoteEnd == null) return;
|
||||
if (string.IsNullOrEmpty(color))
|
||||
{
|
||||
if (verboseLogging) Debug.LogWarning($" 归还 HoldNoteEndSegment 时 color 为空!音符名: {holdNoteEnd.name}");
|
||||
Destroy(holdNoteEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
HoldNote holdNoteScript = holdNoteEnd.GetComponent<HoldNote>();
|
||||
if (holdNoteScript != null)
|
||||
{
|
||||
holdNoteScript.ResetState(); // 确保重置音符状态
|
||||
}
|
||||
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(holdNoteEndPools[idx], holdNoteEnd, holdPoolContainer);
|
||||
}
|
||||
|
||||
public void ReturnHoldNoteSegment(GameObject holdNote, string color)
|
||||
{
|
||||
if (holdNote == null) return;
|
||||
if (string.IsNullOrEmpty(color))
|
||||
{
|
||||
if (verboseLogging) Debug.LogWarning($" 归还 HoldNoteSegment 时 color 为空!音符名: {holdNote.name}");
|
||||
Destroy(holdNote);
|
||||
return;
|
||||
}
|
||||
|
||||
HoldNote holdNoteScript = holdNote.GetComponent<HoldNote>();
|
||||
if (holdNoteScript != null)
|
||||
{
|
||||
holdNoteScript.ResetState(); // 确保重置音符状态
|
||||
}
|
||||
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(holdNotePools[idx], holdNote, holdPoolContainer);
|
||||
}
|
||||
|
||||
private int GetColorIndexFromName(string colorName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(colorName)) return 0;
|
||||
if (colorIndexMap != null && colorIndexMap.TryGetValue(colorName, out int idx)) return idx;
|
||||
if (verboseLogging) Debug.LogError($"未识别的颜色: {colorName},默认使用红色");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class PoolItem : MonoBehaviour
|
||||
{
|
||||
public string prefabName;
|
||||
public bool inPool;
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using GamePlay; // for PoolItem
|
||||
using System.Collections;
|
||||
|
||||
public class NotePool : MonoBehaviour
|
||||
{
|
||||
public static NotePool Instance { get; private set; }
|
||||
|
||||
public GameObject[] notePrefabs; // ������Ԥ���壨����ɫ�洢��
|
||||
public GameObject[] holdNotePrefabs; // ������Ƭ��Ԥ����
|
||||
public GameObject[] holdNoteEndPrefabs; // ������β��ר�ó�����Ԥ���壨��ѡ��
|
||||
public GameObject[] startNotePrefabs; // ������startƬ��Ԥ����
|
||||
private int poolSize = 24; // ÿ�����͵������ش�С (increased)
|
||||
private int maxPoolSize = 120; // �ص�������� (increased)
|
||||
|
||||
// ��ɫ�������Ŀ���ӳ�䣬����ÿ�� switch
|
||||
private Dictionary<string, int> colorIndexMap;
|
||||
|
||||
private Dictionary<int, Stack<GameObject>> notePools; // �����������
|
||||
private Dictionary<int, Stack<GameObject>> holdNotePools; // ������Ƭ�ζ����
|
||||
private Dictionary<int, Stack<GameObject>> holdNoteEndPools; // ������������������
|
||||
private Dictionary<int, Stack<GameObject>> startNotePools; // ������startƬ�ζ����
|
||||
|
||||
// �������������ڲ㼶����֯���еĶ����ڵ��Ժ���
|
||||
private Transform notePoolContainer;
|
||||
private Transform holdPoolContainer;
|
||||
private Transform startPoolContainer;
|
||||
|
||||
// ���Ƶ��������Ĭ�Ϲر�����������
|
||||
public bool verboseLogging = false;
|
||||
|
||||
[Header("Prewarm Settings")]
|
||||
[Tooltip("If true, the pool will be prewarmed across multiple frames to avoid a large GC/instantiation spike at scene load.")]
|
||||
public bool prewarmOnStart = true;
|
||||
[Tooltip("Number of instantiated pool items to create per frame during prewarming.")]
|
||||
// Raised default so heavy pools finish prewarming faster and reduce hitch at first spawn.
|
||||
public int prewarmPerFrame = 200;
|
||||
|
||||
// internal prewarm coroutine handle
|
||||
private Coroutine prewarmCoroutine = null;
|
||||
|
||||
// Expose whether prewarm has completed
|
||||
public bool IsPrewarmed => prewarmCoroutine == null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
// ��ʼ����ɫӳ��
|
||||
colorIndexMap = new Dictionary<string, int>
|
||||
{
|
||||
{ "red", 0 },
|
||||
{ "green", 1 },
|
||||
{ "yellow", 2 },
|
||||
{ "purple", 3 },
|
||||
{ "blue", 4 }
|
||||
};
|
||||
|
||||
// ��������
|
||||
notePoolContainer = new GameObject("_NotePool_Notes").transform;
|
||||
notePoolContainer.SetParent(transform, false);
|
||||
holdPoolContainer = new GameObject("_NotePool_HoldSegments").transform;
|
||||
holdPoolContainer.SetParent(transform, false);
|
||||
startPoolContainer = new GameObject("_NotePool_StartSegments").transform;
|
||||
startPoolContainer.SetParent(transform, false);
|
||||
|
||||
notePools = new Dictionary<int, Stack<GameObject>>();
|
||||
holdNotePools = new Dictionary<int, Stack<GameObject>>();
|
||||
holdNoteEndPools = new Dictionary<int, Stack<GameObject>>();
|
||||
startNotePools = new Dictionary<int, Stack<GameObject>>();
|
||||
|
||||
int colorCount = Mathf.Max(1, notePrefabs != null ? notePrefabs.Length : 0);
|
||||
|
||||
for (int i = 0; i < colorCount; i++)
|
||||
{
|
||||
notePools[i] = new Stack<GameObject>();
|
||||
holdNotePools[i] = new Stack<GameObject>();
|
||||
holdNoteEndPools[i] = new Stack<GameObject>();
|
||||
startNotePools[i] = new Stack<GameObject>();
|
||||
}
|
||||
|
||||
// minimal immediate entries to ensure safety (one each)
|
||||
for (int i = 0; i < colorCount; i++)
|
||||
{
|
||||
if (notePrefabs != null && i < notePrefabs.Length && notePrefabs[i] != null)
|
||||
AddToPool(notePools[i], notePrefabs[i], notePoolContainer);
|
||||
if (startNotePrefabs != null && i < startNotePrefabs.Length && startNotePrefabs[i] != null)
|
||||
AddToPool(startNotePools[i], startNotePrefabs[i], startPoolContainer);
|
||||
if (holdNotePrefabs != null && i < holdNotePrefabs.Length && holdNotePrefabs[i] != null)
|
||||
AddToPool(holdNotePools[i], holdNotePrefabs[i], holdPoolContainer);
|
||||
if (holdNoteEndPrefabs != null && i < holdNoteEndPrefabs.Length && holdNoteEndPrefabs[i] != null)
|
||||
AddToPool(holdNoteEndPools[i], holdNoteEndPrefabs[i], holdPoolContainer);
|
||||
}
|
||||
|
||||
if (prewarmOnStart)
|
||||
{
|
||||
prewarmCoroutine = StartCoroutine(PrewarmCoroutine(colorCount));
|
||||
}
|
||||
|
||||
// Also ensure particle and judge prefab warm-up if AnimationController exists in scene.
|
||||
var anim = AnimationController.Global ?? FindObjectOfType<AnimationController>();
|
||||
if (anim != null)
|
||||
{
|
||||
anim.PrewarmParticles(2);
|
||||
if (verboseLogging) Debug.Log("NotePool: requested AnimationController prewarm");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator PrewarmCoroutine(int colorCount)
|
||||
{
|
||||
// Calculate targets
|
||||
int noteTarget = poolSize; // per color
|
||||
int startTarget = poolSize;
|
||||
int holdTarget = poolSize * 5;
|
||||
int endTarget = poolSize * 5;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int createdThisFrame = 0;
|
||||
bool allReached = true;
|
||||
|
||||
for (int i = 0; i < colorCount; i++)
|
||||
{
|
||||
// note pool
|
||||
while (notePools[i].Count < noteTarget && createdThisFrame < prewarmPerFrame)
|
||||
{
|
||||
if (notePrefabs != null && i < notePrefabs.Length && notePrefabs[i] != null)
|
||||
AddToPool(notePools[i], notePrefabs[i], notePoolContainer);
|
||||
createdThisFrame++;
|
||||
}
|
||||
if (notePools[i].Count < noteTarget) allReached = false;
|
||||
|
||||
// start pool
|
||||
while (startNotePools[i].Count < startTarget && createdThisFrame < prewarmPerFrame)
|
||||
{
|
||||
if (startNotePrefabs != null && i < startNotePrefabs.Length && startNotePrefabs[i] != null)
|
||||
AddToPool(startNotePools[i], startNotePrefabs[i], startPoolContainer);
|
||||
createdThisFrame++;
|
||||
}
|
||||
if (startNotePools[i].Count < startTarget) allReached = false;
|
||||
|
||||
// hold middle pool
|
||||
while (holdNotePools[i].Count < holdTarget && createdThisFrame < prewarmPerFrame)
|
||||
{
|
||||
if (holdNotePrefabs != null && i < holdNotePrefabs.Length && holdNotePrefabs[i] != null)
|
||||
AddToPool(holdNotePools[i], holdNotePrefabs[i], holdPoolContainer);
|
||||
createdThisFrame++;
|
||||
}
|
||||
if (holdNotePools[i].Count < holdTarget) allReached = false;
|
||||
|
||||
// hold end pool
|
||||
while (holdNoteEndPools[i].Count < endTarget && createdThisFrame < prewarmPerFrame)
|
||||
{
|
||||
if (holdNoteEndPrefabs != null && i < holdNoteEndPrefabs.Length && holdNoteEndPrefabs[i] != null)
|
||||
AddToPool(holdNoteEndPools[i], holdNoteEndPrefabs[i], holdPoolContainer);
|
||||
else if (holdNotePrefabs != null && i < holdNotePrefabs.Length && holdNotePrefabs[i] != null)
|
||||
AddToPool(holdNoteEndPools[i], holdNotePrefabs[i], holdPoolContainer);
|
||||
createdThisFrame++;
|
||||
}
|
||||
if (holdNoteEndPools[i].Count < endTarget) allReached = false;
|
||||
|
||||
if (createdThisFrame >= prewarmPerFrame)
|
||||
break;
|
||||
}
|
||||
|
||||
if (allReached) break;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
prewarmCoroutine = null;
|
||||
}
|
||||
|
||||
private GameObject GetObjectFromPool(Stack<GameObject> pool, GameObject prefab, Transform container)
|
||||
{
|
||||
if (pool == null)
|
||||
return InstantiateAndPrepare(prefab);
|
||||
|
||||
if (pool.Count > 0)
|
||||
{
|
||||
GameObject obj = pool.Pop();
|
||||
if (obj == null)
|
||||
{
|
||||
// ������ж����ѱ����٣������¶���
|
||||
obj = InstantiateAndPrepare(prefab);
|
||||
}
|
||||
else
|
||||
{
|
||||
// �Ż���ʹ�� PoolItem ��ʶ������Ƿ�����ͬһ prefab
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi == null || prefab == null || pi.prefabName != prefab.name)
|
||||
{
|
||||
// ���ٴ������͵Ķ����滻Ϊ��ȷ�� prefab ʵ��
|
||||
if (pi == null && verboseLogging) Debug.LogWarning("PoolItem missing on pooled object, replacing.");
|
||||
Destroy(obj);
|
||||
obj = InstantiateAndPrepare(prefab);
|
||||
}
|
||||
}
|
||||
obj.transform.SetParent(null);
|
||||
obj.SetActive(true);
|
||||
|
||||
// mark as taken from pool
|
||||
GamePlay.PoolItem takenPi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (takenPi != null) takenPi.inPool = false;
|
||||
|
||||
return obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
// �ؿ�ʱֱ��ʵ�����¶���
|
||||
GameObject obj = InstantiateAndPrepare(prefab);
|
||||
obj.SetActive(true);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject InstantiateAndPrepare(GameObject prefab)
|
||||
{
|
||||
if (prefab == null) return null;
|
||||
GameObject obj = Instantiate(prefab);
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi == null) pi = obj.AddComponent<GamePlay.PoolItem>();
|
||||
pi.prefabName = prefab.name;
|
||||
pi.inPool = false;
|
||||
return obj;
|
||||
}
|
||||
|
||||
private void ReturnObjectToPool(Stack<GameObject> pool, GameObject obj, Transform container)
|
||||
{
|
||||
if (obj == null || pool == null) return; // ��ֹ������
|
||||
|
||||
// �����ظ��黹������������ڳ��ڣ�ֱ�Ӻ���
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||||
if (pi != null && pi.inPool) return;
|
||||
|
||||
// �������ƶ��������������ñ任
|
||||
obj.transform.SetParent(container, false);
|
||||
obj.transform.localPosition = Vector3.zero;
|
||||
obj.transform.localRotation = Quaternion.identity;
|
||||
obj.transform.localScale = Vector3.one;
|
||||
|
||||
// ��Ϊ�Ǽ���״̬�Ա㸴��
|
||||
obj.SetActive(false);
|
||||
|
||||
// ���Ϊ���ڳ���
|
||||
if (pi != null) pi.inPool = true;
|
||||
|
||||
if (pool.Count < maxPoolSize)
|
||||
{
|
||||
pool.Push(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ��������������ٶ�������Խ�ʡ�ڴ�
|
||||
Destroy(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToPool(Stack<GameObject> pool, GameObject prefab, Transform container)
|
||||
{
|
||||
if (prefab == null) return;
|
||||
if (pool.Count < maxPoolSize)
|
||||
{
|
||||
GameObject obj = Instantiate(prefab, container);
|
||||
// ensure PoolItem exists and is marked in pool
|
||||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>() ?? obj.AddComponent<GamePlay.PoolItem>();
|
||||
pi.prefabName = prefab.name;
|
||||
pi.inPool = true;
|
||||
|
||||
obj.SetActive(false);
|
||||
pool.Push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject GetNote(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(notePools[colorIndex], notePrefabs[colorIndex], notePoolContainer);
|
||||
}
|
||||
|
||||
public GameObject GetStartNote(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(startNotePools[colorIndex], startNotePrefabs[colorIndex], startPoolContainer);
|
||||
}
|
||||
|
||||
public GameObject GetHoldNoteSegment(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(holdNotePools[colorIndex], holdNotePrefabs[colorIndex], holdPoolContainer);
|
||||
}
|
||||
|
||||
// ��������β��ר�óػ�ȡβ��Ƭ�Σ����δ����β��Ԥ������Ϊ�жΣ�
|
||||
public GameObject GetHoldNoteEndSegment(string color)
|
||||
{
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
GameObject prefab = (holdNoteEndPrefabs != null && holdNoteEndPrefabs.Length > colorIndex && holdNoteEndPrefabs[colorIndex] != null)
|
||||
? holdNoteEndPrefabs[colorIndex]
|
||||
: holdNotePrefabs[colorIndex];
|
||||
return GetObjectFromPool(holdNoteEndPools[colorIndex], prefab, holdPoolContainer);
|
||||
}
|
||||
|
||||
public void ReturnNote(GameObject note, string color)
|
||||
{
|
||||
if (note == null) return;
|
||||
NoteController noteScript = note.GetComponent<NoteController>();
|
||||
if (noteScript != null)
|
||||
{
|
||||
noteScript.ResetState();
|
||||
}
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(notePools[idx], note, notePoolContainer);
|
||||
}
|
||||
|
||||
public void ReturnStartNote(GameObject startNote, string color)
|
||||
{
|
||||
if (startNote == null) return;
|
||||
if (string.IsNullOrEmpty(color))
|
||||
{
|
||||
if (verboseLogging) Debug.LogWarning($" �黹 StartNote ʱ color Ϊ�գ�������: {startNote.name}");
|
||||
Destroy(startNote);
|
||||
return;
|
||||
}
|
||||
|
||||
HoldNote holdNote = startNote.GetComponent<HoldNote>();
|
||||
if (holdNote != null)
|
||||
{
|
||||
holdNote.ResetState(); // ȷ����������״̬
|
||||
}
|
||||
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(startNotePools[idx], startNote, startPoolContainer);
|
||||
}
|
||||
|
||||
// �������黹β��Ƭ��
|
||||
public void ReturnHoldNoteEndSegment(GameObject holdNoteEnd, string color)
|
||||
{
|
||||
if (holdNoteEnd == null) return;
|
||||
if (string.IsNullOrEmpty(color))
|
||||
{
|
||||
if (verboseLogging) Debug.LogWarning($" �黹 HoldNoteEndSegment ʱ color Ϊ�գ�������: {holdNoteEnd.name}");
|
||||
Destroy(holdNoteEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
HoldNote holdNoteScript = holdNoteEnd.GetComponent<HoldNote>();
|
||||
if (holdNoteScript != null)
|
||||
{
|
||||
holdNoteScript.ResetState(); // ȷ����������״̬
|
||||
}
|
||||
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(holdNoteEndPools[idx], holdNoteEnd, holdPoolContainer);
|
||||
}
|
||||
|
||||
public void ReturnHoldNoteSegment(GameObject holdNote, string color)
|
||||
{
|
||||
if (holdNote == null) return;
|
||||
if (string.IsNullOrEmpty(color))
|
||||
{
|
||||
if (verboseLogging) Debug.LogWarning($" �黹 HoldNoteSegment ʱ color Ϊ�գ�������: {holdNote.name}");
|
||||
Destroy(holdNote);
|
||||
return;
|
||||
}
|
||||
|
||||
HoldNote holdNoteScript = holdNote.GetComponent<HoldNote>();
|
||||
if (holdNoteScript != null)
|
||||
{
|
||||
holdNoteScript.ResetState(); // ȷ����������״̬
|
||||
}
|
||||
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(holdNotePools[idx], holdNote, holdPoolContainer);
|
||||
}
|
||||
|
||||
private int GetColorIndexFromName(string colorName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(colorName)) return 0;
|
||||
if (colorIndexMap != null && colorIndexMap.TryGetValue(colorName, out int idx)) return idx;
|
||||
if (verboseLogging) Debug.LogError($"δʶ�����ɫ: {colorName}��Ĭ��ʹ�ú�ɫ");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class PoolItem : MonoBehaviour
|
||||
{
|
||||
public string prefabName;
|
||||
public bool inPool;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using TMPro;
|
||||
|
||||
@@ -29,6 +29,14 @@ public class NoteSpawner : MonoBehaviour
|
||||
[Range(0.8f, 1.25f)]
|
||||
public float speedMultiplier = 1f;
|
||||
|
||||
[Header("Calibration")]
|
||||
[Tooltip("Tolerance (world units) for snapping middle segments to expected position after spawn.")]
|
||||
public float calibrateTolerance = 0.02f;
|
||||
[Tooltip("How many calibration checks to perform after spawn (spread over frames).")]
|
||||
public int calibrateChecks = 2;
|
||||
[Tooltip("Interval (seconds realtime) between calibration checks.")]
|
||||
public float calibrateInterval = 0.01f;
|
||||
|
||||
// optional: constants for runtime clamping (kept for internal use)
|
||||
private const float SpeedMultiplierMin = 0.8f;
|
||||
private const float SpeedMultiplierMax = 1.25f;
|
||||
@@ -94,7 +102,15 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (GameConfig.verboseLogs) Debug.Log($"delay: {delay}");
|
||||
|
||||
if (delay > 0)
|
||||
yield return new WaitForSeconds(delay);
|
||||
{
|
||||
// Use scaled-time wait so spawning is paused while Time.timeScale==0 (PauseManager pause)
|
||||
float target = Time.time + delay;
|
||||
// Wait using frames so this loop respects Time.timeScale (Time.time won't advance when paused)
|
||||
while (Time.time < target)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (note.type == "hold")
|
||||
{
|
||||
@@ -263,6 +279,10 @@ public class NoteSpawner : MonoBehaviour
|
||||
float visualScaleMid = 0.9f * smLocalHold + 0.1f;
|
||||
holdSeg.ApplyVisualScale(visualScaleMid);
|
||||
holdSeg.visualSpeedMultiplier = smLocalHold;
|
||||
|
||||
// Immediately calibrate position and schedule additional checks to correct any offset
|
||||
holdSeg.CalibratePosition(spawnPoint.position, calibrateTolerance);
|
||||
StartCoroutine(CalibrateAfterSpawn(holdSeg, spawnPoint.position));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -298,6 +318,10 @@ public class NoteSpawner : MonoBehaviour
|
||||
float visualScaleEnd = 0.9f * smLocalHold + 0.1f;
|
||||
holdEnd.ApplyVisualScale(visualScaleEnd);
|
||||
holdEnd.visualSpeedMultiplier = smLocalHold;
|
||||
|
||||
// schedule calibration for end as well to be safe
|
||||
holdEnd.CalibratePosition(spawnPoint.position, calibrateTolerance);
|
||||
StartCoroutine(CalibrateAfterSpawn(holdEnd, spawnPoint.position));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -305,6 +329,16 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator CalibrateAfterSpawn(HoldNote seg, Vector3 spawnPos)
|
||||
{
|
||||
if (seg == null) yield break;
|
||||
for (int i = 0; i < Mathf.Max(1, calibrateChecks); i++)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(calibrateInterval);
|
||||
if (seg == null || !seg.gameObject.activeSelf) yield break;
|
||||
seg.CalibratePosition(spawnPos, calibrateTolerance);
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculateSpeed(float noteTravelTime)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user