Adding iPhone Touch Scrolling to a UnityGUI ScrollView

When running on the iPhone, UnityGUI ScrollViews act as if the player is using a mouse. The player can tap on or drag the scrollbar, but they can’t drag the list itself up and down with their finger. And naturally, that’s the first thing my playtester tried to do. So it would be useful to be able to add touch behavior to UnityGUI scrolling lists.


Pawns Puzzle Picker

Here’s how I did it. This article may be useful to someone porting UnityGUI code to the iPhone, or who just wants a working example of how to use the different iPhone touch phases.

(For a downloadable Unity package demonstrating everything in this post, scroll to the bottom.)

Design

The desired behaviors are:

  1. Allow the player to drag the list up and down
  2. Single-tap selects a row
  3. List has inertia, drifts to stop when the user lets go

Note that when a touch begins, we don’t yet know whether the player is starting a tap or a drag. And if they drag a short distance and then let go, did they tap that row or not? To avoid ambiguity many apps have the user press a “Done” button to confirm their selection. Other possibilities are to require a double-tap on a row, or treat it as a tap only if it is released quickly.

I decided to copy behavior I liked in Things for iPhone, which highlights the row when it is first touched. But if the user starts to drag it, the highlight disappears. So a tap can be slow as long as the finger doesn’t move. I like this because you can see what you’ve selected, and cancel it (with a short drag) if you missed your target.

Implementation

It should be possible to access touches during the OnGUI logic. I didn’t do it that way. Instead, my OnGUI code draws the scrolling list but no longer responds to clicks (although as I note below, that code can be left in for cross-platform purposes.) The code for detecting and responding to touches is now all in Update. (This is just a matter of preference, but by separating the row drawing from the code that handles input, I suspect it will be easier to replace the iPhone touch logic when porting the code to a new platform.)

1. Responding to Drags

The code for dragging the list turns out to be easy. All we need to do is detect when a touch delta has occurred. We then adjust the list’s current scroll position by the same amount.

if (touch.phase == TouchPhase.Moved)
{
    // dragging
    scrollPosition.y += touch.deltaPosition.y;
}

Depending on what controls you have in your scrolling list, you may find that the one that is being touched for the drag highlights, and stays highlighted after the player lets go. The solution is either to use a control that doesn’t behave that way (e.g. a Label instead of a Button) or to change the GUIStyle/GUISkin to make selected controls look the same as unselected.

2. Responding to Taps

Things can get a little trickier for detecting single-taps on a control in the list.

It’s tempting to keep the standard GUI click detection code in place. That can be used in some cases, but not if we want the behavior that I described above, where the row is highlighted when the touch begins, and can turn into a selection or a drag depending on if the finger moves or not.

So instead I took out the GUI click-response code and manage the taps myself. The trick here is in figuring out which row was tapped from the touch coordinates. Touch coordinates need to be adjusted as follows:

  • UnityGUI y-coordinates are inverted when compared to touch coordinates.
  • Touch coordinates need to be adjusted by the amount that the list has been scrolled.
  • UnityGUI windows and scrollframes also add offsets to the coordinates.

In my example the scrolling list is in a ScrollView, which in turn is in a Window. So the code to calculate which row a tap was on looks like this:

private int TouchToRowIndex(Vector2 touchPos)
{
    float y = Screen.height - touchPos.y;  // invert y coordinate
    y += scrollPosition.y;  // adjust for current scroll position
    y -= windowMargin.y;    // adjust for window y offset
    y -= listMargin.y;      // adjust for scrolling list offset within the window
    int irow = (int)(y / rowSize.y);
		
    irow = Mathf.Min(irow, numRows);  // they might have touched below last row
    return irow;
}

We also need to test if a tap is actually inside the scrollable list. (Again, we have to offset the list rectangle with the coordinates of the Window it is inside.)

bool IsTouchInsideList(Vector2 touchPos)
{
	Vector2 screenPos    = new Vector2(touchPos.x, Screen.height - touchPos.y);  // invert y coordinate
        Vector2 listSize     = new Vector2(windowRect.width - 2*listMargin.x, windowRect.height - 2*listMargin.y);
	Rect rAdjustedBounds = new Rect(listMargin.x + windowMargin.x, listMargin.y + windowMargin.y, listSize.x, listSize.y);

	return rAdjustedBounds.Contains(screenPos);
}

Now the code to handle drags and taps is as follows. I also added code to handle the Cancelled touch phase, which like the Moved phase cancels the selection. (Taps that are not inside the list are ignored; drags that go outside the list cancel the touch.)

