碰撞检测

本指南讨论如何在物理对象之间处理碰撞,或忽略(过滤)特定对象集之间的碰撞。

碰撞事件

物理引擎碰撞事件通过标准 Corona 事件监听器模型公开,具有三种类型:

collision(碰撞)

对于常规碰撞检测,您应该监听名为 `“collision”`(参考) 的事件。`“collision”` 事件包含 `“began”`(开始)和 `“ended”`(结束)两个阶段,分别表示初始接触和接触断开的时刻。如果您没有实现 `“collision”` 监听器,则此事件不会触发。

preCollision(碰撞前)

`“preCollision”` 事件(参考)在对象开始交互之前立即触发。此事件通常与下文所述的 physicsContact(物理接触) 结合使用。

请注意,preCollision 事件非常“嘈杂”,每次接触可能会报告多次,从而可能影响性能。因此,只有在您真正需要碰撞前检测时,才应该监听这些事件。我们还建议您在感兴趣的对象上使用局部监听器,而不是全局监听物理世界中的所有 `“preCollision”` 事件。

postCollision(碰撞后)

`“postCollision”` 事件(参考)在对象交互之后立即触发。这是唯一报告碰撞力和摩擦力的事件。有关此主题的更多信息,请参阅下面的 碰撞力和摩擦力 部分。如果您没有实现 `“postCollision”` 监听器,则此事件不会触发。

与 `“preCollision”` 事件一样,postCollision 事件非常“嘈杂”,每次接触可能会报告多次,从而可能影响性能。因此,只有在您真正需要碰撞后检测时,才应该监听这些事件。我们还建议您在感兴趣的对象上使用局部监听器,而不是全局监听物理世界中的所有 `“postCollision”` 事件。

某些物体类型会(或不会)与其他物体类型发生碰撞。在两个物理对象之间的碰撞中,至少**一个**对象必须是动态的,因为这是唯一与任何其他类型碰撞的物体类型。有关物体类型的详细信息,请参阅 物理物体 指南。

重要

目前,如果 Corona 代码尝试修改仍在碰撞中的对象,Box2D 物理引擎很容易在碰撞期间崩溃。这是因为 Box2D 仍在计算这些对象的迭代数学。但是,您的碰撞处理程序可以设置一个标志或通过 timer.performWithDelay() 包含一个时间延迟,以便操作可以在下一个应用程序周期或之后发生。

可以通过 display.remove()object:removeSelf() 在同一碰撞事件时间步长内完全删除对象,但在碰撞事件期间**不能**调用以下 API 和方法:

传播规则

Corona 中的碰撞事件具有类似于触摸事件的传播模型。您可以使用它通过限制创建的事件数量来进一步优化游戏性能。默认情况下,两个对象之间的碰撞将为第一个对象触发一个局部事件,然后为第二个对象触发一个局部事件,然后在 Runtime 对象中触发一个全局事件,假设所有对象都启用了活动的监听器。但是,您可能只对其中的一些信息感兴趣。

任何返回 `true` 的碰撞事件处理程序都将停止该碰撞事件的进一步传播,即使还有其他监听器本来可以接收到它。这允许您进一步限制创建并传递给 Lua 端的事件数量。虽然单个事件的开销不是很大,但是大量的事件会影响整体性能,因此限制事件传播是一个好习惯。

碰撞处理

碰撞在成对的对象之间报告,可以使用对象监听器在对象上**局部**检测,或者使用运行时监听器**全局**检测。

重要

在使用 Corona 显示组 和 Box2D 时,务必记住 Box2D 期望所有物理对象共享一个**全局坐标系**。分组和未分组的显示对象都可以正常工作,因为它们将共享该组的内部坐标。但是,如果将物理对象添加到不同的显示组中,并且这些组彼此独立地移动、缩放或旋转,则会出现意外结果。作为一般规则,**不要**更改包含物理对象的显示组的位置、比例或旋转。请参阅 物理引擎注意事项/限制

