UDN
Search public documentation:

MobileMenuTechnicalGuideKR
English Translation
日本語訳
中国翻译

Licensees can log in.

Red links require licensee log in.


Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 홈 > 모바일 홈 > 모바일 메뉴 테크니컬 가이드
UE3 홈 > 유저 인터페이스와 HUD > 모바일 메뉴 테크니컬 가이드

모바일 메뉴 테크니컬 가이드


개요


모바일 메뉴 시스템은 터치 인풋에 반응하는 유저 인터페이스를 모바일 디바이스용으로 생성하는 것입니다. 이 시스템은 개별 메뉴나 인터페이스를 나타내기 위한 씬 개념을 사용하며, 각 씬에는 버튼이나 이미지 또는 텍스트 라벨같은 콘트롤을 몇 이든 어떤 조합으로든 포함될 수 있습니다.

mobilemenuscene.jpg

모바일 메뉴 시스템이 보통 메인 및 일시정지 등의 메뉴 제작에 쓰이기는 하지만, 이러한 씬은 게임플레이가 발행중이지 않을 때 표시되는 메뉴용으로만 쓰이는 것은 아닙니다. 게임플레이가 계속되는 와중에 겹쳐 표시할 수도 있으며, 발생한 입력의 위치와 종류에 따라 입력을 받을 수도 통과시킬 수도 있습니다. 이를 통해 모바일 메뉴 시스템이 표준 인풋 존 콘트롤을 증폭시켜 화면상에 커스텀 콘트롤을 만들어 내거나 HUD 생성을 위한 조직적인 방법을 제공하는 것도 가능합니다.


전에 언급한 대로 씬이란 개별 인터페이스를 이루는 관련 콘트롤 그룹을 담은 용기입니다. 씬은 게임이 처음 실행될 때 표시되는 메인 메뉴가 될 수도 있고, 게임을 일시정지할 때 표시되는 메뉴가 될 수도 있고, 화면 하단에 놓인 버튼 바가 될 수도 있습니다. 각 씬에는 경계 (Left, Top, Width, Height) 세트가 있으며, 거기서 입력을 처리하는 콘트롤 배열도 있습니다.

MobileMenuScene

MobileMenuScene 클래스는 모바일 메뉴 시스템의 모든 씬에 대한 베이스 클래스입니다. 기본적인 씬의 작동방식을 정의하며, 사용자로부터의 입력을 처리하고, 콘트롤을 그리는 등의 역할을 합니다.

MobileMenuScene 속성

Display 표시

  • SceneCaptionFont 씬 캡션 폰트 - 씬의 모든 버튼 캡션을 그리는 데 사용할 단일 폰트를 지정합니다.
  • Opacity 불투명도 - 씬과 그 콘트롤 전부의 전체적인 불투명도입니다.

General 일반

  • MenuName 메뉴 이름 - 씬을 식별하는 데 사용되는 고유명을 지정합니다.
  • MenuObjects 메뉴 오브젝트 - 씬의 콘트롤 오브젝트를 전부 담는 배열입니다.
  • InputOwner 인풋 오너 - 씬의 관리를 담당하는 MobilePlayerInput 을 가리킵니다.
  • bSceneDoesNotRequireInput 씬이 입력을 요하지 않음? - 참이면 씬은 터치 입력을 받지 않습니다. 거짓이면 입력이 씬에 전달되어 처리됩니다. 디폴트는 거짓입니다. 이 옵션을 참으로 설정하면 씬은 HUD가 됩니다.

Position 위치

  • [Left/Top] [왼쪽/위쪽] - 씬의 [왼쪽/위쪽] 가장자리의 픽셀 단위 [가로/세로] 위치입니다. 주: 디폴트 속성에서 [0.0, 1.0] 범위의 상대적 위치로 지정할 수도 있으나, 씬이 초기화되면 픽셀로 변환됩니다.
  • [Width/Height] [폭/높이] - 씬의 [폭/높이]입니다. 주: 디폴트 속성에서 [0.0, 1.0] 범위의 상대적 위치로 지정할 수도 있으나, 씬이 초기화되면 픽셀로 변환됩니다.
  • bRelative[Left/Top/Width/Height] 상대적 [왼쪽/위쪽/폭/높이]? - 참이면 디폴트 속성에 정의된 Left 값은 [0.0, 1.0] 범위의 상대값으로 간주되며, 씬이 초기화될 때 절대 픽셀 값으로 변환됩니다.
  • bApplyGlobalScale[Left/Width] [왼쪽/폭]에 글로벌 스케일 적용? - 참이면 [Left/Width] 값에 X 방향으로 글로벌 스케일 인수를 곱해줍니다. 이를 통해 화면 해상도와 픽셀 밀도가 다른 여러 디바이스에서 적절한 위치를 잡을 수 있습니다.
  • bApplyGlobalScale[Top/Height] [위쪽/높이]에 글로벌 스케일 적용? - 참이면 [Top/Height] 값에 Y 방향으로 글로벌 스케일 인수를 곱해줍니다. 이를 통해 화면 해상도와 픽셀 밀도가 다른 여러 디바이스에서 적절한 위치를 잡을 수 있습니다.

Sound 사운드

  • UI[Touch/UnTouch]Sound UI [터치/언터치] 사운드 - [터치/언터치] 이벤트가 발생할 때 재생할 사운드큐를 가리킵니다.

MobileMenuScene 함수

Display 표시

  • GetGlobalScale[X/Y] 글로벌 스케일 구하기 - 게임이 실행되고 있는 디바이스에 따라 가로 세로 글로벌 스케일 인수를 반환합니다.
  • RenderScene [Canvas] [RenderDelta] 렌더 씬 - 씬을 렌더링하기 위해 InputOwner 가 호출합니다. 씬이 그에 속한 콘트롤 전부에서 단순히 RenderObject() 를 호출하는 것입니다.
    • Canvas 캔버스 - 씬을 그리는 데 사용할 Canvas 를 가리킵니다.
    • RenderDelta 렌더 델타 - 지난 렌더 사이클 이후로 경과된 기간을 담습니다.

General 일반

  • InitMenuScene [PlayerInput] [ScreenWidth] [ScreenHeight] 메뉴 씬 초기화 - 씬과 그 콘트롤을 초기화시키기 위해 엔진이 호출합니다.
    • PlayerInput 플레이어 인풋 - 씬 관리를 담당하는 MobilePlayerInput 을 가리킵니다.
    • ScreenWidth 화면 폭 - 게임이 실행되는 디바이스의 화면 폭을 담습니다.
    • ScreenHeight 화면 높이 - 게임이 실행되는 디바이스의 화면 높이를 담습니다.
  • Opened [Mode] 열림 - 씬이 열렸을 때 호출됩니다.
    • Mode 모드 - MobilePlayerInputOpenMenuScene() 함수로 전달되는 옵션 문자열을 담습니다.
  • MadeTopMenu 탑 메뉴가 됨 - 씬이 열리거나 다른 씬이 닫히거나 해서 스택의 맨위 씬이 되면 호출됩니다.
  • Closing 닫는중 - 씬 닫기 요청이 있은 후 실제 닫기 프로세스가 발생하기 전에 호출됩니다. 씬 닫기를 허용할 때는 참을, 닫기를 덮어써 열린 상태로 남아있으려면 거짓을 반환합니다.
  • Closed 닫힘 - 씬이 닫혀 Inputowner 의 씬 스택에서 제거되었을 때 호출됩니다.
  • CleanUpScene 씬 비우기 - 네이티브. 모든 씬 참조 및 메모리를 비웁니다.
  • FindMenuObject [Tag] 메뉴 오브젝트 찾기 - MenuObjects 배열에서 주어진 Tag 에 일치하는 콘트롤을 반환합니다.
    • Tag 태그 - 검색할 콘트롤의 Tag 입니다.

Input 인풋

  • OnTouch [Sender] [TouchX] [TouchY] [bCancel] 터치시 - 씬이 소유한 콘트롤에 (언터치 이벤트 상의) 터치가 발생했을 때 엔진이 호출하는 이벤트 토막입니다. 콘트롤 터치용 커스텀 함수성을 제공하려면 서브클래스에서 이를 덮어써야 합니다.
    • Sender 보낸이 - 터치된 MobileMenuObject 를 가리킵니다.
    • Touch[X/Y] 터치[X/Y] - 터치의 픽셀 단위 [가로/세로] 위치를 담습니다.
    • bCancel 취소? - 참이면 터치는 사용자가 아닌 시스템 이벤트 같은 외부 요인에 의해 취소된 것입니다.
  • OnSceneTouch [EventType] [TouchX] [TouchY] 씬 터치시 - 디바이스에 터치 이벤트가 발생할 때 엔진이 호출하는 이벤트 토막입니다. 입력이 처리되었다면 참을, 입력을 전달하려면 거짓을 반환합니다. 씬의 콘트롤에 꼭 직접 관계되지 않은 커스텀 터치 입력 처리 기능을 제공하기 위해서는 서브클래스에서 이를 덮어써야 합니다. ALERT! 중요: 현재 이는 씬에 외부 터치 입력 처리를 허용하기 위해 씬의 경계 밖 터치가 발생했을 때만 호출되고 있으나, 설명처럼 범용 입력 처리기로 사용되게 하기 위해 재작업 예정입니다.
    • EventType 이벤트 타입 - 터치 이벤트의 EZoneTouchEvent 유형을 담습니다. 터치 이벤트 유형에 관련된 상세 정부는 Mobile Input System KR 페이지를 참고하시기 바랍니다.
    • Touch[X/Y] 터치[X/Y] - 터치의 픽셀 단위 [가로/세로] 위치를 담습니다.
  • MobileMenuCommand [Command] 모바일 메뉴 명령 - 실행 또는 콘솔 명령을 실행합니다. 현재 구현되지 않았습니다.
    • Command 명령 - 실행시킬 실행 또는 콘솔 명령입니다.

콘트롤


콘트롤은 사용자에게 정보를 표시하거나 터치 입력을 받기 위해 씬에 추가되는 개별 컴포넌트입니다. 그러나 콘트롤 자체가 어떤 입력을 실제로 처리하는 것은 아닙니다. 디폴트로 모든 입력 처리를 담당하는 것은 씬입니다. 씬 자체에는 시각적인 성분이 없으며, 콘트롤이 씬의 시각적인 부분을 이루는 것입니다.

MobileMenuObject

MobileMenuObject 클래스는 모바일 메뉴 시스템의 모든 콘트롤에 대한 베이스 클래스입니다. 콘트롤은 'touched'와 'not touched'의 두 가지 상태가 있으며, 외형 및/또는 그 각각의 상태에 대한 작동방식도 별개의 것으로 구분 가능합니다. 디폴트 상태는 'not touched'로, 사용자가 터치할 때까지 그대로 유지됩니다. 터치되면 'touched' 상태가 되었다가, 사용자가 더이상 터치하지 않으면 'not touched' 상태로 되돌아갑니다.

주: 위의 용어 "상태"는 UnrealScript 의 State 기능을 말하지 않습니다. 그보다는 단지 콘트롤이 사용자로부터의 입력에 따라 각기 다른 외형 및/또는 작동방식을 가질 수 있음을 나타내기 위해 쓰인 것입니다.

MobileMenuObject 속성