fInsideList = IsTouchInsideList(touch.position);
if (touch.phase == TouchPhase.Began && fInsideList)
{
	selected = TouchToRowIndex(touch.position);
}
else if (touch.phase == TouchPhase.Canceled || !fInsideList)
{
	selected = -1;
}
else if (touch.phase == TouchPhase.Moved &&  && fInsideList)
{
	// dragging
	selected = -1;   // cancel the selection in favor of the drag
	scrollPosition.y += touch.deltaPosition.y;
}
else if (touch.phase == TouchPhase.Ended)
{
       // Was it a tap, or a drag-release?
       if ( selected > -1 )
       {
           Debug.Log("Player selected row " + selected);
       }
}

We also need to highlight the row that is being touched, at least until the player starts to drag it. I couldn’t find a documented way to tell UnityGUI to draw the next row with a different state (i.e. Hover or Focus, instead of Normal.) So instead I have a separate GUIStyle for selected rows. In the OnGUI code that draws the rows, I have the following:

       	if ( iRow == selected )
               	GUI.Button(rBtn, rowLabel, rowSelectedStyle);
       	else
       		GUI.Button(rBtn, rowLabel);

3. Inertia

When the user drags and then lets go a standard iOS scrolling list, it doesn’t stop moving immediately. We want that same effect of drifting to a stop. I do this by calculating an initial velocity when the touch ends. This moves the list for half a second, or until a new touch begins.

When the code detects that the touch has ended (TouchPhase.Ended) it stores the y-component of last touch’s deltaPosition. Dividing this by the deltaTime gives the starting velocity, which is gradually reduced to zero over the next half-second. This doesn’t have exactly the same feel that iOS lists have, but it’s very close.

Here is the code that sets up the inertia when the touch phase has just ended. It stores the time of the release and how fast the list was being scrolled at the time.

if (touch.phase == TouchPhase.Ended)
{
     // Was it a tap, or a drag-release?
     if ( selected > -1 )
     {
         Debug.Log("Player selected row " + selected);
     }
     else
     {
         // impart momentum, using last delta as the starting velocity
	 // ignore delta < 10; rounding issue can cause ultra-high velocity
	 if (Mathf.Abs(touch.deltaPosition.y) >= 10) 
	     scrollVelocity = (int)(touch.deltaPosition.y / touch.deltaTime);
	 timeTouchPhaseEnded = Time.time;
     }
}

I ran into a surprising bug where small finger movements would scroll the list extremely quickly. It turns out that a tiny movement of the finger can get rounded up to a delta of 5 pixels. This is a small amount, but given a fast framerate, this rounded amount represents a monstrously high velocity! The simple solution was for my inertia code to ignore very small movements. Less than 10 pixels seemed to work well in my tests on an iPhone 2g. (It’s possible that higher-resolution iPhones and iPads round by a different amounts.)

Here is the code that runs when there are no touches (or multiple touches, which I chose to ignore.) If the list has some leftover velocity from the last drag, it moves the list for a while longer. This uses the variables that were stored by the previous code sample.

if (Input.touchCount != 1)
{
    selected = -1; 

    if ( scrollVelocity != 0.0f )
    {
	// slow down over time
	float t = (Time.time - timeTouchPhaseEnded) / inertiaDuration;
	float frameVelocity = Mathf.Lerp(scrollVelocity, 0, t);
	scrollPosition.y += frameVelocity * Time.deltaTime;
				
	// after N seconds, we've stopped
	if (t >= inertiaDuration) scrollVelocity = 0.0f;
    }
   return;
}

Testing reveals that the list works well now. But there is a question of what to do with the original scrollbar. Dragging on it is a bit weird because it moves in the opposite direction as the list. In other words, you drag the list up to move down, but you drag the scrollbar up to move up. For Pawns I simply made the scrollbar very narrow to discourage tapping on it, but it remains as a visual cue that this is a scrollable list.

Miscellaneous

Note that if you make your controls respond only to touches, they can only be tested via UnityRemote or with an actual iPhone or iPad. So I found it useful to keep the original click logic in the OnGUI method as well. The code can be wrapped with a check to make it run only for non-iOS builds:

        if ( fClicked && Application.platform != RuntimePlatform.IPhonePlayer )
        {
           Debug.Log("Player mouse-clicked on row ");
        }

However, note that this code still runs in Unity Remote, which can lead to cases where a touch is interpreted both as touch and as a click.

One last optimization: we can reduce the number of drawcalls a lot by only drawing rows that are actually visible- a trick I blogged about earlier.

