物理体

本指南讨论如何在 Corona 应用程序中创建 Box2D 物理体,包括多边形和多元素物体。

概述

物理世界基于物理体之间的相互作用。Corona 将这些物理体视为其图形对象的扩展。这些物体可以绑定到 Corona 显示对象,两者之间一一对应,Corona 将自动处理所有位置更新和其他同步任务。

标准对象的读/写属性(例如 xyrotation)应该继续在物理体上正常工作。但是,如果对象的 bodyType 为动态,则物理引擎可能会“反作用”于您手动移动对象的尝试,因为它可能持续受到重力或其他力的影响。

可以使用 object:removeSelf()display.remove() 以通常的方式删除具有物理体的显示对象。在这种情况下,它将从可见屏幕**和**物理模拟中删除(物理体数据将被销毁)。或者,您可以使用 physics.removeBody() 仅删除物理体,同时将核心显示对象保留在屏幕上。有关更多详细信息以及关于碰撞的重要说明,请参阅 销毁对象

创建物理体

Corona 中的所有物理体都是使用 physics.addBody() 函数创建的。这允许您仅用一行代码将任何 Corona 显示对象转换为模拟物理对象,包括分配物理属性。

physics.addBody( object, { properties } )

物理属性

所有物理体都具有三个核心属性,每个属性都可以定义为属性表中的键值对。这些属性是可选的,除非另有指定,否则将应用默认值。

密度

density(密度)乘以物体形状的面积即可确定其质量。此参数基于水的标准值 1.0,因此比水轻的材料(例如木材)的密度低于 1.0,而较重的材料(例如石头)的密度大于 1.0。但是,您可以随意将密度值设置为适合您模拟的值,因为整体对象行为还将取决于重力和像素到米的比例设置(请参阅 物理引擎设置)。默认值为 1.0

摩擦力

friction(摩擦力)可以是任何非负值。值 0.0 表示无摩擦,1.0 表示摩擦力相当强。默认值为 0.0

弹性

bounce(弹性)是 Box2D 属性,称为“恢复系数”,它决定了碰撞后返回的对象速度的多少。大于 0.3 的值相当“有弹性”,弹性值为 1.0 的物体将永远反弹——例如,如果物体掉到地上,它将反弹到大约它掉落的高度。高于 1.0 的弹性值是有效的,它们实际上会在每次碰撞中获得速度。默认值为 0.2,略有弹性。

--examples:

local crate = display.newImage( "crate.png", 100, 200 )
physics.addBody( crate, { density=1.0, friction=0.3, bounce=0.2 } )

local balloon = display.newImage( "balloon.png", 200, 200 )
physics.addBody( balloon, { density=0.1, friction=0.1, bounce=0.4 } )

请注意,预先声明的物理属性表可以多次使用

local crate1 = display.newImage( "crate1.png", 100, 200 )
local crate2 = display.newImage( "crate2.png", 180, 280 )
 
local crateMaterial = { density=1.0, friction=0.3, bounce=0.2 }
 
physics.addBody( crate1, crateMaterial )
physics.addBody( crate2, crateMaterial )

物体类型

物理体可以是三种类型之一:**动态**、**静态** 或 **运动学**。

动态 (dynamic)

动态物体是完全模拟的。它们可以在代码中手动移动,但通常它们会根据重力或反作用碰撞力等力移动。这是 Box2D 中物理对象的默认物体类型。动态物体可以与所有物体类型碰撞。

静态 (static)

静态物体在模拟下不会移动,它们的行为就像具有无限质量一样。静态物体可以由用户手动移动,但它们不接受速度的应用。静态物体仅与动态物体碰撞,而不与其他静态物体或运动学物体碰撞。

运动学 (kinematic)

运动学物体仅根据其速度在模拟下移动。运动学物体**不会**响应重力等力。它们可以由用户手动移动,但通常通过设置它们的速度来移动它们。运动学物体仅与动态物体碰撞,而不与其他运动学物体或静态物体碰撞。

重要

如上所述,某些物体类型会(或不会)与其他物体类型碰撞。在两个物理对象之间的碰撞中,至少**一个**对象必须是动态的,因为这是唯一与任何其他类型碰撞的物体类型。

在 Corona 中声明物体类型可以通过 physics.addBody() 调用内联完成,也可以使用 object.bodyType 在创建后完成。

--inline:
physics.addBody( triangle, "static", { density=1.6, friction=0.5, bounce=0.2 } )

--post-creation:
physics.addBody( triangle, { density=1.6, friction=0.5, bounce=0.2 } )
triangle.bodyType = "static"

矩形物体

默认情况下,physics.addBody() 构造函数将应用一个围绕关联图像或矢量对象边缘的矩形体。这对于平台、大型地面物体和其他简单的矩形物体很有用。请注意,此矩形包括图像周围的透明像素(如果存在)。

