6

I am currently writing a game using the fantastic monogame framework. I am having trouble reacting to touch input correctly. When a user drags horizontally or vertically I want to perform an action once per drag. The problem is the gesture is getting issued multiple times for each drag. Here is my code:

var gesture = default(GestureSample);

while (TouchPanel.IsGestureAvailable)
    gesture = TouchPanel.ReadGesture();

if (gesture.GestureType == GestureType.VerticalDrag)
{
    if (gesture.Delta.Y < 0)
        return new RotateLeftCommand(_gameController);

    if (gesture.Delta.Y > 0)
        return new RotateRightCommand(_gameController);
}

if (gesture.GestureType == GestureType.HorizontalDrag)
{
    if (gesture.Delta.X < 0)
        return new RotateLeftCommand(_gameController);

    if (gesture.Delta.X > 0)
        return new RotateRightCommand(_gameController);
}

I have moved all of this code into one block for the purposes of this example. The RotateRightCommand or RotateLeftCommand that is returned is then executed by the calling code which rotates the object on the screen. This whole block of code is being run in the update loop in monogame. I think I am missing something in terms of resetting the touch input and that's why I'm getting 3 or 4 RotateCommands returned for one drag. What am I doing wrong?

Kevin Holditch
  • 5,165
  • 3
  • 19
  • 35

2 Answers2

16

Edit: You're not using gestures correctly here. A drag gesture is generated every time a finger moves across the screen. If you move your finger from one side of the screen to the other (Very quickly), you'll get a lot of small drag gestures. This is how XNA and MonoGame work.

This is the logic you're looking for:

var touchCol = TouchPanel.GetState();

foreach (var touch in touchCol)
{
    // You're looking for when they finish a drag, so only check
    // released touches.
    if (touch.State != TouchState.Released)
        continue;

    TouchLocation prevLoc;

    // Sometimes TryGetPreviousLocation can fail. Bail out early if this happened
    // or if the last state didn't move
    if (!touch.TryGetPreviousLocation(out prevLoc) || prevLoc.State != TouchState.Moved)
        continue;

    // get your delta
    var delta = touch.Position - prevLoc.Position ;

    // Usually you don't want to do something if the user drags 1 pixel.
    if (delta.LengthSquared() < YOUR_DRAG_TOLERANCE)
        continue;

    if (delta.X < 0 || delta.Y < 0)
        return new RotateLeftCommand(_gameController);

    if (delta.X > 0 || delta.Y > 0)
        return new RotateRightCommand(_gameController);   
}

If you'd rather do gestures, you can just do this:

var gesture = default(GestureSample);

while (TouchPanel.IsGestureAvailable)
{
    gesture = TouchPanel.ReadGesture();

    if (gesture.GestureType == GestureType.DragComplete)
    {
        if (gesture.Delta.Y < 0 || gesture.Delta.X < 0)
            return new RotateLeftCommand(_gameController);
        if (gesture.Delta.Y > 0 || gesture.Delta.X > 0)
            return new RotateRightCommand(_gameController);
    }
}

Either way will give you the behavior you're looking for. You still need to put the logic inside of the loop as I said in the original post, because you can have multiple gestures queued up, and you don't want to assume that the last one in the stack is the drag. (Thanks to multitouch you can have drags, taps, drag complete, etc).

I'm not 100% sure if this is your problem... but your logic is wrong here. The CodeFormatting boss is beating me here, but it should be more like this:

var gesture = default(GestureSample);

while (TouchPanel.IsGestureAvailable)
{
    gesture = TouchPanel.ReadGesture();

    if (gesture.GestureType == GestureType.VerticalDrag)
    {
        if (gesture.Delta.Y < 0)
            return new RotateLeftCommand(_gameController);
        if (gesture.Delta.Y > 0)
            return new RotateRightCommand(_gameController);
    }

    if (gesture.GestureType == GestureType.HorizontalDrag)
    {
        if (gesture.Delta.X < 0)
            return new RotateLeftCommand(_gameController);
        if (gesture.Delta.X > 0)
            return new RotateRightCommand(_gameController);
    }
}

By not putting the TouchPanel.ReadGesure() inside of a loop, you're only reading one gesure per frame. The available gestures are put into a queue, and only removed when ReadGesture() is called. Therefore if your framerate hiccups, or if you can't read a gesture as fast as you the OS can read it (Which is very possible), you'll be working with old information. You can see that here. For this application, I suggest taking each gesture, and combining it into one or two, then deciding what you should do based on that.

RayBatts
  • 255
  • 1
  • 9
  • Thanks for your feedback. My read gesture is in the while loop though. I have just ommited the braces as its a single line while loop. I thought that by doing that it would stay in the while loop whilst there is a gesture available. Unless I'm missing something :S – Kevin Holditch Mar 01 '13 at 11:10
  • 1
    You are missing something. Both the gesture reading and processing needs to be in the same while loop. What the code you posted does is read gestures until there are no more available, then process the last one it read. If there were 3 gestures in the queue, you'd throw the first two away and then process the third. What you should be doing is reading the first, process it. Read the second, process it, etc. You're throwing away data, which can easily be part of the problem. Edit: What platform are you working with? I wrote a lot of MonoGame's input code and can take a look into it. – RayBatts Mar 01 '13 at 22:40
  • 1
    Thanks so much for your help Ray that has sorted it. The problem was like you correctly pointed out my mis-understanding of the way that gestures worked. I thought that it would only give you complete gestures. The problem I was seeing was that a long drag was being interpreted as 3 or 4 smaller drags like you said. I wasn't aware that you had to code for this. Thank you so much for your help. – Kevin Holditch Mar 04 '13 at 08:49
  • I've just tried plugging in this code and I'm having a problem because the Delta is always 0, 0 for the DragComplete event. So I am having to remember the event before and use the delta from that. I'm not sure if that's intentional or not so thought I'd point it out. – Kevin Holditch Apr 30 '13 at 21:01
  • @KevinHolditch Logically that makes sense. `DragComplete` will occur when the finger is no longer moving, aka zero delta. If the finger is lifted while still moving(above a threshold) then it would be considered a `Swipe` or `'Flick`. – Jim Buck May 20 '13 at 19:54
2

I guess that depends on what you consider one drag. MonoGame and XNA consider the detection of change in touch input from one update to the next a drag. So if you are thinking that you will receive one Gesture from the time you touch your finger to the screen until you remove it, that would be incorrect in XNA and MonoGame terms. The Gesture that gets returned is the Delta from T-1 to T where T is Now and T-1 is the previous call to Update.

borrillis
  • 656
  • 4
  • 7
  • Thanks for your help. I though that may be the case which is why I put the while loop at the top. I'm presuming that if you drag continously (up the entire length of the screen) that the IsGestureAvailable will return true for the whole movement. In which case the drag should only be recorded once by the above code. That is why I am confused that when you perform a drag such as the one I described you get 3 or 4 commands returned by the code above. – Kevin Holditch Feb 07 '13 at 08:38
  • Try including your "if" statements in the "while" along with the ReadGesture(). I saw better results with that. However, it still does not wait until you are done dragging. VerticalDrag and HorizontalDrag are just constrained versions of FreeDrag and so you'll get multiple gestures if you do a long swipe along the screen. – borrillis Feb 07 '13 at 23:38