Display 표시

  • bIsHidden 숨겨짐? - 참이면 콘트롤은 렌더링되지 않습니다.
  • bIsHighlighted 반전됨? - 참이면 콘트롤은 '반전된' 상태, 즉 선택된 라디오 버튼과 같은 상태가 됩니다.
  • Opacity 불투명도 - 콘트롤의 불투명도입니다.

General 일반

  • bHasBeenInitialized 초기화되었는지? - 참이면 콘트롤은 현재 화면 크기에 맞게 초기화된 것입니다.
  • Tag 태그 - 콘트롤 식별에 쓰이는 고유명을 지정합니다.
  • OwnerScene 오너 씬 - 콘트롤이 속하는 MobileMenuScene 을 가리킵니다.

Input 입력

  • [Top/Bottom/Left/Right]Leeway [상/하/좌/우] 여유치 - 콘트롤의 최[상/하/좌/우]단 가장자리를 따라 힛박스 밖으로 얼마까지 떨어진 터치를 콘트롤의 것으로 간주할 것인지를 지정합니다.
  • bIsActive 활성인지? - 참이면 이 콘트롤은 활성인 것으로 간주하고 탭을 받습니다.
  • bIsTouched 터치되었는지? - 참이면 현재 콘트롤 위로 터치 이벤트가 발생하고 있습니다.
  • InputOwner 인풋 오너 - 콘트롤 관리를 담당하는 MobilePlayerInput 을 가리킵니다.

Position 위치

  • [Left/Top] [왼쪽/위쪽] - 콘트롤 [왼쪽/위쪽] 가장자리의 픽셀 단위 [가로/새로] 위치입니다. 주: 이는 디폴트 속성에서 [0.0, 1.0] 범위의 상대 위치로 지정할 수도 있습니다만, 콘트롤이 초기화되면 픽셀로 변환됩니다.
  • [Width/Height] [폭/높이] - 콘트롤의 [폭/높이]입니다. 주: 이는 디폴트 속성에서 [0.0, 1.0] 범위의 상대 위치로 지정할 수도 있습니다만, 콘트롤이 초기화되면 픽셀로 변환됩니다.
  • bRelative[Left/Top/Width/Height] 상대적 [왼쪽/위쪽/폭/높이]? - 참이면 디폴트 속성에서 지정된 [Left/Top/Width/Height] 값은 [0.0, 1.0] 범위의 상대값으로 간주되며, 콘트롤이 초기화될 때 절대 픽셀값으로 변환됩니다.
  • bApplyGlobalScale[Left/Width] 글로벌 스케일 [왼쪽/폭] 적용? - 참이면 [Left/Width] 값에 X 방향으로 글로벌 스케일 인수를 곱해줍니다. 이를 통해 화면 해상도와 픽셀 밀도가 다른 여러 디바이스에서 적절한 위치를 잡을 수 있습니다.
  • bApplyGlobalScale[Top/Height] 글로벌 스케일 [위쪽/높이] 적용? - 참이면 [Top/Height] 값에 Y 방향으로 글로벌 스케일 인수를 곱해줍니다. 이를 통해 화면 해상도와 픽셀 밀도가 다른 여러 디바이스에서 적절한 위치를 잡을 수 있습니다.
  • bHeightRelativeToWidth 높이가 폭에 상대적? - 참이면 콘트롤 생성시 디폴트 속성에 지정된 Height 는 실제 Width 에 대해 ([0.0, 1.0] 범위의) 상대값으로 간주됩니다. 콘트롤의 실제 Height 는 지정된 Height 에 실제 Width 를 곱하여 계산됩니다.
  • [X/Y]Offset [X/Y]오프셋 - 콘트롤의 경계에 상대적인 [가로/세로] 오프셋을 지정하며, 콘트롤을 그리는 데 사용 가능합니다. 디폴트로 이 값은 [0.0, 1.0] 범위의 퍼센트 값이라 가정됩니다.
  • b[X/Y]OffsetIsActual [X/Y]오프셋 실제? - 참이면 [X/Y]Offset 값은 픽셀 값인 것으로 가정됩니다.

MobileMenuObject 함수

  • InitMenuObject [PlayerInput] [Scene] [ScreenWidth] [ScreenHeight] 메뉴 오브젝트 초기화 - 콘트롤 초기화를 위해 엔진이 호출합니다.
    • PlayerInput 플레이어 인풋 - 콘트롤 관리를 담당하는 MobilePlayerInput 를 가리킵니다.
    • Scene 씬 - 콘트롤이 속하는 MobileMenuScene 를 가리킵니다.
    • Screen[Width/Height] 화면 [폭/높이] - 게임이 실행되는 디바이스 화면의 [폭/높이]를 담습니다.
  • RenderObject [Canvas] 렌더 오브젝트 - 콘트롤을 화면에 그리기 위해 각 프레임에 속하는 콘트롤, 씬에 의해 호출됩니다.
    • Canvas 캔버스 - 콘트롤을 그리는 데 사용되는 Canvas 를 가리킵니다.

MobileMenuButton

MobileMenuButton 클래스는 터치했을 때 일정 동작을 하는 이미지 및/또는 텍스트를 표시할 수 있는 콘트롤입니다. 디폴트로 (bIsActive=true) 입력을 받는 유일한 콘트롤입니다. 버튼에 터치가 (엄밀히 'untouch' 이벤트가) 발생하면, 그를 소유하는 씬은 통지를 받습니다. 버튼이 'untouched' 상태일 때는 (bIsTouched=false) 한 이미지를, 'touched' 상태일 때는 (bIsTouched=true) 다른 이미지를 표시합니다.

control_buttons.jpg

Properties 속성

  • Images 이미지 - 각 상태마다 하나씩, 버튼을 렌더링하는 데 사용되는 Texture2D 둘로 된 배열입니다. [0] 요소는 'not touched' 상태에 사용되며, [1] 요소는 'touched' 상태에 사용됩니다.
  • ImagesUVs 이미지 UV - 각 상태마다 하나씩, 버튼을 렌더링하는 데 사용되는 Images 텍스처 구역 지정 UVCoords 둘로 된 배열입니다. [0] 요소는 'not touched' 상태에 사용되며, [1] 요소는 'touched' 상태에 사용됩니다.
  • ImageColor 이미지 컬러 - 버튼 이미지를 변조시킬 색을 지정합니다.
  • Caption 캡션 - 옵션이며, 버튼에 표시할 텍스트 라벨을 지정합니다.
  • CaptionColor 캡션 컬러 - Caption 텍스트를 그리는 데 사용되는 색을 지정합니다.

Functions 함수

  • RenderCaption [Canvas] 렌더 캡션 - 캡션이 지정된 경우, 버튼의 Caption 텍스트를 그립니다.
    • Canvas 캔버스 - 텍스트를 그리는 데 사용할 Canvas 를 가리킵니다.

MobileMenuImage

MobileMenuImage 클래스는 이미지를 텍스처나 텍스처 일부의 형태로 화면상에 표시하는 클래스입니다. 입력을 받지 않는 콘트롤, 즉 이미지 상의 터치 이벤트는 등록되지 않고 씬의 OnTouch() 이벤트로 전송되며, 이 클래스는 본질적으로 장식 콘트롤이 됩니다.

Properties 속성

  • Image 이미지 - 이미지를 그릴 때 사용할 텍스처를 지정하는 데 쓰이는 Texture2D 입니다.
  • ImageDrawStyle 이미지 드로 스타일 - 이미지를 그리는 데 사용할 MenuImageDrawStyle 을 지정합니다.
    • IDS_Normal 이드스_보통 - 지정된 텍스처 구역을 스케일 조절하지 않고 이미지 경계에 맞도록 잘라내어 그립니다.
    • IDS_Stretched 이드스_늘임 - 지정된 텍스처 구역을 이미지 경계에 맞도록 스케일 조절하여 그립니다.
    • IDS_Tile 이드스_타일 - 좌상단 구역은 유지한 채, 이미지 경계에 맞도록 구역의 폭과 높이를 변경합니다. 지정된 구역이 이미지 경계보다 크면 이미지를 잘라내고, (구역이 풀 텍스처라 가정할 때) 구역이 이미지 경계보다 작으면 이미지를 이어붙입니다. 주: 아틀라스 텍스처를 사용할 때는 작동법을 종잡을 수 없게 되니 이 옵션은 피하는 것이 좋습니다.
  • ImageUVs 이미지 UV - 이미지를 그릴 때 사용할 Image 텍스처의 구역을 지정하는 UVCoords 입니다.
  • ImageColor 이미지 컬러 - 이미지를 그릴 때 Image 텍스처를 변조시킬 색을 지정합니다.

MobileMenuLabel

MobileMenuLabel 클래스는 화면상에 텍스트 문자열을 표시하는 클래스입니다. 커스텀/다이내믹 데이터나 텍스트를 사용자에게 표시할 때 좋습니다. 이 콘트롤은 입력을 받지 않으며, 터치 이벤트는 등록되지 않고 씬의 OnTouch() 이벤트로 전송됩니다.

Properties 속성

  • Caption 캡션 - 화면상에 표시되는 라벨의 텍스트 문자열을 지정합니다.
  • TextFont 텍스트 폰트 - 텍스트를 그리는 데 사용할 폰트를 지정합니다.
  • TextColor 텍스트 컬러 - 라벨이 터치되지 않았을 때 텍스트를 그리는 데 사용할 색을 지정합니다.
  • TouchedColor 터치된 컬러 - 라벨이 터치될 때 텍스트를 그리는 데 사용할 색을 지정합니다.
  • Text[X/Y]Scale 텍스트 [X/Y] 스케일 - 텍스트를 그리는 데 사용할 [가로/세로] 스케일 인수를 지정합니다.
  • bAutoSize - 참이면 라벨의 WidthHeight 는 매 드로 사이클마다 렌더링되는 텍스트의 크기에 맞게 조절됩니다. 거짓이면 라벨의 WidthHeight 는 변하지 않습니다.

커스텀 콘트롤

기본 내장 콘트롤은 (위의 설명대로 버튼, 이미지, 라벨 등) 몇 없는 반면, 약간의 독창성을 발휘하여 어떤 종류의 콘트롤도 실용적으로 조합해 낼 수 있습니다. 예로 각각에 해당하는 라벨이 붙어 있는 버튼이 여럿 있는데, 그 중 하나가 눌리면 해당 버튼은 '선택' 이미지로 바뀌고 나머지는 '미선택' 이미지로 바뀌는 버튼 그룹을 들어볼 수 있겠습니다. 라디오 버튼 그룹의 느낌을 낼 수 있는 것입니다.

좀 더 복잡한 콘트롤 외형을 내기 위해 씬 내 콘트롤을 조합하는 방법은 씬에 많은 커스텀 로직을 필요로 합니다. 그리 복잡한 콘트롤을 재사용하려는 경우 이와 같은 것이 이상적이지 않다는 것은 명확합니다. 콘트롤이 입력을 바로 처리하거나 받지는 않는다 쳐도, 좀 더 복잡한 유형의 재사용 가능 콘트롤을 만들려면 시스템에 부가적인 변경 작업을 해 줘야 합니다. 예로 일반 입력을 다룬 다음 그 각각의 콘트롤에서 처리할 수 있도록 전달해 주려면 MobileMenuScene 의 서브클래스를 만들어야 할 수도 있습니다. 이런 식으로 커스텀 콘트롤이 스와이프 동작에 따른 스크롤 목록처럼, 터치 입력에 직접 반응하도록 하는 복잡한 작동법도 가능해 집니다. (커스텀 리스트 콘트롤 부분 참고)