// draw call optimization: don't actually draw the row if it is not visible
// rBtn is the rectangle of the row we are about to draw
// rScrollFrame is the rectangle of the scrollview
if ( rBtn.yMax >= scrollPosition.y && 
     rBtn.yMin <= (scrollPosition.y + rScrollFrame.height) )
{
    	// ... GUI drawing commands for the current row ...
}

Demo

The following Unity 4 package (UPDATED 7/4/2013) contains sample GUI scripts that demonstrate a simple scrolling list that responds to iPhone/iPad touches. (In theory these should work on Android as well.) One script is written in C#, the other in JavaScript, but they are otherwise identical.
GUITouchScroll Package for Unity 4

Import it into your project. This will create a new folder, GUITouchScroll. Drag one of the GUITouchScroll prefabs that's in the folder into your Scene view to create the sample scrolling list-- one prefab uses the C# script, the other Javascript. The number of rows, and the window and list dimensions can be adjusted in the Inspector. The example uses the default UnityGUI skin, but this also can be replaced in the Inspector. You can also adjust the inertia, which is how quickly the list stops moving after you let go of a drag. (Fun tip: if you set inertia to 10 seconds a fast drag can leave the list bouncing up and down a few times!)

To test the scrolling and tap behavior, run the scene with Unity Remote or on an iOS device. When a row is tapped, a line of text will be output to the Unity debugging console. (Keep in mind that the scrolling will not look or feel very smooth on Unity Remote, due to the reduced frame rate.)

Important: If you are developing for Android please note that the touch events apparently work a little differently, which breaks the inertia. Please see the comments below from Ali, Greg, and Roberto for ways to fix it. My thanks to them!

(7/4/2013) I have added code to section 2 for detecting whether the touch is inside the scrolling list or not. Thanks for reporting that bug, everyone! This code has also been tested with Unity 4 now and it works without modification.

