Chapter 3 Controlling and Scrolling
@implementation GameScene { __weak CCNode *_levelNode; __weak CCPhysicsNode *_physicalNode; __weak CCNode *_playerNode; __weak CCNode *_backgroundNode;}
注意__weak關鍵字。總的來說,聲明一個obejct pointer 變量而不是由類created 或者說owned的時候,最好都使用__weak,尤其是在cocos2d中,應該總是聲明一個引用,當這個引用不是parent或者node的“兄弟”(sibling)時。如果沒有__weak關鍵字,默認生成一個strong引用。
通過名字找到Player Node
在GameScene中添加代碼:
- (void)didLoadFromCCB { NSLog(@"GameScene created!"); // 使得可以接受輸入的事件 (enable receiving input events) // 這句話允許GameScene類去接受觸摸事件 self.userInteractionEnabled = YES; // load the current level 載入當前level [self loadLevelNamed:nil];}
- (void)loadLevelNamed:(NSString*)levelCCB { // 在scene中獲取當前level的player,遞歸尋找 _playerNode = [self getChildByName:@"player" recursively:YES]; // 如果沒有找到,NSAssert會拋出一個異常 NSAssert1(_playerNode, @"player node not found in level:%@", levelCCB);}
下面的代碼用于實現通過觸摸移動物體到觸摸的位置
- (void)touchBegan:(CCTouch*)touch withEvent:(UIEvent*)event { _playerNode.position = [touch locationInNode:self];}
NOTE:書中第一個參數類型為UITouch* 報錯,改為CCTouch后即可實現功能。
查閱API,摘抄如下:
Called when a touch began. Behavior notes:
- (void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event
Contains the touch.
Current event information.
CCResponder.h
分配Level-Node變量
在SPRiteBuilder中分配變量和通過名字獲取一個node是一樣的,僅僅是個人習慣問題。但是不推薦頻繁使用getChildByName:方法在schedule methond中(不太懂這是什么方法)和updata:方法中,特別是遞歸查找和a deep-node hierarch。
Caution:在SpriteBuilder中分配一個變量僅僅適用于CCB文件的直接descendants(后代--不知如何翻譯),不可以對通過Sub File(CCBFile)導入的另一個CCB指定node為變量或者properties。這也是為何player node通過名字獲取。
打開GameScene.ccb,
note:A doc root var assigns a node to a correspondingly named ivar or property declared in the CCB root node's custom class
Doc root var:分配一個node,為在CCB根node的自定義類中聲明的相對應名字的變量或者屬性。
做完這步后,_levelNode變量會在它發送didLoadFromCCB消息之前被CCBReader分配,這是創建一個在CCB中包含的node的最簡單,最有效的方法。
用CCActionMoveTo移動Player
為了平滑的移動player到指定位置,可以修改如下代碼:
- (void)touchBegan:(CCTouch*)touch withEvent:(UIEvent*)event { // _playerNode.position = [touch locationInNode:self]; CGPoint pos = [touch locationInNode:_levelNode]; CCAction *move = [CCActionMoveTo actionWithDuration:0.2 position:pos]; [_playerNode runAction:move];}
觸摸點根據_levelNode轉化。這一點很重要,保證了player可以在整個_levelNode上移動,而不是被禁錮在屏幕空間中。但是這一點目前還看不出來,因為還沒有添加滾動(scrolling)。
但是此時,如果增加duration(持續時間),會發現移動的動作并沒有疊加,player也不會停在你最后一次點擊的地方。所以必須添加一個tag,有了這個tag,可以在執行新的動作之前,停止當前動作,代碼更改如下:
- (void)touchBegan:(CCTouch*)touch withEvent:(UIEvent*)event { // _playerNode.position = [touch locationInNode:self]; [_playerNode stopActionByTag:1]; CGPoint pos = [touch locationInNode:_levelNode]; CCAction *move = [CCActionMoveTo actionWithDuration:20.2 position:pos]; move.tag = 1; [_playerNode runAction:move];}
滾動Level(Scrolling the Level)
在2D游戲中,更普遍的做法是相反方向移動content layer,已達到滾動效果。
在Cocos2D和OpenGl中,沒有camera的概念,只有device screen(設備屏幕).
Scheduling Updates(調度更新)
如果player移動到右邊和上邊,那我們要做的事情實際上是移動_levelNode向左邊和下邊方向移動。player的位置限定在level node中,左下角左邊為(0,0),在這個程序中,范圍是4000*500 points。
在GameScene中添加如下代碼:
// the updata:method is automatically called once per frame// update方法在每一幀都被自動調用- (void)update:(CCTime)delta { // update scroll node position to player node, with offset to center player in the view [self scrollToTarget:_playerNode];}
update:方法自動被Cocos2d調用,在底層,每一幀,node出現在屏幕之前,都回被調用。
不像之前的Cocos2d版本,你不再需要去明確調度更新(you no longer have to explicitly schedule the update:method.)
你可以使用node schedule和unschedule方法調度其他的方法或blocks.(you can still schedule other methods or blocks using the node schedule and unschedule methods)
例如:延遲運行一個selector,可以寫為:
[self scheduleOnce:@selector(theDelayedMethod:)delay:2.5]:
然后再相同的類中實現對應的selector。這個selector必須使用一個CCTime參數:
-(void)theDelayedMethod:(CCTime)delta {
//your code
}
Caution:永遠不要使用NSTimer等。這些時間方法在node或者Cocos2d暫停時候不會自動暫停。
delta參數是delta time,或者difference in time。
在60幀每秒時,delta時間經常取大約0.0167,單位是秒。
delta time通常用作以相同的速度移動nodes,而忽略幀速率。我們在這本書中不使用delta time,因為我們使用Cocos2d的物理引擎。
Moving the level Node in the Opposite Derection
向相反方向移動Level Node
在GameScene.m中添加scrollToTarget方法以完成滾動:
- (void)scrollToTarget:(CCNode*)target { CGSize viewSize = [CCDirector sharedDirector].viewSize; CGPoint viewCenter = CGPointMake(viewSize.width / 2.0,viewSize.height / 2.0); CGPoint viewPos = ccpSub(target.positionInPoints, viewCenter); CGSize levelSize = _levelNode.contentSizeInPoints; viewPos.x = MAX(0.0, MIN(viewPos.x, levelSize.width - viewSize.width)); viewPos.y = MAX(0.0,MIN(viewPos.y, levelSize.height - viewSize.height)); _levelNode.positionInPoints = ccpNeg(viewPos);}
前兩行的作用是指定view的尺寸到viewSize,值為屏幕以points為單位的值。
然后計算view的中心點。
viewPos變量被初始化為目標的positionInPoints減去中心點viewCenter。
這個使用ccpSub做的減法是為了保持目標node保持中心位置,如果不做這一步,目標node會消失在屏幕的左下角。
levelSize變量被定義為_lovelNode.contentSizeInPoints,在下面兩行中,它用于夾住viewPos。
因為屏幕永遠不應該比viewCenter滾動的更接近于level的邊界,所以使用減法。每個邊界的距離相加等于viewSize。或者換句話說,可以滾動的區域是viewCenter的兩倍或者一個viewSize ???
level區域和可滾動區域的關系圖:箭頭表示可滾動區域。注意player在接近level邊界的時候已經不在中心位置了。
Parallax Scrolling 視差滾動
有很多種實現視差滾動的方法,最簡單的方法是給每個layer不同的速度,并移動layers。但是這種方法有一個缺點,就是你永遠也不可能知道每個layer到底需要多大,而且很難判斷當player到達一個level中的點時,背景的哪一個部分會是可見的。
Working with Images
如果你只有2x規模的圖片版本,可以適配retina iphone和non-retina ipad,你可以改變“Scale from”設置,從Default改變為2x。這不能起到節省內存的作用,SpriteBuilder會創建一個低規模的1x和一個高規模的4x版本。這意味著4x版本的圖片和原始的2x版本的圖片有一樣程度的細節。你也可以為各種規模的圖片采用不同的圖片。強烈建議創建所有images。
SpriteBuilder給你兩個選擇:要么創建所有圖片,4x和568x384,以便在ipads上運行,要么創建2x,568x320,然后只為了在iphone設備上運行。
舉例來說,填滿ipads Retina屏幕需要最少2048x1536個像素(defult 4x),如果是2272x1536更好,可以更好的覆蓋4-inch的iphon屏幕。如果你的app只為了在iphone上運行,(對所有圖片使用2x規模),那么覆蓋整個retina屏幕的圖片需要1136x640像素點(568X320 points)。但是如果你希望稍后添加ipad版本,那么就需要你為ipad retina屏幕設計你所有的圖片了。
Project Setting
如果你開發一個僅在Iphone上運行的app,SpriteBuilder給了你改變默認4x規模的選擇。File-Project-Setting dialog
如果你想你的iPad版本顯示對應的更大的游戲世界,你可以改變“Default scaling from”,設定為2x(phonehd),注意設置不會應用在已經存在的圖片上,只有伺候新的添加進SpriteBuilder的images會改變。
注意:Apple要求app開發者支持Retina。強烈建議使用設置:Scale from setting 1x for any images,因為在iPad Retina屏幕上的顯示質量會很低。
設置:phone:僅在iPhone3GS上適用
phonehd:在iPhone4和更新的設備上適用
tablet:對非Retina ipads:Ipad1 和2,iPad mini1上適用
tablethd:對iPad3和iPadmini2和更新的設備上適用
其他的選項:
Audio quality:影響發布的音頻文件的大小和質量。level 1創建最小但是質量最差的文件。
Screen mode:主要的應用情況是當你的游戲僅僅是iPhone上運行時,并且你希望把3.5和4英寸iphone作為同一個設備,這種情況下可以考慮使用fixed mode,但是通常不建議使用因為這會使得屏幕的布局很困難。
Orientation:橫屏或者豎屏設置。
Adding Addition Background Layers
很明顯,需要更多的layers去達到景深效果。距離觀察者更近的layer,它的size就需要更大。
Prepare to Parallax in 3 2 1...
使得背景layers視差滾動需要一些初始化步驟。
首先,引進physics node,并使得player node變成physics node的子node。現在,physics node是level的內容容器,而不是level.ccb本身。
讓player作為另一個node的子node的主要原因是為了能夠在視差背景中獨立移動level內容。如果繼續使用Level.ccb做為player的父node,the changes made to the player's parent position would offset all of the level1.ccb child nodes,including the background .這會使得背景滾動的代碼復雜得多,并且很難添加一個靜止不動的node,比如暫停按鈕。
NOTE:不能把player node拖拽到background node下。因為background node 是Sub File node,不可以接受子nodes。還有其他幾類無法擁有子nodes的:Particle System,Label TTF, Label BM-Font,Button,Text Field,Slider,Scroll View。
現在需要分配CCPhysicsNode引用_physicsNode變量。因為getChildByName:返回一個CCNode類的引用,所以必須強轉返回的node。
_physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO];
- (void)loadLevelNamed:(CCNode*) levelCCB { _physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO]; _background = [_levelNode getChildByName:@"background" recursively:NO]; _playerNode = [_physicsNode getChildByName:@"player" recursively:NO]; NSAssert1(_playerNode, @"not found %@", levelCCB); NSAssert1(_physicsNode, @"not found %@", levelCCB); NSAssert1(_background, @"not found %@", levelCCB);}
現在,需要對scrollToTarget方法進行修改:
_levelNode.positionInPoints = ccpNeg(viewPos);
改為
_physicsNode.positionInPoints = ccpNeg(viewPos);
這樣,_backgroundNode的位置就會被解放,可以根據_physicsNode的位置獨立的更新。
現在,scrollToTarget方法的代碼為:
- (void)scrollToTarget:(CCNode*)target { // 屏幕大小 480 * 320 CGSize viewSize = [CCDirector sharedDirector].viewSize; // player的中心位置 (240,160) CGPoint viewCenter = CGPointMake(viewSize.width / 2.0, viewSize.height / 2.0); // levelNode的size 4000 * 500; CGSize levelSize = _levelNode.contentSizeInPoints; // CGPoint viewPos = ccpSub(target.positionInPoints, viewCenter); viewPos.x = MAX(0.0, MIN(viewPos.x, levelSize.width - viewSize.width)); viewPos.y = MAX(0.0, MIN(viewPos.y, levelSize.height - viewSize.height)); _physicsNode.positionInPoints = ccpNeg(viewPos);}
現在,必須獲取每個背景layers在_physisNode的位置更新時視差滾動的位置。
因為你想要每個layers的位置對應_physicsNode的位置,(在這個方法中是viewPos),那么考慮到viewPos(view的中心)應該與level的邊界保持一定的距離就很重要了。這個最小的距離必須至少在水平方向是viewCenter.width,在垂直方向是viewCenter.height.這樣才可以阻止可視區域出現在level邊界的外面.
這幅圖可以幫助理解,想象viewPos是每個Viewable Area的中心,那么實際的Scrollable Area矩形(比如,viewPos合法位置)必須比Level Area的左下角大,必須比右上角小。
這樣,每個背景layer相對應的位置不能和整個尺寸的level相比,也就是4000x500個points。
例子:在Iphone5上,viewCenter是284x160.這樣的話,scrollable area就是:
284x160 到( 4000 - 284) x (500 - 160) = 3716 x 340 points.
換句話說,這個Scrollable Area是level的尺寸減去view的尺寸。這樣,通過scrollable area(levelSize - viewSize)分割viewPos給了你_physicsNode當前位置在scrollable area上的百分比:
CGPoint viewPosPercent = CGPointMake(viewPos.x / (levelSize.width - viewSize.width),viewPos.y / (levelSize.height - viewSize.height));
現在,得到了_physicsNode的位置的范圍(0.0到1.0之間),0.0指的是scrollalbe area的左下角的位置,284x160,1.0指的是右上角的位置 3716x340.下一步必須運用這個百分比在每一個layer上,把每個layer自己的尺寸算進去。
試著計算: 使用568x384作為layerSize的寬和高,并且用568x320作為viewSize的寬和高,計算當viewPosPercent是0.5,0.5的時候,layerPos是多少。
- (void)scrollToTarget:(CCNode*)target { // 屏幕大小 480 * 320 CGSize viewSize = [CCDirector sharedDirector].viewSize; // player的中心位置 (240,160) CGPoint viewCenter = CGPointMake(viewSize.width / 2.0, viewSize.height / 2.0); // levelNode的size 4000 * 500; CGSize levelSize = _levelNode.contentSizeInPoints; // CGPoint viewPos = ccpSub(target.positionInPoints, viewCenter); viewPos.x = MAX(0.0, MIN(viewPos.x, levelSize.width - viewSize.width)); viewPos.y = MAX(0.0, MIN(viewPos.y, levelSize.height - viewSize.height)); _physicsNode.positionInPoints = ccpNeg(viewPos); CGPoint viewPosPercent = CGPointMake(viewPos.x / (levelSize.width - viewSize.width),viewPos.y / (levelSize.height - viewSize.height)); for (CCNode *layer in _backgroundNode.children) { CGSize layerSize = layer.contentSizeInPoints; CGPoint layerPos = CGPointMake(viewPosPercent.x * (layerSize.width - viewSize.width), viewPosPercent.y * (layerSize.height - viewSize.height)); layer.positionInPoints = ccpNeg(layerPos); }}
- (void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event { //_playerNode.position = [touch locationInNode:self]; [_playerNode stopActionByTag:1]; CGPoint pos = [touch locationInNode:_physicsNode]; CCAction *move = [CCActionMoveTo actionWithDuration:0.2 position:pos]; move.tag = 1; [_playerNode runAction:move];}
試著計算
未完待續d
新聞熱點
疑難解答