SpriteBuilder 學習筆記四

---恢復內容開始---node

Chapter 5數組

Timelines & Triggersapp

SpriteBuilder的一個主要特性就是能夠用關鍵幀建立Timeline動畫。甚至能夠經過提供合適的碰撞屬性讓靜態physics body也能產生動畫效果。這一章會經過使用CCBAnimationManager類建立動畫效果,也會建立一個可複用的trigger node,你能夠把這個trigger node放在level中,這個trigger可讓你作到當player進入trigger區域時,運行相應的代碼----好比播放目標node的動畫效果。less

What Are Timelines and Keyframes?oop

1. Timeline Controls:這些控制選項讓你能夠重置,快速向前,向後,中止,和播放動畫效果。最右邊的按鈕用於在播放中重置播放循環,可是在game中沒有影響。測試

2. Timeline List:一個CCB文件能夠包含多個timelines。經過下拉菜單,能夠添加,移除,複製和重命名timeline,圖中選擇當前編輯的是「default timeline」。動畫

3.Timeline Chain:這個控制項容許你具體設定Timeline噹噹前timeline播放結束後是否應該被播放。若是被設置爲No chained timeline,那麼Timeline僅僅會被播放一次。爲了在遊戲中循環一個Timeline,能夠在下拉菜單中設置Timeline爲當前正在編輯的。爲了循環圖中的Timeline,應該把這一項改爲Default Timeline。ui

4.Timeline Scale:這個控制規模。spa

5.Timeline Cursor:這個顯示當前動畫的時間點。debug

6.Keyframes:這些長方形表明關鍵幀。能夠向左右拖動它們,改變它們的位置。兩個長方形之間的時關鍵幀段,能夠右擊它們改變寬鬆模式(easing mode)。

下圖是能夠有動畫效果屬性的表。注意在全部nodes中不是全部屬性均可以有動畫效果。是否能夠有動畫效果首先是看node的類型和node是不是CCB文件的root node。Root node根本不能有動畫效果,除非是Sprite CCB文件,你能夠至少對顏色,透明度,和Sprite屬性進行動畫。

Using the Timeline Editor

在Tileless Editor view中,拖動一個doughnut圖片到Level.ccb的CCPhysicsNode中。能夠把它做爲player進入這一level的地點。

在Timeline中選中doughnut圖片。如今能夠開始添加關鍵幀,來建立Timeline動畫。首先改變Timeline持續時間從默認的10秒到6秒。

你也許會好奇Frames框表示什麼。SpriteBuilder播放動畫是以每秒30幀的速率。因此若是何時你須要一個Timeline的持續時間小於1秒,或者一秒半的時候,就必須編輯Frames框了。可選的值是0到29.好比,若是你須要持續時間爲1.5秒,那麼必須在Secs框中輸入1,在Frames框中輸入15.

確保動畫的整個持續時間在Timeline中是可見的。使用Timeline Scale滑塊確保6秒的動畫在時間軸上都是可見的。

 

Adding Keyframes

確保選擇了正確的node,而且Timeline Cursor也在正確的位置,如今能夠添加關鍵幀了。一個方法是經過菜單Animations->Insert Keyframes,而後選擇合適的關鍵幀類型,好比,Position或者Scale。可是最好使用快捷鍵完成操做。

在Cursor開始處,按下S。

如今,移動Cursor到3-seconds處,按下S。

在6秒處按下S。

注意到,這兩個關鍵幀如今用紅色的水平線鏈接在一塊兒了。

Caution:若是你試圖使開啓了physics模式的node的Scale屬性進行動畫效果,那會引起一個異常。問題在於one naturally assumes the physics shape would scale along with the node ,but it does not.

 

Editing  Animation Settings

那麼動畫效果如何決定哪些值應該被插入呢?到目前爲止尚未任何效果。這裏的關鍵是關鍵幀容許你編輯給定的屬性,在給定的時間點。如今,Scale屬性被設置成1,1。

確保doughnut仍然被選中,切換到屬性選項卡。在3s處的關鍵幀的Scale屬性的X和Y軸上改變值,均爲1.3。

 

Looping Animations...Wheeeee!

爲了確保在game中動畫的循環,而不只僅是在SpriteBuilder中,必須chain the Timeline to itself.

一個Timeline Chain簡單來講就是告訴動畫在另外一個timeline動畫播放完後開始播放另外一個動畫。因此,把一個動畫和自身串起來會致使循環。爲了完成循環,把「No chained timeline"改爲」Default Timeline"。

 

Easing Animations with Ease

可使用easing模式使動畫平滑。easing影響兩個關鍵幀之間的值如何改變。爲了編輯easing模式,右擊關鍵幀段。

圖1:

注意到在選擇一個easing mode後,粉紅色的線段發生了變化。若是你選擇了Instant模式,粉紅色線條會徹底消失。理論上說,Instant不是一個easing mode。 

---恢復內容結束---

Chapter 5

Timelines & Triggers

SpriteBuilder的一個主要特性就是能夠用關鍵幀建立Timeline動畫。甚至能夠經過提供合適的碰撞屬性讓靜態physics body也能產生動畫效果。這一章會經過使用CCBAnimationManager類建立動畫效果,也會建立一個可複用的trigger node,你能夠把這個trigger node放在level中,這個trigger可讓你作到當player進入trigger區域時,運行相應的代碼----好比播放目標node的動畫效果。

What Are Timelines and Keyframes?