106 thoughts on “Adding iPhone Touch Scrolling to a UnityGUI ScrollView

  1. Pingback: UnityGUI Unresponsive on iPhone Retina Displays | MindTheCube

  2. Bob

    Hi. When I build to iPhone (4.1) I get the following coming up in console, and none of the rows display etc

    (Filename: Line: -1)

    MissingMethodException: Method not found: ‘Default constructor not found…ctor() of UnityEngine.GUI+ScrollViewState’.
    at System.Activator.CreateInstance (System.Type type, Boolean nonPublic) [0x00000] in :0
    at System.Activator.CreateInstance (System.Type type) [0x00000] in :0
    at UnityEngine.IDList.GetStateObject (System.Type t, Int32 controlID) [0x00000] in :0
    at UnityEngine.GUIUtility.GetStateObject (System.Type t, Int32 controlID) [0x00000] in :0
    at UnityEngine.GUI.BeginScrollView (Rect position, Vector2 scrollPosition, Rect viewRect, Boolean alwaysShowHorizontal, Boolean alwaysShowVertical, UnityEngine.GUIStyle horizontalScrollbar, UnityEngine.GUIStyle verticalScrollbar, UnityEngine.GUIStyle background) [0x00000] in :0
    at UnityEngine.GUI.BeginScrollView (Rect position, Vector2 scrollPosition, Rect viewRect, Boolean alwaysShowHorizontal, Boolean alwaysShowVertical) [0x00000] in :0
    at GUITouchScroll.DoWindow (Int32 windowID) [0x00000] in :0
    at UnityEngine.GUI+_Window.Do () [0x00000] in :0
    at UnityEngine.GUI.EndWindows (UnityEngine.IDList idlist) [0x00000] in :0
    at UnityEngine.GUIUtility.EndGUI (Int32 doLayout, Int32 doWindows, UnityEngine.IDList idlist) [0x00000] in :0

  3. Pingback: TouchScrolling on GUI for Unity3d iPhone « the codebook

  4. Andy

    Wow. I spent an entire work day trying to figure out how to just make it scroll in response to a finger swipe. And it was one line of code? THANK YOU SO MUCH! =D Totally helped me out. Wish I had seen it sooner though.

  5. Pingback: Tutorials Unity 3D : PowenKo

  6. D. Amos

    Excellente! Got it up and running in minutes while watching ‘Ancient Aliens’. How do I put an image into a row? I see the options to place to right or left, or image only. Tried putting into one of the background slots. Otherwise great. Very difficult to fine working
    tuts for IOS! Appreciate it!

  7. Matt Post author

    Glad it helped! To do an image instead of text, simply replace the rowLabel string with a texture. For example:

    GUI.Button(rBtn, rowLabel);
    

    would be

    GUI.Button(rBtn, rowTexture);
    

    If you want both text and texture, I believe you need to construct a GUIContent first. Something like:

    GUIContent contents = new GUIContent(rowLabel, rowTexture);
    GUI.Button(rBtn, contents);
    

    When I tried that it put the texture to the left of the string, rather than underneath. To make the texture the button’s background you are probably better off either working the texture into the GUIStyle of the button, or, pre-rendering each button (image and text) beforehand, so your game only is dealing with textures.

  8. Roi

    Hi mat.
    Thank you for this great tutorial.
    I am working on translating it to Js (i am new to unity and this is a great way to learn)
    and got it all working except one thing, which i dont understand exactly:

    private int TouchToRowIndex(Vector2 touchPos)
    {
    float y = Screen.height – touchPos.y; // invert y coordinate
    y += scrollPosition.y; // adjust for current scroll position
    y -= windowMargin.y; // adjust for window y offset
    y -= listMargin.y; // adjust for scrolling list offset within the window
    int irow = (int)(y / rowSize.y);

    irow = Mathf.Min(irow, numRows); // they might have touched below last row
    return irow;
    }

    the first line in the code , are you setting an int, is it a function, this is the only thing i didnt manage so far, and i see it effects the selction on the row and…\
    if you can help out this will be great. once its done, do you want to have the code so you can put it up here as well?

  9. Roi

    I got it.
    here is the script in Java, if you want to upload, just make sure i didnt do anything stupid, it works for me so far, but if you have any comments i would love to learn!
    again, thank you!!!!

    @script ExecuteInEditMode()

    //Original Script in C from http://www.mindthecube.com/blog/2010/09/adding-iphone-touches-to-unitygui-scrollview/comment-page-1#comment-2935

    public var optionsSkin : GUISkin; // this sets an optionSkin to load from skins
    public var rowSelectedStyle : GUIStyle; //this sets the row style within the script, you can add more styles and skins.

    // Internal variables for managing touches and drags
    private var selected :int = -1;
    private var scrollVelocity :float = 0f;
    private var timeTouchPhaseEnded = 0f;
    private var previousDelta :float = 0f;
    private var inertiaDuration :float = 0.5f;

    public var scrollPosition :Vector2 ;

    // size of the window and scrollable list
    public var numRows : int;
    public var rowSize : Vector2;
    public var windowMargin : Vector2;
    public var listMargin : Vector2;
    private var windowRect :Rect ;
    var windowID : int;

    function Update() //check for touch
    {
    if (iPhoneInput.touchCount != 1) //if this is a short touch
    {
    selected = -1;

    if ( scrollVelocity != 0.0f )
    {
    // slow down over time
    var t :float = (Time.time – timeTouchPhaseEnded) / inertiaDuration;
    var frameVelocity : float = Mathf.Lerp(scrollVelocity, 0, t);
    scrollPosition.y += frameVelocity * Time.deltaTime;

    // after N seconds, we’ve stopped
    if (t >= inertiaDuration) scrollVelocity = 0.0f;
    }
    return;
    }

    var touch : iPhoneTouch = iPhoneInput.touches[0];
    if (touch.phase == iPhoneTouchPhase.Began)
    {

    selected = TouchToRowIndex(touch.position);
    previousDelta = 0.0f;
    scrollVelocity = 0.0f;
    }
    else if (touch.phase == iPhoneTouchPhase.Canceled)
    {
    selected = -1;
    previousDelta = 0f;
    }
    else if (touch.phase == iPhoneTouchPhase.Moved)
    {
    // dragging
    selected = -1;
    previousDelta = touch.deltaPosition.y;
    scrollPosition.y += touch.deltaPosition.y;
    }
    else if (touch.phase == iPhoneTouchPhase.Ended)
    {
    // Was it a tap, or a drag-release?
    if ( selected > -1 )
    {
    Debug.Log(“Player selected row ” + selected);
    }
    else
    {
    // impart momentum, using last delta as the starting velocity
    // ignore delta = 10)
    scrollVelocity = touch.deltaPosition.y / touch.deltaTime;
    timeTouchPhaseEnded = Time.time;
    }
    }

    }

    function OnGUI () //this deals with the display
    {
    GUISkin = optionsSkin; // if you chose a guiskin before it will use it for the window not the Rows!!!!

    windowRect = Rect(windowMargin.x, windowMargin.y,
    Screen.width – (2*windowMargin.x), Screen.height – (2*windowMargin.y)); //this draws the bg
    GUI.Window (0, windowRect, GUI.WindowFunction (DoWindow), “GUI Scrolling with Touches”); //this draws the frame
    }

    function DoWindow (windowID) //here you build the table
    {
    var listSize : Vector2 = Vector2(windowRect.width – 2*listMargin.x,
    windowRect.height – 2*listMargin.y);

    var rScrollFrame :Rect = Rect(listMargin.x, listMargin.y, listSize.x, listSize.y);
    var rList :Rect = Rect(0, 0, rowSize.x, numRows*rowSize.y);

    scrollPosition = GUI.BeginScrollView (rScrollFrame, scrollPosition, rList, false, false);

    var rBtn :Rect = Rect(0, 0, rowSize.x, rowSize.y);

    for (var iRow : int = 0; iRow = scrollPosition.y &&
    rBtn.yMin <= (scrollPosition.y + rScrollFrame.height) )
    {
    var fClicked : boolean = false;
    var rowLabel : String = "Row Number " + iRow; //this is what will be written in the rows

    if ( iRow == selected )
    {
    fClicked = GUI.Button(rBtn, rowLabel, rowSelectedStyle);
    }
    else
    {
    fClicked = GUI.Button(rBtn, rowLabel);
    }

    // Allow mouse selection, if not running on iPhone.
    // Note: this code will be triggered
    if ( fClicked && Application.platform != RuntimePlatform.IPhonePlayer )
    {
    Debug.Log("Player mouse-clicked on row " + iRow);
    }
    }

    rBtn.y += rowSize.y;
    }
    GUI.EndScrollView();
    }
    var touchPos: Vector2;
    function TouchToRowIndex(touchPos) : int //this checks which row was touched

    {
    //
    var y :float = Screen.height – touchPos.y; // invert coordinates
    y += scrollPosition.y; // adjust for scroll position
    y -= windowMargin.y; // adjust for window y offset
    y -= listMargin.y; // adjust for scrolling list offset within the window
    var irow: int = y / rowSize.y;

    irow = Mathf.Min(irow, numRows); // they might have touched beyond last row
    return irow;

    }

  10. Matt Post author

    Thanks for that contribution, Roi! I’ve added a note to the end of my article, directing those who use JavaScript to see your comment. Glad you got it working, and very pleased that people are finding this article useful.

  11. Heudes

    I use Android, and inertia is not working … how do I make it work?
    Congratulations for the tutorial!

  12. Matt Post author

    Sorry- I don’t have the Android version to test with, and noone else has mentioned this issue to me. I suggest sprinkling Debug statements (or using breakpoints) to make sure the Ended phase is getting hit, and why it’s not setting the scroll velocity- it could either be a velocity calculation error, or it could be skipping over that code. Note that if you are running in a simulator (or very slow device) you might not get the effect. .. Let me know what you find out!

  13. chester

    hi..

    how can i change the text settings ??
    example ; i wanna change form “Row Number # ” to “ABC” ..
    and..
    how can i click on a button to go to another scene?

    your help is very appreciated .. and thanks..

    ^____^

  14. Matt Post author

    To respond to taps on a button, replace the code that says: Debug.Log("Player selected row " + selected);
    Replace it with the desired command. For example, you could load another scene with the command Application.LoadLevel.

    The text of each button is set by the line: string rowLabel = "Row Number " + iRow;
    Replace it with your own text, such as: string rowLabel = "ABC";
    (That’s not a great example, because every line will have the same label, but you get the idea. A more likely use is to have an array of strings, and you’d display the first name on line 1, the second name on line 2, and so on.)

    Hope this helps.

  15. Heudes

    I’m using Android 2.3, in dispositive Samsung Galaxi S… that’s a good, Tabblet..
    And yet without soluction… me sorry! And thank’s for the attempt of help.. Matt.

  16. Sushil

    Thanks so much for sharing this. I’ll be using this for scrolling thumbnails with checkboxes. Is there anyway to retract/collapse the whole menu thing when a GUI button is tapped and then expand it when its tapped again. This is because I dont want the menu to be appearing all the time, rather only when the user wants, he should be able to open/close it.
    Anyway great work! Thanks in advance

  17. Matt Post author

    Glad you found it useful.

    Inside the OnGUI method, the call to “GUI.Window” is what puts up the scrolling list. So you could easily add a button that toggles a boolean value when the user clicks on it. And then you’d wrap the GUI.Window call with an IF statement so that it only gets called when the boolean is true.

    If you wanted to get fancier you could animate the menu a bit, have it expand gradually, rather than just suddenly appear. This would require changing the size of the scrollRect for a second or two until it reaches its full size. Mathf.Lerp is a useful function for that kind of transition. (But you may find the non-animated version is good enough.) Hope this helps.

  18. Lucas

    getting error in the js translated version.

    unexpected char: 0x2013

    and it on this line of code:

    var t :float = (Time.time – timeTouchPhaseEnded) / inertiaDuration;

  19. Matt Post author

    You can get rid of the “unexpected character” errors by retyping in minus signs ‘-‘, and also smart-quotes are being used instead of quotes. I see at least two other problems though (braces mismatch, and a missing loop iterator.) It’s close though. I’ll debug the Javascript version and post it later this week.

  20. Matt Post author

    New packages for download! CSharp and JavaScript versions, updated and tested under Unity 3.5.
    I have updated the code in the article as well.

  21. pasko

    Thanks for the tutorial.
    You may want to add a little operation for the “bounce” effect when the scroll reaches end or start of the list, in the inertia section.
    The line can be:
    if(scrollPosition.y rList.height – rScrollFrame.height)
    scrollVelocity = – scrollVelocity;

  22. pasko

    sorry, miscopy&paste

    if(scrollPosition.y rList.height – rScrollFrame.height)
    scrollVelocity = –scrollVelocity;

  23. pasko

    ah no, not a miscopy. It is a filter for html entities.

    if(scrollPosition.y LESSTHAN 0 OR scrollPosition.y GREATERTHAN rList.height – rScrollFrame.height)
    scrollVelocity = –scrollVelocity;

  24. Matt Post author

    Neat idea, pasko! (I’m sorry code can’t be pasted here properly.)

    I played with your suggested change a little; I can’t test it with UnityRemote unfortunately because of the framerate. I’ll have to try it in on an actual device sometime.

    Incidentally, I guess you put your code in DoWindow– with small changes it can go in Update, which I think makes sense (that’s where the frame’s velocity is calculated.)

    Thanks for the feedback!

  25. emo

    hi, great job, but i can’t adjust the row to upperleft…also i put a icon in the row, and the same problem.

  26. Matt Post author

    If you are wondering how to make the text on each button appear on the left instead of in the middle, the easiest way is the following.

    1. Menu: Assets->Create->GUI Skin
    2. Select the gameObject in the scene that has my script on it. Probably called GUITouchScroll_JS or GUITouchScroll_CS, unless you renamed it.
    3. In the Inspector, assign the new skin to the “Options Skin” variable.

    Nothing appears to happen, because the new skin looks just like the default skin. But now you can edit the skin.

    4. Select the skin you created. In the Inspector, click on Button. Under button find “Alignment” and set it to MiddleLeft. Now the text is on the left of each button!

    There are lots of other skin settings you can play with– you can change the font, or the position of images on the button. Or you can change the look completely by getting brand new guiskins from the internet, or design your own.

  27. emo

    OH!, thanks Matt!.
    All works!
    Another thing, I have your prefab and a GUI.button in another part of the scrren. it seems that whatever i touch on the screen, even if is out of the menu it loads a item of the prefab when i compile for ipad.

  28. Matt Post author

    Ouch, a bug! In my example the scrolling list takes up most of the screen, so I didn’t notice that taps outside of the visible list aren’t being ignored. My suggested solution would be to add a test to the function TouchToRowIndex, to make it return -1 if the touch is on a row that’s not visible. Next time I fiddle with this code I’ll see about adding that check, but I can’t promise when I’ll get to it.

    Thanks for reporting it!

  29. emo

    Ok, thanks Matt.
    do u know how to change the normal background in Row Selected style at runtime?

  30. rod

    hi, am trying to change the entire style of the row, because i want to put a differente background for each one, but even if i’ve change the Selected Row Style i cant get any different style

  31. Matt Post author

    Selected Row Style is only applied when a row is selected (and not being dragged). (The color can change, the font can be bolded, or whatever.)

    Try changing the skin’s Button style instead. On 6/5 I posted instructions on editing the skin. You can set the button’s style there, in the inspector. I would try that if you haven’t already, just to make sure you are changing the right thing.

    To do it at runtime, you will need to do a little more work. If all buttons have the same background, then you can set the Button’s Background to any texture you like. But if you want to make each background different, you may want to declare one new GUIStyle for each different row background. (They could be in an array, or declared individually just like SelectedRowStyle.) Then you’d edit my calls to GUILayout.Button. Right now one call uses the Selected Row Style, the other lets the skin format the button. Change the one that uses the skin to instead use one of your new GUIStyles.

  32. aarora2

    Hi to all,

    This tutorial is very good…

    But I need a little help…
    I also want 4 number of Buttons in a single row. or you can say 4 number of colums buttons in a single row.
    Is it possible?
    If yes please reply me as soon as possible.

    Thanks in advance…

  33. Matt Post author

    It’s certainly possible. In the code you’ll see a couple lines that call GUI.Button. One is for the selected version of the line, the other is the plain unselected version. That’s where you’d add 6 new calls to GUI.Button (3 in the selected style, 3 in the unselected style.) Here’s what the first half of that code might look like:
    if ( iRow == selected )
    {
    fClicked = GUI.Button(rBtn, rowLabel, rowSelectedStyle);
    fClicked = fClicked || GUILayout.Button(rBtn2, "2", rowSelectedStyle);
    fClicked = fClicked || GUILayout.Button(rBtn3, "3", rowSelectedStyle);
    fClicked = fClicked || GUILayout.Button(rBtn4, "4", rowSelectedStyle);
    }
    else

    I didn’t put in the code to calculate the size and position of each button (rBtn2, rBtn3, and rBtn4.) Instead of doing that you could switch the rows to use GUILayout instead of GUI. That gives you less control over the widths, but if each row is displaying mostly icons the widths may come out the same anyway.

    Note that the more buttons you have in each row, the worse the performance will be. If it gets bad try displaying fewer rows (in the example script set Window Margin Y to a larger number.)

  34. Pingback: Unity 開発用メモ | アレとコレ.jp

  35. Manas

    I used texture array in place of fClicked = GUI.Button(rBtn, rowLabel) i have
    fClicked = GUI.Button(rBtn, rowTexture[iRow]);

    the textures are working fine but i want to a load a new level with the same image as on button when the image button is clicked.

    Any idea how to do this..??

  36. Matt Post author

    My apologies, I missed this comment when you posted it a couple months ago. I’ll respond now, just in case you are still wrestling with this.

    Not exactly sure what you are asking. Do you want the image on the button to be a picture of the level, and then you click on it to go to that level? There are two ways to do that. The first is to take a snapshot of each level, then that as the texture for the scrolling menu. (This would also work for saved games, but you’d have to make your game take the snapshot picture when the player saves the game, then load that texture dynamically into the menu. I can’t walk you through the whole thing here, but it is possible in Unity and who knows, if you google “Unity3d load screenshot texture” you might find someone’s done it already. Also search answers.unity3d.com.)

    For level selection, instead of a scrolling menu you could simply load the level, show it to the user and give the player arrow buttons to let them skip to the next/previous level. In my iOS game Pawns I do both of these: I have a scrolling menu to select any level by name (but there’s no picture of the level). I also have a way for the player to skip to the next or previous level.

    Hope this helps.

  37. Rayan Adam

    Hi Matt …. thanks for this great job

    i have a problem in positioning the whole window gui, when ever i change the window margin it keeps scaling the window at the center of the screen,

    am trying to move it to the bottom left of the screen …. as well as the list inside the window i want to position it in upper left of the window gui .

    any ideas …. thanks in advance

  38. ali

    hopefully you are still checking this. but did anybody ever figure out why inertia wont work on android? it works perfectly fine on my iphone, but only the normal touch scrolling is responding on the nexus im using

    using the same exact code on both

  39. ali

    nevermind. i figured it out. apparently android devices don’t like to trigger “TouchPhase.Ended”
    i just had “scrollVelocity = touch.deltaPosition.y/Mathf.Abs(touch.deltaTime);” and “timeTouchPhaseEnded = Time.time;” update in “TouchPhase.Moved” all the time… kind of spam updates the variables. but at least it works

  40. Matt Post author

    @Rayan: It’s all doable. One way would be to replace the margin variable with a Rect, to store the 4 margin values for the top, left, right, and bottom. You would need to find all the code that currently uses the single margin variable and change it.

  41. Rayan Adam

    Hi Matt .. I made come changes on the code where buttons becom labels, and insted of select effect on the button i make the label change its style when i finish a mission (where there is 3 mission for example)… I did what you told me by changing the margin into Rect … but when I run the game the list behaves weird !!!

    the screen width for my game is [787×630]

    and if you have any advises for me to change on the code like for the missions code or the screen resolution, plz let me know :]

    here is the code after modification :

    using UnityEngine;
    using System.Collections;

    [ExecuteInEditMode]

    public class ListTest : MonoBehaviour {

    public GUISkin optionsSkin;
    public GUIStyle MissionCompletedStyle;

    // Internal variables for managing touches and drags
    protected int selected = -1;
    private float scrollVelocity = 0f;
    private float timeTouchPhaseEnded = 0f;
    private const float inertiaDuration = 0.5f;

    public Vector2 scrollPosition;

    // Number and Size of the Rows
    public int numRows;
    public Vector2 rowSize;

    //public Vector2 TestValues;

    // size of the window and scrollable list
    public Rect listRect;
    public Rect windowRect;

    void Update()
    {
    if (Input.touchCount != 1)
    {
    selected = -1;

    if ( scrollVelocity != 0.0f )
    {
    // slow down over time
    float t = (Time.time – timeTouchPhaseEnded) / inertiaDuration;
    float frameVelocity = Mathf.Lerp(scrollVelocity, 0, t);
    scrollPosition.y += frameVelocity * Time.deltaTime;

    // after N seconds, we’ve stopped
    if (t >= inertiaDuration) scrollVelocity = 0.0f;
    }
    return;
    }

    Touch touch = Input.touches[0];
    if (touch.phase == TouchPhase.Began)
    {
    selected = TouchToRowIndex(touch.position);
    scrollVelocity = 0.0f;
    }
    else if (touch.phase == TouchPhase.Canceled)
    {
    selected = -1;
    }
    else if (touch.phase == TouchPhase.Moved)
    {
    // dragging
    selected = -1;
    scrollPosition.y += touch.deltaPosition.y;
    }
    else if (touch.phase == TouchPhase.Ended)
    {
    // impart momentum, using last delta as the starting velocity
    // ignore delta = 10)
    scrollVelocity = (int)(touch.deltaPosition.y / touch.deltaTime);
    timeTouchPhaseEnded = Time.time;

    }

    }

    void OnGUI ()
    {
    GUI.skin = optionsSkin;

    windowRect = new Rect( windowRect.x, windowRect.y,
    Screen.width – (2*windowRect.x), Screen.height – (2*windowRect.y));
    // Screen.width*(windowRect.x/787), Screen.height*(windowRect.y/630), Screen.width*(windowRect.width/787), Screen.height*(windowRect.height/630));
    GUI.Window(0, windowRect, (GUI.WindowFunction)DoWindow, “Objectives”);
    }

    void DoWindow (int windowID)
    {
    listRect = new Rect( listRect.x, listRect.y,
    windowRect.width – 2*listRect.x, windowRect.height – 2*listRect.y );
    // Screen.width*(windowRect.x/787), Screen.height*(windowRect.y/630), Screen.width*(windowRect.width/787), Screen.height*(windowRect.height/630));

    Rect rScrollFrame = new Rect(listRect.x, listRect.y, listRect.width, listRect.height);
    Rect rList = new Rect(0, 0, Screen.width*(rowSize.x/787), numRows*(Screen.height*(rowSize.y/630)));

    scrollPosition = GUI.BeginScrollView (rScrollFrame, scrollPosition, rList, false, false);

    Rect rLbl = new Rect(0, 0, Screen.width*(rowSize.x/787), Screen.height*(rowSize.y/630));

    for (int iRow = 0; iRow = scrollPosition.y && rLbl.yMin = scrollPosition.y && rLbl.yMin = scrollPosition.y && rLbl.yMin <= (scrollPosition.y + rScrollFrame.height) ){
    if(Mission3.Done)
    GUI.Label(rLbl, "Mission 3", MissionCompletedStyle);
    else
    GUI.Label(rLbl, "Mission 3"); // Mission 3
    }
    rLbl.y += Screen.height*(rowSize.y/630);
    }
    GUI.EndScrollView();
    }

    private int TouchToRowIndex(Vector2 touchPos)
    {
    float y = Screen.height – touchPos.y; // invert coordinates
    y += scrollPosition.y; // adjust for scroll position
    y -= windowRect.y; // adjust for window y offset
    y -= listRect.y; // adjust for scrolling list offset within the window
    int irow = (int)(y / rowSize.y);

    irow = Mathf.Min(irow, numRows); // they might have touched beyond last row
    return irow;
    }

    }

  42. Rayan Adam

    a reminder for what I asked you in the first place …. I want to put the list on the left bottom of the screen but it keeps centering itself in the middle of the screen ??

    and thanks in advance :))

Leave a Reply

Your email address will not be published. Required fields are marked *