cocos2d游戏地图的放大、缩小、移动

晨曦之光 发布于 2012/05/28 16:46
阅读 1K+
收藏 1

http://www.cnblogs.com/dingwenjie/archive/2012/03/28/2419805.html


    在我刚学习cocos2d的时候,我是通过子龙山人的blog: http://www.cnblogs.com/andyque/MyPosts.html学到了许多知识,也是这个blog和知易网的教程把我带入了cocos2d的大门,虽然现在依然还是很菜,但是也算是入门了呵呵。推荐大家多看看子龙山人的blog!

   我记得以前在子龙山人译的文章:碰撞检测和收集物品--如何使用cocos2d制作基于tilempa地图的游戏:第二部分:http://www.cnblogs.com/andyque/archive/2011/05/03/2033620.html#2152489的评论中,我们曾讨论过只缩放一个图层的问题,我们当时值说了缩放,并没有说拖动的问题,下面有朋友提到了放大后拖动问题,因为一个层放大并且移动后,在进行缩放的话,是有可能导致地图的一部分跑到屏幕边界以外的。今天我们就来做一个demo,虽然不美观,没有什么动画效果(喜欢的话可以自己添加),但是可以解决我们的问题。

首先

    启动XCode,点击“File\New Project...”,选择cocos2d Application template,并且把工程命名为TestBackground.

接着你需要一张图片作为你的游戏的背景地图,我是从网上找了一张480*320的图片(正好和iphone的横向吻合),我把它命名为background.jpg,你也可以和我一样随意从网上找到你喜欢的图片作为你测试用的背景,然后我们需要把这张图片加入我们的项目,所以,拖动你下载好的背景图片到你的工程的resource文件夹下,并确保选中:

好了我们已经准备好需要的准备工作了,让我们正是开始这个demo吧。

开始

     我们先替换HelloWorldLayer.h的interface如下:

@interface HelloWorldLayer : CCLayer
{
    CCSprite* backGround;
    CGSize size;
}

我们声明了一个ccsprite作为我们的背景图片,然后我们也声明了一个CGSize来存储屏幕的尺寸(这个只是我的个人习惯,常用的变量缓存起来就不需要每次用时都调方法去取)。

然后我们转到HelloWorldLayer.m,替换模板的init方法如下:

复制代码
-(id) init
{
    if( (self=[super init])) {  

        backGround = [CCSprite spriteWithFile:@"ground.jpg"];  //1

    size = [[CCDirector sharedDirector] winSize]; //2
    
        self.anchorPoint = CGPointZero;  //3
    self.position =  CGPointZero;   //4

        backGround.position = ccp( size.width /2 , size.height/2 ); //5
        
    [self addChild: backGround]; //6
 
    }
    return self;
}
复制代码

 

第一,我们加载我们下载的背景图片;接着给我们定义的size赋值,让它缓存屏幕的尺寸([[CCDirector sharedDirector] winSize]得到的屏幕尺寸和设备的横竖屏相关),这样当我们想用屏幕尺寸的时候就不用每次都去调用方法去取了;然后我们设置self,也就是HelloWorldLayer(我们的放大缩小移动都是操作这个layer)的anchorPoint为(0,0)《默认是(0.5,0.5)》,这样我们的layer的锚点就位于它的左下角了,我设置锚点为左下角只是便于以后的计算;出于同样的需要我们设置self.position为(0,0);然后我们设置backGround.positon为屏幕的中央,因为它的锚点是默认的(0.5,0.5),所以我们的480*320的图片会很好的贴合这个横向的iphone的屏幕;最后我们把背景图片加入我们的层。现在你运行项目,应该会得到一个显示了背景画面的效果。

     现在我们只是显示了一个图片,什么都做不了,太没劲了,接下来让我们做点有意思的事吧,缩放我们的背景图片。我们要感谢苹果,让我们以极其简单的方式实现我们想要的缩放的功能,我们唯一要做的就是添加手势呵呵。在我们添加手势之前,让我们回到HelloWorldLayer.h文件,添加一个lastSacle变量,下面你能看到它的伟大作用,在刚学cocos2d的时候第一次涉及到缩放问题的时候,屏幕的scale在第二次手势的时候的突然变动,这着实困扰了我这一阵子,后来引入了这个变量,问题迎刃而解。

    吼吼,进入正题,我们要加入手势喽。在HelloWorldLayr.m的init方法中,在 backGround = [CCSprite spriteWithFile:@"ground.jpg"] 这句之前加入如下代码:

 UIPinchGestureRecognizer *gestureRecognizer = [[[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchFrom:)] autorelease];    //1
      [[[CCDirector sharedDirector] openGLView] addGestureRecognizer:gestureRecognizer]; //2
      lastScale = 1.f; //3