local platform = display.newImage( "platform.png", 600, 200 )
physics.addBody( platform, { density=1.0, friction=0.3, bounce=0.2 } )

如果您不希望物理体与此边界矩形匹配,则必须使用圆形 radius 属性或多边形坐标表定义更具体的形状数据(请参阅下文)。

如有疑问,请使用 physics.setDrawMode() 检查物理引擎实际如何考虑该对象。

圆形物体

通过将 `radius` 参数添加到属性表中来创建圆形物体。这适用于球、岩石和其他在计算碰撞时可以被视为近似圆形的物体。

local ball = display.newImage( "ball.png", 100, 100 )
physics.addBody( ball, { radius=50, density=1.0, friction=0.3, bounce=0.2 } )

请注意,Box2D 碰撞几何中不存在非圆形椭圆,因为圆形物体作为一种特殊情况存在。要制作椭圆形物体,请考虑使用多边形坐标表(请参阅下文)。

多边形物体

可以使用 `shape` 参数创建多边形物体。这是一个 Lua 表,包含**x** 和 **y** 坐标对,其中每对定义形状的顶点。这些坐标是相对于显示对象的**中心**指定的——因此,当 `(0,0)` 对应于对象的中心时,顶点 `(-20,-10)` 定义了中心左侧 20 像素和中心上方 10 像素的点。

--triangle:
local triangle = display.newImage( "triangle.png" )
triangle.x = 200
triangle.y = 150

--define the shape table (once created, this can be used multiple times)
local triangleShape = { 0,-35, 37,30, -37,30 }

physics.addBody( triangle, { shape=triangleShape, density=1.6, friction=0.5, bounce=0.2 } )


--pentagon:
local pentagon = display.newImage( "pentagon.png" )
pentagon.x = 200
pentagon.y = 50

local pentagonShape = { 0,-37, 37,-10, 23,34, -23,34, -37,-10 }

physics.addBody( pentagon, { shape=pentagonShape, density=3.0, friction=0.8, bounce=0.3 } )
重要
  • 顶点必须按**顺时针**顺序定义。即使您以逆时针顺序指定顶点,物体在 `hybrid` 或 `debug` 视图中也可能显示正确,但碰撞将无法正常工作。

  • 多边形形状必须完全**凸起**。您不能创建具有凹面的形状,例如碗或杯子。要完成这样的任务,您必须将物体组装成多个多边形,如下面的 多元素物体 中所述。

  • 多边形形状最多可以有 **8 个顶点**,因此最多可以有 8 条边。如果需要更多,则必须将物体组装成多个相邻的多边形。

多元素物体

以上示例假设只有一个元素的物体——矩形、圆形或凸多边形。但是,在更高级的情况下,您需要从**多个**多边形构造物体,以实现更精确的碰撞边界。此外,由于 Box2D 中的多边形必须是**凸**的,因此任何具有凹形的物体都必须通过组装多个物体元素来构造。

创建多元素物体时,每个元素都必须指定为单独的 多边形形状。尽管这些形状是单独声明的,但它们将被视为整个物体的一部分,并且在施加物理力时它们不会独立移动/弯曲。

多元素物体的构造函数与简单的多边形构造函数基本相同——只需在第一个之后声明其他元素即可

physics.addBody( object, "static",
    { bodyElement1 },
    { bodyElement2 },
    --etc.
)

请注意,每个物体元素都可以具有自己独特的物理属性以及其碰撞边界的形状定义。例如

local car = display.newImage( "car.png" )
local roofShape = { -20,-10, 20,-10, 20,10, -20,10 }
local hoodShape = { 0,-35, 37,30, -37,30 }
local trunkShape = { 0,-37, 37,-10, 23,34, -23,34, -37,-10 }

physics.addBody( car, "dynamic",
    { density=3.0, friction=0.5, bounce=0.2, shape=roofShape },
    { density=6.0, friction=0.6, bounce=0.4, shape=hoodShape },
    { density=4.0, friction=0.5, bounce=0.4, shape=trunkShape }
)

偏移/带角度的矩形物体

如果要创建不跨越显示对象的完整宽度和高度的矩形物体,可以定义 `box` 参数并在其中指定所需的键值对。这种类型的物体也可以从显示对象的中心偏移和/或设置为 `0` 以外的角度(旋转)。

  • halfWidth - 物体宽度的一半。此属性是必需的。
  • halfHeight - 物体高度的一半。此属性是必需的。
  • x - 距显示对象中心的 **x** 偏移量 (±)。此属性是可选的,默认为 `0`。
  • y - 距显示对象中心的 **y** 偏移量 (±)。此属性是可选的,默认为 `0`。
  • angle - 物体的角度(旋转)。此属性是可选的,默认为 `0`。
local body = display.newRect( 100, 200, 40, 40 )

