cocos2d游戲地圖的放大、縮小、移動
首先
啟動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雖然笨拙,但是它還是能工作的,如果你有更好的實現的方法,留言告訴我,我會好好學習的,謝謝。能力有限,文中可能會用不對的地方,希望大家指教。謝謝!!
轉自:http://www.cnblogs.com/dingwenjie/archive/2012/03/28/2419805.html