第一句,我们声明了一个UIPinchGestureRecognizer手势gestureRecognizer,这就是两指的捏合的缩放手势,我们初始化一个UIPinchGestureRecognizer手势,设置这个手势的委托为self,所以我们的self应该实现这个手势的回调方法,然后指明这个手势的回调方法是handlePinchFrom:,带一个冒号表明这个方法有一个默认的参数,是这个触发回调的UIPinchGestureRecognizer手势本身,并且我们对这个手势调用autorelease方法,这样它会在不需要的时候自己释放,然后我们把初始化得到的手势赋给我们的gestureRecognizer。

     第二句,我们把这个手势加入到CCDirector的openGLView中,因为手势必须要加入到UIView中才能工作,所以我们把它加入到CCDirector的openGLView中,在项目的AppDelegate.m的applicationDidFinishLaunching:方法中,我们可以看到,CCDirector的openGLView是一个EAGLView类,而EAGLView是继承自UIView的。

     第三句,我们给lastScale赋一个初值1,代表图层没有缩放时的默认scale值。

    现在我们就可以就收pinch手势事件了,但是我们还缺一件事情,我们还要实现手势的回调方法,所以,在HelloWorldLayer.m文件中我们添加以下方法:

复制代码
-(void) handlePinchFrom:(UIPinchGestureRecognizer*)recognizer
{
    if([recognizer state] == UIGestureRecognizerStateBegan)  //1
    {    
        lastScale = self.scale; //2
    }
    float nowScale;    //3
    nowScale = (lastScale - 1) + recognizer.scale;    //4
    nowScale = MIN(nowScale,2);//设置缩放上限   //5
    nowScale = MAX(nowScale,1);//设置缩放下限   //6
   
   //-1.得到移动允许的范围

   //-2.添加缩小时的处理

    self.scale = nowScale;//7
}
复制代码

在这个方法里面第一句,我们判断这个手势的状态

      第二句如果手势状态是开始,我们把我们现在的图层的sacle值赋给lastScale,lastScale就代表我们进行一次新的pinch手势之前的图层的sacle.

      第三句我们申请一个float记录现在pinch手势进行时,我们的layer需要变化成的scale.

      第四句,recognizer.scale是pinch手势的scale值,它每次都是从1开始,以两指的距离为参考,如果捏合两指,则scale变小;如果两指向外拉,则scale变大,了解了这个基本知识就可以理解这句代码了,因为recognizer.scale是从1开始的,所以我们用这次手势之前的scale值即lastScale,减1再加上recognizer.scale。这样想,如果我们在本次pinch手势之前我们的layer.scale是2的话,那么如果本次recongizer.scale是从1变到0.5的话,我们应该设置我们的layer.scale是从多少变到呢?显然我们希望它从2变到1.5;同样如果本次recongizer.scale从1变到2,我们应该设置我们的layer.scale从2变到3,这就是我们这个表达式的意思。如果在进行本次pinch之前从来没有进行过pinch手势的操作,也就是说我们从来没有变化过layer.scale会怎样呢?呵呵,我们已经在init方法里初始化了lastScale = 1.f了呵呵呵。

     第五和第六句是对缩放的上下限的限制,保证我们的layer.scale不会小于1也不会大于2。

     //-1和//-2暂时不考虑,后面我们会用到。

    第七句就是根据我们得到的应该变化到的scale来设置layer的scale值。

    ok,运行这个项目,他已经能放大缩小了。

   

 你可能会奇怪,它只能一屏幕的左下角为基点进行缩放,这是因为我们的layer的anchorPoint被我们设置成了(0,0),就是左下角了,不用担心这样会很奇怪,我们马上就要加入新的手势,让地图移动了,这样就不会因为只从左下角缩放而不能看到其他地图区域而奇怪了。我定self.anchorPoint和self.position为(0,0),是出于我的计算方便的考虑。如果你坚持认为这样不好,你也可以把self.anchorPoint改为(0.5,0.5),并把self.point改为屏幕的中央,这样会看到缩放是从中间开始的,如果你这样做了的话,你在下面的坐标计算的话可能会不太方便,并且和我的算法可能有差别,如果用差别的话请自行调整。

