Unity

[Unity] 디자인 패턴 : Dirty Flag 패턴

sunlight-dby 2025. 6. 18. 00:37

 

 

이 글은 해당 유튜브를 보고 진행한 것에 대해 공부한 것을 정리한 글입니다.

 


 

더티 플래그 패턴 (Dirty Flag Pattern)

더티 플래그는 오브젝트의 상태가 마지막으로 처리되거나 렌더링된 이후에 변경되었는지 여부를 나타내는 Boolean입니다.

만약 오브젝트가 '더티' 상태라면 업데이트를 수행하고, 그렇지 않다면 무시하여 불필요한 연산을 줄입니다.


특징

  • 계산 비용 절감에 최적화
    • 복잡한 계산이나 구성, 비용이 높은 환경에서 매우 유용합니다.
      • 장면에서 많은 계산이나 업데이트가 발생할 때가 이에 해당합니다.
    • 객체가 자주 변하지 않는다면, 업데이트를 지연시켜 CPU 사용량을 줄일 수 있습니다.
  • 단순한 Boolean 플래그 기반 구조
    • 객체 내부에 isDirty와 같은 불리언 플래그를 두어 상태를 추적합니다.
    • 상태 변경이 감지되었을 때만 다시 계산이 수행됩니다.

장점

  • 대규모 시스템 및 절차적 시스템에서 효율적
    • 더티 플래그 패턴은 대규모 시뮬레이션 또는 절차적 콘텐츠를 생성하는 시스템에서 비용 절감에 효과적입니다.
  • 모바일 기기에서 특히 유리
    • 모바일 기기와 같이 메모리가 제한적인 플랫폼에 필수적이라고 할 수 있습니다.
  • 성능에 직접적인 영향
    • 비용이 많이 드는 연산을 필요할 때만 실행하므로 불필요한 계산이 최소화되고 메모리 효율성이 향상됩니다.
    • 불필요한 매 프레임 연산 제거로 FPS를 향상시킵니다.

단점

  • 컴포넌트 간 결합도 증가
    • 상태 변경 여부를 추적하기 위해 여러 컴포넌트가 연결될 수 있습니다.
    • 이로 인해 종속성과 리스크가 늘어날 수 있습니다.
  • 상태 지연 가능성
    • 더티 플래그가 설정되기 전가지 상태 업데이트가 지연되며, 일시적으로 오래된 상태를 표시할 수 있습니다.
  • 디버깅 복잡성 증가
    • 어떤 시점에 플래그가 켜졌는지 추적해야 하므로 디버깅이 어려울 수 있습니다.

이러한 단점 극복을 위해, 더티 플래그를 통해서 개체 상태가 변경될 가능성이 있는 지점에 전략적으로 검사를 수행합니다.


대표적인 활용 예시 : 씬 하이어라키

Scene Hierarchy

 

씬 하이어라이키의 Transform에서, 하위 transform은 상위 transform에 업데이트가 필요할 때까지 업데이트를 생략합니다.

하위 transform의 업데이트 생략으로 인해, 상태가 자주 변경되는 동적 개체에 대한 불필요한 트랜스폼 행렬 연산을 방지하여 성능을 최적화합니다.


대표적 활용 예시 : UGUI

Unity GUI


Graphic 컴포넌트의 더티 처리

  • CanvasRenderer와 연결된 그래픽 요소의 Layout 또는 Mesh 변경 시 '더티' 상태로 처리합니다.
  • 이로 인해 Unity는 해당 요소에 대해 레이아웃 및 메시 정보를 재계산합니다.
  • Layout & Graphic 변경 흐름
    • UI Element Layout 변경 -> Dirty Layout
      • Layout 관련 속성(예: RectTransform의 위치, 크기)이 바뀌면 부모 LayoutGroup에 영향을 주며, 계층 구조의 깊이별로 레이아웃이 다시 정렬됩니다.
    • UI Element Graphic 변경 Dirty Graphic
      • Graphic 요소가 사용하는 Vertex나 Material 관련 데이터가 변경되면 Dirty 상태가 됩니다. 
      • Vertex 데이터 Dirty (ex : RectTransform)  메시 다시 빌드
      • Material 데이터 Dirty (ex : Texture) 연결된 Canvas Renderer의 Material 업데이트
  • 모든 enabled 요소들의 메시를 재생성합니다.
    • 심지어 alpha == 0으로 완전히 투명하더라도 메시가 생성되어 렌더링 대상에 포함될 수 있으므로 성능 최적화 시 주의가 필요합니다.
  • Batching을 기준으로 머터리얼을 재생성합니다.
    • 머티리얼은 Batching 기준에 따라 재생성되며, 같은 머티리얼/텍스처를 공유하는 요소들은 하나의 드로우콜로 처리됩니다.

Batch building (Canvas)

  • Canvas는 내부적으로 더티 상태를 추적하며, 필요할 때만 렌더링 명령(Batch)을 재구성합니다.
    • 예: 텍스트 변경, UI 위치 이동, 그래픽 변경 등
  • Dirty 상태가 될 때까지 기존 Batch 데이터를 캐시하여 재사용함으로써 불필요한 재계산을 방지합니다.
  • 배치 계산은 CanvasRenderer 단위로 수행되며, 하위의 독립된 Canvas 컴포넌트는 별도의 배치로 처리됩니다.
  • 배치 빌딩 시 수행되는 연산
    • 요소 깊이별 정렬
    • 중첩 관계 확인 (오버레이, 마스크 등)
    • Shared Material 비교 (동일한 머터리얼이면 드로우콜 합침)