local offsetRectParams = { halfWidth=5, halfHeight=10, x=10, y=0, angle=60 }

physics.addBody( body, "dynamic", { box=offsetRectParams } )

边缘形状(链式)物体

可以通过 `chain` 表中的顶点数组定义边缘形状(链式)物体。边缘形状不像多边形物体那样限于凸角。

可选地,您可以使用布尔值 `connectFirstAndLastChainVertex` 参数连接(闭合)链的末端。如果设置为 `true`,第一个和最后一个顶点将通过一条直线连接。如果设置为 `false`(默认值),则边缘形状将具有断开的末端。

local body = display.newRect( 150, 200, 40, 40 )

physics.addBody( body, "static",
    {
        chain={ -120,-140, -100,-90, -80,-60, -40,-20, 0,0, 40,0, 70,-10, 110,-20, 140,-20, 180,-10 },
        connectFirstAndLastChainVertex = true
    }
)
重要

您不应该构造具有自相交线段的边缘形状物体——换句话说,您对顶点的定义不应导致链的任何线段与其他线段相交。这样做可能会破坏形状的预期碰撞检测。

轮廓物体

如果您不想为更复杂的显示对象定义特定的物体形状或多元素物体,Corona 可以通过 graphics.newOutline() 函数自动勾勒显示对象的轮廓。轮廓物体的限制比多边形物体少,例如,轮廓物体形状可以是凸的或凹的。

local image_name = "star.png"

local image_outline = graphics.newOutline( 2, image_name )

local image_star = display.newImageRect( image_name )

physics.addBody( image_star, { outline=image_outline } )

销毁物理体

可以使用 object:removeSelf()display.remove() 销毁物理实体,就像销毁其他显示对象一样。在这种情况下,对象将从可见屏幕物理模拟中移除(物理实体数据将被销毁)。

object:removeSelf()
--OR:
display.remove( object )

或者,您可以使用 physics.removeBody() 仅移除物理实体,同时保留屏幕上的核心显示对象。

physics.removeBody( object )
注意

在这两种情况下,Box2D 实体都将安全地保留到当前世界步骤结束。但是,它的 Lua 引用将立即被删除。因此,您应该避免意外地多次移除同一个 Lua 对象。这可能发生在处理对象碰撞时,在碰撞完全解决之前可能会触发多个事件阶段。为了防止这种情况发生,请使用 event.phase 的条件过滤,例如

local function onCollision( self, event )

    if ( event.phase == "began" ) then
        display.remove( crate1 )
        crate1 = nil
    end
end

传感器

任何实体——或多元素实体的任何特定元素——都可以变成传感器。传感器不会与其他实体发生物理交互,但当其他实体穿过它们时,它们会产生 碰撞 事件。如果您需要检测对象何时与特定区域(传感器)碰撞,但不希望因此产生任何力反应,这将非常有用。

将实体元素设置为传感器可以通过在 physics.addBody() 调用中内联完成,也可以在创建后使用 object.isSensor 完成。

--inline:
physics.addBody( goal, "static", { isSensor=true } )

--post-creation:
physics.addBody( goal, "static" )
goal.isSensor = true
注意

与传感器碰撞的对象将触发 "began" 事件阶段,就像普通的非传感器对象一样,当它们退出传感器的碰撞边界时,它们还会触发 "ended" 事件阶段。此外,对于多元素传感器实体中的每个元素都会发生碰撞事件。

物体属性

在 Corona 中,许多原生的 Box2D 设置/获取方法已被简化为显示对象上的简单属性。以下示例假设已使用其中一种构造方法创建了物理实体 object

object.bodyType

object.bodyType 是物理实体类型的字符串值。可能的值包括 dynamic(动态)、static(静态)或 kinematic(运动)。有关详细信息,请参阅 实体类型 部分。请注意,您不能在碰撞事件期间更改实体类型,因此您必须在轻微、不易察觉的延迟后将此事件排队。

local function onCollisionDelay()
    --change the body type
    object.bodyType = "kinematic"
end

timer.performWithDelay( 10, onCollisionDelay )

object.isAwake

object.isAwake 是实体当前“唤醒”状态的布尔值。默认情况下,所有物理实体在几秒钟内没有任何交互时都会自动“休眠”,并且它们会停止模拟,直到发生碰撞等事件将其唤醒。此属性可以获取对象的当前状态或强制将其唤醒。

object.isSleepingAllowed

object.isSleepingAllowed 是一个布尔值,表示实体是否允许休眠。保持实体唤醒会产生更大的性能开销,通常情况下这是不必要的,因为与其他实体的碰撞会自动将其唤醒。但是,在诸如倾斜重力模拟的情况下,强制保持恒定的唤醒状态很有用,因为休眠的实体不会响应全局重力的变化。

object.isBodyActive