接下来

     让我们开始加入另一个手势UIPanGestureRecognizer,这个是平移手势,它处理单指的滑动手势。和之前一样,正式加入手势之前,我们先到HelloWorldLayer.h的interface里加两个变量和一个方法,我待会儿会解释他们的作用,ok,调整你的HelloWorldLayer.h看起来像这样:

复制代码
#import "cocos2d.h"

@interface HelloWorldLayer : CCLayer
{
    float lastScale;
    CCSprite* backGround;
    CGSize size;
    CGRect allowRect;     //1
    CGPoint lastPosition;  //2
}

+(CCScene *) scene;
-(CGRect) rectOfPositionAllow; //3

@end
复制代码

 

注释1,我们定义一个CGRect变量allowRect,我们用这个来存储我们通过手势移动我们的layer的时候,layer的position允许的范围区域。

      注释2,像刚才的pinch手势需要记录之前的scale一样,我们用一个变量 lastPosition来记录pan手势之前的layer的位置。

      注释3,定义一个方法rectOfPositionAllow,此方法返回一个CGRect是layer在当前scale情况下,layer.position允许的范围。

再次回到HelloWorldLayer.m文件,现在需要做的是,在init方法中,pinch手势的上面加入如下代码:

UIPanGestureRecognizer *gestureRecognizer1 = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanFrom:)] autorelease];
[[[CCDirector sharedDirector] openGLView] addGestureRecognizer:gestureRecognizer1];

和添加pinch手势完全一样,只是这次我们的回调方法是:handlePanFrom:方法,不用多解释了吧。接着在@end之前添加这个方法的实现吧:

复制代码
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
    
    if (recognizer.state == UIGestureRecognizerStateBegan) 
    {  
        lastPosition = self.position;
        
    } else if (recognizer.state == UIGestureRecognizerStateChanged) 
    { 
        
        CGPoint translation = [recognizer translationInView:recognizer.view];
        translation = ccp(translation.x, -translation.y);
        translation = ccpMult(translation, 0.7f);
        CGPoint newPos = ccpAdd(lastPosition, translation);
     //加入允许的范围的判断
        self.position = newPos;        
    } 
    
}
复制代码

还是同pinch手势的回调方法一样,我们先判断手势的状态,如果如果是开始的话,我们把layer当前的position赋值给lastPsoition.

      如果手势的状态是正在改变,我们调用pan手势的translationInView方法得到pan手势在recongizer.view上(即ccdirector 的openGLView)的位移量,由于UIView用的坐标系和cocos2d用的是不一样的,cocos2d是左下角原点,而UIView是左上角原点,所以需要转换。我们把得到的位移量的y坐标取负值,这样就得到了我们需要的位移量,然后我又把这个位移量乘以了0.7f是因为我觉得这个位移太灵敏了,通过乘以一个小于1的数,我相当于减小了这个位移量,你可以自己调整这个值,最后我们用得到的位移量加上我们在手势之前的layer的position,就得到我们应该移动到的位置,并把它赋给self.position.

    终于可以移动和缩放layer了,但是移动的时候会发现问题,我们的地图移出屏幕了。

这个可不是我们想要的结果,我们现在来调整这个问题,加入坐标移动的限制,是时候想起我们的rectOfPositionAllow:方法了,这个就是我们解决这个问题的灵丹妙药哈哈,在HelloWorldLayer.m文件下面实现下面方法:

复制代码
-(CGRect) rectOfPositionAllow
{
    CGRect theRect;
    theRect.origin.x = size.width - self.boundingBox.size.width;
    theRect.origin.y = size.height - self.boundingBox.size.height;
    theRect.size.width = abs(size.width - self.boundingBox.size.width);
    theRect.size.height = abs(size.height - self.boundingBox.size.height);
    return theRect;
}
复制代码

 