플랫폼 별 성능 차이

  • Unity는 Canvas 관련 연산을 멀티스레드로 분산 처리하며, 가능한 연산은 Job System 또는 내부 최적화 루틴에 의해 백그라운드 처리됩니다.
  • 모바일 Soc처럼 CPU 코어가 적은 환경과 데스크톱 CPU처럼 CPU 코어가 많은 환경 간 성능차이가 매우 큽니다.

더티 플래그 패턴 사용 예시

대규모 오픈 월드

  • 전체 게임 환경을 한 번에 로드하는 것은 리소스가 부족하기에, 플레이어가 현재 보고 있는 게임 세계의 일부만 로드합니다.

복잡한 계층 구조의 트랜스폼

  • 애니메이션화된 캐릭터에 자식 트랜스폼이 있는 경우, 부모 트랜스폼이 업데이트될 때까지 애니메이션을 최소화할 수 있습니다.
  • 전략 게임에서 유닛이 편대를 갖추고 있다면, 편대 그룹 전체가 더티하다고 표시되었을 때만 유닛의 움직임을 업데이트할 수 있습니다.

물리 시뮬레이션

  • 복잡한 상호 작용 또는 많은 수의 오브젝트가 관여되는 물리 시뮬레이션의 경우, 오브젝트의 상태가 변할 때(위치, 속도, 외부 힘 등)만 다시 계산하여 성능을 최적화할 수 있습니다.
  • 물리 오브젝트가 외부 힘이나 위치 변화가 없으면 계산을 생략하고, Rigidbody, Collider 드으이 상태가 변할 때만 다시 계산하여 최적화할 수 있습니다.

경로 탐색

  • 장애물 이동, 타겟 변경 등 특정 상황에서만 더티 플래그를 설정하여, 경로를 업데이트하여 비용을 절감할 수 있습니다.

절차적 콘텐츠 생성

  • 플레이어 이동이나 게임 이벤트 같은 특정 트리거를 바탕으로 절차적 터레인을 다시 생성할 시점을 시스템에 전달할 수 있습니다.

UI 시스템

  • UI의 레이아웃 또는 그래픽 변경 시 더티 플래그로 상태 갱신을 트리거합니다.
  • 창 크기 조정, 콘텐츠 갱신 등 이벤트 발생 시에만 다시 배치 또는 렌더링을 수행할 수 있습니다.

더티 플래그 패턴 샘플 프로젝트

해당 샘플 프로젝트는 더티 플래그 패턴을 활용한 동적 구역 관리 시스템 예시입니다.

주로 오픈월드 게임이나 대형 씬에서 플레이어 주변만 로드하고, 멀어지면 언로드함으로써 성능을 최적화하는 용도로 사용됩니다.

더티 플래그 패턴 샘플 씬


GameSectors 클래스

public class GameSectors: MonoBehaviour
{
    public Player player;
    public Sector[] sectors;
    private void Update()
    {
        foreach (Sector sector in sectors)
        {
            bool isPlayerClose = sector.IsPlayerClose(player.transform.position);
            
            // 구역의 상태를 변경해야 하는지 확인
            If (isPlayerClose != sector.IsLoaded)
            {
                sector.MarkDirty();
            }
            // 더티 플래그를 바탕으로 구역 업데이트
            if(sector.IsDirty)
            {
                if (isPlayerClose)
                {
                    sector.LoadContent();
                }
                else
                {
                    sector.UnloadContent();
                }
                // 더티 플래그 재설정
                sector.Clean(); 
            }
        }
    }
}
  • Update()에서 매 프레임마다 모든 Sector를 순회하며 플레이어와의 거리를 비교합니다.
  • 로드 상태와 실제 거리 조건이 다르면 MarkDirty() 호출함으로써, 업데이트가 필요함을 표시합니다.
  • IsDirty 상태인 경우에만 실제 로드 / 언로드를 실행합니다.
  • 최종적으로 Clean()을 호출해 다시 '깨끗한 상태'로 전환합니다.

해당 코드는 변경이 필요한 경우에만 연산을 수행하도록 하여 불필요한 리소스를 낭비하지 않습니다.


Sector 클래스

 public class Sector : MonoBehaviour
{
    [Tooltip(“Minimum distance to load”)] 
    public float m_LoadRadius;
    
    // ...
    
    public bool IsLoaded { get; private set; } = false;
    public bool IsDirty { get; private set; } = false;
        
    void Awake()
    {
        // ...
        
        Clean();
        IsLoaded = false;
    }
    
    public void MarkDirty()
    {
        IsDirty = true;
        Debug.Log($”Sector {gameObject.name} is marked dirty”);
    
        // ...
    }
    
    public void LoadContent()
    {
        IsLoaded = true;
        
        // ...
        
        // 프로젝트에서 씬 콘텐츠를 로드하는 로직
        
    }
    
    public void UnloadContent()
    {
        IsLoaded = false;
        
        //...
        
        // 씬 콘텐츠를 언로드하는 로직 
       
    }
    
    public bool IsPlayerClose(Vector3 playerPosition)
    {
       return Vector3.Distance(playerPosition, transform.position + m_CenterOffset) <= m_LoadRadius;
    }
    
    public void Clean()
    {
        IsDirty = false;
    }
       
    // ...
    
}

 

해당 코드에서 씬 콘텐츠를 로드하거나 언로드하는 로직을 구현해 놓고, IsPlayerClose 메서드를 통해 플레이어와 섹터의 거리를 계산해 로드 조건 충족 여부를 반환합니다.