NLDClient-yudde/ProjectNLD/Assets/Code/Scripts/Framework/UI/BackgroundRelativePosition.cs

396 lines
11 KiB
C#
Raw Normal View History

2026-01-16 11:55:56 +08:00
using UnityEngine;
using UnityEngine.UI;
using System;
[RequireComponent(typeof(RectTransform))]
[ExecuteAlways] // 在编辑模式下也执行
public class BackgroundRelativePosition : MonoBehaviour
{
[Header("背景参考")]
[SerializeField] private RectTransform _background; // 参考背景
[Header("设计分辨率设置")]
[SerializeField] private Vector2 _designResolution = new Vector2(1920, 1080); // 设计分辨率
[Header("设计时位置")]
[SerializeField] private Vector2 _designPosition; // 在设计分辨率下的位置(相对于父节点中心)
[Header("相对位置模式")]
[SerializeField] private RelativeMode _relativeMode = RelativeMode.UV;
[Header("更新设置")]
[SerializeField] private bool _autoUpdate = true; // 自动更新
[SerializeField] private UpdateMode _updateMode = UpdateMode.OnStartAndResolutionChange;
private RectTransform _rectTransform;
private Vector2 _lastScreenSize;
private Vector2 _lastBackgroundSize;
public enum RelativeMode
{
UV, // 使用UV坐标0-1范围
PixelPerfect, // 像素完美缩放
AnchorBased // 基于锚点
}
public enum UpdateMode
{
OnStartAndResolutionChange,
EveryFrame,
Manual
}
#region 属性
/// <summary>
/// 背景参考
/// </summary>
public RectTransform Background
{
get => _background;
set
{
_background = value;
UpdatePosition();
}
}
/// <summary>
/// 设计时位置(在设计分辨率下相对于父节点中心的位置)
/// </summary>
public Vector2 DesignPosition
{
get => _designPosition;
set
{
_designPosition = value;
UpdatePosition();
}
}
/// <summary>
/// 设计分辨率
/// </summary>
public Vector2 DesignResolution
{
get => _designResolution;
set
{
_designResolution = value;
UpdatePosition();
}
}
/// <summary>
/// 获取当前相对位置UV坐标
/// </summary>
public Vector2 RelativeUV
{
get
{
if (_background == null || _rectTransform == null) return Vector2.zero;
// 计算当前相对于背景的UV坐标
Vector2 backgroundSize = _background.rect.size;
Vector2 relativePos = _rectTransform.anchoredPosition - _background.anchoredPosition;
return new Vector2(
(relativePos.x + backgroundSize.x * 0.5f) / backgroundSize.x,
(relativePos.y + backgroundSize.y * 0.5f) / backgroundSize.y
);
}
}
#endregion
#region Unity生命周期
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
_lastScreenSize = new Vector2(Screen.width, Screen.height);
// 如果没有设置背景,尝试从父对象获取
if (_background == null)
{
Transform parent = transform.parent;
if (parent != null)
{
_background = parent.GetComponent<RectTransform>();
}
}
}
private void Start()
{
if (_updateMode == UpdateMode.OnStartAndResolutionChange)
{
UpdatePosition();
}
}
private void Update()
{
if (!_autoUpdate) return;
switch (_updateMode)
{
case UpdateMode.EveryFrame:
UpdatePosition();
break;
case UpdateMode.OnStartAndResolutionChange:
CheckResolutionChange();
break;
}
}
private void OnRectTransformDimensionsChange()
{
if (_autoUpdate && _updateMode != UpdateMode.Manual)
{
UpdatePosition();
}
}
#if UNITY_EDITOR
private void OnValidate()
{
// 在编辑器模式下验证时更新
if (_rectTransform == null)
_rectTransform = GetComponent<RectTransform>();
if (Application.isPlaying || UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
{
return;
}
// 编辑器模式下,参数改变时更新
UnityEditor.EditorApplication.delayCall += () =>
{
if (this != null && _autoUpdate)
{
UpdatePosition();
}
};
}
#endif
#endregion
#region 核心方法
/// <summary>
/// 更新Item位置
/// </summary>
public void UpdatePosition()
{
if (_background == null || _rectTransform == null)
{
Debug.LogWarning("BackgroundRelativePosition: 背景或RectTransform未设置", this);
return;
}
// 保存原始锚点
Vector2 originalAnchorMin = _rectTransform.anchorMin;
Vector2 originalAnchorMax = _rectTransform.anchorMax;
Vector2 originalPivot = _rectTransform.pivot;
try
{
// 根据模式计算位置
Vector2 newPosition = CalculatePosition();
// 设置位置
_rectTransform.anchoredPosition = newPosition;
// 记录背景大小变化
_lastBackgroundSize = _background.rect.size;
}
finally
{
// 恢复原始锚点设置
_rectTransform.anchorMin = originalAnchorMin;
_rectTransform.anchorMax = originalAnchorMax;
_rectTransform.pivot = originalPivot;
}
}
/// <summary>
/// 计算当前位置
/// </summary>
private Vector2 CalculatePosition()
{
switch (_relativeMode)
{
case RelativeMode.UV:
return CalculateUVPosition();
case RelativeMode.PixelPerfect:
return CalculatePixelPerfectPosition();
case RelativeMode.AnchorBased:
return CalculateAnchorBasedPosition();
default:
return CalculateUVPosition();
}
}
/// <summary>
/// 使用UV模式计算位置
/// </summary>
private Vector2 CalculateUVPosition()
{
// 获取背景实际大小
Vector2 backgroundSize = _background.rect.size;
if (backgroundSize.x <= 0 || backgroundSize.y <= 0)
{
Debug.LogWarning("BackgroundRelativePosition: 背景大小为0", this);
return _rectTransform.anchoredPosition;
}
// 计算设计分辨率下item相对于设计背景的相对位置以背景中心为原点
// _designPosition是相对于父节点中心的位置
// 假设在设计分辨率下,背景也是相对于父节点中心居中的
// 那么item相对于背景的偏移 = _designPosition - 背景位置偏移
// 但如果背景居中,背景位置偏移 = (0, 0)
Vector2 relativeToDesignBackground = _designPosition;
// 计算相对位置的比例(相对于设计背景大小)
Vector2 relativeRatio = new Vector2(
relativeToDesignBackground.x / _designResolution.x,
relativeToDesignBackground.y / _designResolution.y
);
// 应用该比例到当前背景,得到相对于当前背景中心的位置
Vector2 relativePos = new Vector2(
relativeRatio.x * backgroundSize.x,
relativeRatio.y * backgroundSize.y
);
// 加上背景的实际位置,得到相对于父节点的最终位置
return _background.anchoredPosition + relativePos;
}
/// <summary>
/// 使用像素完美模式计算位置
/// </summary>
private Vector2 CalculatePixelPerfectPosition()
{
// 获取背景实际大小
Vector2 backgroundSize = _background.rect.size;
Vector2 designBackgroundSize = _designResolution;
// 计算缩放比例
float scaleX = backgroundSize.x / designBackgroundSize.x;
float scaleY = backgroundSize.y / designBackgroundSize.y;
// 应用缩放
Vector2 scaledPosition = new Vector2(
_designPosition.x * scaleX,
_designPosition.y * scaleY
);
// 加上背景的位置偏移
return _background.anchoredPosition + scaledPosition;
}
/// <summary>
/// 使用锚点模式计算位置
/// </summary>
private Vector2 CalculateAnchorBasedPosition()
{
// 将设计位置转换为锚点值
Vector2 designUV = new Vector2(
(_designPosition.x + _designResolution.x * 0.5f) / _designResolution.x,
(_designPosition.y + _designResolution.y * 0.5f) / _designResolution.y
);
// 直接设置锚点这会改变RectTransform的布局方式
_rectTransform.anchorMin = designUV;
_rectTransform.anchorMax = designUV;
_rectTransform.pivot = new Vector2(0.5f, 0.5f);
// 返回背景位置
return _background.anchoredPosition;
}
/// <summary>
/// 检查分辨率变化
/// </summary>
private void CheckResolutionChange()
{
Vector2 currentScreenSize = new Vector2(Screen.width, Screen.height);
Vector2 currentBackgroundSize = _background != null ? _background.rect.size : Vector2.zero;
if (currentScreenSize != _lastScreenSize ||
(_background != null && currentBackgroundSize != _lastBackgroundSize))
{
_lastScreenSize = currentScreenSize;
_lastBackgroundSize = currentBackgroundSize;
UpdatePosition();
}
}
/// <summary>
/// 从当前位置反推设计位置
/// </summary>
public void CalculateDesignPositionFromCurrent()
{
if (_background == null || _rectTransform == null)
return;
// 获取当前相对位置
Vector2 currentPos = _rectTransform.anchoredPosition - _background.anchoredPosition;
Vector2 backgroundSize = _background.rect.size;
// 计算当前UV
Vector2 currentUV = new Vector2(
(currentPos.x + backgroundSize.x * 0.5f) / backgroundSize.x,
(currentPos.y + backgroundSize.y * 0.5f) / backgroundSize.y
);
// 反推设计位置
_designPosition = new Vector2(
currentUV.x * _designResolution.x - _designResolution.x * 0.5f,
currentUV.y * _designResolution.y - _designResolution.y * 0.5f
);
Debug.Log($"设计位置已更新为: {_designPosition}");
}
/// <summary>
/// 强制立即更新位置
/// </summary>
public void ForceUpdate()
{
UpdatePosition();
}
#endregion
#region 编辑器辅助
#if UNITY_EDITOR
[UnityEditor.MenuItem("GameObject/UI/Background Relative Position", false, 10)]
static void CreateBackgroundRelativeItem(UnityEditor.MenuCommand menuCommand)
{
// 创建GameObject
GameObject go = new GameObject("BackgroundRelativeItem");
UnityEditor.GameObjectUtility.SetParentAndAlign(go, menuCommand.context as GameObject);
// 添加必要的组件
go.AddComponent<Image>();
go.AddComponent<BackgroundRelativePosition>();
// 注册撤销操作
UnityEditor.Undo.RegisterCreatedObjectUndo(go, "Create " + go.name);
// 选中新创建的对象
UnityEditor.Selection.activeObject = go;
}
#endif
#endregion
}