using UnityEngine; using UnityEngine.UI; using System.Collections; using System.Collections.Generic; using System.Linq; using TMPro; using Bansonic; using UnityEngine.EventSystems; using UnityEngine.InputSystem; public class delayTapperPrefab : MonoBehaviour { [Header("data")] [SerializeField] private float delayAmountSeconds; [Header("delay tapper")] public TMP_InputField delayAmountNow; public Slider runSlider; public TMP_InputField bpmInputField; public Button startButton; public Button resetButton; public Button saveButton; public Button quitButton; public TMP_Dropdown modeDropdown; public Image[] modeSpecificImages; // 4个特定的Image public Button endiasble_hori; public GameObject hori_object; [HideInInspector] public controllerSettings mainSettings; public Button toggle_extra_ui; public GameObject extra_ui_object; public enum TapperMode { EightBeats, FourBeats } private TapperMode currentMode = TapperMode.FourBeats; public GameObject tapperHandlePrefab; public GameObject tapperHandlePrefab_toput; [Header("Audio Playback")] public AudioSource audioSource; public AudioSource hit_sfx_as; public float musicStartTime = 0f; public float musicEndTime = 0f; [Header("Tapper Stats")] [SerializeField] public List tapValueOffsets = new List(); // 每次点击相对最近拍点的原始偏移(秒)。用于最终的中位数 + 去异常值统计, // 取代旧版"累计平均"(平均会被漏拍/误触严重拉偏且会随点击次数稀释)。 private readonly List rawOffsetSamples = new List(); [Header("Audio Offset UI (device latency knob)")] [Tooltip("音频偏移输入框(ms):只移动音乐播放时刻补偿设备/蓝牙延迟,绝不改判定。与输入校准偏移独立。")] public TMP_InputField audioOffsetInput; public Button audioOffsetSaveButton; [SerializeField] private int BPM = 120; private Coroutine sliderCoroutine; private Coroutine audioCoroutine; private bool isInternalSetting = false; private bool isAudioReady = false; private bool isPaused = false; private bool isRunning = false; // dspTime-based clock. currentElapsed is derived from AudioSettings.dspTime relative to // the cycle's start dsp, NOT accumulated Time.deltaTime — this matches the game's // GameplayClock and the audio thread exactly, removing frame-rate drift. private double cycleStartDsp = 0d; // dspTime at which the current beat cycle began private double pausedAtDsp = 0d; // dspTime captured when paused private double accumulatedPauseDsp = 0d; // total paused duration within the current cycle private float currentElapsed = 0f; private Dictionary originalMuteStates = new Dictionary(); private const string DELAY_PREFS_KEY = "UserGlobalDelaySeconds"; private const string AUDIO_OFFSET_PREFS_KEY = "UserAudioOffsetSeconds"; // = GameManager.AudioOffsetPrefsKey private void Awake() { // 强制确保 List 已初始化 if (tapValueOffsets == null) tapValueOffsets = new List(); } private void Start() { if (bpmInputField != null) { // 锁定 BPM 为 120 且不可编辑 isInternalSetting = true; bpmInputField.text = "120"; bpmInputField.interactable = false; // 禁用交互 isInternalSetting = false; // 监听点击或编辑事件 bpmInputField.onSelect.AddListener(delegate { OnInputFieldSelected(); }); bpmInputField.onValueChanged.AddListener(OnInputFieldChanged); } if (startButton != null) { // 在代码中显式绑定按钮,防止 Inspector 遗漏 startButton.onClick.RemoveListener(OnStartButtonClicked); startButton.onClick.AddListener(OnStartButtonClicked); // 允许点击按钮 startButton.interactable = true; UpdateStartButtonText(); } if (saveButton != null) { saveButton.onClick.RemoveListener(OnSaveButtonClicked); saveButton.onClick.AddListener(OnSaveButtonClicked); } if (quitButton != null) { quitButton.onClick.RemoveListener(OnQuitButtonClicked); quitButton.onClick.AddListener(OnQuitButtonClicked); } // 读取保存的延迟值 float savedDelay = PlayerPrefs.GetFloat(DELAY_PREFS_KEY, 0f); if (delayAmountNow != null) { isInternalSetting = true; delayAmountNow.text = Mathf.RoundToInt(savedDelay * 1000f).ToString(); isInternalSetting = false; } if (modeDropdown != null) { modeDropdown.onValueChanged.RemoveListener(OnModeChanged); modeDropdown.onValueChanged.AddListener(OnModeChanged); // 默认设置为四拍模式 (index 1) modeDropdown.value = 1; // 初始化显示状态 OnModeChanged(modeDropdown.value); } if (delayAmountNow != null) { delayAmountNow.onEndEdit.RemoveListener(OnDelayAmountEndEdit); delayAmountNow.onEndEdit.AddListener(OnDelayAmountEndEdit); } if (resetButton != null) { resetButton.onClick.RemoveListener(OnResetButtonClicked); resetButton.onClick.AddListener(OnResetButtonClicked); } if (endiasble_hori != null) { endiasble_hori.onClick.RemoveListener(OnEndisableHoriClicked); endiasble_hori.onClick.AddListener(OnEndisableHoriClicked); } if (toggle_extra_ui != null) { toggle_extra_ui.onClick.RemoveListener(OnToggleExtraUIClicked); toggle_extra_ui.onClick.AddListener(OnToggleExtraUIClicked); } SetupAudioOffsetUI(); // 开始加载音频 StartCoroutine(PrepareAudioAndEnableButton()); // 查找场景中所有的 AudioSource 并将非指定的 AudioSource 静音 MuteOtherAudioSources(); } /// /// 音频偏移(设备延迟补偿)独立于输入校准偏移:它只移动音乐播放时刻(GameManager 读取 /// UserAudioOffsetSeconds),绝不改判定。此处提供一个独立输入框+保存按钮,读写该 PlayerPrefs。 /// private void SetupAudioOffsetUI() { if (audioOffsetInput != null) { float savedAudioOffset = PlayerPrefs.GetFloat(AUDIO_OFFSET_PREFS_KEY, 0f); isInternalSetting = true; audioOffsetInput.text = Mathf.RoundToInt(savedAudioOffset * 1000f).ToString(); isInternalSetting = false; audioOffsetInput.onEndEdit.RemoveListener(OnAudioOffsetEndEdit); audioOffsetInput.onEndEdit.AddListener(OnAudioOffsetEndEdit); } if (audioOffsetSaveButton != null) { audioOffsetSaveButton.onClick.RemoveListener(OnAudioOffsetSaveClicked); audioOffsetSaveButton.onClick.AddListener(OnAudioOffsetSaveClicked); } } private void OnAudioOffsetEndEdit(string input) { if (isInternalSetting) return; if (int.TryParse(input, out int value)) { if (value < -1000 || value > 1000) { int clamped = Mathf.Clamp(value, -1000, 1000); gNotice.warning.display($"音频偏移超出范围 (-1000~1000),已重置为 {clamped}"); isInternalSetting = true; audioOffsetInput.text = clamped.ToString(); isInternalSetting = false; } } else { gNotice.warning.display("非法的音频偏移输入,已重置为 0"); isInternalSetting = true; audioOffsetInput.text = "0"; isInternalSetting = false; } } private void OnAudioOffsetSaveClicked() { if (audioOffsetInput != null && int.TryParse(audioOffsetInput.text, out int msValue)) { int clamped = Mathf.Clamp(msValue, -1000, 1000); float seconds = clamped / 1000f; PlayerPrefs.SetFloat(AUDIO_OFFSET_PREFS_KEY, seconds); PlayerPrefs.Save(); gNotice.warning.display($"已保存音频偏移: {clamped}ms ({seconds:F3}s)"); Debug.Log($"[delayTapperPrefab] Saved audio offset to PlayerPrefs: {seconds}s"); } else { gNotice.warning.display("音频偏移无效,未保存"); } if (EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(null); } } private void OnDestroy() { // 销毁时恢复音频源状态 RestoreAudioSources(); } private void MuteOtherAudioSources() { originalMuteStates.Clear(); // 查找场景中所有的 AudioSource (包括非激活的) AudioSource[] allAudioSources = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var asrc in allAudioSources) { // 如果是指定的两个音频源之一,不静音 if (asrc == audioSource || asrc == hit_sfx_as) { // 确保它们是取消静音的,以便能听到声音 asrc.mute = false; continue; } // 记录原始静音状态 if (!originalMuteStates.ContainsKey(asrc)) { originalMuteStates[asrc] = asrc.mute; asrc.mute = true; // 静音 } } Debug.Log($"[delayTapperPrefab] 已静音 {originalMuteStates.Count} 个音频源"); } private void RestoreAudioSources() { int restoredCount = 0; foreach (var kvp in originalMuteStates) { if (kvp.Key != null) { kvp.Key.mute = kvp.Value; restoredCount++; } } originalMuteStates.Clear(); Debug.Log($"[delayTapperPrefab] 已恢复 {restoredCount} 个音频源的静音状态"); } public void OnResetButtonClicked() { Debug.Log("[delayTapperPrefab] Reset 按钮被点击"); // 1. 停止滚动和音频 StopAndResetTapper(); isRunning = false; isPaused = false; currentElapsed = 0f; accumulatedPauseDsp = 0d; pausedAtDsp = 0d; UpdateStartButtonText(); // 2. 清空 Prefab 和采样数据 ClearTapperHandles(); // 3. 重置 UI 显示 if (delayAmountNow != null) { isInternalSetting = true; delayAmountNow.text = "0"; isInternalSetting = false; } // 4. 取消按钮焦点 if (EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(null); } } private void OnEndisableHoriClicked() { if (hori_object != null) { bool isActive = hori_object.activeSelf; hori_object.SetActive(!isActive); Debug.Log($"[delayTapperPrefab] hori_object state toggled to: {!isActive}"); } // 取消按钮焦点,防止空格触发重复点击 if (EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(null); } } private void OnToggleExtraUIClicked() { if (extra_ui_object != null) { bool isActive = extra_ui_object.activeSelf; extra_ui_object.SetActive(!isActive); Debug.Log($"[delayTapperPrefab] extra_ui_object state toggled to: {!isActive}"); } // 取消按钮焦点,防止空格触发重复点击 if (EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(null); } } private void OnSaveButtonClicked() { if (delayAmountNow != null && float.TryParse(delayAmountNow.text, out float msValue)) { float seconds = msValue / 1000f; PlayerPrefs.SetFloat(DELAY_PREFS_KEY, seconds); PlayerPrefs.Save(); gNotice.warning.display($"已保存全局延迟: {msValue}ms ({seconds:F3}s)"); Debug.Log($"[delayTapperPrefab] Saved delay to PlayerPrefs: {seconds}s"); // 刷新父界面的显示 if (mainSettings != null) { mainSettings.RefreshDelayText(); } } if (EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(null); } // 保存后销毁物体 OnQuitButtonClicked(); } private void OnQuitButtonClicked() { Debug.Log("[delayTapperPrefab] Quit 按钮被点击,销毁物体"); Destroy(this.gameObject); } private void UpdateStartButtonText() { if (startButton == null) return; Text btnText = startButton.GetComponentInChildren(); if (btnText == null) { TMP_Text tmpText = startButton.GetComponentInChildren(); if (tmpText != null) { if (!isRunning) tmpText.text = "开始测试"; else if (isPaused) tmpText.text = "继续"; else tmpText.text = "暂停"; } return; } if (!isRunning) btnText.text = "开始测试"; else if (isPaused) btnText.text = "继续"; else btnText.text = "暂停"; } private void OnStartButtonClicked() { if (!isAudioReady) { gNotice.warning.display("音频未就绪"); return; } if (!isRunning) { // 第一次开始 isRunning = true; isPaused = false; currentElapsed = 0f; StartTapperFromInput(); } else { // 切换暂停/继续 isPaused = !isPaused; if (isPaused) { // 暂停:记录暂停时刻的 dspTime,冻结 cycle 时钟。 pausedAtDsp = AudioSettings.dspTime; if (audioSource != null && audioSource.isPlaying) audioSource.Pause(); Debug.Log("[delayTapperPrefab] 暂停测试"); } else { // 继续:把暂停期间流逝的 dspTime 累加进 pause 补偿,使 cycle 时钟不计入暂停时长。 accumulatedPauseDsp += AudioSettings.dspTime - pausedAtDsp; if (audioSource != null) audioSource.UnPause(); Debug.Log("[delayTapperPrefab] 继续测试"); } } UpdateStartButtonText(); if (EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(null); } } private void OnDelayAmountEndEdit(string input) { if (isInternalSetting) return; if (int.TryParse(input, out int value)) { if (value < -1000 || value > 1000) { int clampedValue = Mathf.Clamp(value, -1000, 1000); gNotice.warning.display($"延迟值超出范围 (-1000~1000),已重置为 {clampedValue}"); isInternalSetting = true; delayAmountNow.text = clampedValue.ToString(); isInternalSetting = false; } } else { gNotice.warning.display("非法的延迟值输入,已重置为 0"); isInternalSetting = true; delayAmountNow.text = "0"; isInternalSetting = false; } } private void OnModeChanged(int index) { // 0 = 8拍模式, 1 = 4拍模式 (由 Dropdown 设置顺序决定) currentMode = (index == 1) ? TapperMode.FourBeats : TapperMode.EightBeats; // 如果是 4 拍模式,将 4 张图 alpha 设置为 0 if (modeSpecificImages != null) { foreach (var img in modeSpecificImages) { if (img != null) { Color c = img.color; c.a = (currentMode == TapperMode.FourBeats) ? 0f : 1f; img.color = c; } } } Debug.Log($"[delayTapperPrefab] 模式切换:{currentMode}"); // 切换模式时重置数据 StopAndResetTapper(); ClearTapperHandles(); } private IEnumerator PrepareAudioAndEnableButton() { isAudioReady = false; if (audioSource == null || audioSource.clip == null) { Debug.LogWarning("[delayTapperPrefab] 无音频源或片段"); isAudioReady = true; yield break; } // 如果音频还未加载,则手动触发加载 if (audioSource.clip.loadState != AudioDataLoadState.Loaded) { Debug.Log($"[delayTapperPrefab] 正在加载音频: {audioSource.clip.name}..."); audioSource.clip.LoadAudioData(); // 等待加载完成 while (audioSource.clip.loadState == AudioDataLoadState.Loading) { yield return null; } if (audioSource.clip.loadState != AudioDataLoadState.Loaded) { Debug.LogError($"[delayTapperPrefab] 音频加载失败! 状态: {audioSource.clip.loadState}"); } else { Debug.Log("[delayTapperPrefab] 音频加载成功"); isAudioReady = true; } } else { Debug.Log("[delayTapperPrefab] 音频已在内存中"); isAudioReady = true; } } private void OnInputFieldSelected() { if (isInternalSetting) return; Debug.Log("[delayTapperPrefab] InputField 被选中,停止播放并重置"); StopAndResetTapper(); } private void OnInputFieldChanged(string val) { if (isInternalSetting) return; Debug.Log("[delayTapperPrefab] InputField 内容改变,停止播放并重置"); StopAndResetTapper(); } public void StartTapperFromInput() { Debug.Log("[delayTapperPrefab] StartTapperFromInput 按钮被点击"); // 0. 点击后立刻取消按钮焦点,防止空格触发重复点击 if (EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(null); } if (!isAudioReady) { gNotice.warning.display("音频未就绪"); return; } // 强制锁定 BPM 为 120 BPM = 120; if (bpmInputField != null) { isInternalSetting = true; bpmInputField.text = "120"; isInternalSetting = false; } // 点击启动按钮时,手动清理一次旧数据 ClearTapperHandles(); RunTapperProcess(); StartAudioPlayback(); } private void StartAudioPlayback() { if (audioSource == null || audioSource.clip == null) { Debug.LogWarning("[delayTapperPrefab] AudioSource 或 AudioClip 未分配,跳过播放"); return; } // 停止当前可能正在运行的播放协程 if (audioCoroutine != null) StopCoroutine(audioCoroutine); // 强制停止当前音频并直接跳转到指定时间,确保没有重音或残留播放 audioSource.Stop(); float start = Mathf.Clamp(musicStartTime, 0f, audioSource.clip.length); audioSource.time = start; audioCoroutine = StartCoroutine(AudioPlaybackRoutine()); } private void Update() { // 如果暂停,不执行任何 Update 逻辑 if (isPaused) return; // 改进监听逻辑:只要 Slider 正在滚动,就允许记录空格。 // 用 Input System 的 Keyboard.current 捕获,并在按下的同一帧读取 AudioSettings.dspTime, // 与游戏内判定同源(dspTime),消除 Time.deltaTime 帧量化误差。 if (sliderCoroutine != null && Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame) { SpawnTapperHandle(AudioSettings.dspTime); } } // 由 dspTime 推导当前 cycle 内已流逝时间(秒),与游戏时钟一致,无帧率漂移。 private float ElapsedFromDsp() { double reference = isPaused ? pausedAtDsp : AudioSettings.dspTime; double elapsed = reference - cycleStartDsp - accumulatedPauseDsp; return Mathf.Max(0f, (float)elapsed); } private void SpawnTapperHandle(double pressDsp) { Debug.Log("[delayTapperPrefab] SpawnTapperHandle 被调用"); if (runSlider == null) { Debug.LogError("runSlider 为空"); return; } // 1. 用按键时刻的 dspTime 推导 cycle 内流逝时间,再算相对最近拍点的偏移,精度到音频缓冲级。 double reference = pressDsp - cycleStartDsp - accumulatedPauseDsp; float elapsedAtPress = Mathf.Max(0f, (float)reference); float beatDuration = 60f / Mathf.Max(BPM, 1); // 每拍时长 (120BPM 为 0.5s) float nearestBeatTime = Mathf.Round(elapsedAtPress / beatDuration) * beatDuration; float offsetSeconds = elapsedAtPress - nearestBeatTime; // 2. 记录原始样本(用于中位数/去异常),同时保留 ms 字符串到 Inspector List 便于查看。 rawOffsetSamples.Add(offsetSeconds); string offsetStr = (offsetSeconds * 1000f).ToString("F2") + "ms"; tapValueOffsets.Add(offsetStr); // 3. 用中位数 + 去异常值(MAD)计算稳健延迟,取代旧的累计平均(易被漏拍/误触拉偏、随次数稀释)。 int finalDelayMs = Mathf.RoundToInt(ComputeRobustDelaySeconds() * 1000f); // 4. 输出到 delayAmountNow if (delayAmountNow != null) { isInternalSetting = true; delayAmountNow.text = finalDelayMs.ToString(); isInternalSetting = false; } Debug.Log($"[delayTapperPrefab] 模式={currentMode}, 点击#{rawOffsetSamples.Count}: Time={elapsedAtPress:F3}s, NearestBeat={nearestBeatTime:F3}s, Offset={offsetSeconds*1000f:F2}ms, RobustDelay={finalDelayMs}ms"); if (tapperHandlePrefab == null || tapperHandlePrefab_toput == null) return; // 获取 Slider 的 Handle RectTransform handle = runSlider.handleRect; if (handle == null) return; // 实例化到指定父物体下 GameObject newHandle = Instantiate(tapperHandlePrefab, tapperHandlePrefab_toput.transform); // 同步 Handle 的位置 newHandle.transform.position = handle.position; newHandle.transform.rotation = handle.rotation; // 播放点击音效 if (hit_sfx_as != null) { hit_sfx_as.Play(); } } /// /// 稳健地估计玩家的输入偏移(秒):先取样本中位数,再用 MAD(绝对中位差)剔除离群点, /// 最后对保留样本取均值。相比累计平均,能抵抗漏拍、误触和偶发大偏差, /// 且不会随点击次数增多而被历史值稀释。样本不足时直接返回中位数/单值。 /// private float ComputeRobustDelaySeconds() { int n = rawOffsetSamples.Count; if (n == 0) return 0f; if (n == 1) return rawOffsetSamples[0]; float median = Median(rawOffsetSamples); // MAD = median(|x_i - median|)。用 1.4826 换算成与标准差可比的尺度。 var absDeviations = new List(n); for (int i = 0; i < n; i++) absDeviations.Add(Mathf.Abs(rawOffsetSamples[i] - median)); float mad = Median(absDeviations); float robustSigma = mad * 1.4826f; // 阈值:偏离中位数超过 3 个稳健标准差的样本视为异常值。 // MAD 为 0(样本高度一致)时不剔除任何点,直接用中位数。 if (robustSigma <= 1e-6f) return median; float threshold = 3f * robustSigma; double sum = 0d; int kept = 0; for (int i = 0; i < n; i++) { if (Mathf.Abs(rawOffsetSamples[i] - median) <= threshold) { sum += rawOffsetSamples[i]; kept++; } } return kept > 0 ? (float)(sum / kept) : median; } private static float Median(List values) { int n = values.Count; if (n == 0) return 0f; var sorted = values.OrderBy(v => v).ToList(); int mid = n / 2; return (n % 2 == 1) ? sorted[mid] : 0.5f * (sorted[mid - 1] + sorted[mid]); } private void ClearTapperHandles() { Debug.Log("[delayTapperPrefab] ClearTapperHandles 被显式调用,即将清空 List 和累计数据"); if (tapperHandlePrefab_toput == null) return; int childCount = tapperHandlePrefab_toput.transform.childCount; for (int i = childCount - 1; i >= 0; i--) { Destroy(tapperHandlePrefab_toput.transform.GetChild(i).gameObject); } // 重置所有采样数据 tapValueOffsets.Clear(); rawOffsetSamples.Clear(); Debug.Log("[delayTapperPrefab] 已手动清理 TapperHandles 和所有采样数据"); } private IEnumerator AudioPlaybackRoutine() { // 再次确保开始时间不超过结束时间且在音频长度范围内 float start = Mathf.Clamp(musicStartTime, 0f, audioSource.clip.length); float end = Mathf.Clamp(musicEndTime, start, audioSource.clip.length); if (end <= start) { Debug.LogWarning("[delayTapperPrefab] 音乐结束时间小于或等于开始时间,无法播放"); yield break; } // 此时直接开始播放即可,因为在 StartAudioPlayback 中已经跳转过时间 audioSource.Play(); Debug.Log($"[delayTapperPrefab] 音频开始播放(跳转):{start}s -> {end}s"); while (audioSource.isPlaying && audioSource.time < end) { yield return null; } audioSource.Stop(); Debug.Log("[delayTapperPrefab] 音频播放结束,停止 Slider 并清理数据"); // 重置运行状态和按钮文本 isRunning = false; isPaused = false; UpdateStartButtonText(); // 音频停止时,同时停止并重置 Slider StopAndResetTapper(); // 清理所有数据和 Prefab ClearTapperHandles(); } public void StopAndResetTapper() { if (sliderCoroutine != null) { Debug.Log("[delayTapperPrefab] 停止当前滚动协程"); StopCoroutine(sliderCoroutine); sliderCoroutine = null; } if (audioCoroutine != null) { StopCoroutine(audioCoroutine); audioCoroutine = null; } if (audioSource != null && audioSource.isPlaying) { audioSource.Stop(); } if (runSlider != null) { runSlider.value = 0f; } } public void RunTapperProcess() { Debug.Log("[delayTapperPrefab] RunTapperProcess 准备启动协程"); if (sliderCoroutine != null) StopCoroutine(sliderCoroutine); sliderCoroutine = StartCoroutine(RunTapperProcessCoroutine()); } private IEnumerator RunTapperProcessCoroutine() { if (runSlider == null) { Debug.LogError("[delayTapperPrefab] runSlider 为空,无法开始滚动!"); yield break; } // 强制确保 Slider 的范围是 0 到 1 runSlider.minValue = 0f; runSlider.maxValue = 1f; while (true) { // 每次循环开始时清理视觉 Prefab (如果不是第一次启动) // 注意:这里我们只清理生成的 Handle,不重置数据,因为数据是跨循环累计的 ClearVisualHandlesOnly(); // 8拍模式走完 8 拍,4拍模式走完 4 拍 int beatsInCycle = (currentMode == TapperMode.FourBeats) ? 4 : 8; float cycleDuration = (60f / Mathf.Max(BPM, 1)) * beatsInCycle; // 用 dspTime 作为 cycle 时钟起点,与按键捕获同源。每个 cycle 重置 pause 累计。 cycleStartDsp = AudioSettings.dspTime; accumulatedPauseDsp = 0d; currentElapsed = 0f; Debug.Log($"[delayTapperPrefab] 开始 {beatsInCycle} 拍滚动,BPM: {BPM}, 单次循环时长: {cycleDuration}s"); while (currentElapsed < cycleDuration) { if (!isPaused) { currentElapsed = ElapsedFromDsp(); runSlider.value = Mathf.Clamp01(currentElapsed / cycleDuration); } yield return null; } currentElapsed = 0f; runSlider.value = 0f; } } private void ClearVisualHandlesOnly() { if (tapperHandlePrefab_toput == null) return; int childCount = tapperHandlePrefab_toput.transform.childCount; for (int i = childCount - 1; i >= 0; i--) { Destroy(tapperHandlePrefab_toput.transform.GetChild(i).gameObject); } } public void on_close_img() { gNotice.recommendation.display("打开了!但是没有完全打开!", 2f, 2f); } }