메뉴 씬 작업하기


모바일 메뉴 시스템 사용법은 매우 간단합니다. 새로운 커스텀 메뉴에 콘트롤을 더한 다음, 인풋 시스템 초기화 이전의 어느 시점에서든 열거나 닫을 수 있습니다. 이는 MobilePlayerInput 클래스가 메뉴 시스템 관리를 담당하기에 그렇습니다. 터치 입력 처리는 메뉴 자체 내에서 수행되며, 어느 시나리오든 실제적으로 허용될 만큼 유연합니다.

콘트롤 만들기

씬에 콘트롤을 더하는 것은 일반적으로 씬 클래스의 defaultproperties 블록 내에 서브-오브젝트를 만들어 수행합니다. 각 콘트롤은 서브-오브젝트로 생성된 이후 씬의 MenuObjects 배열에 추가됩니다. 콘트롤이 defaultproperties 블록 내에 생성되는 순서는 그다지 중요하지 않으나, MenuObjects 배열에 추가되는 순서는 콘트롤이 렌더링되고 입력을 받는 순서를 결정하기에 매우 중요합니다.

콘트롤을 새로 만들어 MenuObjects 배열에 추가시키기 위한 전형적인 서브오브젝트 블록 형태는 다음과 같습니다:

Begin Object Class=MobileMenuButton Name=ExploreButton
   Tag="EXPLORE"
   Left=0.35
   Top=0.35
   Width=140
   Height=24
   bRelativeLeft=true
   bRelativeTop=true
   TopLeeway=20
   Images(0)=Texture2D'CastleUI.menus.T_CastleMenu2'
   Images(1)=Texture2D'CastleUI.menus.T_CastleMenu2'
   ImagesUVs(0)=(bCustomCoords=true,U=306,V=220,UL=310,VL=48)
   ImagesUVs(1)=(bCustomCoords=true,U=306,V=271,UL=310,VL=48)
End Object
MenuObjects(0)=ExploreButton

이 예제에서 살펴보면, 버튼은 MenuObjects 배열에서 첫 항목으로 (0 요소) 명시적 추가되어 있습니다. 이런 식으로 순서를 명시적으로 지정하면 defaultproperties 블록 내에 원하는 순서대로 콘트롤을 만들 수 있습니다.

같은 버튼을 MenuObjects 배열 끝에 밀어넣는 것은 이런 식으로도 간단히 가능합니다:

Begin Object Class=MobileMenuButton Name=ExploreButton
   Tag="EXPLORE"
   Left=0.35
   Top=0.35
   Width=140
   Height=24
   bRelativeLeft=true
   bRelativeTop=true
   TopLeeway=20
   Images(0)=Texture2D'CastleUI.menus.T_CastleMenu2'
   Images(1)=Texture2D'CastleUI.menus.T_CastleMenu2'
   ImagesUVs(0)=(bCustomCoords=true,U=306,V=220,UL=310,VL=48)
   ImagesUVs(1)=(bCustomCoords=true,U=306,V=271,UL=310,VL=48)
End Object
MenuObjects.Add(ExploreButton)

이 방법을 사용할 때 중요한 점은, 콘트롤을 만들어 바라는 순서대로 MenuObjects 배열에 추가시킬 수 있다는 것입니다.

어떤 방법을 사용하든, 서브-오브젝트 블록에 대한 구문은 동일하게 유지됩니다. 각 블록은 아래와 같이 시작됩니다:

Begin Object Class=[ControlClass] Name=[ControlName]

이는 새로운 ControlClass 유형의 서브오브젝트가 ControlName 이란 이름으로 생성됨을 의미합니다. 여기에 콘트롤의 속성에 대한 값 설정이 이어지며, 보통 읽기 좋은 형태로 나타나게 됩니다. 마지막으로 모든 속성 값을 설정한 이후, 다음 블록으로 마무리합니다:

End Object

이제 이 서브 오브젝트는 MenuObjects 배열에 할당할 때와 마찬가지로, 그 이름을 통해 defaultproperties 블록에 참조시킬 수 있습니다.

실제 기능은 없지만 배경 이미지에 버튼 하나로 된 단순한 씬을 구성해 보자면 이와 같습니다:

class MobileMenuExample extends MobileMenuScene;

defaultproperties
{   
   Left=0
   Top=0
   Width=1.0
   Height=180
   bRelativeWidth=true
   
   Begin Object Class=MobileMenuImage Name=Background
      Tag="Background"
      Left=0
      Top=0
      Width=1.0
      Height=1.0
      bRelativeWidth=true
      bRelativeHeight=true
      Image=Texture2D'CastleUI.menus.T_CastleMenu2'
      ImageDrawStyle=IDS_Stretched
      ImageUVs=(bCustomCoords=true,U=0,V=30,UL=1024,VL=180)
   End Object
   MenuObjects.Add(Background)

   Begin Object Class=MobileMenuButton Name=ExploreButton
      Tag="EXPLORE"
      Left=0.35
      Top=0.35
      Width=140
      Height=24
      bRelativeLeft=true
      bRelativeTop=true
      TopLeeway=20
      Images(0)=Texture2D'CastleUI.menus.T_CastleMenu2'
      Images(1)=Texture2D'CastleUI.menus.T_CastleMenu2'
      ImagesUVs(0)=(bCustomCoords=true,U=306,V=220,UL=310,VL=48)
      ImagesUVs(1)=(bCustomCoords=true,U=306,V=271,UL=310,VL=48)
   End Object
   MenuObjects.Add(ExploreButton)
}

이 결과는 아래와 같습니다:

addcontrols.jpg

메뉴 관리하기

모바일 메뉴 시스템은 MobilePlayerInput 클래스에 의해 관리됩니다. 여기에는 씬 여닫기에 관련된 함수성은 물론 렌더링하라고 알리는 것도, 사용자로부터의 입력을 씬과 그 콘트롤에 전달하는 것도 포함됩니다.

씬 스택

언제든지 다수의 씬을 열 수 있습니다. 열린 씬 전부는 MobilePlayerInput 내 스택(배열)에 담깁니다. 마지막으로 열린 씬은 항상 스택 맨위에 위치합니다. 입력은 씬 스택의 위에서 아래로 필터링됩니다. 스택 맨위의 씬이 입력을 처리하면, 스택에서 그 아래의 씬은 그 입력에 접근하지 못합니다. 스택의 씬은 아래에서 위로 렌더링되기에, 맨위에 있는 씬이 다른 씬 위에 렌더링되게 됩니다.

메뉴 씬 열기

MobilePlayerInput 클래스는 메뉴 씬을 여는 데 사용되는 여러가지 함수를 포함하고 있습니다.

  • OpenMenuScene [SceneClass] [Mode] 메뉴 씬 열기 - 주어진 클래스의 메뉴 씬을 새로 엽니다. 열린 씬으로의 참조를 반환합니다.
    • SceneClass 씬 클래스 - 열 메뉴 씬 클래스를 지정합니다. MobileMenuScene 의 서브클래스여야 합니다.
    • Mode 모드 - 옵션. 씬의 Opened() 함수에 전달할 문자열을 지정합니다.
  • OpenMobileMenu [MenuClassName] 모바일 메뉴 열기 - 문자열 형태로 주어진 클래스의 메뉴 씬을 엽니다.
    • MenuClassName 메뉴 클래스 이름 - 열 메뉴 씬 클래스 이름을 문자열 형태로 지정합니다.
  • OpenMobileMenuMode [MenuClassName] [Mode] 모바일 메뉴 모드 열기 - 문자열 형태로 주어진 클래스의 메뉴 씬을 옵션 모드로 엽니다.
    • MenuClassName 메뉴 클래스 이름 - 열 메뉴 씬 클래스 이름을 문자열 형태로 지정합니다.
    • Mode 모드 - 옵션. 씬의 Opened() 함수에 전달할 문자열을 지정합니다.

메뉴 씬 닫기

MobilePlayerInput 클래스는 메뉴 씬을 닫는 데 사용할 수 있는 함수가 둘 포함되어 있습니다.

  • CloseMenuScene [SceneToClose] 메뉴 씬 닫기 - 지정된 메뉴 씬을 닫습니다.
    • SceneToClose 닫을 씬 - 닫을 씬을 가리킵니다.
  • CloseAllMenus 모든 메뉴 닫기 - 씬 스택에 있는 메뉴 씬을 전부 닫습니다.

메뉴 씬 렌더링하기

MobilePlayerInput 클래스는 씬 스택에 있는 각 씬에게 매 프레임 렌더링 명령을 내리기도 합니다.

  • RenderMenus [Canvas Canvas] [RenderDelta] 렌더 메뉴 - 씬 스택에 있는 메뉴 전부를 렌더링하고자 엔진이 매 프레임 호출합니다.
    • Canvas 캔버스 - 씬을 그리는 데 사용할 Canvas 를 가리킵니다.
    • RenderDelta 렌더 델타 - 지난 렌더 사이클 이후 경과한 기간을 담습니다.

터치 입력

씬의 경계 내 화면에 사용자가 터치하면, 씬은 엔진으로부터 터치 입력 통지를 받습니다. 씬은 이러한 통지를 사용하여 터치 입력을 동작으로 해석합니다. 어느 씬에서 입력 통지를 갖는지를 결정하는 주요 방법은 둘 있습니다.

콘트롤 터치

활성 콘트롤이 터치되었을 때 씬은 콘트롤에서 (OnTouch() 이벤트를 통해) 통지를 받아, 씬이 터치 결과를 알아서 적당한 식으로 처리하도록 합니다. 이 이벤트는 사용자가 콘트롤을 "언터치"할 때, 즉 버튼을 누를 때가 아닌 버튼을 뗄 때만 호출됩니다.

  • OnTouch [Sender] [TouchX] [TouchY] [bCancel] 터치시 - 씬이 소유하는 콘트롤에 (Untouch 이벤트의) 터치가 발생했을 때 엔진이 호출하는 이벤트 토막입니다. 콘트롤 터치용 커스텀 함수성을 제공하려면 서브클래스에서 이를 덮어써야 합니다.
    • Sender 보낸이 - 터치된 MobileMenuObject 를 가리킵니다.
    • Touch[X/Y] 터치[X/Y] - 터치의 픽셀 단위 [가로/세로] 위치를 담습니다.
    • bCancel 취소? - 참이면 터치는 사용자가 아닌 시스템 이벤트와 같은 외부 요인에 의해 취소된 것입니다.

기본 버튼 예제

버튼에서 입력을 받아 어떤 동작을 취하게 하는 기본 예제를 만들어 보려면, 위에서 '콘트롤 만들기' 예제를 따 오시기 바랍니다.

class MobileMenuExample extends MobileMenuScene;

