UDN
Search public documentation:

MobileMenuTechnicalGuide
日本語訳
中国翻译
한국어

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 Home > Mobile Home > Mobile Menu Technical Guide
UE3 Home > User Interfaces & HUDs > Mobile Menu Technical Guide

Mobile Menu Technical Guide


Overview


The mobile menu system allows user interfaces that respond to touch input to be created for mobile devices. The system uses the idea of scenes to represent individual menus or interfaces and each scene can contain any number or combination of controls, such as buttons, images, and text labels.

mobilemenuscene.jpg

Although the mobile menu system is usually used for creating menus, such as the main menu or pause menu, these scenes are not meant solely for menus displayed when no gameplay is occurring. They can be displayed overlaid while gameplay continues and can either capture input or let it pass through depending on where and what type of input occurred. This allows the mobile menu system to augment the standard input zone controls to create custom controls on-screen as well as providing a structured way to create heads up displays.

Scenes


As mentioned previously, scenes are the containers for groups of related controls that make up an individual interface. A scene could be the main menu first displayed when the game launches, or the menu displayed when the game is paused, or a scene could be a button bar located along the bottom of the screen. Each scene has a set of bounds (Left, Top, Width, and Height) and an array of controls for which it processes input.

MobileMenuScene

The MobileMenuScene class is the base class for all scenes in the mobile menu system. It defines the behavior of a basic scene; handles input from the user, draws the controls, etc.

MobileMenuScene Properties

Display

  • SceneCaptionFont - Specifies a single font to use for drawing the captions of all buttons in the scene.
  • Opacity - The overall opacity of the scene and all of its controls.

General

  • MenuName - Specifies a unique name by which the scene can be identified.
  • MenuObjects - An array holding all the scene's control objects.
  • InputOwner - A reference to the MobilePlayerInput responsible for managing the scene.
  • bSceneDoesNotRequireInput - If TRUE, the scene will receive no touch input. Otherwise, input will be passed to the scene for handling. The default is FALSE. Setting this to TRUE essentially makes the scene a heads up display.

Position

  • Left - The horizontal location in pixels of the left edge of the scene. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the scene is initialized.
  • Top - The vertical location in pixels of the top edge of the scene. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the scene is initialized.
  • Width - The width of the scene. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the scene is initialized.
  • Height - The height of the scene. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the scene is initialized.
  • bRelativeLeft - If TRUE, the Left value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the scene is initialized.
  • bRelativeTop - If TRUE, the Top value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the scene is initialized.
  • bRelativeWidth - If TRUE, the Width value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the scene is initialized.
  • bRelativeHeight - If TRUE, the Height value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the scene is initialized.
  • bApplyGlobalScaleLeft - If TRUE, the Left value will be multiplied by the global scale factor in the X direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.
  • bApplyGlobalScaleTop - If TRUE, the Top value will be multiplied by the global scale factor in the Y direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.
  • bApplyGlobalScaleWidth - If TRUE, the Width value will be multiplied by the global scale factor in the X direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.
  • bApplyGlobalScaleHeight - If TRUE, the Height value will be multiplied by the global scale factor in the Y direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.

Sound

  • UITouchSound - References a SoundCue to play when a touch event occurs.
  • UIUnTouchSound - References a SoundCue to play when an untouch event occurs.

MobileMenuScene Functions

Display

  • GetGlobalScale[X/Y] - Returns the horizontal or vertical global scale factor depending on the device the game is running on.
  • RenderScene [Canvas] [RenderDelta] - Called by the InputOwner to render the scene. Simply calls RenderObject() on all controls belonging by the scene.
    • Canvas - References the Canvas to use to draw the scene.
    • RenderDelta - Holds the amount of time since the last render cycle.

General

  • InitMenuScene [PlayerInput] [ScreenWidth] [ScreenHeight] - Called by the engine to initialize the scene and its controls.
    • PlayerInput - References the MobilePlayerInput responsible for managing the scene.
    • ScreenWidth - Holds the width of the screen of the device the game is running on.
    • ScreenHeight - Holds the height of the screen of the device the game is running on.
  • Opened [Mode] - Called when the scene has been opened.
    • Mode - Holds the optional string passed to the OpenMenuScene() function of MobilePlayerInput.
  • MadeTopMenu - Called when the scene becomes the top scene in the stack, either by being opened or by another scene being closed.
  • Closing - Called when the scene is requested to be closed, but before the closing process occurs. Return TRUE to allow the scene to be closed. Return FALSE to override the closing and remain open.
  • Closed - Called when the scene has been closed and removed from the scene stack of the Inputowner.
  • CleanUpScene - Native. Cleans up all scene references and memory.
  • FindMenuObject [Tag] - Returns a control from the MenuObjects array matching the given Tag.
    • Tag - The Tag of the control to search for.

Input

  • OnTouch [Sender] [TouchX] [TouchY] [bCancel] - Event stub called by engine when a touch (on the Untouch event) occurs on a control owned by the scene. Subclasses should override this to provide custom functionality for control touches.
    • Sender - References the MobileMenuObject that was touched.
    • TouchX - Holds the horizontal location in pixels of the touch.
    • TouchY - Holds the vertical location in pixels of the touch.
    • bCancel - If TRUE, the touch was canceled by an outside force, such as a system event, and not from the user.
  • OnSceneTouch [EventType] [TouchX] [TouchY] - Event stub called by the engine when any touch event occurs on the device. Return TRUE if the input was handled. Otherwise, return false to pass the input on. Subclasses should override this to provide custom touch input processing not necessarily directly related to the controls of the scene. ALERT! IMPORTANT: Currently this is only called when a touch occurs outside the bounds of the scene to allow scenes to handle external touch input, but is expected to be reworked to be a general input handler as described.
    • EventType - Holds the EZoneTouchEvent type of the touch event. See the Mobile Input System page for more information on touch event types.
    • TouchX - Holds the horizontal location in pixels of the touch.
    • TouchY - Holds the vertical location in pixels of the touch.
  • MobileMenuCommand [Command] - Executes an exec or console command. Currently not implemented.
    • Command - The exec or console command to execute.

Controls


Controls are the individual components added to a scene to either display information to the user or receive touch input. However, the controls themselves do not actually handle any input themselves. The scene is responsible for all input handling by default. These are the visible elements that make up a scene, as the scene itself has no visual component.

MobileMenuObject

