DirectX11/Eternal Return 모작

[DirectX 11 Eternal Return 모작] 14. UI

Vfly 2025. 9. 3. 04:29

전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client

 

GitHub - HyangRim/DirectX11-Engine-Client

Contribute to HyangRim/DirectX11-Engine-Client development by creating an account on GitHub.

github.com


게임 엔진 UI 시스템

 

1. 계층적 UI 아키텍쳐

컴포넌트 기반 설계

class UIPanel : public Component
{
    vector<weak_ptr<GameObject>> m_childElements;
    map<wstring, weak_ptr<GameObject>> m_namedElements;
};

 

특징

  • Component 패턴 활용으로 확장성 있는 UI 시스템
  • 계층 구조 관리 : UIPanel을 루트로 하는 트리 구조
  • Named Element 시스템 : 문자열 키를 통한 UI 요소 접근

모든 UI ( Button, Text, Image )는 단독으로 사용하지 않고 UIPanel에 등록한 뒤 사용.

 

좌표 변환 시스템

Vec2 UIPanel::LocalToWorldPosition(const Vec2& localPos)
{
    Vec2 worldPos;
    worldPos.x = m_position.x + localPos.x - (m_size.x / 2.0f);
    worldPos.y = m_position.y + localPos.y - (m_size.y / 2.0f);
    return worldPos;
}


void UIPanel::UpdateChildPositions()
{
    // 패널 위치가 변경되면 자식 요소들의 위치도 업데이트
    // weak_ptr을 사용하여 안전하게 접근
    Vec2 panelLeftTop = Vec2(m_position.x - m_size.x / 2.f
        , m_position.y - m_size.y / 2.f);


    for (auto it = m_childElements.begin(); it != m_childElements.end();) {
        if (auto child = it->lock()) {
            // 자식 위치 업데이트 로직
            if (auto button = child->GetButton()) {
                button->UpdatePosition(panelLeftTop);
            }
            else if (auto text = child->GetText()) {
                text->UpdatePosition(panelLeftTop);
            }
            else if (auto ImageUI = child->GetImageUI())
            {
                ImageUI->UpdatePosition(panelLeftTop);
            }
            else if (auto d2dText = child->GetD2DText()) {
                d2dText->UpdatePosition(panelLeftTop);
            }
            else if (auto uiPanel = child->GetUIPanel()) {
                uiPanel->UpdatePosition(panelLeftTop);
            }

            ++it;
        }
        else {
            // 만료된 weak_ptr 제거
            it = m_childElements.erase(it);
        }
    }
}

 

특징 

  • 로컬 / 월드 좌표 변환 : 부모 - 자식 관계에서의 상대 좌표 처리
  • 자동 위치 동기화 : 부모 이동 시 자식들 자동 업데이트

UI들은 부모 UI의 좌상단을 기준 (0,0)으로 좌에서 우로 x가 증가하고, 위에서 아래로 y가 증가하는 기준으로 배치가됨

 

2. 이벤트 시스템

2.1 Delegate패턴 활용

// Button 클래스의 이벤트 델리게이트
class Button : public Component
{
public:
    Delegate<void()> OnClick;
    Delegate<void()> OnHoverEnter;
    Delegate<void()> OnHoverExit;
    
    void Update() override
    {
        if (IsMouseOverButton() && INPUT->GetButtonDown(KEY_TYPE::LBUTTON))
        {
            OnClick.Invoke();
        }
        
        bool currentHover = IsMouseOverButton();
        if (currentHover && !m_wasHovered)
        {
            OnHoverEnter.Invoke();
            m_wasHovered = true;
        }
        else if (!currentHover && m_wasHovered)
        {
            OnHoverExit.Invoke();
            m_wasHovered = false;
        }
    }
};

 

2.2 상태머신 기반 버튼 시스템

ButtonState Button::ProcessStateTransition(const MouseInputState& input) const
{
    if (input.mouseInside) {
        if (input.leftPressed || input.rightPressed) 
            return ButtonState::Pressed;
        return ButtonState::Hovered;
    }
    return ButtonState::Normal;
}

 


2.3 람다를 활용한 동적 바인딩

// 스킨 버튼의 동적 이벤트 바인딩
button->OnClick += [this, charIndex, skinIndex]() {
    UpdateFullImage(skinIndex);
    PlaySkinPreviewAnimation(charIndex, skinIndex);
    SOUND->PlaySound(L"SFX/UI_Click.wav", 2, 0.5f);
};