defaultproperties
{   
   Left=0
   Top=0
   Width=1.0
   Height=180
   bRelativeWidth=true
   
   Begin Object Class=MobileMenuImage Name=Background
      Tag="Background"
      Left=0
      Top=0
      Width=1.0
      Height=1.0
      bRelativeWidth=true
      bRelativeHeight=true
      Image=Texture2D'CastleUI.menus.T_CastleMenu2'
      ImageDrawStyle=IDS_Stretched
      ImageUVs=(bCustomCoords=true,U=0,V=30,UL=1024,VL=180)
   End Object
   MenuObjects.Add(Background)

   Begin Object Class=MobileMenuButton Name=ExploreButton
      Tag="EXPLORE"
      Left=0.35
      Top=0.35
      Width=140
      Height=24
      bRelativeLeft=true
      bRelativeTop=true
      TopLeeway=20
      Images(0)=Texture2D'CastleUI.menus.T_CastleMenu2'
      Images(1)=Texture2D'CastleUI.menus.T_CastleMenu2'
      ImagesUVs(0)=(bCustomCoords=true,U=306,V=220,UL=310,VL=48)
      ImagesUVs(1)=(bCustomCoords=true,U=306,V=271,UL=310,VL=48)
   End Object
   MenuObjects.Add(ExploreButton)
}

버튼을 터치했을 때 뭔가 하게 만들려면, OnTouch() 이벤트에 버튼 눌림 처리 함수성을 추가시켜 덮어써 줘야 합니다. 먼저 (MobileMenuScene 클래스에서 복사할 수 있는) 함수 시그너처를 추가합니다.:

function OnTouch(MobileMenuObject Sender,float TouchX, float TouchY, bool bCancel)
{
}

다음으로 약간의 모난 경우를 처리해 줘야 합니다. Sender 가 비어 있거나 bCancel 파라미터가 설정된 경우, 함수는 아무 것도 하지 않고 반환해야 합니다.

if(Sender == none)
{
   return;
}

if(bCancel)
{
   return;
}

마지막으로, 버튼이 눌렸을 경우도 처리해 줘야 합니다. 이 예제에서는 단순히 메뉴 자체가 닫히도록 하는 버튼입니다.

if(Sender.Tag ~= "EXPLORE")
{
   InputOwner.CloseMenuScene(self);
}

먼저 SenderTag 를 검사하여 (~= 연산자로 대소문자 무관 검사) defaultproperties 안 버튼에 지정된 Tag 와 같은지 확인합니다. 검사를 통과하고 버튼이 콘트롤에 터치된 경우, CloseMenuScene() 함수가 Inputowner 상에서 호출되는데, 이는 로컬 플레이어의 경우 MobilePlayerInput 으로 여기에 메뉴 자체 로의 참조 self 를 전달합니다.

씬 클래스에 완전 통합된 OnTouch() 함수는:

class MobileMenuExample extends MobileMenuScene;

function OnTouch(MobileMenuObject Sender,float TouchX, float TouchY, bool bCancel)
{
   if(Sender == none)
   {
      return;
   }
   
   if(bCancel)
   {
      return;
   }
   
   if(Sender.Tag ~= "EXPLORE")
   {
      InputOwner.CloseMenuScene(self);
   }
}

defaultproperties
{   
   Left=0
   Top=0
   Width=1.0
   Height=180
   bRelativeWidth=true
   
   Begin Object Class=MobileMenuImage Name=Background
      Tag="Background"
      Left=0
      Top=0
      Width=1.0
      Height=1.0
      bRelativeWidth=true
      bRelativeHeight=true
      Image=Texture2D'CastleUI.menus.T_CastleMenu2'
      ImageDrawStyle=IDS_Stretched
      ImageUVs=(bCustomCoords=true,U=0,V=30,UL=1024,VL=180)
   End Object
   MenuObjects.Add(Background)

   Begin Object Class=MobileMenuButton Name=ExploreButton
      Tag="EXPLORE"
      Left=0.35
      Top=0.35
      Width=140
      Height=24
      bRelativeLeft=true
      bRelativeTop=true
      TopLeeway=20
      Images(0)=Texture2D'CastleUI.menus.T_CastleMenu2'
      Images(1)=Texture2D'CastleUI.menus.T_CastleMenu2'
      ImagesUVs(0)=(bCustomCoords=true,U=306,V=220,UL=310,VL=48)
      ImagesUVs(1)=(bCustomCoords=true,U=306,V=271,UL=310,VL=48)
   End Object
   MenuObjects.Add(ExploreButton)
}

이 메뉴 작동방식은 이와 같습니다:

메뉴 열림

basicinput_0.jpg

버튼 터치됨

basicinput_1.jpg

메뉴 닫힘

basicinput_2.jpg

커스텀 터치 인풋

OnSceneTouch 씬 터치시

씬은 OnSceneTouch() 이벤트 편을 통해 모든 터치 인풋 통지를 받습니다. 이를 통해 씬이 특정 콘트롤에서의 터치 이벤트를 처리할 수 있을 뿐만 아니라, 스와이프나 기타 제스처같은 커스텀 유형 인풋을 수행하기 위한 범용 입력도 처리할 수 있습니다.

  • OnSceneTouch [EventType] [TouchX] [TouchY] 씬 터치시 - 씬의 경계 내에 터치 이벤트가 발생했을 때 엔진이 호출하는 이벤트 토막입니다. 씬의 콘트롤에 꼭 직접 관계되지 않는 커스텀 터치 입력 처리 기능을 제공하려면 서브클래스에서 이를 덮어써야 합니다.
    • EventType 이벤트 유형 - 터치 이벤트의 EZoneTouchEvent 유형을 담습니다. 터치 이벤트 유형에 관련된 상세 정보는 Mobile Input System KR 페이지를 참고해 주시기 바랍니다.
    • Touch[X/Y] 터치[X/Y] - 터치의 픽셀 단위 [가로/세로] 위치를 담습니다.

커스텀 인풋 메뉴 예제


약간의 창의성만 있으면 기본 버튼, 이미지, 라벨 이상의 메뉴와 콘트롤을 만들 수 있습니다. 여기서는 씬에 모든 입력 처리를 넘기지 않고 자체적으로 입력을 처리하는 커스텀 콘트롤은 물론 스크롤 목록, 콤보박스, 새 버튼, 라벨 등도 만들어 보도록 하겠습니다. 이 콘트롤들은 모두, 씬에서의 입력을 쉽게 전달받을 수 있도록 커스텀 인터페이스를 구현하는 새로운 베이스 콘트롤 클래스로부터 확장하게 되며, 그 자체가 새로운 커스텀 씬 클래스가 됩니다.

예제 메뉴를 실제로 사용하는 모습과, 이 예제로 어떤 것이 가능하겠는지 등을 알아보려면 이 비디오를 보시기 바랍니다:

(보려면 좌클릭, 내려받으려면 우클릭 > 다른 이름으로 저장)

examplemenu_1.jpg

기본 터치가능 콘트롤

커스텀 터치가능 콘트롤은 모든 입력 이벤트를 그 콘트롤에 전달하기 위해 씬에 의지합니다. 차례로 각 콘트롤은 입력을 자체 처리하거나, 자식 콘트롤로 전달하거나, 아니면 이 둘을 조합하거나 합니다.

새 콘트롤 구현으로 들어가 보기 전에 이런 생각이 들 수 있습니다. "왜 콘트롤이 입력을 처리하게 해야하지?" 좋은 질문입니다. 그리고 실제로 메뉴 씬과 콘트롤에서 필요한 함수성 종류까지 내려오기도 합니다. 이 경우 본질적으로는 다른 서브-콘트롤의 용기가 되는 복잡한 콘트롤을 만들게 됩니다. 그와 같은 경우에서 콘트롤이 터치되었는가를 알아보고 그 입력에 따라 반응하도록 하는 것이 좋습니다. 버튼이 클릭되었을 때 부모에 알이고, 부모는 그에 맞춰 특정 동작을 수행하도록 하는 능력이 중요한 요인입니다. 이것이 불가능하다면 콘트롤 간의 복잡한 상호작용을 수행하기 위한 로직 전부가 씬 자체에 위치해 있어야 할 터인데, 그렇게 되면 목록같은 복잡한 콘트롤 만들기의 용이함이 극도로 제한될 것입니다. 즉 목록을 사용하려 할 때마다 본질적으로 전체 목록 로직을 다시 구현해야 하는 것입니다.

ITouchable 인터페이스

새로운 터치가능 콘트롤을 만들기 위한 첫 단계는, 입력을 주려는 커스텀 콘트롤이 구현할 인터페이스를 만드는 것입니다. 이 인터페이스는 극히 단순한, 단일 함수 선언 입니다.

Interface ITouchable;

function OnTouch(EZoneTouchEvent Type, float X, float Y);

여기서 OnTouch 함수는 MobileMenuScene 클래스의 OnSceneTouch() 함수를 흉내내어, 그 함수가 터치 입력 데이터를 이 ITouchable 인터페이스를 구현하는 콘트롤에 단순 전달할 수 있도록 합니다.

씬 클래스

범용 터치 이벤트에 완전히 접근하려면 약간의 속임수를 끌어들여야 합니다. 폭이나 높이가 없는 베이스 씬 클래스를 새로 만들어서, 새로운 델리게이트를 통해 실제 씬에 입력 전달을 처리하도록 합니다. 실제 씬 클래스는 열릴 때 헬퍼로써 새로운 베이스 씬을 열고, 그 OnSceneTouch() 함수를 베이스 씬의 델리게이트에 할당합니다. 그리고서 OnSceneTouch() 함수로부터 입력을 받은 다음 터치가능 콘트롤로 전달하는 것입니다. 그 이후 터치가능 콘트롤을 사용하는 씬은 모두 이 클래스에서 확장되며, 자동으로 그러한 콘트롤 작동 준비를 처리하는 함수성을 갖게 됩니다.

입력 처리

입력 전달의 배경이 되는 개념은, 씬 클래스가 그 OnSceneTouch() 함수를 통해 모든 터치 입력 이벤트의 통지를 받는다는 것입니다. 이 함수 안에서, 씬에 속하는 모든 콘트롤 목록인 MenuObjects 배열이 반복처리되어, 배열의 각 항목을 ITouchable 인터페이스로 던져(cast) 줍니다. 던지기가 성공하면 콘트롤은 터치가능해 지며, 입력은 인터페이스에서 상속된 OnTouch() 함수를 사용하는 콘트롤에 전달됩니다. 추가로 여기서, 어느 콘트롤도 입력이 스와이프인지 탭인지를 오너 씬에 물을 수 있도록 약간의 기본적 스와이프 감지가 수행됩니다. 그 기능이 필요할 때마다 각 콘트롤에서 이 함수성을 재구현할 필요 없이 말입니다.

목록 처리

베이스 씬 클래스는 새로운 콘트롤 클래스 UDNMobileMenuLint 또한 참조합니다. 이 클래스는 아직 없는데, 만들어지면 항목 스크롤 목록을 표시하는 데 사용될 것입니다. 보이는 목록은 항상 전체화면일 것이며, 씬 내의 다른 콘트롤 위에 겹쳐놓아 줘야 합니다. 콘트롤은 모두 순서대로 렌더링되기에 어느 시점에 어느 콘트롤이 목록을 표시하려는지 알 수 있는 방법이 없으며, 이를 보장하기 위한 유일한 방법은 목록 렌더링 처리를 씬에 맡겨 목록을 사용하는 콘트롤은 무엇이든 필요에 따라 그 목록을 씬에 할당하도록 하는 것입니다.

헬퍼 씬 클래스

class UDNMobileMenuBase extends MobileMenuScene;


/**
 * (씬 밖에) 범용 터치가 발생하면 델리게이트 발동
 */
delegate bool OnInputTouch(EZoneTouchEvent EventType, float TouchX, float TouchY);

/**
 * 터치 입력 발생시 호출
 */
function bool OnSceneTouch(EZoneTouchEvent EventType, float TouchX, float TouchY)
{
   OnInputTouch(EventType, TouchX, TouchY);
   
   return true;   
}

defaultproperties
{
}

실제 씬 클래스

class UDNMobileMenuScene extends MobileMenuScene;

/** 초기 터치 이벤트의 캐시된 위치 */
var Vector StartTouchLocation;

/** 지난 터치 업데이트 이벤트의 현재 위치 */
var Vector CurrentTouchLocation;

/** 현재 터치가 스와이프면 참 */
var bool bSwipe;

/** 터치가 스와이프로 간주되기 위한 최소 이동 거리. 이보다 작으면 탭. */
var float SwipeTolerance;

/** 콤보 박스가 그 목록을 모든 콘트롤 위에 겹쳐 표시하도록 하기 위해 사용 */
var instanced UDNMobileMenuList List;

var UDNMobileMenuBase HelperScene;


function Opened(string Mode)
{
   Super.Opened(Mode);
   
   //새 베이스 씬 열기 - 범용 터치 이벤트를 받을 수 있도록 폭이나 높이가 없음 
   HelperScene = UDNMobileMenuBase(InputOwner.OpenMenuScene(class'UDNMobileMenuBase'));
   
   //우리 핸들러를 베이스 씬의 인풋 델리게이트로 할당
   HelperScene.OnInputTouch = OnSceneTouch;
}

function Closing()
{
   //Cleanup - 씬을 닫기 전에 헬퍼 씬 닫기
   if(HelperScene != none)
   {
      InputOwner.CloseMenuScene(HelperScene);
   }

   Super.Closing();
}

function bool OnSceneTouch(EZoneTouchEvent EventType, float TouchX, float TouchY)
{
   local MobileMenuObject Touchable;
   
   //초기 터치 이벤트
   if(EventType == ZoneEvent_Touch)
   {
      //스와이프 비우기
      bSwipe = false;
      
      //터치 위치 캐시
      StartTouchLocation.X = TouchX;
      StartTouchLocation.Y = TouchY;
   }
   //터치 업데이트 이벤트
   else if(EventType == ZoneEvent_Update)
   {      
      //현재 터치 위치 저장
      CurrentTouchLocation.X = TouchX;
      CurrentTouchLocation.Y = TouchY;
      
      //이 터치가 스와이프인지 확인
      CheckSwipe();
   }

   //우리 목록이 표시되지 않으면 입력을 터치가능 콘트롤로 전달
   if(List == none || List.bIsHidden)
   {
      foreach MenuObjects(Touchable)
      {   
         if(ITouchable(Touchable) != none)
         {
            ITouchable(Touchable).OnTouch(EventType, TouchX, TouchY);
         }
      }   
   }
   //목록이 보이면 터치 입력을 전달
   else if(List != none && !List.bIsHidden)
   {
      ITouchable(List).OnTouch(EventType, TouchX, TouchY);
   }
   
   //입력을 처리했음을 확인하기 위해 참 반환
   return true;
}

function CheckSwipe()
{
   //터치를 스와이프로 간주할 만큼 움직였는지 검사
   if(VSize(StartTouchLocation - CurrentTouchLocation) > SwipeTolerance)
   {
      bSwipe = true;
   }   
}

function RenderScene(Canvas Canvas,float RenderDelta)
{
   //목록이 보이면 그리기
   if(List != none && !List.bIsHidden)
   {
      List.RenderObject(Canvas);
   }
   //목록이 없으면 우리 콘트롤 그리기
   else
   {
      Super.RenderScene(canvas, RenderDelta);
   }
}

defaultproperties
{
   SwipeTolerance = 5.0
}

베이스 터치가능 콘트롤 클래스

베이스 터치가능 콘트롤 클래스는 MobileMenuObject 콘트롤 클래스를 확장하며, 새 ITouchable 인터페이스를 구현하고, 터치 위치가 경계 내인지 & 현재 터치가 오너 씬에서 스와이프인지 검사하기 위한 헬퍼 함수 한 쌍을 추가합니다. ITouchable 인터페이스에 OnTouch() 함수 역시도 정의해야 합니다. 이 함수의 기본 구현은 단순히, 입력을 검사하여 콘트롤이 터치되었는지 알아보고 그에 맞게 bIsTouchedbIsHightlighted 변수를 설정해 주는 것입니다.

class UDNMobileMenuObject extends MobileMenuObject
   implements(ITouchable) DependsOn(UDNMobileMenuBase);

   
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{   
   if(bIsActive)
   {
      if(CheckBounds(X, Y))
      {
         if(Type == ZoneEvent_Touch)
         {
            bIsTouched = true;
            bIsHighlighted = true;
         }
         else if(Type == ZoneEvent_Untouch || Type == ZoneEvent_Cancelled)
         {
            bIsTouched = false;
            bIsHighlighted = false;
         }
      }
      else
      {
         bIsTouched = false;
         bIsHighlighted = false;
      }
   }
}

function bool CheckBounds(float X, float Y)
{
   if(X >= Left && X <= Left + Width && Y >= Top && Y <= Top + Height)
   {
      return true;
   }
   
   return false;
}

function bool CheckSwipe()
{
   return UDNMobileMenuScene(OwnerScene).bSwipe;
}

커스텀 라벨

터치가능 라벨 콘트롤은 표준 MobileMenuButtonMobileMenuLabel 클래스의 수정 버전입니다. 그 클래스의 코드는 새 클래스로 병합시킨 다음, 단순 박스나 경계 배경 시각 성분같은 부가 렌더링 함수성, 캡션 텍스트를 정렬하기 위한 기능 등을 추가하기 위해 변경되었습니다.

class UDNMobileMenuLabel extends UDNMobileMenuObject;
   

/** 라벨을 이루는 이미지 둘. [0] = 언터치됨, [1] = 터치됨 */
var Texture2D Images[2];

/** 이미지에 대한 UV 좌표. [0] = 언터치됨, [1] = 터치됨 */
var UVCoords ImagesUVs[2];

/** 이미지 덮어쓰기용 색 저장 */
var LinearColor ImageColors[2];

/** 라벨용 현지화가능 캡션 */
var string Caption;

/** 캡션용 색 저장 */
var LinearColor CaptionColors[2];

/** 텍스트를 그리는 데 사용될 폰트 저장 */
var font TextFont;

/** 참이면 라벨의 텍스츠 중앙정렬. 거짓이면 왼쪽정렬. */
var bool bCenterText;

/** 참이면 라벨 텍스트 줄바꾸기 안함 */
var bool bClipText;

/** 텍스트 중앙정렬을 하지 않을 때의 여백 픽셀 수 */
var float TextPadding;

/** 라벨 배경을 그리는 데 사용할 색. 지정된 텍스처가 없으면 사용 */
var LinearColor BackgroundColors[2];

/** 라벨 테두리를 그리는 데 사용할 색. 지정된 텍스처가 없으면 사용 */
var LinearColor BorderColors[2];

/** 네 테두리 폭. (순서: 위, 오른쪽, 아래, 왼쪽) */
var float BorderWidth[4];

/** 참이면 라벨 이미지를 그리는 데 DrawTileStretched() 사용. 거짓이면 DrawTile() 사용. */
var bool bStretchBackground;


/**
 * 라벨 초기화 - 이미지 좌표 구성
 */
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{
   local int i;
   
   Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);

   //커스텀 좌표 없음, 이미지 전체 크기로 설정
   for (i=0;i<2;i++)
   {
      if (!ImagesUVs[i].bCustomCoords && Images[i] != none)
      {
         ImagesUVs[i].U = 0.0f;
         ImagesUVs[i].V = 0.0f;
         ImagesUVs[i].UL = Images[i].SizeX;
         ImagesUVs[i].VL = Images[i].SizeY;
      }
   }
}


/**
 * 위젯 렌더링
 */
function RenderObject(canvas Canvas)
{
   local int Idx;
   local LinearColor DrawColor;

   //라벨이 터치 또는 반전되고 있는지에 따라 이미지/컬러 인덱스 구하기
   Idx = (bIsTouched || bIsHighlighted) ? 1 : 0;
   
   //이미지 세트가 있다면 그리기
   if(Images[Idx] != none)
   {   
      //배경 이미지 그리기용으로 캔버스 구성
      Canvas.SetPos(OwnerScene.Left + Left - Canvas.OrgX, OwnerScene.Top + Top - Canvas.OrgY);
      Drawcolor = ImageColors[Idx];
      Drawcolor.A *= Opacity * OwnerScene.Opacity;
      
      //배경 이미지를 늘여 그리기
      if(bStretchBackground)
      {
         Canvas.DrawTileStretched(Images[Idx], Width, Height,ImagesUVs[Idx].U, ImagesUVs[Idx].V, ImagesUVs[Idx].UL, ImagesUVs[Idx].VL, DrawColor, true, true);
      }
      //배경 이미지를 스케일 조절하여 그리기
      else
      {
         Canvas.DrawTile(Images[Idx], Width, Height,ImagesUVs[Idx].U, ImagesUVs[Idx].V, ImagesUVs[Idx].UL, ImagesUVs[Idx].VL, DrawColor, true);
      }
   }
   //설정된 이미지가 없어, 단순한 사각 배경 그리기
   else
   {
      //배경 사각형 그리기용 캔버스 구성
      Canvas.SetPos(OwnerScene.Left + Left - Canvas.OrgX, OwnerScene.Top + Top - Canvas.OrgY);
      Canvas.DrawColor.R = byte(BackgroundColors[Idx].R * 255.0);
      Canvas.DrawColor.G = byte(BackgroundColors[Idx].G * 255.0);
      Canvas.DrawColor.B = byte(BackgroundColors[Idx].B * 255.0);
      Canvas.DrawColor.A = byte(BackgroundColors[Idx].A * 255.0);
      Canvas.DrawRect(Width, Height);
      
      //테두리 그리기
      DrawBorder(Canvas);
   }

   //캡션 텍스트 그리기
   RenderCaption(Canvas);
}

/**
 * 라벨의 테두리 그리기
 *
 * @param Canvas - 드리기에 사용되는 캔버스 오브젝트
 */
function DrawBorder(Canvas Canvas)
{
   local int Idx;
   
   Idx = (bIsTouched || bIsHighlighted) ? 1 : 0;
   
   //위쪽 테두리 그리기
   if(BorderWidth[0] > 0)
   {
      Canvas.SetPos(Left - Canvas.OrgX, Top - Canvas.OrgY);
      Canvas.DrawColor.R = byte(BorderColors[Idx].R * 255.0);
      Canvas.DrawColor.G = byte(BorderColors[Idx].G * 255.0);
      Canvas.DrawColor.B = byte(BorderColors[Idx].B * 255.0);
      Canvas.DrawColor.A = byte(BorderColors[Idx].A * 255.0);
      Canvas.DrawRect(Width, BorderWidth[0]);
   }
   
   //오른쪽 테두리 그리기
   if(BorderWidth[1] > 0)
   {
      Canvas.SetPos(Left + Width - 1 - Canvas.OrgX, Top + Height - 1 - Canvas.OrgY);
      Canvas.DrawColor.R = byte(BorderColors[Idx].R * 255.0);
      Canvas.DrawColor.G = byte(BorderColors[Idx].G * 255.0);
      Canvas.DrawColor.B = byte(BorderColors[Idx].B * 255.0);
      Canvas.DrawColor.A = byte(BorderColors[Idx].A * 255.0);
      Canvas.DrawRect(BorderWidth[1], Height);
   }
   
   //아래쪽 테두리 그리기
   if(BorderWidth[2] > 0)
   {
      Canvas.SetPos(Left - Canvas.OrgX, Top + Height - 1 - Canvas.OrgY);
      Canvas.DrawColor.R = byte(BorderColors[Idx].R * 255.0);
      Canvas.DrawColor.G = byte(BorderColors[Idx].G * 255.0);
      Canvas.DrawColor.B = byte(BorderColors[Idx].B * 255.0);
      Canvas.DrawColor.A = byte(BorderColors[Idx].A * 255.0);
      Canvas.DrawRect(Width, BorderWidth[2]);
   }
   
   //왼쪽 테두리 그리기
   if(BorderWidth[3] > 0)
   {
      Canvas.SetPos(Left - Canvas.OrgX, Top - Canvas.OrgY);
      Canvas.DrawColor.R = byte(BorderColors[Idx].R * 255.0);
      Canvas.DrawColor.G = byte(BorderColors[Idx].G * 255.0);
      Canvas.DrawColor.B = byte(BorderColors[Idx].B * 255.0);
      Canvas.DrawColor.A = byte(BorderColors[Idx].A * 255.0);
      Canvas.DrawRect(BorderWidth[3], Height);
   }
}

/**
 * 라벨의 텍스트 그리기
 *
 * @param Canvas - 그리기에 사용되는 캔버스 오브젝트
 */
function RenderCaption(canvas Canvas)
{
   local float X,Y,UL,VL;
   local FontRenderInfo FRI;
   local int Idx;
   
   Idx = (bIsTouched || bIsHighlighted) ? 1 : 0;

   //텍스트가 설정되어 있어야만 그리기
   if (Caption != "")
   {
      Canvas.Font = TextFont;
      Canvas.TextSize(Caption,UL,VL);

      //필요하면 텍스트 중앙정렬, 아니면 왼쪽정렬
      if(bCenterText)
      {
         X = Left + (Width / 2) - (UL/2);
         Y = Top + (Height /2) - (VL/2);
      }
      else
      {
         X = Left + 5;
         Y = Top + (Height /2) - (VL/2);
      }

      //캡션 그리기용 캔버스 구성
      Canvas.SetPos(OwnerScene.Left + X - Canvas.OrgX, OwnerScene.Top + Y - Canvas.OrgY);
      Canvas.DrawColor.R = byte(CaptionColors[Idx].R * 255.0);
      Canvas.DrawColor.G = byte(CaptionColors[Idx].G * 255.0);
      Canvas.DrawColor.B = byte(CaptionColors[Idx].B * 255.0);
      Canvas.DrawColor.A = byte(CaptionColors[Idx].A * 255.0);

      //텍스트 그리기 - 필요하면 잘라내기
      FRI.bClipText = bClipText;
      Canvas.DrawText(Caption, false, 1.0, 1.0, FRI);
   }
}

defaultproperties
{
   ImageColors(0)=(r=1.0,g=1.0,b=1.0,a=1.0)
   ImageColors(1)=(r=0.5,g=0.5,b=0.5,a=1.0)
   CaptionColors(0)=(r=0.0,g=0.0,b=0.0,a=1.0)
   CaptionColors(1)=(r=1.0,g=1.0,b=1.0,a=1.0)
   
   bIsActive=false
   bCenterText=false
   bStretchBackground=true
   
   BorderWidth(0)=0
   BorderWidth(1)=0
   BorderWidth(2)=0
   BorderWidth(3)=0
}

터치가능 버튼

터치가능 버튼 콘트롤은 UDNMobileMenuLabel 클래스의 단순한 확장으로, 탭 감지 및 버튼이 "클릭됨" 을 리스너에게 알리기 위한 새 OnClick 델리게이트 호출 기능을 추가하기 위해 OnTouch() 함수를 덮어쓰는 것입니다.

class UDNMobileMenuButton extends UDNMobileMenuLabel;
   

/** 
 * 버튼이 탭을 받으면 호출되는 델리게이트
 */
delegate OnClick(UDNMobileMenuObject Sender, float X, float Y);

/**
 * ITouchable 인터페이스의 OnTouch() 멤버 구현.
 * 부모가 터치 이벤트를 받을 때 호출.
 *
 * @param Type - 터치 이벤트 유형
 * @param X - 터치 이벤트 가로 위치
 * @param Y - 터치 이벤트 세로 위치
 */
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
   Super.OnTouch(Type, X, Y);
   
   //버튼 경계 내에서 끝나는 터치, 리스너에게 탭을 알리고자 OnClick 델리게이트 발동
   if(Type == ZoneEvent_Untouch && CheckBounds(X, Y))
   {
      OnClick(self, X, Y);
   }
}

defaultproperties
{
   bIsActive=true
   bCenterText=true
}

스크롤 목록

스크롤 목록 클래스는 iOS 인터페이스에서와 같은 표준 목록 콘트롤과 유사한 방식으로 작동하는 콘트롤을 새로 만들기 시도해 봅니다. UDNMobileMenuButton 마다 하나씩 항목 목록으로, 터치 입력을 사용하여 위아래 스크롤이 가능합니다. 스와이프는 터치가 끝난 후에도 얼마간 목록을 계속해서 움직이게 만들어 "관성" 스크롤 동작을 냅니다. 이 목록은 텍스트 잘라내기(클리핑) 제한때문에, 즉 뷰포트 규모에 따라서만 잘라낼 수 있기에 항상 전체화면이어야 합니다.

class UDNMobileMenuList extends UDNMobileMenuObject;
   
/** 목록의 제목으로 표시할 텍스트 */
var string Title;   

/** 제목 텍스트 색 */
var Color CaptionColor;

/** 참이면 캡션 텍스트 중앙정렬. 거짓이면 왼쪽정렬. */
var bool bCenterText;
   
/** 목록 배경에 채울 색 */
var Color BackgroundColor;

/** 목록 테두리 색 */
var Color BorderColor;

/** 목록 항목의 배경색. [0] 은 비선택, [1] 은 선택 항목. */
var LinearColor ItemBackgroundColors[2];

/** 목록 항목의 테두리 색. [0] 은 비선택, [1] 은 선택 항목. */
var LinearColor ItemBorderColors[2];

/** 목록 항목 텍스트 색. [0] 은 비선택, [1] 은 선택 항목. */
var LinearColor ItemCaptionColors[2];

/** 목록 항목 텍스트 폰트. */
var Font ItemFont;

/** 목록을 닫는 데 사용할 취소 버튼으로의 참조. */
var instanced UDNMobileMenuButton Cancel;

/** 참이면 목록은 취소가능. (즉 취소 버튼이 표시됨) */
var bool bHasCancel;

/** 메뉴에 속하는 항목 목록 */
var instanced array<UDNMobileMenuButton> Items;

/** 현재 선택된 항목 인덱스 */
var int SelectedIndex;

/** 목록 내 각 항목 셀의 높이 */
var float ItemHeight;

/** 제목 바 높이 */
var float TitleBarHeight;

/** 제목 바 색 */
var Color TitleBarColor;

/** 목록이 제어되는 중이면, 즉 사용자가 현재 목록을 스크롤중이거나 하면 참. */
var bool bActive;

/** 목록에 대한 지난 터치 이벤트의 캐시된 위치 저장 */
var vector LastTouchLocation;

/** 목록이 활성 상태일 때 스크롤시킬 양. (터치 업데이트간의 델타. 목록과 사용자의 손가락 동기화 유지.) */
var float ScrollAmount;

/** 목록이 비활성 상태일 때 스크롤시킬 양. (스와이프를 통해 목록에 저장되는 속도가 얼마인지.) */
var float ScrollInertia;

/** 지난 렌더링 업데이트의 캐시된 시간 저장 */
var float LastRenderTime;

/** ScrollInertial(스크롤 관성) 제어에 사용되는 터치 이동에 대한 곱수 */
var float SwipeFactor;

/** 목록이 경계 너머로 스크롤되었을 때 되잡아끌 속도에 대한 곱수 */
var float SnapFactor;

/** 목록의 경계 너머 스크롤 가능한 거리 */
var float ScrollLimit;


/****************************************
* 초기화 및 렌더링 함수
****************************************/

/**
 * 목록 초기화
 */
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{   
   Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //텍스트를 목록의 경계에 맞춰 잘라내기가 쉽지 않기에, 목록을 강제로 디바이스 화면의 크기에 맞춤
   Left = 0;
   Top = 0;
   Width = ScreenWidth;
   Height = ScreenHeight;
   
   //취소 버튼 초기화 (메뉴 시스템은 콘트롤 내의 콘트롤에 대한 처리가 내장되어있지 않음)
   Cancel.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //취소 버튼 위치를 제목 바 오른쪽에 세로 중앙정렬
   Cancel.Top = Top + ((TitleBarHeight - Cancel.Height) / 2);
   Cancel.Left = Left + Width - Cancel.Width - 1;
   
   //버튼에서 클릭 이벤트 잡기
   Cancel.OnClick = HandleCancel;
}

/**
 * 목록 그리기
 */
function RenderObject(canvas Canvas)
{
   local int i;
   local float DeltaTime;
   local float ScrollDelta;
   
   Canvas.SetPos(Left, Top);
   
   //목록 배경 그리기
   Canvas.DrawColor = BackgroundColor;
   Canvas.DrawRect(Width, Height);
   
   //목록 수동 스크롤에 사용할 렌더 델타 계산
   if(LastRenderTime != 0)
   {
      DeltaTime = InputOwner.Outer.WorldInfo.RealTimeSeconds - LastRenderTime;
   }
   lastRenderTime = InputOwner.Outer.WorldInfo.RealTimeSeconds;   
   
   if(Items.Length > 0)
   {
      //이 렌더 패스에 대한 스크롤 델타 계산 및 목록 경계 밖이면 목록 잡아채기
      if(Items[0].Top > Top + TitleBarHeight)
      {
         ScrollInertia = Top + TitleBarHeight - Items[0].Top;
         ScrollDelta = ScrollInertia * DeltaTime * SnapFactor;
      }
      else if(Items[Items.Length - 1].Top + ItemHeight < Top + Height)
      {
         ScrollInertia = (Top + Height) - (Items[Items.Length - 1].Top + ItemHeight);
         ScrollDelta = ScrollInertia * DeltaTime * SnapFactor;
      }
      else
      {      
         ScrollDelta = ScrollInertia * DeltaTime;
      }
      
      //각 목록 항목 렌더링
      for(i=0; i < Items.Length; i++)
      {
         //오리엔테이션이 변경된 경우 여기서 항목 크기 강제
         Items[i].Left = left;
         Items[i].Top += bActive ? ScrollAmount : ScrollDelta;
         Items[i].Width = Width;
         Items[i].Height = ItemHeight;
         
         //일부라도 보이는 항목만 렌더링
         if(Items[i].Top + ItemHeight > Top && Items[i].Top < Top + Height)
         {
            Items[i].RenderObject(Canvas);
         }
      }
   }
   
   if(bActive)
   {
      //능동이면 활성 스크롤 0화
      ScrollAmount = 0;
   }
   else
   {
      //수동이면 스크롤 관성 감소
      ScrollInertia -= ScrollDelta;
   }
   
   //제목 바 그리기 - 목록 항목이 제목 바에 클리핑되게 목록 항목 이후에 그리기
   Canvas.SetPos(Left, Top);   
   Canvas.DrawColor = TitleBarColor;
   Canvas.DrawRect(Width, TitleBarHeight);
   
   //제목 그리기
   RenderTitle(Canvas);
   
   //목록 테두리 그리기
   Canvas.SetPos(Left, Top + TitleBarHeight);   
   Canvas.DrawColor = BorderColor;
   Canvas.DrawBox(Width, Height - TitleBarHeight);
   
   //원하는 경우 취소 버튼 그리기
   if(bHasCancel)
   {
      Cancel.RenderObject(Canvas);
   }
}

