Game Development Community

Pinch to Zoom and Swipe to Pan - iTGB 1.2

by Justin Mosiman · 05/28/2009 (7:27 pm) · 29 comments

Below is my resource to enable pinch to zoom and swipe to pan in iTGB 1.2. The pinch and swipe behave as you'd expect on any normal iPhone/iPod Touch application. The limitations to that are that the pinch cannot zoom in/out more than its maximum value, and the swipe does not have any momentum attached to it. While it is easy to come up with a formula to determine the momentum, there was a delay between lifting your finger and the camera moving (using setTargetCameraPosition). Because of that, I have decided not to include that code here. But if anybody gets a working fluid transition, please post it in the comments.

These additions assume you've completed Dave Calabrese's excellent multitouch resource.

Ok, lets get started.

Additions:
iPhoneOGLVideo.h (around line 75):
CGFloat currentAngle;//for knowing our current oriantion
    
    NSMutableArray *activeTouches; // stores the latest position of all of the touches

iPhoneOGLVideo.mm (around line 290):
@property (nonatomic, retain) EAGLContext *context;
@property (nonatomic, retain) NSMutableArray *activeTouches;

iPhoneOGLVideo.mm (around line 304):
@synthesize context;
@synthesize activeTouches;

iPhoneOGLVideo.mm (around line 354):
activeTouches = [[NSMutableArray alloc] init];
    
    return self;

iPhoneOGLVideo.mm (around line 456):
- (void)dealloc {
        
    if ([EAGLContext currentContext] == context) {
        [EAGLContext setCurrentContext:nil];
    }
    
    [activeTouches release];
    [context release];    
    [super dealloc];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSUInteger touchCount = [activeTouches count];
    // Enumerates through all touch objects
    for (UITouch *touch in touches){
        CGPoint point = [touch locationInView:self];
        // PUAP -Mat platform::init sets the window to 480x480 for easy rotation, which means this needs to be done to
        //keep the curser at the right point, if we are in landscape
        if( platState.portrait == false ) {
            point.y -= (480 - 320);    
        }
        createMouseDownEvent( touchCount, point.x, point.y );
        [activeTouches addObject:[NSValue valueWithCGPoint:point]];
        touchCount++;
    }
}



extern Vector<Event*> TouchMoveEvents;



- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSEnumerator *touchEnum = [activeTouches objectEnumerator];
    NSValue *touchPointValue;

    NSUInteger touchCount = 0;
    // Enumerates through all touch objects
    for (UITouch *touch in touches){
        CGPoint point = [touch locationInView:self];
        // PUAP -Mat platform::init sets the window to 480x480 for easy rotation, which means this needs to be done to
        //keep the curser at the right point, if we are in landscape
        if( platState.portrait == false ) {
            point.y -= (480 - 320);    
        }
        
        // Figure out the correct touch count
        touchCount = 0;
        while(touchPointValue = [touchEnum nextObject] ){
            if([touchPointValue CGPointValue].x == [touch previousLocationInView:self].x &&
                [touchPointValue CGPointValue].y == ([touch previousLocationInView:self].y - (platState.portrait == false ? 160 : 0))){
                [activeTouches replaceObjectAtIndex:touchCount withObject:[NSValue valueWithCGPoint:point]];
                break;
            }
            touchCount++;
        }
        createMouseMoveEvent( touchCount, point.x, point.y );
        touchEnum = [activeTouches objectEnumerator];
    }
    
 }

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSEnumerator *touchEnum = [activeTouches objectEnumerator];
    NSValue *touchPointValue;
    
    NSUInteger touchCount = 0;
    // Enumerates through all touch objects
    for (UITouch *touch in touches){
        CGPoint point = [touch locationInView:self];
        // PUAP -Mat platform::init sets the window to 480x480 for easy rotation, which means this needs to be done to
        //keep the curser at the right point, if we are in landscape
        if( platState.portrait == false ) {
            point.y -= (480 - 320);    
        }
        
        // Figure out the correct touch count
        touchCount = 0;
        while(touchPointValue = [touchEnum nextObject] ){
            if([touchPointValue CGPointValue].x == ([touch locationInView:self].x) &&
               [touchPointValue CGPointValue].y == ([touch locationInView:self].y - (platState.portrait == false ? 160 : 0))){
                [activeTouches removeObjectAtIndex:touchCount];
                break;
            }
            touchCount++;
        }
        
        createMouseUpEvent( touchCount, point.x, point.y );
        touchEnum = [activeTouches objectEnumerator];
    }
}

Modifications to Dave's resource:

iPhoneInput.mm (around line 966, 981, 995. MAKE SURE YOU MAKE THREE ADDITIONS. Adding the touchNumber to mouse move,down, and up)
event.yPos = y;
   event.action = SI_MAKE;
   event.touchNumber = touchNumber;