The MobileMenuObject class is the base class for all controls in the mobile menu system. Controls have two states, 'touched' and 'not touched', and it can have a distinct appearance and/or behavior for each of these states. The default state is 'not touched', which it remains at until touched by the user. When touched, it enters the 'touched' state until no longer being touched by the user, at which point it returns to the not 'touched' state.

Note: The use of the term "state" above does not refer to the State feature of UnrealScript. Rather, it is simply used to denote that controls can have different appearances and/or behavior based on input from the user.

MobileMenuObject Properties

Display

  • bIsHidden - If TRUE, the control will not be rendered.
  • bIsHighlighted - If TRUE, the control will be 'highlighted', e.g. a radio button that is selected.
  • Opacity - The opacity of the control.

General

  • bHasBeenInitialized - If TRUE, the control has been initialized to the current screen size.
  • Tag - Specifies a unique name by which the control can be identified.
  • OwnerScene - References the MobileMenuScene the control belongs to.

Input

  • TopLeeway - Specifies how far a touch can be outside the hitbox along the top edge of the control and still be considered over the control.
  • BottomLeeway - Specifies how far a touch can be outside the hitbox along the bottom edge of the control and still be considered over the control.
  • LeftLeeway - Specifies how far a touch can be outside the hitbox along the left edge of the control and still be considered over the control.
  • RightLeeway - Specifies how far a touch can be outside the hitbox along the right edge of the control and still be considered over the control.
  • bIsActive - If TRUE, this control is considered to be active and accepts taps.
  • bIsTouched - If TRUE, a touch event is currently occurring over the control.
  • InputOwner - References the MobilePlayerInput responsible for managing the control.

Position

  • Left - The horizontal location in pixels of the left edge of the control. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the control is initialized.
  • Top - The vertical location in pixels of the top edge of the control. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the control is initialized.
  • Width - The width of the control. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the control is initialized.
  • Height - The height of the control. Note: This can be specified as a relative location in the range [0.0, 1.0] in the default properties, but it will be converted to pixels when the control is initialized.
  • bRelativeLeft - If TRUE, the Left value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the control is initialized.
  • bRelativeTop - If TRUE, the Top value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the control is initialized.
  • bRelativeWidth - If TRUE, the Width value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the control is initialized.
  • bRelativeHeight - If TRUE, the Height value specified in the default properties will be treated as a relative value in the range [0.0, 1.0] and converted to an absolute pixel value when the control is initialized.
  • bApplyGlobalScaleLeft - If TRUE, the Left value will be multiplied by the global scale factor in the X direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.
  • bApplyGlobalScaleTop - If TRUE, the Top value will be multiplied by the global scale factor in the Y direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.
  • bApplyGlobalScaleWidth - If TRUE, the Width value will be multiplied by the global scale factor in the X direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.
  • bApplyGlobalScaleHeight - If TRUE, the Height value will be multiplied by the global scale factor in the Y direction. This allows for proper positioning when targeting multiple devices with different screen resolutions and pixel densities.
  • bHeightRelativeToWidth - If TRUE, the Height specified in the default properties when the control is created will be considered a relative value (in the range [0.0, 1.0]) to the actual Width. The actual Height of the control is then calculated by multiplying the specified Height by the actual Width..
  • XOffset - Specifies a horizontal offset relative to the control's bounds that can be used when drawing the control. By default, this value is assumed to be a percentage in the range [0.0, 1.0].
  • YOffset - Specifies a vertical offset relative to the control's bounds that can be used when drawing the control. By default, this value is assumed to be a percentage in the range [0.0, 1.0].
  • bXOffsetIsActual - If TRUE, the XOffsetValue will be assumed to be a pixel value.
  • bYOffsetIsActual - If TRUE, the YOffsetValue will be assumed to be a pixel value.

MobileMenuObject Functions

  • InitMenuObject [PlayerInput] [Scene] [ScreenWidth] [ScreenHeight] - Called by the engine to initialize the control.
    • PlayerInput - References the MobilePlayerInput responsible for managing the control.
    • Scene - References the MobileMenuScene the control belongs to.
    • ScreenWidth - Holds the width of the screen of the device the game is running on
    • ScreenHeight - Holds the height of the screen of the device the game is running on
  • RenderObject [Canvas] - Called by the scene the control belongs to each frame in order to draw the control to the screen.
    • Canvas - References the Canvas used to draw the control.

MobileMenuButton

The MobileMenuButton class is a control which can display an image and/or text which causes some action to be performed when it is touched. It is the only control which accepts input by default (bIsActive=true). When a touch occurs (technically, an 'untouch' event) over the button, the owning scene will receive a notification. When the button is in the 'untouched' state (bIsTouched=false), it displays one image, and it displays a different image when in the 'touched' state (bIsTouched=true).

control_buttons.jpg

Properties

  • Images - An array of two (2) Texture2Ds used to render the button, one for each state. The [0] element is used for the 'not touched' state, while the [1] element is used for the 'touched' state.
  • ImagesUVs - An array of two (2) UVCoords specifying the region of the Images texture to use when rendering the button, one for each state. The [0] element is used for the 'not touched' state, while the [1] element is used for the 'touched' state.
  • ImageColor - Specifies a color to modulate the button image by.
  • Caption - Specifies an optional text label to display on the button.
  • CaptionColor - Specifies the color to use for drawing the Caption text.

Functions

  • RenderCaption [Canvas] - Draws the Caption text of the button, if a caption is specified.
    • Canvas - References the Canvas used to draw the text.

MobileMenuImage

The MobileMenuImage class displays an image on the screen, in the form of a texture or portion of a texture. This control receives no input, meaning touch events on the image will not be registered and sent to the scene's OnTouch() event, making this essentially a decorative control.

Properties

  • Image - A Texture2D used specifying the texture to use when drawing the image.
  • ImageDrawStyle - Specifies the MenuImageDrawStyle to use to draw the image.
    • IDS_Normal - Draws the region of the texture specified un-scaled and clipped by the image bounds.
    • IDS_Stretched - Draws the region of the texture specified scaled to fill the image bounds.
    • IDS_Tile - Keeps the top left corner of the region to draw, but modifies the width and height of the region to match the bounds of the image. This will clip the image if the region specified is larger than the image bounds or tile the image if the region is smaller than the image bounds (assuming the region is the full texture). Note: When using an atlas texture, this option should probably be avoided as it will cause unwanted behavior.
  • ImageUVs - A UVCoords specifying the region of the Image texture to use when drawing the image.
  • ImageColor - Specifies a color to modulate the Image texture by when drawing the image.

MobileMenuLabel