button->OnHoverEnter += [this, charIndex, skinIndex]() {
    ShowSkinTooltip(charIndex, skinIndex);
    SOUND->PlaySound(L"SFX/UI_Hover.wav", 2, 0.3f);
};

 

3. 스크롤뷰 시스템

복잡한 리스트를 효율적으로 표시하기 위한 스크롤뷰를 구현

class ScrollView : public Component
{
public:
    void Create(Vec2 position, Vec2 size, shared_ptr<Material> backgroundMaterial);
    void SetScrollDirection(ScrollDirection direction);
    void SetContentSize(Vec2 contentSize);
    void SetPixelClipping(bool enable);
    
    // 동적 패널 추가
    shared_ptr<UIPanel> AddPanel(Vec2 position, Vec2 size, shared_ptr<Material> material);
    
private:
    void UpdateScrollPosition();
    void ApplyClipping();
    
    Vec2 m_scrollOffset;
    Vec2 m_contentSize;
    bool m_pixelClipping;
    ScrollDirection m_scrollDirection;
};

 

3.1 캐릭터 선택창 구현

3.1.1 동적 캐릭터 리스트 생성

 

캐릭터 데이터를 기반으로 동적으로 UI를 생성

void CharacterSelectScene::CreateScrollableCharacterList()
{
    // ScrollView 생성
    m_characterList = make_shared<GameObject>();
    auto scrollView = make_shared<ScrollView>();
    m_characterList->AddComponent(scrollView);
    
    // 스크롤뷰 설정
    Vec2 scrollPos = Vec2(280, 340);
    Vec2 scrollSize = Vec2(500, 415);
    scrollView->Create(scrollPos, scrollSize, nullptr);
    scrollView->SetScrollDirection(ScrollDirection::Vertical);
    scrollView->SetContentSize(Vec2(500.0f, 1800.0f)); // 실제 컨텐츠 크기
    scrollView->SetPixelClipping(true); // 픽셀 클리핑 활성화
    
    // 캐릭터별 UI 생성
    int characterIndex = 0;
    for (int i = 0; i < 20; i++)
    {
        Vec2 cardPos = Vec2(-208 + (i % 5) * 100, -140 + (i / 5) * 122);
        Vec2 cardSize = Vec2(106, 166);
        
        shared_ptr<UIPanel> panel = scrollView->AddPanel(cardPos, cardSize, nullptr);
        
        if (i == 0) {
            CreateRandomButton(panel); // 랜덤 선택 버튼
        } else {
            CreateCharacterButton(panel, characterIndex++);
        }
    }
}

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

 

 

3.2  캐릭터 버튼 생성 및 이벤트 처리

각 캐릭터에 대한 버튼과 이벤트를 동적으로 생성

void CreateCharacterButton(shared_ptr<UIPanel> panel, int charIndex)
{
    // 버튼 머티리얼 복제 (각각 독립적인 상태 관리)
    shared_ptr<Material> normalMaterial = RESOURCES->Get<Material>(L"CharSlotNormal")->Clone();
    shared_ptr<Material> hoverMaterial = RESOURCES->Get<Material>(L"CharSlotRollOver")->Clone();
    
    // 버튼 생성
    auto button = panel->AddButton(Vec2(61, 79), Vec2(122, 158), normalMaterial, L"CharButton");
    button->SetNormalMaterial(normalMaterial);
    button->SetHoveredMaterial(hoverMaterial);
    button->SetPressedMaterial(hoverMaterial);
    
    // 이벤트 등록 - 람다를 사용한 동적 바인딩
    button->OnClick += [this, charIndex]() {
        UpdateSkinList(charIndex);
        UpdateFullImage(0); // 기본 스킨으로 설정
        OnCharacterImageButtonClicked(charIndex);
    };
    
    button->OnHoverEnter += [this]() {
        OnCharacterSelectButtonHover();
    };
    
    // 캐릭터 이미지 추가
    wstring charTag = L"CharLobby" + characterNames[charIndex];
    shared_ptr<Material> charMaterial = RESOURCES->Get<Material>(charTag)->Clone();
    auto imageUI = panel->AddImageUI(Vec2(0.f, 0.f));
    imageUI->AddImageLayer(0, Vec2(60, 78), Vec2(121, 157), charMaterial, 1);
    
    // 캐릭터 이름 텍스트
    panel->AddText(Vec2(50.f, 120.f), characterKoreanNames[charIndex], 
                   13.f, Vec4(1.f), 1.f, Vec4(0.f), 0.f, L"GameTest", TextAlignment::Left);
}

 

 

