본문 바로가기

레퍼런스/고도엔진

고도엔진 튜토리얼 #7 GUI 튜토리얼(GUI tutorial)

도입


프로그래머들이 열렬히 싫어하는 것이 있다면, 그것은 사용자 인터페이스(GUIs)를 프로그래밍 하는 것이다. 지루하고, 또 지루하며 도전적이지도 않기 때문입니다. 다음과 같은 몇가지 측면이 문제를 더 심각하게 만듭니다 :


  • UI 요소의 픽셀 정렬은 어렵습니다(디자이너가 의도했던 것처럼요).
  • UI는 설계 및 적합성 문제로 테스트 기간 동안 계속 변경됩니다.
  • 다양한 디스플레이 해상도에 적합한 화면 크기 조정 처리를 해야합니다.
  • 정적으로 보이지 않으려고, 화면 구성 요소를 애니메이션화 해야 합니다.


GUI 프로그래밍은 프로그래머가 과로하게 되는 주된 원인입니다. 고도로 개발하는 중에는(그리고 이전 엔진에서도 반복적으로), 즉시 모드, 컨테이너, 앵커, 스크립팅 등과 같은 UI 개발을 위한 몇가지 기술과 철학이 시행되었습니다. 이는 항상 프로그래머의 UI 개발 동안의 스트레스를 줄이는 것이 주요 목표였습니다.


결국, 고도엔진에서 결과로 나타나는 UI 하위 시스템은 이 문제에 대한 효과적인 해결책이고, 몇가지 다른 접근법들을 섞으므로서 작동합니다. 학습 곡선이 다른 툴킷에 비해 약간 더 가파르긴 하지만 개발자들은 디나이너 및 애니메이터와 동일한 세트의 도구를 공유함으로써 매우 짧은 시간에 복잡한 사용자 인터페이스를 결합할 수 있습니다.



제어


UI 요소의 기본 노드는 제어입니다(다른 툴킷에서는 위젯 혹은 박스라고도 함). 사용자 인터페이스 기능을 제공하는 모든 노드가 이를 기반으로 합니다.


다른 제어 장치의 자식으로 씬 트리에 제어 장치를 배치할 때, 그 좌표(위치, 크기)는 항상 상위 항목에 상대적입니다. 이렇게 하면 복잡한 사용자 인터페이스를 빠르게 시각적으로 편집할 수 있는 기반이 설정됩니다.



입력과 그리기


Control은 Contorl.input_event() 콜백을 통해 입력 이벤트를 수신합니다. 포커스가 있는 제어 장치 하나만, 키보드/조이패드 이벤트를 수신할 수 있습니다.(Contorl.set_focus_mode()Control.grab_focus() 참조).


마우스 동작 이벤트는 마우스 포인터 바로 아래에 있는 컨트롤에 의해 수신됩니다. 제어 장치가 마우스 버튼 이벤트를 수신했을 때, 포인터가 제어 범위를 벗어나더라도 후속 동작 이벤트가 버튼이 놓아질 때까지 수신합니다.


CanvasItem(제어 장치)에서 상속하는 모든 클래스와 마찬가지로, 제어 장치가 다시 그려질 필요가 있는 모든 때(프로그래머는 CanvasItem를 다시 크리는 큐에 CanvasItem.update()를 호출해서 사용합니다.)와 CanvasItem._draw() 콜백이 시작될 때 수신됩니다. 만약 컨트롤이 보이지 않는다면(아직 다른 CanvasItem 속성 전에), 컨트롤은 어떤 입력도 받지 않습니다.


그러나 대개, 프로그래머는 UIs를 만들 때 그리기와 입력 이벤트를 직접적으로 건드릴 필요가 없습니다(커스텀 컨트롤을 만들 때 더 유용합니다). 대신 컨트롤은 액션이 발생할 때 문맥적 정보를 다른 종류의 신호에 방출합니다. 예를 들면 버튼은 "pressed" 신호가 눌렸을 때, 슬라이더는 드래그 될 때 "value_changed"를 방출합니다.



커스텀 입력과 미니 튜토리얼