The MobileMenuLabel class displays a string of text on the screen. This is useful for presenting custom or dynamic data or text to the user. This control receives no input, meaning touch events will not be registered and sent to the scene's OnTouch() event.

Properties

  • Caption - Specifies the string of text the label will display on the screen.
  • TextFont - Specifies the font to use to draw the text.
  • TextColor - Specifies the color to use to draw the text when the label is not being touched.
  • TouchedColor - Specifies the color to use to draw the text when the label is being touched.
  • TextXScale - Specifies the horizontal scaling factor to use to draw the text.
  • TextYScale - Specifies the vertical scaling factor to use to draw the text.
  • bAutoSize - If TRUE, the Width and Height of the label will be adjusted to fit the size of the rendered text each draw cycle. Otherwise, the Width and Height of the label are not altered.

Custom Controls

While there are only a few basic built-in controls (the button, image, and label described above), these can be combined to create practically any type of control you can think of with a little ingenuity. For example, you could have a group of buttons each with an associated label that when any one button in the group is touched, that button toggles to a 'selected' image and all other buttons in the group toggle to an 'unselected' image. This would give the impression of a group of radio buttons.

This method of combining controls within a scene to give the appearance of more complex controls requires a lot of custom logic to be embedded in the scene. It should be apparent that this may not be ideal if you wish to reuse such a complex control. Given that controls do not directly handle or receive input, though, creating more complex types of reusable controls requires additional modifications to the system. For instance, it may be necessary to create a subclass of the MobileMenuScene class which handles generic input and then passes that on to each of its controls for them to handle. This makes it possible for custom controls to have complex behaviors that react directly to touch input, such as a list that scrolls based on swipes (see the Custom List Control section).

Working with Menu Scenes


Using the mobile menu system is a straightforward process. Controls are added to new custom menus, which can then be opened or closed at any point prior to the input system being initialized. This is a requirement as the MobilePlayerInput class is responsible for managing the menu system. Touch input handling is performed inside of the menu itself and is flexible enough to allow for practically any scenario.

Creating Controls

Adding controls to scenes is generally done by creating sub-objects in the defaultproperties block of the scene class. Each control is created as a sub-object and then added to the MenuObjects array of the scene. The order in which the controls are created inside the defaultproperties block is not necessarily important; however, the order in which they are added to the MenuObjects array is very important as it determines the order in which the controls are rendered and receive input.

A typical subobject block creating a new control and adding it to the MenuObjects array looks like the following:

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

Notice in this example, the button is explicitly added as the first item (element 0) in the MenuObjects array. Explicitly specifying the order this way allows you to create the controls in an order you want within the defaultproperties block.

The same button could also simply be pushed onto the end of the MenuObjects array like this:

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)

Using this method, it is very important that the controls are created and added to the MenuObjects array in the desired order.

whichever method you choose to employ, the syntax for the sub-object block remains the same. Each block begins with a:

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

This signifies a new subjobject of type ControlClass is to be created with the name ControlName. This is followed by setting any desired values for the properties of the control, which are usually indented for good form and easy readability. Finally, after all the property values have been set, the block is closed with:

End Object

At this point, the subobject can be referenced in the defaultproperties block by its name, as seen when it is assigned to the MenuObjects array.

A simple scene with a background image and a single button, though with no real functionality, would look like the following:

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)
}

This would result in the following menu:

addcontrols.jpg

Managing Menus

The mobile menu system is managed by the MobilePlayerInput class. It contains the functionality for opening and closing scenes as well as telling them to render and passing on input from the user to the scene and its controls.

Scene Stack

Multiple scenes can be open at any time. All open scenes are held in a stack (an array) in the MobilePlayerInput. The last scene opened is always on the top of the stack. Input is filtered through the scene stack from top to bottom. If a scene on the top of the stack handles the input, the scenes lower in the stack will not have access to that input. Scenes in the stack are rendered in from bottom to top so the top scene will render over all other scenes.

Opening Menu Scenes

The MobilePlayerInput class contains several functions that can be used to open menu scenes.

  • OpenMenuScene [SceneClass] [Mode] - Opens a new menu scene of the given class. Returns a reference to the opened scene.
    • SceneClass - Specifies the class of menu scene to open. Must be a subclass of MobileMenuScene.
    • Mode - Optional. Specifies a string to be passed to the scene's Opened() function.
  • OpenMobileMenu [MenuClassName] - Opens a menu scene given a class in the form of a string.
    • MenuClassName - Specifies the name of the class of menu scene to open in the form of a string.
  • OpenMobileMenuMode [MenuClassName] [Mode] - Opens a menu scene given a class in the form of a string, with an optional mode.
    • MenuClassName - Specifies the name of the class of menu scene to open in the form of a string.
    • Mode - Optional. Specifies a string to be passed to the scene's Opened() function.

Closing Menu Scenes

The MobilePlayerInput class contains two functions that can be used to close menu scenes.

  • CloseMenuScene [SceneToClose] - Closes the specified menu scene.
    • SceneToClose - References the scene to be close.
  • CloseAllMenus - Closes all menu scenes in the scene stack.

Rendering Menu Scenes

The MobilePlayerInput class also handles telling each scene in the scene stack to render each frame.

  • RenderMenus [Canvas Canvas] [RenderDelta] - Called by the engine each frame to render all the menus in the scene stack.
    • Canvas - References the Canvas to use to draw the scene.
    • RenderDelta - Holds the amount of time since the last render cycle.

Touch Input

Scenes receive touch input notifications from the engine when the user touches the screen within the bounds of the scene. The scene can use these notifications to interpret the touch input into actions. There are two main methods in which scenes get input notifications.

Control Touches

When an active control is touched, the scene receives a notification from the control (via the OnTouch() event) allowing the scene to process the result of the touch any way it sees fit. This event is only called when the user "untouches" the control, similar to the release of a button as opposed to the pressing of the button.

  • OnTouch [Sender] [TouchX] [TouchY] [bCancel] - Event stub called by engine when a touch (on the Untouch event) occurs on a control owned by the scene. Subclasses should override this to provide custom functionality for control touches.
    • Sender - References the MobileMenuObject that was touched.
    • TouchX - Holds the horizontal location in pixels of the touch.
    • TouchY - Holds the vertical location in pixels of the touch.
    • bCancel - If TRUE, the touch was canceled by an outside force, such as a system event, and not from the user.

Basic Button Example

To create a basic example of getting input from a button and using it to perform some action, take the 'Creating controls' example from above.

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)
}

