多元素碰撞

本教程涵盖了 Corona 物理引擎的一个重要主题,特别是涉及多元素物理刚体的高级策略。

首先,我们应该定义什么是多元素刚体。本质上,多元素刚体是由两个或多个“形状”组成的物理刚体,以创建一个整体。它定义您通过使用焊接关节或其他关节(如布娃娃)连接多个物理刚体而组装的物理对象。相反,一个多元素刚体是由多个形状组装而成,但它被视为一个统一的、坚实的整体,其中各个元素不会移动或弯曲。

为什么使用多元素刚体?

对于物理引擎老手来说,这是老生常谈,但基本上,在 Box2D 中,所有基于形状的物理刚体必须遵守以下规则:

  1. 必须是最多八条边的多边形。
  2. 必须包含任何凹角

这对于可以用标准凸多边形定义的刚体来说是可以的,但是对于不能仅用凸角描绘或不能用八条边或更少边精确表示的刚体呢?

解决方案是多元素刚体,其中精灵/图像由多个凸形形状表示以创建一个统一的刚体。

逐元素碰撞控制

如果您以前使用过多元素刚体,您就会知道它们提供了一些强大的功能,但也存在一些限制。

多元素功能

多元素刚体具有一些独特的功能,可以帮助您克服各种设计障碍,包括:

  • 各个元素可以具有独特的碰撞过滤器。如果您希望多元素刚体的某些部分与世界上某些但不是所有其他物理对象发生碰撞/反应,这将非常有用。

  • 各个元素可以设置为传感器,允许所有其他对象穿过它们,同时仍然返回碰撞检测事件(此方法在允许跳跃教程中使用)。

  • 在碰撞中,每个元素可以返回一个整数,该整数与其在physics.addBody() 函数中声明的顺序有关,例如,声明的第一个元素将返回 1,第二个元素返回 2,依此类推。这允许您精确定位刚体中参与碰撞事件的部分,并采取相应的措施。多元素尽管具有上述功能,但仍然存在以下限制:

多元素限制

一旦为元素或刚体声明了碰撞过滤器,则在运行时无法更改它。

  • 如果将元素声明为传感器,则在运行时无法将其单独更改为

  • 非传感器(反之亦然) — 只有整个刚体可以在创建后在传感器或 之间切换行为。(反之亦然) — 只有整个刚体可以在创建后在传感器或不要担心,本教程将向您展示如何克服上述两个限制!我们将使用物理接触来做到这一点,该功能允许您通过使用碰撞前监听器预先确定碰撞实际发生时会发生什么。这主要用于根据您的应用程序逻辑完全避免碰撞,我们将在本教程中将此方法扩展到

克服限制

刚体。多元素一个可能的用例是多元素“装甲敌人”,如下图所示。在一个理论游戏中,英雄必须攻击这个敌人并摧毁其盔甲的各个部分(头盔、胸甲、盾牌等),才能突破到内部的骨骼。这种情况需要一种独特的方法,因为“传统”方法容易出现以下问题:

虽然敌人可能由几个单独的不同刚体构成,然后使用焊接关节组装,但这会在敌人组装中产生另一个级别的复杂性,并且可能导致某种程度的物理不稳定性,即使使用焊接关节也固有这种不稳定性。

  • 如上所述,

  • 刚体的各个元素可以最初设置为传感器(或不设置为传感器),但在创建它之后,在刚体上使用object.isSensor时,它是多元素“全有或全无”的。因此,您不能仅将一个“被摧毁”的盔甲块设置为传感器,同时确保其他盔甲块保留物理响应,例如导致弹丸物理反弹。由于这些原因,我们转向物理接触并结合

逐元素碰撞检测来克服我们的“可破坏盔甲”障碍。让我们研究如何为这个敌人创建一个

组装刚体

刚体。本质上,方法如下:多元素在屏幕上显示敌人对象。

  1. 定义刚体的形状,为了方便起见,从头部开始,基本上向下进行。
  2. 添加物理刚体,并将每个形状以有序的元素列表传递给physics.addBody() 调用。
  3. 请注意,在第 30 行,我们还声明了一个简单的 armorStates 表来跟踪每个盔甲对象的“状态”。这将用于确定特定元素是
-- Set up physics engine
local physics = require( "physics" )
physics.start()
physics.setDrawMode( "normal" )
physics.setGravity( 0,0 )

local armoredSkeleton = display.newImageRect( "skeleton.png", 200, 256 )
armoredSkeleton.x, armoredSkeleton.y = display.contentCenterX, display.contentCenterY

local armorPieces = {
    helmet = { -38,-103,-26,-118,-15,-120,-4,-118,8,-103,8,-63,-38,-63 },
    mantle = { -68,-55,-52,-63,20,-63,37,-55,47,-44,55,-20,-89,-20,-80,-44 },
    chest = { 44,-44,54,54,-85,54,-76,-44 },
    shield = { 88,-10,98,13,86,65,98,42,66,80,41,86,41,-33,66,-28 },
    faulds = { 48,54,53,80,-87,80,-85,54 },
    legLeft = { -34,80,-34,127,-72,127,-72,80 },
    legRight = { 43,80,43,127,5,127,5,80 }
}