1. Timeline Controls:這些控制選項讓你能夠重置,快速向前,向後,中止,和播放動畫效果。最右邊的按鈕用於在播放中重置播放循環,可是在game中沒有影響。

2. Timeline List:一個CCB文件能夠包含多個timelines。經過下拉菜單,能夠添加,移除,複製和重命名timeline,圖中選擇當前編輯的是「default timeline」。

3.Timeline Chain:這個控制項容許你具體設定Timeline噹噹前timeline播放結束後是否應該被播放。若是被設置爲No chained timeline,那麼Timeline僅僅會被播放一次。爲了在遊戲中循環一個Timeline,能夠在下拉菜單中設置Timeline爲當前正在編輯的。爲了循環圖中的Timeline,應該把這一項改爲Default Timeline。

4.Timeline Scale:這個控制規模。

5.Timeline Cursor:這個顯示當前動畫的時間點。

6.Keyframes:這些長方形表明關鍵幀。能夠向左右拖動它們,改變它們的位置。兩個長方形之間的時關鍵幀段,能夠右擊它們改變寬鬆模式(easing mode)。

下圖是能夠有動畫效果屬性的表。注意在全部nodes中不是全部屬性均可以有動畫效果。是否能夠有動畫效果首先是看node的類型和node是不是CCB文件的root node。Root node根本不能有動畫效果,除非是Sprite CCB文件,你能夠至少對顏色,透明度,和Sprite屬性進行動畫。

Using the Timeline Editor

在Tileless Editor view中,拖動一個doughnut圖片到Level.ccb的CCPhysicsNode中。能夠把它做爲player進入這一level的地點。

在Timeline中選中doughnut圖片。如今能夠開始添加關鍵幀,來建立Timeline動畫。首先改變Timeline持續時間從默認的10秒到6秒。

你也許會好奇Frames框表示什麼。SpriteBuilder播放動畫是以每秒30幀的速率。因此若是何時你須要一個Timeline的持續時間小於1秒,或者一秒半的時候,就必須編輯Frames框了。可選的值是0到29.好比,若是你須要持續時間爲1.5秒,那麼必須在Secs框中輸入1,在Frames框中輸入15.

確保動畫的整個持續時間在Timeline中是可見的。使用Timeline Scale滑塊確保6秒的動畫在時間軸上都是可見的。

 

Adding Keyframes

確保選擇了正確的node,而且Timeline Cursor也在正確的位置,如今能夠添加關鍵幀了。一個方法是經過菜單Animations->Insert Keyframes,而後選擇合適的關鍵幀類型,好比,Position或者Scale。可是最好使用快捷鍵完成操做。

在Cursor開始處,按下S。

如今,移動Cursor到3-seconds處,按下S。

在6秒處按下S。

注意到,這兩個關鍵幀如今用紅色的水平線鏈接在一塊兒了。

Caution:若是你試圖使開啓了physics模式的node的Scale屬性進行動畫效果,那會引起一個異常。問題在於one naturally assumes the physics shape would scale along with the node ,but it does not.

 

Editing  Animation Settings

那麼動畫效果如何決定哪些值應該被插入呢?到目前爲止尚未任何效果。這裏的關鍵是關鍵幀容許你編輯給定的屬性,在給定的時間點。如今,Scale屬性被設置成1,1。

確保doughnut仍然被選中,切換到屬性選項卡。在3s處的關鍵幀的Scale屬性的X和Y軸上改變值,均爲1.3。

 

Looping Animations...Wheeeee!

爲了確保在game中動畫的循環,而不只僅是在SpriteBuilder中,必須chain the Timeline to itself.

一個Timeline Chain簡單來講就是告訴動畫在另外一個timeline動畫播放完後開始播放另外一個動畫。因此,把一個動畫和自身串起來會致使循環。爲了完成循環,把「No chained timeline"改爲」Default Timeline"。

 

Easing Animations with Ease

可使用easing模式使動畫平滑。easing影響兩個關鍵幀之間的值如何改變。爲了編輯easing模式,右擊關鍵幀段。

圖:

 

注意到在選擇一個easing mode後,粉紅色的線段發生了變化。若是你選擇了Instant模式,粉紅色線條會徹底消失。理論上說,Instant不是一個easing mode。 It simply sets the animated value to that of the keyframe.對於其餘全部的easing modes,粉紅色線段的一端或者兩端變得有一些陰影,這取決於你是選的In,Out,仍是In/Out。

In:應用於關鍵幀端的開始。

Out:應用於關鍵幀段的結束。

In/Out:應用於In和Out。

圖:

 

Caution:若是兩關鍵幀之間的時間很短,好比只有10幀或更少,你會很難注意到easing效果。

 

Keyframe Animations for Physics Nodes

關鍵幀動畫只能應用在physics node設置成Static的狀況。

Adding the Gear and Saw

在SpriteBuilder的File view中,選擇Sprites Sheets文件夾,建立一個New Forder,命名爲GameElements。並Make Smart Sprite Sheet.拖動circularsaw1.png和gear1.png到該文件夾中。

如今重複以下的步驟,建立兩個新ccb文件,命名爲Gear1.ccb和Saw1.ccb。

1.右擊Prefabs文件夾,選擇New File。

2.分別命名爲Gear1.ccb和Saw1.ccb,更改成Node類型。稍後會解釋爲什麼是Node,而不是sprite。

3.選擇Tileless Editor View。打開Gear1.ccb,拖動gear1到stage。打開Saw1.ccb,拖動saw1到stage。

