---恢復內容開始---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:消息都會被推遲。