219 lines
9.0 KiB
C#
219 lines
9.0 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.EventSystems;
|
|
|
|
namespace Gpm.Ui
|
|
{
|
|
/// <summary>
|
|
/// 이 컴포넌트가 부착된 UI 요소의 크기가 변경될 때, 부모 UI 요소의 레이아웃을 강제로 다시 계산하도록 요청하는 역할을 합니다.
|
|
/// 주로 중첩된 레이아웃 그룹(Layout Group)이나 ContentSizeFitter 사용 시, 자식의 크기 변경이 부모에게 제대로 전달되지 않아
|
|
/// UI가 깨져 보일 때 사용하면 유용합니다.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <b>UI 계층 구조 예시:</b>
|
|
/// <code>
|
|
/// - VerticalPanel (VerticalLayoutGroup)
|
|
/// - HorizontalPanel (HorizontalLayoutGroup, ContentSizeFitter, LayoutUpdater)
|
|
/// - Item1
|
|
/// - Item2
|
|
/// </code>
|
|
/// 위 구조에서 'HorizontalPanel'에 아이템이 추가되어 너비가 변경될 때, 'ContentSizeFitter'가 'HorizontalPanel'의 크기를 조절합니다.
|
|
/// 이때 'HorizontalPanel'에 부착된 'LayoutUpdater'는 자신의 크기 변경을 감지하고,
|
|
/// 부모인 'VerticalPanel'에게 "자식 크기가 바뀌었으니 너의 레이아웃을 다시 정렬해!"라고 알려주는 역할을 합니다.
|
|
/// 결과적으로 'VerticalPanel'은 변경된 'HorizontalPanel'의 크기에 맞춰 레이아웃을 올바르게 갱신하게 됩니다.
|
|
/// </example>
|
|
[ExecuteAlways] // 에디터 모드에서도 스크립트가 실행되어, UI 변경을 실시간으로 확인할 수 있게 합니다.
|
|
[RequireComponent(typeof(RectTransform))] // 이 컴포넌트는 RectTransform이 필수적으로 필요함을 명시합니다.
|
|
public class LayoutUpdater : UIBehaviour
|
|
{
|
|
// 성능 최적화를 위해 부모 RectTransform 컴포넌트를 캐싱하는 변수입니다.
|
|
private RectTransform m_Parent;
|
|
|
|
// 프레임 단위로 동일 부모에 대한 중복 마킹을 방지하기 위한 정적 캐시
|
|
private static HashSet<RectTransform> s_MarkedParents = new HashSet<RectTransform>();
|
|
private static int s_LastFrameMarked = -1;
|
|
|
|
// 부모별 마지막 마킹 시간을 기록하여 너무 잦은 마킹을 제한할 수 있습니다.
|
|
private static Dictionary<RectTransform, float> s_LastMarkedTime = new Dictionary<RectTransform, float>();
|
|
|
|
// 이 인스턴스가 재빌드를 예약했는지 표시하는 플래그
|
|
private bool m_PendingRebuild = false;
|
|
|
|
[SerializeField]
|
|
[Tooltip("부모 체인 상위도 함께 마킹할지 여부. true이면 마크 체인을 따라 위로 올라가며 마킹합니다.")]
|
|
private bool markParentChain = false;
|
|
|
|
[SerializeField]
|
|
[Tooltip("마킹할 부모 체인의 깊이. 0 이하이면 제한 없음(최대16으로 내부 제한).")]
|
|
private int markChainDepth = 1;
|
|
|
|
[SerializeField]
|
|
[Tooltip("같은 부모에 대해 마킹을 다시 수행하기 전에 최소 대기 시간(초). 0이면 제한 없음.")]
|
|
private float minMarkInterval = 0f;
|
|
|
|
/// <summary>
|
|
/// 이 컴포넌트가 부착된 GameObject의 부모 RectTransform에 대한 참조입니다.
|
|
/// </summary>
|
|
public RectTransform rectParent
|
|
{
|
|
get
|
|
{
|
|
if (m_Parent == null)
|
|
{
|
|
if (transform.parent != null)
|
|
{
|
|
m_Parent = transform.parent.GetComponentInParent<RectTransform>();
|
|
}
|
|
}
|
|
|
|
return m_Parent;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이 컴포넌트가 부착된 RectTransform의 크기나 앵커 등이 변경될 때 호출됩니다.
|
|
/// 부모의 레이아웃을 즉시 다시 계산하도록 강제합니다.
|
|
/// </summary>
|
|
protected override void OnRectTransformDimensionsChange()
|
|
{
|
|
base.OnRectTransformDimensionsChange();
|
|
|
|
// 즉시 강제 재빌드를 하지 않고, 동일 프레임 내 중복 호출을 방지하며
|
|
// 다음 프레임에 일괄 처리하도록 예약합니다.
|
|
ScheduleRebuild();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 부모 레이아웃을 다시 계산해야 함을 시스템에 알립니다.
|
|
/// </summary>
|
|
/// <param name="force">true일 경우, 즉시 레이아웃을 다시 계산합니다. false일 경우, 다음 프레임에 다시 계산하도록 예약합니다.</param>
|
|
protected void SetDirty(bool force = false)
|
|
{
|
|
RectTransform parent = rectParent;
|
|
if (parent == null)
|
|
{
|
|
return;
|
|
}
|
|
if (force == true)
|
|
{
|
|
// 즉시 레이아웃을 강제로 다시 계산합니다.
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(parent);
|
|
}
|
|
else
|
|
{
|
|
// 다음 레이아웃 계산 주기에 맞춰 다시 계산하도록 표시만 해둡니다.
|
|
LayoutRebuilder.MarkLayoutForRebuild(parent);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이 인스턴스에서 부모 레이아웃 재빌드를 예약합니다.
|
|
/// 동일 프레임 내 중복 예약을 방지합니다.
|
|
/// 실제 마킹은 LateUpdate에서 수행됩니다(프레임 단위 배치).
|
|
/// </summary>
|
|
private void ScheduleRebuild()
|
|
{
|
|
m_PendingRebuild = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// LateUpdate에서 예약된 재빌드를 일괄 처리합니다.
|
|
/// 동일 프레임 내 동일 부모에 대해 중복 호출을 방지하기 위해 정적 캐시를 사용합니다.
|
|
/// 추가 옵션: 부모 체인 마킹, 부모별 최소 재마킹 간격.
|
|
/// </summary>
|
|
private void LateUpdate()
|
|
{
|
|
if (!m_PendingRebuild)
|
|
return;
|
|
|
|
// 프레임이 바뀌면 정적 캐시를 리셋
|
|
if (s_LastFrameMarked != Time.frameCount)
|
|
{
|
|
s_LastFrameMarked = Time.frameCount;
|
|
s_MarkedParents.Clear();
|
|
}
|
|
|
|
RectTransform parent = rectParent;
|
|
if (parent == null)
|
|
{
|
|
m_PendingRebuild = false;
|
|
return;
|
|
}
|
|
|
|
// 내부적으로 체인 깊이에 음수나 0이 들어오면 무제한으로 처리하지만 안전을 위해 상한을 둡니다.
|
|
int maxDepth = markChainDepth <= 0 ? 16 : Mathf.Clamp(markChainDepth, 1, 16);
|
|
|
|
if (markParentChain)
|
|
{
|
|
// 부모 체인 따라 올라가며 마킹
|
|
RectTransform current = parent;
|
|
int depth = 0;
|
|
while (current != null && depth < maxDepth)
|
|
{
|
|
TryMarkParent(current);
|
|
|
|
// 다음 상위 부모 검색
|
|
Transform next = current.parent;
|
|
current = next != null ? next.GetComponent<RectTransform>() : null;
|
|
depth++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TryMarkParent(parent);
|
|
}
|
|
|
|
m_PendingRebuild = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 주어진 부모 RectTransform을 마킹(및 시간 제한 검사)합니다.
|
|
/// </summary>
|
|
/// <param name="parent">마킹 대상 부모</param>
|
|
private void TryMarkParent(RectTransform parent)
|
|
{
|
|
if (parent == null)
|
|
return;
|
|
|
|
// 이미 같은 프레임에 같은 부모를 마킹했다면 중복 방지
|
|
if (s_MarkedParents.Contains(parent))
|
|
return;
|
|
|
|
// 최소 재마킹 간격 검사
|
|
if (minMarkInterval > 0f)
|
|
{
|
|
if (s_LastMarkedTime.TryGetValue(parent, out float lastTime))
|
|
{
|
|
if (Time.unscaledTime - lastTime < minMarkInterval)
|
|
return; // 아직 대기 시간 미만
|
|
}
|
|
}
|
|
|
|
s_MarkedParents.Add(parent);
|
|
s_LastMarkedTime[parent] = Time.unscaledTime;
|
|
LayoutRebuilder.MarkLayoutForRebuild(parent);
|
|
}
|
|
|
|
protected override void OnTransformParentChanged()
|
|
{
|
|
// 부모가 없을 수 있으므로 null 확인
|
|
if (transform.parent != null)
|
|
m_Parent = transform.parent.GetComponentInParent<RectTransform>();
|
|
else
|
|
m_Parent = null;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// (Unity 에디터 전용) 스크립트가 로드되거나 인스펙터에서 값이 변경될 때 호출됩니다.
|
|
/// 에디터에서 실시간으로 레이아웃 변경을 확인할 수 있도록 업데이트를 요청합니다.
|
|
/// </summary>
|
|
protected override void OnValidate()
|
|
{
|
|
// 에디터에서는 즉시 반영이 필요할 수 있으므로 기존 동작 유지
|
|
SetDirty(false);
|
|
}
|
|
#endif
|
|
}
|
|
} |