4.打開屬性選項卡,設置gear/saw sprite的位置爲0,0,錨點位置爲0.5,0.5。

5.打開屬性選項卡,選擇Enable physics複選框。改變body type爲static。保證Categories和Masks爲空。

6.在Collision type中,輸入gear,和saw,對應各自的屬性框。

7.對於gear sprite:

圖:

8.對於saw sprite:改變physics shape爲Circle,改變suggested radius從208到190。

CCSprite 的root node只能夠有一些視覺效果:sprite frames,opacity,和color。這就是爲何我不使用Sprite CCB文件,可是取而代之的是使用Node文件,用一個sprite做爲child node。

這樣,能夠具備徹底的動畫效果。

 

Animation the Gear and Saw

隨意建立額外的Gear1.ccb和Saw1.ccb複製品,它們有相同的內容,可是不一樣的旋轉動畫。若是你這樣作,你應該爲這些額外添加的CCB文件選擇合適的名字。好比說,Gear1_180AndBack.ccb。

Tip:注意到如今沒有複製命令。可是若是之後會有,最可能出現該命令的地方是右擊CCB文件。幸運的是,能夠用Finder複製CCB文件。在Finder中,會發現全部文件,在SpriteBuilder Resources文件夾中。能夠很簡單的經過複製粘貼在Finder中建立ccb文件的複製文件。

 

不用擔憂建立了太多複製文件,被髮布的CCB文件很小,若是sprites使用相同的圖片,那麼幾乎不會增長內存使用量。一個sprite實例,包括紋理,使用的內存小於1kb。

 

選擇gear/saw sprite node,改變默認Timeline持續時間爲4秒。

選擇gear/saw,把Timeline cursor移動到00:00處,按"R",建立一幅關鍵幀。把Timeline cursor移動到04:00處,按「R",建立一幅關鍵幀,輸入」360「。

最後,Timeline Chain須要從No chained timeline改變到Default Timeline,這樣能夠保證無限的旋轉。

 

Adding Gears and Saws to the Level

如今應該放置一個或多個gear和saw對象於Level1.ccb中。第一步是拖動一個Node到CCPhysicsNode中,命名爲gear和saw。第二步是拖動gear和saw從Tileless Edit view到node上。

Tip:當你在Level1.ccb中,按下Play按鈕,會注意到不只level-entry doughnut有動畫效果,gear和saw也會播放它們各自的動畫效果。這容許你去預先看動畫效果,即便一些動畫效果是在別人CCB文件中編輯的。

如今,你也許想知道:當編輯一個Timeline動畫時,以前你不能編輯動畫Node的屬性,除非是Timeline Cursor精確指向的關鍵幀。可是如今你能夠播放和暫停動畫,而且不論Timeline Cursor指向哪,總能夠編輯node的屬性。

爲了理解,考慮你在編輯特別的Node的關鍵幀,好比,Gear1.ccb文件。經過添加關鍵幀,而且編輯關鍵幀的特殊屬性,你能夠告訴node在動畫期間到底該怎麼作。好比位置,旋轉,大小等屬性,這些屬性必須與node的當前狀態相關聯。而後你放置一個Gear1.ccb和Level1.ccb的實例。這樣就建立了一個Sub File Node,做爲Gear1.ccb的引用。這個Sub File Node表明了Gear1的特定的實例。Sub File node容許你去指定node的初始位置,旋轉和大小。在播放期間,Gear的Timeline根據Sub File Node的位置,旋轉和大小播放。

 

How Not to Autoplay Animations

至今爲止,當啓動遊戲後,動畫效果會自動播放。遲早你但願它不要這樣。

假設你但願在合適的時間,當遊戲運行時去播放動畫,這是你須要在代碼中完成的。爲了建立一個一開始不活動的saw,應該建立一個Saw1.ccb的複製。在Finder中建立該複製,重命名爲:Saw1_noautoplay.ccb。

Editing Timelines to Uncheck Autoplay

首先,須要阻止動畫效果從開始就播放。點擊」Default Timeline「,

點擊Edit Timelines,

注意到Default Timeline的Autoplay複選框默認已經被勾上。首先,解鎖複選框以阻止Timeline動畫自動播放。

 

Playing Animations Programmatically

在Level1.ccb中放置Saw1_noautoplay.ccb,做爲gears和saws node的子node。而且把名字改爲:sawNoAutoplay。

如今發佈和運行APP,能夠注意到saw再也不自動播放,可是如何讓它在到了特定的time時開始播放呢。

Spritebuilder自帶不少使用的類。你已經知道了CCBReader,這是負責載入CCB files的。另外一個使用最多的是CCAnimationManager。它負責存儲和播放動畫timelines,也就是Cocos2D指的sequences。

Note:Sequences和Timelines是相近的術語,儘管不是徹底相同。Cocos2D uses the term sequences for any set of CCAction classes wrapped in  CCActionSequence class that runs the actions it wraps in sequential order.舉例來講,當一個node從A移動到B,而後旋轉90度,而後移動到C,這就是sequence。一個SpriteBuilder中的Timeline能夠被認爲是多個sequences的複合,由於舉例來講,你能夠對一個node的透明度和大小進行動畫,同時進行移動和旋轉動做。總的來講,一個Timeline是一個或多個sequences同時進行播放。

每一個node實例都有一個animationManager屬性,你能夠進入CCAnimationManager,而且經過名字播放動畫。

快速測試以下,增長代碼在loadLevelNamed:方法中:

