Files
bansonic_beta_main/Assets/delayTapperPrefab/delayTapperPrefab.cs
T
2026-07-30 23:15:58 +08:00

870 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> tapValueOffsets = new List<string>();
// 每次点击相对最近拍点的原始偏移(秒)。用于最终的中位数 + 去异常值统计,
// 取代旧版"累计平均"(平均会被漏拍/误触严重拉偏且会随点击次数稀释)。
private readonly List<float> rawOffsetSamples = new List<float>();
[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<AudioSource, bool> originalMuteStates = new Dictionary<AudioSource, bool>();
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<string>();
}
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();
}
/// <summary>
/// 音频偏移(设备延迟补偿)独立于输入校准偏移:它只移动音乐播放时刻(GameManager 读取
/// UserAudioOffsetSeconds),绝不改判定。此处提供一个独立输入框+保存按钮,读写该 PlayerPrefs。
/// </summary>
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<AudioSource>(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<Text>();
if (btnText == null)
{
TMP_Text tmpText = startButton.GetComponentInChildren<TMP_Text>();
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();
}
}
/// <summary>
/// 稳健地估计玩家的输入偏移(秒):先取样本中位数,再用 MAD(绝对中位差)剔除离群点,
/// 最后对保留样本取均值。相比累计平均,能抵抗漏拍、误触和偶发大偏差,
/// 且不会随点击次数增多而被历史值稀释。样本不足时直接返回中位数/单值。
/// </summary>
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<float>(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<float> 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);
}
}