构建场景
对于场景的使用,你已经学过了很多的东西。这里对重要的事实再快速回顾一下:
· 场景(SKScene
对象),用来提供SKView
对象要渲染的内容。
· 场景的内容被创建成树状的节点对象。场景是根节点。
· 在场景由视图呈现时,它运行动作并模拟物理,然后渲染节点树。
· 你可以通过子类化SKScene
类创建自定义的场景。
心中有了这些基本概念之后,是时候来学习更多关于节点树和建设场景的知识了。
节点给子节点提供坐标系
当一个节点被放置在节点树中时,它的position
属性把它定位在由它的父节点提供的坐标系内。Sprite Kit在iOS和OS X中使用相同的坐标系。图4-2展示了Sprite Kit的坐标系。与UIKit或AppKit一样,坐标值用点来测量;如果必要,在渲染场景时会把点转换为像素。正数的x坐标在右边而正数的y坐标在屏幕上方。
Sprite Kit还有一个标准的旋转约定(rotation convention)。图4-2展示了相反的坐标约定。弧度为0
的角指定正x轴。沿反时针方向是正角度。
当你的仅使用Sprite Kit代码时,一致的坐标系意味着你可以轻松地在游戏的iOS和OS X版本之间共享代码。然而,它更意味着当你编写特定OS专用(OS-specific)的用户界面代码时,你可能需要在操作系统的视图坐标约定与Sprite Kit坐标系之间进行转换。最常见的情况就是使用iOS视图,它们有一个不同的坐标约定。
只有某些节点包含内容
不是所有的节点都绘制内容。例如,SKSpriteNode
类绘制一个精灵,但SKNode
类不画任何东西。读取某个指定节点对象的frame
属性,你就可以知道它是否绘制内容。节点在父节点的坐标系中绘制,frame代表了它在该坐标系中的可视区域。如果节点绘制内容,frame具有一个非零的尺寸。对于场景,frame总是反映场景坐标空间中的可见部分。
如果一个节点有绘制内容的后代节点,节点的子树也有可能提供内容,即使它本身并不提供任何内容。你可以调用节点的calculateAccumulatedFrame
方法来检索一个矩形,它包括整个绘制节点及它所有后代的区域。
创建场景
场景由视图来呈现。它的很多属性对视图如何呈现场景都有影响。这些属性允许你定义场景的原点位置和场景的尺寸。如果场景的尺寸与视图不匹配,你还可以定义场景缩放方式以适合视图。
场景的尺寸定义其可见区域
在场景首次初始化时,它的size
属性由指定初始化器配置。场景的尺寸以点为单位指定场景中可见部分的尺寸。这只用于指定场景的可见部分。树中的节点可以定位在该区域之外,这些节点仍由场景处理,但被渲染器(renderer)忽略。
使用锚点在视图中定位场景的坐标系
缺省情况下,一个场景的原点被放置在视图的左下角上,如图4-3中所示。因此,一个场景初始化为宽1024
和高768
,在左下角是原点(0,0)
,右上角坐标是(1024,768)
。frame包含(0,0)-(1024,768
)。
场景的position
属性被Scene Kit忽略,因为场景始终是一个节点树的根节点。它的默认值是CGPointZero
,且你不能改变它。但是,你可以通过设置场景的anchorPoint
属性移动它的原点。锚点在单位坐标空间中指定,并选择封闭视图中的一个点。
锚点的默认值是CGPointZero
,放置于左下角。场景的可见坐标空间是从(0,0)
到
(width,hight) 。对于不滚动场景内容的游戏,默认的锚点是最有用的。
锚点第二模式(second-mode)的值通常是(0.5,0.5)
,在中间的视图,如图4-4中所示,把场景的原点定在视图的中心。场景的可视坐标空间是从(-width/2,-hight/ 2)
到(width/2,hight/2)
。
当你想轻松地相对屏幕的中心定位节点时,把场景的锚点定在中心是最有用的,比如一个滚动游戏。
总结一下,anchorPoint
和size
属性用来计算场景的frame,frame包含了场景的可见部分。
缩放场景的内容以适合视图
场景渲染后,它的内容被复制到呈现视图。如果视图和场景的尺寸相同,则内容可以直接复制到视图中。如果两者不一样,那么场景会被缩放以适合视图。scaleMode
属性决定内容如何缩放。
当你设计游戏时,你应该决定处理场景的size
和scaleMode
属性的战略。以下是一些最常见的策略:
· 以恒定尺寸实例化场景,并且永远不改变它。必要时允许Sprite Kit把内容缩放到视图。这场样景有一个可预见的坐标系统和frame。然后,你的美术资产和游戏逻辑可以基于这个坐标系。
· 调整游戏中的场景的尺寸。在必要的地方,调整你的游戏逻辑和美术资产来匹配场景的尺寸。
· 将scaleMode
属性设置为SKSceneScaleModeResizeFill
。Sprite Kit会自动调整场景的尺寸,使其始终与视图的尺寸相匹配。在必要的地方,调整你的游戏逻辑和美术资产来匹配场景的尺寸。
当你计划使用一个恒定尺寸的场景,清单4-1展示了一个典型的实现。与你在“深入Sprite Kit”中创建的例子一样,这个代码指定了第一次呈现场景时要执行的方法。这个方法配置场景的属性,包括它的缩放模式,然后添加内容。在此示例中,缩放模式被设置为SKSceneScaleModeAspectFit
,它
在两个维度上以相同的比例缩放内容,并确保所有的场景的内容都可见。在必要的地方,这种模式会添加黑边(letterboxing)。
- - (void)createSceneContent
- {
- self.scaleMode = SKSceneScaleModeAspectFit;
- self.backgroundColor = [SKColor blackColor];
- / /在这里添加更多的场景内容
- ...
- }
如果你希望在运行时改变场景的尺寸,那么应该用初始的场景尺寸来确定要使用的美术资产,以及任何依赖于场景尺寸的游戏逻辑。你的游戏应该重写场景的didChangeSize:
方法,每当场景变化尺寸时会调用此方法。当这个方法被调用时,你应该更新场景的内容,以匹配新的尺寸。
创建节点树
你可以通过创建节点之间的父子关系的方式来创建节点树。每个节点维护一个有序的子节点列表,可以通过读取节点的children
属性进行引用。子节点在树中的顺序会影响场景处理的多个方面,包括碰撞测试(hit testing)和渲染。所以,适当地组织节点树是很重要的。
表4-1列出了构建节点树最常用的方法。完整的方法列表在SKNode类参考中提供。
表4-1 操作节点树的常用方法
方法 | 描述 |
| 添加一个节点到接收者的子节点列表的末尾。 |
| 插入一个孩子到接收者的子节点列表中的特定位置。 |
| 从父节点中移除接收节点。 |
当你需要直接调整节点树,可以使用表4-2中的属性查看(uncover)树的结构。
表4-2 横移节点树
属性 | 描述 |
| 接收节点的子节点所形成的 |
| 如果该节点是另一个节点的子节点,这个属性指向父节点。否则,它为nil。 |
| 如果该节点包含在场景中的任何地方,它返回作为节点树的根的场景节点。否则,它为nil。 |
理解节点树的绘制顺序
场景渲染的标准行为遵循以下一对简单的规则:
· 父节点先绘制自身的内容再渲染子节点。
· 子节点以它们在子节点数组中的顺序依次渲染。
图4-5展示了如何渲染有三个子节点的节点。
在你在“深入Sprite Kit”写的代码中,创建了一个场景,还有一个飞船和多们岩石。两个灯被指定为飞船的子节点,而飞船和岩石又是场景的子节点。因此,场景用以下方式渲染其内容:
1. 场景渲染它本身,清除内容为它的背景色。
2. 场景渲染飞船节点。
3. 飞船节点渲染它的子节点,即飞船上的灯光。
4. 场景渲染岩石节点,它们在场景的子节点数组中的飞船节点后出现。
重要提醒:SKCropNode
和SKEffectNode
节点类轻微地改变了场景的渲染行为。它们不绘制自己的内容,而是改变它们的子节点在场景中的渲染方式。虽然如此,还是用相同的绘制顺序。要想了解更多信息,请参阅“使用其他节点类型”。
维护节点的子节点的顺序,有时会比你感兴趣的工作还要多。给每个节点一个明确的深度值并允许Sprite Kit管理你的绘制顺序,会更容易。你可以使用节点的zPosition
属性这样做。当一个节点创建时,zPosition的
属性设为0.0
。通过设置节点的z轴位置,相对于它的同级节点,你让它更靠近或更远离顶层的渲染顺序。下面是z轴位置添加后场景的渲染方式:
· 父节点先绘制自身的内容再渲染子节点(不变)。
· 父节点渲染子节点从z值最大的孩子开始,并从z值最小的孩子结束。所以,z轴位置表示从子节点到一个假想的摄像机(camera)位置的距离。
· 如果两个子节点有相同的z值,则在数组中较早出现的那个先绘制。
图4-5展示了有三个子节点的节点。通常情况下,子节点将按它们出现在子节点数组的顺序依次渲染。然而,在这种情况下,这三个节点有自定义的深度值,导致它们以不同的顺序渲染。
碰撞测试的顺序与绘制顺序相反
当Sprite Kit处理场景内的触摸或鼠标事件时,它在场景中查找想接受该事件的最接近节点。如果该节点不想处理事件,则检查下一个最接近的节点,依此类推。处理碰撞测试的顺序基本上是绘制顺序的反方向:
1. 父节点只在它的子节点传给它后才接受事件。
2. 子节点从最小的z值到最大的z值进行处理。
3. 如果两个子节点有相同的z值,先测试数组中后出现的那个。
在碰撞测试中要考虑一个节点,它的userInteractionEnabled
属性必须设置为YES
。场景节点以外的任何节点的默认值都是NO
。要接收事件的节点需要从它的父类(iOS上的UIResponder
和OS X上的NSResponder
)实现适当的响应方法。这是你在Sprite Kit中必须实现特定平台的代码为数不多的地方之一。
有时候,你也想直接查找节点,而不是依赖于标准的事件处理机制。Sprite Kit允许你问一个节点是否有任何的后代节点与坐标系的特定点相交。调用nodeAtPoint:
方法找到的第一个与该点相交的后代节点,或使用nodesAtPoint:
方法接收与该点相交所有节点的数组。
使用节点的深度来添加其他效果
Sprite Kit只使用zPosition的
值来确定碰撞测试和绘制顺序。但是,你可以使用你指定的值来实现自己的游戏特效。例如,你可以:
· 使用的节点的深度来确定节点在屏幕上移动的速度。通过增加不同深度的节点,你可以模拟视差滚动(parallax scrolling)。
· 使用节点的深度来影响它渲染的方式。
搜索节点树
通过组织树中的节点来确定精确的场景渲染顺序,而不是通过那些节点在你游戏中扮演的角色。正因为如此,SKNode
类提供了name
属性。你可以命名一个节点,以区别于树中的其他节点,然后搜索这些节点。
节点的名称应该是没有任何标点的字母数字字符串。清单4-2展示了你可以如何命名三个不同的节点来区分它们彼此。
- playerNode.name = @“player”;
- monsterNode1.name = @“goblin”;
- monsterNode2.name = @“ogre”;
当你的命名游戏的节点时,你应该决定名称是否是唯一的。如果你决定一个节点名是唯一的,那么该名字就是为了识别该节点而不是其他。另一方面,如果节点的名字不是你的游戏中唯一的,它通常代表了相关节点的集合。例如,在清单4-2中,可能游戏中有多个小妖精,你或许想用相同的名称识别它们。但玩家可能是游戏中唯一的节点。
在你的应用程序中,节点名称通常有两个目的:
· 你可以根据节点的名称编写自己的实现游戏逻辑的代码。例如,两个物理对象碰撞时,你可能会使用节点名称来确定碰撞如何影响游戏。
· SpriteKit还为你提供了一些强大的工具来搜索场景内的节点。
SKNode
类实现了搜索节点树的两种方法:
· childNodeWithName:
方法搜索节点的子节点,直到找到一个匹配的节点,然后停止并返回该节点。这种方法通常用于对具有唯一名称的节点进行搜索。
· enumerateChildNodesWithName:usingBlock:
方法搜索节点的子节点,并在找到的每个匹配的节点调用一次block。当你想找到的所有节点共享同一个名称时,你可以使用此方法。
清单4-3展示了在你的场景类上你可以如何创建方法来查找玩家节点。
- - (SKNode *)playerNode
- return [self childNodeWithName:@“player”];
当这个方法在场景上调用时,场景搜索它的子节点(且仅搜索子节点)中名称
属性匹配搜索字符串的节点,然后返回这个节点。当指定搜索字符串时,你可以指定节点的名称或类的名称。例如,如果你为玩家节点创建了自己的子类,并把它命名为PlayerSprite
,那么你可以指定PlayerSprite
作为搜索字符串代替player
;它
将返回相同的节点。
高级搜索
默认的搜索只搜索一个节点的子节点,而且必须完全匹配节点或类的名称。然而,Sprite Kit提供了一个表达式搜索语法,允许你进行更高级的搜索。例如,你可以像之前一样做同样的搜索,但搜索整个场景树。或者你可以搜索节点的子节点,但匹配某个模式,而不需要精确匹配。
表4-3描述了不同的语法选项。搜索使用常见的正则表达式语义。
表4-3 搜索语法选项
表4-4展示了一些有用的搜索字符串来帮助你入门。
表4-4 搜索示例
节点的很多属性适用于它的后代
当你改变一个节点的属性,效果往往传播到该节点的后代。净效果是一个子节点的渲染不仅基于它自身的属性,也基于它祖先的属性。
表4-5 属性影响节点的后代
坐标空间之间的转换
在使用节点树时,有时你需要把位置从一个坐标空间转换到另一个。例如,当你指定物理系统中的关节(joints),关节位置被指定在场景坐标。所以,如果你在本地坐标系有那些点,你需要将它们转换为场景的坐标空间。
清单4-4展示了如何将一个节点的位置转换到场景坐标系中。场景被要求进行转换。记住一个节点的位置在它父节点的坐标系统中指定,所以代码传递node.parent
作为要
转换的节点。你可以通过调用convertPoint:toNode:
方
法执行反向的相同转换。
- CGPoint positionInScene = [node.scene convertPoint:node.position fromnode:node.parent];
你需要进行坐标转换的一个情况是在执行事件处理的时候。鼠标和触摸事件需要从window坐标转换到视图坐标,并从那里进入场景。为了简化你需要写的代码, Sprite Kit增加了一些方便的方法:
· 在iOS上,使用UITouch
对象的locationInNode:
和previousLocationInNode:
将
触摸位置转换到节点的坐标系。
· 在OS X上,使用NSEvent
对象的locationInNode:
方法,将鼠标事件转换到节点的坐标系。