- (void)loadLevelNamed:(NSString*)levelCCB {
    
    _physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO];
    //_physicsNode.debugDraw = _drawPhysicsShapes;
    _physicsNode.collisionDelegate = self;
    _playerNode = [_physicsNode getChildByName:@"player" recursively:NO];
    _backgroundNode = [_levelNode getChildByName:@"background" recursively:NO];
    NSAssert1(_playerNode, @"_playerNode not found! %@", levelCCB);
    NSAssert1(_physicsNode, @"_physicsNode not found! %@", levelCCB);
    NSAssert1(_backgroundNode, @"_backgound not found !%@", levelCCB);
    CCNode *sawNoAutoplay = [_physicsNode getChildByName:@"sawNoAutoplay" recursively:YES];
    [sawNoAutoplay.animationManager runAnimationsForSequenceNamed:@"Default Timeline"];
}

最後兩行代碼是新加入的代碼,第一行經過名字獲取node的引用。animation manager開始運行sequences,名字叫Default Timeline。運行APP,會發現非自動播放的saw如今開始有動畫效果了。

Note:每一個CCB文件有本身的CCAnimationManager實例,這樣特殊的CCB文件的timelines能被存儲。全部在CCB文件中被編輯的Timeline動畫被存儲在CCB文件的root node的CCAnimationManager中。

 

Animation Playback Completion Callback

CCAnimationManager類容許你在動畫的播放結束後和一個循環動畫重複的時候獲得通知。爲了收到這些通知,必須實現CCBAnimationManagerDelegate協議。

代碼以下:

#import "CCNode.h"

@interface GameScene : CCNode <CCPhysicsCollisionDelegate,CCBAnimationManagerDelegate>

@end
- (void)loadLevelNamed:(NSString*)levelCCB {
    
    _physicsNode = (CCPhysicsNode*)[_levelNode getChildByName:@"physics" recursively:NO];
    //_physicsNode.debugDraw = _drawPhysicsShapes;
    _physicsNode.collisionDelegate = self;
    _playerNode = [_physicsNode getChildByName:@"player" recursively:NO];
    _backgroundNode = [_levelNode getChildByName:@"background" recursively:NO];
    NSAssert1(_playerNode, @"_playerNode not found! %@", levelCCB);
    NSAssert1(_physicsNode, @"_physicsNode not found! %@", levelCCB);
    NSAssert1(_backgroundNode, @"_backgound not found !%@", levelCCB);
    CCNode *sawNoAutoplay = [_physicsNode getChildByName:@"sawNoAutoplay" recursively:YES];
    sawNoAutoplay.animationManager.delegate = self;//添加的代碼
    [sawNoAutoplay.animationManager runAnimationsForSequenceNamed:@"Default Timeline"];
    
}

而後添加completedAnimationSequenceNamed:callback方法。

- (void)completedAnimationSequenceNamed:(NSString *)name {
    NSLog(@"completed animation sequences:%@",name);
}

每次這個方法運行時,會記錄下播放完成的動畫的名字。該方法也會在一個循環動畫完成而且從新開始時運行。This allows you to wait for a looping animation to play back to complete before stopping or replacing it with another animation,in order to prevent nasty jumps in the animation playback.

或者,你一樣能夠選擇設置一個block,當動畫結束時運行。優點在於你不須要實現CCBAnimationManagerDelegate協議,劣勢是你要把CCAnimationManager做爲輸入,這樣你必須涉及到lastCompletedSequenceName 屬性,以獲得結束的Timeline的名字。

 

Differentiating Between Animations

再次提醒每一個CCB文件有本身的CCAnimationManager實例。在這個例子中,只有Saw1_noautoplay.ccb動畫會調用毀掉方法。

一樣的,若是你使用self.animationManager.delegate = self,那麼這個方法會在每次GameScene.ccb的Timeline動畫結束時運行。可是,好比當Player.ccb或者Backround1.ccb結束的時候,不會運行。

注意到,使用相同的類做爲多個動畫manager實例的代理是可行的。好比,若是你在GameScene中須要獲得其餘CCB動畫事件的通知,應該這麼寫:

self.animationManager.delegate = self;

_levelNode.animationManager.delegate = self;

_playerNode.animationManager.delegate = self;

_backgroundNode.animationManager.delegate = self;

 

Triggering Animations on Collision

 

Adding Trigger Targets to the Level

首先,在loadLevelNamed:方法中移除:

 CCNode *sawNoAutoplay = [_physicsNode getChildByName:@"sawNoAutoplay" recursively:YES];
    [sawNoAutoplay.animationManager runAnimationsForSequenceNamed:@"Default Timeline"];

接下來,打開Level1.ccb,假設你已經在gears and saws的node中添加了一個Saw1_noautoplay.ccb實例,那麼再添加一個,你至少須要兩個。

你應該爲在Timeline中已經存在的saw實例重命名,而且複製它,排列好位置。

選擇每一個Saw1_noautoplay prefab實例,改變它們的名字屬性爲triggerSawRotation。trigger node會獲得一樣的名字,以在trigger 區域和tobe-triggered nodes之間創建聯繫。

Creating a Trigger CCB

什麼是一個好的trigger node?Color nodes是最好的選擇,可是總的來講你可使用任何類型的node。trigger區域應該是在編輯的時候可見,可是是透明的。The node color can be used to hightlight different types of triggers with different colors。

你應該建立一個trigger 對象做爲一個模板。

在Prefabs中建立一個New File,名爲TrigerOnce.ccb,類型爲Layer。大小是128x128.