event.h (around line 150):
struct ScreenTouchEvent : public Event  
{  
   S32 xPos, yPos;  
   U8    action;
   S32   touchNumber;

The following are some changes from Dave's resource. While Dave's resource would work well for more than two touches, we only want two, so I'm calling the first touch onMouseDown and the second onRightMouseDown.

EDIT: Forgot to add mMouseRightButtonDown's and mMouseRightButtonEvent's declaration, thank you Nathan.

guiCanvas.h (around line 101):
bool                       mMouseButtonDown;       ///< Flag to determine if the button is depressed
   bool                       mMouseRightButtonDown;  ///< Flag to determine if the right button is depressed
   bool                       mMouseMiddleButtonDown; ///< Middle button flag
   GuiEvent                   mLastEvent;
   GuiEvent                   mMouseRightButtonEvent;

Still guiCanvas.h, I kept the method Dave created, processScreenTouchEvent, but I removed rootScreenTouchUp and rootScreenTouchDown.

Edit: another piece of the code that I forgot to add
guiCanvas.cc (around line 297):
mMouseButtonDown = false;
   mMouseRightButtonDown = false;
   mMouseMiddleButtonDown = false;

guiCanvas.cc (around line 425):
void GuiCanvas::processScreenTouchEvent(const ScreenTouchEvent *event)
{
   //copy the cursor point into the event
   mLastEvent.mousePoint.x = S32(event->xPos);
   mLastEvent.mousePoint.y = S32(event->yPos);
	
   //see if button was pressed
   if (event->action == SI_MAKE)
   {
      //  printf("Touch number %d ",event->touchNumber);
      U32 curTime = Platform::getVirtualMilliseconds();
	  mNextMouseTime = curTime + mInitialMouseDelay;
		
	  mLastMouseDownTime = curTime;
	  mLastEvent.mouseClickCount = mLastMouseClickCount;
	   
      if(event->touchNumber == 0){
         if(mMouseButtonDown)
            rootMouseDragged(mLastEvent);
         else
            rootMouseDown(mLastEvent);
	  }else if(event->touchNumber == 1){
         if(mMouseRightButtonDown)
            rootRightMouseDragged(mLastEvent);
         else
            rootRightMouseDown(mLastEvent);
	  }
   }
   //else button was released
   else
   {
      if(event->touchNumber == 0){
         rootMouseUp(mLastEvent);
		 
		 if(mMouseRightButtonDown){
			// If the right mouse button is down, it now becomes the mouse button.  Therefore, we need to let the right mouse method know that
			// we are leaving it and instead going to the first mouse button.
			rootRightMouseUp(mMouseRightButtonEvent);
		    rootMouseDown(mMouseRightButtonEvent);
            mMouseRightButtonDown = false;
		 }
      }else if(event->touchNumber == 1){
         rootRightMouseUp(mLastEvent);
      }
      if(!mMouseButtonDown && !mMouseRightButtonDown)
         mNextMouseTime = 0xFFFFFFFF;
   }
}

guiCanvas.cc (around line 892):
void GuiCanvas::rootRightMouseDown(const GuiEvent &event)
{
   mPrevMouseTime = Platform::getVirtualMilliseconds();
   mMouseRightButtonDown = true;
   mMouseRightButtonEvent = event;

guiCanvas.cc (around line 926):
void GuiCanvas::rootRightMouseDragged(const GuiEvent &event)
{
   mPrevMouseTime = Platform::getVirtualMilliseconds();
   mMouseRightButtonEvent = event;

The last part of my additions are a complete new class, which I called t2dPlaySceneWindow. Create a new file called t2dPlaySceneWindow.h and t2dPlaySceneWindow.cc in your project under the folder T2D. Then add the following:

t2dPlaySceneWindow.h:
//-----------------------------------------------------------------------------
// 2D Play Scene Window - the window used to actually play the game. 
// Players can pan the camera using the swipe motion and zoom by pinching the window.
//-----------------------------------------------------------------------------

#ifndef _T2DPLAYSCENEWINDOW_H_
#define _T2DPLAYSCENEWINDOW_H_

#ifndef _T2DSCENEWINDOW_H_
#include "./t2dSceneWindow.h"
#endif


class t2dPlaySceneWindow : public t2dSceneWindow
{
    typedef t2dSceneWindow Parent;

private:
	// Although technically we aren't dealing with mouse clicks, I am treating the first finger to touch the screen as mMousePoint
	// and the second mRightMousePoint
	Point2I mMousePoint;
	Point2I mRightMousePoint;
	bool mRightMouseDown;
	t2dVector mScenePrevMousePoint; // The scene coordinates are used for object selection and panning
	
	// Camera swipe/pan
	Point2I mMinCameraViewLimit;
	Point2I mMaxCameraViewLimit;
	
	// Camera pinch/zoom
	F32 mPreviousPinchDistance;
	F32 mMinCameraZoomLimit;
	F32 mMaxCameraZoomLimit;

public:

	t2dPlaySceneWindow();

	static void initPersistFields();

	virtual bool onAdd();

    void onMouseDown( const GuiEvent& event );
    void onMouseDragged( const GuiEvent& event );
	void onMouseUp( const GuiEvent& event );
    void onRightMouseDown( const GuiEvent& event );
	void onRightMouseDragged( const GuiEvent& event );
	void onRightMouseUp( const GuiEvent& event );
	
	void pinchZoom();

    /// Declare Console Object.
    DECLARE_CONOBJECT(t2dPlaySceneWindow);
};

#endif // _T2DPLAYSCENEWINDOW_H_

t2dPlaySceneWindow.cc:
#include "./t2dPlaySceneWindow.h"

IMPLEMENT_CONOBJECT(t2dPlaySceneWindow);

// declare the functions first, a simple 2D distance function
F32 distance(Point2I mousePoint, Point2I rightMousePoint)
{	
	return sqrt(pow(mousePoint.x - rightMousePoint.x,2)+pow(mousePoint.y - rightMousePoint.y,2));
}

t2dPlaySceneWindow::t2dPlaySceneWindow()
{
	
	mMousePoint = Point2I(0,0);
	mRightMousePoint = Point2I(0,0);
	mRightMouseDown = false;
	
	mMinCameraViewLimit.set(0,0);
	mMaxCameraViewLimit.set(0,0);
	
	mPreviousPinchDistance = 0.0f;
	mMinCameraZoomLimit = 0.1f;
	mMaxCameraZoomLimit = 1.0f;
}

//-----------------------------------------------------------------------------
// Initialise Persistent Fields.
//-----------------------------------------------------------------------------
void t2dPlaySceneWindow::initPersistFields()
{
    // Call Parent.
   Parent::initPersistFields();

   // Add Fields.
   addField( "MinCameraViewLimit", TypePoint2I, Offset(mMinCameraViewLimit, t2dPlaySceneWindow) );
   addField( "MaxCameraViewLimit", TypePoint2I, Offset(mMaxCameraViewLimit, t2dPlaySceneWindow) );
}

//-----------------------------------------------------------------------------
// OnAdd
//-----------------------------------------------------------------------------
bool t2dPlaySceneWindow::onAdd()
{
    if(!Parent::onAdd())
        return false;
	
    // If the min camera view limit is not the same as the max, we have a camera limit
	if(mMinCameraViewLimit != mMaxCameraViewLimit){
		setViewLimitOn(t2dVector(mMinCameraViewLimit.x,mMinCameraViewLimit.y),t2dVector(mMaxCameraViewLimit.x,mMaxCameraViewLimit.y));
	}
    return true;
}

//-----------------------------------------------------------------------------
// Mouse Event Handler.
//-----------------------------------------------------------------------------

void t2dPlaySceneWindow::onMouseDown( const GuiEvent& event )
{
	Parent::onMouseDown(event);
	
	mMousePoint = event.mousePoint;		
}

void t2dPlaySceneWindow::onMouseDragged( const GuiEvent& event )
{
	Parent::onMouseDragged(event);

	if(mRightMouseDown){
		// perform a pinch
		mMousePoint = event.mousePoint;	
		pinchZoom();
	}else{		
		// perform a swipe
		t2dVector sceneMousePoint;
		windowToSceneCoord(t2dVector(event.mousePoint.x,event.mousePoint.y), sceneMousePoint);
		windowToSceneCoord(t2dVector(mMousePoint.x,mMousePoint.y), mScenePrevMousePoint);
		
		setTargetCameraPosition(t2dVector(getCurrentCameraPosition().mX+(1.1*(mScenePrevMousePoint.mX - sceneMousePoint.mX)),getCurrentCameraPosition().mY),
								getCurrentCameraWidth(),getCurrentCameraHeight());
		startCameraMove( 0.0f );
		mMousePoint = event.mousePoint;	
	}
}

void t2dPlaySceneWindow::onMouseUp( const GuiEvent& event )
{
	Parent::onMouseUp(event);
	
	// The right mouse button could still be down if you lifted your first finger up before your second.
	// The second finger now becomes the first finger (onMouseDown will be called for the new first finger)
	if(mRightMouseDown){
		mRightMouseDown = false;
	}
}

void t2dPlaySceneWindow::onRightMouseDown( const GuiEvent& event )
{
	Parent::onRightMouseDown(event);
	
	mRightMousePoint = event.mousePoint;
	mRightMouseDown = true;
	
	mPreviousPinchDistance = distance(mMousePoint, mRightMousePoint);
}

void t2dPlaySceneWindow::onRightMouseDragged( const GuiEvent& event )
{
	Parent::onRightMouseDragged(event);
	
	mRightMousePoint = event.mousePoint;
	
	// If the right mouse was dragged, we have a left mouse so definitely need to do a pinch zoom
	pinchZoom();
}

void t2dPlaySceneWindow::onRightMouseUp( const GuiEvent& event )
{
	Parent::onRightMouseUp(event);
	
	mRightMouseDown = false;
}

void t2dPlaySceneWindow::pinchZoom()
{
	F32 currentPinchDistance = distance(mMousePoint, mRightMousePoint);
	F32 targetZoom = getCurrentCameraZoom();
	
	if(currentPinchDistance < mPreviousPinchDistance){
		// Zoom out
		F32 zoomLength = getCurrentCameraZoom() - mMinCameraZoomLimit;
		targetZoom -= 0.0002 * zoomLength * currentPinchDistance;
	}else if(currentPinchDistance > mPreviousPinchDistance){
		// Zoom in
		F32 zoomLength = mMaxCameraZoomLimit - getCurrentCameraZoom();
		targetZoom += 0.0002 * zoomLength * currentPinchDistance;
	}
	
	if(targetZoom < mMinCameraZoomLimit)
		targetZoom = mMinCameraZoomLimit;
	else if(targetZoom > mMaxCameraZoomLimit)
		targetZoom = mMaxCameraZoomLimit;
	
	mPreviousPinchDistance = currentPinchDistance;
	
	setTargetCameraZoom(targetZoom);
	startCameraMove( 0.0f );
}

In your GUI file, you're going to have to change t2dSceneWindow to t2dPlaySceneWindow.

I wasn't able to figure out a perfect algorithm for the pinch, so you'll probably have to modify the constant (line 132/136) to suite your needs based off of how close and far you want to allow (mMinCameraZoomLimit and mMaxCameraZoomLimit).

If I missed anything or you see any improvements that can be made, please let me know. Otherwise, we have pinch to zoom and swipe to pan on the iPhone!!

About the author

Recent Blogs

Page«First 1 2 Next»
#21
09/07/2009 (12:50 am)
Apparently I suck at creating resources, thank you for finding this error.

mRightMouseLastEvent should be mMouseRightButtonEvent, I've updated the resource, but you'll need to replace it in rootRightMouseDown and rootRightMouseDragged.

Let me know if you have any other problems.
#22
09/07/2009 (7:57 am)
Thanks, error has gone, but now debugger say: "error: t2dGroundUnit.h: No such file or directory".
And pinch zoom works only few times, then nothing works, even simple touch on objects and I need to restart the game.
#23
09/07/2009 (11:56 am)
Oops, remove the include of t2dGroundUnit from t2dPlaySceneWindow.h.

Dave was having the same problem as you with it only working for a little while and then not working. I just sent him an email to see if he somehow got it to work, I don't have any idea what is wrong.
#24
10/17/2009 (3:48 pm)
Justin & Dave, thanks for your awesome resources! Unfortunately I'm another person who it stops working after a couple inputs. I'll see if I can figure anything out but I'm under the impression both Dave and Justin are way more experienced at this than I am. :(
#25
10/17/2009 (4:05 pm)
Hey Josh,

I was working with Dave to try to get it working about a month ago. But we were having some problems and I wasn't able to dedicate enough time to it since I've been trying to finish up my game. I am in the testing phase of the game so hopefully I will be able to submit by the end of next week.

After that I'll create a new project from a fresh version of iTGB1.2 (iTGB 1.3 beta modifies some of the onMouse code, so I'd rather work with what I know works) and post all of the files that I modified to get it working.

Justin
#26
10/17/2009 (6:13 pm)
Awesome, thanks Justin I appreciate you keeping up with this. This code will help tremendously for a strategy game I am building. If I can be useful on this just let me know. Perhaps you can give me your attempt at momentum? I don't mind seeing if I can make that work properly.

Josh

Edit: my email if you don't mind sending me your try on the swipe momentum: josh at arrivalgame.com
#27
10/17/2009 (6:48 pm)
I'm wrapping up a huge project milestone right now as well. Next week I am going to be in Texas as I'm doing a seminar on developing video games at Lee College. However, the following week, I can most certainly devote some time to helping Justin nail down the final problems with this. (I've actually also got a ton of other code I'd love to work on getting into the official head build, as we've made numerous improvements and modifications to iTorque2D while developing Shinobi Ninja Attacks). Justin, ping me sometime soon and we'll work out a few days to make this happen.
#28
11/26/2009 (7:18 am)
Hey, wondering if anyone has been able to look at this. If it helps I would be willing to pay something to have this finished off. I need it for a game and my deadline is coming up soon so panic is starting to creep in. :)

Thanks,
Josh
#29
02/13/2011 (10:33 am)
Is this zoom still relevant? usable, effective?
Page«First 1 2 Next»