GUI ScrollView doesn't work with GUI.matrix

I’m using this test code:

Vector2 scrollVector;
void OnGUI()
{
    int DesignedForWidth = 800;
    int DesignedForHeight = 480;
    float xs = Screen.width / (float)DesignedForWidth;
    float ys = Screen.height / (float)DesignedForHeight;
    GUI.matrix = Matrix4x4.TRS(Vector3.zero,  Quaternion.AngleAxis(0, new Vector3(0, 1, 0)), new Vector3(xs, ys, 1));
    Debug.Log(xs + "," + ys + "," + Screen.width + "," + Screen.height);

    GUILayout.BeginHorizontal(GUI.skin.box);
    scrollVector = GUILayout.BeginScrollView(scrollVector, GUILayout.Width(800), GUILayout.Height(480));

    for (int x = 0; x < 30; x++)
        GUILayout.Label("test" + x);

    GUILayout.EndScrollView();
    GUILayout.EndHorizontal();
}

Which produces these results when I resize the game area in the editor. The top image is 800x480 and the bottom image is 400x240. I was expecting the scroll view to fill the game window regardless of the window height. I want to use something like this to display a list of high scores with Facebook profile pictures next to each item in list.

As you can hopefully see in the image above, the ScrollView scrollbar stays on the right side, but the box part of the ScrollView is half of the expected width. Seems like maybe there is a bug that is scaling this area twice?

I’m using Unity 4.3.4f1 (I tried to upgrade but it broke several of my existing games so not an easy option). This is a 2D project.

The more I worked with it, I found that GUI.ScrollView just wasn’t very good, and I think an actual “scrollbar” (thumb, track, etc) is pretty “old school” and/or business application like, not so much “cool” for games. So I created this class that you can attach to any Transform to allow the user to scroll it by dragging the mouse or finger.

This is for 2D games, but could probably be adapted/modified for 3D:

Example usage:

  1. Place an empty game object (Object A)

  2. Add a BoxCollider2D to Object A and adjust the size to cover whatever part of screen you want your scrollable area to cover (you can skip this step, don’t add collider, if you want it to cover the entire screen)

  3. Add another empty game object (Object B) as child of Object A

  4. Add this script to Object B

  5. Add your list items as children of Object B

At this point you can play/run your game and see how it works. You will need to adjust some of the public variables that the script exposes based on your games needs. I find it best to adjust these while playing, then make note of the values that work best and re-apply them after you stop game.

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Transform))]
public class Scroller : MonoBehaviour
{
    // Set to min X/Y position you want to allow for the transform this script is attached to.
    public float MinScroll = 0;

    // Set to max X/Y position you want to allow for the transform this script is attached to.
    public float MaxScroll = 100;

    // Set the amount of bounce.  Use 0 to disable bounce.  Higher number will make bounce more obvious.
    public float BounceAmount = 5;

    // Direction of scroll.  You can change this at any time, but you'll probably want to reposition the
    //  transform manually if you do, to make sure it isn't already scrolled in one direction.
    public enum Directions { Vertical, Horizontal }
    public Directions Direction = Directions.Vertical;

    // Set a collider if you want to limit the area that input is tracked in.  If none set, input will be tracked everywhere.
    public Collider2D Target;

    // Higher number makes the "fling to scroll" slow down faster (more friction), set to 0 to make it scroll without slowing
    //  down (until end of list), or set to negative to make it speed up as it scrolls.
    public float Friction = 100;

    // Depending on your camera size (game unit to screen size relationship), you may need to adjust these to get "fling to move"
    //  to work well.
    public float MinVelocityRecalcMovementAmount = 1;
    public float VelocitySpeed = 1;

    // Internal variables used for state tracking, do not modify.
    Vector3 LastMousePos = Vector3.zero;
    Vector3 LastMouseVelocityPos;
    float LastMouseVelocityTime;
    float LastMouseVelocity;
    float BounceStartTime = 0;
    float BounceStart;
    bool TrackMouse = false;

    // Just gets/sets the correct X or Y offset based on Direction.
    float Offset
    {
        get
        {
            if (Direction == Directions.Vertical)
                return transform.localPosition.y;
            return transform.localPosition.x;
        }
        set
        {
            Vector3 v3 = transform.localPosition;
            if (Direction == Directions.Vertical)
                v3.y = value;
            else
                v3.x = value;
            transform.localPosition = v3;
        }
    }

    // The max scroll value including bounce (i.e.-can scroll beyond max, then it bounces back)
    float BouncedMax
    {
        get
        {
            return MaxScroll + BounceAmount;
        }
    }

    // Min scroll value including bounce
    float BouncedMin
    {
        get
        {
            return MinScroll - BounceAmount;
        }
    }

    void Update()
    {
        // Translate screen to world -- screen is harder to adjust for because of all of the different screen sizes
        //  we support -- world units are easier/constant, as long as camera size doesn't change.
        Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);