In order to make the button do something when touched, the OnTouch() event needs to be overridden, adding functionality to handle the button press. First, add the function signature (which can be copied from the MobileMenuScene class):

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

Next, a few edge cases should be handled. In case the Sender is empty or the bCancel parameter is set, the function should just return without doing anything.

if(Sender == none)
{
   return;
}

if(bCancel)
{
   return;
}

Finally, the case of the button being pressed should be handled. in this example, the button will simply cause the menu to close itself.

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

First, the Tag of the Sender is checked (a case insensitive check is used via the ~= operator) to make sure it is the same as the Tag specified for the button in the defaultproperties. If the check succeeds and the button was the touched control, the CloseMenuScene() function is called on the Inputowner, which is the MobilePlayerInput for the local player, passing it a reference to the menu itself, self.

The full OnTouch() function integrated into the scene class would be:

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)
}

The menu should now behave like so:

Menu Open

basicinput_0.jpg

Button Touched

basicinput_1.jpg

Menu Closed

basicinput_2.jpg

Custom Touch Input

OnSceneTouch

Scenes get notifications of all touch input by way of the OnSceneTouch() event. This allows the scene to not only process touch events from specific controls, but to also process generic input for performing custom types of input, such as swipes or other gestures.

  • OnSceneTouch [EventType] [TouchX] [TouchY] - Event stub called by the engine when any touch event occurs within the bounds of the scene. Subclasses should override this to provide custom touch input processing not necessarily directly related to the controls of the scene.
    • EventType - Holds the EZoneTouchEvent type of the touch event. See the Mobile Input System page for more information on touch event types.
    • TouchX - Holds the horizontal location in pixels of the touch.
    • TouchY - Holds the vertical location in pixels of the touch.

Custom Input Menu Example


Making a menu and controls that go beyond the basic buttons, images, and labels can be done with a little creativity. This example will demonstrate creating custom controls that handle input themselves, instead of leaving all input handling up to the scene, including a scrolling list as well as a combobox and a new button and label. These controls will all extend from a new base control class that implements a custom interface which allows them to easily be passed input from the scene, which will itself be a new custom scene class.

To see the example menu demonstrated in action and to get of taste of what is possible using this example, view this video:

(Click to view - Right-click > Save As to download)

examplemenu_1.jpg

Basic Touchable Controls

The custom touchable controls rely on the scene to pass all input events on to its controls. In turn, each control either handles the input itself, passes it on to any children controls, or a combination of the two.

Before we jump into implementing the new controls, you may be wondering, "Why would I want the controls to handle input?" This is a good question, and it really comes down to the kind of functionality you need from your menu scenes and controls. In this case, we will be creating complex controls that are essentially containers for other sub-controls. In such a case, it is very beneficial for the controls to be able to determine whether they have been touched and react to the input accordingly. A crucial factor is the ability for a button to notify its parent when it has been clicked and allow the parent to perform a specific action based on that. If this were not possible, all of the logic for doing complex interactions between controls would have to be located in the scene itself, which would drastically limit the usefulness of creating complex controls like lists. You would essentially be re-implementing the entire list logic every time you wanted to use a list.

ITouchable Interface

The first step to creating the new touchable controls involves creating the interface that will be implemented by any custom control which wants to be given input. This interface is extremely simple, declaring a single function.

Interface ITouchable;

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

The OnTouch function here mimics the OnSceneTouch() function from the MobileMenuScene class, allowing that function to simply pass on the touch input data to any control which implements this ITouchable interface.

Scene Classes

We have to pull a little trickery off to get full access to generic touch events. A new base scene class with no width or height is created to handle passing the input on to the actual scene through a new delegate. The actual scene class opens a new base scene as a helper scene when it is opened and assigns its OnSceneTouch() function to the base scene's delegate. It then takes the input from the OnSceneTouch() function and passes it on to any touchable controls. All scenes which use touchable controls will then extend from this class and automatically have the functionality for handling those controls ready to go.

Input Handling

The general idea behind passing the input is the scene class will receive notification of all touch input events through its OnSceneTouch() function. Inside this function, the list of all controls belonging to the scene, the MenuObjects array, will be iterated casting each item in the array to the ITouchable interface. If the cast succeeds, the control is touchable and the input is passed to that control using the OnTouch() function inherited from the interface. In addition, some basic swipe detection is performed here so that any control can quickly query the owner scene to see if the input is a swipe or a tap, without being required to re-implement this functionality in each control where it is needed.

List Handling

The base scene class also references a new control class, the UDNMobileMenuList. This class does not exist yet, but once created it will be used to display scrolling lists of items. Any visible list will always be fullscreen and needs to be overlaid on top of any other controls within the scene. Because the controls are all rendered in order and there is no way to know which controls might want to display lists at some point, the only way to guarantee this is to let the scene handle the rendering of any list and have any control using a list assign their list to the scene when necessary.

Helper Scene Class

class UDNMobileMenuBase extends MobileMenuScene;


/**
 * Delegate fired when generic touch occurs (outside the scene)
 */
delegate bool OnInputTouch(EZoneTouchEvent EventType, float TouchX, float TouchY);

/**
 * Called when touch input occurs
 */
function bool OnSceneTouch(EZoneTouchEvent EventType, float TouchX, float TouchY)
{
   OnInputTouch(EventType, TouchX, TouchY);
   
   return true;   
}

defaultproperties
{
}

Real Scene Class

class UDNMobileMenuScene extends MobileMenuScene;

/** Cached location of the initial touch event */
var Vector StartTouchLocation;

/** Current location of the last touch update event */
var Vector CurrentTouchLocation;

/** TRUE if the current touch is a swipe */
var bool bSwipe;

/** Minimum distance a touch must move to be a swipe. Less than this, it is a tap. */
var float SwipeTolerance;

/** Used to allow combo boxes to display their lists overlaid over all controls */
var instanced UDNMobileMenuList List;

var UDNMobileMenuBase HelperScene;


function Opened(string Mode)
{
   Super.Opened(Mode);
   
   //open new base scene - has no width or height so can get generic touch events
   HelperScene = UDNMobileMenuBase(InputOwner.OpenMenuScene(class'UDNMobileMenuBase'));
   
   //Assign our handler to the input delegate of the base scene
   HelperScene.OnInputTouch = OnSceneTouch;
}

function Closing()
{
   //Cleanup - close the helper scene before closing this scene
   if(HelperScene != none)
   {
      InputOwner.CloseMenuScene(HelperScene);
   }

   Super.Closing();
}

