Wednesday 10 August 2011

Updated, how to scroll and pinch-zoom a tiled map

So, silly me, I didn't realise the original code I posted earlier in this blog doesn't work for big maps. This is because in big maps, the layer slides down to the bottom left, which would ruin the boundaries.

 So instead I came up with a method where you get the top right and the bottom left corners of the tiled map, finds their co-ordinates, then converts it to world space.

From here, the boundaries are drawn up where in the code below, I have taken into account a 40x320 UI bar running up the right side of the screen. My code is tried and tested on tiled maps of varying size, hope it will help someone.

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

{
    
    
    if (hasPickedButton == NO) {
        
        // "allObjects" returns an NSArray of all the objects in the set
        NSArray *touchArray = [touches allObjects];
        
        
        
        // Only run the following code if there is more than one touch, wanting to resize
        if ([touchArray count] > 1)
        {
            
            
            // We're going to track the first two touches (i.e. first two fingers)
            // Create "UITouch" objects representing each touch
            UITouch *fingerOne = [touchArray objectAtIndex:0];
            UITouch *fingerTwo = [touchArray objectAtIndex:1];
            
            // Convert each UITouch object to a CGPoint, which has x/y coordinates we can actually use
            CGPoint pointOne = [fingerOne locationInView:[fingerOne view]];
            CGPoint pointTwo = [fingerTwo locationInView:[fingerTwo view]];
            
            CGPoint pointOnePrev = [fingerOne previousLocationInView:[fingerOne view]];
            CGPoint pointTwoPrev = [fingerTwo previousLocationInView:[fingerTwo view]];
            
            // The touch points are always in "portrait" coordinates
            // You will need to convert them if in landscape (which we are)
            pointOne = [[CCDirector sharedDirector] convertToGL:pointOne];
            pointTwo = [[CCDirector sharedDirector] convertToGL:pointTwo];
            
            pointOnePrev = [[CCDirector sharedDirector] convertToGL:pointOnePrev];
            pointTwoPrev = [[CCDirector sharedDirector] convertToGL:pointTwoPrev];
            
            CGPoint point1Map = [mapLayer convertToNodeSpace:pointOne];
            CGPoint point2Map = [mapLayer convertToNodeSpace:pointTwo];
            CGPoint midPoint = ccpMidpoint(point1Map, point2Map);
            
            mapLayer.anchorPoint = ccp(midPoint.x/mapWidth, midPoint.y/mapWidth);
            
            
            //this statement finds the difference in the pinches that the player is doing now, and the pinch the player was doing
            
            //if they were pinching in, subtract the distance of the pinch from the distance of the previous pinch
            
            if ((sqrt(pow(pointOnePrev.x - pointTwoPrev.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) > sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) {
                distance -= sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))/100;
                distance = fabsf(distance);
            }
            //otherwise they are pinching out and so add on more to the distance
            else  if ((sqrt(pow(pointOnePrev.x - pointTwoPrev.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) < sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))) {
                distance += sqrt(pow(pointOne.x - pointTwo.x, 2.0) + pow(pointOne.y - pointTwo.y, 2.0))/100;
                distance = fabsf(distance);
            }
            
            // Get the distance between the touch points
            
            // Scale the distance based on the overall width of the screen (multiplied by a constant, just for effect)
            mapScale = distance / [CCDirector sharedDirector].winSize.width * 3;
            
            
            //we don't want the player to be able to zoom out to the point where the map is smaller than the screen
            float minFactor = MAX(440.0/mapWidth, 320.0/mapHeight);
            
            
            if (mapScale <= minFactor) {
                //set this here, so if the player keeps zooming out out out, even though it doesn't change the scale factor, the distance is always decremented. this line keeps it to the last distance that had any effect, stops jumping camera
                distance = lastGoodDistance;
            }
            //we don't want him zooming in larger than the original image
            
            if (mapScale>1.0) {
                distance = lastGoodDistance;
            }
            
            if ((mapScale< 1.0) && (mapScale > minFactor)){
                
                //set the last distance which had any affect to be the current one
                
                lastGoodDistance= distance;

                //allow the zoom to take place, but don't permentatly apply it just yet, we want to make sure the final scaled map is within the bounds of the view before moving it (otherwise it appears shakey)
                float oldscale = mapLayer.scale;
                [mapLayer setScale:mapScale];
                          //get the co-ordinates of the BL and TR tiles for this scale in wordspace, then put scale back to normal for now
                
                CGSize mapTiles = [levelMap mapSize];
                float y = mapTiles.height;
                float x = mapTiles.width;
                
                CCSprite * grid = [mapBgLayer tileAt:ccp(0, y-1)];
                CGPoint bottomLeftGrid = grid.position;
                bottomLeftGrid = [levelMap convertToWorldSpace:bottomLeftGrid];
                
                grid = [mapBgLayer tileAt:ccp(x-1, 0)];
                CGPoint topRightGrid = grid.position;
                
                topRightGrid = [levelMap convertToWorldSpace:topRightGrid];
                
                          //restore the old mapscale until we sort out the position
                mapLayer.scale = oldscale;
                
                //check to see if the layer is drifting off the screen while the zoom is taking place
                
                CGPoint difference = ccp(0, 0);
                
                float degreeOfAwkard = (1- mapScale)*10;
                
                float awkardOffsetX = degreeOfAwkard *3.51;
                float awkardOffsetY = degreeOfAwkard *3.5;
                
                if (bottomLeftGrid.x > 0) {
                    
                    float offBy = 0 - bottomLeftGrid.x;
                    difference.x += offBy;
                    
                }
                
                if (bottomLeftGrid.y > 0) {
                    float offBy = 0 - bottomLeftGrid.y;
                    difference.y += offBy;
                    
                }
                
                if (topRightGrid.x < 400 + awkardOffsetX) {
                    
                    float offBy = 400 - topRightGrid.x + awkardOffsetX;
                    difference.x += offBy;
                }
                
                if (topRightGrid.y < 280 + awkardOffsetY) {
                    float offBy = 280 - topRightGrid.y + awkardOffsetY;
                    difference.y += offBy;
                    
                }
                
                
                mapLayer.position = ccpAdd(mapLayer.position, difference);
                //      [mapLayer runAction:[CCMoveBy actionWithDuration:0.05 position:difference]];
                mapLayer.scale = mapScale;
                
                
            }
            
        }  
        
        
        if ([touchArray count] ==1){
            
            CGSize mapTiles = [levelMap mapSize];
            float y = mapTiles.height;
            float x = mapTiles.width;
            
            CCSprite * grid = [mapBgLayer tileAt:ccp(0, y-1)];
            CGPoint bottomLeftGrid = grid.position;
            bottomLeftGrid = [levelMap convertToWorldSpace:bottomLeftGrid];
            
            grid = [mapBgLayer tileAt:ccp(x-1, 0)];
            CGPoint topRightGrid = grid.position;
            
            topRightGrid = [levelMap convertToWorldSpace:topRightGrid];
            
            
            UITouch * fingerOne = [touchArray objectAtIndex:0];
            
            CGPoint newTouchLocation = [fingerOne locationInView:[fingerOne view]];
            newTouchLocation = [[CCDirector sharedDirector] convertToGL:newTouchLocation];
            
            CGPoint oldTouchLocation = [fingerOne previousLocationInView:fingerOne.view];
            oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation];
            
            //get the difference in the finger touches when the player was dragging
            CGPoint difference = ccpSub(newTouchLocation, oldTouchLocation);
            
            //adds this on to the layers current position, effectively moving it
            CGPoint bottomLeft = ccpAdd(mapLayer.position, difference);
            
            //check to see if the map edges of the map are showing in the screen, if so bringing them back on the view so no black space can be seen
            
            bottomLeft = ccpAdd(mapLayer.position, difference);
            
            bottomLeftGrid = ccpAdd(bottomLeftGrid, difference);
            
            
            topRightGrid = ccpAdd(topRightGrid, difference);
            //don't ask why, but the boundary changes by about 3pxls per 0.1 scale
            float degreeOfAwkard = (1- mapScale)*10;
            
            float awkardOffsetX = degreeOfAwkard *3.51;
            float awkardOffsetY = degreeOfAwkard *3.5;
            
            
            if (bottomLeftGrid.x > 0) {
                //finds how much the map is off the boundary
                float offBy = 0 - bottomLeftGrid.x;
                difference.x += offBy;
                
            }
            
            if (bottomLeftGrid.y > 0) {
                float offBy = 0 - bottomLeftGrid.y;
                difference.y += offBy;
                
            }
            
            if (topRightGrid.x < 400 + awkardOffsetX) {
                
                float offBy = 400 - topRightGrid.x + awkardOffsetX;
                difference.x += offBy;
            }
            
            if (topRightGrid.y < 280 + awkardOffsetY) {
                float offBy = 280 - topRightGrid.y + awkardOffsetY;
                difference.y += offBy;
                
            }
            //adjusts the map so it is within the boundary just
            mapLayer.position = ccpAdd(mapLayer.position, difference);
            
        }
        
    }
}

1 comment: