using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.UI; public class controllerSettings : MonoBehaviour { [Header("Inspector")] public Button[] keyButtons; public Button secondary_track3Button; public Text errorText; [Header("Message Colors")] public Color messageErrorColor = Color.red; public Color messageSuccessColor = Color.green; [Header("Inspector")] public Slider noteSpeedMultipler_slider; public Text noteSpeedMultipler_valueText; public Button noteSpeed_DecreaseButton; public Button noteSpeed_IncreaseButton; public Button noteSpeed_ResetButton; [Header("call dt prefab")] public GameObject delayTapper_prefab; public GameObject dt_to_put; public Button launch_delayTapper; public Text currentDelayText; private readonly string[] colors = { "red", "green", "yellow", "purple", "blue" }; private readonly string[] bindingDisplayNames = { "轨道1键位", "轨道2键位", "轨道3键位", "轨道4键位", "轨道5键位", "轨道3键位2" }; private const int SecondaryTrack3Slot = 5; private const float NoteSpeedMin = 1f; private const float NoteSpeedMax = 3.25f; private const string NoteSpeedPrefKey = "noteSpeedMultiplier"; private const string NoteSpeedDefaultVersionKey = "noteSpeedMultiplierDefaultVersion"; private const int NoteSpeedDefaultVersion = 2; private const float NoteSpeedDefault = 2.0f; private const float ContinuousInitialDelay = 0.5f; private const float ContinuousRepeatRate = 0.05f; private const float StepAmount = 0.01f; private bool isRebinding; private int rebindingIndex = -1; private KeyCode previousKey = KeyCode.None; private bool previousKeyValid; private Coroutine blinkCoroutine; private Coroutine messageCoroutine; private Coroutine continuousChangeCoroutine; // Input System interactive rebind: a throwaway InputAction listens for the next keyboard // press and reports the bound control's path, which we convert back to KeyCode so the // existing KeyCode-based storage and conflict-swap logic stay unchanged. private InputAction rebindAction; private InputActionRebindingExtensions.RebindingOperation rebindOperation; private void Start() { RefreshButtonLabels(); ClearMessage(); SetupKeyButtonListeners(); SetupNoteSpeedControls(); SetupDelayTapper(); RefreshDelayText(); } private void OnDestroy() { if (keyButtons != null) { foreach (Button button in keyButtons) { if (button != null) button.onClick.RemoveAllListeners(); } } if (secondary_track3Button != null) secondary_track3Button.onClick.RemoveAllListeners(); if (noteSpeedMultipler_slider != null) noteSpeedMultipler_slider.onValueChanged.RemoveListener(OnNoteSpeedSliderChanged); if (noteSpeed_DecreaseButton != null) noteSpeed_DecreaseButton.onClick.RemoveAllListeners(); if (noteSpeed_IncreaseButton != null) noteSpeed_IncreaseButton.onClick.RemoveAllListeners(); if (noteSpeed_ResetButton != null) noteSpeed_ResetButton.onClick.RemoveAllListeners(); if (launch_delayTapper != null) launch_delayTapper.onClick.RemoveAllListeners(); CleanupRebindOperation(); } private void Update() { // Clicking away from the key buttons cancels an in-progress rebind. Key capture itself // is handled by the Input System RebindingOperation started in OnKeyButtonClicked. if (isRebinding && Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame && !IsPointerOverAnyKeyButton()) { CancelRebind(); } } public void RefreshDelayText() { if (currentDelayText == null) return; const string delayPrefsKey = "UserGlobalDelaySeconds"; if (PlayerPrefs.HasKey(delayPrefsKey)) { float savedDelaySeconds = PlayerPrefs.GetFloat(delayPrefsKey, 0f); int ms = Mathf.RoundToInt(savedDelaySeconds * 1000f); currentDelayText.text = $"当前偏移:{(ms >= 0 ? "+" : "")}{ms}ms"; } else { currentDelayText.text = "未设定偏移数值"; } } public void OnLaunchDelayTapperClicked() { if (delayTapper_prefab == null || dt_to_put == null) { Debug.LogWarning("[controllerSettings] Prefab or target container is missing!"); return; } GameObject instantiated = Instantiate(delayTapper_prefab, dt_to_put.transform); Debug.Log($"[controllerSettings] Instantiated delay tapper prefab under {dt_to_put.name}"); delayTapperPrefab dtp = instantiated.GetComponent(); if (dtp != null) { dtp.mainSettings = this; } } private void SetupKeyButtonListeners() { if (keyButtons != null) { for (int i = 0; i < keyButtons.Length && i < colors.Length; i++) { int idx = i; if (keyButtons[i] == null) continue; keyButtons[i].onClick.RemoveAllListeners(); keyButtons[i].onClick.AddListener(() => OnKeyButtonClicked(idx)); } } if (secondary_track3Button != null) { secondary_track3Button.onClick.RemoveAllListeners(); secondary_track3Button.onClick.AddListener(() => OnKeyButtonClicked(SecondaryTrack3Slot)); } } private void SetupNoteSpeedControls() { if (noteSpeedMultipler_slider != null) { noteSpeedMultipler_slider.minValue = NoteSpeedMin; noteSpeedMultipler_slider.maxValue = NoteSpeedMax; EnsureDefaultNoteSpeedPreference(); float saved = Mathf.Clamp(PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault), NoteSpeedMin, NoteSpeedMax); noteSpeedMultipler_slider.value = saved; UpdateNoteSpeedText(saved); noteSpeedMultipler_slider.onValueChanged.RemoveAllListeners(); noteSpeedMultipler_slider.onValueChanged.AddListener(OnNoteSpeedSliderChanged); EventTrigger trigger = noteSpeedMultipler_slider.gameObject.GetComponent(); if (trigger == null) trigger = noteSpeedMultipler_slider.gameObject.AddComponent(); trigger.triggers.RemoveAll(e => e.eventID == EventTriggerType.PointerUp); EventTrigger.Entry entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp }; entry.callback.AddListener(_ => OnNoteSpeedSliderPointerUp()); trigger.triggers.Add(entry); } if (noteSpeed_DecreaseButton != null) { noteSpeed_DecreaseButton.onClick.RemoveAllListeners(); noteSpeed_DecreaseButton.onClick.AddListener(() => ChangeNoteSpeedBy(-StepAmount)); AddButtonContinuousEvents(noteSpeed_DecreaseButton, -StepAmount); } if (noteSpeed_IncreaseButton != null) { noteSpeed_IncreaseButton.onClick.RemoveAllListeners(); noteSpeed_IncreaseButton.onClick.AddListener(() => ChangeNoteSpeedBy(StepAmount)); AddButtonContinuousEvents(noteSpeed_IncreaseButton, StepAmount); } if (noteSpeed_ResetButton != null) { noteSpeed_ResetButton.onClick.RemoveAllListeners(); noteSpeed_ResetButton.onClick.AddListener(ResetNoteSpeed); } } private void SetupDelayTapper() { if (launch_delayTapper == null) return; launch_delayTapper.onClick.RemoveAllListeners(); launch_delayTapper.onClick.AddListener(OnLaunchDelayTapperClicked); } private bool IsPointerOverAnyKeyButton() { if (EventSystem.current == null) return false; Vector2 mousePos = Mouse.current != null ? Mouse.current.position.ReadValue() : Vector2.zero; PointerEventData pointerData = new PointerEventData(EventSystem.current) { position = mousePos }; List results = new List(); EventSystem.current.RaycastAll(pointerData, results); if (results.Count == 0) return false; foreach (RaycastResult result in results) { GameObject go = result.gameObject; if (IsPointerOverButton(go, secondary_track3Button)) return true; if (keyButtons == null) continue; foreach (Button button in keyButtons) { if (IsPointerOverButton(go, button)) return true; } } return false; } private static bool IsPointerOverButton(GameObject go, Button button) { return go != null && button != null && (go == button.gameObject || go.transform.IsChildOf(button.transform)); } private void OnKeyButtonClicked(int index) { if (isRebinding || !IsValidBindingSlot(index)) return; isRebinding = true; rebindingIndex = index; previousKey = GetKeyForSlot(index); previousKeyValid = true; blinkCoroutine = StartCoroutine(BlinkUnderscore(index)); ShowPersistentMessage("按下新按键", messageErrorColor); StartInteractiveRebind(); } /// /// Start an Input System interactive rebind that waits for the next keyboard press. /// The captured control path is mapped back to KeyCode and fed into the existing /// HandleRebindKey conflict-swap logic, keeping storage and behavior equivalent. /// private void StartInteractiveRebind() { CleanupRebindOperation(); rebindAction = new InputAction("Rebind", InputActionType.Button, expectedControlType: "Button"); // PerformInteractiveRebinding needs an existing binding (index 0) to override. rebindAction.AddBinding("/space"); rebindOperation = rebindAction.PerformInteractiveRebinding(0) .WithControlsExcluding("/leftButton") .WithControlsExcluding("/rightButton") .WithControlsExcluding("/middleButton") .WithControlsExcluding("/position") .WithControlsExcluding("/delta") .WithCancelingThrough("/escape") .OnCancel(_ => { CleanupRebindOperation(); CancelRebind(); }) .OnComplete(op => { // selectedControl.name is the control name (e.g. "d", "space"); map to KeyCode. KeyCode captured = ResolveKeyCodeFromControl(op.selectedControl); CleanupRebindOperation(); if (captured == KeyCode.None) { // Unmappable key: treat like a cancel so we don't clobber the binding. CancelRebind(); return; } HandleRebindKey(captured); }) .Start(); } private static KeyCode ResolveKeyCodeFromControl(InputControl control) { if (control == null) return KeyCode.None; // control.path -> "/Keyboard/d"; the last segment is the control name. string name = control.name; // e.g. "d", "space", "leftShift" return InputSystemKeyMap.PathToKeyCode("/" + name); } private void CleanupRebindOperation() { if (rebindOperation != null) { rebindOperation.Dispose(); rebindOperation = null; } if (rebindAction != null) { rebindAction.Dispose(); rebindAction = null; } } private IEnumerator BlinkUnderscore(int index) { Text txt = GetButtonText(index); if (txt == null) yield break; while (isRebinding && rebindingIndex == index) { txt.text = "_"; yield return new WaitForSeconds(0.5f); txt.text = ""; yield return new WaitForSeconds(0.25f); } RefreshButtonLabel(index); } private void HandleRebindKey(KeyCode key) { if (key == KeyCode.Escape) { CancelRebind(); return; } if (!IsValidBindingSlot(rebindingIndex)) return; int conflictSlot = FindSlotByKey(key, rebindingIndex); KeyCode oldKey = GetKeyForSlot(rebindingIndex); if (conflictSlot >= 0) { SetKeyForSlot(rebindingIndex, key); SetKeyForSlot(conflictSlot, oldKey); string msg = $"按键冲突:{KeyBindingManager.GetDisplayName(key)}已与{GetSlotDisplayName(conflictSlot)}互换。"; ShowMessage(msg, messageErrorColor, 2f); StartCoroutine(FlashButtonInvalid(rebindingIndex)); } else { SetKeyForSlot(rebindingIndex, key); ClearMessage(); ShowMessage("按键设置已保存", messageSuccessColor, 2f); } previousKeyValid = false; previousKey = KeyCode.None; RefreshButtonLabels(); InputManager.Instance?.RefreshKeyLabels(); GameplayInputActions.Instance?.RefreshBindings(); StopRebind(); } private int FindSlotByKey(KeyCode key, int ignoredSlot) { for (int i = 0; i <= SecondaryTrack3Slot; i++) { if (i == ignoredSlot || !IsValidBindingSlot(i)) continue; if (GetKeyForSlot(i) == key) return i; } return -1; } private bool IsValidBindingSlot(int index) { return (index >= 0 && index < colors.Length) || index == SecondaryTrack3Slot; } private KeyCode GetKeyForSlot(int index) { if (index == SecondaryTrack3Slot) return KeyBindingManager.GetSecondaryKeyForTrack3(); if (index >= 0 && index < colors.Length) return KeyBindingManager.GetKeyForColor(colors[index]); return KeyCode.None; } private void SetKeyForSlot(int index, KeyCode key) { if (index == SecondaryTrack3Slot) { KeyBindingManager.ChangeSecondaryTrack3KeyBinding(key); return; } if (index >= 0 && index < colors.Length) { KeyBindingManager.ChangeKeyBinding(colors[index], key); } } private string GetSlotDisplayName(int index) { if (index >= 0 && index < bindingDisplayNames.Length) return bindingDisplayNames[index]; return $"键位{index + 1}"; } private void ShowMessage(string message, Color color, float duration = 2f) { if (errorText == null) return; if (messageCoroutine != null) StopCoroutine(messageCoroutine); messageCoroutine = StartCoroutine(ShowMessageCoroutine(message, color, duration)); } private IEnumerator ShowMessageCoroutine(string message, Color color, float duration) { errorText.text = message; errorText.color = color; float t = 0f; while (t < duration) { t += Time.deltaTime; yield return null; } errorText.text = string.Empty; messageCoroutine = null; } private void ShowPersistentMessage(string message, Color color) { if (errorText == null) return; if (messageCoroutine != null) { StopCoroutine(messageCoroutine); messageCoroutine = null; } errorText.text = message; errorText.color = color; } private void ClearMessage() { if (messageCoroutine != null) { StopCoroutine(messageCoroutine); messageCoroutine = null; } if (errorText != null) errorText.text = string.Empty; } private IEnumerator FlashButtonInvalid(int index) { Button button = GetButton(index); if (button == null || button.image == null) yield break; Color original = button.image.color; button.image.color = Color.red; yield return new WaitForSeconds(0.35f); button.image.color = original; } private void CancelRebind() { CleanupRebindOperation(); if (previousKeyValid && IsValidBindingSlot(rebindingIndex)) { SetKeyForSlot(rebindingIndex, previousKey); InputManager.Instance?.RefreshKeyLabels(); GameplayInputActions.Instance?.RefreshBindings(); ClearMessage(); ShowMessage("按键设置已保存", messageSuccessColor, 2f); } previousKeyValid = false; previousKey = KeyCode.None; StopRebind(); } private void StopRebind() { isRebinding = false; rebindingIndex = -1; if (blinkCoroutine != null) { StopCoroutine(blinkCoroutine); blinkCoroutine = null; } RefreshButtonLabels(); } private void RefreshButtonLabels() { for (int i = 0; i < colors.Length; i++) { RefreshButtonLabel(i); } RefreshButtonLabel(SecondaryTrack3Slot); } private void RefreshButtonLabel(int index) { Text txt = GetButtonText(index); if (txt == null) return; txt.text = KeyBindingManager.GetDisplayName(GetKeyForSlot(index)); } private Button GetButton(int index) { if (index == SecondaryTrack3Slot) return secondary_track3Button; if (keyButtons == null || index < 0 || index >= keyButtons.Length) return null; return keyButtons[index]; } private Text GetButtonText(int index) { Button button = GetButton(index); return button != null ? button.GetComponentInChildren() : null; } private void OnNoteSpeedSliderChanged(float value) { UpdateNoteSpeedText(value); SaveNoteSpeedValue(value); } private void OnNoteSpeedSliderPointerUp() { if (noteSpeedMultipler_slider == null) return; PlayerPrefs.SetFloat(NoteSpeedPrefKey, noteSpeedMultipler_slider.value); PlayerPrefs.Save(); } private void UpdateNoteSpeedText(float value) { if (noteSpeedMultipler_valueText == null) return; int percentage = Mathf.RoundToInt(value * 100f); noteSpeedMultipler_valueText.text = $"{percentage}%"; } private void SaveNoteSpeedValue(float value) { float v = Mathf.Clamp(value, NoteSpeedMin, NoteSpeedMax); PlayerPrefs.SetFloat(NoteSpeedPrefKey, v); PlayerPrefs.Save(); } private void ChangeNoteSpeedBy(float delta) { if (noteSpeedMultipler_slider == null) return; float current = noteSpeedMultipler_slider.value; float newValue = current; const float eps = 1e-5f; if (delta > 0f) { float ceilStep = Mathf.Ceil(current / StepAmount) * StepAmount; newValue = Mathf.Abs(current - ceilStep) < eps ? Mathf.Min(current + StepAmount, NoteSpeedMax) : Mathf.Min(ceilStep, NoteSpeedMax); } else if (delta < 0f) { float floorStep = Mathf.Floor(current / StepAmount) * StepAmount; newValue = Mathf.Abs(current - floorStep) < eps ? Mathf.Max(current - StepAmount, NoteSpeedMin) : Mathf.Max(floorStep, NoteSpeedMin); } newValue = Mathf.Clamp(newValue, NoteSpeedMin, NoteSpeedMax); newValue = (float)Math.Round(newValue, 3); noteSpeedMultipler_slider.value = newValue; } private void ResetNoteSpeed() { if (noteSpeedMultipler_slider == null) return; noteSpeedMultipler_slider.value = NoteSpeedDefault; SaveNoteSpeedValue(NoteSpeedDefault); UpdateNoteSpeedText(NoteSpeedDefault); } private static void EnsureDefaultNoteSpeedPreference() { if (PlayerPrefs.HasKey(NoteSpeedPrefKey)) { int version = PlayerPrefs.GetInt(NoteSpeedDefaultVersionKey, 0); float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault); if (version < NoteSpeedDefaultVersion && Mathf.Approximately(saved, 3f)) { saved = NoteSpeedDefault; } saved = Mathf.Clamp(saved, NoteSpeedMin, NoteSpeedMax); PlayerPrefs.SetFloat(NoteSpeedPrefKey, saved); PlayerPrefs.SetInt(NoteSpeedDefaultVersionKey, NoteSpeedDefaultVersion); PlayerPrefs.Save(); return; } PlayerPrefs.SetFloat(NoteSpeedPrefKey, NoteSpeedDefault); PlayerPrefs.SetInt(NoteSpeedDefaultVersionKey, NoteSpeedDefaultVersion); PlayerPrefs.Save(); } private void AddButtonContinuousEvents(Button button, float delta) { EventTrigger trigger = button.gameObject.GetComponent(); if (trigger == null) trigger = button.gameObject.AddComponent(); EventTrigger.Entry downEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown }; downEntry.callback.AddListener(_ => StartContinuousChange(delta)); trigger.triggers.Add(downEntry); EventTrigger.Entry upEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp }; upEntry.callback.AddListener(_ => StopContinuousChange()); trigger.triggers.Add(upEntry); EventTrigger.Entry exitEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit }; exitEntry.callback.AddListener(_ => StopContinuousChange()); trigger.triggers.Add(exitEntry); } private void StartContinuousChange(float delta) { ChangeNoteSpeedBy(delta); if (continuousChangeCoroutine != null) StopCoroutine(continuousChangeCoroutine); continuousChangeCoroutine = StartCoroutine(ContinuousChangeRoutine(delta)); } private void StopContinuousChange() { if (continuousChangeCoroutine == null) return; StopCoroutine(continuousChangeCoroutine); continuousChangeCoroutine = null; } private IEnumerator ContinuousChangeRoutine(float delta) { yield return new WaitForSeconds(ContinuousInitialDelay); while (true) { ChangeNoteSpeedBy(delta); yield return new WaitForSeconds(ContinuousRepeatRate); } } }