function bool OnSceneTouch(EZoneTouchEvent EventType, float TouchX, float TouchY)
{
   local MobileMenuObject Touchable;
   
   //initial touch event
   if(EventType == ZoneEvent_Touch)
   {
      //Clear swipe
      bSwipe = false;
      
      //Cache touch location
      StartTouchLocation.X = TouchX;
      StartTouchLocation.Y = TouchY;
   }
   //touch update event
   else if(EventType == ZoneEvent_Update)
   {      
      //Save current touch location
      CurrentTouchLocation.X = TouchX;
      CurrentTouchLocation.Y = TouchY;
      
      //See if this touch is a swipe
      CheckSwipe();
   }

   //if our list isn't displayed, pass input to touchable controls
   if(List == none || List.bIsHidden)
   {
      foreach MenuObjects(Touchable)
      {   
         if(ITouchable(Touchable) != none)
         {
            ITouchable(Touchable).OnTouch(EventType, TouchX, TouchY);
         }
      }   
   }
   //if the list is visible, pass touch input to it
   else if(List != none && !List.bIsHidden)
   {
      ITouchable(List).OnTouch(EventType, TouchX, TouchY);
   }
   
   //return true to acknowledge we handled the input
   return true;
}

function CheckSwipe()
{
   //check if the touch moved enough to be a swipe
   if(VSize(StartTouchLocation - CurrentTouchLocation) > SwipeTolerance)
   {
      bSwipe = true;
   }   
}

function RenderScene(Canvas Canvas,float RenderDelta)
{
   //Draw the list if it is visible
   if(List != none && !List.bIsHidden)
   {
      List.RenderObject(Canvas);
   }
   //Draw our controls if no list
   else
   {
      Super.RenderScene(canvas, RenderDelta);
   }
}

defaultproperties
{
   SwipeTolerance = 5.0
}

Base Touchable Control Class

The base touchable control class extends the base MobileMenuObject control class; implementing the new ITouchable interface and adding a couple helper functions for checking if the location of a touch is within its bounds and finding out if the current touch is a swipe from the owner scene. It also must define the OnTouch() function declared in the ITouchable interface. The base implementation of this function simply checks the input to see whether the control is being touch and sets the bIsTouched and bIsHighlighted variables accordingly.

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;
}

Custom Label

The touchable label control is a modified version of the standard MobileMenuButton and MobileMenuLabel classes. The code for those classes was merged into the new class and then modified to add some additional rendering functionality, such as simple box and border background visual components and the ability to align the caption text.

class UDNMobileMenuLabel extends UDNMobileMenuObject;
   

/** The 2 images that make up the label. [0] = the untouched, [1] = touched */
var Texture2D Images[2];

/** The UV Coordinates for the images. [0] = the untouched, [1] = touched */
var UVCoords ImagesUVs[2];

/** Holds the color override for the image */
var LinearColor ImageColors[2];

/** Localizable caption for the label */
var string Caption;

/** Holds the color for the caption */
var LinearColor CaptionColors[2];

/** Holds the font that will be used to draw the text */
var font TextFont;

/** If TRUE, center the text in the label. Otherwise, left align it. */
var bool bCenterText;

/** If TRUE, the label text will not wrap */
var bool bClipText;

/** Number of pixels to pad text when not centering text */
var float TextPadding;

/** Colors to use to draw the background of the label. Used if no texture is specified */
var LinearColor BackgroundColors[2];

/** Colors to use to draw the borders of the label. Used if no texture is specified. */
var LinearColor BorderColors[2];

/** Widths of the four borders. (Order: Top, Right, Bottom, Left) */
var float BorderWidth[4];

/** If TRUE, use DrawTileStretched() to draw the label image. Otherwise, use DrawTile(). */
var bool bStretchBackground;


/**
 * Initialize label - Setup image coords
 */
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{
   local int i;
   
   Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);

   //No custom coords, set to full size of image
   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;
      }
   }
}


/**
 * Render the widget
 */
function RenderObject(canvas Canvas)
{
   local int Idx;
   local LinearColor DrawColor;

   //Get image/color index based on if the label is being touched or highlighted
   Idx = (bIsTouched || bIsHighlighted) ? 1 : 0;
   
   //If there is an image set, draw it
   if(Images[Idx] != none)
   {   
      //Set up Canvas for drawing the background image
      Canvas.SetPos(OwnerScene.Left + Left - Canvas.OrgX, OwnerScene.Top + Top - Canvas.OrgY);
      Drawcolor = ImageColors[Idx];
      Drawcolor.A *= Opacity * OwnerScene.Opacity;
      
      //Draw background image stretched
      if(bStretchBackground)
      {
         Canvas.DrawTileStretched(Images[Idx], Width, Height,ImagesUVs[Idx].U, ImagesUVs[Idx].V, ImagesUVs[Idx].UL, ImagesUVs[Idx].VL, DrawColor, true, true);
      }
      //Draw background image scaled
      else
      {
         Canvas.DrawTile(Images[Idx], Width, Height,ImagesUVs[Idx].U, ImagesUVs[Idx].V, ImagesUVs[Idx].UL, ImagesUVs[Idx].VL, DrawColor, true);
      }
   }
   //No image set, draw simple rect background
   else
   {
      //Set up Canvas for drawing the background rect
      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);
      
      //Draw border
      DrawBorder(Canvas);
   }

   //Draw caption text
   RenderCaption(Canvas);
}

/**
 * Draw the label's border
 *
 * @param Canvas - Canvas object used for drawing
 */
function DrawBorder(Canvas Canvas)
{
   local int Idx;
   
   Idx = (bIsTouched || bIsHighlighted) ? 1 : 0;
   
   //Draw top border
   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]);
   }
   
   //Draw right border
   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);
   }
   
   //Draw bottom border
   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]);
   }
   
   //Draw left border
   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);
   }
}

/**
 * Draw the label's text
 *
 * @param Canvas - Canvas object used for drawing
 */
function RenderCaption(canvas Canvas)
{
   local float X,Y,UL,VL;
   local FontRenderInfo FRI;
   local int Idx;
   
   Idx = (bIsTouched || bIsHighlighted) ? 1 : 0;

   //Only draw if some text is set
   if (Caption != "")
   {
      Canvas.Font = TextFont;
      Canvas.TextSize(Caption,UL,VL);

      //Center text if necessary, left-align otherwise
      if(bCenterText)
      {
         X = Left + (Width / 2) - (UL/2);
         Y = Top + (Height /2) - (VL/2);
      }
      else
      {
         X = Left + 5;
         Y = Top + (Height /2) - (VL/2);
      }

      //Set up Canvas for drawing caption
      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);

      //Draw text - clip if desired
      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
}

