using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; namespace Gpm.Ui { /// /// 이 컴포넌트가 부착된 RectTransform의 크기가 변경될 때, 지정된 'target' RectTransform들의 크기를 동일하게 맞추는 역할을 합니다. /// 주로 ContentSizeFitter와 함께 사용되어 동적으로 변하는 콘텐츠의 크기에 맞춰 배경 이미지 등의 크기를 조절할 때 유용합니다. /// /// /// UI 계층 구조 예시: /// /// - Background (RectTransform) /// - Content (RectTransform, VerticalLayoutGroup, ContentSizeFitter, ContentSizeSetter) /// - Item1 (Image) /// - Item2 (Image) /// - ... /// /// 위와 같은 구조에서 'Content' GameObject에 이 스크립트를 추가하고, /// 'target' 배열에 'Background'의 RectTransform을 할당하면, /// 'Content'의 크기가 내부 아이템(Item1, Item2)에 의해 변경될 때마다 'Background'의 크기도 함께 조절됩니다. /// [ExecuteAlways] // 에디터 모드에서도 스크립트가 실행되도록 하여, UI 변경을 실시간으로 확인할 수 있게 합니다. [RequireComponent(typeof(RectTransform))] // 이 컴포넌트는 RectTransform이 필수적으로 필요함을 명시합니다. public class ContentSizeSetter : UIBehaviour { // 성능 최적화를 위해 RectTransform 컴포넌트를 캐싱하는 변수입니다. [System.NonSerialized] private RectTransform m_Rect; /// /// 이 컴포넌트가 부착된 GameObject의 RectTransform에 대한 참조입니다. /// 처음 접근할 때 GetComponent를 통해 초기화하고, 이후에는 캐시된 값을 사용합니다. /// private RectTransform rectTransform { get { if (m_Rect == null) { m_Rect = GetComponent(); } return m_Rect; } } [Tooltip("넓이에 크기 조절 적용")] [SerializeField] public bool EnableWidth = true; [Tooltip("높이에 크기 조절 적용")] [SerializeField] public bool EnableHeight = true; /// /// 크기를 조절할 때 추가할 여백(margin)입니다. /// x, y 값을 설정하여 target의 너비와 높이에 각각 추가적인 공간을 줄 수 있습니다. /// [Tooltip("크기를 조절할 때 추가할 여백(margin)입니다.")] [SerializeField] public Vector2 margin; /// /// 이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다. /// 인스펙터 창에서 크기를 동기화할 UI 요소들을 여기에 할당합니다. /// [Tooltip("이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다.")] [SerializeField] public RectTransform[] target; // 중복 마킹을 방지하기 위한 정적 캐시 private static HashSet s_MarkedTargets = new HashSet(); private static int s_LastFrameMarked = -1; private static Dictionary s_LastMarkedTime = new Dictionary(); // 예약 플래그 private bool m_PendingUpdate = false; [SerializeField] [Tooltip("같은 target에 대해 재마킹하기 전 최소 대기 시간(초).0이면 제한 없음.")] private float minMarkInterval = 0f; /// /// 컴포넌트가 활성화될 때 호출되는 Unity 생명주기 함수입니다. /// 레이아웃을 다시 계산하도록 시스템에 요청합니다. /// protected override void OnEnable() { // 활성화 시 즉시 반영이 필요할 수 있으므로 예약 대신 즉시 마킹 ScheduleUpdate(); } /// /// 이 컴포넌트가 부착된 RectTransform의 크기나 앵커 등이 변경될 때 호출됩니다. /// 이 스크립트의 핵심 로직으로, target 배열에 있는 모든 RectTransform의 크기를 현재 RectTransform의 크기에 맞게 업데이트합니다. /// protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); // 변경이 발생하면 즉시 적용하지 않고 다음 프레임에 일괄 처리하도록 예약합니다. ScheduleUpdate(); } /// /// 예약 처리: 다음 LateUpdate에서 적용 /// private void ScheduleUpdate() { if (!IsActive()) return; m_PendingUpdate = true; } /// /// LateUpdate에서 예약된 변경을 한 번에 처리합니다. /// 동일 프레임/짧은 시간 내 중복 레이아웃 마킹을 방지합니다. /// private void LateUpdate() { if (!m_PendingUpdate) return; // 프레임이 바뀌면 정적 캐시를 리셋 if (s_LastFrameMarked != Time.frameCount) { s_LastFrameMarked = Time.frameCount; s_MarkedTargets.Clear(); } if (rectTransform == null || target == null || target.Length == 0) { m_PendingUpdate = false; return; } // 원본 크기 계산(여기서 sizeDelta는 변경하지 않음) Vector2 desired = new Vector2(rectTransform.sizeDelta.x + margin.x, rectTransform.sizeDelta.y + margin.y); for (int i = 0; i < target.Length; i++) { var t = target[i]; if (t == null) continue; // 계산된 최종 크기 float w = EnableWidth ? desired.x : t.sizeDelta.x; float h = EnableHeight ? desired.y : t.sizeDelta.y; // 필요할 경우에만 할당하여 변경 횟수 감소 if (t.sizeDelta.x != w || t.sizeDelta.y != h) { t.sizeDelta = new Vector2(w, h); } TryMarkTarget(t); } m_PendingUpdate = false; } /// /// 주어진 target을 마킹(및 시간 제한 검사)합니다. /// /// 마킹 대상 private void TryMarkTarget(RectTransform t) { if (t == null) return; // 이미 같은 프레임에 마킹했다면 중복 방지 if (s_MarkedTargets.Contains(t)) return; // 최소 재마킹 간격 검사 if (minMarkInterval > 0f) { if (s_LastMarkedTime.TryGetValue(t, out float lastTime)) { if (Time.unscaledTime - lastTime < minMarkInterval) return; } } s_MarkedTargets.Add(t); s_LastMarkedTime[t] = Time.unscaledTime; LayoutRebuilder.MarkLayoutForRebuild(t); } /// /// UI 레이아웃을 다시 계산해야 함을 시스템에 알립니다. /// /// true일 경우, 즉시 레이아웃을 다시 계산합니다. false일 경우, 다음 프레임에 다시 계산하도록 예약합니다. protected void SetDirty(bool force = false) { if (IsActive() == false) { return; } if (force == true) { // 즉시 레이아웃을 강제로 다시 계산합니다. LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform); } else { // 다음 레이아웃 계산 주기에 맞춰 다시 계산하도록 표시만 해둡니다. (일반적인 경우) LayoutRebuilder.MarkLayoutForRebuild(rectTransform); } } #if UNITY_EDITOR /// /// (Unity 에디터 전용) 스크립트가 로드되거나 인스펙터에서 값이 변경될 때 호출됩니다. /// 에디터에서 margin과 같은 값을 변경했을 때, 실시간으로 UI에 반영되도록 레이아웃 업데이트를 요청합니다. /// protected override void OnValidate() { SetDirty(false); } #endif } }