Sprite Kit中的物理模拟通过添加物理体场景来进行。物理体是一个模拟的物理对象,该对象连接到场景的节点树中的节点。它使用节点的位置和方向把它自身放置在模拟中。每一个物理体具有其他定义模拟如何操作它的特性。这些属性包括物理对象的先天属性,如它的质量或密度,也包括施加于它的属性,如它的速度。这些特性定义了主体如何移动,它在模拟中是如何受到力的影响,以及它是如何响应与其他物理体的碰撞。
每次场景计算一个新的动画帧,它模拟连接到节点树的物理体上的力和碰撞的作用。它为每个物理体计算最终的位置、方向和速度。然后,场景更新每个相应节点的位置和旋转角度。
要在你的游戏中使用物理,你需要:
· 将物理体附加到节点树中的节点上。请参阅“所有物理都在物理体上模拟”。
· 配置物理体的物理属性。请参阅“配置物理体的物理属性”。
· 定义场景的物理模拟的全局特点,如重力。请参阅“配置物理世界。”
· 在游戏需要支持的地方,设置场景中的物理体的速度或对它们施加力或动量(impulses)。请参阅“让物理体移动”。
· 定义场景中的物理体相互接触时如何交互。请参阅“使用碰撞和接触”。
· 优化你的物理模拟来限制它必须执行的计算的数量。请参阅“在游戏中使用物理的提示和技巧。”
SpriteKit使用国际单位制,也被称为SI,或米-千克-秒体制。在必要的情况下,你可能需要查阅其他在线参考资料以了解更多有关Sprite Kit使用的物理方程式的知识。
SKPhysicsBody
对象定义系统中的物理体的形状和模拟参数。当场景模拟物理时,它对所有连接到场景树的物理体执行计算。所以,你创建一个SKPhysicsBody
对象,配置其属性,然后将其赋值给节点的physicsBody
属性。
物理体有三种:
· 动态体积(dynamic volume)模拟系统中一个有体积和质量、可以受力和碰撞作用的物理对象。使用动态体积表示场景中需要移动和互相碰撞的项目。
· 静态体积(static volume)与动态体积相似,但它的速度将被忽略,且它不受力或碰撞的作用。然而,因为它仍然具有体积,其他的对象可以弹开它或与它进行交互。使用静态体积表示场景中占用空间但不应该被模拟移动的项目。例如,你可以使用静态体积表示一个迷宫的墙壁。
把静态和动态体积划分为不同的实体是有用的,在实践中,这是可以适用于任何基于体积的物理体的两种不同的模式。这可以是有用的,因为它可以让你选择性地启用或禁用对主体的作用。
· 边(edge)是一个静态无体积的主体。边从来不会被模拟移动,且它们的质量并不重要。边被用于表示场景内的负空间(negative space)(如另一实体中的空心点)或一条不可逾越且不可见的微薄的边界。例如,边经常用于表示场景的边界。
边和体积之间的主要区别是,边允许在其自身的边界内的移动,而体积被认为是一个固态物体。如果边通过其他方法移动,它们只会与体积交互,而不是与其他的边。
Sprite Kit提供了一些标准的形状,以及基于任意路径的形状。图8-1展示可用的形状。
图8-1 物理体
在大多数情况下,一个物理体应具有尺寸和形状,其值接近对应的节点的可视化表现。例如,在图8-2中,火箭具有一个窄的形状,用圆形或者矩形都不能很好地表现的。选择一个凸多边形形状能与精灵的插图相匹配。
图8-2 匹配形状与接近的表现
然而,在为你的物理体选择一个形状时,不要过于精确。更复杂的形状,需要更多的工作来正确模拟。对于基于体积的主体,请遵循下列准则:
· 圆是最高效的形状。
· 基于路径的多边形是最低效的,且计算工作随着多边形的复杂度成倍增加。
基于边的主体的计算比基于体积的主体开销更昂贵。这是因为与它交互的主体可能是开放边的任一边,或者封闭形状的里面或外面。使用这些准则:
· 线条和矩形是最有效的基于边的主体。
· 边回路(edge loops)和边链(edge chains)是最昂贵的,且计算工作随着路径的复杂度成倍增加。
通过调用SKPhysicsBody
类的方法之一来创建一个物理体。每个类的方法定义要创建基于体积还是基于边的主体,以及它有什么样的形状。
清单8-1所示的代码,经常用于不需要滚动内容的游戏中。在这种情况下,游戏希望物理体击中场景边界而反弹回到游戏区。
清单8-1 场景边界
清单8-2展示的代码,为球形或圆形对象创建物理体。由于物理体被附加到一个精灵对象上,它通常需要体积。在这种情况下,假定精灵图像的锚点接近圆心,然后计算圆的半径并用于创建物理体。
清单8-2 一个圆形精灵的物理体
如果物理体显着小于精灵的图像,用于创建物理体的数据可能需要由另外一些源提供,如一个属性列表。见“Sprite Kit最佳实践”。
SKPhysicsBody
类定义了一些用来确定如何模拟物理体的属性。这些属性会影响到主体对力如何反应、它本身能生成何种力(模拟摩擦)以及它对场景中的碰撞如何反应。在大多数情况下,属性是用来模拟物理效果的。
每个单独的主体也有其自身的属性值来确定它对场景中的力和碰撞如何作出反应。下面是最重要的属性:
· mass
属性决定力是如何影响主体,以及当主体参与碰撞时它有多大的动量。
· friction
属性决定了主体表面的粗糙度。它被用来计算一个主体沿其他主体表面移动时产生的摩擦力。
· linearDamping
和angularDamping
属性是用来计算主体在世界中移动时的摩擦。例如,它可能用于模拟空气或水的摩擦。
· restitution
属性决定主体在碰撞过程中保持能多少能量,即它的弹力。
其他的属性被用来决定模拟在主体本身上如何进行:
· dynamic
属性决定该主体是否由物理子系统来模拟。
· affectedByGravity
属性决定模拟是否对主体产生重力。关于物理世界的更多信息,请参阅“配置物理世界”。
· allowsRotation
属性决定力是否能对主体传递角速度(angularvelocity)。
你应该设置场景中每个基于体积的主体的质量,使它能对施加给它的力作出正确反应。
一个物理体的mass
、area
、density
属性是相互关联的。当你初次创建主体时,主体的体积就计算好了,且以后永远不会改变。另外两个属性值会根据下面的公式同时发生变化:
质量=
密度×
体积
在你配置一个物理体时,你有两种选择:
· 设置主体的mass
属性。然后density
属性会自动重新计算。当你想要精确地控制每一个主体的质量时,这种方法是最有用的。
· 设置主体的density
属性。然后mass
属性会自动重新计算。当你有一组以不同尺寸创建的类似的主体集合时,这种方法是最有用的。例如,如果你的物理体被用来模拟小行星,你可能让所有小行星有相同的密度,然后为每个行星 设置一个合适的多边形边框。每个主体根据它在屏幕上的尺寸自动计算出适当的质量。
大多数情况下,你配置完一个物理体,然后永远不会改变它。例如,一个主体的质量在游戏过程中是不太可能改变的。不过,你并没有被限制这样做。有些种类的游戏可能需要有能力来调整主体的属性,即使模拟正在执行中。这里有几个例子时,你可能这样做:
· 在一个逼真的火箭模拟中,火箭消耗燃料提供推力。在燃料用完时,火箭的质量变化了。为了在Sprite Kit中实现这个,你可以创建一个包括fual
属性的火箭类。当火箭推动时,燃料被减少,重新计算相应主体的质量。
· damping属性通常是基于主体的特性和它要穿越的介质。例如,真空不提供阻力,而水比空气提供更大阻力。如果你的游戏模拟多个环境,并且允许主体在那些环境之间移动,那么每当主体移动到一个新的环境时,需要更新主体的damping属性。
通常情况下,你将这些变化作为场景预处理和后处理的一部分。请参见“高级场景处理。”
场景中的所有物理体都是物理世界的一部分。物理世界是附属于场景的SKPhysicsWorld
对象。它定义了模拟的两个重要的特性:
· gravity
属性对模拟中基于体积的主体施加加速度。静态体和affectedByGravity
属性设置为NO
的物理体则不受影响。
· speed
属性决定了模拟的运行速率。
默认情况下,只有重力施加到场景中的物理体。在某些情况下,这可能是已经足以构建一个游戏。但在大多数情况下,你需要采取其他步骤来改变物理体的速度。
首先,你可以通过设置物理体的velocity
和angularVelocity
属性直接控制其速度。与许多其他特性一样,你通常只在第一次创建物理体时设置这些属性一次,然后让物理模拟在需要加以调整。例如,假设你正在做一个基于空间的游戏,在游戏中火箭飞船可以发射导弹。当船发射一枚导弹时,该导弹应该有一个起始的船的速度,加上一个额外的发射方向的向量。清单8-3展示了一个计算发射速度的实现。
清单8-3 计算导弹的初始速度
一旦主体处于模拟中,更常见的做法是根据作用于主体的力来调整主体的速度。速度变化的另一个来源,碰撞,将在后面讨论。
默认的作用于主体的力的集合包括:
· 物理世界提供的重力
· 由主体自身的属性产生的阻力
· 与系统中的另一个主体接触产生的摩擦力
然而,你也可以把自己的力和动量施加到物理体上。大多数情况下,你在模拟执行前的预处理步骤施加力和动量。你的游戏逻辑负责决定那些力需要施加并调用适当的方法施加那些力。
你可以选择施加一个力或动量:
· 力会作用一段时间,根据在你施加力与下一帧模拟处理之间经过的模拟时间量。所以,持续地施加力到主体,你需要在每次一个新帧正在处理时调用适当的方法。
· 动量使主体的速度发生瞬间变化,这种变化独立于已经过的模拟时间量。这意味着动量通常用于立即改变主体的速度,而力用于持续性效果。
所以,继续回到火箭的例子,火箭飞船可能在火箭开动它的发动机时对它施加了一个力。然而,当它发射导弹时,它可能以火箭自身的速度发射导弹,然后施加一个单一的动量给它来得到最初的爆发速度。
因为力和动量是对相同概念的建模——调整主体的速度——本节其余部分只集中讨论力。你可以用三种方式之一施加力到一个主体:
· 线力(linear force),仅影响主体的线速度。
· 角力(angular force),仅影响主体的角速度。
· 对主体上的一个点施加的力。根据对象的形状和力施加的点的位置,物理模拟计算主体的角速度和线速度的单独改变。
清单8-4展示了一个精灵的子类可能实现来对船舶施加力的代码。它在主引擎被激活时使火箭加速。由于引擎是在火箭的后面,力成直线地施加到火箭,不担心角度变化。代码计算基于火箭当前方向的推力矢量。该方向基于根据相应节点的zRotation
属性,但插图(artwork)的方向可能会跟节点的方向有所不同。推力应始终面向插图。请参阅“绘制你的内容”。
清单8-4 施加火箭推力
清单8-5展示了一个类似的效果,但是这一次火箭通过力而旋转,所以此推力被施加成角推力。
清单8-5 施加侧向推力
迟早,两个主体将试图占据同一空间。由你的游戏决定会发生什么。Sprite Kit允许物理体之间有两种交互:
· 接触(contact)是在你需要知道两个主体在互相触碰(touch)时使用。在大多数情况下,当你在碰撞发生时你需要让游戏变化的话,你可以使用接触。
· 碰撞是用来防止从两个对象穿过对方。当一个人的主体撞击(strike)另一个主体,Sprite Kit自动计算碰撞的结果,并对碰撞中的主体施加动量。
你的游戏配置场景中的物理体,以确定何时应该发生碰撞,以及何时物理体之间的交互需要执行额外的游戏逻辑。限制这些交互不仅对定义游戏逻辑很重要,对从Sprite Kit中获得良好性能也是必要的。Sprite Kit使用两种机制限制每帧中交互的数量:
· 基于边的物理体永远不会与其他基于边的主体发生作用。这意味着,即使你通过重新定位节点来移动基于边的主体,物理体也从来不会发生相互碰撞或接触。
· 每一个物理体都被分类(categorized)。类别(Categories)由你的应用程序定义,每个场景最多可以有32个类别。当你配置一个物理体,你定义它属于哪些类别,以及它想跟哪些类别的主体发生作用。接触和碰撞分别规定。
通过一个例子的探索,接触和碰撞系统更容易理解。在这个例子中,场景被用来实现一个太空游戏。两个火箭飞船在外太空的某部分决斗。这个太空区域有一些可能会跟飞船发生碰撞的行星和小行星。最后,因为这是一场决斗,两个火箭飞船都装备有导弹,他们可以向对方发射。这个简单的描述定义了例子的粗糙的玩法。但是实现这个例子,什么主体在场景中以及它们如何交互需要更精确的表达。
注意: 这个例子也作为示例代码可以获得:SpriteKit PhysicsCollisions。
从上面的描述中,你可以看到,游戏中有4种出现在场景的单位类型(unit types):
· 导弹
· 火箭飞船
· 小行星(Asteroids)
· 行星(Planets)
单位类型的数量之少表明,一列简单的类别将是一个良好的设计。虽然场景被限制为32个类别,这个设计只需要4个,每个单元类型对应一个。每个物理体属于一个且只属于一个类别。因此,导弹可能会以一个精灵节点的形式出现。导弹的精灵节点有一个关联的物理体,而那个主体是属于导弹类别的。其他节点和物理体都以类似的方式定义。
专家提示: 考虑对其他游戏相关的信息用其余类别进行编码。然后,在实现你的游戏逻辑时使用这些类别。例如,在实现一个更高级的火箭游戏的逻辑时,任何以下属性都可能是有用的:
· 人造对象
· 自然对象
· 碰撞过程中发生的爆炸
给定这4个类别后,下一个步骤是定义这些物理体之间允许的交互。接触交互通常是首要解决的,因为它们几乎都是由你自己的游戏逻辑指示的。在许多情况下,如果检测到接触,则你也需要计算碰撞。在接触导致两个物理体之一从场景移除时,这是最常见的。
表8-1描述了火箭游戏的接触交互。
表8-1 火箭的接触网格
|
导弹 |
火箭飞船 |
小行星 |
行星 |
导弹 |
NO |
YES |
YES |
YES |
火箭飞船 |
NO |
YES |
YES |
YES |
小行星 |
NO |
NO |
NO |
YES |
行星 |
NO |
NO |
NO |
NO |
· 飞船接触到飞船、小行星或行星会受到损伤。
· 小行星接触行星会被摧毁。
让这些交互对称是没有必要的,因为Sprite Kit在每帧对每个接触只调用你的委托一次。两个主体的任意一个都可以指定它对接触有兴趣。所以,由于在导弹撞击飞船时它已经请求了一个接触的消息,飞船就不需要请求相同的接触消息了。
下一个步骤是决定场景应该何时计算碰撞。每个主体描述场景中的哪些主体可以在发生撞击时调整自身的速度。表8-2描述了允许的碰撞的一个列表。
表8-2 火箭的碰撞网格
|
导弹 |
火箭飞船 |
小行星 |
行星 |
导弹 |
NO |
NO |
NO |
NO |
火箭飞船 |
NO |
YES |
YES |
YES |
小行星 |
NO |
YES |
YES |
NO |
行星 |
NO |
NO |
NO |
YES |
· 导弹在与其他物体的任何交互中总是被摧毁,所以导弹忽略所有与其他主体的碰撞。同样,导弹被认为质量太轻而不足以在碰撞中移动其他主体。虽然游戏可以选择让导弹与其他的导弹碰撞,但在本游戏中没有选择这样做,因为会有很多飞来飞去的导弹。因为每个导弹可能需要对所有其他导弹进行测试,这些交互将需要大量额外的计算。
· 飞船在乎与飞船、小行星和行星的碰撞。
· 小行星不在乎与行星的碰撞,因为在游戏对接触状态的描述中,小行星会被毁灭。
· 行星只在乎与其他行星发生碰撞。其余的都没有足够的质量移动行星,因此本游戏忽略这些碰撞并避免潜在的昂贵的计算。
一旦你已经确定好你的分类和交互,你需要在你的游戏的代码中实现它。分类和交互各由一个32位掩码(mask)定义。每当一个潜在的交互发生时,每个主体的类别掩码针对其他主体的接触和碰撞掩码进行测试。通过对两个掩码进行逻辑与运算来执行这些测试。如果结果是一个非零数,则该交互发生。
以下是你如何把太空战争设计到Sprite Kit代码中的方法:
1. 定义类别掩码值:
清单8-6 太空决斗的类别掩码值
2. 在一个物理体初始化时,设置其categoryBitMask
、collisionBitMask
和contactTestBitMask
属性。
清单8-7展示了对于表8-1和表8-2中的火箭飞船条目(entry)的一个典型的实现。
清单8-7 对火箭的接触和碰撞掩码赋值
3. 接触委托赋值为场景的物理世界。通常情况下,委托协议由场景实现。这是清单8-8中的情况。
清单8-8 添加场景作为接触委托
4. 实现接触委托方法来添加游戏的逻辑。
清单8-9 接触委托的部分实现
这个例子展示了实现你的接触委托时要考虑的几个重要概念:
· 传递一个声明哪些主体参与碰撞的SKPhysicsContact
对象给委托。
· 接触中的主体可以以任何顺序出现。在火箭的代码中,交互网格总是以类别排好的顺序指定。在这种情况下,在接触发生时代码依赖这个对两个条目进行排序。一个简单的选择可能是检查接触中的两个主体并进行分派(dispatch),如果其中一个主体匹配掩码条目的话。
· 当你的游戏需要使用接触时,你需要根据碰撞的两个主体来确定结果。考虑研究了Double Dispatch模式,并用它来移交(farmout)对系统中的其他对象的工作。在场景中嵌入所有的逻辑会导致冗长、复杂而难于阅读的接触的委托方法。
· 物理体允许访问包含它们的节点。这对访问碰撞中的节点的其他的信息,是非常有用的。例如,你可以使用该节点的类、它的name
属性或存储在其userData
字典中的数据,来确定应如何处理该接触。
当Sprite Kit进行碰撞检测时,它首先要确定场景中所有的物理体的位置。然后确定是否发生了碰撞或接触。这种计算方法很快,但有时可能会导致遗漏(miss)了碰撞。一个小的主体可能移动得相当快,以致于它完全通过了另一个物理体后,在两个物理体相互触碰的地方,根本没有一帧动画。
如果你有必须碰撞的物理体,你可以提示Sprite Kit使用一个更精确的碰撞模型检查交互。这种模式的开销是比较昂贵的,所以应谨慎使用。当任意一个主体使用精确碰撞时,多个运动位置被接触和测试,以确保检测到所有的接触。
虽然你可以使用上面已经描述的物理系统来制作很多有趣的游戏,但是通过把物理体连接在一起,你可以让你的设计更进一步。物理体使用联合(joints)连接。当场景模拟物理时,计算力是如何作用于主体会考虑到这些联合。
图8-3 以不同的方式把节点连接在一起,
表8-3描述了你可以在Sprite Kit中创建的各种联合。
表8-3 SpriteKit中实现的Joint类
类别名称 |
描述 |
|
固定联合在某个参考点融合两个主体在一起。固定联合用于创建以后可以打散的复杂形状。 |
|
滑动联合允许两个主体的锚点沿选定的轴滑动。 |
|
弹簧联合就像弹簧那样,它的长度是两个主体之间的初始距离。 |
|
界限联合限定了两个主体之间的最大距离,就像他们是用绳子连接那样。 |
|
针联合允许两个主体独立地绕锚点旋转,就像钉在一起那样。 |
联合使用物理世界来添加到模拟或从模拟中移除。当你创建一个联合,连接联合的点总是在场景的坐标系中指定。这可能需要你首先把节点坐标转换成场景坐标,然后再创建一个联合。
要在你的游戏中使用物理联合,你遵循以下步骤:
1. 创建两个物理体。
2. 把物理体附加到场景中的一对SKNode
对象。
3. 使用上面列出的子类之一创建一个联合对象。
4. 如果有必要,配置联合对象的属性来定义联合应该如何操作。
5. 取回(Retrieve)场景的SKPhysicsWorld
对象。
6. 调用物理世界的addJoint:
方法。
有时,在场景中查找物理体是必要的。例如,你可能需要:
· 发现物理体是否位于场景的某个区域。
· 检测物理体(如由玩家控制的那个)何时穿过某条特定的线。
· 跟踪两个物理体之间的视线(line-of-sight),看是否有另一个物理体夹在两个对象之间,例如墙。
在某些情况下,这些类型的交互,可以使用碰撞和接触系统实现。例如,要发现一个物理体何时进入一个区域,你可以创建一个物理体并将它附加到场景中的一个无形的节点上。然后,配置物理体的碰撞掩码,以致它从来不与任何东西碰撞,并配置它的接触掩码在检测你感兴趣的物理体。你的接触委托在渴望的交互发生时被调用。
然而,像视线这样的概念,用这种设计却不容易实现。要实现这些,你使用场景的物理世界。通过物理世界,你可以搜索沿着射线的所有物理体或与一个特定的点或矩形相交的物理体。
下面将用一个例子来说明这些基本技术。清单8-10展示视线检测系统的一种可能的实现。它从场景的地点以一个特定的方向投射一条射线,寻找沿着射线最近的物理体。如果找到一个物理体,那么它测试该类别的掩码,看看这是否它应该攻击的一个目标。如果它看到一个目标,就攻击它。
清单8-10 从场景中心发射射线
实现同样行为的另一种方式可能是,将场景中的两个物理体设置为射线的起始和结束位置。例如,你可能会使用玩家的游戏对象的位置作为一个位置而一个敌人单位的位置作为另一个位置。
搜索与点或矩形相交的物理体,执行也是类似的,使用bodyAtPoint:
和bodyInRect:
方法。
有时你不能根据场景内最接近的物理体做一个简单的判断。例如,在你的游戏的逻辑中,你可能会决定,并非所有的物理体都遮挡视线。在这种情况下,你就需要枚举沿着射线的所有物理体。你会使用enumerateBodiesAlongRayStart:end:usingBlock:
方法实现这个。你提供了一个block给沿着射线的每个主体都调用一次。然后,你可以使用此信息,对于视线是否存在目标做出更明智的决定。
构建一个基于物理的游戏时,考虑了以下建议。
在你花大量时间把物理添加到场景前,你应该先了解你包含要什么样的主体到场景以及他们如何互相交流。在这一步,你应该去让每个主体经受这个过程:
· 它是一个边、一个静态体积还是动态体积?请参阅“所有物理都在物理体上模拟”。
· 什么样的形状与主体最接近,牢记一些形状的计算比其他的昂贵?请参阅“使用匹配图形表现的物理形状”。
· 它是什么类型的主体,它如何与其他主体交互?请参阅“使用碰撞和接触”
· 这主体移动得快吗,或者它是很微小(tiny)的吗?如果是这样,决定高精度碰撞检测是否必要。请参阅“对小或快速移动的对象使用高精度碰撞”。
虽然知道Sprite Kit以国际单位制测量项目是有用的,对精确数字的担心并不是那么重要。你的火箭飞船重1公斤还是100万公斤并没有多大关系,只要这个质量与其他在游戏中使用的物理值一致。通常情况下,比例比所使用的实际值更重要。
游戏设计通常是一个反复的过程,因为你在模拟中会调整(tweak)数字。这类设计往往导致在你的游戏中有许多的硬编码的数字。抗拒这样做的冲动!相反,实现这些数字作为可以被相应的节点对象或物理体归档的数据。你的代码应该提供行为,但用于实现这些行为的具体数字应该是可编辑的值,可以由美工或设计师调整或测试。
大部分信息保存在物理体和相应的节点中,物理体和节点可以使用Cocoa创立的标准归档机制进行归档。这暗示着你自己的工具也可能能够保存和加载这些档案,并把它们作为首选的数据格式。这通常是可能的,但是要记住,一个物理体的形状是无法从对象确定的私有数据。这意味着如果在你的工具中你的确使用归档作为保存数据的主要格式,你可能还需要归档用于创建物理体的其他信息。使用Sprite Kit来开发工具的一般话题在“Sprite Kit最佳实践”中描述。
一个物理体只有非常少的特性是固定的。物理体的范围之外,大多数属性可以在任何时候改变。你可以利用这一点。下面是几个例子:
· 一个静态体积可以锁定在适当的地方,直到玩家进行解锁的任务。然后,主体变成一个动态体积并与其他对象交互。
· 可以从多个更小的物理体构造一个框并使用固定联合放到一起。用接触掩码创建各个部分,这样,当他们碰到了什么东西时,接触委托可以打破联合。
· 作为在整个场景移动的对象,根据它所在介质调整它的线性和旋转阻尼。 例如,当对象移动到水中时,更新该属性来匹配。