/**
 * 목록의 제목 텍스트 그리기
 */
function RenderTitle(canvas Canvas)
{
   local float X,Y,UL,VL;
   local FontRenderInfo FRI;

   //제목이 없어도 걱정 뚝
   if (Title != "")
   {
      //폰트 설정 (씬 폰트를 빌려쓰겠지만, 커스텀 폰트 추가 가능)
      Canvas.Font = OwnerScene.SceneCaptionFont;
      Canvas.TextSize(Title,UL,VL);

      //위치 계산 - 중앙정렬 또는 왼쪽정렬
      if(bCenterText)
      {
         X = Left + (Width / 2) - (UL/2);
         Y = Top + (Height /2) - (VL/2);
      }
      else
      {
         X = Left + (TitleBarHeight /2) - (VL/2);
         Y = Top + (TitleBarHeight /2) - (VL/2);
      }

      //제목 텍스트를 클리핑하여 그리기 (들어맞지 않으면 ... 식으로 줄이는 기능 추가 가능.)
      Canvas.SetPos(OwnerScene.Left + X - Canvas.OrgX, OwnerScene.Top + Y - Canvas.OrgY);
      Canvas.DrawColor = CaptionColor;
      FRI.bClipText = true;
      Canvas.DrawText(Title, false, 1.0, 1.0, FRI);
   }
}

/****************************************
* 항목 관리 함수
****************************************/

/**
 * 목록에 새 항목 추가
 *
 * @param item - 추가시킬 새 항목 값
 */
function AddItem(string item)
{
   local UDNMobileMenuButton NewItem;
   local Vector2D ViewportSize;
   
   //새 항목 (버튼) 생성
   NewItem = new(Outer) class'UDNMobileMenuButton';
   
   if(NewItem != none)
   {
      //새 항목 초기화
      LocalPlayer(InputOwner.Outer.Player).ViewportClient.GetViewportSize(ViewportSize);
      NewItem.InitMenuObject(InputOwner, OwnerScene, ViewportSize.X, ViewportSize.Y);
      
      //새 항목 속성 설정
      NewItem.Caption = item;
      NewItem.CaptionColors[0] = ItemCaptionColors[0];
      NewItem.CaptionColors[1] = ItemCaptionColors[1];
      NewItem.TextFont = ItemFont;
      NewItem.Left = Left;
      NewItem.BackgroundColors[0] = ItemBackgroundColors[0];
      NewItem.BackgroundColors[1] = ItemBackgroundColors[1];
      NewItem.BorderColors[0] = ItemBorderColors[0];
      NewItem.BorderColors[1] = ItemBorderColors[1];
      NewItem.Top = Top + TitleBarHeight + (Items.Length * ItemHeight);
      NewItem.OnClick = OnSelect;
      NewItem.bCenterText = false;
      NewItem.BorderWidth[2] = 1;
      
      //선택 항목이 되려는 경우, 반전시킴
      if(Items.Length == SelectedIndex)
      {
         NewItem.bIsHighlighted = true;
      }
      
      //항목을 목록에 추가
      Items.AddItem(NewItem);
   }
}

/**
 * 목록에서 값 제거
 * 
 * @param Idx - 제거할 항목의 인덱스
 */
function RemoveItem(int Idx)
{
   local int i;
   
   //목록에서 항목 제거
   Items.Remove(Idx, 1);
   
   //다음 항목을 위로 이동
   for(i=Idx; i < Items.Length; i++)
   {
      Items[i].Top -= ItemHeight;
   }
   
   //선택된 항목을 제거한 경우, 첫 항목으로 선택 재설정
   if(SelectedIndex == Idx)
   {
      SelectedIndex = 0;
      if(Items.Length > 0)
      {
         Items[0].bIsHighlighted = true;
      }
   }
}

/**
 * 목록의 값 구하기
 *
 * 선택된 항목의 캡션 반환
 */
function string GetValue()
{
   //선택된 인덱스에 액세스할 항목을 가져야 함
   if(Items.Length > 0)
   {
      //선택된 항목의 캡션 (값) 반환
      return Items[SelectedIndex].Caption;
   }
   else
   {
      //항목 없음 - 빈 문자열 반환
      return "";
   }
}

/****************************************
* 인풋 함수
****************************************/
   
/**
 * 목록 내 항목이 선택되면 델리게이트 발동
 */
delegate OnChange(int Idx, string Item, float X, float Y);

/**
 * 목록이 취소되면 델리게이트 발동
 */
delegate OnCancel();

/**
 * ITouchable 인터페이스 구현
 */
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
   local UDNMobileMenuButton Label;
   local float ActualY;
   
   Super.OnTouch(Type, X, Y);
   
   //초기 터치 이벤트
   if(Type == ZoneEvent_Touch)
   {
      //목록 밖 또는 제목 바에 터치된 경우 무시
      if(CheckBounds(X, Y) && Y > Top + TitleBarHeight)
      {
         //목록 활성으로 설정 - 사용자가 제어중
         bActive = true;
         
         //터치 위치 캐시
         LastTouchLocation.X = X;
         LastTouchLocation.Y = y;
         
         //스크롤 값 리셋
         ScrollAmount = 0;
         ScrollInertia = 0;
      }
   }
   //터치 진행중
   else if(Type == ZoneEvent_Update || Type == ZoneEvent_Stationary)
   {
      //목록이 활성이 아니면 무시 - 터치가 목록 밖에서 시작된 듯
      if(bActive)
      {
         /************************************************************
         *   다음과 같은 경우에만 스크롤 업데이트:
         *
         *    * 스크롤이 필요할 만큼 항목이 있음
         *    * 목록이 이미 경계 밖으로 스크롤되지 않음
         *    
         ************************************************************/
         if(   (Items.Length * ItemHeight > Height) && 
            ((Items[0].Top - (Top + TitleBarHeight) < ScrollLimit) || (Y < LastTouchLocation.Y)) && 
            (((Top + Height) - (Items[Items.Length - 1].Top + ItemHeight) < ScrollLimit) || (Y > LastTouchLocation.Y)))
         {
            //사용된 관성을 구하고자 실제 관성에 디바이스 스케일링과 커스텀 스와이프 인수로 곱해줌
            ActualY = (Y - LastTouchLocation.Y) * class'MobileMenuScene'.static.GetGlobalScaleY() * SwipeFactor;
            
            //이동이 무시가능한 수준이면 스크롤 관성 리셋. 아니면 새로운 델타만큼 증가.
            if(Abs(ActualY) < UDNMobileMenuScene(OwnerScene).SwipeTolerance)
            {
               ScrollInertia = 0;
            }
            else
            {
               ScrollInertia += ActualY;
            }
            
            //활성 스크롤 및 증분용 이동 델타 계산
            ActualY = (Y - LastTouchLocation.Y);
            ScrollAmount += ActualY;
         }
         
         //캐시된 터치 위치 업데이트
         LastTouchLocation.X = X;
         LastTouchLocation.Y = y;
      }
   }
   //사용자가 손가락을 들어 터치 종료
   else if(Type == ZoneEvent_Untouch)
   {
      //스와이프가 아니며 목록 내임
      if(!CheckSwipe() && CheckBounds(X, Y))
      {   
         //제목 바 안이 아님 - 항목 위의 탭인 듯
         if(Y > Top + TitleBarHeight)
         {
            //모든 항목 위에 탭 전달
            foreach Items(Label)
            {
               Label.OnTouch(Type, X, Y);
            }
         }
         //제목 바에 탭
         else
         {
            //목록이 취소 가능하면 취소 버튼에 탭 처리 위임
            if(bHasCancel)
            {
               Cancel.OnTouch(Type, X, Y);
            }
         }
      }
      
      //수동으로 가기 전에 스크롤 관성 마지막 업데이트
      if(bActive)
      {
         ActualY = (Y - LastTouchLocation.Y) * class'MobileMenuScene'.static.GetGlobalScaleY() * SwipeFactor;
         ScrollInertia += ActualY;
         
         //목록 수동으로 설정
         bActive = false;
      }
   } 
   //터치가 미완 상태로 종료
   else if(Type == ZoneEvent_Cancelled)
   {      
      //수동으로 설정
      bActive = false;
      
      //모든 스크롤 값 0화
      ScrollAmount = 0;
      ScrollInertia = 0;
   }
}

/**
 * 목록 내 새 항목 선택.
 * 목록 내 각 항목의 OnClick() 으로 할당.
 */
function OnSelect(UDNMobileMenuObject Sender, float X, float Y)
{
   local UDNMobileMenuButton Label;
   local int i;
   
   i = 0;
   
   if(UDNMobileMenuButton(Sender) != none)
   {
      //보낸이가 일치하는 항목을 찾고자 항목 반복처리
      foreach Items(Label)
      {
         //일치하는 항목 찾음
         if(Label == UDNMobileMenuButton(Sender))
         {
            //새 선택 인덱스 설정
            SelectedIndex = i;
            
            //선택된 항목 반전
            Sender.bIsHighlighted = true;
            
            //리스닝중인 것에 알리고자 OnChange 델리게이트 호출
            OnChange(i, Label.Caption, X, Y);
            
            //모든 작업이 끝났으니 반복처리 빠져나가기
            break;
         }
         
         i++;
      }
   }
   
   //수동으로 설정
   bActive = false;
   
   //모든 스크롤 값 0화
   ScrollAmount = 0;
   ScrollInertia = 0;
}

/**
 * 목록 취소하기.
 * 취소 버튼의 OnClick() 으로 할당.
 */
function HandleCancel(UDNMobileMenuObject Sender, float X, float Y)
{
   //리스닝중인 것에 알리고자 OnCancel 델리게이트 호출
   OnCancel();
   
   //수동으로 설정
   bActive = false;
   
   //모든 스크롤 값 0화
   ScrollAmount = 0;
   ScrollInertia = 0;
}

