!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> » Taming Touch / Multi-Touch on the iPhone -- Mobile Perspectives

Taming Touch / Multi-Touch on the iPhone



By deans ~ June 23rd, 2009. Filed under: Resources.

Gameplay in iPuck (App Store) is straightforward.  The player manipulates his/her sprite to knock the puck into the opponent’s goal, while guarding their own goal.  When I transitioned to Chipmunk for the 2D physics modeling, I had an opportunity to manifest two different behaviors as the player sprite is dragged.  “Great,” I thought, “I’ll have it behave one way when the player is dragging with one finger, and the other way when dragging with two fingers.”  Sounds good, right?

Unfortunately, it didn’t work.  I ended up writing a bunch of needlessly complex code that sort of emulated the behavior that I wanted.  I almost decided to abandon the functionality because I couldn’t get it to work reliably in all situations.  I knew that there had to be a better way to solve this problem.

Some of the challenges include:

  • First, touchesBegan:withEvent: gets called every time a finger touches the screen.  Thus, if the second finger touches just a tiny bit later than the first, touchesBegan:withEvent: actually gets called with two separate single touchesBegan events.  It’s easy enough to have a test to see whether the app is already processing a touch, but there are potential reliability issues with this because I have to guarantee that the touch state will always be cleaned up, or I might not be able recognize a legitimate touchesBegan:withEvent: call.
     
  • Second, when the set of touches is presented to touchesMoved:withEvent:, a given set may contain a touch point from either finger, or both fingers.  If I get touches for both fingers, the order is predictable (first finger’s touch in element 0, second finger’s touch in element 1), within the scope of a single touch sequence (down, move, up).  However, if I only get one touch point, it could be from either finger, so I thought that my code had to try to sort out whether it could process the point, or not, by filtering for an “acceptable” delta (I saw this technique in some sample code).  I could use a heuristic technique to do this, but it’s really just a best guess.  Of course, this filtering can make for confusing gameplay, because the user thinks that they’re moving their the puck sprite, but if the “master” finger isn’t moving as fast as the “secondary” finger, the sprite may not behave as they are expecting.
     
  • Finally, each time the player lifts either, or both, finger(s), touchesEnded:withEvent: gets called, and I had to figure out which finger had actually lifted so that I could take the appropriate action.

After stewing about the problem overnight, I dug into Apple’s documentation and found the answer.  Unfortunately, the code samples that I referenced when I wrote iPuck completely ignore this method.  In the discussion below, I’m focusing on two fingered multi-touches, because that’s the simplest scenario.  Everything that I say, however, extends completely to n finger touches.

Most of the sample code running around handled single touches in a fashion something like this:

NOTE: THIS ONLY WORKS RELIABLY IF YOUR VIEW DOES NOT HAVE MULTITOUCH ENABLED


- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    // get the first touch
    UITouch *touch = [touches anyObject];
    if ([touch view] == myView) {
        CGPoint touchPt = [[touches anyObject] locationInView:myView];
        // do something with the point

    }
}


- (void)touchesMoved: (NSSet*)touches withEvent: (UIEvent*)event {
    // drag the sprite object along.
    CGPoint pt = [[touches anyObject] locationInView:myView];
    // do something with the new pt.
}


- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    // If the touch was in myView, handle it.
    UITouch *touch = [touches anyObject];
    if ([touch view] == myView) {
        // clean up
    }
}


- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    // If the touch was in the myView, handle it.
    UITouch *touch = [touches anyObject];
    if ([touch view] == myView) {
        // clean up
    }
}

My first effort, as described above, was to simply convert the set of touches into an array and try to figure out what I was getting.


    NSArray *touchArr = [touches allObjects];
    NSInteger touchCnt = [touchArr count];
    UITouch *newTouch;
    CGPoint pt;
    if (touchCnt >= 1) {
        newTouch = [touchArr objectAtIndex:0];
        pt1 = [newTouch locationInView:myView];
        // do something with pt1
    }

Sadly, this didn’t work very well, so I went to Apple’s excellent documentation.  The answer to my problem is right there, in the UITouch Class Reference document:

A UITouch object is persistent throughout a multi-touch sequence.

That’s it!

I quickly changed my code to keep track of the specific UITouch objects.  We can organize these touch objects in a number of ways, including static module variables, an array of UITouch*‘s or an NSMutableDictionary (indexed by the touch objects’ IDs), depending on the specific requirement of the situation.

This allows me to do things like


static UITouch *g_firstTouch = nil;


- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    // get the first touch
    NSArray *touchArr = [touches allObjects];
    NSInteger touchCnt = [touchArr count];
    UITouch *aTouch = [touchArr objectAtIndex:0];


        if (([aTouch view] == myView) && (g_firstTouch == nil)) {
            // keep track of the first touch object in the view.
            // this touch will represent this finger for the duration
            // of the sequence
            g_firstTouch = aTouch;
            // more stuff.

code to loop through the touchArr doing other processing not shown

- (void)touchesMoved: (NSSet*)touches withEvent: (UIEvent*)event {

looping code not shown
        if (aTouch == g_firstTouch) {
            //do something apppropriate

or
    if (firstTouchNotInSet) {
        // the touchesMoved method was called, but our first touch
        // wasn’t in the set, use its current data
        CGPoint pt1 = [g_firstTouch locationInView:myView];
        // do something with pt1



- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

looping code not shown
        if (aTouch == g_firstTouch) {
            // the first touch is over, do something appropriate.

 
Notes:

  • I extracted this code from our apps, and tried to make it generic to illustrate the points.  I didn’t recompile/retest the extracted code, so I may have botched the process a little bit – apologies in advance
  • Don’t forget that you have to enable multi-touch, it’s disabled by default.  You’ll need to set the multipleTouchEnabled property:  [myView setMultipleTouchEnabled:Yes];
  • The timestamp property of UIEvent can provide another way to organize your app’s events
  • For sprite movement, I typically have to apply deltas to the previous sprite location, I’ve always kept track of the previous touch point within my touchesMoved:withEvent method.  It turns out that this isn’t strictly necessary, as UITouch has the
    - (CGPoint)previousLocationInView:(UIView *)view
    method, which provides the exact same functionality – I haven’t verified the performance impact, but it could simplify your code.
  • After all of this, I decided that I didn’t like two-finger dragging for controlling the player sprite. Multi-touch is great for swipes and gestures, but it doesn’t seem to lend itself to the quick movements and rapid direction changes needed to play the game, so I came up with a completely different interface implementation for v1.1 of iPuck.

 
Bottom Line:

We can take advantage of the fact that each specific UITouch object spans the duration of a down / drag / up operation to unambiguously identify each touch and track it from touchesBegan: through touchesMoved: and take appropriate action at touchesEnded:.  This allows us to reliably track the movement of each individual finger through the touch sequence and to respond appropriately to calls when not all of the fingers are represented in the touch set.

 
——–
Technorati Tags:  , , , , ,

2 Responses to Taming Touch / Multi-Touch on the iPhone

  1. TMcB

    Excellent article, I was having the exact same problem – now I know the touches are persistent its all sorted!

  2. deans

    Hi TMcB- I’m glad that we could help!