Note:Layer類型很重要由於放在Level中時,它給你一個長方形的區域。你也許注意到saw和gear實例有可旋轉的區域。長方形區域在使用時更方便,並且你會常常須要改變node的大小,由於你須要去適應trigger區域的大小和長方形的形狀。

拖動一個Color node到TriggerOnce.ccb中。位置設爲0,0.長寬設置爲128x128。選一個喜歡的顏色,透明度設置爲0.3,之後根據須要常常修改顏色和透明度。

勾選Enable Physics,改變類型爲static,Collision type爲trigger。Categories和Masks目前能夠爲空。

爲color node在Item Code Connections中輸入自定義類名:Trigger。這會建立一個Trigger實例,在你每次添加TriggerOnce.ccb到level中時。

由於這個特定的trigger僅僅應該觸發一次,因此打開屬性區,在底部,點擊Edit Custom Properties,加上一個自定義屬性:triggerCount,類型爲Int,值爲1.

再添加一個trigger CCB,命名爲:TriggerForever.ccb.

注意到,一樣能夠建立一個多邊形的trigger。可是,一個多邊形形狀不會在SpriteBuilder中反應,由於color node填充一個長方形區域。若是你但願可視化顯示一個多邊形trigger區域,你必須使用sprite node和對應的trigger圖片。

另外一方面,你老是可使用多個長方形trigger創建一個更復雜的trigger區域。Trigger碰撞區域基本不會須要如此的精確。因此最好避免使用多邊形形狀的trigger。

Adding Triggers to the Level

首先拖動一個Node到Level1.ccb的CCPhysicsNode中,命名爲:triggers。這是你的trigger nodes容器。

如今拖動一個TriggerOnce,從Tileless Editor View到triggers node中。改變trigger的名字屬性爲triggerSawRotation。這個名字鏈接了trigger和你以前添加的saw nodes,它們的名字相同。當player(或者其餘nodes)觸發了trigger後,目標nodes會收到一個消息。

改變trigger的位置,改變trigger的大小。確保player會和它發生碰撞。

也能夠建立一個trigger,若是你但願建立一個更復雜的trigger形狀。多個triggers能夠互相重疊而沒有其餘問題。

 

Creating the Trigger Class

如今運行這個APP,會收到一個錯誤,由於CCBReader會發現自定義類Trigger沒法被找到。

爲了修復這個錯誤,須要添加一個你在Code Connections中自定義名字相同名字的類。

運行APP,還會收到錯誤消息,由於忽略了triggerCount屬性。使用@property,由於你可能須要在運行時由其餘類獲取或者改變這個值。代碼以下:

#import "CCNode.h"
@protocol TriggerDelegate <NSObject>
- (void)didTriggerWithNode:(CCNode*)activator;
@end
@interface Trigger : CCNode
@property int triggerCount;
- (void)triggerActiveBy:(CCNode*)activator;
@end

 如今運行APP無錯誤,可是trigger還不能真正工做。

Programming the Trigger Class

Trigger.m實現起來有些複雜,可是能夠很方便的複用。

Initializing Trigger and Target Arrays

以下:

#import "Trigger.h"
static NSMutableDictionary *targetArrays;
static NSMutableDictionary *triggerArrays;
@implementation Trigger {
    BOOL _repeatsForever;
}
- (void)didLoadFromCCB {
    if(targetArrays == nil ) {
        targetArrays = [NSMutableDictionary dictionary];
        triggerArrays = [NSMutableDictionary dictionary];
    }
    [targetArrays removeAllObjects];
    [triggerArrays removeAllObjects];
    _repeatsForever = (_triggerCount <= 0);
}
@end

一開始,聲明瞭兩個static NSMutableDictionary變量。關鍵字static意味着這些變量能夠被Trigger class的全部實例共享。本質上,它們是全局變量,but because they are declared in the scope of the implementation file they are accessible only to code in the Trigger.m file.

@implementation區域聲明瞭一個_repeatsForever變量,它能夠被用於決定特定的trigger will never remove itself.

 didLoadFromCCB方法包含對兩個全局dictionary的初始化和清理代碼。若是targetArrays是nil,它會建立一個NSMutableDictionary實例並聲明它。

下一步,兩個dictionary都要作removeAllObjects。緣由是:到最後,你確定會要去改變levels,基於這個緣由,兩個dictionary被聲明爲全局變量(static),當你改變levels而且呈現其餘關的時候,它們和它們所包含的內容會再內存中保留。

因此,在每一個新的level中至少有一個Trigger實例,這些dictionary須要清理任何保留着的引用。

Note:讓每一個trigger removeAllObjects看起來彷佛效率不高,彷佛作一次就夠了,但我以爲增長額外的代碼來檢查是否targetArrays和triggerArrays每次建立實例時是否爲空是合理的理由。 

最後,_repeatsForever被設置爲_triggerCount等於或者小於0的結果,這意味着若是_triggerCount被初始化爲0或者負數,trigger會重複。

Tip:在OC中,全部static變量和全部ivars都被初始化爲0.對於BOOL ivars,0等於NO;對於id和OC類指針變量,0等於nil。

「Furthermore, since automatic reference counting (ARC) is enabled in every SpriteBuilder project, each local variable of type id and Objective-C class pointers are initialized to nil as well. The only variables you have to assign a value before reading from them are local variables that are primitive data types (i.e., BOOL, int, CGFloat, double, NSUInteger and similar) and C structs and C pointers like void*.」

摘錄來自: Steffen Itterheim. 「Learn SpriteBuilder for iOS Game Development」。 iBooks.

 

Finding Triggers and Targets

下一步是爲每一個trigger匹配targets。Triggers和targets相鏈接,只要簡單的對trigger和target nodes使用相同的名字。不只能夠一個trigger對應多個targets,一樣能夠多個相同名字的triggers激活相同的target(s)。

Finding and Storing Triggers

在Trigger.m的@end上方添加代碼:

- (void)onEnter {
    [super onEnter];
    self.parent.visible = NO;
    NSAssert1(_parent.name.length > 0, @"Trigger node has no name:%@", self);
    self.name = _parent.name;
    NSPointerArray *triggers = [triggerArrays objectForKey:_name];
    if(triggers == nil) {
        triggers = [NSPointerArray weakObjectsPointerArray];
        [triggerArrays setObject:triggers forKey:_name];
    }
    [triggers addPointer:(void*)self];
    if([targetArrays objectForKey:_name] == nil) {
        NSPointerArray *targets = [NSPointerArray weakObjectsPointerArray];
        [targetArrays setObject:targets forKey:_name];
        [self addTargetsTo:targets searchInNode:self.scene];
        NSAssert1(targets.count > 0, @"no target found for trigger named %@", _name);
    }
}

注意:你不能用didLoadFromCCB方法去找到全部的triggers和targets;取而代之的是必須使用onEnter。由於didLoadFromCCB在每一個單獨的node加載後就馬上執行,若是你在didLoadFromCCB中運行這段代碼,極可能致使target或者trigger nodes尚未被加載。最快能夠開始搜索全部target或者trigger node是當onEnter運行時。onEnter方法被髮送給每一個node 實例,在它做爲一個child被添加到其餘node以後。

Caution:注意onEnter須要調用[super onEnter]。若是沒有調用[super onEnter],編譯器會警告你,另外,scheduling和input events不會工做或者不會正確工做。

上述代碼的第一項工做是設置trigger node的visible status爲NO。事實上,這是在設置trigger的parent node。這是由於視覺上表明trigger區域的color node是Trigger自定義類。可是color node是TriggerOnce.ccb的子node。我喜歡隱藏root node,以便我想去使用多個color nodes設計更復雜的triggers。

下一步,node接管了parent的名字。這是由於在Level1.ccb中,Name屬性被聲明爲Sub File node實例,它是TriggerOnce.ccb的引用。這結束於聲明這個名字給Trigger.ccb root node,也就是color node的parent。以_parent.name聲明self.name是爲了方便,由於這容許你使用在CCNode類中聲明的_name變量。

Tip:由於忘記給trigger一個name是常見的錯誤,因此決定在這裏加入一個assertion去警告你若是_parent.name.length是0,即意味着parent的name也是nil活着一個空string。這個assertion會在兩種狀況下報錯:若是_parent.name 是nil,整個test會一樣返回nil(也就是0)由於發送給nil對象的message默認返回0.

你也許會疑問爲何沒有直接分配_name變量或者爲何經過_parent.name獲得name而不是self.parent.name.

根據經驗,你應該傾向於使用一個available的變量,when merely reading its value。這使得代碼更簡潔,效率更高。可是,當分配一個值給屬性時,最佳實踐是經過self.name分配屬性而不是直接分配給_name ivar。這裏的根本緣由是self.name = @".."內部運行屬性setter方法[self setName:@".."]。繞過屬性setter會致使不少問題。若是你對於直接分配給一個ivar可能的影響有疑問,建議老是分配給屬性或者運行property setter方法。

下一個代碼段:

NSPointerArray *triggers = [triggerArrays objectForKey:_name];
    if(triggers == nil) {
        triggers = [NSPointerArray weakObjectsPointerArray];
        [triggerArrays setObject:triggers forKey:_name];
    }
[triggers addPointer:(void*)self];

第一行,一個NSPointerArray經過使用trigger的_name查找object獲得。起初,沒有任何object,致使triggers爲nil。若是這樣的話,if語句塊會建立一個NSPointerArray而且分配給triggers,同時,使用trigger的_name做爲key,在triggerArrays dictionary中存儲。

定義NSPointerArray的部分頗有趣。它聽起來像一個常規的NSArray或者NSMutableArray,可是它實際上不是它們的子類。它確實表現的很像NSMutableArray。它的特色在於存儲指針引用而且容許你存儲0值weak引用,這意味着triggers數組不包含trigger nodes added to it。

考慮到trigger和targe nodes有可能隨時被移除出node hierarchy,不論它們有沒有被激活。

「Trigger nodes should not be kept from deallocating simply because you maintain a list of triggers in a global array. Hence, storing them weakly (not retaining) in an NSPointerArray allows triggers that are removed from the node hierarchy to deallocate.」

摘錄來自: Steffen Itterheim. 「Learn SpriteBuilder for iOS Game Development」。 iBooks.

隨着triggers 數組創建完畢,剩下的工做就是在addPointer中傳遞self,可是必須加上(void*),由於NSPointerArray存儲普通的void*指針。

 

Finding and Storing Targets

 if([targetArrays objectForKey:_name] == nil) {
        NSPointerArray *targets = [NSPointerArray weakObjectsPointerArray];
        [targetArrays setObject:targets forKey:_name];
        [self addTargetsTo:targets searchInNode:self.scene];
        NSAssert1(targets.count > 0, @"no target found for trigger named %@", _name);
     }