더 깊이 들어가기 전에, 커스텀 컨트롤을 만드는 것은 컨트롤이 어떻게 작동하는지 그림으로 보여주는 것이 좋은 방법입니다. 컨트롤은 보기보다 복잡하지 않기 때문입니다.


추가적으로, 다양한 목적으로 수십개의 컨트롤을 가지고 오지만, 종종 새로운 기능성을 창조함으로써 특정한 기능성을 얻기가 더 쉽습니다.


시작으로, 싱글 노드 씬을 만듭니다. "Control" 타입의 노드는 2D 에디터의 화면 특정 영역을 차지합니다. 다음과 같죠 :


../../_images/singlecontrol.png

노드에 스크립트를 더합니다, 다음과 같죠 :


extends Control

var tapped=false

func _draw():

    var r = Rect2( Vector2(), get_size() )
    if (tapped):
        draw_rect(r, Color(1,0,0) )
    else:
        draw_rect(r, Color(0,0,1) )

func _input_event(ev):

    if (ev.type==InputEvent.MOUSE_BUTTON and ev.pressed):
        tapped=true
        update()


그리고 씬을 실행합니다. 사각형이 클릭/가볍게 두드려 졌을 때, 파랑에서 빨강으로 색이 변합니다. 이벤트와 그림 사이의 시너지가 대부분의 컨트롤이 내부적으로 작동하는 방식입니다.



UI 복잡도


전에 언급했듯이, 고도는 유저 인터페이스에서 사용하기 위한 수십개의 컨트롤을 포함합니다. 그런 컨트롤들은 두 카테고리로 나뉩니다. 첫번째는 대부분의 게임 유저 인터페이스를 만드는데 잘 먹히는 작은 컨트롤들의 집합입니다. 두번째로는(대부분의 컨트롤은 이 타입입니다.) 복잡한 사용자 인터페이스 및 스타일을 통한 균일한 스키닝(skinning)을 위해 고안되었습니다. 어떤 경우에 어떤 것을 사용해야 하는지 이해하는 데 도움이 되도록 다음과 같이 설명을 제공합니다.



간단한 UI 컨트롤


이 컨트롤의 집합은 복잡한 상호작용이나 정보를 제공하는 방법이 필요하지 않는 경우 대부분의 게임에 적합합니다. 일반적인 텍스쳐로 쉽게 스킨드(skinned) 될 수 있습니다.


  • Label : 텍스트를 보여주기 위한 노드
  • TextureFrame : 크기를 조정하거나 고정할 수 있는 단일 텍스쳐가 표시됩니다.
  • TextureButton :  누르거나, 가리키거나, 비활성화할 수 있는 상태와 같은 간단한 텍스쳐 버튼을 표시합니다.
  • TextureProgress : 단일 진행 바를 표시합니다.

추가적으로, 이 경우에 앵커를 이용해서 컨트롤을 다시 위치시키는 것을 효율적으로 할 수 있습니다(더 많은 정보를 위해 Size and anchors 튜토리얼을 보세요).


어떤 경우에도, 간단한 게임이 복잡한 UI 행동을 필요로 하는 경우가 있습니다. 예를 들어 ScrollContainer와 VBoxContainer가 필요한 요소의 스크롤 목록입니다(상위 점수 테이블 같은 것 말입니다). 이런 종류의 발전된 컨트롤은 일반적인 제어 장치와 혼합될 수 있습니다(이들도 모두 컨트롤입니다).



복잡한 UI 컨트롤


나머지 컨트롤(수십개의 컨트롤이 있습니다)은 일반적으로 다음과 같은 시나리오를 위한 것입니다 :


  • PC RPGs, MMOs, 전략, 심즈 등 복잡한 UIs가 필요한 게임
  • 콘텐츠 창조 속도를 올리기 위해 커스텀 개발 도구를 만들기
  • 엔진 기능을 확장하기 위해 고도 에디터 플러그인을 만들기

위치를 새로 지정하기 위한 컨트롤을 위한 이런 종류의 인터페이스들은 컨테이너로 좀 더 흔하게 행할 수 있다(Size and anchors 튜토리얼에서 더 많은 정보를 얻으세요)