2013年1月8日
教程:多元素物理体
本周的教程是关于 Corona 物理引擎的另一个教程 — 具体来说,是关于涉及多元素物理体的进阶技巧。
首先,我应该定义一下什么是多元素体。多元素体是由两个或多个“形状”组成的物理体,以创建一个整体。它不是指您通过使用焊接关节或其他关节(如布娃娃)连接多个物理对象而组装成的物理对象。多元素体由多个形状组装而成,但它被视为一个统一的、坚实的整体,其中各个元素不会移动或弯曲。
为什么要使用多元素体?
这对物理老手来说是老生常谈,所以我长话短说。在 Box2D 中,所有物理体都必须使用最多八个边的多边形形状绘制,并且没有凹角。
对于可以在标准凸多边形中定义的物体,这很好。但是,对于不能仅用凸角追踪或不能在八个或更少边中准确追踪的物体又该如何处理?如果您违反这些规则,碰撞反应充其量是“不可预测的”。
解决方案是多元素体:使用多个凸形追踪基本形状以创建一个统一的物体。您应该尝试使用简单的形状和最少的数量来组成您的多元素体。
第一部分 — 每个元素的碰撞控制
如果您以前使用过多元素体,您会知道它们提供了一些强大的功能,但也存在一些限制。让我们先来了解一下这些功能
- 各个元素可以具有唯一的碰撞过滤器。如果您希望多元素体的某些部分与世界中的某些物理对象发生碰撞/反应,而不是与所有物理对象发生碰撞/反应,这将非常有用。
- 可以将各个元素设置为传感器,允许所有其他物体穿过它们,同时仍返回碰撞检测事件。
- 在碰撞中,每个元素都可以返回一个整数,该整数与它在physics.addBody()函数中声明的顺序有关 — 例如,声明的第一个元素将返回
1
,第二个返回2
,依此类推。这允许您找出多元素体的哪个部分参与了碰撞事件,并采取适当的措施。
尽管有这些独特的功能,但以下限制仍然存在
- 一旦为元素或物体声明了碰撞过滤器,就无法在运行时更改它。
- 如果一个元素被声明为传感器,则无法在运行时单独将其更改为非传感器 — 只能在传感器或非传感器的行为之间切换整个物体。
克服这些限制
不要害怕,今天的教程将向您展示如何克服这两个限制。我们将使用物理接触来实现这一点,这是我在之前的教程中介绍的一个功能。如果您还没有阅读它,您可以在这里找到它。
回顾上一个教程,PhysicsContact允许您通过使用预碰撞监听器来预先确定实际发生碰撞时会发生什么。这允许您根据您的应用程序逻辑完全取消碰撞。在本教程中,我们将把这种用法扩展到多元素体。
一个可能的用例是一个多元素的“太空星云”,如果没有更好的说法(上图)。在理论游戏中,英雄星际战斗机必须攻击每个外围护盾舱才能摧毁它们,并清除通往内部中心核的道路。这种情况需要一种独特的方法,因为“传统”方法容易受到这些限制的影响。
- 这个物体不能由几个较小的物体构成并通过关节连接,因为当一个外围舱被摧毁(以及连接到它的关节)时,其余的结构将变得物理不稳定。
- 当使用object.isSensor时,它是“全部或全部不选”的,因此您不能只将一个被摧毁的舱变成传感器,同时确保其他舱保留物理响应。
因此,我们求助于PhysicsContact,结合每个元素的碰撞检测,来解决我们的“可破坏护盾”问题。
组装星云
让我们研究如何在 Corona 中创建多元素体。我们将创建一个 9 元素体来追踪星云。在 Corona 中,我们只需这样做
- 在屏幕上显示我们的星云图像。
- 声明星云的形状,从顶部开始并绕行(为了我们的方便)。请注意,我们必须为外围舱使用八边形,因为您不能在多元素体上“偏移”径向形状。虽然它们不如圆形精确,但八边形应该足以满足我们的碰撞需求。
- 添加物理体并将每个形状按顺序的元素列表传递给 API。必须注意此顺序,因为它与返回碰撞检测的整数有关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
local nebula = display.newImage( "nebula.png" ) nebula.x, nebula.y = display.contentWidth/2, display.contentHeight/2 local podT = { 1,-89, 14,-83, 20,-70, 14,-57, 1,-51, -12,-57, -18,-70, -12,-83 } local beamTR = { 19,-63, 63,-19, 59,-14, 14,-59 } local podR = { 69,-20, 82,-14, 88,-1, 82,12, 69,18, 56,12, 50,-1, 56,-14 } local beamBR = { 19,61, 14,56, 58,13, 62,17 } local podB = { 1,49, 14,55, 20,68, 14,81, 1,87, -12,81, -18,68, -12,55 } local beamBL = { -18,63, -64,17, -59,13, -14,58 } local podL = { -70,-20, -57,-14, -51,-1, -57,12, -70,18, -83,12, -89,-1, -83,-14 } local beamTL = { -18,-65, -14,-61, -59,-15, -64,-20 } physics.addBody( nebula, "dynamic", { shape=podT }, { shape=beamTR }, { shape=podR }, { shape=beamBR }, { shape=podB }, { shape=beamBL }, { shape=podL }, { shape=beamTL }, { radius=24 } -- Radial body used for the nucleus ) local shieldStates = { true, true, true, true, true, true, true, true } |
此外,最后,我们必须设置一个简单的 shieldStates
表来管理我们的八个护盾对象。这将用于确定特定元素是开启还是关闭——或者换句话说,此表将跟踪护盾元素在游戏逻辑中是“完好”还是“被摧毁”。我们可以使用一个简单的包含八个布尔值的非索引表来实现此目的。
基本的预碰撞监听器
接下来,我们将声明基本的预碰撞监听器。如前一篇教程中所述,如果我们打算使用物理接触,则必须使用预碰撞监听器,因为我们将告诉 Corona 立即在碰撞发生之前而不是在碰撞发生时管理碰撞状态。
1 2 3 4 5 6 |
local function nebulaCollide( self,event ) print( event.selfElement ) end nebula.preCollision = nebulaCollide nebula:addEventListener( "preCollision", nebula ) |
此函数仅完成基本功能。根据您声明它们的顺序,任何与星云碰撞的物体都会将该元素的相应整数作为 event.selfElement
返回。因此,由于我们将上部舱声明为第一个元素,因此涉及它的碰撞将返回 1
。与右上梁的碰撞将返回 2
,与右侧舱的碰撞将返回 3
,依此类推。
增强预碰撞监听器
现在我们知道星云的哪个元素参与了碰撞,我们可以将其与我们的 shieldStates
表结合起来,以确定是否应该发生碰撞。如果护盾元素在我们的游戏逻辑中“被摧毁”,我们可以使用物理接触 — event.contact
— 指示 Corona 完全取消碰撞,使其看起来好像该元素甚至不存在(我们的最终目的)。
1 2 3 4 5 6 7 8 9 10 |
local function nebulaCollide( self,event ) -- 从“shieldStates”表查询位置(和状态) local isElementIntact = shieldStates[event.selfElement] if ( isElementIntact == false ) then event.contact.isEnabled = false -- 使用物理接触来避免碰撞 end end |
管理 shieldStates
表格非常简单。要“摧毁”较低的护盾舱(第五个位置),只需编写代码
1 |
shieldStates[5] = false |
在此概念的基础上,你现在可以管理你的星云护盾并制定其他创造性的方法,包括
- 如果一个护盾舱被摧毁,也摧毁相邻的光束。
- 经过一段时间后,“重建”一个护盾舱及其相邻的光束。
- 通过扩展
shieldStates
表格设置来管理每个舱的健康状况。
如你所见,物理接触 与 逐元素检测 相结合,解决了一个传统方法无法克服的难题。
第二部分 — 多元素物体和传感器
现在我们将讨论关于传感器中多元素物体的一个常见的误解。
如果你对 Corona 物理引擎进行过任何程度的实验,你就会知道,一个 传感器 可以是任何合法形状和类型的物理物体(动态、运动学 或 静态),但它不会像弹跳那样与其他物体发生物理反应。
关于多元素物体经常被误解的是,即使你可能认为该物体从碰撞的角度来看是一个整体的统一对象,每个元素都会返回与传感器的碰撞事件。如果一个多元素物体在传感器上漂移时,你突然收到几个“开始”阶段的事件,或者当一个小元素漂移回传感器区域外时,你收到一个“结束”阶段的事件,这可能会导致一些主要的困惑。
这实际上是按设计进行的。例如,你可能需要感知一辆赛车的 前轮 是否漂离了赛道,而驾驶舱仍留在赛道上。然而,如果需要感知整个多元素物体是否在传感器区域内或外,该如何解决呢?例如,一条“跳跃的鱼”完全离开由传感器定义的“水体”?
计算碰撞
可以通过计算鱼身体中每个元素发生的碰撞来解决这个跳跃的鱼的场景。我们将为此构建一个值表,并将其命名为
elementStates
。在“开始”阶段,我们将相应的计数增加 1,在“结束”阶段,我们将计数减少 1。如上所述,每个元素都会返回与传感器的碰撞事件,因此如果四个传感器与一个元素重叠,则其关联的计数将为 4。如果该元素随后漂移到这 三个 传感器之外,则计数将减少到 1。当元素的计数等于 0 时,我们知道它完全在所有传感器的范围之外。
我们还将定义一个名为 elementsIn
的核心属性,以计算鱼的 总共 有多少元素在所有传感器的范围内或范围外。对于这条鱼,此值永远不会超过 5
,因为它是一个 5 元素物体。最后,我们将定义一个名为 inWater
的简单布尔标志,以便我们可以将多个碰撞报告过滤为仅一个“完全在内”和“完全在外”状态的报告。为了方便和编码效率,我们将所有这三项定义为鱼的 属性。
这是基本设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
local fish = display.newImage( "jumpfish.png" ) fish.x, fish.y = display.contentWidth/2, display.contentHeight/2-200 local tail = { -117,12, -123,-46, -68,-13 } local bodyBack = { -89,-26, -61,-39, -20,-46, 20,-49, 42,27, -12,28, -66,16, -94,0 } local bodyFront = { 20,-49, 71,-43, 107,-32, 121,-20, 126,-10, 108,5, 78,19, 43,27 } local finBack = { -39,23, -11,29, -10,41, -32,50 } local finFront = { -9,51, -11,28, 41,27, 15,42 } physics.addBody( fish, "dynamic", { shape=tail }, { shape=bodyBack }, { shape=bodyFront }, { shape=finBack }, { shape=finFront } ) fish.elementStates = { 0,0,0,0,0 } -- 每个元素碰撞计数的表格 fish.elementsIn = 0 fish.inWater = false |
正如您所见,我们定义鱼的元素的方式与第一部分中的星云类似。此外,我们创建了表 elementStates
和属性 elementsIn
和 inWater
。
管理计数
鱼需要一个标准碰撞监听器,而不是预碰撞监听器(这次我们不访问物理接触功能)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
local function fishCollide( self,event ) if ( event.phase == "began" ) then if ( self.elementStates[event.selfElement] == 0 ) then self.elementsIn = self.elementsIn+1 end self.elementStates[event.selfElement] = self.elementStates[event.selfElement]+1 elseif ( event.phase == "ended" ) then self.elementStates[event.selfElement] = self.elementStates[event.selfElement]-1 if ( self.elementStates[event.selfElement] == 0 ) then self.elementsIn = self.elementsIn-1 end end if ( self.elementsIn == 0 and self.inWater == true ) then self.inWater = false print( "FISH ENTIRELY OUT OF THE WATER!" ) elseif ( self.elementsIn == 5 and self.inWater == false ) then self.inWater = true print( "FISH ENTIRELY IN THE WATER!" ) end end fish.collision = fishCollide fish:addEventListener( "collision", fish ) |
现在让我们逐步了解逻辑
began 阶段
- 首先,我们检查碰撞元素的计数是否为
0
。如果是,我们知道此元素是第一次进入传感器区域,我们可以安全地将鱼的总elementsIn
计数增加 1。 - 接下来,我们将此特定元素的计数增加 1。
ended 阶段
- 首先,我们从碰撞元素的计数中减去 1。
- 接下来,我们检查元素的计数是否为
0
。如果是,我们知道它完全位于所有传感器区域之外,并且我们将鱼的总elementsIn
计数减少 1。
条件检查
- 首先,我们检查鱼的总
elementsIn
计数是否为0
并且它之前是否在水中。如果两个条件都通过,我们知道鱼已经完全离开水面,并且我们将inWater
标志设置为false
。 - 对于
elseif
条件,我们检查鱼的elementsIn
计数是否为5
并且它之前没有浸入水中(所有元素)。如果两个条件都通过,我们知道鱼完全在水中,并且我们将inWater
标志设置为true
。
这处理了鱼的传感器碰撞,包括“完全在内部”和“完全在外部”条件。此方法甚至可以与重叠和相邻的传感器一起使用!例如,如果您使用核心主体和顶部的一些“波浪”构建了水传感器区域,则此代码将处理与这些传感器接触的鱼的所有元素。
总结
今天的教程就到这里。正如您所了解的那样,多元素物理体具有一些联合组装体不具备的宝贵特性——但它们也带来了一些障碍。希望本教程向您展示了如何在基于物理的应用程序中克服这些障碍。
David
发布于 06:48h,1 月 9 日这是一篇非常有用的帖子。我真的很喜欢更多从学徒到高级水平的博客帖子,其中有很多示例。太棒了!
谢谢,
David
Kawika
发布于 08:41h,1 月 9 日一个配套视频将有助于理解这个概念!
Brent Sorrentino
发布于 09:21h,1 月 9 日你好 Kawika,
您想看哪一部分?关于每个元素碰撞的部分,还是关于传感器的部分?我或许可以制作一个关于其中一个(或两者)的短视频。
谢谢,Brent
Anshu
发布于 12:36h,1 月 10 日@Brent
如果您不介意,这是我对视频的愿望清单 🙂
一个视频教程,展示
1) 多元素物体方法与联合组装物体方法的优缺点。例如,第一部分中提到的两个限制的图形演示(1. 此物体不能由几个较小的物体构成,并通过关节连接,因为…… 并且 2. 当使用 object.isSensor 时,它是“全有或全无”,所以你不能……)将是完美的
2) 关于这两部分的短视频,就像…… >>关于每个元素碰撞的部分,还是关于传感器的部分?我或许可以制作一个关于其中一个(或两者)的短视频。<<
谢谢
-Anshu
lblake
发布于 14:38h,1 月 10 日你好 Brent,
我想看关于这两个示例的短视频…
Anshu
发布于 12:35h,1 月 10 日@Brent
如果您不介意,这是我对视频的愿望清单 🙂
一个视频教程,展示
1) 多元素物体方法与联合组装物体方法的优缺点。例如,第一部分中提到的两个限制的图形演示(1. 此物体不能由几个较小的物体构成,并通过关节连接,因为…… 并且 2. 当使用 object.isSensor 时,它是“全有或全无”,所以你不能……)将是完美的
2) 关于这两部分的短视频,就像…… >>关于每个元素碰撞的部分,还是关于传感器的部分?我或许可以制作一个关于其中一个(或两者)的短视频。<<
谢谢
-Anshu
Adam
发布于 10:39h,4 月 10 日你好,
很棒的帖子!是否有可用的代码下载?
谢谢,Adam
Brent Sorrentino
发布于 10:44h,4 月 10 日你好 Adam,
通常我会为物理教程打包一个完整的项目,但我没有为本教程打包(抱歉)。但是,您可以查看其他教程中的“物理接触”演示,其中分享了此处讨论的一些原理和概念。
教程
https://www.coronalabs.com/blog/2012/11/27/introducing-physics-event-contact/
项目
https://www.dropbox.com/s/4q1y01fvsvv0s3k/PhysicsContact.zip
谢谢,
Brent Sorrentino
Kumar Vyas
发布于 22:39h,8 月 13 日你好,
有人可以向我解释如何创建形状 tail、bodyBack、bodyFront、finBack、finFront 吗?
local tail = { -117,12, -123,-46, -68,-13 }
local bodyBack = { -89,-26, -61,-39, -20,-46, 20,-49, 42,27, -12,28, -66,16, -94,0 }
local bodyFront = { 20,-49, 71,-43, 107,-32, 121,-20, 126,-10, 108,5, 78,19, 43,27 }
local finBack = { -39,23, -11,29, -10,41, -32,50 }
local finFront = { -9,51, -11,28, 41,27, 15,42 }
我的意思是,如何为尾部等设置这些值,例如 -117,12, -123,-46, -68,-13