局部碰撞处理

局部碰撞处理最适合用于一对多碰撞场景,例如一个玩家对象可能与多个敌人碰撞,道具等。对于局部碰撞处理,每个碰撞事件都包含一个 `self` 表 ID,表示对象本身,以及 `event.other`,其中包含参与碰撞的另一个 Corona 显示对象的表 ID。由于 Corona 显示对象的行为类似于 Lua 表,您可以随意将任意数据添加到这些表中,例如名称、类别指示符、点值,甚至存储的函数,然后在碰撞时检索这些数据。例如,您可能希望以易于访问的字符串格式存储对象名称。

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" )

全局碰撞处理

全局碰撞处理最适合用于多对多碰撞场景,例如多个英雄角色可能与多个敌人碰撞。对于全局碰撞处理,每个碰撞事件都包含 `event.object1` 和 `event.object2`,它们指示对参与碰撞的两个对象的引用。同样,您可能希望以字符串格式存储每个对象的名称,并在碰撞事件期间检索它。

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`,或者它们可能颠倒过来。因此,如果您在条件语句中比较这两个对象,则可能需要构建一个多条件子句来检测这两种可能性。

多元素碰撞

涉及多元素物体(请参阅 物理物体 指南)的碰撞事件还会返回参与碰撞的具体物体部件。这允许更精细的游戏逻辑 - 例如,您可以决定与火箭头部碰撞的对象比与其尾翼碰撞的对象造成更大的伤害。

对于多元素一个物体,其元素由一个数字**索引**标识,其中添加到物体的第一个元素的编号为 `1`,第二个元素的编号为 `2`,第三个元素的编号为 `3`,依此类推。在至少涉及一个多元素物体的碰撞事件期间,将返回以下属性:

对于**局部**碰撞事件,将返回两个额外的整数值:

  1. event.selfElement
  2. event.otherElement

类似地,**全局**碰撞事件返回两个额外的值:

  1. event.element1
  2. event.element2

在每种情况下,这些值都将指示参与碰撞的**物体元素**的**索引**。例如,假设一个对象是一枚火箭,具有三个物体元素:头部、驾驶舱和尾部。并假设物体元素是按该特定顺序配置的:首先是头部,其次是驾驶舱,最后是尾部。如果在其尾部发生碰撞,则 `event.selfElement` 的值将为 `3`。如果在其驾驶舱发生碰撞,则 `event.selfElement` 的值将为 `2`。

射线投射

Corona 包括对**射线投射**的内置支持。这允许您从一个点到另一个点发射一束射线(一条直线),用它来检测该路径中是否存在一个或多个物理物体。有关实现射线投射的详细信息,请参阅 射线投射和反射 教程。

物理接触

physicsContact(物理接触) 由 Box2D 创建,用于在特殊情况下管理/覆盖碰撞行为。通常用于 preCollision 事件检测中,这允许您覆盖碰撞的某些属性,甚至完全 void 碰撞。

例如,在平台游戏中,您可能希望构建单侧平台,角色可以垂直跳过该平台,但只能从下方跳过。您可以通过比较角色和平台的位置来查看角色是否在平台下方,然后通过在 preCollision 事件处理程序中将 event.contact.isEnabled 设置为 `false` 来完全 void 碰撞。

物理接触还可以用于独立于物体的固有设置来修改碰撞的反弹和摩擦力。例如,使用 preCollision 事件监听器,您可以有条件地检查两个特定对象类型是否发生碰撞,然后分别通过 event.contact.bounceevent.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。此条件逻辑可用于仅在碰撞力高于某个阈值时才执行操作 - 例如,您可以决定游戏角色仅在弹丸以足够大的力撞击它时才受到伤害。过滤也很重要,因为 `“postCollision”` 事件会在对象“稳定”时记录一系列越来越小的力,并且通常最好忽略低于所选阈值的力报告。

碰撞过滤

在某些情况下,您可能希望完全避免某些对象之间的碰撞交互。例如,玩家发射的子弹显然应该与敌人碰撞,但通常不需要让子弹与……碰撞道具。在 Corona 中,有两种方法可以实现这一点

categoryBits/maskBits(类别位/掩码位)

此方法涉及通过“碰撞过滤器”定义将 categoryBitsmaskBits 分配给您的对象,这是一个在 body 构造期间分配给 filter 键的可选表。一个对象仅在其 categoryBits 属于其分配的 maskBits 中时才会与其他对象发生碰撞。通常,一个对象只分配一个类别位,但可能有一个或多个掩码位,具体取决于它应该与哪些其他事物发生碰撞。

在下面的示例中,redCollisionFiltermaskBits 值为 3。此过滤器应用于 redSquare 对象,因此 redSquare 将仅与类别位为 12 的对象发生碰撞 — 在这种情况下,包括其他红色方块 (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. 观察最上一行的数字:以二进制递增的 1512。这些“位”值是确定碰撞过滤器值的关键方面。对于复杂的游戏,您可以继续超过 512 — 必要时最多可达 32768(16 列) — 但对于大多数游戏来说,10 列就足够了。

  2. 现在观察每种对象类型的行。由于每种对象类型必须有两个碰撞过滤器的值categoryBitsmaskBits,因此每行细分为类别碰撞对象

  3. 对于游戏中的每种对象类型,从最上一行中选择一个位数,并在其关联的类别子行中标记一个带圆圈的 此数字对于每种对象类型都应该是唯一的 — 请对多种对象类型使用相同的类别编号。

  4. 接下来,确定每种对象类型是否可以与相同类型的其他对象发生碰撞。如果为 true,则在碰撞对象子行中标记一个带圆圈的 在本例中,请注意小行星应与其他小行星碰撞,外星人应与其他外星人碰撞,但子弹不应与其他子弹碰撞。

  5. 接下来,对于每种对象类型,检查其行上方和下方的所有其他对象类型。如果该对象类型应与任何其他对象类型发生碰撞,请在其他类型的碰撞对象子行中标记一个 x。记住将每个 x 保留在与您为对象类型选择的类别位值 () 相同的列中。

  6. 请注意,碰撞过滤器以“反射”方式工作,您必须考虑所有关联的碰撞对象类型。例如,小行星和外星人应该与子弹碰撞,因此可能很容易在子弹碰撞对象行中为每个标记一个 x,然后停止该过程。但是,您还必须在小行星和外星人的子弹位数列中标记一个 x。如果您未能考虑这种关联中的对象类型,则碰撞过滤器将无法正常工作!

  7. 当所有对象类型都完成后,逐行处理图表以确定总和列值。对于每个子行,将所有标记的总位值相加(求和)。例如,玩家碰撞对象子行在位列 24 中有一个 x,因此正确的总和值为 6

  8. 计算所有总和值后,只需将碰撞过滤器的 categoryBits 值设置为类别总和。同样,将过滤器的 maskBits 值设置为碰撞对象总和。例如,总和为 16 的玩家对象过滤器可以配置如下

local playerCollisionFilter = { categoryBits=1, maskBits=6 }
重要

如果您稍后添加其他游戏元素,则必须再次处理整个图表(所有对象类型)因为如果新的对象类型应与任何先前的对象类型发生碰撞,则这些先前的对象类型的值可能会更改。因此,建议您将过滤器声明为 Lua 变量,无论何时何地创建它们,多个游戏对象都可以访问这些变量。

groupIndex(组索引)

另一种碰撞过滤方法是为每个对象分配一个 groupIndex。此值可以是正整数或负整数,它可能是指定碰撞规则的更简单方法。具有相同正 groupIndex 值的对象将始终相互碰撞,而具有相同负 groupIndex 值的对象将永远不会相互碰撞。

local greenCollisionFilter = { groupIndex = -2 }

如果同时将 groupIndexcollisionBits/maskBits 分配给一个对象,则 groupIndex 具有更高的优先级。