Touchable Button

The touchable button control is a simple extension of the UDNMobileMenuLabel class which overrides the OnTouch() function to add tap detection and call a new OnClick delegate to alert any listeners that the button has been "clicked".

class UDNMobileMenuButton extends UDNMobileMenuLabel;
   

/** 
 * Delegate called when the button receives a tap 
 */
delegate OnClick(UDNMobileMenuObject Sender, float X, float Y);

/**
 * Implementation of the OnTouch() member of the ITouchable interface.
 * Called when the parent receives a touch event.
 *
 * @param Type - type of touch event
 * @param X - Horizontal location of touch event
 * @param Y - Vertical location of touch event
 */
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
   Super.OnTouch(Type, X, Y);
   
   //touch ending within the button bounds, fire OnClick delegate to alert listeners of a tap
   if(Type == ZoneEvent_Untouch && CheckBounds(X, Y))
   {
      OnClick(self, X, Y);
   }
}

defaultproperties
{
   bIsActive=true
   bCenterText=true
}

Scrolling List

The scrolling list class attempts to create a new control which behaves similar to the standard list controls found in iOS interfaces. It is a list of items, each one a UDNMobileMenuButton, which can be scrolled up or down using touch input. Swiping will cause the list to continue to move for a bit after the touch ends exhibiting an "inertial" scrolling behavior. These lists must always be fullscreen because of the limitations of clipping text, i.e. it can only be clipped by the extents of the viewport.

class UDNMobileMenuList extends UDNMobileMenuObject;
   
/** text to display as the title of the list. */
var string Title;   

/** Color to use for drawing the title text */
var Color CaptionColor;

/** If TRUE, center the caption text. otherwise, left align it */
var bool bCenterText;
   
/** Color to use for filling the list background */
var Color BackgroundColor;

/** Color to use for the list border */
var Color BorderColor;

/** Colors to use for drawing the background of the list items. [0] is for non-selected items. [1] is for the selected item. */
var LinearColor ItemBackgroundColors[2];

/** Colors to use for drawing the border of the list items. [0] is for non-selected items. [1] is for the selected item. */
var LinearColor ItemBorderColors[2];

/** Colors to use for drawing the text of list items. [0] is for non-selected items. [1] is for the selected item. */
var LinearColor ItemCaptionColors[2];

/** Font to use for drawing list item text. */
var Font ItemFont;

/** Reference to the Cancel button used to close the list */
var instanced UDNMobileMenuButton Cancel;

/** If TRUE, the list be be cancelable (i.e., the Cancel button will be displayed) */
var bool bHasCancel;

/** The list of items belonging to the menu */
var instanced array<UDNMobileMenuButton> Items;

/** Index of the currently selected item */
var int SelectedIndex;

/** Height of each item cell in the list */
var float ItemHeight;

/** Height of the title bar */
var float TitleBarHeight;

/** Color of the title bar */
var Color TitleBarColor;

/** TRUE when the list is being controlled, i.e. when the user is actively scrolling the list. */
var bool bActive;

/** Holds the cached location of the last touch event for the list */
var vector LastTouchLocation;

/** The amount to scroll the list while active. (This is the delta between touch updates. It keeps the list in sync with the user's finger.) */
var float ScrollAmount;

/** The amount to scroll the list when inactive. (This is how much stored velocity the list has from a swipe.) */
var float ScrollInertia;

/** Holds the cached time of the last render update. */
var float LastRenderTime;

/** Multiplier for the touch movement used to control the ScrollInertia. */
var float SwipeFactor;

/** Multiplier for the speed at which the list snaps back when scrolled past its bounds. */
var float SnapFactor;

/** Distance past the list's bounds it can be actively scrolled. */
var float ScrollLimit;


/****************************************
* Init and Rendering Functions
****************************************/

/**
 * Initialize the list
 */
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{   
   Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //Force list to the size of the device's screen since text can't be easily clipped by the list's bounds 
   Left = 0;
   Top = 0;
   Width = ScreenWidth;
   Height = ScreenHeight;
   
   //initialize the Cancel button (the menu system has no built-in handling for controls within controls)
   Cancel.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //Position the Cancel button vertically centered on the right of the title bar
   Cancel.Top = Top + ((TitleBarHeight - Cancel.Height) / 2);
   Cancel.Left = Left + Width - Cancel.Width - 1;
   
   //Grab click events from the button
   Cancel.OnClick = HandleCancel;
}

/**
 * Draw the list
 */
function RenderObject(canvas Canvas)
{
   local int i;
   local float DeltaTime;
   local float ScrollDelta;
   
   Canvas.SetPos(Left, Top);
   
   //Draw the list's background
   Canvas.DrawColor = BackgroundColor;
   Canvas.DrawRect(Width, Height);
   
   //Calculate the render delta to use for passively scrolling the list
   if(LastRenderTime != 0)
   {
      DeltaTime = InputOwner.Outer.WorldInfo.RealTimeSeconds - LastRenderTime;
   }
   lastRenderTime = InputOwner.Outer.WorldInfo.RealTimeSeconds;   
   
   if(Items.Length > 0)
   {
      //Calculate scroll delta for this render pass and snap the list back if outside the list bounds
      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;
      }
      
      //render each list item
      for(i=0; i < Items.Length; i++)
      {
         //force item size here in case orientation changes
         Items[i].Left = left;
         Items[i].Top += bActive ? ScrollAmount : ScrollDelta;
         Items[i].Width = Width;
         Items[i].Height = ItemHeight;
         
         //Only render item if part of it is visible
         if(Items[i].Top + ItemHeight > Top && Items[i].Top < Top + Height)
         {
            Items[i].RenderObject(Canvas);
         }
      }
   }
   
   if(bActive)
   {
      //Zero out active scroll if active
      ScrollAmount = 0;
   }
   else
   {
      //decrement scroll inertia if passive
      ScrollInertia -= ScrollDelta;
   }
   
   //Draw title bar - we draw after the list items so they are clipped by it
   Canvas.SetPos(Left, Top);   
   Canvas.DrawColor = TitleBarColor;
   Canvas.DrawRect(Width, TitleBarHeight);
   
   //Draw title
   RenderTitle(Canvas);
   
   //Draw list border
   Canvas.SetPos(Left, Top + TitleBarHeight);   
   Canvas.DrawColor = BorderColor;
   Canvas.DrawBox(Width, Height - TitleBarHeight);
   
   //Drawn Cancel button if desired
   if(bHasCancel)
   {
      Cancel.RenderObject(Canvas);
   }
}

