加入了浮动小游戏功能和设置界面
运行时请手动修改运行库目录fdBrowser
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
public class controllerSettings : MonoBehaviour
|
||||
{
|
||||
[Header("按键绑定")]
|
||||
public Button[] keyButtons; // assign 5 buttons in Inspector
|
||||
public Text errorText; // UI text to show error/success messages
|
||||
[Header("Message Colors")]
|
||||
public Color messageErrorColor = Color.red;
|
||||
public Color messageSuccessColor = Color.green;
|
||||
private Coroutine messageCoroutine = null;
|
||||
|
||||
[Header("音符流动速度滑动变阻器")]
|
||||
public Slider noteSpeedMultipler_slider;
|
||||
public Text noteSpeedMultipler_valueText;
|
||||
public Button noteSpeed_DecreaseButton;
|
||||
public Button noteSpeed_IncreaseButton;
|
||||
public Button noteSpeed_ResetButton;
|
||||
|
||||
// internal colors/order must match KeyBindingManager usage
|
||||
private readonly string[] colors = new string[] { "red", "green", "yellow", "purple", "blue" };
|
||||
|
||||
private bool isRebinding = false;
|
||||
private int rebindingIndex = -1;
|
||||
private Coroutine blinkCoroutine = null;
|
||||
|
||||
// store previous key so we can restore if user cancels
|
||||
private KeyCode previousKey = KeyCode.None;
|
||||
private bool previousKeyValid = false;
|
||||
|
||||
// slider range
|
||||
private const float NoteSpeedMin = 0.8f;
|
||||
private const float NoteSpeedMax = 1.25f;
|
||||
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
|
||||
|
||||
// continuous change
|
||||
private Coroutine continuousChangeCoroutine = null;
|
||||
private const float ContinuousInitialDelay = 0.5f; // increased to avoid accidental long-press
|
||||
private const float ContinuousRepeatRate = 0.05f;
|
||||
private const float StepAmount = 0.01f;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// initialize button labels from KeyBindingManager
|
||||
RefreshButtonLabels();
|
||||
|
||||
// initialize errorText
|
||||
if (errorText != null)
|
||||
{
|
||||
errorText.text = string.Empty;
|
||||
}
|
||||
|
||||
// hook up listeners for key buttons
|
||||
if (keyButtons != null)
|
||||
{
|
||||
for (int i = 0; i < keyButtons.Length && i < colors.Length; i++)
|
||||
{
|
||||
int idx = i;
|
||||
keyButtons[i].onClick.RemoveAllListeners();
|
||||
keyButtons[i].onClick.AddListener(() => OnKeyButtonClicked(idx));
|
||||
}
|
||||
}
|
||||
|
||||
// initialize slider
|
||||
if (noteSpeedMultipler_slider != null)
|
||||
{
|
||||
noteSpeedMultipler_slider.minValue = NoteSpeedMin;
|
||||
noteSpeedMultipler_slider.maxValue = NoteSpeedMax;
|
||||
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, 1.0f);
|
||||
saved = Mathf.Clamp(saved, NoteSpeedMin, NoteSpeedMax);
|
||||
noteSpeedMultipler_slider.value = saved;
|
||||
UpdateNoteSpeedText(saved);
|
||||
|
||||
noteSpeedMultipler_slider.onValueChanged.RemoveAllListeners();
|
||||
noteSpeedMultipler_slider.onValueChanged.AddListener(OnNoteSpeedSliderChanged);
|
||||
|
||||
// Add PointerUp event to save value when sliding ends (kept for compatibility)
|
||||
EventTrigger trigger = noteSpeedMultipler_slider.gameObject.GetComponent<EventTrigger>();
|
||||
if (trigger == null) trigger = noteSpeedMultipler_slider.gameObject.AddComponent<EventTrigger>();
|
||||
trigger.triggers.RemoveAll(e => e.eventID == EventTriggerType.PointerUp);
|
||||
var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
|
||||
entry.callback.AddListener((data) => { OnNoteSpeedSliderPointerUp(); });
|
||||
trigger.triggers.Add(entry);
|
||||
}
|
||||
|
||||
// setup buttons
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (keyButtons != null)
|
||||
{
|
||||
foreach (var b in keyButtons)
|
||||
if (b != null) b.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
if (noteSpeedMultipler_slider != null)
|
||||
{
|
||||
noteSpeedMultipler_slider.onValueChanged.RemoveListener(OnNoteSpeedSliderChanged);
|
||||
// Do not remove EventTrigger to avoid affecting other listeners, but it's okay on destroy
|
||||
}
|
||||
|
||||
if (noteSpeed_DecreaseButton != null) noteSpeed_DecreaseButton.onClick.RemoveAllListeners();
|
||||
if (noteSpeed_IncreaseButton != null) noteSpeed_IncreaseButton.onClick.RemoveAllListeners();
|
||||
if (noteSpeed_ResetButton != null) noteSpeed_ResetButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// If waiting for a new key, also cancel if user clicks outside UI/buttons
|
||||
if (isRebinding)
|
||||
{
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
if (!IsPointerOverAnyKeyButton())
|
||||
{
|
||||
// clicked outside the key buttons while rebinding -> cancel and restore
|
||||
CancelRebind();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRebinding) return;
|
||||
|
||||
// detect any keydown by iterating KeyCode values
|
||||
foreach (KeyCode kc in Enum.GetValues(typeof(KeyCode)))
|
||||
{
|
||||
// ignore mouse buttons and joystick buttons
|
||||
if (kc >= KeyCode.Mouse0 && kc <= KeyCode.Mouse6) continue;
|
||||
if (kc >= KeyCode.JoystickButton0 && kc <= KeyCode.Joystick8Button19) continue;
|
||||
if (Input.GetKeyDown(kc))
|
||||
{
|
||||
HandleRebindKey(kc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointerOverAnyKeyButton()
|
||||
{
|
||||
if (EventSystem.current == null) return false;
|
||||
var pointerData = new PointerEventData(EventSystem.current) { position = Input.mousePosition };
|
||||
var results = new List<RaycastResult>();
|
||||
EventSystem.current.RaycastAll(pointerData, results);
|
||||
if (results.Count == 0) return false;
|
||||
foreach (var res in results)
|
||||
{
|
||||
var go = res.gameObject;
|
||||
// check if this gameobject is one of the keyButtons or a child of one
|
||||
for (int i = 0; i < keyButtons.Length; i++)
|
||||
{
|
||||
var btn = keyButtons[i];
|
||||
if (btn == null) continue;
|
||||
if (go == btn.gameObject || go.transform.IsChildOf(btn.transform))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnKeyButtonClicked(int index)
|
||||
{
|
||||
if (isRebinding) return; // ignore while another rebind in progress
|
||||
if (index < 0 || index >= colors.Length) return;
|
||||
|
||||
isRebinding = true;
|
||||
rebindingIndex = index;
|
||||
// remember previous key so we can restore if user cancels
|
||||
previousKey = KeyBindingManager.GetKeyForColor(colors[index]);
|
||||
previousKeyValid = true;
|
||||
// start blinking underscore on the clicked button
|
||||
blinkCoroutine = StartCoroutine(BlinkUnderscore(index));
|
||||
// show persistent prompt
|
||||
ShowPersistentMessage("按下新按键", messageErrorColor);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// restore label when finished
|
||||
RefreshButtonLabel(index);
|
||||
}
|
||||
|
||||
private void HandleRebindKey(KeyCode key)
|
||||
{
|
||||
// ignore ESC
|
||||
if (key == KeyCode.Escape)
|
||||
{
|
||||
CancelRebind();
|
||||
return;
|
||||
}
|
||||
|
||||
// check if key already assigned to another color
|
||||
for (int i = 0; i < colors.Length; i++)
|
||||
{
|
||||
if (i == rebindingIndex) continue;
|
||||
KeyCode existing = KeyBindingManager.GetKeyForColor(colors[i]);
|
||||
if (existing == key)
|
||||
{
|
||||
// conflict: reject and keep waiting
|
||||
string msg = $"按键冲突:{KeyBindingManager.GetDisplayName(key)}已被分配给按键{i+1}。";
|
||||
ShowMessage(msg, messageErrorColor, 2f);
|
||||
StartCoroutine(FlashButtonInvalid(rebindingIndex));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// accept binding
|
||||
KeyBindingManager.ChangeKeyBinding(colors[rebindingIndex], key);
|
||||
// update UI labels
|
||||
RefreshButtonLabel(rebindingIndex);
|
||||
InputManager.Instance?.RefreshKeyLabels();
|
||||
|
||||
// clear persistent prompt and show success message
|
||||
ClearMessage();
|
||||
ShowMessage("按键设置已保存", messageSuccessColor, 2f);
|
||||
|
||||
// finish
|
||||
previousKeyValid = false;
|
||||
previousKey = KeyCode.None;
|
||||
StopRebind();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Show a persistent message (until explicitly cleared)
|
||||
private void ShowPersistentMessage(string message, Color color)
|
||||
{
|
||||
if (errorText == null) return;
|
||||
// stop any timed message
|
||||
if (messageCoroutine != null)
|
||||
{
|
||||
StopCoroutine(messageCoroutine);
|
||||
messageCoroutine = null;
|
||||
}
|
||||
errorText.text = message;
|
||||
errorText.color = color;
|
||||
}
|
||||
|
||||
// Clear any displayed message immediately
|
||||
private void ClearMessage()
|
||||
{
|
||||
if (messageCoroutine != null)
|
||||
{
|
||||
StopCoroutine(messageCoroutine);
|
||||
messageCoroutine = null;
|
||||
}
|
||||
if (errorText != null) errorText.text = string.Empty;
|
||||
}
|
||||
|
||||
private IEnumerator FlashButtonInvalid(int index)
|
||||
{
|
||||
Button b = GetButton(index);
|
||||
if (b == null) yield break;
|
||||
Color original = b.image.color;
|
||||
b.image.color = Color.red;
|
||||
yield return new WaitForSeconds(0.35f);
|
||||
b.image.color = original;
|
||||
}
|
||||
|
||||
private void CancelRebind()
|
||||
{
|
||||
// restore previous key if we have one
|
||||
if (previousKeyValid && rebindingIndex >= 0 && rebindingIndex < colors.Length)
|
||||
{
|
||||
KeyBindingManager.ChangeKeyBinding(colors[rebindingIndex], previousKey);
|
||||
InputManager.Instance?.RefreshKeyLabels();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// update RefreshButtonLabel to use display name
|
||||
private void RefreshButtonLabel(int index)
|
||||
{
|
||||
Text txt = GetButtonText(index);
|
||||
if (txt == null) return;
|
||||
KeyCode k = KeyBindingManager.GetKeyForColor(colors[index]);
|
||||
txt.text = KeyBindingManager.GetDisplayName(k);
|
||||
}
|
||||
|
||||
private Button GetButton(int index)
|
||||
{
|
||||
if (keyButtons == null) return null;
|
||||
if (index < 0 || index >= keyButtons.Length) return null;
|
||||
return keyButtons[index];
|
||||
}
|
||||
|
||||
private Text GetButtonText(int index)
|
||||
{
|
||||
Button b = GetButton(index);
|
||||
if (b == null) return null;
|
||||
Text t = b.GetComponentInChildren<Text>();
|
||||
return t;
|
||||
}
|
||||
|
||||
private void OnNoteSpeedSliderChanged(float value)
|
||||
{
|
||||
UpdateNoteSpeedText(value);
|
||||
// save on every change
|
||||
SaveNoteSpeedValue(value);
|
||||
}
|
||||
|
||||
private void OnNoteSpeedSliderPointerUp()
|
||||
{
|
||||
if (noteSpeedMultipler_slider != null)
|
||||
{
|
||||
float value = noteSpeedMultipler_slider.value;
|
||||
PlayerPrefs.SetFloat(NoteSpeedPrefKey, value);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNoteSpeedText(float value)
|
||||
{
|
||||
if (noteSpeedMultipler_valueText != null)
|
||||
{
|
||||
noteSpeedMultipler_valueText.text = value.ToString("F3");
|
||||
}
|
||||
}
|
||||
|
||||
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 cur = noteSpeedMultipler_slider.value;
|
||||
float step = StepAmount;
|
||||
float newVal = cur;
|
||||
const float eps = 1e-5f;
|
||||
if (delta > 0f)
|
||||
{
|
||||
float ceilStep = Mathf.Ceil(cur / step) * step;
|
||||
if (Mathf.Abs(cur - ceilStep) < eps)
|
||||
{
|
||||
newVal = Mathf.Min(cur + step, NoteSpeedMax);
|
||||
}
|
||||
else
|
||||
{
|
||||
newVal = Mathf.Min(ceilStep, NoteSpeedMax);
|
||||
}
|
||||
}
|
||||
else if (delta < 0f)
|
||||
{
|
||||
float floorStep = Mathf.Floor(cur / step) * step;
|
||||
if (Mathf.Abs(cur - floorStep) < eps)
|
||||
{
|
||||
newVal = Mathf.Max(cur - step, NoteSpeedMin);
|
||||
}
|
||||
else
|
||||
{
|
||||
newVal = Mathf.Max(floorStep, NoteSpeedMin);
|
||||
}
|
||||
}
|
||||
newVal = Mathf.Clamp(newVal, NoteSpeedMin, NoteSpeedMax);
|
||||
// round to 3 decimals for display and consistency
|
||||
newVal = (float)System.Math.Round(newVal, 3);
|
||||
noteSpeedMultipler_slider.value = newVal; // will trigger OnNoteSpeedSliderChanged and save
|
||||
}
|
||||
|
||||
private void ResetNoteSpeed()
|
||||
{
|
||||
if (noteSpeedMultipler_slider == null) return;
|
||||
noteSpeedMultipler_slider.value = 1.0f; // triggers change and save
|
||||
SaveNoteSpeedValue(1.0f);
|
||||
UpdateNoteSpeedText(1.0f);
|
||||
}
|
||||
|
||||
private void AddButtonContinuousEvents(Button btn, float delta)
|
||||
{
|
||||
EventTrigger trigger = btn.gameObject.GetComponent<EventTrigger>();
|
||||
if (trigger == null) trigger = btn.gameObject.AddComponent<EventTrigger>();
|
||||
|
||||
// PointerDown -> start continuous change
|
||||
var downEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
|
||||
downEntry.callback.AddListener((data) => { StartContinuousChange(delta); });
|
||||
trigger.triggers.Add(downEntry);
|
||||
|
||||
// PointerUp -> stop continuous change
|
||||
var upEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
|
||||
upEntry.callback.AddListener((data) => { StopContinuousChange(); });
|
||||
trigger.triggers.Add(upEntry);
|
||||
|
||||
// Also stop on PointerExit in case cursor leaves button while pressed
|
||||
var exitEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit };
|
||||
exitEntry.callback.AddListener((data) => { StopContinuousChange(); });
|
||||
trigger.triggers.Add(exitEntry);
|
||||
}
|
||||
|
||||
private void StartContinuousChange(float delta)
|
||||
{
|
||||
// perform immediate single step
|
||||
ChangeNoteSpeedBy(delta);
|
||||
if (continuousChangeCoroutine != null) StopCoroutine(continuousChangeCoroutine);
|
||||
continuousChangeCoroutine = StartCoroutine(ContinuousChangeRoutine(delta));
|
||||
}
|
||||
|
||||
private void StopContinuousChange()
|
||||
{
|
||||
if (continuousChangeCoroutine != null)
|
||||
{
|
||||
StopCoroutine(continuousChangeCoroutine);
|
||||
continuousChangeCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator ContinuousChangeRoutine(float delta)
|
||||
{
|
||||
yield return new WaitForSeconds(ContinuousInitialDelay);
|
||||
while (true)
|
||||
{
|
||||
ChangeNoteSpeedBy(delta);
|
||||
yield return new WaitForSeconds(ContinuousRepeatRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47a45212060d63a4f98f13e30622253a
|
||||
@@ -0,0 +1,471 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public class graphicSettings : MonoBehaviour
|
||||
{
|
||||
public Dropdown screenMode_Dropdown;
|
||||
public Dropdown resolution_Dropdown;
|
||||
public Dropdown frameRate_Dropdown;
|
||||
|
||||
public Image resolutionLock_Image;
|
||||
|
||||
private List<Vector2Int> availableResolutions = new List<Vector2Int>();
|
||||
private int tempFreeResOptionIndex = -1;
|
||||
private Vector2Int lastScreenSize;
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitScreenModeDropdown();
|
||||
InitResolutionDropdown();
|
||||
InitFrameRateDropdown();
|
||||
lastScreenSize = new Vector2Int(Screen.width, Screen.height);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (screenMode_Dropdown != null)
|
||||
screenMode_Dropdown.onValueChanged.RemoveListener(OnScreenModeChanged);
|
||||
if (resolution_Dropdown != null)
|
||||
resolution_Dropdown.onValueChanged.RemoveListener(OnResolutionChanged);
|
||||
if (frameRate_Dropdown != null)
|
||||
frameRate_Dropdown.onValueChanged.RemoveListener(OnFrameRateChanged);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Screen.width != lastScreenSize.x || Screen.height != lastScreenSize.y)
|
||||
{
|
||||
lastScreenSize.x = Screen.width;
|
||||
lastScreenSize.y = Screen.height;
|
||||
OnUserResolutionChanged(new Vector2Int(Screen.width, Screen.height));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUserResolutionChanged(Vector2Int newSize)
|
||||
{
|
||||
if (Screen.fullScreenMode != FullScreenMode.Windowed)
|
||||
return;
|
||||
|
||||
int match = availableResolutions.FindIndex(v => v.x == newSize.x && v.y == newSize.y);
|
||||
if (match >= 0)
|
||||
{
|
||||
RemoveTempFreeOptionIfExists();
|
||||
resolution_Dropdown.SetValueWithoutNotify(match);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureTempFreeOptionExists();
|
||||
resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureTempFreeOptionExists()
|
||||
{
|
||||
if (resolution_Dropdown == null) return;
|
||||
if (tempFreeResOptionIndex >= 0) return;
|
||||
var opt = new Dropdown.OptionData("自由分辨率");
|
||||
resolution_Dropdown.options.Add(opt);
|
||||
tempFreeResOptionIndex = resolution_Dropdown.options.Count - 1;
|
||||
}
|
||||
|
||||
private void RemoveTempFreeOptionIfExists()
|
||||
{
|
||||
if (resolution_Dropdown == null) return;
|
||||
if (tempFreeResOptionIndex < 0) return;
|
||||
if (tempFreeResOptionIndex < resolution_Dropdown.options.Count)
|
||||
resolution_Dropdown.options.RemoveAt(tempFreeResOptionIndex);
|
||||
tempFreeResOptionIndex = -1;
|
||||
}
|
||||
|
||||
private void InitScreenModeDropdown()
|
||||
{
|
||||
if (screenMode_Dropdown == null) return;
|
||||
|
||||
var options = new List<string>() { "窗口(自由窗口)", "全屏(自适应)", "无边框窗口" };
|
||||
screenMode_Dropdown.ClearOptions();
|
||||
screenMode_Dropdown.AddOptions(options);
|
||||
|
||||
int current = MapFullScreenModeToIndex(Screen.fullScreenMode);
|
||||
if (PlayerPrefs.HasKey("screenMode"))
|
||||
{
|
||||
int saved = PlayerPrefs.GetInt("screenMode", current);
|
||||
saved = Mathf.Clamp(saved, 0, options.Count - 1);
|
||||
current = saved;
|
||||
ApplyScreenMode(current);
|
||||
}
|
||||
|
||||
screenMode_Dropdown.SetValueWithoutNotify(current);
|
||||
screenMode_Dropdown.onValueChanged.AddListener(OnScreenModeChanged);
|
||||
}
|
||||
|
||||
private int MapFullScreenModeToIndex(FullScreenMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case FullScreenMode.Windowed:
|
||||
return 0;
|
||||
case FullScreenMode.ExclusiveFullScreen:
|
||||
return 1;
|
||||
case FullScreenMode.FullScreenWindow:
|
||||
return 2;
|
||||
case FullScreenMode.MaximizedWindow:
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScreenModeChanged(int index)
|
||||
{
|
||||
ApplyScreenMode(index);
|
||||
PlayerPrefs.SetInt("screenMode", index);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
bool isWindowed = (index == 0);
|
||||
if (resolutionLock_Image != null)
|
||||
resolutionLock_Image.gameObject.SetActive(!isWindowed);
|
||||
if (resolution_Dropdown != null)
|
||||
resolution_Dropdown.interactable = isWindowed;
|
||||
}
|
||||
|
||||
private void ApplyScreenMode(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.Windowed;
|
||||
Screen.fullScreen = false;
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(ApplyWindowStyleNextFrame(true));
|
||||
Screen.SetResolution(Screen.width, Screen.height, FullScreenMode.Windowed);
|
||||
}
|
||||
else if (index == 1)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
|
||||
Screen.fullScreen = true;
|
||||
Resolution res = Screen.currentResolution;
|
||||
Screen.SetResolution(res.width, res.height, FullScreenMode.ExclusiveFullScreen, res.refreshRate);
|
||||
}
|
||||
else if (index == 2)
|
||||
{
|
||||
Screen.fullScreenMode = FullScreenMode.FullScreenWindow;
|
||||
Screen.fullScreen = true;
|
||||
Resolution res = Screen.currentResolution;
|
||||
Screen.SetResolution(res.width, res.height, FullScreenMode.FullScreenWindow, res.refreshRate);
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(ApplyWindowStyleNextFrame(false));
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"ApplyScreenMode failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator ApplyWindowStyleNextFrame(bool enable)
|
||||
{
|
||||
yield return null;
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
EnableWindowedModeResizable(enable);
|
||||
}
|
||||
|
||||
private void EnableWindowedModeResizable(bool enable)
|
||||
{
|
||||
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
|
||||
try
|
||||
{
|
||||
IntPtr hWnd = GetForegroundWindow();
|
||||
if (hWnd == IntPtr.Zero) return;
|
||||
|
||||
const int GWL_STYLE = -16;
|
||||
const int WS_OVERLAPPEDWINDOW = unchecked((int)0x00CF0000);
|
||||
const int WS_POPUP = unchecked((int)0x80000000);
|
||||
|
||||
if (IntPtr.Size == 8)
|
||||
{
|
||||
long style = GetWindowLongPtr64(hWnd, GWL_STYLE);
|
||||
if (enable)
|
||||
{
|
||||
style &= ~((long)WS_POPUP);
|
||||
style |= WS_OVERLAPPEDWINDOW;
|
||||
}
|
||||
else
|
||||
{
|
||||
style &= ~((long)WS_OVERLAPPEDWINDOW);
|
||||
style |= (long)WS_POPUP;
|
||||
}
|
||||
SetWindowLongPtr64(hWnd, GWL_STYLE, style);
|
||||
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
else
|
||||
{
|
||||
int style = GetWindowLong32(hWnd, GWL_STYLE);
|
||||
if (enable)
|
||||
{
|
||||
style &= ~WS_POPUP;
|
||||
style |= WS_OVERLAPPEDWINDOW;
|
||||
}
|
||||
else
|
||||
{
|
||||
style &= ~WS_OVERLAPPEDWINDOW;
|
||||
style |= WS_POPUP;
|
||||
}
|
||||
SetWindowLong32(hWnd, GWL_STYLE, style);
|
||||
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"EnableWindowedModeResizable failed: {e}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitResolutionDropdown()
|
||||
{
|
||||
if (resolution_Dropdown == null) return;
|
||||
|
||||
var candidates = new List<Vector2Int>() {
|
||||
new Vector2Int(1024, 576),
|
||||
new Vector2Int(1152, 648),
|
||||
new Vector2Int(1280, 720),
|
||||
new Vector2Int(1366, 768),
|
||||
new Vector2Int(1600, 900),
|
||||
new Vector2Int(1920, 1080),
|
||||
new Vector2Int(2560, 1440),
|
||||
new Vector2Int(3440, 1440),
|
||||
new Vector2Int(3200, 1800),
|
||||
new Vector2Int(3840, 2160),
|
||||
new Vector2Int(5120, 2880),
|
||||
new Vector2Int(7680, 4320),
|
||||
new Vector2Int(15360, 8640)
|
||||
};
|
||||
|
||||
int maxW = Screen.currentResolution.width;
|
||||
int maxH = Screen.currentResolution.height;
|
||||
const int MIN_W = 1024;
|
||||
const int MIN_H = 720;
|
||||
const int MAX_DIM = 16384;
|
||||
|
||||
availableResolutions.Clear();
|
||||
var options = new List<string>();
|
||||
|
||||
foreach (var c in candidates)
|
||||
{
|
||||
if (c.x < MIN_W || c.y < MIN_H) continue;
|
||||
if (c.x > MAX_DIM || c.y > MAX_DIM) continue;
|
||||
if (c.x > maxW || c.y > maxH) continue;
|
||||
availableResolutions.Add(c);
|
||||
}
|
||||
|
||||
if (availableResolutions.Count == 0)
|
||||
{
|
||||
var cur = new Vector2Int(Screen.width, Screen.height);
|
||||
if (cur.x >= MIN_W && cur.y >= MIN_H)
|
||||
availableResolutions.Add(cur);
|
||||
}
|
||||
|
||||
foreach (var r in availableResolutions)
|
||||
{
|
||||
options.Add($"{r.x} × {r.y}");
|
||||
}
|
||||
|
||||
resolution_Dropdown.ClearOptions();
|
||||
resolution_Dropdown.AddOptions(options);
|
||||
|
||||
int selected = 0;
|
||||
for (int i = 0; i < availableResolutions.Count; i++)
|
||||
{
|
||||
if (availableResolutions[i].x == Screen.width && availableResolutions[i].y == Screen.height)
|
||||
{
|
||||
selected = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int savedResIndex = PlayerPrefs.GetInt("resolutionIndex", -2);
|
||||
if (savedResIndex >= 0 && savedResIndex < availableResolutions.Count)
|
||||
{
|
||||
resolution_Dropdown.SetValueWithoutNotify(savedResIndex);
|
||||
var sav = availableResolutions[savedResIndex];
|
||||
Screen.SetResolution(sav.x, sav.y, Screen.fullScreenMode, Screen.currentResolution.refreshRate);
|
||||
RemoveTempFreeOptionIfExists();
|
||||
}
|
||||
else if (savedResIndex == -1)
|
||||
{
|
||||
int cw = PlayerPrefs.GetInt("customResW", -1);
|
||||
int ch = PlayerPrefs.GetInt("customResH", -1);
|
||||
if (cw > 0 && ch > 0)
|
||||
{
|
||||
Screen.SetResolution(cw, ch, Screen.fullScreenMode, Screen.currentResolution.refreshRate);
|
||||
EnsureTempFreeOptionExists();
|
||||
resolution_Dropdown.options[tempFreeResOptionIndex].text = $"{cw} × {ch}";
|
||||
resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
resolution_Dropdown.SetValueWithoutNotify(selected);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resolution_Dropdown.SetValueWithoutNotify(selected);
|
||||
}
|
||||
resolution_Dropdown.onValueChanged.AddListener(OnResolutionChanged);
|
||||
|
||||
int screenMode = PlayerPrefs.GetInt("screenMode", MapFullScreenModeToIndex(Screen.fullScreenMode));
|
||||
bool isWindowed = (screenMode == 0) || (Screen.fullScreenMode == FullScreenMode.Windowed);
|
||||
if (resolutionLock_Image != null)
|
||||
resolutionLock_Image.gameObject.SetActive(!isWindowed);
|
||||
resolution_Dropdown.interactable = isWindowed;
|
||||
}
|
||||
|
||||
private void SyncResolutionDropdownToCurrent()
|
||||
{
|
||||
if (resolution_Dropdown == null) return;
|
||||
int idx = availableResolutions.FindIndex(v => v.x == Screen.width && v.y == Screen.height);
|
||||
if (idx >= 0)
|
||||
{
|
||||
RemoveTempFreeOptionIfExists();
|
||||
resolution_Dropdown.SetValueWithoutNotify(idx);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureTempFreeOptionExists();
|
||||
resolution_Dropdown.options[tempFreeResOptionIndex].text = $"{Screen.width} × {Screen.height}";
|
||||
resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResolutionChanged(int index)
|
||||
{
|
||||
if (index < 0) return;
|
||||
if (index >= availableResolutions.Count)
|
||||
{
|
||||
PlayerPrefs.SetInt("resolutionIndex", -1);
|
||||
PlayerPrefs.SetInt("customResW", Screen.width);
|
||||
PlayerPrefs.SetInt("customResH", Screen.height);
|
||||
PlayerPrefs.Save();
|
||||
return;
|
||||
}
|
||||
if (index >= availableResolutions.Count) return;
|
||||
var r = availableResolutions[index];
|
||||
FullScreenMode mode = Screen.fullScreenMode;
|
||||
int refresh = Screen.currentResolution.refreshRate;
|
||||
Screen.SetResolution(r.x, r.y, mode, refresh);
|
||||
PlayerPrefs.SetInt("resolutionIndex", index);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
RemoveTempFreeOptionIfExists();
|
||||
|
||||
if (mode == FullScreenMode.Windowed)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(ApplyWindowStyleNextFrame(true));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitFrameRateDropdown()
|
||||
{
|
||||
if (frameRate_Dropdown == null) return;
|
||||
|
||||
var fpsOptions = new List<string>() { "24", "30", "60", "90", "120", "144", "165", "210", "240", "300", "无限制", "垂直同步" };
|
||||
frameRate_Dropdown.ClearOptions();
|
||||
frameRate_Dropdown.AddOptions(fpsOptions);
|
||||
|
||||
// Determine current setting
|
||||
int saved = PlayerPrefs.GetInt("frameRateIndex", -999);
|
||||
int selectIndex = 2; // default to 60
|
||||
if (saved != -999 && saved >= 0 && saved < fpsOptions.Count)
|
||||
{
|
||||
selectIndex = saved;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (QualitySettings.vSyncCount > 0)
|
||||
selectIndex = fpsOptions.Count - 1; // VSync index
|
||||
else
|
||||
{
|
||||
int current = Application.targetFrameRate;
|
||||
if (current <= 0)
|
||||
selectIndex = fpsOptions.Count - 2; // unlimited
|
||||
else
|
||||
{
|
||||
// find nearest match
|
||||
int[] candidates = new int[] { 24, 30, 60, 90, 120, 144, 165, 210, 240, 300 };
|
||||
int best = 0; int bestDiff = int.MaxValue;
|
||||
for (int i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
int d = Math.Abs(candidates[i] - current);
|
||||
if (d < bestDiff) { bestDiff = d; best = i; }
|
||||
}
|
||||
selectIndex = best;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frameRate_Dropdown.SetValueWithoutNotify(selectIndex);
|
||||
frameRate_Dropdown.onValueChanged.AddListener(OnFrameRateChanged);
|
||||
// apply selection
|
||||
ApplyFrameRateByIndex(selectIndex);
|
||||
}
|
||||
|
||||
private void OnFrameRateChanged(int index)
|
||||
{
|
||||
ApplyFrameRateByIndex(index);
|
||||
PlayerPrefs.SetInt("frameRateIndex", index);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
private void ApplyFrameRateByIndex(int index)
|
||||
{
|
||||
if (index < 0) return;
|
||||
int[] fpsValues = new int[] { 24, 30, 60, 90, 120, 144, 165, 210, 240, 300 };
|
||||
if (index < fpsValues.Length)
|
||||
{
|
||||
QualitySettings.vSyncCount = 0;
|
||||
Application.targetFrameRate = fpsValues[index];
|
||||
}
|
||||
else if (index == fpsValues.Length)
|
||||
{
|
||||
// 无限制
|
||||
QualitySettings.vSyncCount = 0;
|
||||
Application.targetFrameRate = -1; // unlimited / platform default
|
||||
}
|
||||
else if (index == fpsValues.Length + 1)
|
||||
{
|
||||
// 垂直同步
|
||||
QualitySettings.vSyncCount = 1; // enable vsync
|
||||
Application.targetFrameRate = -1;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "GetWindowLong")]
|
||||
private static extern int GetWindowLong32(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
|
||||
private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
|
||||
private static extern long GetWindowLongPtr64(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
|
||||
private static extern long SetWindowLongPtr64(IntPtr hWnd, int nIndex, long dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
private const uint SWP_NOSIZE = 0x0001;
|
||||
private const uint SWP_NOMOVE = 0x0002;
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_FRAMECHANGED = 0x0020;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0d532c98d6ca9a4ea5a2bd80cc4f193
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32d5540b04c3c3d41909f074d774d8bc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class offsetDeterminer : MonoBehaviour
|
||||
{
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28e4b9a4015da4847a8e725526e80be9
|
||||
Reference in New Issue
Block a user