전체 코드 : 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);
}
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
슬라이더를 조작하여 음향 조절 가능
결론
- 확장성 있는 아키텍쳐 : Component 패턴과 계층 구조로 유연한 UI 시스템
- 성능 최적화 : 뷰포트 컬링과 GPU 클리핑으로 대용량 UI 처리
- 실시간 반응성 : 이벤트 기반 시스템으로 즉각적인 사용자 인터랙션
'DirectX11 > Eternal Return 모작' 카테고리의 다른 글
| [DirectX 11 Eternal Return 모작] 16. Direct2D 텍스트 렌더링 (0) | 2025.09.03 |
|---|---|
| [DirectX 11 Eternal Return 모작] 15. HUD와 상태바 (0) | 2025.09.03 |
| [DirectX 11 Eternal Return 모작] 13. 전투 , 데미지 시스템 (0) | 2025.09.03 |
| [DirectX 11 Eternal Return 모작] 12. NavMesh (0) | 2025.09.03 |
| [DirectX 11 Eternal Return 모작] 11. 스킬 시스템 (0) | 2025.09.03 |