using UnityEngine; using UnityEngine.UI; using System.Collections; using System.Collections.Generic; using TMPro; using Bansonic; using UnityEngine.EventSystems; 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 float totalOffsetSum = 0f; // 用于计算平均值的累计偏移 private int tapCount = 0; // 累计点击次数 [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; private float currentElapsed = 0f; private Dictionary originalMuteStates = new Dictionary(); private const string DELAY_PREFS_KEY = "UserGlobalDelaySeconds"; 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); } // 开始加载音频 StartCoroutine(PrepareAudioAndEnableButton()); // 查找场景中所有的 AudioSource 并将非指定的 AudioSource 静音 MuteOtherAudioSources(); } 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; 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) { // 暂停 if (audioSource != null && audioSource.isPlaying) audioSource.Pause(); Debug.Log("[delayTapperPrefab] 暂停测试"); } else { // 继续 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 正在滚动,就允许记录空格 if (sliderCoroutine != null && Input.GetKeyDown(KeyCode.Space)) { SpawnTapperHandle(); } } private void SpawnTapperHandle() { Debug.Log("[delayTapperPrefab] SpawnTapperHandle 被调用"); if (runSlider == null) { Debug.LogError("runSlider 为空"); return; } // 1. 直接基于当前时间(currentElapsed)计算偏移,不再依赖 Slider 的百分比,提高精度 float beatDuration = 60f / Mathf.Max(BPM, 1); // 每拍时长 (120BPM 为 0.5s) float nearestBeatTime = Mathf.Round(currentElapsed / beatDuration) * beatDuration; float offsetSeconds = currentElapsed - nearestBeatTime; // 2. 记录差值到 List (Inspector 显示用,保留 ms 字符串格式) string offsetStr = (offsetSeconds * 1000f).ToString("F2") + "ms"; tapValueOffsets.Add(offsetStr); // 3. 计算平均偏移逻辑 tapCount++; totalOffsetSum += offsetSeconds; float averageOffsetSeconds = totalOffsetSum / tapCount; int finalDelayMs = Mathf.RoundToInt(averageOffsetSeconds * 1000f); // 4. 输出到 delayAmountNow if (delayAmountNow != null) { isInternalSetting = true; delayAmountNow.text = finalDelayMs.ToString(); isInternalSetting = false; } Debug.Log($"[delayTapperPrefab] 模式={currentMode}, 点击#{tapCount}: Time={currentElapsed:F3}s, NearestBeat={nearestBeatTime:F3}s, Offset={offsetSeconds*1000f:F2}ms, AvgDelay={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(); } } 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(); totalOffsetSum = 0f; tapCount = 0; 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(); // 如果不是暂停后继续,则重置 currentElapsed if (!isPaused) currentElapsed = 0f; // 8拍模式走完 8 拍,4拍模式走完 4 拍 int beatsInCycle = (currentMode == TapperMode.FourBeats) ? 4 : 8; float cycleDuration = (60f / Mathf.Max(BPM, 1)) * beatsInCycle; Debug.Log($"[delayTapperPrefab] 开始 {beatsInCycle} 拍滚动,BPM: {BPM}, 单次循环时长: {cycleDuration}s"); while (currentElapsed < cycleDuration) { if (!isPaused) { currentElapsed += Time.deltaTime; 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); } }