---恢复内容开始---
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模式,右击关键帧段。
图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:消息都会被推迟。