本指南讨论如何在物理对象之间处理碰撞,或忽略(过滤)特定对象集之间的碰撞。
物理引擎碰撞事件通过标准 Corona 事件监听器模型公开,具有三种类型:
对于常规碰撞检测,您应该监听名为 `“collision”`(参考) 的事件。`“collision”` 事件包含 `“began”`(开始)和 `“ended”`(结束)两个阶段,分别表示初始接触和接触断开的时刻。如果您没有实现 `“collision”` 监听器,则此事件不会触发。
`“preCollision”` 事件(参考)在对象开始交互之前立即触发。此事件通常与下文所述的 physicsContact(物理接触) 结合使用。
请注意,preCollision 事件非常“嘈杂”,每次接触可能会报告多次,从而可能影响性能。因此,只有在您真正需要
`“postCollision”` 事件(参考)在对象交互之后立即触发。这是唯一报告碰撞力和摩擦力的事件。有关此主题的更多信息,请参阅下面的 碰撞力和摩擦力 部分。如果您没有实现 `“postCollision”` 监听器,则此事件不会触发。
与 `“preCollision”` 事件一样,postCollision 事件非常“嘈杂”,每次接触可能会报告多次,从而可能影响性能。因此,只有在您真正需要
某些物体类型
目前,如果 Corona 代码尝试修改仍在碰撞中的对象,Box2D 物理引擎很容易在碰撞期间崩溃。这是因为 Box2D 仍在计算这些对象的迭代数学。但是,您的碰撞处理程序可以设置一个标志或通过 timer.performWithDelay() 包含一个时间延迟,以便操作可以在下一个应用程序周期或之后发生。
可以通过 display.remove() 或 object:removeSelf() 在同一碰撞事件时间步长内完全删除对象,但在碰撞事件期间**不能**调用以下 API 和方法:
Corona 中的碰撞事件具有类似于触摸事件的传播模型。您可以使用它通过限制创建的事件数量来进一步优化游戏性能。默认情况下,两个对象之间的碰撞将为第一个对象触发一个局部事件,然后为第二个对象触发一个局部事件,然后在 Runtime 对象中触发一个全局事件,假设所有对象都启用了活动的监听器。但是,您可能只对其中的一些信息感兴趣。
任何返回 `true` 的碰撞事件处理程序都将停止该碰撞事件的进一步传播,即使还有其他监听器本来可以接收到它。这允许您进一步限制创建并传递给 Lua 端的事件数量。虽然单个事件的开销不是很大,但是大量的事件会影响整体性能,因此限制事件传播是一个好习惯。
碰撞在成对的对象之间报告,可以使用对象监听器在对象上**局部**检测,或者使用运行时监听器**全局**检测。
在使用 Corona 显示组 和 Box2D 时,务必记住 Box2D 期望所有物理对象共享一个**全局坐标系**。分组和未分组的显示对象都可以正常工作,因为它们将共享该组的内部坐标。但是,如果将物理对象添加到不同的显示组中,并且这些组彼此独立地移动、缩放或旋转,则会出现意外结果。作为一般规则,**不要**更改包含物理对象的显示组的位置、比例或旋转。请参阅 物理引擎注意事项/限制
局部碰撞处理最适合用于
local crate1 = display.newImage( "crate.png" ) physics.addBody( crate1, { density=3.0, friction=0.5, bounce=0.3 } ) crate1.myName = "first crate" local crate2 = display.newImage( "crate.png" ) physics.addBody( crate2, { density=3.0, friction=0.5, bounce=0.3 } ) crate2.myName = "second crate" local function onLocalCollision( self, event ) if ( event.phase == "began" ) then print( self.myName .. ": collision began with " .. event.other.myName ) elseif ( event.phase == "ended" ) then print( self.myName .. ": collision ended with " .. event.other.myName ) end end crate1.collision = onLocalCollision crate1:addEventListener( "collision" ) crate2.collision = onLocalCollision crate2:addEventListener( "collision" )
全局碰撞处理最适合用于
local crate1 = display.newImage( "crate.png", 100, 200 ) physics.addBody( crate1, { density = 1.0, friction = 0.3, bounce = 0.2 } ) crate1.myName = "first crate" local crate2 = display.newImage( "crate.png", 100, 120 ) physics.addBody( crate2, { density = 1.0, friction = 0.3, bounce = 0.2 } ) crate2.myName = "second crate" local function onGlobalCollision( event ) if ( event.phase == "began" ) then print( "began: " .. event.object1.myName .. " and " .. event.object2.myName ) elseif ( event.phase == "ended" ) then print( "ended: " .. event.object1.myName .. " and " .. event.object2.myName ) end end Runtime:addEventListener( "collision", onGlobalCollision )
使用全局方法检测碰撞时,无法确定哪个是参与碰撞的“第一个”和“第二个”对象。换句话说,`event.object1` 和 `event.object2` 分别可以是 `crate1` 和 `crate2`,或者它们可能颠倒过来。因此,如果您在条件语句中比较这两个对象,则可能需要构建一个
涉及
对于
对于**局部**碰撞事件,将返回两个额外的整数值:
event.selfElement
event.otherElement
类似地,**全局**碰撞事件返回两个额外的值:
event.element1
event.element2
在每种情况下,这些值都将指示参与碰撞的**物体元素**的**索引**。例如,假设一个对象是一枚火箭,具有三个物体元素:头部、驾驶舱和尾部。并假设物体元素是按该特定顺序配置的:首先是头部,其次是驾驶舱,最后是尾部。如果在其尾部发生碰撞,则 `event.selfElement` 的值将为 `3`。如果在其驾驶舱发生碰撞,则 `event.selfElement` 的值将为 `2`。
Corona 包括对**射线投射**的内置支持。这允许您从一个点到另一个点发射一束
physicsContact(物理接触) 由 Box2D 创建,用于在特殊情况下管理/覆盖碰撞行为。通常用于 preCollision 事件检测中,这允许您覆盖碰撞的某些属性,甚至完全 void 碰撞。
例如,在平台游戏中,您可能希望构建
物理接触还可以用于独立于物体的固有设置来修改碰撞的反弹和摩擦力。例如,使用 preCollision 事件监听器,您可以有条件地检查两个特定对象类型是否发生碰撞,然后分别通过 event.contact.bounce 或 event.contact.friction 增加或减少反弹或摩擦力。一个实际的例子可能是“弹球”游戏,其中可以使用 physicsContact 来实现以下目标:
发生碰撞后,您可以使用 postCollision 事件检测来获取碰撞的直接力,以及两个物体之间的侧向力,这实际上是摩擦力。
碰撞的直接力在 `“postCollision”` 事件中报告为 `event.force`,摩擦力可作为 `event.friction` 使用。
local function onPostCollision( self, event ) if ( event.force > 1.0 ) then print( "force: " .. event.force ) print( "friction: " .. event.friction ) end end object.postCollision = onPostCollision object:addEventListener( "postCollision" )
在上面的示例中,非常小的力被有条件地过滤掉event.force > 1.0
在某些情况下,您可能希望完全避免某些对象之间的碰撞交互。例如,玩家发射的子弹显然应该与敌人碰撞,但通常不需要让子弹与……碰撞
此方法涉及通过“碰撞过滤器”定义将 categoryBits
和 maskBits
分配给您的对象,这是一个在 body 构造期间分配给 filter
键的可选表。一个对象仅在其 categoryBits
属于其分配的 maskBits
中时才会与其他对象发生碰撞。通常,一个对象只分配一个类别位,但可能有一个或多个掩码位,具体取决于它应该与哪些其他事物发生碰撞。
在下面的示例中,redCollisionFilter
的 maskBits
值为 3
。此过滤器应用于 redSquare
对象,因此 redSquare
将仅与类别位为 1
或 2
的对象发生碰撞 — 在这种情况下,包括其他红色方块 (categoryBits=2
) 和地板 (categoryBits=1
)。同时,它将穿过任何蓝色方块 (categoryBits=4
),因为位 4
不包含在其 maskBits
值中。
local floorCollisionFilter = { categoryBits=1, maskBits=6 } -- Floor collides only with 2 and 4 local redCollisionFilter = { categoryBits=2, maskBits=3 } -- Red collides only with 1 and 2 local blueCollisionFilter = { categoryBits=4, maskBits=5 } -- Blue collides only with 1 and 4 local floor = display.newRect( 0, 0, 320, 80 ) physics.addBody( floor, "static", { bounce=0.8, filter=floorCollisionFilter } ) local redSquare = display.newRect( 0, 80, 40, 40 ) redSquare:setFillColor( 1, 0, 0 ) physics.addBody( redSquare, { friction=0, filter=redCollisionFilter } ) local blueSquare = display.newRect( 80, 80, 40, 40 ) blueSquare:setFillColor( 0, 0, 1 ) physics.addBody( blueSquare, { friction=0, filter=blueCollisionFilter } )
可以使用简单的
观察最上一行的数字:以二进制递增的 1
到 512
。这些“位”值是确定碰撞过滤器值的关键方面。对于复杂的游戏,您可以继续超过 512
— 必要时最多可达 32768
(16 列) — 但对于大多数游戏来说,10 列就足够了。
现在观察每种对象类型的行。由于每种对象类型必须有两个碰撞过滤器的值categoryBits
和 maskBits
)
对于游戏中的每种对象类型,从最上一行中选择一个位数,并在其关联的类别
接下来,确定每种对象类型是否可以与相同类型的其他对象发生碰撞。如果为 true,则在碰撞对象
接下来,对于每种对象类型,检查其行上方和下方的所有其他对象类型。如果该对象类型应与任何其他对象类型发生碰撞,请在其他类型的碰撞对象
请注意,碰撞过滤器以“反射”方式工作,您必须考虑所有关联的碰撞对象类型。例如,小行星和外星人应该与子弹碰撞,因此可能很容易在子弹碰撞对象行中为每个标记一个 x,然后停止该过程。但是,您还必须在小行星和外星人的子弹位数列中标记一个 x。如果您未能考虑这种关联中的对象类型,则碰撞过滤器将无法正常工作!
当所有对象类型都完成后,逐行处理图表以确定总和列值。对于每个2
和 4
中有一个 x,因此正确的总和值为 6
。
计算所有总和值后,只需将碰撞过滤器的 categoryBits
值设置为类别总和。同样,将过滤器的 maskBits
值设置为碰撞对象总和。例如,总和为 1
和 6
的玩家对象过滤器可以配置如下
local playerCollisionFilter = { categoryBits=1, maskBits=6 }
如果您稍后添加其他游戏元素,则必须再次处理整个图表
另一种碰撞过滤方法是为每个对象分配一个 groupIndex
。此值可以是正整数或负整数,它可能是指定碰撞规则的更简单方法。具有相同正 groupIndex
值的对象将始终相互碰撞,而具有相同负 groupIndex
值的对象将永远不会相互碰撞。
local greenCollisionFilter = { groupIndex = -2 }
如果同时将 groupIndex
和 collisionBits
/maskBits
分配给一个对象,则 groupIndex
具有更高的优先级。