Monday, July 23, 2012

Smooth Operator

Creating a scrolling box is harder than you might think.

Matt did most of the work on this, but I thought I would write it up in case anyone else finds it handy.  Final Game Maker Studio code will be included at the bottom.

So, scrolling.

When you click on a series of items, you expect a certain behavior when you move your finger or mouse.  You are clicking and dragging a set of items.  Here's the diagram:


The measurements given are arbitrary, but useful because they are concrete and can be generalized later.  The red regions indicate where the list item should be hiding behind the border (not quite possible with GMS - you can draw a portion of a sprite [the list item background], but not part of text).

You touch somewhere (hit) and drag to another location (drag), which creates a distance to travel (trav).

The list of items should follow proportional to how far you moved, and stop according to the velocity when you lift your finger.  That is, if you click and continue to hold the finger at the end, it should move proportionally and then stop.  If you end with a flick, it should keep scrolling.

Still with me?

So, the coordinate system is expressed as y increases as you go down the screen.  Standard for graphics work.  In the diagram, the boundary box that will contain the list items is from (50, 113) to (350, 377).  To simplify the diagram, I left the x coordinates out and will leave them out from now on.  So the box where the list items are shown is from y = 113 to y = 113 + 264 = 377.

The list itself might be any size, but in this diagram is 318 tall, and trying to fit into 264 space.  So, some things will be hidden.  That's the easy part.

The hard part is controlling when the list should move, how much, and when it should stop.

When you touch the screen and move, your movement is being sampled 60 times per second, so 60 times per second, trav is calculated.  If you touch on the screen within the list box, say at y=300 and drag up to y=210 in a half second, this results in an average speed of -90 (distance) / 30 (samples in .5 seconds) = -3.  So, each list item should move that distance per step as well.

Anytime your finger is moving, trav is being recalculated.  When you lift your finger, trav stops being recalculated.  One of the cool things is that if you keep trav as it last was, that is, you keep moving all the list items at the same rate as was last calculated as trav, "fling" magically works.  Greater distance travelled in less time and those list items can really move!

So, here's where the problems begin.

There is no deceleration yet.  So you "fling" and the list items just keep moving.  And, while it's fun to watch a to do list disappear off the screen, and then fling in the other direction and seconds later watch it disappear the other way, it's not practical.  Additionally, it takes one step to establish the initial speed, so it will be laggy and appear to "jump" at times.  The first solution is to add acceleration and deceleration.

(Pictures to be added)

Ideal movement accelerates and decelerates
Slower movements move the list items much more slowly and quick movements move the list items very fast, and then they slowly come to a stand still.  There are several ways to do this.  I think the solution settled on was to multiply the current moving speed of the list items by .92 per step.  So after 0.5 seconds, the list items are traveling .92^30 = 0.0819662036 times their original speed.  Fairly fast deceleration.  When the items are traveling less than 1px per step, the speed is set to 0 and they stop moving.  Acceleration can be applied similarly, but I think the solution in this case was to judge the magnitude of trav and set the speed of the list items to ratios according to pre-defined regions.

Boundaries should keep list items filling the box:
When you fling up, the list items should stop moving when the last one is fully displaying at the bottom of the box.  Similarly, fling down and it will stop when the first list item is fully displayed at the top of the box.  It should 'stick' until moved by the finger again.

This is where calculations come in.  You can calculate from the first list item, the last list item, or both.

Both approach:
If the first list item's y value > surrounding boxes top y, then all the list items need to travel back to the surrounding boxes y.  If ( (y-27) > 113 ) then trav = 113 - y + 27.  Funny thing though.  Applying just this equation causes the list to become a slingshot as you use your finger to draw the first list item down below the top of the box, because trav can become HUGE!

So you apply damping when the list is within, say, 5 pixels of where it's supposed to be and just set the trav to 0.  If (abs( y - 27 - 113) < 5) then trav = 0.

And you would think you could do the same thing for the bottom item...
If the last list item's y value < surrounding boxes bottom y, then all the list items need to travel back.  So, If ( (y+28) < 377 ) then trav = 377 - y - 28.  Again, dampen it within 5 pixels.  If (abs( y + 27 - 337) < 5) then trav = 0.

There are more potential problems!  
  • The list items can stop too early, off screen.  The solution?  add another condition so the abs() functions only check on one side.  That is, If (y > 113 && abs( y - 27 - 113) < 5) then trav = 0.
  • The list might 'bounce' back and forth if trav is much greater than your tolerance.  This shouldn't happen with the out of bounds correction and the buffer, but if you choose to not move the list items the full distance to the end when the first item goes past the top, smooth acceleration can cause bouncing.
  • Rounding errors!
  • The list might 'get stuck' depending on the order of the statements.


Anyone else care to venture a guess at the methods based just on the first element or just on the last element?


CODE:



s = ds_list_size(lone.lil);

if dropdown.shift != 0
{
hoverbox = false;
fade = 0;
}

if hoverbox = true
{
    if fade < 1
    {
        fade += 0.05;
    }
}
else
{
    fade = 0;
}


if itemnumber = (s-1) //checking to see if last item in list
{
    global.bottom = y; //get grabbed by scroll bar

    if dropdown.shift > 0 && abs(distance_to_point(x,dropdown.y+400-34)) < 5 //
    {
        dropdown.shift = 0;
    }

    if y+17 < (dropdown.y+400)
    {
        dropdown.shift = ((dropdown.y+400)-(y+17));
    }
   
    if abs(dropdown.shift) > 1
    {
        if room_speed > 30 //deceleration based on room speed
        {
            dropdown.shift = ((dropdown.shift * 0.98)); //deceleration
        }
        else
        {
            dropdown.shift = ((dropdown.shift * 0.96)); //deceleration
        }
    }
    else
    {
        dropdown.shift = (0);
    }
   
}

else

if itemnumber = 1
{

    if dropdown.shift > 0 && abs(distance_to_point(x,dropdown.y+18)) < 100-70
    {
        dropdown.shift = 5;
    }
   
    if dropdown.shift > 0 && abs(distance_to_point(x,dropdown.y+16+35)) < 5
    {
        dropdown.shift = 0;
    }
   
    if (y-17) > (dropdown.y+18)
    {
       dropdown.shift = ((dropdown.y+18)-(y-17)+0); ///////
    }  
   

}









No comments:

Post a Comment