在这个方法里我们先申请了一个CGRect变量theRect,我们用这个theRect来存储我们的layer在移动时所允许移动到的区域。很明显,我们的layer允许移动的范围受限于我们的layer当前的sacle的大小。在本例中,我们允许的layer的scale是1到2,想一下,当layer.scale是1的时候,我们的图片大小刚好是和屏幕一样的,这时候如果我们有任何的移动,我们的图片都会跑到屏幕外边去的;再想一下,我们的layer.scale是2的时候是什么情况呢?让我们来看一下我做的一个简易图吧:

 在这个简易图中,我用黑色代表屏幕区域,用绿红黄蓝四个颜色框代表当我们的layer.scale是2时,保证我们的图片不跑到屏幕外边去的情况下,移动layer时上下左右所能到达的极限位置。每个极限位置我都用紫色的点标注了,这四个点组成了一个区域,当我们的layer.scale是2的时候,我们在移动layer时必需保证我们的layer的position在这个区域内才能保证我们的图片不会跑到外边去,当scale是1时不允许移动,当scale是2时允许的范围是这样的,当scale是其它数时允许的范围又是另外一个区域……哦my lady gaga,它是变的,我该怎么办呢?呵呵,不要担心,它是有规律的。其实再简单不过了,目前看来,我们得到这个允许的区域时,只涉及到两个元素,一个是屏幕的尺寸一个是图片的尺寸,因为屏幕的尺寸是不会变化的(不考虑竖屏的情况下),那变化的就只有layer的尺寸了,layer尺寸变化的理由是layer.scale被我们调整了。我们可以轻易的得到当前scale情况下layer的边框,就是layer的boundingBox方法,它反映了当前layer的边框,boundingBox的size就是当前layer的宽和高。由上图中我们可以很容易的看出,layer.position.x所允许的最小值是屏幕的宽减去当前layer的宽,允许的最大值是0;layer.position.y所允许的最小值是屏幕的高减去当前layer的高,允许的最大值是0。所以我们得出了rectOfPositionAllow方法中的代码,方法最后我们将计算得到的允许的position的区域返回。

     (注:我得出上述代码是基于我们的layer.anchorPoint和layer.position在不移动情况下都是0,如果你不是这个情况,你需要根据你的anchorPoint和position做相应的偏移)

     接下来,调整handlePanFrom:方法中注释://加入允许的范围后的代码 self.position = newPos; 为:

if (CGRectContainsPoint(allowRect, newPos))
     {
         self.position = newPos;
     }    

还记得我们的allowRect变量吗?我们用它来储存当前scale情况下,layer.position允许的范围。我们在改变layer.position之前先做了个判断,如果我们希望layer移动到的新位置在这个允许的范围内,我们就移动Layer,如果不在的话我们就不移动。看起来,一切都很美好,可是你现在运行项目的话会发现,根本移动不了我们的layer,太正常了呵呵,因为我们还没有给我们的allowRect赋值呢,他现在是没有允许的区域的,是时候给它一个合适的值了,那么,在呢儿给它赋值比较好呢?我们之前谈到了,我们的这个允许的范围是变化的,而其实影响这个变化的因素基本上是layer当前的scale,又因为我们的layer的scale值我们是通过pinch手势来操纵它变化的,所以追根溯源我们应该在pinch手势的回调处理方法中给我们的allowRect赋值。

    so,调整我们的handlePinchFrom:方法,还记得那个注释://-1吗?对,在它的下面添加这个赋值操作:

 allowRect = [self rectOfPositionAllow];

在这个handlePinchFrom:方法里面,我们通过我们的rectOfPositionAllow方法得到当前layer允许移动的范围,并把它赋值给我们的allowRect变量,这样的话,只要我们的layer.scale一变化,我们就可以得到一个新的允许的范围。

然后在我们移动layer.position的时候,就可以根据这个allowRect进行判断了。

      在此运行我们的项目,它已经能工作百分之八十了,放大,移动,都没问题,但是,当我们移动后在缩小的话,抱歉,图片又跑到屏幕外边去了。让我们分析一下为什么会这样,原因是这样的,在我们的layer的scale大于1的时候,我们移动我们的layer,layer的位置被移动到了(0,0)以外的地方了,然后当我们缩小的时候,我们的layer的位置是不变的,所以,当我们把layer的scale有其他值变回1的时候,我们的layer大小变成了和屏幕一样,可是位置却不在左下角,所以不能完全贴合我们的屏幕,导致图片看起来跑到屏幕外边了。那么应该怎么解决这个问题呢,我们是因为在scale还原到1是由于layer.psoition没有回到原点造成的,那么解决的办法就是,当我们还原我们的layer.scale的时候,同时也还原我们的layer.position,只要保证当layer.scale变化时,我们的layer.position也相应变化,当layer.scale最后变为1时,layer.position也变为原点,只要scale和position同步变化,那么就可以解决我们的问题。明白了这个,让我们解决它吧。

      ok,让我们在修正一下这最后的问题。在handlePinchFrom:方法的注释://-2下面,self.scale = nowScale;的上面添加如下代码:

复制代码
if (lastScale > nowScale)  //1
    {
        CGPoint newPosition =  ccpSub(self.position, ccpMult ( ccpNormalize(self.position) ,ccpLength(self.position) *(lastScale - nowScale)/(lastScale - 1))) ; //2
        if (CGRectContainsPoint(allowRect, newPosition))  //3
        {
            self.position = newPosition; //4
        }
    }
复制代码

 

我们的问题是在缩小layer时发生的,所以我们就针对性的处理,放大没问题我们就不多此一举的调整它了。所以,我们先进行了条件判断,判断了lastScale和nowScale的大小,如果lastScale大于nowScale,说明我们是在进行缩小的操作。如果条件为真,那么我们进行一些计算,对我们的layer的position进行调整。注释2这句代码比较长,我们来一点一点的分析它。

      先看等式的右边,这是一个对点的减法操作:

           减法的第一个参数是我们的layer的position

           第二个参数是一个点的乘法运算,这个乘法运算是我们的重点:

                   乘法的第一个参数是把self.position进行了normalize的操作,这个操作会把向量进行标准化,使其参数变为单位为1的向量。(我们直接

                                    用self.position做为参数是因为我们的layer的position和layer的anchorPoint默认情况下都是(0,0),如果你的情况不是

                                    这样,你需要进行normalize操作的参数应该是,当前layer的position和当layer.scale为1时《也就是不允许layer移动时》

                                    layer应该的位置,这两点之间的向量)

                   乘法的第二个参数是我们先计算当前layer的position和当layer.scale为1时layer应该的位置之间的距离,ccpLength()会返回传入的向量的

                                    距离,也就是说如果我们的layer从当前scale变回1的话,需要沿着这第一个参数的向量的方向移动这么长的距离。然后我们用

                                    这个距离  乘以layer的lastScale到layer的nowScale的变化数  从layer的lastScle到layer的scale为1的变化数 的比例关系,

                                     然后我们得到的是从layer的lastScale到nowScale时,layer.position需要移动的距离

                  这个乘法运算的结果就是我们在缩小layer.scale时,layer.position需要移动的向量。

          然后我们这个减法运算的结果就是,layer.scale变化到nowScale时,layer.position应该变化到的位置。(注:我们用减法是因为,减法的第二个参数

          得到的向量是负的,而我们缩小layer.scale时,layer.position是要向上移动的,所以用减法正好。)

     再然后我们把计算得到的位置赋给一个变量newPsoition。

     在注释//3和注释//4,我们对这个新得到的位置,重新进行判断,它是否在我们允许移动的范围内,如果在的话就调整layer.position的位置,否则不处理。

      现在你的handlePinchFrom:方法看起来应该是这样:

复制代码
-(void) handlePinchFrom:(UIPinchGestureRecognizer*)recognizer
{
    if([recognizer state] == UIGestureRecognizerStateBegan)
    {    
        lastScale = self.scale;
    }
    float nowScale;    
    nowScale = (lastScale - 1) + recognizer.scale;    
    nowScale = MIN(nowScale,2);//设置缩放上限
    nowScale = MAX(nowScale,1);//设置缩放下限   
   
    allowRect = [self rectOfPositionAllow];
    
    if (lastScale > nowScale)
    {

        CGPoint newPosition =  ccpSub(self.position, ccpMult ( ccpNormalize(self.position) ,ccpLength(self.position) *(lastScale - nowScale)/(lastScale - 1))) ;
        if (CGRectContainsPoint(allowRect, newPosition))
        {
            self.position = newPosition;
        }
    }
    self.scale = nowScale;
}
复制代码

  好了,大功告成,他已经能正常工作了,运行看看。呵呵。 

   完整源码!         

     这个demo虽然笨拙,但是它还是能工作的,如果你有更好的实现的方法,留言告诉我,我会好好学习的,谢谢。能力有限,文中可能会用不对的地方,希望大家指教。谢谢!!

2012-03-28: 11:30:52   




原文链接:http://blog.csdn.net/love_hot_girl/article/details/7547260
加载中
返回顶部
顶部