using UnityEngine; using UnityEngine.UI; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using System.Collections.Generic; using System; using System.Runtime.InteropServices; public class graphicSettings : MonoBehaviour { private const string PrefKeyScreenMode = "screenMode"; private const string PrefKeyResolutionIndex = "resolutionIndex"; private const string PrefKeyCustomResW = "customResW"; private const string PrefKeyCustomResH = "customResH"; public Dropdown screenMode_Dropdown; public Dropdown resolution_Dropdown; public Dropdown frameRate_Dropdown; public Toggle superResolution_Toggle; public Image resolutionLock_Image; private List availableResolutions = new List(); private int tempFreeResOptionIndex = -1; private Vector2Int lastScreenSize; private const string PREF_SUPER_RES = "SuperResolution"; void Start() { InitScreenModeDropdown(); InitResolutionDropdown(); InitFrameRateDropdown(); InitSuperResolutionToggle(); lastScreenSize = new Vector2Int(Screen.width, Screen.height); } public static void ApplySavedDisplaySettingsAtStartup(MonoBehaviour coroutineRunner) { // 移动端(Android/iOS)由系统管理分辨率与全屏,没有"窗口模式"概念。 // 这里的 PC 逻辑(Screen.SetResolution 到 PC 存的窗口分辨率 / 切 Windowed 模式 / // 后续的 Win32 SetWindowLong 窗口样式操作)在手机上会破坏渲染表面,导致启动黑屏 // (只剩最顶层加载 Canvas 的 "%" 文本可见)。移动端直接跳过,交给系统与横屏设置处理。 #if UNITY_ANDROID || UNITY_IOS if (!Application.isEditor) { return; } #endif int screenMode = PlayerPrefs.GetInt(PrefKeyScreenMode, 0); screenMode = Mathf.Clamp(screenMode, 0, 2); try { ApplyScreenModeStatic(screenMode, coroutineRunner); ApplySavedResolutionStatic(); } catch (Exception ex) { Debug.LogWarning($"[graphicSettings] Failed to apply saved display settings at startup: {ex}"); } } 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); if (superResolution_Toggle != null) superResolution_Toggle.onValueChanged.RemoveListener(OnSuperResolutionChanged); } 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() { "窗口化(可调整大小)", "全屏(独占模式)", "无边框窗口" }; screenMode_Dropdown.ClearOptions(); screenMode_Dropdown.AddOptions(options); int current = MapFullScreenModeToIndex(Screen.fullScreenMode); if (PlayerPrefs.HasKey("screenMode")) { int saved = PlayerPrefs.GetInt(PrefKeyScreenMode, 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(PrefKeyScreenMode, 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) { ApplyScreenModeStatic(index, this); } private System.Collections.IEnumerator ApplyWindowStyleNextFrame(bool enable) { yield return ApplyWindowStyleNextFrameStatic(enable); } private void EnableWindowedModeResizable(bool enable) { EnableWindowedModeResizableStatic(enable); } private void InitResolutionDropdown() { if (resolution_Dropdown == null) return; var candidates = new List() { 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(); 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} 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(PrefKeyResolutionIndex, -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.refreshRateRatio); RemoveTempFreeOptionIfExists(); } else if (savedResIndex == -1) { int cw = PlayerPrefs.GetInt(PrefKeyCustomResW, -1); int ch = PlayerPrefs.GetInt(PrefKeyCustomResH, -1); if (cw > 0 && ch > 0) { Screen.SetResolution(cw, ch, Screen.fullScreenMode, Screen.currentResolution.refreshRateRatio); EnsureTempFreeOptionExists(); resolution_Dropdown.options[tempFreeResOptionIndex].text = $"{cw} x {ch}"; resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex); } else { resolution_Dropdown.SetValueWithoutNotify(selected); } } else { resolution_Dropdown.SetValueWithoutNotify(selected); } resolution_Dropdown.onValueChanged.AddListener(OnResolutionChanged); int screenMode = PlayerPrefs.GetInt(PrefKeyScreenMode, 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} x {Screen.height}"; resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex); } } private void OnResolutionChanged(int index) { if (index < 0) return; if (index >= availableResolutions.Count) { PlayerPrefs.SetInt(PrefKeyResolutionIndex, -1); PlayerPrefs.SetInt(PrefKeyCustomResW, Screen.width); PlayerPrefs.SetInt(PrefKeyCustomResH, Screen.height); PlayerPrefs.Save(); return; } if (index >= availableResolutions.Count) return; var r = availableResolutions[index]; FullScreenMode mode = Screen.fullScreenMode; Screen.SetResolution(r.x, r.y, mode, Screen.currentResolution.refreshRateRatio); PlayerPrefs.SetInt(PrefKeyResolutionIndex, index); PlayerPrefs.Save(); RemoveTempFreeOptionIfExists(); if (mode == FullScreenMode.Windowed) { StopAllCoroutines(); StartCoroutine(ApplyWindowStyleNextFrame(true)); } } private void InitFrameRateDropdown() { if (frameRate_Dropdown == null) return; var fpsOptions = new List() { "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); // default to 90hz instead of unlimited int selectIndex = 3; // index for 90 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 = 3; // default to 90 when targetFrameRate is unset/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) { // Documentation text normalized. QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; // unlimited / platform default } else if (index == fpsValues.Length + 1) { // Documentation text normalized. QualitySettings.vSyncCount = 1; // enable vsync Application.targetFrameRate = -1; } } private void InitSuperResolutionToggle() { // Default is OFF (0). bool isOn = PlayerPrefs.GetInt(PREF_SUPER_RES, 0) == 1; // Apply setting immediately to ensure correct render scale state // even if the toggle UI is missing or not assigned yet. ApplySuperResolution(isOn); if (superResolution_Toggle != null) { superResolution_Toggle.isOn = isOn; superResolution_Toggle.onValueChanged.AddListener(OnSuperResolutionChanged); } } private void OnSuperResolutionChanged(bool isOn) { PlayerPrefs.SetInt(PREF_SUPER_RES, isOn ? 1 : 0); PlayerPrefs.Save(); ApplySuperResolution(isOn); } private void ApplySuperResolution(bool isOn) { var urpAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; if (urpAsset != null) { urpAsset.renderScale = isOn ? 2.0f : 1.0f; } } private static void ApplyScreenModeStatic(int index, MonoBehaviour coroutineRunner) { try { if (index == 0) { Screen.fullScreenMode = FullScreenMode.Windowed; Screen.fullScreen = false; Screen.SetResolution(Screen.width, Screen.height, FullScreenMode.Windowed, Screen.currentResolution.refreshRateRatio); if (coroutineRunner != null) { coroutineRunner.StartCoroutine(ApplyWindowStyleNextFrameStatic(true)); } } else if (index == 1) { Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen; Screen.fullScreen = true; Resolution res = Screen.currentResolution; Screen.SetResolution(res.width, res.height, FullScreenMode.ExclusiveFullScreen, res.refreshRateRatio); } else if (index == 2) { Screen.fullScreenMode = FullScreenMode.FullScreenWindow; Screen.fullScreen = true; Resolution res = Screen.currentResolution; Screen.SetResolution(res.width, res.height, FullScreenMode.FullScreenWindow, res.refreshRateRatio); if (coroutineRunner != null) { coroutineRunner.StartCoroutine(ApplyWindowStyleNextFrameStatic(false)); } } } catch (Exception ex) { Debug.LogWarning($"ApplyScreenMode failed: {ex}"); } } private static void ApplySavedResolutionStatic() { int savedResIndex = PlayerPrefs.GetInt(PrefKeyResolutionIndex, -2); if (savedResIndex == -1) { int customWidth = PlayerPrefs.GetInt(PrefKeyCustomResW, -1); int customHeight = PlayerPrefs.GetInt(PrefKeyCustomResH, -1); if (customWidth > 0 && customHeight > 0) { Screen.SetResolution(customWidth, customHeight, Screen.fullScreenMode, Screen.currentResolution.refreshRateRatio); } return; } if (savedResIndex < 0) { return; } Vector2Int[] candidates = { 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) }; if (savedResIndex >= candidates.Length) { return; } Vector2Int resolution = candidates[savedResIndex]; Resolution currentResolution = Screen.currentResolution; if (resolution.x > currentResolution.width || resolution.y > currentResolution.height) { return; } Screen.SetResolution(resolution.x, resolution.y, Screen.fullScreenMode, currentResolution.refreshRateRatio); } private static System.Collections.IEnumerator ApplyWindowStyleNextFrameStatic(bool enable) { yield return null; yield return new WaitForSeconds(0.05f); EnableWindowedModeResizableStatic(enable); } private static void EnableWindowedModeResizableStatic(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 &= ~((int)WS_OVERLAPPEDWINDOW); style |= (int)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 } #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 }