using System; using System.Collections; using System.Reflection; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace AppUI { public static class GameViewSizeHelper { public const int PortraitWidth = ScreenOrientationHelper.PortraitWidth; public const int PortraitHeight = ScreenOrientationHelper.PortraitHeight; public const int LandscapeWidth = ScreenOrientationHelper.LandscapeWidth; public const int LandscapeHeight = ScreenOrientationHelper.LandscapeHeight; const string CustomSizeLabelPrefix = "SB_Fixed_"; #if UNITY_EDITOR static GameViewPrePlaySnapshot _prePlaySnapshot; public struct GameViewPrePlaySnapshot { public bool HasValue; public int GroupType; public int SizeIndex; public int ViewportWidth; public int ViewportHeight; public int ConfigWidth; public int ConfigHeight; public string SizeTypeName; } public static void CapturePrePlaySnapshot() { _prePlaySnapshot = default; Vector2 vp = Handles.GetMainGameViewSize(); _prePlaySnapshot.ViewportWidth = Mathf.RoundToInt(vp.x); _prePlaySnapshot.ViewportHeight = Mathf.RoundToInt(vp.y); if (TryGetEditorSizeContext(out Type gameViewType, out _, out _, out object group, out Type groupClass, out EditorWindow gameView)) { _prePlaySnapshot.GroupType = GetActiveGameViewSizeGroupType(gameViewType); var indexField = gameViewType.GetField( "m_SizeSelectionID", BindingFlags.Instance | BindingFlags.NonPublic); if (indexField != null) _prePlaySnapshot.SizeIndex = (int)indexField.GetValue(gameView); if (TryReadGameViewSizeAtIndex(groupClass, group, _prePlaySnapshot.SizeIndex, out Vector2 cfg, out string typeName)) { _prePlaySnapshot.ConfigWidth = Mathf.RoundToInt(cfg.x); _prePlaySnapshot.ConfigHeight = Mathf.RoundToInt(cfg.y); _prePlaySnapshot.SizeTypeName = typeName ?? ""; } } _prePlaySnapshot.HasValue = true; Debug.Log( $"[AppUI.GameViewSizeHelper] 记录 Play 前 Game 视图: " + $"视口={_prePlaySnapshot.ViewportWidth}x{_prePlaySnapshot.ViewportHeight}, " + $"index={_prePlaySnapshot.SizeIndex}, group={_prePlaySnapshot.GroupType}, " + $"配置={_prePlaySnapshot.ConfigWidth}x{_prePlaySnapshot.ConfigHeight} ({_prePlaySnapshot.SizeTypeName})"); } public static void RestorePrePlaySnapshot() { if (!ScreenOrientationEditorSettings.IsEnabled || !_prePlaySnapshot.HasValue) return; CancelEnforce(); ScheduleRestorePrePlay(120); EditorApplication.delayCall += () => TryRestorePrePlaySnapshot(logResult: true); } public static void CancelEnforce() { GameViewEnforceState.Cancel(); } static void ScheduleRestorePrePlay(int editorFrames) { if (!_prePlaySnapshot.HasValue) return; GameViewEnforceState.RequestRestore(_prePlaySnapshot, editorFrames); } static bool TryRestorePrePlaySnapshot(bool logResult = false) { if (!_prePlaySnapshot.HasValue) return false; var snap = _prePlaySnapshot; bool restored = false; if (TryGetEditorSizeContext(out Type gameViewType, out _, out _, out object group, out Type groupClass, out EditorWindow gameView)) { int count = (int)groupClass.GetMethod("GetTotalCount", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, null); if (snap.SizeIndex >= 0 && snap.SizeIndex < count) restored = ApplyGameViewSizeIndex(gameViewType, gameView, snap.SizeIndex, snap.GroupType); } if (!IsViewportMatchingTarget(snap.ViewportWidth, snap.ViewportHeight)) { if (snap.SizeTypeName == "FixedResolution" && snap.ConfigWidth > 0 && snap.ConfigHeight > 0) restored |= TrySetGameViewResolution(snap.ConfigWidth, snap.ConfigHeight, logResult: false); else restored |= TrySetGameViewResolution(snap.ViewportWidth, snap.ViewportHeight, logResult: false); } else { restored = true; } if (logResult) { Vector2 vp = Handles.GetMainGameViewSize(); Debug.Log( $"[AppUI.GameViewSizeHelper] 恢复 Play 前 Game 视图: " + $"目标视口={snap.ViewportWidth}x{snap.ViewportHeight}, " + $"当前视口={Mathf.RoundToInt(vp.x)}x{Mathf.RoundToInt(vp.y)}, ok={restored}"); } return restored; } static bool NeedsRestorePrePlaySnapshot() { if (!_prePlaySnapshot.HasValue) return false; return !IsViewportMatchingTarget(_prePlaySnapshot.ViewportWidth, _prePlaySnapshot.ViewportHeight); } public static IEnumerator EditorApplyPortraitCoroutine(float maxWaitSeconds = 2f) { if (!ScreenOrientationEditorSettings.IsEnabled) yield break; yield return EditorSetResolutionCoroutine(PortraitWidth, PortraitHeight, maxWaitSeconds); } public static IEnumerator EditorSwapOrientationCoroutine(bool toLandscape, float maxWaitSeconds = 3f) { if (!ScreenOrientationEditorSettings.IsEnabled) yield break; int w = toLandscape ? LandscapeWidth : PortraitWidth; int h = toLandscape ? LandscapeHeight : PortraitHeight; yield return EditorSetResolutionCoroutine(w, h, maxWaitSeconds); } public static void ApplyPortraitResolution() { if (!ScreenOrientationEditorSettings.IsEnabled) return; ScheduleEnforce(PortraitWidth, PortraitHeight, 90); } public static void ScheduleEnforce(int width, int height, int editorFrames = 60) { if (!ScreenOrientationEditorSettings.IsEnabled) return; GameViewEnforceState.Request(width, height, editorFrames); } public static bool IsViewportMatchingTarget(int width, int height) { Vector2 vp = Handles.GetMainGameViewSize(); return Mathf.RoundToInt(vp.x) == width && Mathf.RoundToInt(vp.y) == height; } public static bool NeedsConfigEnforce(int width, int height) { if (IsViewportMatchingTarget(width, height)) return false; if (TryGetSelectedFixedConfigSize(out Vector2 config)) { return Mathf.RoundToInt(config.x) != width || Mathf.RoundToInt(config.y) != height; } return true; } static IEnumerator EditorSetResolutionCoroutine(int width, int height, float maxWaitSeconds) { bool done = false; bool ok = false; ScheduleEnforce(width, height, Mathf.CeilToInt(maxWaitSeconds * 60f)); EditorApplication.delayCall += () => { ok = TrySetGameViewResolution(width, height, logResult: true); done = true; }; while (!done) yield return null; float elapsed = 0f; while (elapsed < maxWaitSeconds) { if (ok && (width > height ? Handles.GetMainGameViewSize().x >= Handles.GetMainGameViewSize().y : Handles.GetMainGameViewSize().y >= Handles.GetMainGameViewSize().x)) break; elapsed += Time.unscaledDeltaTime; yield return null; } yield return null; } public static bool TrySetGameViewResolution(int width, int height, bool logResult = false) { if (!ScreenOrientationEditorSettings.IsEnabled) return false; if (IsViewportMatchingTarget(width, height)) return true; try { if (!TryGetEditorSizeContext(out Type gameViewType, out object sizesInstance, out Type sizesType, out object group, out Type groupClass, out EditorWindow gameView)) { Debug.LogWarning("[AppUI.GameViewSizeHelper] 无法获取 GameView 上下文"); return false; } int groupType = GetActiveGameViewSizeGroupType(gameViewType); string label = CustomSizeLabelPrefix + width + "x" + height; int index = FindFixedResolutionIndexByLabel(groupClass, group, label); if (index < 0) index = FindFixedResolutionIndex(groupClass, group, width, height); if (index < 0) index = AddCustomFixedSize(sizesType, sizesInstance, groupClass, group, width, height, label); if (index < 0) { Debug.LogWarning($"[AppUI.GameViewSizeHelper] 无法创建 Fixed {width}x{height}"); return false; } int count = (int)groupClass.GetMethod("GetTotalCount", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, null); if (index < 0 || index >= count) { Debug.LogWarning($"[AppUI.GameViewSizeHelper] index={index} 越界 (count={count})"); return false; } if (!ApplyGameViewSizeIndex(gameViewType, gameView, index, groupType)) { Debug.LogWarning($"[AppUI.GameViewSizeHelper] 应用 index={index} 失败"); return false; } TrySetPlayModeTargetSize(gameView, gameViewType, width, height); gameView.Repaint(); EditorApplication.QueuePlayerLoopUpdate(); bool verified = VerifyAppliedFixedSize(gameViewType, gameView, groupClass, group, width, height, out Vector2 applied, out string appliedType); if (logResult) { Vector2 viewport = Handles.GetMainGameViewSize(); Debug.Log( $"[AppUI.GameViewSizeHelper] 设置 {label} 目标={width}x{height} group={groupType} index={index}/{count - 1}, " + $"下拉项={Mathf.RoundToInt(applied.x)}x{Mathf.RoundToInt(applied.y)} ({appliedType}) verified={verified}, " + $"视口={Mathf.RoundToInt(viewport.x)}x{Mathf.RoundToInt(viewport.y)}"); } return true; } catch (Exception ex) { Debug.LogWarning($"[AppUI.GameViewSizeHelper] 异常: {ex}"); return false; } } static bool TryGetSelectedFixedConfigSize(out Vector2 size) { size = default; if (!TryGetRawSelectedConfigSize(out size, out string typeName)) return false; return typeName == "FixedResolution"; } static bool TryGetRawSelectedConfigSize(out Vector2 size, out string sizeTypeName) { size = default; sizeTypeName = ""; try { if (!TryGetEditorSizeContext(out Type gameViewType, out _, out _, out object group, out Type groupClass, out EditorWindow gameView)) return false; var indexField = gameViewType.GetField( "m_SizeSelectionID", BindingFlags.Instance | BindingFlags.NonPublic); if (indexField == null) return false; int index = (int)indexField.GetValue(gameView); return TryReadGameViewSizeAtIndex(groupClass, group, index, out size, out sizeTypeName); } catch { return false; } } static bool TryGetEditorSizeContext( out Type gameViewType, out object sizesInstance, out Type sizesType, out object group, out Type groupClass, out EditorWindow gameView) { gameViewType = null; sizesInstance = null; sizesType = null; group = null; groupClass = null; gameView = null; var editorAssembly = typeof(Editor).Assembly; gameViewType = editorAssembly.GetType("UnityEditor.GameView"); sizesType = editorAssembly.GetType("UnityEditor.GameViewSizes"); if (gameViewType == null || sizesType == null) return false; sizesInstance = GetGameViewSizesInstance(editorAssembly, sizesType); if (sizesInstance == null) return false; gameView = GetActiveGameViewWindow(gameViewType); if (gameView == null) return false; int groupType = GetActiveGameViewSizeGroupType(gameViewType); group = sizesType.GetMethod("GetGroup")?.Invoke(sizesInstance, new object[] { groupType }); if (group == null) return false; groupClass = group.GetType(); return true; } static EditorWindow GetActiveGameViewWindow(Type gameViewType) { var editorAssembly = typeof(Editor).Assembly; var playModeViewType = editorAssembly.GetType("UnityEditor.PlayModeView"); if (playModeViewType != null) { var getPlayModeView = playModeViewType.GetMethod( "GetPlayModeView", BindingFlags.NonPublic | BindingFlags.Static); if (getPlayModeView != null) { var playView = getPlayModeView.Invoke(null, new object[] { gameViewType }) as EditorWindow; if (playView != null) return playView; } } var all = Resources.FindObjectsOfTypeAll(gameViewType); if (all != null && all.Length > 0) return all[0] as EditorWindow; return EditorWindow.GetWindow(gameViewType, false, null, false); } static void TrySetPlayModeTargetSize(EditorWindow gameView, Type gameViewType, int width, int height) { var prop = gameViewType.GetProperty( "targetSize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (prop != null && prop.CanWrite) prop.SetValue(gameView, new Vector2(width, height)); gameViewType.GetMethod( "UpdateZoomAreaDimensions", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(gameView, null); } static bool ApplyGameViewSizeIndex(Type gameViewType, EditorWindow gameView, int index, int groupType) { var indexField = gameViewType.GetField( "m_SizeSelectionID", BindingFlags.Instance | BindingFlags.NonPublic); if (indexField != null) indexField.SetValue(gameView, index); var sizeSelectionCallback = gameViewType.GetMethod( "SizeSelectionCallback", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (sizeSelectionCallback != null) sizeSelectionCallback.Invoke(gameView, new object[] { index, gameView }); gameViewType.GetMethod( "UpdateZoomAreaDimensions", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(gameView, null); try { var groupUpdated = gameViewType.GetMethod( "GameViewSizeGroupUpdated", BindingFlags.Instance | BindingFlags.NonPublic); if (groupUpdated != null) { var ps = groupUpdated.GetParameters(); if (ps.Length == 0) groupUpdated.Invoke(gameView, null); else if (ps.Length == 1) groupUpdated.Invoke(gameView, new object[] { groupType }); } } catch { } return indexField != null || sizeSelectionCallback != null; } static bool VerifyAppliedFixedSize( Type gameViewType, EditorWindow gameView, Type groupClass, object group, int width, int height, out Vector2 applied, out string appliedType) { applied = default; appliedType = ""; var indexField = gameViewType.GetField( "m_SizeSelectionID", BindingFlags.Instance | BindingFlags.NonPublic); if (indexField == null) return false; int index = (int)indexField.GetValue(gameView); if (!TryReadGameViewSizeAtIndex(groupClass, group, index, out applied, out appliedType)) return false; return appliedType == "FixedResolution" && Mathf.RoundToInt(applied.x) == width && Mathf.RoundToInt(applied.y) == height; } static bool TryReadGameViewSizeAtIndex( Type groupClass, object group, int index, out Vector2 size, out string sizeTypeName) { size = default; sizeTypeName = ""; int count = (int)groupClass.GetMethod("GetTotalCount", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, null); if (index < 0 || index >= count) return false; var gameViewSize = groupClass.GetMethod("GetGameViewSize", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, new object[] { index }); var sizeObjType = gameViewSize.GetType(); int w = (int)sizeObjType.GetProperty("width", BindingFlags.Instance | BindingFlags.Public) .GetValue(gameViewSize); int h = (int)sizeObjType.GetProperty("height", BindingFlags.Instance | BindingFlags.Public) .GetValue(gameViewSize); var sizeTypeEnum = sizeObjType.GetProperty("sizeType", BindingFlags.Instance | BindingFlags.Public) .GetValue(gameViewSize); sizeTypeName = sizeTypeEnum?.ToString() ?? ""; size = new Vector2(w, h); return true; } static object GetGameViewSizesInstance(Assembly editorAssembly, Type sizesType) { var scriptableSingletonOpen = editorAssembly.GetType("UnityEditor.ScriptableSingleton`1"); if (scriptableSingletonOpen == null) return null; var closed = scriptableSingletonOpen.MakeGenericType(sizesType); return closed.GetProperty("instance", BindingFlags.Public | BindingFlags.Static) ?.GetValue(null, null); } static int GetActiveGameViewSizeGroupType(Type gameViewType) { var method = gameViewType.GetMethod( "GetCurrentGameViewSizeGroupType", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); if (method != null) return (int)method.Invoke(null, null); switch (EditorUserBuildSettings.activeBuildTarget) { case BuildTarget.Android: return 3; case BuildTarget.iOS: return 2; default: return 0; } } static int FindFixedResolutionIndex(Type groupClass, object group, int width, int height) { int count = (int)groupClass.GetMethod("GetTotalCount", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, null); for (int i = 0; i < count; i++) { if (!TryReadGameViewSizeAtIndex(groupClass, group, i, out Vector2 s, out string typeName)) continue; if (typeName != "FixedResolution") continue; if (Mathf.RoundToInt(s.x) == width && Mathf.RoundToInt(s.y) == height) return i; } return -1; } static int FindFixedResolutionIndexByLabel(Type groupClass, object group, string label) { int count = (int)groupClass.GetMethod("GetTotalCount", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, null); for (int i = 0; i < count; i++) { var gameViewSize = groupClass.GetMethod("GetGameViewSize", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, new object[] { i }); var sizeObjType = gameViewSize.GetType(); var displayText = sizeObjType.GetProperty("displayText", BindingFlags.Instance | BindingFlags.Public) ?.GetValue(gameViewSize) as string; var baseText = sizeObjType.GetProperty("baseText", BindingFlags.Instance | BindingFlags.Public) ?.GetValue(gameViewSize) as string; if (label.Equals(displayText, StringComparison.Ordinal) || label.Equals(baseText, StringComparison.Ordinal)) return i; } return -1; } static int AddCustomFixedSize( Type sizesType, object sizesInstance, Type groupClass, object group, int width, int height, string label) { var editorAssembly = typeof(Editor).Assembly; var gameViewSizeEnumType = editorAssembly.GetType("UnityEditor.GameViewSizeType"); var gameViewSizeType = editorAssembly.GetType("UnityEditor.GameViewSize"); var fixedResolution = Enum.Parse(gameViewSizeEnumType, "FixedResolution"); object newSize = null; foreach (var ctor in gameViewSizeType.GetConstructors()) { var p = ctor.GetParameters(); if (p.Length == 4) newSize = ctor.Invoke(new object[] { fixedResolution, width, height, label }); else if (p.Length == 3) newSize = ctor.Invoke(new object[] { fixedResolution, width, height }); if (newSize != null) break; } if (newSize == null) return -1; int countBefore = (int)groupClass.GetMethod("GetTotalCount", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, null); groupClass.GetMethod("AddCustomSize", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, new[] { newSize }); sizesType.GetMethod("SaveToHDD", BindingFlags.Instance | BindingFlags.Public) ?.Invoke(sizesInstance, null); var indexOfMethod = groupClass.GetMethod("IndexOf", BindingFlags.Instance | BindingFlags.Public); if (indexOfMethod != null) { int idx = (int)indexOfMethod.Invoke(group, new[] { newSize }); if (idx >= 0) return idx; } int countAfter = (int)groupClass.GetMethod("GetTotalCount", BindingFlags.Instance | BindingFlags.Public) .Invoke(group, null); if (countAfter == countBefore + 1) return countAfter - 1; return FindFixedResolutionIndexByLabel(groupClass, group, label); } sealed class GameViewEnforceState { static int _targetW; static int _targetH; static int _framesLeft; static bool _hooked; static bool _restoreMode; public static void Request(int width, int height, int frames) { _restoreMode = false; _targetW = width; _targetH = height; _framesLeft = Mathf.Max(_framesLeft, frames); EnsureHooked(); } public static void RequestRestore(GameViewPrePlaySnapshot snapshot, int frames) { _restoreMode = true; _targetW = snapshot.ViewportWidth; _targetH = snapshot.ViewportHeight; _framesLeft = Mathf.Max(_framesLeft, frames); EnsureHooked(); } public static void Cancel() { _framesLeft = 0; _restoreMode = false; if (_hooked) { EditorApplication.update -= OnEditorUpdate; _hooked = false; } } static void EnsureHooked() { if (!_hooked) { EditorApplication.update += OnEditorUpdate; _hooked = true; } } static void OnEditorUpdate() { if (_framesLeft <= 0 || !ScreenOrientationEditorSettings.IsEnabled) { if (_framesLeft <= 0 && _hooked) { EditorApplication.update -= OnEditorUpdate; _hooked = false; _restoreMode = false; } return; } _framesLeft--; if (_restoreMode) { if (NeedsRestorePrePlaySnapshot()) TryRestorePrePlaySnapshot(logResult: false); } else if (NeedsConfigEnforce(_targetW, _targetH)) { TrySetGameViewResolution(_targetW, _targetH, logResult: false); } } } #endif } }