0

I'm doing a simple match-three game, similar to Bejeweled and I just want to move sprite objects by touching the sprite object and then move it one step in four directions like left, right, up and down. I do this by comparing the X and Y values on Down with the X and Y values on Move. It's working but it's far from perfect! It's so easy to get a wrong value if the movement isn't straight. My questions is: is there a way to improve this and make it better?

I have also looked at gesture, but this seems very complicated to use with a surfaceview that I have.

@Override
public boolean onTouch(View v, MotionEvent event) {

    switch (event.getAction()) {

    case MotionEvent.ACTION_DOWN:
        Log.i("test","Down");

        touchActionDownX = (int)event.getX();
        touchActionDownY = (int)event.getY();
        touchActionMoveStatus = true;

        gameLoop.touchX = (int)event.getX();
        gameLoop.touchY = (int)event.getY();
        gameLoop.touchActionDown = true;
        break;

    case MotionEvent.ACTION_POINTER_UP:

        touchActionMoveStatus = true;

        break;

    case MotionEvent.ACTION_MOVE:
        //Log.i("test","Move");
        gameLoop.touchActionMove = true;

        if(touchActionMoveStatus) {

        touchActionMoveX = (int)event.getX();
        touchActionMoveY = (int)event.getY();

        if(touchActionMoveX < touchActionDownX)
            Log.i("test","Move Left");
        else if(touchActionMoveX > touchActionDownX)
            Log.i("test","Move Right");
        else if(touchActionMoveY < touchActionDownY)
            Log.i("test","Move Up");
        else if(touchActionMoveY > touchActionDownY)
            Log.i("test","Move Down");

        touchActionMoveStatus = false; // Will be set to true when pointer is up
        }

        break;
    }

    // return false;
    return true; // This gets the coordinates all the time
}
3D-kreativ
  • 9,053
  • 37
  • 102
  • 159

3 Answers3

3

Try something like this:

@Override
public boolean onTouch(View v, MotionEvent event) {

    //You may have to play with the value and make it density dependant.
    int threshold = 10;

    switch (event.getAction()) {

    case MotionEvent.ACTION_DOWN:
        Log.i("test","Down");

        touchActionDownX = (int)event.getX();
        touchActionDownY = (int)event.getY();
        touchActionMoveStatus = true;

        gameLoop.touchX = (int)event.getX();
        gameLoop.touchY = (int)event.getY();
        gameLoop.touchActionDown = true;
        break;

    case MotionEvent.ACTION_POINTER_UP:

        touchActionMoveStatus = false;

        break;

    case MotionEvent.ACTION_MOVE:
        //Log.i("test","Move");
        gameLoop.touchActionMove = true;

        if(touchActionMoveStatus) {

        touchActionMoveX = (int)event.getX();
        touchActionMoveY = (int)event.getY();

        if(touchActionMoveX < (touchActionDownX - threshold) && (touchActionMoveY > (touchActionDownY - threshold)) && (touchActionMoveY  (touchActionDownY + threshold))){
            Log.i("test","Move Left");//If the move left was greater than the threshold and not greater than the threshold up or down
            touchActionMoveStatus = false;
        }
        else if(touchActionMoveX > (touchActionDownX + threshold) && (touchActionMoveY > (touchActionDownY - threshold)) && (touchActionMoveY < (touchActionDownY + threshold))){
            Log.i("test","Move Right");//If the move right was greater than the threshold and not greater than the threshold up or 
            touchActionMoveStatus = false;
       }
        else if(touchActionMoveY < (touchActionDownY - threshold) && (touchActionMoveX > (touchActionDownX - threshold)) && (touchActionMoveX < (touchActionDownX + threshold))){
            Log.i("test","Move Up");//If the move up was greater than the threshold and not greater than the threshold left or right
            touchActionMoveStatus = false;
        }
        else if(touchActionMoveY > (touchActionDownY + threshold) && (touchActionMoveX > (touchActionDownX - threshold)) && (touchActionMoveX < (touchActionDownX + threshold))){
            Log.i("test","Move Down");//If the move down was greater than the threshold and not greater than the threshold left or right
            touchActionMoveStatus = false;
        }
        }

        break;
    }

    // return false;
    return true; // This gets the coordinates all the time
}

Or use a ratio:

@Override
public boolean onTouch(View v, MotionEvent event) {

    //You may have to play with the value. 
    //A value of two means you require the user to move twice as 
    //far in the direction they intend to move than any perpendicular direction.
    float threshold = 2.0;

    switch (event.getAction()) {

    case MotionEvent.ACTION_DOWN:
        Log.i("test","Down");

        touchActionDownX = (int)event.getX();
        touchActionDownY = (int)event.getY();
        touchActionMoveStatus = true;

        gameLoop.touchX = (int)event.getX();
        gameLoop.touchY = (int)event.getY();
        gameLoop.touchActionDown = true;
        break;

    case MotionEvent.ACTION_POINTER_UP:

        touchActionMoveStatus = true;

        break;

    case MotionEvent.ACTION_MOVE:
        //Log.i("test","Move");
        gameLoop.touchActionMove = true;

        if(touchActionMoveStatus) {

        touchActionMoveX = (int)event.getX();
        touchActionMoveY = (int)event.getY();

        // I haven't tested this so you may have a few typos to correct.
        float ratioLeftRight = Math.abs(touchActionMoveX - touchActionDownX)/Math.abs(touchActionMoveY - touchActionDownY)
        float ratioUpDown = Math.abs(touchActionMoveY - touchActionDownY)/Math.abs(touchActionMoveX - touchActionDownX)

        if(touchActionMoveX < touchActionDownX && ratioLeftRight > threshold){
            Log.i("test","Move Left");
            touchActionMoveStatus = false;
        }
        else if(touchActionMoveX > touchActionDownX && ratioLeftRight > threshold){
            Log.i("test","Move Right");
            touchActionMoveStatus = false;
        }
        else if(touchActionMoveY < touchActionDownY && ratioUpDown > threshold){
            Log.i("test","Move Up");
            touchActionMoveStatus = false;
        }
        else if(touchActionMoveY > touchActionDownY && ratioUpDown > threshold){
            Log.i("test","Move Down");
            touchActionMoveStatus = false;
        }
        }

        break;
    }

    // return false;
    return true; // This gets the coordinates all the time
}
Larry McKenzie
  • 3,253
  • 25
  • 22
  • Not perfect but you should get the idea. You could take it further with additional conditions. If you don't understand I can add additional code. – Larry McKenzie May 25 '13 at 07:57
  • Now that I see your code, it's just the way I done to make it to work better, but I just used a value of 5. But none of theses values are working. With a threshold I don't get any result at all!? Please add more code if it could be better! :) – 3D-kreativ May 25 '13 at 08:01
  • If you are not getting any results then my threshold is too high. I will lower it and add the addition conditions I am talking about. – Larry McKenzie May 25 '13 at 08:05
  • I have testedthe value of 10, but I guess I still get bad values if movements is a little bit diagonal!? What additional conditions? – 3D-kreativ May 25 '13 at 08:19
  • By adding the conditions like in my edit it doesn't register a move when someone moves diagonal passing both left and up thresholds for example. – Larry McKenzie May 25 '13 at 08:24
  • Hmm, I tesed your code, but it'n not working perfect :( The user must be able to move to the right and a little bit diagonal, and still get a move to the right value. It's almost unpossible to move your finger that straight. I find it extra hard to get a value moving dowmnvards. I didn't thought this should be so hard to do!? Could there be further improvements or any other ideas how to solve this? Thanks for your efforts! – 3D-kreativ May 25 '13 at 08:37
  • It would help to see actual values that are being output for various moves. Then you could accurately tweak the threshold. Another idea I just thought about is using a ratio of movement left vs up or down. So if you moved 25 pixels left and five up 25/5 > 5/25 with a threshold that requires you to move twice as far left as up or down. This would handle short moves more effectively. – Larry McKenzie May 25 '13 at 08:44
  • You might also consider using a GestureListener. – Larry McKenzie May 25 '13 at 08:47
  • A ratio like you write could be better, do you have time to make an update in your code? – 3D-kreativ May 25 '13 at 08:48
  • I have tested a GestureListener, but from what I found in the Internet, it's very complicated to use with a surfaceView – 3D-kreativ May 25 '13 at 08:48
  • Added a ratio example you will likely need to tweak a bit. Hope it helps, I am headed to bed. – Larry McKenzie May 25 '13 at 09:04
  • Something else I thought of... You may want to change touchActionMoveStatus on a successful move.(Not after first move event.) I am not sure how many times ACTION_MOVE is called while sliding a finger. – Larry McKenzie May 25 '13 at 09:22
  • OK, I will consider that also. – 3D-kreativ May 25 '13 at 09:35
  • I made a minor change that could make a big difference. Because action move can be called several times in one move and we were canceling the move after the first call. This should enable you to increase the threshold and really fine tune the touch controls on either example. – Larry McKenzie May 26 '13 at 08:24
  • Yes I moved touchActionMoveStatus. – Larry McKenzie May 26 '13 at 13:03
  • The threshold might vary from device to device. You should get it at runtime using ViewConfiguration.get(context).getScaledTouchSlop() instead. – Bitcoin Cash - ADA enthusiast Jun 11 '15 at 03:57
3

I would choose the dimension with the LARGEST movement and completely ignore the other, for example if the move is x=10 and y=8 then only use the x dimension (i.e. left/right) and vice versa.

