0516

2025년 5월 16일

카테고리: 게임엔진, UI 시스템


0516.gif


판넬 시스템을 만들었다.

사실 UI 시스템에서 고려해야 할 문제 중에서,

가장 큰 주제 중 하나가

"'겹치는 창 UI' 를 어떻게 구현할 것인가"

이다.

저건 UI 라이브러리 구조를 설계한다고 다짐했었을 때,

제일 "눈에 띄는" 문제였다.

분명 직관적으로 어떻게 만들지는 감이 안 잡히지만,

저 문제를 제대로 풀기만 하면

그럴듯한 UI 시스템이 완성된다고 생각했다.


여기서 내가 항상 강조하고 싶던 것을 적어두겠다.

이건 내가 생각해놓은

"(코드 기반) UI 프로그래머가 지켜야 하는" 절대조건 2개이다.

절대조건 1. "UI는 절대로 겹치면 안된다."

절대조건 2. "조건 (1) 이 위배되는 경우는,
'레이어'의 개념이 도입되어
서로 겹치는 UI 오브젝트가
각기 다른 레이어에 속해 있을 때이다."


나는, 우선 IControl 을 상속받는

Panel 클래스를 만들었다.

그 Panel 클래스는 Rect 기준으로

상단 부분에 특정 영역이 있어서,

그 영역을 마우스로 잡고 드래그하면

Panel 자체의 포지션이 변화한다.

(또한 오른쪽 아래 작은 영역으로 Rect의 크기또한 변화시킬 수 있다)


이걸로 끝인가?

마우스로 움직일 수 있는 창, 즉 판넬 시스템은?

걍 Panel 클래스 만들면 끝나는가?

아니다.

이게 문제가 되는 이유는,

판넬을 유저가 움직인 경우,

UI 오브젝트와 겹칠 위험이 있기 때문이다.

예를 들어서 -

Button 하나와 판넬이 겹쳤다고 해 보자.

이때는 2가지 상황으로 나뉜다.

  1. 하이라키가 btn 다음에 panel이 있는 경우
        Root
        ㄴbtn
        ㄴpanel
    

이 경우는,

판넬이 버튼보다 위에(=나중에) 그려진다. 이건 좋다.

근데?? 어?

update() 메서드는 버튼이 먼저 불린다.

즉, 커서에 대한 AABB 충돌 감지는 버튼이 먼저 해버린다.

판넬이 버튼을 가리고 있는데도, 그걸 무시하고 버튼을 누를 수 있게 되버린다.

  1. 하이라키가 panel 다음에 btn이 있는 경우
    Root
    ㄴpanel
    ㄴbtn

이러면 1번 경우보다 문제가 시각적으로 보인다.

버튼이 더 위에(=나중에) 그려진다.

이러면 이건 판넬 시스템이 아니다.


따라서, 2개의 문제를 해결해야 한다.

  1. 판넬은 어떤 UI 오브젝트보다 위에 그려져야 한다.
  2. 판넬에 대한 충돌 감지가 어떤 UI 오브젝트보다 먼저 되어야 한다.

나는 이렇게 2개의 문제를 정의하고,

해결책을 매우 직관적으로 표현할 수 있었다.

바로,

  1. Panel의 Panel::draw() 는 가장 늦게 불린다.
  2. Panel의 Panel::update() 가장 빠르게 불린다.

이것이다.

사실 여기까지 왔으면 답은 간단하다.

원래

Scene 안에 여러 개의 (UI) Object 가 있는 구조였다면??

그냥 Scene과 Object 사이에 한 클래스 계층을 추가해 주면 되는 것이다.

즉, ObjectLayer 를 추가해서 Scene 안에 여러 개의 Object Layer 가 있고, Object Layer 안에 여러 개의 Object 가 있으면 된다.

또 나는 default object layer 개념으로,

기존에 있던 'Scene 안에 있는 여러 object' 는

계속 유지하도록 했다.

layers 가 채워지면

layers의 마지막 레이어는 가장 위 레이어가 되고,

가장 아래 레이어는 default object layer 이다.


그거면 된다.

그런 다음에?

vec<ObjectLayer*> layers {};

가 있다고 할 때,

Scene::update() 안에서는

// 반대로!!!! 가장 위 레이어부터 업데이트
for (auto riter = layers.rbegin(); riter != layers.rend(); riter++) {
    (*iter)->update();
}
// default layer update 해주기
m_root->update();

또 Scene::draw() 안에서는

// default layer draw 먼저 하기
m_root->draw();
// 아래 -> 위 레이어 순으로 그려가기
for (auto iter = layers.begin(); iter != layers.end(); iter++) {
    (*iter)->draw();
}

참고로 update()에서,

ObjectLayer 단에서 당연히 검사를 해야 한다.

'이 레이어 단에서 호버링/포커싱이 되었는가?'

이 경우에는,

'이후의 레이어'에 대해서는 호버링/포커싱을

'무시' 해야 한다. 포커싱 take 를 allow 하면 안된다.

또,

이것도 매우 당연히,

그 레이어에서 포커싱이 일어났다면,

그 레이어를 Scene의 레이어 리스트에서

가장 위로 올려서

창이 가장 위에 그려지게 해야 한다.


결국 도출한 결과 코드만 보면 별 게 없지만,

문제 정의는 깔끔하게 되어서 재미있었다.