// Searching for targets

 注意到targets僅僅在給定_name卻沒有在targetArrays dictionary中找到對應項時被搜索。當你有多個相同名字的triggers時,這能夠阻止屢次查到相同的targets。只有第一個trigger須要查找targets,由於其餘的triggers會指向相同的targets。

若是在targetArrays中沒有給定_name的array,如上代碼會建立一個NSPointerArray,用於存儲指向targets的weak指針。相同的array使用trigger _name做爲它的key。下一個method作了一個遞歸查找,從self.scene node開始。它把全部相同名字的target nodes加入targets array。

- (void)addTargetsTo:(NSPointerArray*)targets searchInNode:(CCNode*)node {
    for(CCNode* child in node.children) {
        if([child.name isEqualToString:_name]) {
            if([child conformsToProtocol:@protocol(TriggerDelegate)]) {
                [targets addPointer:(void*)child];
            }
        }
        [self addTargetsTo:targets searchInNode:child];
    }
}

addTargets:searchInNode:方法遞歸運行,以找到所給定的名字的全部nodes。在這裏,不能使用getChildByName:方法,由於這會返回第一個child node,可是你須要全部相同名字的nodes的列表。

若是一個target node有正確的名字,而且服從TriggerDelegate協議,那麼一個target node的引用就被添加到targets array中。

最後一行遞歸查找。這確保了在scene中得每一個node都被處理過了,而且不會有任何遺漏。

着一樣意味着,當前版本的trigger系統沒法識別加載GameScene.ccb以後添加的nodes。

你能夠添加一個+(void) addTarget:(CCNode*)target方法。你應該確保target node服從TriggerDelegate協議。要添加一個target node,你應該在其餘任何類中調用[Trigger addTarget:aTargetNode]。

Forwarding Trigger Events to Targets

剩下的工做是當trigger被激活後通知target nodes。triggerActivatedBy:方法:

- (void)triggerActiveBy:(CCNode *)activator {
    NSPointerArray *targets = [targetArrays objectForKey:_name];
    for (id target in targets) {
        [target didTriggerWithNode:activator];
    }
    if(_repeatsForever == NO) {
        NSPointerArray *triggers = [triggerArrays objectForKey:_name];
        [self updateTriggers:triggers];
        [self cleanupTriggers:triggers];
    }
}

 基於trigger的_name,在targetArrays已經包含了targets的名單。全部的targets都被列舉,向每個target發送di'dTriggerWithNode消息,這容許每一個target在被triggered時運行custom的代碼。

在循環中使用id關鍵字聲明target變量讓你能夠發送didTriggerWithNode:消息,編譯器不會發出selector沒有聲明的警告。

若是由於某些緣由,你須要用CCNode*聲明target,你必須包含TriggerDelegate協議。能夠選擇的另外一種循環爲:

for(CCNode<TriggerDelegate>*target in targets)

同時,注意:各自的target變量有可能爲nil,由於targets是一個存儲弱指針的array。幸運的是,你不須要作額外的nil檢查,由於發送消息給nil objects在OC中是合法的。

Caution:As with casting,給變量聲明添加一個協議不能自動讓object繼承協議的selectors。因此仍然會引發一個未聲明的selector運行錯誤。必須實現didTriggerWithNode:方法。

Cleaning Up Triggers and Targets

除非trigger不停地重複,即它的初始triggerCount被設置爲1或者更多,那麼,NSPointerArray包含全部相同名字的triggers須要更新或者進行可能的清除。下面兩個方法起到這樣的做用。

- (void)updateTriggers:(NSPointerArray*)triggers {
    for(Trigger *trigger in triggers) {
        trigger.triggerCount -- ;
    }
}
- (void)cleanupTriggers:(NSPointerArray*)triggers {
    if(_triggerCount <=0) {
        for(Trigger *trigger in triggers) {
            [trigger.parent removeFromParent];
        }
        [targetArrays removeObjectForKey:_name];
        [triggerArrays removeObjectForKey:_name];
    }
}

updateTriggers:方法減小triggerCount屬性。它假設全部相同名字的trigger開始都有相同的triggerCount;不然,一些triggers可能比其餘triggers更容易觸發,這通常不是你指望的。特別是,它禁止你結合TriggerOnce.ccb和TriggerForever.ccb去觸發相同的targets。若是這是你想要的,你必須更新cleanupTriggers:,以便它僅僅移除那些triggerCount已經減到0的triggers,或者是沒有被設置爲永遠重複的triggers。

cleanUpTriggers方法檢測剛被減小的trigger的_triggerCount。若是是0或者更少,就能夠認爲全部的triggers已經完成了各自的工做,能夠被移除。就像以前說的,每一個trigger都是CCNode的child,TriggerOnce.ccb的root node,因此最好是經過發送removeFromParent消息以移除trigger的parent。移除parent會同時移除任何child nodes,包括trigger node自身。

考慮到這些triggers已經作完了工做,那麼target或者triggers的NSPointerArray都再也不須要。因此,它們被移除出targetArrays和triggerArrays。

 

Informing Triggers About Collisions

如今,剩下的工做是發送triggerActivatedBy:消息給碰撞中的Trigger實例,而且實現一個服從TriggerDelegate協議的類,以便當didTriggerWithNode:selector運行時,運行自定義代碼。

如今是時候讓在TriggerOnce.ccb中得color node的Collision類型起做用了。你已經設置了"trigger"類型。可是,你仍然須要在GameScene.m中添加碰撞方法,該方法第三個參數是trigger。