physics.addBody( armoredSkeleton, "dynamic",
    { shape = armorPieces["helmet"] },
    { shape = armorPieces["mantle"] },
    { shape = armorPieces["chest"] },
    { shape = armorPieces["shield"] },
    { shape = armorPieces["faulds"] },
    { shape = armorPieces["legLeft"] },
    { shape = armorPieces["legRight"] }
)

local armorStates = { true, true, true, true, true, true, true }

开启还是关闭 —或者换句话说,确定游戏碰撞逻辑中盔甲元素是“完好无损”还是“被摧毁”。为此,我们可以使用一个简单的非索引表,包含 7 个布尔值(7 个盔甲块),所有值最初都设置为 true如果您将物理绘制模式设置为 "hybrid"(第 4 行)并运行此代码,则骨架将类似于此处的图像。请注意,某些盔甲块与其他盔甲块重叠 — 在这种情况下,这是完全可以接受的,因为玩家仍然需要

逐个摧毁各个盔甲块。.

重要

应注意刚体元素的声明顺序(第 21-27 行),因为它与碰撞检测期间返回的特定元素的整数有关。了解此整数及其与哪个刚体元素相关对于扩展此基本场景至关重要 — 例如,如果您知道头盔被击中,您可能希望将骨架图像/精灵更改为视觉上没有头盔的帧,或者您可能希望根据“头部击中”而不是“腿部击中”来造成额外伤害。

碰撞前监听器

接下来,我们将声明基本的碰撞前监听器。如果我们打算利用物理接触,则必须使用这种类型的监听器,因为我们将告诉 Corona 在碰撞发生之前立即管理碰撞状态,而不是在碰撞发生管理碰撞状态。

local function skeletonHit( self, event )

    print( event.selfElement )
end
 
armoredSkeleton.preCollision = skeletonHit
armoredSkeleton:addEventListener( "preCollision" )

此函数只完成基本操作。与骨架碰撞的任何物体都将返回特定刚体元素的相应整数作为 event.selfElement,根据它们的声明顺序。因此,因为我们将头盔声明为第一个元素,所以涉及头盔的碰撞将返回 1。与披风的碰撞将返回 2,与胸甲的碰撞将返回 3,依此类推。

使用物理接触

此时,skeletonHit() 函数将告诉我们哪个特定的盔甲块参与了碰撞,但仅此而已。这没有达到我们的目标,所以让我们扩展它以访问 armorStates 表并确定是否应该发生碰撞。

local function skeletonHit( self, event )

    -- Dictate the collision behavior based on the armor element state
    if ( armorStates[event.selfElement] == false ) then
        -- Use physics contact to void collision
        event.contact.isEnabled = false
    else
        -- Set the associated armor element state to "destroyed"
        armorStates[event.selfElement] = false
    end
end

armoredSkeleton.preCollision = skeletonHit
armoredSkeleton:addEventListener( "preCollision" )

基本上,如果在我们的游戏逻辑中盔甲元素被“摧毁”(第 35 行),我们可以使用物理接触 (event.contact) 指示 Corona 完全避免碰撞,使其看起来好像该元素根本不存在(我们的最终目的)。相反,如果盔甲元素仍然完好无损,我们允许碰撞自然发生,但我们通过将其在 armorStates 表中的索引设置为 false 来将其状态切换为“被摧毁” — 这将确保盔甲元素在下次碰撞事件中不会引起物理响应。

基本上就是这样!使用此代码,可以将每个盔甲块从活动状态切换到非活动状态,从而让您在精细级别控制游戏逻辑,同时仍然使用单个统一的物理刚体。

如果您真的想发挥创意,可以考虑将此概念扩展到盔甲生命值。在上面的场景中,对任何特定盔甲块的一次击中都会将其“摧毁”,但这不太现实 — 毕竟,盔甲不应该能够在被摧毁之前吸收一定量的伤害吗?

一种方法是为 armorStates 表中的每个盔甲块使用整数生命值(而不是布尔值 true/false。请注意,我们为胸甲(第三个值)设置了比腿部等较弱的盔甲块更高的值(第六个和第七个值):

local armorStates = { 10, 5, 20, 15, 8, 5, 5 }

接下来,调整 skeletonHit() 函数以处理整数而不是布尔值 true/false 值。

local function skeletonHit( self, event )

    -- Dictate the collision behavior based on the armor element state
    if ( armorStates[event.selfElement] == 0 ) then
        -- Use physics contact to void collision
        event.contact.isEnabled = false
    else
        -- Subtract 1 from the armor piece's health value
        armorStates[event.selfElement] = armorStates[event.selfElement] - 1
    end
end

本质上,在第 35 行,我们不再检查值是否为 false,而是检查值是否为0 —这表示盔甲块的“生命值”已减少为零,可以将其视为被摧毁。此外,在第 40 行,我们从盔甲块的生命值中减去 1,而不是像原始版本那样将其设置为 false

另一种创造性的选择是检测骨骼盔甲上的碰撞**力**,根据抛射物撞击盔甲部件的力度/速度来造成更多伤害。这种方法在碰撞后处理的独特性教程中有更详细的描述。

总结

正如您在本教程中学到的那样,多元素物理实体拥有一些关节组装实体所不具备的宝贵特性,但它们也带来了一些障碍。希望这些技巧能够帮助您在基于物理的应用程序中克服这些障碍!

本教程中的人物美术由Ponywolf提供,他是优秀的开源游戏模板(例如Match 3 Space RPGSticker Knight PlatformerEndless Sk8boarder)的创建者。