defaultproperties
{
   CaptionColor=(R=255,G=255,B=255,A=255)
   BackgroundColor=(R=64,G=64,B=64,A=255)
   BorderColor=(R=64,G=64,B=64,A=255)
   ItemBackgroundColors(0)=(R=0.5,G=0.5,B=0.5,A=1.0)
   ItemBackgroundColors(1)=(R=0.75,G=0.75,B=0.75,A=1.0)
   ItemBorderColors(0)=(R=0.25,G=0.25,B=0.25,A=1.0)
   ItemBorderColors(1)=(R=1.0,G=1.0,B=1.0,A=1.0)
   ItemCaptionColors(0)=(R=1.0,G=1.0,B=1.0,A=1.0)
   ItemCaptionColors(1)=(R=1.0,G=1.0,B=1.0,A=1.0)
   ItemFont=Font'EngineFonts.SmallFont'
   
   ItemHeight=48
   SwipeFactor=5
   SnapFactor=5
   ScrollLimit=20
   
   TitleBarHeight=32
   TitleBarColor=(R=96,G=96,B=96,A=255)
   
   Begin Object class=UDNMobileMenuButton name=CancelButton
      Tag="Cancel"
      Width=64
      Height=32
      Caption="Cancel"
      TextFont=Font'EngineFonts.SmallFont'
      CaptionColors(0)=(r=1.0,g=1.0,b=1.0,a=1.0)
      CaptionColors(1)=(r=0.0,g=0.0,b=0.0,a=1.0)
      Images(0)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
      Images(1)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
      ImagesUVs(0)=(bCustomCoords=true,U=0,V=896,UL=256,VL=128)
      ImagesUVs(1)=(bCustomCoords=true,U=0,V=896,UL=256,VL=128)
   End Object
   Cancel=CancelButton
}

콤보박스

콤보박스 콘트롤은 메뉴의 세팅 값을 옵션 목록에서 선택할 수 있는 단순한 방법을 제공합니다. 탭되면 스크롤 목록 콘트롤을 여닫는 터치가능 버튼이 콤보박스의 주요 시각 성분입니다. 목록에는 콤보박스의 옵션이 전부 표시되며, 하나를 선택하면 목록이 닫히고 콤보박스의 값을 새로이 선택된 옵션으로 설정합니다.

주: 콤보박스에 대한 옵션은 현재 콤보박스가 생성될 때 하드 코딩을 요합니다. 이는 오브젝트별-환경설정 속성 또는 사용가능한 옵션 목록을 채우기 위한 추가 방법으로써 작동하도록 쉽게 변경 가능합니다.

class UDNMobileMenuComboBox extends UDNMobileMenuObject;


/** 콤보박스를 시각적으로 표현해 내는 버튼 */
var instanced UDNMobileMenuButton Label;

/** 콤보박스용 새 값 선택에 사용되는 목록 */
var instanced UDNMobileMenulist list;

/** 콤보박스 제목 - 목록 제목 설정시에도 사용 */
var string Title;

/** 참이면 제목 표시. 거짓이면 현재 값 표시. */
var bool bShowTitle;

/** 목록을 채우는 데 사용되는 값 목록. */
var array<string> Items;

/** 콤보 박스를 이 값 인덱스로 초기화. */
var int InitialSelectedIndex;

/** 새 값 선택을 위해 목록이 열려 있으면 참. */
var bool bIsOpen;


/****************************************
* 초기화 및 렌더링 함수
****************************************/

/**
 * 콤보박스 초기화
 */
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{
   local string item;
   
   Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //라벨 및 목록 초기화
   Label.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   List.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //라벨의 위치와 크기 설정
   Label.Left = Left;
   Label.Width = Width;
   Label.Top = Top;
   Label.height = height;
   
   //라벨 캡션 초기화
   Label.Caption = Items[InitialSelectedIndex];
   
   //델리게이트 구성
   Label.OnClick = OnClick;
   List.OnChange = OnSelect;
   List.OnCancel = OnCancel;   
   
   //목록 초기화
   List.bHasCancel = true;
   List.SelectedIndex = InitialSelectedIndex;
   List.Title = Title;
   
   //항목 값을 목록에 항목으로 추가
   foreach Items(item)
   {
      List.AddItem(item);
   }
}

/**
 * 콤보박스 그리기
 */
function RenderObject(canvas Canvas)
{      
   //열려있는 경우 아무것도 렌더링 안함 - 오너 씬에 속하는 목록이 렌더링 처리
   if(!bIsOpen)
   {
      //라벨 텍스트 업데이트
      if(bShowTitle)
      {
         Label.Caption = Title;
      }
      else
      {
         Label.Caption = GetValue();
      }
      
      //라벨 그리기
      Label.RenderObject(Canvas);
   }
}

/****************************************
* 목록 관리 함수
****************************************/

/**
 * 콤보박스의 옵션에 항목 추가
 *
 * @param item - 추가시킬 항목 값
 */
function AddItem(string Item)
{
   //값을 내부 목록 밖에 추가
   Items.Additem(Item);
   
   //값을 실제 목록 내 새 항목으로 추가
   List.Additem(Item);
}

/**
 * 값을 콤보박스 옵션에서 제거
 *
 * @param Idx - 제거할 항목 인덱스
 */
function RemoveItem(int Idx)
{
   //내부 목록에서 항목 제거
   Items.Remove(Idx, 1);
   
   //실제 목록에서 값 제거
   List.RemoveItem(Idx);
}

/**
 * 목록이 열렸/닫혔는지 토글
 */
function ToggleList()
{
   //열림? 값 토글
   bIsOpen = !bIsOpen;
   
   //오너 씬의 목록 업데이트
   if(bIsOpen)
   {   
      //우리 목록을 활성 목록으로 설정하고 보이는지 확인
      UDNMobileMenuScene(OwnerScene).List = List;
      List.bIsHidden = false;
   }
   else
   {
      //씬의 목록 참조를 비우고 목록을 보이지 않게 만듦
      UDNMobileMenuScene(OwnerScene).List = none;
      List.bIsHidden = true;
   }
}

/**
 * 콤보박스의 값 (그 목록을 통해) 반환
 */
function string GetValue()
{
   //목록이 값을 구하도록 위임
   return List.GetValue();
}

/****************************************
* 인풋 함수
****************************************/

/**
 * 항목이 선택되면 델리게이트 발동
 */
delegate OnChange(int Idx, string Item, float X, float Y);

/**
 * ITouchable 인터페이스의 OnTouch() 함수 구현
 */
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
   Super.OnTouch(Type, X, Y);
   
   //목록이 열렸으면 입력을 목록에 전달
   if(bIsOpen)
   {
      List.OnTouch(Type, X, Y);
   }
   //목록이 닫힘
   else
   {   
      //탭을 받으면 목록 열림 토글
      if(!CheckSwipe())
      {
         Label.OnTouch(Type, X, Y);
      }
   }
}

function OnClick(UDNMobileMenuObject Sender, float X, float Y)
{
   ToggleList();
}

/**
 * 항목이 선택되면 호출
 * 목록의 OnChange() 로 할당
 */
function OnSelect(int Idx, string item, float X, float Y)
{
   //목록 닫기
   ToggleList();
   
   //리스닝중인 것에 우리 OnChange 델리게이트 발동
   OnChange(Idx, Item, X, Y);
   
   //오너 씬에게 탭을 받았다고 알려줌
   OwnerScene.OnTouch(self, X, Y, false);
}

/**
 * 선택 취소, 목록 닫기
 */
function OnCancel()
{
   //선택이 취소되어, 목록을 닫음
   ToggleList();
}

defaultproperties
{   
   Begin Object class=UDNMobileMenuButton name=Label0
      Tag="Combo_LabelItem"
      bCenterText=false
      TextFont=Font'EngineFonts.SmallFont'
      CaptionColors(0)=(R=0.125,G=0.125,B=0.125,A=1.0)
      CaptionColors(1)=(R=0.125,G=0.125,B=0.125,A=1.0)
      Images(0)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
      Images(1)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
      ImagesUVs(0)=(bCustomCoords=true,U=0,V=768,UL=1024,VL=128)
      ImagesUVs(1)=(bCustomCoords=true,U=0,V=768,UL=1024,VL=128)
   End Object
   Label=Label0
   
   Begin Object class=UDNMobileMenuList name=List0
      Tag="Combo_ItemsList"
   End Object
   List=List0
}

메뉴 씬 예제 - 모두 종합하기

최종 단계는 이와 같은 새 콘트롤을 모두 사용하여 샘플 메뉴를 만드는 것입니다. 헬퍼 씬에서 입력을 받아 터치가능 콘트롤에 전달할 수 있도록 베이스 UDNMobileMenuScene 클래스에서 확장해야 할 것입니다. 이 예제는 두 개의 콤보박스와 한 개의 라벨로 구성됩니다. 어느 콤보박스 값을 변경해도 라벨을 새로운 값으로 업데이트합니다. 또한 콤보박스 중 하나에는 항상 그 "이름"이, 다른 하나에는 현재 값이 표시됩니다.

class UDNMobileMenuExample extends UDNMobileMenuScene;

/**
 * 콘트롤에서 기본 탭 처리
 */
event OnTouch(MobileMenuObject Sender,float TouchX, float TouchY, bool bCancel)
{
   //콤보박스 1 변경됨, 라벨을 그 값으로 설정
   if(Sender.Tag == "Combo1")
   {
      UDNMobileMenuLabel(FindMenuObject("Title")).Caption = UDNMobileMenuComboBox(FindMenuObject("Combo1")).GetValue();
   }
   //콤보박스 2 변경됨, 라벨을 그 값으로 설정
   else if(Sender.Tag == "Combo2")
   {
      UDNMobileMenuLabel(FindMenuObject("Title")).Caption = UDNMobileMenuComboBox(FindMenuObject("Combo2")).GetValue();
   }
}

defaultproperties
{
   //콤보박스 생성
   Begin Object class=UDNMobileMenuComboBox name=Combo0
      Tag="Combo1"
      Title="Setting One"
      Height=32
      Width=256
      Top=20
      Left=20
      Items(0)="Item 0"
      Items(1)="Item 1"
      Items(2)="Item 2"
      Items(3)="Item 3"
      Items(4)="Item 4"
      Items(5)="Item 5"
      InitialSelectedIndex=3
      bShowTitle=true
   End Object
   MenuObjects.Add(Combo0)
   
   //콤보박스 생성
   Begin Object class=UDNMobileMenuComboBox name=Combo1
      Tag="Combo2"
      Title="Setting Two"
      Height=32
      Width=256
      Top=60
      Left=20
      Items(0)="Object 0"
      Items(1)="Object 1"
      Items(2)="Object 2"
      Items(3)="Object 3"
      Items(4)="Object 4"
      Items(5)="Object 5"
      Items(6)="Object 6"
      Items(7)="Object 7"
      Items(8)="Object 8"
      Items(9)="Object 9"
      Items(10)="Object 10"
      Items(11)="Object 11"
      Items(12)="Object 12"
      Items(13)="Object 13"
      Items(14)="Object 14"
      Items(15)="Object 15"
      InitialSelectedIndex=9
   End Object
   MenuObjects.Add(Combo1)
   
   //라벨 생성
   Begin Object class=UDNMobileMenuLabel name=Label0
      Tag="Title"
      Height=32
      Width=160
      Left=296
      Top=20
      TextFont=Font'EngineFonts.SmallFont'
      CaptionColors(0)=(R=1.0,G=1.0,B=1.0,A=1.0)
      CaptionColors(1)=(R=1.0,G=1.0,B=1.0,A=1.0)
      Images(0)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
      Images(1)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
      ImagesUVs(0)=(bCustomCoords=true,U=576,V=384,UL=192,VL=192)
      ImagesUVs(1)=(bCustomCoords=true,U=576,V=384,UL=192,VL=192)
   End Object
   MenuObjects.Add(Label0)
}