虽然精灵是建立游戏时使用的最重要的元素,Sprite Kit还提供了许多其他的节点类。这些节点类中的大部分都提供可视化的内容,类似的SKSpriteNode
类。剩下的则不直接绘制自己的内容,而是修改它们在节点树的后代的行为。表6-1列出了所有由Sprite Kit提供的节点类,包括你已经熟悉的SKScene
和SKSpriteNode
类。
表6-1 SpriteKit节点类
类 |
描述 |
|
所有的节点类都从该类派生。它不绘制任何东西。 |
|
场景是在节点树的根节点。它控制动画和动作的处理。 |
|
绘制纹理精灵的节点。 |
|
渲染文本字符串的节点。 |
|
渲染基于Core Graphics路径的形状的节点。 |
|
播放视频内容的节点。 |
|
创建和渲染粒子的节点。 |
|
使用遮罩(mask)修剪其子节点的节点。 |
|
应用Core Image滤镜到其子节点的节点。 |
几乎所有对精灵节点使用的技术都可以应用到其他节点类型。例如,您可以使用动作让屏幕上的其他节点对象动起来,操纵它们的渲染顺序,并在物理模拟内使用它们。请继续阅读来了解如何在你的游戏中使用这些其他节点类。当你对这些类变得熟悉时,你就会明白Sprite Kit所有的可视化能力了。然后您就可以开始设计游戏的外观了。
SKNode
类不绘制任何可视化内容。它的主要作用是提供其他节点类使用的基础行为。然而,这并不意味着在你的游戏中你不能找到有用的方式使用SKNode
对象。下面是一些你可能会在你的游戏引擎内使用基础节点的方式:
· 你有一个由多个节点对象组合的内容,无论是精灵或其他内容的节点。不过,你想在你的游戏中把此内容作为一个单独的对象,而不想令其中任何一个内容节点成为根节点。这时用基本节点是合适的,因为你可以给定它在场景树的位置,然后让所有的其他节点作为其后代。这些个别的零部件,也可以相对于父节点的位置进行移动或调整。
· 使用节点对象组织绘制的内容到一系列的层。例如,许多游戏有一个世界(world)的背景层,有另一个角色层,而文本和其他的游戏信息在第三层。其他游戏有更多的层。创建每个层为基本节点,并把它们按顺序插入到场景中。然后,必要时,可以使个别图层可见或不可见。
· 您需要场景中一个不可见的对象,但要它执行一些其他必要的功能。例如,在一个地牢探索游戏,一个不可见的节点可能用来代表一个隐藏的陷阱。当另一个节点与它相交时,就会触发陷阱。(见“搜索物理主体”。)或另一个例子,你可能会添加一个节点作为另一个节点的子节点,而后者代表玩家在视图中的点的位置。(请参阅“示例:在节点上中心定位场景”。)
在树中用这样的节点代表这些概念有以下优势:
· 您可以通过添加或删除单个节点来添加或删除整个子树。这让场景管理变得有效率。
· 您可以调整的树中的一个节点的属性,这些属性的效果向下传播到节点的后代。例如,如果在基本节点有精灵作为其子节点,旋转基本节点也将旋转所有精灵内容。
· 您可以利用行动、物理接触和其他Sprite Kit的功能来实现此概念。
子类化SKNode
类是一个非常有用的方式在你的游戏中建立更复杂的行为。请参阅“使用子类化来创建您自己的节点行为。”
几乎每个游戏都需要在某些时候显示文本,即使它只是对玩家显示“游戏结束” 。如果你必须自己在OpenGL中实现它,需要相当多的工作才能正确完成。但是Sprite Kit却很容易!SKLabelNode
类完成所有加载字体和创建显示文本所需要的工作。
清单6-1演示了如何创建一个新的文本标签。
清单6-1 添加文本标签
SKLabelNode *winner = [SKLabelNode labelNodeWithFontNamed:@“Chalkduster”]; winner.text = “You Win!” winner.fontSize = 65; winner.fontColor = [SKColor greenColor]; winner.position = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds)); [self addChild:winner];
每次你更改标签节点的属性后,标签节点会在下一次渲染场景时自动更新。
SKShapeNode
类绘制一个标准的CoreGraphics路径。图形路径是可以定义开放或封闭的子路径的直线和曲线的集合。形状节点包含单独的属性来指定线条的颜色和内部填充的颜色。
Shape节点对于不能很容易地分解成纹理精灵的内容是有用的。纹理精灵比形状节点提供更高的性能,所以应在你的游戏引擎谨慎使用它们。然而,形状节点对于在你的游戏内容之上构建和显示调试信息是非常有用的。
清单6-2展示了如何创建一个形状节点的例子。该示例创建一个蓝色填充色和白色边线的圆圈。路径被创建并附加到形状节点的path
属性。
清单6-2 通过路径创建一个形状节点
SKShapeNode *ball = [[SKShapeNode alloc] init]; CGMutablePathRef myPath = CGPathCreateMutable(); CGPathAddArc(MYPATH, NULL, 0.0, 15.0, M_PI*2, YES); ball.path = myPath; ball.lineWidth = 1.0; ball.fillColor = [SKColor blueColor]; ball.strokeColor = [SKColor whiteColor]; ball.glowWidth = 0.5;
从代码中你可以看到形状有三个基本要素:
· 形状的内部填充。fillColor
属性指定了用来填充内部的颜色。
· 形状的边线渲染为一条线。strokeColor和lineWidth
属性定义线条的笔触。
· 从边线扩展的光晕(glow)。glowWidth
和strokeColor
属性定义光晕。
你可以通过设置其颜色为[SKColor clearColor]
禁用任何这些元素。
形状节点提供了一些属性让你控制形状如何融合到帧缓存(framebuffer)中。这些属性的使用方式与SKSpriteNode
类的属性一样。请参阅“融合精灵到帧缓冲中。”
SKVideoNode
类使用AV Foundation框架显示电影内容。与任何其他节点一样,你可以把电影的节点放在节点树内的任何地方,Sprite Kit会正确渲染它。例如,某些用动作定义可能代价高昂的可视化行为,你可能会使用视频节点让它动起来。
视频节点与精灵节点类似,但只提供功能的一个子集:
· size
属性被初始化成视频内容的基本尺寸,但如果你愿意,你可以改变它。视频内容将自动拉伸到新的尺寸。
· anchorPoint
属性定义了内容相对节点位置在什么地方显示。
然而,应遵循以下限制:
· 视频节点总是被均匀拉伸。
· 视频节点不能被着色。
· 视频节点总是使用阿尔法混合模式。
像大部分的节点类那样,创建一个视频节点非常简单。清单6-3展示了一个典型的使用模式。它使用存储在应用程序bundle中的视频初始化视频节点,然后把节点添加到场景。调用节点的的play
方法来启动视频播放。
清单6-3 在场景中显示视频
SKVideoNode *sample = [SKVideoNode videoNodeWithVideoFileNamed:@“sample.m4v”]; sample.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); [self addChild:sample]; [sample play];
节点的play
和pause
方法让你可以控制播放。
如果你需要更精确地控制视频的播放行为,你可以使用AV Foundation从你的视频内容创建AVPlayer
对象,然后使用这个对象初始化视频节点。然后,使用AVPlayer
对象来控制播放,而不是使用节点的播放方法。视频内容将自动显示在视频节点中。欲了解更多信息,请参阅AV Foundation编程指南。
当一个SKEmitterNode
对象被放置在场景中时,它会自动创建并渲染新的粒子。你可以用发射器节点来自动创建特殊效果,包括下雨、爆炸或发射。
粒子类似于SKSpriteNode
对象,它渲染有纹理或无纹理的图像,图像有尺寸、有颜色、且可以融合到场景。但是,粒子在两个重要的方面与精灵不同:
· 粒子的纹理总是均匀拉伸。
· 粒子不能用Sprite Kit中的对象表示(represented)。这意味着你不可以对粒子执行节点相关的任务,也不能给粒子关联物理主体使它们与其他内容相互作用。
粒子是纯粹的可视化对象,他们的行为完全由创建它们的发射器节点定义。发射节点包含很多属性来控制它生成的粒子的行为,包括:
· 粒子的出生率和寿命。你还可以指定发射器自行关闭前能产生的粒子的最大数量。
· 粒子的初始值,包括它的位置、方向、颜色和尺寸。这些初始值通常是随机的。
· 在粒子生命期内应用到在粒子的变化。通常,这些被指定为一个随时间的变化率(rate-of-change)。例如,你可以指定一个粒子以特定的速度旋转,以弧度每秒为单位。发射器每一帧都自动更新粒子的数据。在大多数情况下,你还可以使用关键帧序列(keyframe sequences)创建更复杂的行为。例如,你可以为一个粒子指定一个关键帧序列,让它出来时很小,放大到较大的尺寸,然后在死亡前收缩。
在大多数情况下,你永远不需要在你的游戏中直接配置发射器节点。相反,你应使用Xcode来配置发射器节点的属性。当你改变发射器节点的行为,Xcode立即为你提供一个更新过的视觉效果。一旦完成之后,Xcode可以归档(achieve)配置好的发射器。然后,在运行时,你的游戏使用此归档来实现实例化一个新的发射器节点。
使用Xcode创建你的发射器节点有几个重要的优势:
· 这是学习发射器类的能力的最好方式。
· 你可以更迅速地试验新的粒子效果并立即看到结果。
· 你把粒子效果的设计任务从使用它的编程任务中分离出来。这使得你的美工可以独立于你的游戏代码继续创作新的粒子效果。
有关使用Xcode创建粒子效果的更多信息,请参阅粒子发射器编辑器指南。
清单6-4展示了如何加载由Xcode创建的粒子效果。所有粒子效果都使用Cocoa的标准归档机制保存,所以代码首先创建烟雾效果的一个路径,然后加载归档。
清单6-4 从文件加载粒子效果
- (SKEmitterNode *)newSmokeEmitter { NSString *smokePath = [NSBundle mainBundle] pathForResource:@“smoke” ofType:“skn”]; SKEmitterNode *smoke = [NSKeyedUnarchiver unarchiveObjectWithFile:smokePath]; return smoke; }
SKEmitterNode类提供了许多属性配置发射器节点的行为。事实上,Xcode inspector简单地设置这些属性的值。但是,你可以手工创建发射器节点并配置这些属性,或者你也可以从归档创建一个发射器节点并改变其属性值。例如,假设一下,你使用清单6-4中的烟雾效果来展示对火箭飞船的损坏。随着船受到更多的损坏,你可以提高发射器的出生率来添加更多的烟雾。
用于配置发射器节点的属性的完整列表在SKEmitterNode类参考中描述。然而,首先理解如何创建新的粒子,其次理解一个典型粒子的属性如何在发射器节点中指定,对你是有用的。
只要发射器节点在场景中,它就会发射新粒子。你使用以下属性定义它要创建多少粒子:
· particleBirthRate
属性指定发射器每秒创建的粒子数。
· numParticlesToEmit
属性指定发射器自行关闭之前要创建多少粒子。发射器还可以被配置为产生无限数量的粒子。
当粒子被创建时,它的初始属性值是由发射器的属性决定的。对于每个粒子属性,发射器类声明以下四个属性:
· 属性的平均初始(starting)值。
· 属性值的随机范围。每次发射一个新的粒子,会在该范围内计算一个新的随机值。
· 随时间的变化率,也被称为属性的速度。并非所有属性都有一个速度属性。
· 一个可选的关键帧序列。
清单6-6展示了你可能如何配置发射器的scale
属性。这是节点的xScale
和yScale
属性的一个简化版本,并确定粒子相比它纹理的尺寸有多大。
清单6-5 配置粒子的scale属性
myEmitter.particleScale = 0.3; myEmitter.particleScaleRange = 0.2; myEmitter.particleScaleSpeed = -0.1;
当创建一个新的粒子,其scale值是从0.2
到0.4
的
一个随机数。然后scale值以每秒0.1
的速度减少。所以,如果一个特定的粒子以平均值开始,即0.3
,它会在3
秒内从0.3
减少到0
。
关键帧序列用来为粒子属性提供更复杂的行为。一个关键帧序列,使你可以指定粒子生命期的多个点,并在每个点为属性指定一个值。然后关键帧序列篡改(interpolate)这些点之间的值,并用它们来模拟粒子的属性值。
你可以使用关键帧序列,实现了许多自定义的行为,包括:
· 更改属性值,直到它达到某个指定值。
· 在粒子的整个生命期中使用多个不同的属性值。例如,你可能会在序列中的一部分增加属性的值,而在另一部分减小属性的值。或者,在指定颜色时,你可以指定粒子在其生命期中循环显示多种颜色。
· 使用一个非线性的曲线或分级(stepping)功能改变属性值。
清单6-6展示你可以如何替换清单6-5中的代码来使用序列。当你使用一个序列,值不是随机化的。相反,序列指定所有的属性值。每个关键帧值包含一个值对象和时间戳。时间戳在0
到1.0
的
范围内指定,其中0
表示粒子的诞生而1.0
表示它的死亡。因此,在该序列中,粒子以0.2
的拉伸比例
开始并在序列的四分之一时增加到0.7
。到序列的四分之三时,达到它的最小尺寸0.1
。它保持这个尺寸直到死亡。
清单6-6 使用序列来改变粒子的尺度属性
SKKeyframeSequence * scaleSequence = [[SKKeyframeSequence alloc] initWithKeyframeValues:@[@0.2,@0.7,@0.1 times:@[@0.0,@0.250,@0.75]; myEmitter.particleScaleSequence = scaleSequence;
虽然你没有能力直接访问由Sprite Kit创建的粒子,但是你可以指定一个所有粒子都执行的动作。每当创建新的粒子,粒子发射器告诉该粒子执行动作。你使用动作创建的行为,甚至可以比序列所允许的更复杂。
在粒子上使用动作的目的,是你可以把粒子看作是一个精灵。这意味着你可以执行其他有趣的技巧,如让粒子的纹理动起来。
当发射器创建粒子时,它们被渲染成发射节点的子节点。这意味着它们获得发射器节点的所有特性。所以,如果你旋转发射器节点,所有已产生的粒子的位置也会跟着旋转。根据你使用发射器所模拟的东西,这未必是正确的行为。例如,假设你要使用发射器节点来创建火箭的排气。当引擎在全速燃烧,一个锥形的火焰应该在飞船后面喷出来。这用粒子模拟是很容易的。但是,如果粒子是相对于船渲染的,船转弯时排气也会跟着旋转。那样看起来不对。你真正想要的是粒子产生后,就独立于发射器节点。当发射器节点旋转时,新的粒子有新的方向,而旧的粒子仍保持其原来的方向。你要用目标节点来实现它。
清单6-7展示了如何使用目标节点来配置火箭的排气效果。当自定义精灵节点类实例化排气节点时,它使排气节点成为它本身的子节点。然而,它使粒子重定向到场景。
清单6-7 使用目标节点来重定向产生粒子的地方
- (void)newExhaustNode { SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile: [NSBundle mainBundle] pathForResource:@“exhaust” ofType:@“sks”]; //发射器放置在船的后部。 emitter.position = CGPointMake(0,-40); emitter.name = @“exhaust”; //发送粒子到场景。 emitter.targetNode = self.scene; [self addChild:emitter]; }
当发射器有一个目标节点时,它计算粒子的位置、速度和方向,正如它是该精灵节点的子节点那样。这意味着,如果飞船精灵旋转,排气方向也会自动旋转。然而,一旦这些值计算好,它们被转换到目标节点的坐标系。此后,他们将只受场景节点的属性变化影响。
Sprite Kit中的粒子发射器是构建可视化效果最有力的工具之一。但是,使用不当的话,粒子发射器可能会成为你的应用程序的设计和实施的瓶颈。考虑下面的提示:
· 使用Xcode来创建和测试你的粒子效果,然后在你的游戏中加载归档。
· 在你的游戏的代码里有节制地调整发射器的属性。通常情况下,你这样做是为了指定那些不能由Xcode inspector指定的属性,或者在你的游戏逻辑里控制属性。
· 粒子比精灵节点代价低廉(cheaper),但他们仍然有开销!所以,你应该尽量保持屏幕上的粒子的数量降到最低。尽量以低出生率创建粒子效果,并在粒子一旦不可见时杀掉它们。例如,不是每秒创建数百或数千的粒子,而是降低粒子的出生率并稍微增加粒子的尺寸。通常,你可以用更少的粒子创建效果,但获得同样的净视觉外观(net visual appearance)。
· 除非没有另一种解决方案时,才在粒子上使用动作。在单个(individual)粒子上执行动作可能会是非常昂贵的,特别是如果该粒子发射器还具有较高的出生率的话。
· 每当粒子在产生后就应该独立于发射器节点时,给它们指定目标节点。这通常是发生在发射节点要在场景中移动或旋转的情况下。
· 考虑当粒子发射器在屏幕上不可见时,把它从场景中移除。只在它变得可见前添加它。
SKCropNode
对象不像精灵节点那样直接渲染内容。相反,它改变了它的子节点被渲染时的行为。切割节点允许你裁剪部分由子节点渲染的内容。这使得切割节点对于实现驾驶舱(cockpit)视图、控件和其他游戏的指示器、以及任何子节点不应该绘制在场景的一个特定区域之外的效果,是很有用的。图6-1简单地使用火箭飞船美术对另一个绘制在场景中的精灵应用遮罩(mask)。
图6-1 切割节点进行遮罩操作
裁剪区域通过遮罩指定。该遮罩不是一个固定的图像。它由一个节点渲染,就像Sprite Kit中的任何其他内容。这对简单的口罩和更复杂的行为都是允许的。例如,这里有一些可能你会用来指定一个遮罩的方法:
· 无纹理精灵创建一个遮罩,该遮罩限制内容为场景的一个矩形部分。
· 纹理感精灵是一个像素级精确的遮罩。但也要考虑一个非均匀缩放纹理的好处。这可以允许你创建可以调整尺寸的任意形状的对象。遮罩被调整尺寸和缩放以创建形状,然后动态内容在该遮罩内绘制。
· 可以动态地生成一个复杂遮罩的节点的集合,该遮罩会在每次帧渲染时改变。
清单6-8展示了遮罩的简单使用。它用应用程序bundle中的纹理加载遮罩图像。然后部分场景的内容被渲染,使用遮罩防止它过度绘制(overdrawing)屏幕中游戏用来显示控件的部分。
清单6-8 创建切割节点
SKCropNode * cropNode = [[SKCropNode alloc] init]; myCropNode.position = CGPointMale(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); cropNode.maskNode = [[SKSpriteNode alloc] initWithImageNamed:@“cockpitMask”]; [cropNode addChild:gamePlayNode]; [self addChild:cropNode]; [self addChild:gameControlNodes];
当切割节点在渲染时,遮罩在绘制其后代前先渲染。只与最终遮罩的alpha分量有关。遮罩中任何alpha值为0.05
或更高的像素会呈现。其余的像素会被裁剪。
SKEffectNode
自身不绘制内容。相反,每次使用效果节点渲染新的帧时,效果节点执行一个特效传递给它内容。这个传递经过以下步骤:
1. 效果节点执行一个单独的绘图传递(drawing pass)来渲染其子节点到一个私有帧缓冲区(private framebuffer)。
2. 它应用Core Image效果到私有帧缓冲区。这个阶段是可选的。
3. 然后它融合它的私有帧缓冲区的内容到它父节点的帧缓冲区,使用标准的精灵混合模式之一。
4. 它丢弃其私人的framebuffer。
图6-2展示了效果节点的一个可能的用法。在这个例子中,效果节点的子节点是两个作为灯光节点的精灵。它积累这些灯的效果,应用模糊滤镜来柔化产生的图像,然后使用复合混合模式(multiply blend mode)来应用这个照明到墙壁纹理上。
图6-2 效果节点应用特效到节点的子节点
这里是灯光效果如何产生的过程:
1. 场景中包含两个不同的节点。第一个是表示地面的纹理精灵。第二个是应用灯光的效果节点。
self.lightingNode = [[SKEffectNode alloc] init];
2. 灯光是效果节点的子节点。每个都使用附加混合模式来渲染。
SKSpriteNode *light = [SKSpriteNode spriteWithTexture:lightTexture]; light.blendMode = SKBlendModeAdd; ... [self.lightingNode addChild:light];
3. 应用滤镜来柔化灯光。
如果你指定一个Core Image滤镜,它必须是一个接收单一的输入图像并生成单一的输出图像的滤镜。
-(CIFilter *)blurFilter { CIFilter *filter= [CIFilter filterWithName:@“CIBoxBlur”] // 3 [filter setDefaults]; [filter setValue:[NSNumber numberWithFloat:0] forKey:@“inputRadius”]; return flter; } self.lightingNode.filter = [self blurFilter];
4. 效果节点使用一个复合混合模式来应用照明效果。
self.lightingNode.blendMode = SKBlendModeMultiply;
你已经学了很多关于SKScene
类的东西,但你可能没有注意到,它是SKEffectNode的一个子类!这意味着,任何场景可以对内容应用滤镜。虽然应用滤镜花销可能会非常昂贵(不是所有滤镜都为交互效果精心设计),试验可以帮助你找到一些有趣的方式来使用滤镜。
效果节点通常渲染其内容作为绘制帧的一部分,然后丢弃它们。渲染内容是必要的,因为我们假设内容是每帧都改变的。但是,重新创建这些内容并应用Core Image滤镜的成本可能会非常高。如果内容是静态的,那么这是不必要的。保持渲染的帧缓冲区,而不是抛弃它,可能会更有意义。如果效果节点的内容是静态的,你可以把节点的shouldRasterize
属性设置为YES
。设置此属性将导致以下行为的改变:
· 帧缓冲区在光栅化(rasterization)的末尾不会被丢弃。这也意味着效果节点正在使用更多的内存,而渲染可能需要稍长的时间。
· 当一个新的帧被渲染时,帧缓冲区仅在效果节点的后代的内容已经改变后才会被渲染。
· 更改Core Image滤镜的属性不再导致帧缓冲区的自动更新。你可以通过设置shouldRasterize
的
属性为NO
强制它更新。