/**
 * Draw the list's title text
 */
function RenderTitle(canvas Canvas)
{
   local float X,Y,UL,VL;
   local FontRenderInfo FRI;

   //don't bother if there is no title
   if (Title != "")
   {
      //set font (we're just borrowing the scene font. Could add custom font)
      Canvas.Font = OwnerScene.SceneCaptionFont;
      Canvas.TextSize(Title,UL,VL);

      //Calculate position - Centered or Left-aligned
      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);
      }

      //Draw title text clipped (Could add ability to shorten if it won't fit, i.e. use ...)
      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);
   }
}

/****************************************
* Item Management Functions
****************************************/

/**
 * Add a new item to the list
 *
 * @param item - the value of the new item to add
 */
function AddItem(string item)
{
   local UDNMobileMenuButton NewItem;
   local Vector2D ViewportSize;
   
   //Create a new item (a button)
   NewItem = new(Outer) class'UDNMobileMenuButton';
   
   if(NewItem != none)
   {
      //initialize the new item
      LocalPlayer(InputOwner.Outer.Player).ViewportClient.GetViewportSize(ViewportSize);
      NewItem.InitMenuObject(InputOwner, OwnerScene, ViewportSize.X, ViewportSize.Y);
      
      //Set new item properties
      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 this is to be the selected item, highlight it
      if(Items.Length == SelectedIndex)
      {
         NewItem.bIsHighlighted = true;
      }
      
      //Add the item to the list
      Items.AddItem(NewItem);
   }
}

/**
 * Remove a value from the list
 * 
 * @param Idx - the index of the item to remove
 */
function RemoveItem(int Idx)
{
   local int i;
   
   //remove the item from the list
   Items.Remove(Idx, 1);
   
   //shift subsequent items up
   for(i=Idx; i < Items.Length; i++)
   {
      Items[i].Top -= ItemHeight;
   }
   
   //If we removed the selected item, reset selection to first item
   if(SelectedIndex == Idx)
   {
      SelectedIndex = 0;
      if(Items.Length > 0)
      {
         Items[0].bIsHighlighted = true;
      }
   }
}

/**
 * Get the value of the list.
 *
 * Returns the caption of the selected item
 */
function string GetValue()
{
   //must have items to access selected index
   if(Items.Length > 0)
   {
      //return the caption (value) of the selected item
      return Items[SelectedIndex].Caption;
   }
   else
   {
      //no items - return empty string
      return "";
   }
}

/****************************************
* Input Functions
****************************************/
   
/**
 * Delegate fired when an item is selected in the list
 */
delegate OnChange(int Idx, string Item, float X, float Y);

/**
 * Delegate fired when the list is canceled
 */
delegate OnCancel();

/**
 * Implementation of the ITouchable interface
 */
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
   local UDNMobileMenuButton Label;
   local float ActualY;
   
   Super.OnTouch(Type, X, Y);
   
   //Initial touch event
   if(Type == ZoneEvent_Touch)
   {
      //Ignore if touch is outside list or in title bar
      if(CheckBounds(X, Y) && Y > Top + TitleBarHeight)
      {
         //set list active - user is controlling it
         bActive = true;
         
         //Cache touch location
         LastTouchLocation.X = X;
         LastTouchLocation.Y = y;
         
         //reset scroll values
         ScrollAmount = 0;
         ScrollInertia = 0;
      }
   }
   //Touch in progress
   else if(Type == ZoneEvent_Update || Type == ZoneEvent_Stationary)
   {
      //Ignore if the list is not active - touch must have started outside the list
      if(bActive)
      {
         /************************************************************
         *   We only update scrolling if:
         *
         *    * There are enough items to require scrolling
         *    * The list is not scrolled outside the bounds already
         *    
         ************************************************************/
         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)))
         {
            //Actual inertia is scaled by device scaling and custom swipe factor to get used inertia
            ActualY = (Y - LastTouchLocation.Y) * class'MobileMenuScene'.static.GetGlobalScaleY() * SwipeFactor;
            
            //Reset scroll inertia if the movement is negligible. Otherwise, increment it with new delta.
            if(Abs(ActualY) < UDNMobileMenuScene(OwnerScene).SwipeTolerance)
            {
               ScrollInertia = 0;
            }
            else
            {
               ScrollInertia += ActualY;
            }
            
            //Calculate movement delta for active scrolling and increment
            ActualY = (Y - LastTouchLocation.Y);
            ScrollAmount += ActualY;
         }
         
         //Update cached touch location
         LastTouchLocation.X = X;
         LastTouchLocation.Y = y;
      }
   }
   //touch ending because user lifted finger
   else if(Type == ZoneEvent_Untouch)
   {
      //not a swipe and within the list
      if(!CheckSwipe() && CheckBounds(X, Y))
      {   
         //not in title bar - must be a tap on an item
         if(Y > Top + TitleBarHeight)
         {
            //pass tap on to all items
            foreach Items(Label)
            {
               Label.OnTouch(Type, X, Y);
            }
         }
         //tap on title bar
         else
         {
            //if list can be canceled, let the Cancel button handle the tap
            if(bHasCancel)
            {
               Cancel.OnTouch(Type, X, Y);
            }
         }
      }
      
      //update scroll inertia one last time before going passive
      if(bActive)
      {
         ActualY = (Y - LastTouchLocation.Y) * class'MobileMenuScene'.static.GetGlobalScaleY() * SwipeFactor;
         ScrollInertia += ActualY;
         
         //set list passive
         bActive = false;
      }
   } 
   //touch ending prematurely
   else if(Type == ZoneEvent_Cancelled)
   {      
      //set passive
      bActive = false;
      
      //zero out all scroll values
      ScrollAmount = 0;
      ScrollInertia = 0;
   }
}

/**
 * Selects a new item in the list. 
 * Assigned to the OnClick() of each item in the list.
 */
function OnSelect(UDNMobileMenuObject Sender, float X, float Y)
{
   local UDNMobileMenuButton Label;
   local int i;
   
   i = 0;
   
   if(UDNMobileMenuButton(Sender) != none)
   {
      //iterate items to find item matching sender
      foreach Items(Label)
      {
         //found the matching item
         if(Label == UDNMobileMenuButton(Sender))
         {
            //Set new selected index
            SelectedIndex = i;
            
            //Highlight the selected item
            Sender.bIsHighlighted = true;
            
            //Call OnChange delegate to alert anyone listening
            OnChange(i, Label.Caption, X, Y);
            
            //break out of the iterator since we're done here
            break;
         }
         
         i++;
      }
   }
   
   //set passive
   bActive = false;
   
   //zero out all scroll values
   ScrollAmount = 0;
   ScrollInertia = 0;
}

