MonoGame reading touch gestures

Kevin Holditch picture Kevin Holditch · Feb 2, 2013 · Viewed 13.6k times · Source

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?

Answer

RayBatts picture RayBatts · Mar 1, 2013

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.