위 사진 기준으로 마우스가 니키에 올려져있을때는 뒷 배경이 파란색으로 변하지만 다른 캐릭터 슬롯들은 검은색인 채로 존재.

 

캐릭터 슬롯을 클릭 하게 되면 빨간색 화살표가 가리키는 캐릭터의 전신 이미지와 스킨을 선택할 수 있는 ScrollView가 생성됨

 

 

 

3.3 성능 최적화 시스템

3.3.1 뷰포트 컬링

bool ScrollView::IsUIElementVisible(const Vec2& elementPos, const Vec2& elementSize) const
{
    // AABB 충돌 체크로 뷰포트 영역 내 UI 요소 판별
    return !(elementMax.x < viewportMin.x || elementMin.x > viewportMax.x ||
             elementMax.y < viewportMin.y || elementMin.y > viewportMax.y);
}

 

3.3.2 픽셀 레벨 클리핑

void ScrollView::SetupClippingForElement(shared_ptr<GameObject> element)
{
    // 셰이더 레벨에서 픽셀 클리핑 처리
    shader->PushScrollViewClippingData(clippingRect, m_enablePixelClipping);
    meshRenderer->SetPass(4);  // 클리핑 패스 사용
}

 

 

4.픽셀 클리핑 최적화

4.1 셰이더 기반 클리핑

스크롤뷰의 성능 최적화를 위해 GPU에서 픽셀 클리핑을 수행

// ScrollView 클리핑 상수 버퍼
cbuffer ScrollViewClippingBuffer : register(b10)
{
    float4 ScrollViewClippingRect; // x=left, y=top, z=right, w=bottom
    bool ScrollViewEnableClipping;
    float3 ScrollViewClippingPadding;
};

// 클리핑이 적용된 픽셀 셰이더
float4 PS_Clipped(VertexOutput2 input) : SV_TARGET
{
    // 스크린 좌표로 변환
    float2 screenPos = input.screenPos.xy / input.screenPos.w;
    screenPos = screenPos * 0.5f + 0.5f;
    screenPos.y = 1.0f - screenPos.y;
    
    screenPos.x *= 1366.0f;
    screenPos.y *= 768.0f;
    
    // 클리핑 영역 체크
    if (ScrollViewEnableClipping)
    {
        if (screenPos.x < ScrollViewClippingRect.x || screenPos.x > ScrollViewClippingRect.z ||
            screenPos.y < ScrollViewClippingRect.y || screenPos.y > ScrollViewClippingRect.w)
        {
            discard; // 영역 밖 픽셀 제거
        }
    }
    
    float4 color = DiffuseMap.Sample(ImageSampler, input.uv);
    return color;
}

 

4.2 C++ 클리핑 시스템

void ScrollView::SetPixelClipping(bool enable) 
{
    m_pixelClipping = enable;
    
    if (enable) {
        // 클리핑 영역 계산
        Vec2 screenPos = GetScreenPosition();
        Vec4 clippingRect = Vec4(
            screenPos.x, 
            screenPos.y,
            screenPos.x + m_size.x,
            screenPos.y + m_size.y
        );
        
        // 셰이더에 클리핑 데이터 전송
        m_shader->PushScrollViewClippingData(clippingRect, true);
    }
}

 

5. 다층 이미지 시스템

5.1 레이어 기반 이미지 관리

struct ImageLayer {
    shared_ptr<GameObject> gameObject;
    int layer;
    Vec2 position, size;
    shared_ptr<Material> material;
    uint32 pass;
};

 

특징 

  • Z - order 관리 : 레이어 번호 기반 자동 정렬
  • 동적 레이어 조작 : 런타임에 레이어 추가 / 제거  / 순서 변경 가능
  • Material 독립성 : 레이어별 독립적인 렌더링 설정

 

6. 슬라이더 시스템

6.1 슬라이더 방향별 값 변환

float SliderUI::ScreenToValue(float screenPos)
{
    float t = (screenPos - trackStart) / (trackEnd - trackStart);
    if (m_direction == SliderDirection::VERTICAL) {
        t = 1.0f - t;  // Y축 반전 처리
    }
    return m_minValue + t * (m_maxValue - m_minValue);
}

 

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

슬라이더를 조작하여 음향 조절 가능

 

 

결론

  1. 확장성 있는 아키텍쳐 : Component 패턴과 계층 구조로 유연한 UI 시스템
  2. 성능 최적화 : 뷰포트 컬링과 GPU 클리핑으로 대용량 UI 처리
  3. 실시간 반응성 : 이벤트 기반 시스템으로 즉각적인 사용자 인터랙션