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 // 无存档(首次运行)时,跟随 Player Settings 里的启动全屏模式,而不是硬编码窗口化(0)。 // 否则会把 ProjectSettings 的独占全屏强切成窗口化,且窗口化分支无兜底分辨率, // 导致 Unity 回落到内建默认窗口尺寸 1280x720。 int defaultScreenMode = MapFullScreenModeToIndex(Screen.fullScreenMode); int screenMode = PlayerPrefs.GetInt(PrefKeyScreenMode, defaultScreenMode); 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 static 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) { // 无任何分辨率存档(首次运行,savedResIndex == -2)。 // 全屏模式下 ApplyScreenModeStatic 已用显示器原生分辨率铺满,无需处理。 // 但窗口模式下 ApplyScreenModeStatic 只是沿用启动瞬间的窗口尺寸(常为引擎默认 1280x720), // 这里兜底到 1920x1080(受当前显示器上限约束),避免默认落到 720p。 if (Screen.fullScreenMode == FullScreenMode.Windowed) { const int FallbackW = 1920; const int FallbackH = 1080; Resolution cur = Screen.currentResolution; int w = Mathf.Min(FallbackW, cur.width); int h = Mathf.Min(FallbackH, cur.height); Screen.SetResolution(w, h, FullScreenMode.Windowed, cur.refreshRateRatio); } 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 }