using System.Collections.Generic; using GameFramework.Event; using UnityEngine; using UnityEngine.UI; using UnityGameFramework.Runtime; namespace GeometryTD.CustomComponent { /// /// Keeps gameplay camera and UI in a fixed design aspect, then fills extra area with black masks. /// public class ResolutionAdapterComponent : GameFrameworkComponent { private const float DefaultCanvasPlaneDistance = 100f; private static ResolutionAdapterComponent s_Instance; [SerializeField] private Vector2 _referenceResolution = new Vector2(2560f, 1600f); [SerializeField] private bool _adaptUiCanvasToViewport = true; [SerializeField] private bool _enableBlackMask = true; [SerializeField] private int _maskSortingOrder = short.MaxValue; [SerializeField] private bool _autoCollectSceneCanvases = true; [SerializeField] private float _canvasProbeInterval = 0.2f; [SerializeField] private List _uiRoots = new List(); private readonly List _canvasBuffer = new List(32); private readonly List _trackedCanvases = new List(32); private readonly HashSet _trackedCanvasSet = new HashSet(); private Camera _mainCamera; private Rect _targetViewport = new Rect(0f, 0f, 1f, 1f); private int _cachedScreenWidth = -1; private int _cachedScreenHeight = -1; private bool _canvasCacheDirty = true; private bool _uiEventSubscribed; private bool _sceneEventSubscribed; private bool _missingRootWarned; private float _nextCanvasProbeTime; private Canvas _maskCanvas; private RectTransform _leftMask; private RectTransform _rightMask; private RectTransform _topMask; private RectTransform _bottomMask; public static bool TryGetTargetViewport(out Rect viewport) { if (s_Instance != null && s_Instance._cachedScreenWidth > 0 && s_Instance._cachedScreenHeight > 0) { viewport = s_Instance._targetViewport; return true; } viewport = new Rect(0f, 0f, 1f, 1f); return false; } private void Awake() { s_Instance = this; } private void Start() { TryEnsureUiEventSubscribed(); ApplyAdaptation(true); } private void Update() { TryEnsureUiEventSubscribed(); } private void LateUpdate() { ApplyAdaptation(false); } private void OnDestroy() { UnsubscribeUiEvents(); if (_mainCamera != null) { _mainCamera.rect = new Rect(0f, 0f, 1f, 1f); } if (s_Instance == this) { s_Instance = null; } } private void ApplyAdaptation(bool force) { if (_referenceResolution.x <= 0f || _referenceResolution.y <= 0f) { return; } Camera resolvedCamera = ResolveMainCamera(); bool cameraChanged = resolvedCamera != _mainCamera; bool screenChanged = Screen.width != _cachedScreenWidth || Screen.height != _cachedScreenHeight; bool needRecalculate = force || cameraChanged || screenChanged; if (needRecalculate) { Camera lastCamera = _mainCamera; if (lastCamera != null && lastCamera != resolvedCamera) { lastCamera.rect = new Rect(0f, 0f, 1f, 1f); } _mainCamera = resolvedCamera; _cachedScreenWidth = Screen.width; _cachedScreenHeight = Screen.height; _targetViewport = CalculateViewport(_cachedScreenWidth, _cachedScreenHeight); ApplyCameraViewport(); UpdateMaskLayout(); } if (!_adaptUiCanvasToViewport) { return; } TryMarkCanvasCacheDirtyByProbe(); ApplyCanvasAdaptation(); } private Camera ResolveMainCamera() { if (GameEntry.Scene != null && GameEntry.Scene.MainCamera != null) { return GameEntry.Scene.MainCamera; } if (Camera.main != null) { return Camera.main; } if (_mainCamera != null && _mainCamera.isActiveAndEnabled) { return _mainCamera; } return null; } private Rect CalculateViewport(int width, int height) { if (width <= 0 || height <= 0) { return new Rect(0f, 0f, 1f, 1f); } float referenceAspect = _referenceResolution.x / _referenceResolution.y; float screenAspect = (float)width / height; if (Mathf.Approximately(referenceAspect, screenAspect)) { return new Rect(0f, 0f, 1f, 1f); } if (screenAspect > referenceAspect) { float viewportWidth = referenceAspect / screenAspect; return new Rect((1f - viewportWidth) * 0.5f, 0f, viewportWidth, 1f); } float viewportHeight = screenAspect / referenceAspect; return new Rect(0f, (1f - viewportHeight) * 0.5f, 1f, viewportHeight); } private void ApplyCameraViewport() { if (_mainCamera == null) { return; } _mainCamera.rect = _targetViewport; } private void ApplyCanvasAdaptation() { if (_mainCamera == null) { return; } if (_canvasCacheDirty) { RebuildCanvasCache(); } for (int i = _trackedCanvases.Count - 1; i >= 0; i--) { Canvas canvas = _trackedCanvases[i]; if (canvas == null) { _trackedCanvases.RemoveAt(i); _canvasCacheDirty = true; continue; } ApplyCanvasSettings(canvas); } } private void RebuildCanvasCache() { _canvasCacheDirty = false; _trackedCanvases.Clear(); _trackedCanvasSet.Clear(); bool hasValidRoot = false; if (_uiRoots != null) { foreach (var root in _uiRoots) { if (root == null) { continue; } hasValidRoot = true; CollectCanvases(root); } } if (_autoCollectSceneCanvases || !hasValidRoot) { Canvas[] canvases = Resources.FindObjectsOfTypeAll(); foreach (Canvas canvas in canvases) { if (canvas == null || canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace) { continue; } GameObject gameObject = canvas.gameObject; if (!gameObject.scene.IsValid() || !gameObject.scene.isLoaded) { continue; } if (_trackedCanvasSet.Add(canvas)) { _trackedCanvases.Add(canvas); } } } if (!hasValidRoot && !_autoCollectSceneCanvases && !_missingRootWarned) { _missingRootWarned = true; Log.Warning( "ResolutionAdapterComponent missing injected roots. Assign UI/HPBar root transforms in scene or enable auto collection."); } } private void CollectCanvases(Transform root) { if (root == null) { return; } root.GetComponentsInChildren(true, _canvasBuffer); foreach (Canvas canvas in _canvasBuffer) { if (canvas == null || canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace) { continue; } if (_trackedCanvasSet.Add(canvas)) { _trackedCanvases.Add(canvas); } } _canvasBuffer.Clear(); } private void ApplyCanvasSettings(Canvas canvas) { if (canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace) { return; } canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = _mainCamera; canvas.planeDistance = ResolveCanvasPlaneDistance(_mainCamera); CanvasScaler scaler = canvas.GetComponent(); if (scaler != null) { scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = _referenceResolution; scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = 0f; } } private static float ResolveCanvasPlaneDistance(Camera camera) { if (camera == null) { return DefaultCanvasPlaneDistance; } float minDistance = camera.nearClipPlane + 0.05f; float maxDistance = camera.farClipPlane - 0.05f; if (maxDistance <= minDistance) { return minDistance; } return Mathf.Clamp(DefaultCanvasPlaneDistance, minDistance, maxDistance); } private void TryEnsureUiEventSubscribed() { if (GameEntry.Event == null) { return; } if (!_uiEventSubscribed) { GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged); GameEntry.Event.Subscribe(OpenUIFormUpdateEventArgs.EventId, OnUIFormChanged); GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged); _uiEventSubscribed = true; _canvasCacheDirty = true; } if (!_sceneEventSubscribed) { GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId, OnSceneChanged); _sceneEventSubscribed = true; _canvasCacheDirty = true; } } private void UnsubscribeUiEvents() { if (GameEntry.Event == null) { return; } if (_uiEventSubscribed) { GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged); GameEntry.Event.Unsubscribe(OpenUIFormUpdateEventArgs.EventId, OnUIFormChanged); GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged); _uiEventSubscribed = false; } if (_sceneEventSubscribed) { GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnSceneChanged); _sceneEventSubscribed = false; } } private void OnUIFormChanged(object sender, GameEventArgs e) { _canvasCacheDirty = true; } private void OnSceneChanged(object sender, GameEventArgs e) { _canvasCacheDirty = true; _missingRootWarned = false; } private void TryMarkCanvasCacheDirtyByProbe() { if (_canvasCacheDirty) { return; } if (_canvasProbeInterval > 0f && Time.unscaledTime < _nextCanvasProbeTime) { return; } _nextCanvasProbeTime = Time.unscaledTime + Mathf.Max(0.01f, _canvasProbeInterval); Canvas[] canvases = Resources.FindObjectsOfTypeAll(); foreach (Canvas canvas in canvases) { if (canvas == null || canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace) { continue; } GameObject gameObject = canvas.gameObject; if (!gameObject.scene.IsValid() || !gameObject.scene.isLoaded) { continue; } if (!_trackedCanvasSet.Contains(canvas)) { _canvasCacheDirty = true; return; } } } private void CalculateViewportPadding(out float horizontalPadding, out float verticalPadding) { horizontalPadding = Mathf.Max(0f, (1f - _targetViewport.width) * 0.5f * _cachedScreenWidth); verticalPadding = Mathf.Max(0f, (1f - _targetViewport.height) * 0.5f * _cachedScreenHeight); } private void UpdateMaskLayout() { if (!_enableBlackMask) { SetMaskVisible(false, false); return; } EnsureMaskObjects(); if (_maskCanvas == null) { return; } int maskSortingOrder = Mathf.Clamp(_maskSortingOrder, short.MinValue, short.MaxValue); if (_maskCanvas.sortingOrder != maskSortingOrder) { _maskCanvas.sortingOrder = maskSortingOrder; } CalculateViewportPadding(out float horizontalPadding, out float verticalPadding); bool showVerticalMask = horizontalPadding > 0.5f; bool showHorizontalMask = verticalPadding > 0.5f; SetMaskVisible(showVerticalMask, showHorizontalMask); if (showVerticalMask) { SetVerticalMaskRect(_leftMask, true, horizontalPadding); SetVerticalMaskRect(_rightMask, false, horizontalPadding); } if (showHorizontalMask) { SetHorizontalMaskRect(_topMask, true, verticalPadding); SetHorizontalMaskRect(_bottomMask, false, verticalPadding); } } private void EnsureMaskObjects() { if (_maskCanvas != null) { return; } GameObject maskRoot = new GameObject( "ResolutionMask", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); maskRoot.transform.SetParent(transform, false); _maskCanvas = maskRoot.GetComponent(); _maskCanvas.renderMode = RenderMode.ScreenSpaceOverlay; _maskCanvas.overrideSorting = true; _maskCanvas.sortingOrder = Mathf.Clamp(_maskSortingOrder, short.MinValue, short.MaxValue); CanvasScaler scaler = maskRoot.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize; scaler.scaleFactor = 1f; GraphicRaycaster raycaster = maskRoot.GetComponent(); raycaster.enabled = false; RectTransform rootRect = maskRoot.GetComponent(); rootRect.anchorMin = Vector2.zero; rootRect.anchorMax = Vector2.one; rootRect.anchoredPosition = Vector2.zero; rootRect.sizeDelta = Vector2.zero; _leftMask = CreateMaskRect("LeftMask", rootRect); _rightMask = CreateMaskRect("RightMask", rootRect); _topMask = CreateMaskRect("TopMask", rootRect); _bottomMask = CreateMaskRect("BottomMask", rootRect); } private static RectTransform CreateMaskRect(string maskName, Transform parent) { GameObject maskObject = new GameObject(maskName, typeof(RectTransform), typeof(Image)); maskObject.transform.SetParent(parent, false); Image image = maskObject.GetComponent(); image.color = Color.black; image.raycastTarget = false; return maskObject.GetComponent(); } private void SetMaskVisible(bool showVerticalMask, bool showHorizontalMask) { if (_maskCanvas == null) { return; } bool visible = showVerticalMask || showHorizontalMask; _maskCanvas.enabled = visible; if (_leftMask != null) { _leftMask.gameObject.SetActive(showVerticalMask); } if (_rightMask != null) { _rightMask.gameObject.SetActive(showVerticalMask); } if (_topMask != null) { _topMask.gameObject.SetActive(showHorizontalMask); } if (_bottomMask != null) { _bottomMask.gameObject.SetActive(showHorizontalMask); } } private static void SetVerticalMaskRect(RectTransform maskRect, bool isLeft, float width) { if (maskRect == null) { return; } maskRect.anchorMin = new Vector2(isLeft ? 0f : 1f, 0f); maskRect.anchorMax = new Vector2(isLeft ? 0f : 1f, 1f); maskRect.pivot = new Vector2(isLeft ? 0f : 1f, 0.5f); maskRect.anchoredPosition = Vector2.zero; maskRect.sizeDelta = new Vector2(width, 0f); } private static void SetHorizontalMaskRect(RectTransform maskRect, bool isTop, float height) { if (maskRect == null) { return; } maskRect.anchorMin = new Vector2(0f, isTop ? 1f : 0f); maskRect.anchorMax = new Vector2(1f, isTop ? 1f : 0f); maskRect.pivot = new Vector2(0.5f, isTop ? 1f : 0f); maskRect.anchoredPosition = Vector2.zero; maskRect.sizeDelta = new Vector2(0f, height); } } }