object.isBodyActive 用于设置或获取实体当前的活动状态。非活动实体不会被销毁,但它们会从物理模拟中移除,并停止与其他实体交互。请注意,您不能在碰撞事件期间更改实体的活动状态,因此您必须在轻微、不易察觉的延迟后将此事件排队。

local function onCollisionDelay()
    --change the body's active state to false
    object.isBodyActive = false
end

timer.performWithDelay( 10, onCollisionDelay )

object.isSensor

object.isSensor 是一个只写布尔属性,它在实体的所有元素中设置内部 isSensor=true 属性。有关更多详细信息,请参阅 传感器 部分。由于此属性作用于所有实体元素,因此它会无条件地覆盖各个元素上的任何 isSensor 设置。

object.isFixedRotation

object.isFixedRotation 是一个布尔值,表示实体的旋转是否应该被锁定,即使实体受到偏心力作用。

object.gravityScale

object.gravityScale 可用于调整特定实体上的重力效应。例如,将其设置为 0 会使实体漂浮,即使模拟中的其他对象受到正常重力作用。默认值为 1.0,表示正常重力。您也可以将值设置为高于正常值,但要小心,因为增加重力会降低稳定性。

--make the object float in place, even if the simulation has normal gravity
object.gravityScale = 0

object.angularVelocity

object.angularVelocity 是当前角(旋转)速度的数值,单位为度/秒。

object.angularDamping

object.angularDamping 是一个数值,表示实体的旋转应该被阻尼的程度,即旋转物体在旋转意义上(不是线性)减速到完全停止的速度。默认值为 0,表示实体将无限期地以相同的速度旋转。

object.linearDamping

object.linearDamping 是一个数值,表示实体的线性运动被阻尼的程度,即物体在线性意义上(不是旋转)减速到完全停止的速度。默认值为 `0`,表示实体将无限期地以相同的速度移动。

请注意,恒定线速度的应用是通过下面描述的 object:setLinearVelocity() 方法完成的,这与通过 object.angularVelocity 属性设置的角/旋转速度不同。

object.isBullet

object.isBullet 是一个布尔值,表示在碰撞检测方面是否应将实体视为“子弹”。子弹进行的是连续碰撞检测,而不是周期性检测。这在计算上成本更高,但可以防止快速移动的对象穿过实体障碍物。

物体方法

以下示例假设已使用其中一种构造方法创建了物理实体 object

object:setLinearVelocity()

object:setLinearVelocity() 设置实体线速度的xy 分量,单位为像素/秒。

object:setLinearVelocity( 2, 4 )

object:getLinearVelocity()

object:getLinearVelocity() 返回实体线速度的xy 分量,单位为像素/秒。此函数利用了 Lua 可以返回多个值这一事实,在本例中是两个线速度。

local vx, vy = object:getLinearVelocity()
print( "Linear X velocity = " .. vx )
print( "Linear Y velocity = " .. vy )

object:applyForce()

object:applyForce() 在世界坐标系中的给定点处应用线性力的指定xy 分量。如果目标点是实体的质心,它将倾向于沿直线推动实体。如果目标点偏离中心,实体将绕其质心旋转。对于对称物体,质心和物体的中心将位于相同的位置。

object:applyForce( 500, 2000, object.x, object.y )

object:applyLinearImpulse()

object:applyLinearImpulse()object:applyForce() 类似,不同之处在于冲量是单次、瞬间的力。请参阅下面的 力和冲量 说明。与 object:applyForce() 一样,冲量可以应用于实体上的任何点(质心或偏移点)。

object:applyLinearImpulse( 60, 20, object.x, object.y )

object:applyTorque()

object:applyTorque() 将旋转力应用于实体。正值将导致顺时针扭矩;负值将导致逆时针扭矩。实体将绕其质心旋转。

object:applyAngularImpulse()

object:applyAngularImpulse()object:applyTorque() 类似,不同之处在于冲量是单次、瞬间的力。请参阅下面的 力和冲量 说明。

object:resetMassData()

object:resetMassData() 在实体的默认质量数据被覆盖时很有用。此函数将其重置为根据形状计算出的质量。

力和冲量

一个常见的问题是是否对实体应用冲量。区别在于,冲量旨在模拟对实体的立即冲击/踢动,而力(和扭矩)是随时间施加的东西。因此,为了获得逼真的力/扭矩模拟,您应该在每个应用程序周期中持续应用它,只要您希望力继续下去。您可以为此使用运行时 enterFrame 事件。

local object = display.newImageRect( "leaf.png", 40, 40 )
object.x, object.y = 200,200

physics.addBody( object, "dynamic", { radius=20 } )

local function constantForce()
    object:applyForce( 2, -4, object.x, object.y )
    object:applyTorque( 2 )
end

Runtime:addEventListener( "enterFrame", constantForce )