        // If left mouse button was just pressed (or just touched screen)...
        if (Input.GetMouseButtonDown(0))
        {
            // If not within target bounds, we ignore (if no target we don't limit tracking).
            //  Collider2D.bounds.Contains includes the Z axis in the check, but we want to ignore it, so we create a temp
            //  vector using the Z value of the target for our check.
            if (Target != null && !Target.bounds.Contains(new Vector3(mousePosition.x,mousePosition.y,Target.bounds.center.z)))
                return;

            // Set flag, we will track mouse/touch until up.
            TrackMouse = true;

            // We force last mouse pos to update so that we don't act like user dragged from
            //  whatever spot they were at last click/touch.
            LastMousePos = mousePosition;
            LastMouseVelocityPos = LastMousePos;
            LastMouseVelocityTime = Time.time;

            // Make sure this is cleared
            BounceStartTime = 0;
        }

        // If left mouse button is not currently down (and finger not touching screen)...
        if (!Input.GetMouseButton(0))
        {
            // If left mouse button was just released (or finger just lifted)...
            if (Input.GetMouseButtonUp(0))
            {
                // If they held still for over 300ms before release, clear velocity so we don't scroll.
                float elapsedTime = Time.time - LastMouseVelocityTime;
                if (elapsedTime > 0.3f)
                    LastMouseVelocity = 0;

                // Clear flag, no longer tracking mouse.
                TrackMouse = false;
            }

            // We'll calculate a new offset.
            float newOffset = Offset;

            // If we have velocity to apply...
            if (LastMouseVelocity != 0)
            {
                // Scroll based on velocity
                newOffset += LastMouseVelocity * Time.deltaTime;
                if (newOffset < BouncedMin)
                {
                    newOffset = BouncedMin;
                    LastMouseVelocity = 0;
                }
                else if (newOffset > BouncedMax)
                {
                    newOffset = BouncedMax;
                    LastMouseVelocity = 0;
                }

                // Reduce velocity based on fiction value
                if (LastMouseVelocity < 0)
                {
                    LastMouseVelocity += Time.deltaTime * Friction;
                    if (LastMouseVelocity > 0)
                        LastMouseVelocity = 0;
                }
                else if (LastMouseVelocity > 0)
                {
                    LastMouseVelocity -= Time.deltaTime * Friction;
                    if (LastMouseVelocity < 0)
                        LastMouseVelocity = 0;
                }
            }

            // If scrolled beyond non-bounce range we slowly move back within range (bounce).
            if (newOffset < MinScroll)
            {
                if (BounceStartTime == 0)
                {
                    BounceStartTime = Time.time;
                    BounceStart = newOffset;
                }
                float elapsedTime = Time.time - BounceStartTime;
                float bouncePerc = elapsedTime / 0.3f;
                if (bouncePerc > 1)
                    bouncePerc = 1;
                newOffset = Mathf.Lerp(BounceStart, MinScroll, bouncePerc);
            }
            else if (newOffset > MaxScroll)
            {
                if (BounceStartTime == 0)
                {
                    BounceStartTime = Time.time;
                    BounceStart = newOffset;
                }
                float elapsedTime = Time.time - BounceStartTime;
                float bouncePerc = elapsedTime / 0.3f;
                if (bouncePerc > 1)
                    bouncePerc = 1;
                newOffset = Mathf.Lerp(BounceStart, MaxScroll, bouncePerc);
            }

            // Apply new offset
            Offset = newOffset;
        }

        // Otherwise mouse/touch still down...
        else
        {
            // If not tracking mouse, we do nothing.
            if (!TrackMouse)
                return;

            // Calculate amount of mouse/touch travel in direction we care about.
            float scrollAmount;
            if (Direction == Directions.Vertical)
                scrollAmount = mousePosition.y - LastMousePos.y;
            else
                scrollAmount = mousePosition.x - LastMousePos.x;

            // Scroll by amount of mouse/touch movement, but keep within bounced range.
            float newOffset = Offset;
            newOffset += scrollAmount;
            if (newOffset < BouncedMin)
                newOffset = BouncedMin;
            else if (newOffset > BouncedMax)
                newOffset = BouncedMax;
            Offset = newOffset;

            // Update last mouse position
            LastMousePos = mousePosition;

            // We only update velocity if input has moved more than a set amount...
            if (Direction == Directions.Vertical)
                scrollAmount = mousePosition.y - LastMouseVelocityPos.y;
            else
                scrollAmount = mousePosition.x - LastMouseVelocityPos.x;
            if (Mathf.Abs(scrollAmount) > MinVelocityRecalcMovementAmount)
            {
                float elapsedTime = Time.time - LastMouseVelocityTime;
                LastMouseVelocity = (scrollAmount / elapsedTime) * VelocitySpeed;
                LastMouseVelocityPos = LastMousePos;
                LastMouseVelocityTime = Time.time;
            }
        }
    }
}