575 lines
18 KiB
C#
575 lines
18 KiB
C#
using System.Collections.Generic;
|
|
using GameFramework.Event;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityGameFramework.Runtime;
|
|
|
|
namespace GeometryTD.CustomComponent
|
|
{
|
|
/// <summary>
|
|
/// Keeps gameplay camera and UI in a fixed design aspect, then fills extra area with black masks.
|
|
/// </summary>
|
|
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<Transform> _uiRoots = new List<Transform>();
|
|
|
|
private readonly List<Canvas> _canvasBuffer = new List<Canvas>(32);
|
|
private readonly List<Canvas> _trackedCanvases = new List<Canvas>(32);
|
|
private readonly HashSet<Canvas> _trackedCanvasSet = new HashSet<Canvas>();
|
|
|
|
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<Canvas>();
|
|
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<CanvasScaler>();
|
|
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<Canvas>();
|
|
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<Canvas>();
|
|
_maskCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
_maskCanvas.overrideSorting = true;
|
|
_maskCanvas.sortingOrder = Mathf.Clamp(_maskSortingOrder, short.MinValue, short.MaxValue);
|
|
|
|
CanvasScaler scaler = maskRoot.GetComponent<CanvasScaler>();
|
|
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
|
|
scaler.scaleFactor = 1f;
|
|
|
|
GraphicRaycaster raycaster = maskRoot.GetComponent<GraphicRaycaster>();
|
|
raycaster.enabled = false;
|
|
|
|
RectTransform rootRect = maskRoot.GetComponent<RectTransform>();
|
|
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>();
|
|
image.color = Color.black;
|
|
image.raycastTarget = false;
|
|
|
|
return maskObject.GetComponent<RectTransform>();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|