Also as noted by Larry McKenzie, using a threshold to ignore smaller movements is a good idea to prevent registering accidental movements that the user did not intend. Tweak the threshold value to someting that feels natural.

Here is some code using your example (only the ACTION_MOVE case):

case MotionEvent.ACTION_MOVE:
    //Log.i("test","Move");
    gameLoop.touchActionMove = true;

    if(touchActionMoveStatus) {

        touchActionMoveX = (int)event.getX();
        touchActionMoveY = (int)event.getY();

        // setup a threshold (below which no movement would occur)
        int threshold = 5;        /* tweak this as needed */ 

        // first calculate the "delta" movement amounts
        int xMove = touchActionMoveX - touchActionDownX;
        int yMove = touchActionMoveY - touchActionDownY;

        // now find the largest of the two (note that if they
        // are equal, x is assumed largest)
        if ( Math.abs( xMove ) >= Math.abs( yMove ) )  {  /* X-Axis */
           if ( xMove >= threshold )
              Log.i("test","Move Right");
           else if ( xMove <= -threshold )
              Log.i("test","Move Left");
        }
        else  {                                          /* Y-Axis */
           if ( yMove >= threshold )
              Log.i("test","Move Down");
           else if ( yMove <= -threshold )
              Log.i("test","Move Up");
        }

        touchActionMoveStatus = false; // Will be set to true when pointer is up
    }
}
break;

NOTE: As mentioned in some of the other answers, because multiple events with very small values can occur, it might be best to accumulate (i.e. sum up) the movements UNTIL the threshold is reached - you can use members for this that reset in ACTION_DOWN. Once the threshold is reached (in either dimension) THEN you can perform the checks for which direction.

Alternative Approach

Another way to go about it would be to detect the largest movement in the FIRST ACTION_MOVE event, and then lock all further movements to that dimension. For this you would need to add various state members - these would need to be updated in each state. Here is a rough example (with only the state tracking):

  // members
  private boolean axisLock = false;    /* Track When Lock is Required */
  private boolean axisX = true;        /* Axis to Lock (true) for X, (false) for Y */


  @Override
  public boolean onTouch(View v, MotionEvent event) {

      switch (event.getAction()) {

      case MotionEvent.ACTION_DOWN:

          // set this state so that ACTION_MOVE knows a lock is required
          axisLock = true;

          break;

      case MotionEvent.ACTION_UP:

          // clear the state in case no move was made
          axisLock = false;

      case MotionEvent.ACTION_MOVE:

          // now lock the axis if this is the first move event
          if ( axisLock )  {

             // this will set whether the locked axis is X (true) or Y (false)
             axisX = event.getX() >= event.getY();

             // reset the state (to keep the axis locked)
             axisLock = false;
          }

          // at this point you only need to consider the movement for the locked axis
          if ( axisX )  {
             int movement = (int)event.getX();    /* Get Movement for Locked Axis */
             // check for your movement conditions here
          }
          else  {
             int movement = (int)event.getY();    /* Get Movement for Locked Axis */
             // check for your movement conditions here
          }

          break;
      }

      return true;
  }

You could add many optimizations to this code, for now it just illustrates the basic idea.

free3dom
  • 18,729
  • 7
  • 52
  • 51
2

larry had the right idea, i just want to put in a lil fix,

//put this in the wraping class  
private static int THRESHOLD = 10;
private static int initX;
private static int initY;

@Override
public boolean onTouch(View v, MotionEvent event) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:
    initX = (int)event.getX();
    initY = (int)event.getY();
    break;

case MotionEvent.ACTION_POINTER_UP:
   //you can add in some kind of "move back" animation for the item
    break;

case MotionEvent.ACTION_MOVE:
if(((int)event.getY - initY) > THRESHOLD){
   //move down
   break;
}

if(((int)event.getY - initY) > -THRESHOLD){
   //move up
   break;
}

if(((int)event.getX - initX) > THRESHOLD){
   //move right
   break;
}

if(((int)event.getX - initX) < -THRESHOLD){
   //move left
   break;
}
break;    
}
}

i didn't test this code, only free write it, but i hope you get my idea :)

Dan Levin
  • 718
  • 7
  • 16
  • I like some of your improvements but your code could result in both a left and up movement in one swipe. I like using -threshold though. – Larry McKenzie May 25 '13 at 09:07
  • the difference in my code is that the user doesn't have to make a big movement, it will check for the accumulating movement, when you move, and if its bigger than the threshold, only then move the item, in larrys answer you would have to make a 10 pixels movement for each movement check, thats why you didnt get any results. – Dan Levin May 25 '13 at 09:07
  • how could it? if you reach the threshold then it breaks, and you can't have left and right conditions at the same time anyway – Dan Levin May 25 '13 at 09:08