- (BOOL)ccPhysicsCollisionBegin:(CCPhysicsCollisionPair *)pair player:(CCNode *)player trigger:(Trigger *)trigger {
    [trigger triggerActiveBy:player];
   // return No to allow the bodies to pass through each other
return NO; }

注意到:第三個參數是一個指向Trigger實例的指針,而不是一個CCNode實例。你可使用Trigger*參數,代替CCNode*參數,可是必須保證trigger參數永遠是Trigger類。若是不能,那麼會致使「unrecognized selector sent to instance"錯誤。

同時你必須在GameScene.m中import Trigger.h。

Triggering Target Nodes

每一個能夠被trigger激活的target node須要有以下條件:

1.在SpriteBuilder中分配給node一個自定義類。

2.自定義類的頭文件須要#import "Trigger.h"

3.自定義類的@interface須要採用TriggerDelegate協議。

4.自定義類的@implementation須要有一個-(void)didTriggerWithNode:(CCNode*)activator方法。

由於在被一個trigger激活後播放一段動畫是很常見的功能,因此應該寫一個通用類PlayTimelineWhenTriggered。可是在你寫這個類以前,打開Saw1_noautoplay.ccb,選擇root node,必定不要選saw sprite。切換到Item Code Conneciton,在自定義類中輸入PlayTimelineWhenTriggered。

Caution:PlayTimelineWhenTriggered繼承自CCNode,這就是爲何做爲繼承自CCSprite的sprite node的自定義類時無效。sprites nodes的自定義類須要繼承自CCSprite class。一樣,button的自定義類須要繼承自CCButton。

如今,你須要轉到XCode,添加PlayTimelineWhenTriggered類。

右鍵點擊Source group,選擇New File。輸入PlayTimelineWhenTriggered,而且讓它做爲CCNode的子類。

#import "CCNode.h"
#import "Trigger.h"
@interface PlayTimelineWhenTriggered : CCNode <TriggerDelegate>
@property NSString* timelineName;

@end

採用TriggerDelegate協議是必要的,這可使類的實例經過Trigger class做爲一個target使用。timelineName屬性是爲了給這個類提供更多的彈性。

若是timelineName是nil或者空string,class會在被激活時播放Default Timeline;不然,會播放給定名字的Timeline。在任何你把PlayTimelineWhenTriggered設置爲自定義類的node上,你能夠經過添加自定義屬性String類型的timelineName指定被播放的動畫。

在.m中添加代碼:

#import "PlayTimelineWhenTriggered.h"

@implementation PlayTimelineWhenTriggered
- (void)didLoadFromCCB {
    if(_timelineName.length == 0) {
        _timelineName = @"Default Timeline";
    }
}
- (void)didTriggerWithNode:(CCNode *)activator {
    [self.animationManager runAnimationsForSequenceNamed:_timelineName];
}
@end

didLoadFromCCB方法用於在_timelineName初始化爲空string或者nil時,分配@"Default Timeline"字符串給_timelineName.

_timelineName變量被編譯器自動建立,由於@property的緣故。didTriggerWithNode方法播放特定名字的動畫效果。

你極可能須要建立不少服從TriggerDelegate協議的類,以實現當triggers被激活時作出不一樣的行爲。可是對於每一個這樣的類,你應該盡力去使用自定義屬性以讓你或者你的團隊改變類的行爲。

若是你運行APP,當player進入trigger區域時,激活Saw1_noautoplay.ccb,會發生崩潰。

 

Avoiding the "Physics Space Locked" Error

之因此崩潰是由於Chipmunk錯誤:你不能在空間被鎖住時手動從新索引對象(

you cannot manually reindex objects while the space is locked)必須等待,直到當前隊列或步驟完成。

報錯狀況:!space->locked

這個錯誤是大部分物理引擎表明性的錯誤:在碰撞事件中,你不能修改物理世界肯定的方面(certain aspects of the physics world),(在Chipmunk中被稱爲space)。在這個例子中,播放動畫會改變node的旋轉狀態。Chipmunk不喜歡這樣,由於物理世界(space)被鎖上了,意味着其中的bodies正在被處理,而且須要一個連續的狀態,只有物理引擎自身能夠對它作出改變。

考慮到播放動畫行爲來自於:一個Trigger實例發送了didTriggerWithNode:消息,這in turn有它的triggerActivatedBy:方法,經過GameScene.m的碰撞回調方法:ccPhysicsCollisionBegin:player:trigger。

幸運的是,有一個快速和簡單的解決方法。

在PlayTimelineWhenTriggered.m中更新didTriggerWithNode:方法,以下:

- (void)didTriggerWithNode:(CCNode *)activator {
    //[self.animationManager runAnimationsForSequenceNamed:_timelineName];
    [self scheduleBlock:^(CCTimer *timer) { [self.animationManager runAnimationsForSequenceNamed:_timelineName];} delay:0.0];
}

安排一個0延遲的block推遲執行block的內容,直到下一time,Cocos2D安排運行blocks。最近的話,這會在下一幀運行。

Tip:若是你發現你常常須要使用scheduleBlock:技巧去阻止space locked錯誤,你應該考慮在Trigger.m中更新triggerActivatedBy:方法,這樣的話,對didTriggerWithNode的調用就已經被推遲了一個scheduled block。若是你這樣作了,你就再也不須要在TriggerDelegate類中schedule blocks,而是全部的didTriggerWithNode:消息都會被推遲。

相關文章
相關標籤/搜索