/**
 * Cancels the list. 
 * Assigned to the OnClick() of the Cancel button.
 */
function HandleCancel(UDNMobileMenuObject Sender, float X, float Y)
{
   //Call the OnCancel delegate to alert anyone listening
   OnCancel();
   
   //set passive
   bActive = false;
   
   //zero out all scroll values
   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
}

ComboBox

The combobox control provides a simple way to have settings in a menu whose value can be selected from a list of options. The main visual component of a combobox is a touchable button that toggles open a scrolling list control when tapped. The list displays all the options for the combobox and when one is selected, the list closes, setting the value of the combobox to the new selected option.

Note: The options for the combobox currently require hard coding when the combobox is created. This can easily be changed to work as a per-object-config property or some other method of populating the list of available options.

class UDNMobileMenuComboBox extends UDNMobileMenuObject;


/** The button that visually represents the combo box */
var instanced UDNMobileMenuButton Label;

/** The list used to select new values for the combo box */
var instanced UDNMobileMenulist list;

/** The title of the combo box - used to set the title of the list as well */
var string Title;

/** If TRUE, show the title. Otherwise show the current value. */
var bool bShowTitle;

/** The list of values used to populate the list */
var array<string> Items;

/** The index of the value to initialize the combo box to */
var int InitialSelectedIndex;

/** TRUE when the list is open for selecting a new value */
var bool bIsOpen;


/****************************************
* Init and Render Functions
****************************************/

/**
 * Initializes the combo box
 */
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{
   local string item;
   
   Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //initialize label and list
   Label.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   List.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
   
   //Set position and size of label
   Label.Left = Left;
   Label.Width = Width;
   Label.Top = Top;
   Label.height = height;
   
   //initialize label caption
   Label.Caption = Items[InitialSelectedIndex];
   
   //Set up delegates
   Label.OnClick = OnClick;
   List.OnChange = OnSelect;
   List.OnCancel = OnCancel;   
   
   //initialize list
   List.bHasCancel = true;
   List.SelectedIndex = InitialSelectedIndex;
   List.Title = Title;
   
   //Add item values as items to the list
   foreach Items(item)
   {
      List.AddItem(item);
   }
}

/**
 * Draw the combo box
 */
function RenderObject(canvas Canvas)
{      
   //don't render anything if open - the list belonging to the owner scene handles rendering
   if(!bIsOpen)
   {
      //Update label text
      if(bShowTitle)
      {
         Label.Caption = Title;
      }
      else
      {
         Label.Caption = GetValue();
      }
      
      //Draw the label
      Label.RenderObject(Canvas);
   }
}

/****************************************
* List Management Functions
****************************************/

/**
 * Add an item to the combo box's options
 *
 * @param item - the value of the item to add
 */
function AddItem(string Item)
{
   //Add the value to out internal list
   Items.Additem(Item);
   
   //Add the value as a new item in the actual list
   List.Additem(Item);
}

/**
 * Removes an value from the combo box's options
 *
 * @param Idx - index of the item to remove
 */
function RemoveItem(int Idx)
{
   //remove the item from our internal list
   Items.Remove(Idx, 1);
   
   //Remove the value from the actual list
   List.RemoveItem(Idx);
}

/**
 * Toggle whether the list is open or closed
 */
function ToggleList()
{
   //toggle open value
   bIsOpen = !bIsOpen;
   
   //update the owner scene's list
   if(bIsOpen)
   {   
      //set our list as the active list and make sure it is visible
      UDNMobileMenuScene(OwnerScene).List = List;
      List.bIsHidden = false;
   }
   else
   {
      //Null out the scene's list reference and make our list invisible
      UDNMobileMenuScene(OwnerScene).List = none;
      List.bIsHidden = true;
   }
}

/**
 * Returns the value of the combo box (via it's list)
 */
function string GetValue()
{
   //let list handle getting the value
   return List.GetValue();
}

/****************************************
* Input Functions
****************************************/

/**
 * Delegate fired when an item is selected
 */
delegate OnChange(int Idx, string Item, float X, float Y);

/**
 * Implementation of the OnTouch() of the ITouchable interface
 */
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
   Super.OnTouch(Type, X, Y);
   
   //the list is open, pass input to the list
   if(bIsOpen)
   {
      List.OnTouch(Type, X, Y);
   }
   //the list is closed
   else
   {   
      //we got a tap, toggle the list open
      if(!CheckSwipe())
      {
         Label.OnTouch(Type, X, Y);
      }
   }
}

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

/**
 * Called when an item is selected
 * Assigned to the OnChange() of the list
 */
function OnSelect(int Idx, string item, float X, float Y)
{
   //close the list
   ToggleList();
   
   //fire off our OnChange delegate to anyone listening
   OnChange(Idx, Item, X, Y);
   
   //let the owner scene know we got a tap
   OwnerScene.OnTouch(self, X, Y, false);
}

/**
 * Cancels selection, closing the list
 */
function OnCancel()
{
   //Selection was canceled, close the list
   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
}

Example Menu Scene - Putting It All Together

The final step is to create a sample menu using these new controls. It should extend from the base UDNMobileMenuScene class so that it can accept input from the helper scene and pass it to touchable controls. This example has two comboboxes and a label. Changing the value of either combobox updates the label with its new value. Also, one of the comboboxes always displays its "name", while the other shows its current value.

class UDNMobileMenuExample extends UDNMobileMenuScene;

/**
 * Handle basic taps from controls
 */
event OnTouch(MobileMenuObject Sender,float TouchX, float TouchY, bool bCancel)
{
   //Combobox 1 changed, set label to its value
   if(Sender.Tag == "Combo1")
   {
      UDNMobileMenuLabel(FindMenuObject("Title")).Caption = UDNMobileMenuComboBox(FindMenuObject("Combo1")).GetValue();
   }
   //Combobox 2 changed, set label to its value
   else if(Sender.Tag == "Combo2")
   {
      UDNMobileMenuLabel(FindMenuObject("Title")).Caption = UDNMobileMenuComboBox(FindMenuObject("Combo2")).GetValue();
   }
}

defaultproperties
{
   //Create a combobox
   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)
   
   //Create a combobox
   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)
   
   //Create a label
   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)
}