add the connections and sendToTest.cs for connecting with the game test
This commit is contained in:
@@ -11,15 +11,24 @@ public class BeatmapManager : MonoBehaviour
|
||||
// 音符生成器引用
|
||||
public NoteSpawner noteSpawner;
|
||||
|
||||
|
||||
// Extra fields from beatmap JSON (stored temporarily)
|
||||
[HideInInspector] public string parsedTitle;
|
||||
[HideInInspector] public string parsedComposer;
|
||||
[HideInInspector] public string parsedIllustrator;
|
||||
[HideInInspector] public string parsedCharter;
|
||||
[HideInInspector] public string parsedBeatmapId;
|
||||
[HideInInspector] public string parsedDifficultyName;
|
||||
[HideInInspector] public string parsedCreatedDate;
|
||||
[HideInInspector] public string parsedLastSavedTime;
|
||||
[HideInInspector] public string parsedMusicFile;
|
||||
[HideInInspector] public string parsedBackgroundFile;
|
||||
|
||||
// 保存谱面数据到 JSON 文件
|
||||
//public void SaveBeatmap(string fileName)
|
||||
//{
|
||||
// string json = JsonUtility.ToJson(beatmap, true); // 格式化 JSON 输出
|
||||
// File.WriteAllText(Application.dataPath + "/" + fileName, json);
|
||||
// Debug.Log("谱面已保存:" + Application.dataPath + "/" + fileName);
|
||||
//}
|
||||
[HideInInspector] public int parsedDuration; // as int per request
|
||||
[HideInInspector] public int parsedBpm; // as int per request
|
||||
[HideInInspector] public int parsedDifficulty; // duplicate of beatmap.difficulty
|
||||
[HideInInspector] public int parsedNoteAmount;
|
||||
|
||||
[HideInInspector] public float globalDelaySeconds = 0f;
|
||||
|
||||
// 从 JSON 文件加载谱面数据
|
||||
public void LoadBeatmap(string fileName)
|
||||
@@ -28,92 +37,124 @@ public class BeatmapManager : MonoBehaviour
|
||||
if (File.Exists(path))
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
beatmap = JsonUtility.FromJson<Beatmap>(json);
|
||||
Debug.Log("谱面已加载:" + json);
|
||||
|
||||
// 在加载后立即调用音符生成
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
|
||||
Debug.Log("谱面 JSON 已读取: " + path);
|
||||
ProcessJsonAndLoad(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("文件不存在:" + path);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:从原始 JSON 字符串加载谱面(允许任意磁盘路径先读取后传入)
|
||||
public void LoadBeatmapFromJsonString(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
Debug.LogError("LoadBeatmapFromJsonString: json is null or empty");
|
||||
return;
|
||||
}
|
||||
ProcessJsonAndLoad(json);
|
||||
}
|
||||
|
||||
// 新增:解析 JSON 但不启动 NoteSpawner(用于 test mode 的延迟开始)
|
||||
public bool ParseJsonOnly(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly: json is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse main beatmap
|
||||
Beatmap parsed = null;
|
||||
try
|
||||
{
|
||||
parsed = JsonUtility.FromJson<Beatmap>(json);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"ParseJsonOnly 解析 Beatmap 失败: {ex}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsed == null)
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly: 解析 Beatmap 返回 null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse extras
|
||||
BeatmapExtra extra = null;
|
||||
try
|
||||
{
|
||||
extra = JsonUtility.FromJson<BeatmapExtra>(json);
|
||||
}
|
||||
catch { extra = null; }
|
||||
|
||||
if (extra != null)
|
||||
{
|
||||
parsedTitle = extra.title;
|
||||
parsedComposer = extra.composer;
|
||||
parsedIllustrator = extra.illustrator;
|
||||
parsedCharter = extra.charter;
|
||||
parsedBeatmapId = extra.beatmapId;
|
||||
parsedDifficultyName = extra.difficultyName;
|
||||
parsedCreatedDate = extra.createdDate;
|
||||
parsedLastSavedTime = extra.lastSavedTime;
|
||||
parsedMusicFile = extra.musicFile;
|
||||
parsedBackgroundFile = extra.backgroundFile;
|
||||
|
||||
parsedDuration = extra.duration;
|
||||
parsedBpm = extra.bpm;
|
||||
parsedDifficulty = extra.difficulty;
|
||||
parsedNoteAmount = extra.noteAmount;
|
||||
globalDelaySeconds = extra.globalDelaySeconds;
|
||||
|
||||
if (!string.IsNullOrEmpty(extra.musicFile)) parsed.musicFile = extra.musicFile;
|
||||
if (!string.IsNullOrEmpty(extra.backgroundFile)) parsed.backgroundFile = extra.backgroundFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedTitle = parsed.title;
|
||||
parsedComposer = parsed.composer;
|
||||
parsedIllustrator = parsed.illustrator;
|
||||
parsedCharter = parsed.charter;
|
||||
parsedBeatmapId = parsed.beatmapId;
|
||||
parsedDifficultyName = parsed.difficultyName;
|
||||
parsedCreatedDate = parsed.createdDate;
|
||||
parsedMusicFile = parsed.musicFile;
|
||||
parsedBackgroundFile = parsed.backgroundFile;
|
||||
|
||||
parsedDuration = Mathf.RoundToInt(parsed.duration);
|
||||
parsedBpm = Mathf.RoundToInt(parsed.bpm);
|
||||
parsedDifficulty = parsed.difficulty;
|
||||
}
|
||||
|
||||
// NOTE: Do NOT modify parsed.notes here. globalDelaySeconds will be applied to audio playback instead.
|
||||
|
||||
beatmap = parsed;
|
||||
Debug.Log("ParseJsonOnly: parsed beatmap " + beatmap.title);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 直接传入已经解析的 Beatmap(兼容旧调用)
|
||||
public void LoadBeatmap(Beatmap loadedBeatmap)
|
||||
{
|
||||
beatmap = loadedBeatmap;
|
||||
Debug.Log("谱面已加载:" + beatmap.title);
|
||||
|
||||
// 在加载后立即调用音符生成
|
||||
Debug.Log("谱面已加载(来自 Beatmap 对象):" + (beatmap != null ? beatmap.title : "null"));
|
||||
// No globalDelaySeconds available in this path. Call NoteSpawner directly.
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
}
|
||||
|
||||
//public void LoadBeatmapFromFile(string fileName)
|
||||
//{
|
||||
// string path = Application.persistentDataPath + "/" + fileName;
|
||||
// if (File.Exists(path))
|
||||
// {
|
||||
// string json = File.ReadAllText(path);
|
||||
// Beatmap beatmap = JsonUtility.FromJson<Beatmap>(json);
|
||||
// beatmapManager.LoadBeatmap(beatmap); // 使用 LoadBeatmap 方法传递 Beatmap 对象
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Debug.LogError("文件不存在:" + path);
|
||||
// }
|
||||
//}
|
||||
|
||||
// 创建一个示例谱面并保存
|
||||
//public void CreateSampleBeatmap()
|
||||
//{
|
||||
// beatmap = new Beatmap
|
||||
// {
|
||||
// title = "Moonlight Sonata",
|
||||
// composer = "Ludwig van Beethoven",
|
||||
// illustrator = "John Doe",
|
||||
// charter = "Jane Smith",
|
||||
// beatmapId = "ML-001",
|
||||
// duration = 120.5f,
|
||||
// bpm = 128f,
|
||||
// difficulty = 5,
|
||||
// difficultyName = "Hard",
|
||||
// createdDate = System.DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
// musicFile = "moonlight_sonata.mp3",
|
||||
// backgroundFile = "moonlight_art.png",
|
||||
|
||||
// tracks = new TrackData[]
|
||||
// {
|
||||
// new TrackData { color = "red" },
|
||||
// new TrackData { color = "green" },
|
||||
// new TrackData { color = "yellow" },
|
||||
// new TrackData { color = "purple" },
|
||||
// new TrackData { color = "blue" }
|
||||
// },
|
||||
|
||||
// notes = new NoteData[]
|
||||
// {
|
||||
// new NoteData { trackIndex = 0, time = 1.0f, color = "red", type = "tap", length = 0.0f },
|
||||
// new NoteData { trackIndex = 4, time = 2.0f, color = "blue", type = "tap", length = 0.0f },
|
||||
// new NoteData { trackIndex = 1, time = 3.5f, color = "green", type = "tap", length = 0.0f },
|
||||
// new NoteData { trackIndex = 2, time = 3.5f, color = "yellow", type = "tap", length = 0.0f },
|
||||
// new NoteData { trackIndex = 2, time = 3.5f, color = "yellow", type = "tap", length = 0.0f },
|
||||
// new NoteData { trackIndex = 3, time = 3.5f, color = "purple", type = "tap", length = 0.0f },
|
||||
// }
|
||||
// };
|
||||
|
||||
// SaveBeatmap("test_beatmap.json");
|
||||
//}
|
||||
|
||||
// 读取并加载当前谱面到音符生成器
|
||||
// 读取并加载当前谱面到音符生成器(备用)
|
||||
public void LoadBeatmapFromFile()
|
||||
{
|
||||
string path = Application.streamingAssetsPath + "/Emilia_demo.json";
|
||||
if (File.Exists(path))
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
beatmap = JsonUtility.FromJson<Beatmap>(json);
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
ProcessJsonAndLoad(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -124,8 +165,127 @@ public class BeatmapManager : MonoBehaviour
|
||||
// 在 Start() 方法中初始化
|
||||
void Start()
|
||||
{
|
||||
// 这里可以选择在启动时创建一个示例谱面并加载
|
||||
//CreateSampleBeatmap(); // 创建并保存一个示例谱面
|
||||
LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
|
||||
// 如果处于测试模式,则跳过自动加载谱面
|
||||
if (GameConfig.testMode)
|
||||
{
|
||||
Debug.Log("Test mode active: skipping automatic beatmap load in BeatmapManager.Start");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 加载默认谱面
|
||||
LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Helper class used to parse extra fields from JSON that Beatmap doesn't define
|
||||
[System.Serializable]
|
||||
private class BeatmapExtra
|
||||
{
|
||||
public string title;
|
||||
public string composer;
|
||||
public string illustrator;
|
||||
public string charter;
|
||||
public string beatmapId;
|
||||
public int duration; // as int per request
|
||||
public int bpm; // as int per request
|
||||
public int difficulty;
|
||||
public string difficultyName;
|
||||
public string createdDate;
|
||||
public string lastSavedTime;
|
||||
public string musicFile;
|
||||
public string backgroundFile;
|
||||
public int noteAmount;
|
||||
public float globalDelaySeconds;
|
||||
}
|
||||
|
||||
// Parse JSON string to Beatmap and extras, then pass to NoteSpawner
|
||||
private void ProcessJsonAndLoad(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
Debug.LogError("ProcessJsonAndLoad: json is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse main beatmap
|
||||
Beatmap parsed = null;
|
||||
try
|
||||
{
|
||||
parsed = JsonUtility.FromJson<Beatmap>(json);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"解析 Beatmap 失败: {ex}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed == null)
|
||||
{
|
||||
Debug.LogError("解析 Beatmap 返回 null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse extras using BeatmapExtra which contains the requested fields and types
|
||||
BeatmapExtra 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;
|
||||
parsedCharter = extra.charter;
|
||||
parsedBeatmapId = extra.beatmapId;
|
||||
parsedDifficultyName = extra.difficultyName;
|
||||
parsedCreatedDate = extra.createdDate;
|
||||
parsedLastSavedTime = extra.lastSavedTime;
|
||||
parsedMusicFile = extra.musicFile;
|
||||
parsedBackgroundFile = extra.backgroundFile;
|
||||
|
||||
parsedDuration = extra.duration;
|
||||
parsedBpm = extra.bpm;
|
||||
parsedDifficulty = extra.difficulty;
|
||||
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}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to populate from parsed Beatmap where possible
|
||||
parsedTitle = parsed.title;
|
||||
parsedComposer = parsed.composer;
|
||||
parsedIllustrator = parsed.illustrator;
|
||||
parsedCharter = parsed.charter;
|
||||
parsedBeatmapId = parsed.beatmapId;
|
||||
parsedDifficultyName = parsed.difficultyName;
|
||||
parsedCreatedDate = parsed.createdDate;
|
||||
parsedMusicFile = parsed.musicFile;
|
||||
parsedBackgroundFile = parsed.backgroundFile;
|
||||
|
||||
// convert types
|
||||
parsedDuration = Mathf.RoundToInt(parsed.duration);
|
||||
parsedBpm = Mathf.RoundToInt(parsed.bpm);
|
||||
parsedDifficulty = parsed.difficulty;
|
||||
|
||||
Debug.LogWarning("BeatmapExtra not found in JSON; populated limited fields from Beatmap.");
|
||||
}
|
||||
|
||||
// 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);
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,58 @@
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections;
|
||||
|
||||
public class GameManager : MonoBehaviour
|
||||
{
|
||||
public BeatmapManager beatmapManager; // 负责加载和管理谱面
|
||||
public NoteSpawner noteSpawner; // 音符生成器
|
||||
public AudioSource musicSource; // 音乐播放器
|
||||
[Header("paths")]
|
||||
// 变量声明不再从静态变量中读取
|
||||
public string JSONPath;
|
||||
public string audioPath;
|
||||
public string bgPicPath;
|
||||
|
||||
// Optional: sprite renderer to show background image
|
||||
public SpriteRenderer backgroundRenderer;
|
||||
|
||||
// NEW: UI Text to display banflag content from pressStart
|
||||
[Header("Debug / Test")]
|
||||
public Text banflagTextUI;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 订阅暂停管理器的事件
|
||||
if (PauseManager.Instance != null)
|
||||
{
|
||||
PauseManager.Instance.OnPauseStateChanged += HandlePauseStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// 取消订阅暂停管理器的事件
|
||||
if (PauseManager.Instance != null)
|
||||
{
|
||||
PauseManager.Instance.OnPauseStateChanged -= HandlePauseStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePauseStateChanged(bool isPaused)
|
||||
{
|
||||
if (musicSource != null)
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
musicSource.Pause();
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.UnPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
@@ -16,6 +63,34 @@ public class GameManager : MonoBehaviour
|
||||
if (noteSpawner == null) Debug.LogError("noteSpawner 未赋值!");
|
||||
if (musicSource == null) Debug.LogError("musicSource 未赋值!");
|
||||
|
||||
// 如果 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 下使用外部路径预加载资源并暂停系统,等待按空格继续
|
||||
if (GameConfig.testMode)
|
||||
{
|
||||
// 延迟赋值:在 Start() 中获取静态路径,此时 pressStart.cs 已经运行并更新了它们
|
||||
JSONPath = pressStart.testmode_beatmapJSON_path;
|
||||
audioPath = pressStart.testmode_audioFile_path;
|
||||
bgPicPath = pressStart.testmode_bgPicFile_path;
|
||||
|
||||
Debug.Log("Test mode active: using external paths to preload assets");
|
||||
|
||||
// also update banflag UI if content exists
|
||||
if (banflagTextUI != null && !string.IsNullOrEmpty(pressStart.banflagContent))
|
||||
{
|
||||
banflagTextUI.text = pressStart.banflagContent;
|
||||
}
|
||||
|
||||
StartCoroutine(HandleTestModeStartup());
|
||||
return;
|
||||
}
|
||||
|
||||
// 以下为原有流程(已保留注释,未改写)
|
||||
// 在游戏开始时加载并初始化谱面
|
||||
string beatmapFilePath = Path.Combine(Application.streamingAssetsPath, "Emilia_demo.json");
|
||||
|
||||
@@ -26,6 +101,7 @@ public class GameManager : MonoBehaviour
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(beatmapFilePath);
|
||||
// 假设 Beatmap 是一个已定义的类
|
||||
Beatmap beatmap = JsonUtility.FromJson<Beatmap>(json); // 解析 JSON 为 Beatmap 对象
|
||||
|
||||
if (beatmap == null)
|
||||
@@ -37,7 +113,7 @@ public class GameManager : MonoBehaviour
|
||||
beatmapManager.LoadBeatmap(beatmap); // 传递给 BeatmapManager
|
||||
noteSpawner.LoadBeatmap(beatmap); // 传递给 NoteSpawner
|
||||
|
||||
// 播放背景音乐
|
||||
// 播放背景音乐(延迟播放由 beatmapManager.globalDelaySeconds 决定)
|
||||
if (!string.IsNullOrEmpty(beatmap.musicFile))
|
||||
{
|
||||
AudioClip musicClip = Resources.Load<AudioClip>(beatmap.musicFile);
|
||||
@@ -48,7 +124,16 @@ public class GameManager : MonoBehaviour
|
||||
else
|
||||
{
|
||||
musicSource.clip = musicClip;
|
||||
musicSource.Play();
|
||||
float delay = beatmapManager != null ? beatmapManager.globalDelaySeconds : 0f;
|
||||
if (delay > 0f)
|
||||
{
|
||||
musicSource.PlayDelayed(delay);
|
||||
Debug.Log($"Scheduled music to play after {delay} seconds.");
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -56,4 +141,195 @@ public class GameManager : MonoBehaviour
|
||||
Debug.LogError("谱面中 musicFile 为空!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator HandleTestModeStartup()
|
||||
{
|
||||
// Preload audio from audioPath (absolute)
|
||||
if (!string.IsNullOrEmpty(audioPath))
|
||||
{
|
||||
if (!File.Exists(audioPath))
|
||||
{
|
||||
Debug.LogError($"TestMode audio file not found: {audioPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 注意:这里仍然使用过时的 WWW,建议替换为 UnityWebRequest
|
||||
string url = "file://" + audioPath;
|
||||
using (var www = new WWW(url))
|
||||
{
|
||||
yield return www;
|
||||
if (!string.IsNullOrEmpty(www.error))
|
||||
{
|
||||
Debug.LogError($"加载测试模式音频失败: {www.error}");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
AudioClip clip = www.GetAudioClip(false, false);
|
||||
if (clip == null)
|
||||
{
|
||||
Debug.LogError("从文件获取 AudioClip 失败");
|
||||
}
|
||||
else
|
||||
{
|
||||
musicSource.clip = clip;
|
||||
musicSource.loop = false;
|
||||
Debug.Log("TestMode audio loaded into AudioSource");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"将音频设置到 AudioSource 时出错: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("TestMode audioPath 未配置。");
|
||||
}
|
||||
|
||||
// Preload background image into sprite renderer
|
||||
if (backgroundRenderer != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(bgPicPath))
|
||||
{
|
||||
if (!File.Exists(bgPicPath))
|
||||
{
|
||||
Debug.LogError($"TestMode background image not found: {bgPicPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] imgBytes = null;
|
||||
try
|
||||
{
|
||||
imgBytes = File.ReadAllBytes(bgPicPath);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"读取背景图片文件出错: {ex}");
|
||||
}
|
||||
|
||||
if (imgBytes != null)
|
||||
{
|
||||
Texture2D tex = new Texture2D(2, 2);
|
||||
if (tex.LoadImage(imgBytes))
|
||||
{
|
||||
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)】
|
||||
Color color = backgroundRenderer.color;
|
||||
color.a = 1.0f;
|
||||
backgroundRenderer.color = color;
|
||||
|
||||
// 保持比例不变的最大适应由场景布局决定,记录 assignment
|
||||
Debug.Log("Background sprite assigned (ensure your SpriteRenderer size fits scene)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"无法从字节创建 Texture2D: {bgPicPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("TestMode bgPicPath 未配置。");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("backgroundRenderer 未配置,跳过背景加载。");
|
||||
}
|
||||
|
||||
// Read JSON from JSONPath and parse via BeatmapManager.ParseJsonOnly
|
||||
if (!string.IsNullOrEmpty(JSONPath))
|
||||
{
|
||||
if (!File.Exists(JSONPath))
|
||||
{
|
||||
Debug.LogError($"TestMode beatmap JSON not found: {JSONPath}");
|
||||
yield break; // abort
|
||||
}
|
||||
|
||||
string jsonContent = null;
|
||||
try
|
||||
{
|
||||
jsonContent = File.ReadAllText(JSONPath);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"读取 TestMode JSON 文件出错: {ex}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
bool parsed = beatmapManager.ParseJsonOnly(jsonContent);
|
||||
if (!parsed)
|
||||
{
|
||||
Debug.LogError("ParseJsonOnly 失败,停止启动测试模式。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Do not start spawning yet. We will start after unpausing.
|
||||
Debug.Log("Beatmap parsed (deferred start). Ready to unpause.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("TestMode JSONPath 未配置。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Pause the system until Space pressed
|
||||
// Use PauseManager instead of directly setting Time.timeScale
|
||||
PauseManager.Instance?.Pause(true);
|
||||
UpdateStatusOnConsole("Paused: press Space to start playback");
|
||||
|
||||
// Wait for space key down (use unscaled input loop)
|
||||
while (!Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Resume using PauseManager
|
||||
PauseManager.Instance?.Pause(false);
|
||||
|
||||
// Start spawning using the parsed beatmap
|
||||
if (beatmapManager.beatmap != null)
|
||||
{
|
||||
beatmapManager.LoadBeatmap(beatmapManager.beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
UpdateStatusOnConsole("Playback started");
|
||||
}
|
||||
|
||||
private void UpdateStatusOnConsole(string message)
|
||||
{
|
||||
Debug.Log(message);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,7 @@ public class HoldNote : BaseNote
|
||||
this.noteID = id.ToString();
|
||||
this.keyToPress = key;
|
||||
this.noteColor = color;
|
||||
// hitTime now uses real time base passed in as 'time' plus delay
|
||||
this.hitTime = time + delay;
|
||||
this.segment = (delay == 0f) ? NoteSegment.Start :
|
||||
(isEnd ? NoteSegment.End : NoteSegment.Middle);
|
||||
@@ -117,6 +118,9 @@ public class HoldNote : BaseNote
|
||||
else
|
||||
Debug.Log($"HoldNoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
|
||||
|
||||
// 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}");
|
||||
|
||||
// 在 Setup 时注册初始状态
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, false);
|
||||
if (segment == NoteSegment.Start)
|
||||
@@ -134,82 +138,80 @@ public class HoldNote : BaseNote
|
||||
// 如果当前片段已经判定过,直接返回
|
||||
if (isJudged) return;
|
||||
|
||||
// 自动 Miss 判定:当音符经过判定线且未被判定时
|
||||
if (hasEnteredLine && !isJudged)
|
||||
{
|
||||
if (segment == NoteSegment.Start && Time.time > hitTime + judgeConfig?.missRange)
|
||||
{
|
||||
Debug.Log($"[HoldNote] START段超时自动Miss: {noteColor}");
|
||||
HandleStart();
|
||||
isJudged = true;
|
||||
}
|
||||
else if (segment == NoteSegment.End && Time.time > scheduledEndTime + (judgeConfig?.missRange ?? 0.3f))
|
||||
{
|
||||
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
|
||||
HandleEnd(true);
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// **通用长按状态更新:**
|
||||
// 只有当isHoldActive为true时才检查松开操作,避免误判
|
||||
// 且仅在GetKeyUp时更新状态,避免重复操作
|
||||
if (isHoldActive && Input.GetKeyUp(keyToPress))
|
||||
{
|
||||
isHoldActive = false;
|
||||
Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}");
|
||||
// 如果是中间段或结束段,且因为松手导致长按失效,可以立即回收
|
||||
if (segment == NoteSegment.Middle || segment == NoteSegment.End)
|
||||
|
||||
// 当玩家松开按键时,向 JudgeManager 注册该长音符已被释放,确保尾部能正确检查释放状态
|
||||
if (!hasReleased)
|
||||
{
|
||||
// 注意:End段的判定在HandleEnd里,松手只是让isHoldActive为false,不代表End段判定已完成
|
||||
// 如果 End 段的判定窗口还没过,松手后 isHoldActive = false,End 段在 OnTriggerExit2D 才会回收
|
||||
// 或者在 HandleEnd 之前松手导致长按失效,HandleEnd 内部会判定 Miss
|
||||
hasReleased = true;
|
||||
releaseTime = Time.time;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
Debug.Log($"[HoldNote] RegisterNoteReleased on KeyUp. NoteID: {noteID}, releaseTime={releaseTime:F2}");
|
||||
}
|
||||
}
|
||||
// 如果按键被按下,且Start段已经判定成功,则激活长按状态
|
||||
// 考虑到玩家可能在Start段通过后瞬间松手再按下,这里需要确保只要Start段判定成功且按键按住,isHoldActive就为true
|
||||
|
||||
if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress))
|
||||
{
|
||||
isHoldActive = true;
|
||||
}
|
||||
|
||||
|
||||
// Start段判定
|
||||
if (segment == NoteSegment.Start && hasEnteredLine)
|
||||
{
|
||||
// 在判定窗口内按下按键才触发判定
|
||||
if (Input.GetKeyDown(keyToPress))
|
||||
{
|
||||
HandleStart();
|
||||
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
|
||||
}
|
||||
// 如果Start段进入判定区但玩家未按键,且已经错过最佳判定时机,可以考虑Miss并回收
|
||||
// 这部分逻辑现在放在 OnTriggerExit2D 里处理更合适
|
||||
}
|
||||
// Middle段判定:
|
||||
// 只有当头部已判定、且长按有效(按键持续按住),并且已经到达该Middle段的命中时间点时,才认为该Middle段通过。
|
||||
// Middle段本身没有独立的按键输入判定,它只是长按的延续。
|
||||
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
|
||||
else if (segment == NoteSegment.Middle
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID) // 头部必须已判定
|
||||
&& isHoldActive // 必须处于长按有效状态(按键持续按住)
|
||||
&& !JudgeManager.Instance.HasNoteReleased(noteID)) // 确保End段还未被判定释放
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID)
|
||||
&& isHoldActive
|
||||
&& !JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
{
|
||||
// Debug.Log($"[HoldNote] Middle段状态检查: Time.time={Time.time:F2}, hitTime={hitTime:F2}, hasEnteredLine={hasEnteredLine}");
|
||||
if (Time.time >= hitTime)
|
||||
{
|
||||
// 确保它进入了判定线,但在Update中,即使没完全进入,只要时间到了且条件满足,也算通过
|
||||
// 但是,我们主要希望它在通过判定线后被回收
|
||||
// 因此,Middle段的回收逻辑更多依赖于DelayedAutoReturnCheck或OnTriggerExit2D
|
||||
if (hasEnteredLine) // 确保已经进入过判定线,防止音符提前消失
|
||||
if (hasEnteredLine)
|
||||
{
|
||||
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
|
||||
PlayHitAnimation();
|
||||
// 把通过事件上报给 JudgeManager(记录中段通过)
|
||||
JudgeManager.Instance?.RegisterMiddlePassed(noteID);
|
||||
ReturnToPool();
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// End段判定
|
||||
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
|
||||
else if (segment == NoteSegment.End
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID) // 头部必须已判定
|
||||
&& !JudgeManager.Instance.HasNoteReleased(noteID)) // 确保还未被判定释放
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID)
|
||||
&& !JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
{
|
||||
// 只有当玩家松开按键时才触发End段判定
|
||||
if (Input.GetKeyUp(keyToPress))
|
||||
{
|
||||
HandleEnd();
|
||||
isJudged = true; // 标记为已判定
|
||||
}
|
||||
// 如果 End 段已经达到或超过了预定的释放时间 (scheduledEndTime),
|
||||
// 但玩家仍未松手,且长按状态仍然有效,这可能是玩家按得过长
|
||||
// 这种情况应该给予一个差的判定或者Miss
|
||||
else if (Time.time >= scheduledEndTime + 0.1f && isHoldActive) // 给予一点容错时间
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段超时未松手,判定为Miss: {noteColor}");
|
||||
HandleEnd(true); // 传入true表示强制Miss
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
@@ -219,28 +221,31 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (!collision.CompareTag("JudgmentLine")) return;
|
||||
|
||||
hasEnteredLine = true; // 只要进入判定线,就标记
|
||||
hasEnteredLine = true;
|
||||
|
||||
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 (autoReturnCoroutine != null)
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
|
||||
// Middle段的延迟回收主要确保即使按住也能在适当时间回收
|
||||
// 重新思考这里的必要性,可能让Update和OnTriggerExit2D处理更直接
|
||||
// 暂时移除这里的 DelayedAutoReturnCheck,让 Update 和 OnTriggerExit2D 主导 Middle 段的生命周期
|
||||
// autoReturnCoroutine = StartCoroutine(DelayedAutoReturnCheck());
|
||||
}
|
||||
else if (segment == NoteSegment.End)
|
||||
{
|
||||
if (autoReturnCoroutine != null)
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
|
||||
// End段的延迟回收主要用于处理玩家是否松手,以及超时回收
|
||||
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isJudged)
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
|
||||
HandleEnd();
|
||||
isJudged = true;
|
||||
// ReturnToPool handled inside HandleEnd
|
||||
return;
|
||||
}
|
||||
|
||||
autoReturnCoroutine = StartCoroutine(DelayedAutoReturnCheck());
|
||||
}
|
||||
}
|
||||
@@ -249,44 +254,60 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (!collision.CompareTag("JudgmentLine")) return;
|
||||
|
||||
// Start段离开判定线时如果未被判定,则视为Miss并回收
|
||||
if (segment == NoteSegment.Start)
|
||||
{
|
||||
if (!JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] Start段离开判定线未判定,强制回收 (Miss): {noteColor}");
|
||||
// 可以触发一个Miss判定
|
||||
// JudgeManager.Instance.RecordMiss(noteID);
|
||||
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
|
||||
// 确保未判定的 Start 段在离开判定线时登记为 Miss
|
||||
HandleStart();
|
||||
isJudged = true;
|
||||
ReturnToPool();
|
||||
}
|
||||
// 如果Start段已判定,那么它会通过DelayedReturn被回收,这里不做额外处理
|
||||
}
|
||||
// Middle段离开判定线时,如果头部未判定,或者长按已经失效(松手),则回收
|
||||
else if (segment == NoteSegment.Middle) // 这里修正了拼写错误
|
||||
else if (segment == NoteSegment.Middle)
|
||||
{
|
||||
// 无论Start段是否判定,如果Middle段离开了判定线,就应该回收
|
||||
// 因为它已经完成了它的“通过”或者“错过”的职责
|
||||
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
|
||||
ReturnToPool();
|
||||
}
|
||||
// End段离开判定线时,如果头部未判定,或者长按已经失效,或者 End 段已经完成判定,则回收
|
||||
else if (segment == NoteSegment.End)
|
||||
{
|
||||
if (!isJudged) // 如果End段还没有被判定
|
||||
if (autoReturnCoroutine != null)
|
||||
{
|
||||
// 如果头部没判,或者长按中断,或者玩家按得过久(已经过了scheduledEndTime很多了)
|
||||
if (!JudgeManager.Instance.IsStartJudged(noteID) || !isHoldActive || Time.time > scheduledEndTime + 0.15f)
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
autoReturnCoroutine = null;
|
||||
}
|
||||
|
||||
// 在尾部离开判定线时,按如下规则处理:
|
||||
// - 如果头部未判定:补偿 Miss 并回收
|
||||
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
|
||||
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
|
||||
if (!isJudged)
|
||||
{
|
||||
if (!JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段离开判定线强制回收 (头部未判定或长按中断或超时): {noteColor}");
|
||||
// 可以在这里强制一个Miss判定,如果之前没触发过
|
||||
if (!JudgeManager.Instance.HasNoteReleased(noteID)) // 确保没有重复判定
|
||||
{
|
||||
HandleEnd(true); // 传入true表示强制Miss
|
||||
}
|
||||
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
|
||||
if (!JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
HandleEnd(true);
|
||||
ReturnToPool();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
|
||||
HandleEnd();
|
||||
isJudged = true;
|
||||
ReturnToPool();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定
|
||||
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else // 如果End段已经被判定过了,直接回收
|
||||
else
|
||||
{
|
||||
ReturnToPool();
|
||||
}
|
||||
@@ -295,33 +316,19 @@ public class HoldNote : BaseNote
|
||||
|
||||
private IEnumerator DelayedAutoReturnCheck()
|
||||
{
|
||||
// 这里的 DelayedAutoReturnCheck 主要是针对 End 段的超时未松手检查
|
||||
// 或者 Start 段的延迟补判(这部分已经移到 Update 和 HandleStart)
|
||||
yield return null;
|
||||
|
||||
if (segment == NoteSegment.End)
|
||||
{
|
||||
// 在这里等待,看玩家是否在 scheduledEndTime 之前松手
|
||||
// 如果到达 scheduledEndTime 玩家仍未松手,且长按有效,则继续等待
|
||||
// 如果玩家按得过长,可以在 Update 中触发 Miss 判定
|
||||
float timeToWait = Mathf.Max(0f, scheduledEndTime - Time.time);
|
||||
if (timeToWait > 0f)
|
||||
{
|
||||
yield return new WaitForSeconds(timeToWait);
|
||||
}
|
||||
|
||||
// 到达 scheduledEndTime 后再次检查
|
||||
// 如果此时玩家仍在按住,并且 Start 段已判定,但 End 段还没被判定释放
|
||||
// 说明玩家按得太久,判定为 Miss
|
||||
if (!isJudged && JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress))
|
||||
{
|
||||
Debug.Log($"[HoldNote] DelayedAutoReturnCheck (End段) 判定:玩家按得过长,Miss! {noteColor}");
|
||||
HandleEnd(true); // 强制 Miss
|
||||
}
|
||||
// 无论如何,如果 End 段已经过去了,但还没回收,就回收
|
||||
if (!gameObject.activeSelf)
|
||||
{
|
||||
yield break; // 修正!使用 yield break 来结束协程
|
||||
yield break;
|
||||
}
|
||||
ReturnToPool();
|
||||
}
|
||||
@@ -348,14 +355,13 @@ public class HoldNote : BaseNote
|
||||
isHoldActive = false; // Start Miss,长按状态不激活
|
||||
}
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
|
||||
// Start段判定后立即回收,因为它的视觉部分可能只需要短暂显示
|
||||
StartCoroutine(DelayedReturn());
|
||||
}
|
||||
|
||||
private IEnumerator DelayedReturn()
|
||||
{
|
||||
yield return new WaitForSeconds(0.05f); // 短暂延迟,确保视觉效果或数据更新
|
||||
if (gameObject.activeSelf) // 确保对象还活跃,防止重复回收
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
ReturnToPool();
|
||||
}
|
||||
@@ -363,11 +369,28 @@ public class HoldNote : BaseNote
|
||||
|
||||
private void HandleEnd(bool forceMiss = false)
|
||||
{
|
||||
// 记录释放时间并在 JudgeManager 中登记已释放状态
|
||||
if (!hasReleased)
|
||||
{
|
||||
releaseTime = Time.time;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
}
|
||||
|
||||
string result;
|
||||
|
||||
// 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 Miss
|
||||
if (!hasEnteredLine && !forceMiss)
|
||||
{
|
||||
Debug.Log($"[HoldNote] HandleEnd: 提前松手,记录释放但等待尾部进入判定区再判定. NoteID={noteID}, color={noteColor}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 现在进行正常判定逻辑
|
||||
releaseTime = Time.time;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance.RegisterNoteReleased(noteID, true);
|
||||
string result;
|
||||
// 只要头部判定通过,松手时机在判定区间内就给出对应判定
|
||||
|
||||
if (forceMiss || !JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
result = "Miss";
|
||||
@@ -418,6 +441,7 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
|
||||
teamUIController.Instance?.OnJudgeResult(result); // 新增:更新combo计数
|
||||
Debug.Log($"[HoldNote] END判定结果: {noteColor} {result} (release={releaseTime:F2}, target={scheduledEndTime:F2})");
|
||||
ReturnToPool();
|
||||
}
|
||||
@@ -430,27 +454,21 @@ public class HoldNote : BaseNote
|
||||
autoReturnCoroutine = null;
|
||||
}
|
||||
|
||||
// OnDisable时对End段的补偿处理:
|
||||
// 如果是End段,且尚未被判定,且Start段已判定,但此时按键已经松开 (通过 !Input.GetKey(keyToPress) 判断)
|
||||
// 这意味着玩家在离开判定区或因其他原因导致 OnDisable 时松手,但未触发正常的 HandleEnd
|
||||
if (segment == NoteSegment.End &&
|
||||
!isJudged && // 尚未判定
|
||||
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
|
||||
{
|
||||
// 如果按键已经松开,或者长按状态已经失效 (可能因为松开过快)
|
||||
if (!Input.GetKey(keyToPress) || !isHoldActive)
|
||||
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isHoldActive)
|
||||
{
|
||||
Debug.Log($"[HoldNote] OnDisable补偿End段Miss判定: {noteColor}");
|
||||
HandleEnd(true); // 强制Miss
|
||||
isJudged = true; // 标记已判定
|
||||
}
|
||||
else if (Input.GetKey(keyToPress))
|
||||
{
|
||||
// 如果End段离开了判定区,但是玩家还在按住,这属于按得过长
|
||||
Debug.Log($"[HoldNote] OnDisable:End段在离开判定区时玩家仍未松手,判定为Miss: {noteColor}");
|
||||
HandleEnd(true); // 强制Miss
|
||||
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
|
||||
HandleEnd(); // 正常判定(基于 releaseTime)
|
||||
isJudged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[HoldNote] OnDisable:End段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
|
||||
}
|
||||
}
|
||||
|
||||
controller?.StopMovement();
|
||||
@@ -470,6 +488,8 @@ public class HoldNote : BaseNote
|
||||
|
||||
if (segment == NoteSegment.Start)
|
||||
NotePool.Instance.ReturnStartNote(gameObject, noteColor);
|
||||
else if (segment == NoteSegment.End)
|
||||
NotePool.Instance.ReturnHoldNoteEndSegment(gameObject, noteColor);
|
||||
else
|
||||
NotePool.Instance.ReturnHoldNoteSegment(gameObject, noteColor);
|
||||
}
|
||||
@@ -487,7 +507,6 @@ public class HoldNote : BaseNote
|
||||
isJudged = false;
|
||||
isHoldActive = false; // 重置长按标记
|
||||
hasBeenHeldFromStart = false; // 重置
|
||||
// canJudgeThisSegment = false; // 此变量已被移除或不再关键
|
||||
|
||||
if (autoReturnCoroutine != null)
|
||||
{
|
||||
|
||||
@@ -11,10 +11,21 @@ public class InputManager : MonoBehaviour
|
||||
[Header("轨道判定显示TMP对象(请在Inspector中连接)")]
|
||||
public TextMeshProUGUI[] trackJudgeTexts = new TextMeshProUGUI[5];
|
||||
|
||||
[Header("按键指示文本(独立于判定文本,五个分别对应按键:D,F,Space,J,K)")]
|
||||
public TextMeshProUGUI[] trackKeyTexts = new TextMeshProUGUI[5];
|
||||
|
||||
[Header("是否显示判定文字")]
|
||||
public bool showJudgeText = true;
|
||||
|
||||
// 按键激活/非激活颜色
|
||||
[Header("按键高亮颜色设置")]
|
||||
[Tooltip("按键被按下时的颜色")]
|
||||
public Color keyActiveColor = Color.yellow;
|
||||
[Tooltip("按键未按下时的颜色(默认黑色)")]
|
||||
public Color keyInactiveColor = Color.black;
|
||||
|
||||
// 判定结果对应颜色
|
||||
[Header("判定结果对应颜色")]
|
||||
public Color perfectColor = Color.yellow;
|
||||
public Color greatColor = Color.green;
|
||||
public Color goodColor = Color.cyan;
|
||||
@@ -26,6 +37,15 @@ public class InputManager : MonoBehaviour
|
||||
Instance = this;
|
||||
else
|
||||
Destroy(gameObject);
|
||||
|
||||
// 初始化按键文本为非激活颜色
|
||||
if (trackKeyTexts != null)
|
||||
{
|
||||
foreach (var t in trackKeyTexts)
|
||||
{
|
||||
if (t != null) t.color = keyInactiveColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -35,14 +55,48 @@ public class InputManager : MonoBehaviour
|
||||
KeyCode key = KeyBindingManager.GetKeyForColor(color);
|
||||
if (key == KeyCode.None) continue;
|
||||
|
||||
int index = GetIndexForColor(color);
|
||||
|
||||
if (Input.GetKeyDown(key))
|
||||
{
|
||||
OnKeyPressed?.Invoke(key);
|
||||
// 调用 JudgeManager 统一判断最早的音符
|
||||
JudgeManager.Instance.JudgeEarliestNote(key);
|
||||
|
||||
// 按下时设置对应的按键文本颜色为激活色(如果已设置)
|
||||
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
|
||||
{
|
||||
var txt = trackKeyTexts[index];
|
||||
if (txt != null)
|
||||
txt.color = keyActiveColor;
|
||||
}
|
||||
}
|
||||
if (Input.GetKeyUp(key))
|
||||
{
|
||||
OnKeyReleased?.Invoke(key);
|
||||
|
||||
// 松开时恢复为非激活颜色
|
||||
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
|
||||
{
|
||||
var txt = trackKeyTexts[index];
|
||||
if (txt != null)
|
||||
txt.color = keyInactiveColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int GetIndexForColor(string color)
|
||||
{
|
||||
// 保持与 trackJudgeTexts 相同的索引映射
|
||||
switch (color)
|
||||
{
|
||||
case "red": return 0;
|
||||
case "green": return 1;
|
||||
case "yellow": return 2;
|
||||
case "purple": return 3;
|
||||
case "blue": return 4;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ public class JudgeManager : MonoBehaviour
|
||||
// 记录 End 段的 scheduledEndTime(防止 End 被回收后仍然能判定)
|
||||
private Dictionary<string, float> noteEndTimes = new Dictionary<string, float>();
|
||||
|
||||
// 记录每条长音符中段通过的计数
|
||||
private Dictionary<string, int> middlePassedCounts = new Dictionary<string, int>();
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -28,17 +31,17 @@ public class JudgeManager : MonoBehaviour
|
||||
/// <summary>
|
||||
/// 记录某个颜色的长音符 End 段的 scheduledEndTime
|
||||
/// </summary>
|
||||
public void RegisterScheduledEndTime(string noteColor, float endTime)
|
||||
public void RegisterScheduledEndTime(string noteID, float endTime)
|
||||
{
|
||||
noteEndTimes[noteColor] = endTime;
|
||||
noteEndTimes[noteID] = endTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个颜色的 End 段的 scheduledEndTime
|
||||
/// </summary>
|
||||
public float GetScheduledEndTime(string noteColor)
|
||||
public float GetScheduledEndTime(string noteID)
|
||||
{
|
||||
return noteEndTimes.ContainsKey(noteColor) ? noteEndTimes[noteColor] : 0f;
|
||||
return noteEndTimes.ContainsKey(noteID) ? noteEndTimes[noteID] : 0f;
|
||||
}
|
||||
|
||||
public void RegisterStartJudged(string noteID, bool state)
|
||||
@@ -117,4 +120,28 @@ public class JudgeManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录中段通过,用于调试或进一步判定策略
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
if (startJudgedNotes.ContainsKey(noteID)) startJudgedNotes.Remove(noteID);
|
||||
if (releasedNotes.ContainsKey(noteID)) releasedNotes.Remove(noteID);
|
||||
if (noteEndTimes.ContainsKey(noteID)) noteEndTimes.Remove(noteID);
|
||||
if (middlePassedCounts.ContainsKey(noteID)) middlePassedCounts.Remove(noteID);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ public class Notes : MonoBehaviour
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 过了 Miss 判定时间,销毁音符
|
||||
if (!isHit && Time.timeSinceLevelLoad > hitTime + 0.3f)
|
||||
// 只有进入判定区后,才允许自动miss
|
||||
if (canBeJudged && !isHit && Time.timeSinceLevelLoad > hitTime + 0.3f)
|
||||
{
|
||||
JudgeMiss();
|
||||
}
|
||||
@@ -57,31 +57,44 @@ public class Notes : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void Recycle()
|
||||
{
|
||||
// 如果存在 NotePool,优先归还池,否则销毁物体
|
||||
if (NotePool.Instance != null)
|
||||
{
|
||||
NotePool.Instance.ReturnNote(gameObject, "red"); // color 未保存时默认 red,建议上层调用时传入
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void JudgePerfect()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Perfect!");
|
||||
isHit = true;
|
||||
Destroy(gameObject);
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeGreat()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Great!");
|
||||
isHit = true;
|
||||
Destroy(gameObject);
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeGood()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Good!");
|
||||
isHit = true;
|
||||
Destroy(gameObject);
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeMiss()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Miss!");
|
||||
isHit = true;
|
||||
Destroy(gameObject);
|
||||
Recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ public class Note : BaseNote
|
||||
if (!string.IsNullOrEmpty(judgeResult))
|
||||
{
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult);
|
||||
teamUIController.Instance?.OnJudgeResult(judgeResult); // 新增:更新combo计数
|
||||
}
|
||||
Judge();
|
||||
}
|
||||
@@ -149,7 +150,10 @@ public class Note : BaseNote
|
||||
if (isJudged) return;
|
||||
isJudged = true;
|
||||
Debug.Log($"{keyToPress} Miss");
|
||||
// InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss"); // 已在HandlePress中调用
|
||||
// Ensure UI shows Miss and combo is updated when a note auto-misses
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
// InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss"); // 已在HandlePress中调用 for manual presses
|
||||
ReturnToPool();
|
||||
}
|
||||
|
||||
@@ -182,6 +186,9 @@ public class Note : BaseNote
|
||||
// 离开判定区且未判定则判 Miss
|
||||
if (!inZone && !isJudged)
|
||||
{
|
||||
// Show Miss and update combo
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using GamePlay; // for PoolItem
|
||||
|
||||
public class NotePool : MonoBehaviour
|
||||
{
|
||||
@@ -7,14 +8,27 @@ public class NotePool : MonoBehaviour
|
||||
|
||||
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)
|
||||
@@ -27,79 +41,152 @@ public class NotePool : MonoBehaviour
|
||||
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>>();
|
||||
|
||||
for (int i = 0; i < notePrefabs.Length; i++)
|
||||
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]);
|
||||
AddToPool(startNotePools[i], startNotePrefabs[i]);
|
||||
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]);
|
||||
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)
|
||||
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
|
||||
{
|
||||
if (pool.Count < maxPoolSize)
|
||||
{
|
||||
GameObject obj = Instantiate(prefab);
|
||||
obj.SetActive(true);
|
||||
return obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("对象池已满,生成新的音符!");
|
||||
GameObject obj = Instantiate(prefab);
|
||||
obj.SetActive(true);
|
||||
return obj;
|
||||
}
|
||||
// 池空时直接实例化新对象
|
||||
GameObject obj = InstantiateAndPrepare(prefab);
|
||||
obj.SetActive(true);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReturnObjectToPool(Stack<GameObject> pool, GameObject obj)
|
||||
private GameObject InstantiateAndPrepare(GameObject prefab)
|
||||
{
|
||||
if (obj == null || pool.Contains(obj)) return; // 防止重复回收
|
||||
|
||||
if (obj != null)
|
||||
{
|
||||
obj.SetActive(false); // 设置为非活动状态
|
||||
if (pool.Count < maxPoolSize)
|
||||
{
|
||||
pool.Push(obj);
|
||||
Debug.Log($"已归还对象:{obj.name},当前池容量:{pool.Count}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("对象池已满,无法继续归还对象!");
|
||||
}
|
||||
}
|
||||
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 AddToPool(Stack<GameObject> pool, GameObject prefab)
|
||||
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)
|
||||
{
|
||||
GameObject obj = Instantiate(prefab);
|
||||
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);
|
||||
}
|
||||
@@ -107,41 +194,51 @@ public class NotePool : MonoBehaviour
|
||||
|
||||
public GameObject GetNote(string color)
|
||||
{
|
||||
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(notePools[colorIndex], notePrefabs[colorIndex]);
|
||||
return GetObjectFromPool(notePools[colorIndex], notePrefabs[colorIndex], notePoolContainer);
|
||||
}
|
||||
|
||||
public GameObject GetStartNote(string color)
|
||||
{
|
||||
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(startNotePools[colorIndex], startNotePrefabs[colorIndex]);
|
||||
return GetObjectFromPool(startNotePools[colorIndex], startNotePrefabs[colorIndex], startPoolContainer);
|
||||
}
|
||||
|
||||
public GameObject GetHoldNoteSegment(string color)
|
||||
{
|
||||
|
||||
int colorIndex = GetColorIndexFromName(color);
|
||||
return GetObjectFromPool(holdNotePools[colorIndex], holdNotePrefabs[colorIndex]);
|
||||
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();
|
||||
}
|
||||
ReturnObjectToPool(notePools[GetColorIndexFromName(color)], note);
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(notePools[idx], note, notePoolContainer);
|
||||
}
|
||||
|
||||
public void ReturnStartNote(GameObject startNote, string color)
|
||||
{
|
||||
if (startNote == null) return;
|
||||
if (string.IsNullOrEmpty(color))
|
||||
{
|
||||
Debug.LogWarning($" 归还 StartNote 时 color 为空!音符名: {startNote.name}");
|
||||
if (verboseLogging) Debug.LogWarning($" 归还 StartNote 时 color 为空!音符名: {startNote.name}");
|
||||
Destroy(startNote);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,14 +248,38 @@ public class NotePool : MonoBehaviour
|
||||
holdNote.ResetState(); // 确保重置音符状态
|
||||
}
|
||||
|
||||
ReturnObjectToPool(startNotePools[GetColorIndexFromName(color)], startNote);
|
||||
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))
|
||||
{
|
||||
Debug.LogWarning($" 归还 HoldNoteSegment 时 color 为空!音符名: {holdNote.name}");
|
||||
if (verboseLogging) Debug.LogWarning($" 归还 HoldNoteSegment 时 color 为空!音符名: {holdNote.name}");
|
||||
Destroy(holdNote);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,23 +289,21 @@ public class NotePool : MonoBehaviour
|
||||
holdNoteScript.ResetState(); // 确保重置音符状态
|
||||
}
|
||||
|
||||
ReturnObjectToPool(holdNotePools[GetColorIndexFromName(color)], holdNote);
|
||||
int idx = GetColorIndexFromName(color);
|
||||
ReturnObjectToPool(holdNotePools[idx], holdNote, holdPoolContainer);
|
||||
}
|
||||
|
||||
|
||||
private int GetColorIndexFromName(string colorName)
|
||||
{
|
||||
|
||||
switch (colorName)
|
||||
{
|
||||
case "red": return 0;
|
||||
case "green": return 1;
|
||||
case "yellow": return 2;
|
||||
case "purple": return 3;
|
||||
case "blue": return 4;
|
||||
default:
|
||||
Debug.LogError($"未识别的颜色: {colorName},默认使用红色");
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
float travelTime = (60f / bpm) * 4;
|
||||
float spawnTime = note.time - travelTime;
|
||||
float delay = spawnTime - (Time.time - startTime) + spawnOffset;
|
||||
Debug.Log($"delay: {delay}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"delay: {delay}");
|
||||
|
||||
if (delay > 0)
|
||||
yield return new WaitForSeconds(delay);
|
||||
@@ -92,7 +92,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"生成短音符:颜色={noteData.color}, 按键={key}, 轨道={noteData.trackIndex}");
|
||||
if (GameConfig.verboseLogs) Debug.Log($"生成短音符:颜色={noteData.color}, 按键={key}, 轨道={noteData.trackIndex}");
|
||||
|
||||
GameObject note = notePool.GetNote(noteData.color);
|
||||
if (note == null)
|
||||
@@ -108,8 +108,10 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (noteScript != null)
|
||||
{
|
||||
float noteSpawnTime = Time.time;
|
||||
noteScript.Setup(key, noteData.trackIndex, CalculateSpeed(), noteData.time, noteData.color, judgeConfig);
|
||||
Debug.Log($"到达时间:{noteSpawnTime + (60f / bpm) * 4}");
|
||||
// pass realtime hit time to Note.Setup
|
||||
float realtimeHit = startTime + noteData.time;
|
||||
noteScript.Setup(key, noteData.trackIndex, CalculateSpeed(), realtimeHit, noteData.color, judgeConfig);
|
||||
if (GameConfig.verboseLogs) Debug.Log($"到达时间:{noteSpawnTime + (60f / bpm) * 4}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -143,12 +145,17 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
float segmentInterval = (60f / bpm) / 4f;
|
||||
int segmentCount = Mathf.CeilToInt(noteData.length / segmentInterval);
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
// 修正:scheduledEndTime 应为游戏时间轴上的绝对时间(noteData.time + noteData.length)
|
||||
float scheduledEndTime = noteData.time + noteData.length;
|
||||
if (segmentCount < 1) segmentCount = 1; // 至少一个片段
|
||||
// 如果用节拍计算的片段间隔与实际长度不整除,实际用于分段的间隔应当按实际长度均分,
|
||||
// 以确保尾部音符排在最后一个中段之后并且到达时间等于谱面结束时间。
|
||||
float actualSegmentInterval = noteData.length / segmentCount; // 实际用于中段间隔
|
||||
|
||||
// 生成唯一 id
|
||||
int holdNoteId = Random.Range(100000, 999999);
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
// scheduledEndTime 使用谱面时间(note.time + length),但转换为实时
|
||||
float scheduledEndTime = startTime + (noteData.time + noteData.length);
|
||||
|
||||
// 生成唯一 id(改为自增计数器,避免随机冲突)
|
||||
int holdNoteId = ++holdNoteIdCounter;
|
||||
|
||||
// 生成 Start 段
|
||||
GameObject startObj = notePool.GetStartNote(noteData.color);
|
||||
@@ -163,7 +170,8 @@ public class NoteSpawner : MonoBehaviour
|
||||
HoldNote holdNote = startObj.GetComponent<HoldNote>();
|
||||
if (holdNote != null)
|
||||
{
|
||||
holdNote.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), noteData.time, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig);
|
||||
// pass realtime hit time (startTime + note.time) and delay 0
|
||||
holdNote.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), startTime + noteData.time, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -171,12 +179,10 @@ public class NoteSpawner : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成 Middle 和 End 段
|
||||
// 生成 Middle 片段(1 .. segmentCount-1)
|
||||
for (int i = 1; i < segmentCount; i++)
|
||||
{
|
||||
float segmentDelay = i * segmentInterval;
|
||||
bool isEndSegment = (i == segmentCount - 1);
|
||||
string partType = isEndSegment ? "end" : "middle";
|
||||
float segmentDelay = i * actualSegmentInterval; // 使用实际间隔
|
||||
|
||||
GameObject segObj = notePool.GetHoldNoteSegment(noteData.color);
|
||||
if (segObj == null)
|
||||
@@ -191,13 +197,41 @@ public class NoteSpawner : MonoBehaviour
|
||||
HoldNote holdSeg = segObj.GetComponent<HoldNote>();
|
||||
if (holdSeg != null)
|
||||
{
|
||||
holdSeg.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), noteData.time, segmentDelay, isEndSegment, scheduledEndTime, noteData.color, key, partType, judgeConfig);
|
||||
// 中段都标记为 middle,pass realtime base time and segmentDelay
|
||||
holdSeg.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), startTime + noteData.time, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("长音符片段缺少 HoldNote 组件!");
|
||||
}
|
||||
}
|
||||
|
||||
// 始终在中段之后生成一个明确的 End 段(尾部音符)
|
||||
GameObject endObj = notePool.GetHoldNoteEndSegment(noteData.color);
|
||||
if (endObj == null)
|
||||
{
|
||||
Debug.LogError("对象池返回空 hold note 片段(end)!");
|
||||
return;
|
||||
}
|
||||
endObj.transform.position = spawnPoint.position;
|
||||
endObj.transform.rotation = Quaternion.identity;
|
||||
|
||||
// 为便于识别,将实例名追加后缀
|
||||
if (GameConfig.verboseLogs)
|
||||
endObj.name = endObj.name + "_end";
|
||||
if (GameConfig.verboseLogs) Debug.Log($"生成尾部音符: {endObj.name} (holdId={holdNoteId}, color={noteData.color})");
|
||||
|
||||
HoldNote holdEnd = endObj.GetComponent<HoldNote>();
|
||||
if (holdEnd != null)
|
||||
{
|
||||
// 将 delay 设置为整个 hold 长度 (或 segmentCount * actualSegmentInterval),确保尾部在最后中段之后
|
||||
float endDelay = segmentCount * actualSegmentInterval; // 通常等于 noteData.length
|
||||
holdEnd.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), startTime + noteData.time, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("长音符 end 部分缺少 HoldNote 组件!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user