Box2D物理引擎入门

一、什么是Box2D

Box2D是一个强大的开源物理游戏引擎,用来模拟2D刚体物体运动和碰撞,由Erin Catto早在2007年用C++语言开发。

Box2D集成了大量的物理力学和运动学的计算,并将物理模拟过程封装到类对象中,将对物体的操作,以简单友好的接口提供给开发者。我们只需要调用引擎中相应的对象或函数,就可以模拟现实生活中的加速、减速、抛物线运动、万有引力、碰撞反弹等等各种真实的物理运动。(引用百度百科)

简单的说,Box2D就是一个物理刚体仿真库

二、如何学习使用Box2D

Box2D是一个独立的引擎框架,它的作用是帮助游戏开发者进行一些复杂的物理模拟运算,但是很多情况下它是作为某些游戏引擎的一个子模块存在的。所以,我们可以借助一些游戏引擎来进行学习。对于iPhone、iPad和Android游戏,可以使用Cocos2d游戏引擎和Corona Framework。

关于原生Box2D的学习资料,网络上面是多如牛毛,博客的话在CSDN和博客园上面都有。书籍方面也有诸如《Box2D物理游戏编程初学者指南》之类的东西。至于这些教程写的到底好不好,这也只能仁者见仁,智者见智了。(反正我没看)

我主要是基于bbframework框架开发游戏应用的时候使用到了这款出色的物理引擎,所以本文就在bbframework上进行介绍。

使用的工具:

Sublime Text
quick-x-player
关于bbframework(简称:bb),它是基于quick-cocos2d-x框架的一个再封装框架,其核心应该可以说是cocos2d-Lua引擎(或者cocos2d-X)。而cocos2d其本身并不支持box2d物理引擎(cocos2d-JS除外),所以目前在lua上的box2d接口都是公司通过lua绑定将原生的C++接口绑定到lua上的。(难免有些API和C++原生不太一样)

三、基本概念

Box2D物理引擎里面的所有类名都是以“b2”作为前缀的,以下是几个比较重要的类。

1、世界(b2World)

物理世界只是一个抽象的概念,可以将其理解成是一个盒子,盒子里面放的是各种个样的数学模型和物理模型(或者说就是N多的数学公式和物理公式),所有的物理模拟都在这个盒子内完成

物理世界和cocos2d的渲染世界不同,渲染世界由场景、层和精灵等组成,在游戏运行时,渲染世界是可以看见(渲染显示)、可以摸到(绑定触摸事件)真实存在的。而物理世界的一切就跟万物的灵魂一样,看不见也摸不着,都是默默在后台运行的一些数据片段

万物都是因为混沌初开,世界形成才存在的。同样,要使用物理引擎里面的东西,一切也都要从创建世界开始。代码如下:

 -- 创建世界
    local world = b2World(b2Vec2(0, -9.8))

    -- 允许静止的物体休眠
    world:SetAllowSleeping(true)
    -- 开启连续物理检测,使模拟更加的真实
    world:SetContinuousPhysics(true)

创建世界可以通过调用b2World(gravity)函数进行创建,该函数的参数gravity是物理世界的重力加速度(g)。在物理学中,加速度是具有大小和方向的矢量,所以该参数可以使用二维向量来表示,其数据类型是b2Vec2,创建矢量可以直接调用b2Vec2(x, y)函数。

由于物理运算经常伴随着平方、立方、开平方和开立方,甚至是更高次的幂运算,所以其计算量是非常大的,对于性能的消耗也是非常可观。而游戏恰恰又非常强调运行的流畅性,所以很多时候当物体处于禁止状态的时候并不需要实时进行物理运算,这时候就可以将其从物理模拟中暂时的剔除出去,以提高整体的计算效率。调用物理世界对象的SetAllowSleeping(isSleep)方法就可以设置世界内的物体是否在禁止的时候休眠,处于休眠状态的物体将不参与物理运算。

同时,为了物理模拟的更加真实,通常还需要开启物理世界的连续检测。调用物理世界对象的SetContinuousPhysics(bool)方法便可以设置是否开启连续检测。(连续检测会消耗一定的性能)

世界非常的大,可以说是无边无际,然而游戏设备的屏幕是固定大小的,游戏的渲染画面也就那么大,所以为了保证物理模拟的物体处于可见的画面中,通常还需要给定一个边缘,用于表示物理模拟的世界大小,所有的物体都添加到这个边界里面。

物理世界里面的东西都可以看成是由刚体组成的,所以世界的边界我们也可以创建一个四边形刚体来表示,关于刚体的创建详见下文。

2、刚体(b2Body)

首先,要知道什么是刚体?以《愤怒的小鸟》这款游戏为例,小鸟在离开弹弓之后的运行状态完全是根据真实世界的物理效果进行变化的,那么在物理运算的时候,就需要一个刚体来表示小鸟(但不是小鸟本身),以参与物理运算所以,刚体就是物理世界里面要进行物理模拟的物体

在cocos2d中,你可以简单的把刚体当成是一个数据对象,这个对象里面包含了各种各样用于进行物理运算的数据(比如:质量、位置、旋转角度等)。

那么,什么样的东西适合在物理世界里面创建成刚体呢?物理世界简单的可以包含气体、液体和固体,Box2D是一个刚体仿真库,对于气体和液体的模拟并不是它的职责,所以它适合模拟的东西只剩下固体了,而且是那种在物理模拟中不会发生形变的固体(任何物体都会发生形变,这里只是一种理想状态)。

由于刚体是现实世界物体的一个仿真模拟,所以刚体也必须包含一些现实物体的物理属性,这些属性可以简单的称之为对刚体的描述或者是定义。所以在创建刚体之前,需要先创建该刚体的刚体描述,用来描述刚体的物理属性。

首先来看下Box2D原生对刚体描述的定义:

b2BodyDef()
{
    // 用户数据
    userData = NULL;
    // 刚体位置
    position.Set(0.0f, 0.0f);
    // 刚体角度
    angle = 0.0f;
    // 刚体线性速度
    linearVelocity.Set(0.0f, 0.0f);
    // 刚体角速度
    angularVelocity = 0.0f;
    // 刚体线性阻尼
    linearDamping = 0.0f;
    // 刚体角度阻尼
    angularDamping = 0.0f;
    // 刚体是否可以进行休眠
    allowSleep = true;
    // 刚体初始状态是否处于唤醒状态
    awake = true;
    // 刚体是否固定旋转角度
    fixedRotation = false;
    // 刚体是否是一个快速移动的物体,为了防止发生击穿想象,开启它会增加处理时间
    bullet = false;
    // 刚体类型
    type = b2_staticBody;
    // 刚体是否处于活跃状态
    active = true;
    // 刚体所受重力加速度影响的倍数
    gravityScale = 1.0f;
}

b2BodyDef是刚体描述的结构体类型,它可以包含以上14种物理信息。创建b2BodyDef的代码如下:

 local bodyDef           = b2BodyDef()
    -- 类型:静态(b2_staticBody),平台(b2_kinematicBody),动态(b2_dynamicBody)
    bodyDef.type            = b2_staticBody
    bodyDef.position        = b2Vec2(0, 0)
    bodyDef.angle           = math.rad(0)
    -- 用户数据:存储用户的数据,可以是任何类型的数据。一般要求存储的数据的类型是一致的
    bodyDef.userData        = nil 
    bodyDef.angularDamping  = 0
    bodyDef.linearDamping   = 0
    bodyDef.fixedRotation   = false

调用b2BodyDef()函数便可以创建一个刚体描述对象,然后我们可以随意设置一些描述信息(不设置的时候,它们都有默认值)。

这里有注意的是:

1、刚体的类型:刚体类型分为静态刚体、平台刚体和动态刚体,对应值分别是:0、1和2。静态刚体是不受力的作用而进行移动的,用于模拟地面、墙面等禁止的物体;平台刚体可用于模拟游戏内的移动平台等物体,这些物体和地面等几乎一样,但是可以进行位置移动等;动态刚体是最常见的,所有会动的物体都创建为动态刚体。

2、刚体的位置:在Box2D中可以使用b2Vec2类型的向量来表示坐标点

3、刚体的角度:在Box2D中,刚体的角度是使用弧度制,并非和cocos2d一样的角度制。

4、用户数据:用户数据用于保存一些程序员想要附加给刚体的信息,任何数据类型都可以,一般我们用于保存刚体对应的那个精灵节点对象(CCSprite)。

创建完刚体的描述,就可以通过描述对象告诉物理世界需要创建一个什么样子的物体了。创建刚体的代码如下:

-- 创建一个刚体对象,根据刚体定义创建
local body = world:CreateBody(bodyDef)

通过调用物理世界对象的CreateBody(bodyDef)方法,物理世界就可以根据传递进去的bodyDef对象创建一个对应的刚体对象。

3、形状(b2Shap)

创建好的刚体其实只是一个包含一些物理量的一个质点(有质量但是没有大小的点),然而现实世界中的物体是有各种各样的大小和形状的,所以我们还需要为刚体创建对应的形状。(物体的碰撞模拟也需要借助于形状

Box2D内置了以下几种简单形状:

  • 链条(b2ChainShape)
  • 圆形(b2CircleShape)
  • 边线(b2EdgeShape)
  • 多边形(b2PolygonShape)

除了以上几种之外,还可以借助PhysicsEditor等物理形状编辑器进行描点来创建更加复杂的形状。

在上文介绍世界的时候说到需要创建一个四边形当成物理世界的边界,那么这里可以选择用四条边首位相连,围成一个四边形。代码如下:

local shape1         = b2EdgeShape()
shape1:Set(b2Vec2(0 / 32, 0 / 32), b2Vec2(960 / 32, 0 / 32))
local shape2         = b2EdgeShape()
shape2:Set(b2Vec2(0 / 32, 0 / 32), b2Vec2(0 / 32, 540 / 32))
local shape3         = b2EdgeShape()
shape3:Set(b2Vec2(0 / 32, 540 / 32), b2Vec2(960 / 32, 540 / 32))
local shape4         = b2EdgeShape()
shape4:Set(b2Vec2(960 / 32, 0 / 32), b2Vec2(960 / 32, 540 / 32))

创建b2EdgeShape同样可以通过调用b2EdgeShape()函数来实现,然后调用b2EdgeShape对象的Set(fromPoint, toPoint)方法来指定边线的起点和终点。

这里我游戏的设计分辨率是 960 X 540 ,然后我创建的是一个和游戏设计分辨率同等尺寸的四边形,但是可以看到起点和终点的x、y坐标值都被我除以了32,这是因为Box2D使用的度量是以“米”为单位,而cocos2d的坐标系是以像素为单位的,通常设置其转换比例是1:32,也就是32像素的距离等价于Box2D中的1米,这样的模拟效果是比较好的。

4、夹具(b2Fixture)

创建好形状之后,需要将形状和对应的刚体进行绑定,这样刚体才能拥有形状。b2Fixture类就是用于将形状绑定到刚体上的,b2Fixture我们可以将其称为“夹具”或者“材质”

在创建b2Fixture之前,也需要先创建对应的材质描述对象(b2FixtureDef),设定一些材质信息。材质描述的定义如下:

b2FixtureDef()
{
    // 形状
    shape = NULL;
    // 用户数据
    userData = NULL;
    // 摩擦系数
    friction = 0.2f;
    // 恢复系数
    restitution = 0.0f;
    // 密度
    density = 0.0f;
    // 是否为传感器
    isSensor = false;
}

材质信息中的形状和用户数据请参考上文,这里就不在赘述了。重点看下以下几个属性:

摩擦系数:用于影响刚体的运动,取值通常在区间[0, 1],当然也可以更大。
恢复系数:或者称之为“弹性系数”,用于刚体碰撞后能量的损失计算。取值通常在区间[0, 1],当然也可以更大。0表示发生非弹性碰撞,1表示发生完全弹性碰撞
密度:密度通常用于计算刚体的质量,间接的影响刚体的惯性。
是否为传感器:当设置isSensor为true时,刚体发生碰撞的时候并不会发生碰撞响应(反弹),但是会接收到碰撞的信号,所以该属性可以理解为传感器
同样,根据上面创建好的四条边来创建四个材质定义对象,代码如下:

-- 创建材质描述
local fixtureDef1    = b2FixtureDef()
fixtureDef1.shape    = shape1
local fixtureDef2    = b2FixtureDef()
fixtureDef2.shape    = shape2
local fixtureDef3    = b2FixtureDef()
fixtureDef3.shape    = shape3
local fixtureDef4    = b2FixtureDef()
fixtureDef4.shape    = shape4

这里创建材质描述是调用b2FixtureDef()函数来实现,然后设置了描述对象的形状信息,其它的信息全部使用默认的即可。

有了材质描述,接下来就可以创建对应的夹具(材质)了,代码如下:

– 创建四个夹具

body:CreateFixture(fixtureDef1)
body:CreateFixture(fixtureDef2)
body:CreateFixture(fixtureDef3)
body:CreateFixture(fixtureDef4)

创建夹具的方法是调用刚体的CreateFixture(b2FixtureDef)方法来实现的,并且夹具会见材质上的信息与该刚体进行绑定,一个刚体可以拥有多个夹具。

四、物理调试(Debug)

上文说过,物理世界的一切都是看不见的,但是有时候为了方便排错,可以用其它的方法让物理模拟变得可见。

比如:我们创建好了一个刚体,我们想要知道刚体对应到cocos2d渲染世界里面的位置,那么我们可以在cocos2d渲染世界里面创建一个lable标签或者一个sprite精灵,并放到刚体的位置上面,这样我们就等同于是让刚体可见了。而对于边线、圆之类的刚体形状,我们可以使用一些游戏引擎的绘图API在渲染世界内对应的进行绘制,这样形状也可以看到了。很多时候将这些数据进行可视化会帮助游戏开发者更好的进行物理排错。

在bbframework中,可以使用以下代码进行物理的可视化操作:

local debugDraw = GB2DebugDrawLayer:create(world, 32)
self:add(debugDraw, 9999)
GB2DebugDrawLayer这个类专门用于负责物理对象的可视化模拟,调用该类的create(b2World, PTM_RATIO)方法进行构造时,需要传入物理世界对象和cocos2d与Box2D的度量单位比例(像素/米)。然后将GB2DebugDrawLayer的实例对象添加到当前场景的Layer上。

这样我们便可以在渲染世界里面看到物理模拟的效果了。
这里写图片描述
物理模拟可视化

如上图所示,我们可以看到屏幕的边缘有红色或者绿色的边线,那就是上面创建的世界边缘的四条边。

物理编辑器篇(PhysicsEditor)

在Box2D中,除了可以使用内置的几种基本形状之外,它还支持从外部文件中进行材质(b2Fixture)的创建。在创建一些复杂的刚体形状时,单纯的使用多边形描点是一个比较费劲的工作,这时候借助PhysicsEditor这款出色的物理材质编辑器来进行可视化编辑会大大提高效率。

一、形状编辑
这里写图片描述
PhysicsEditor主界面

如上图所示,PhysicsEditor的主界面大致分为四个区域(工具栏、形状列表、形状编辑面板和参数编辑面板)。

1、工具栏(ToolsBar)

工具栏位于界面的顶部(如果Windows版本的话,在工具栏上方还有有一行菜单栏),工具栏从左往右依次包含以下6个工具项。

  • New:新建文件。
  • Open:打开一个已有的物理材质编辑文件。
  • Save:保存当前的编辑文件。
  • Publish:导出配置文件。
  • Publish As:导出所有。
  • Add Sprites:添加精灵。

2、形状列表(Shapes)

形状列表的面板默认在主界面的左边位置,该面板显示的是文件编辑的形状。为了方便编辑形状,开发者可以直接将要编辑的形状对应的图片拖进该面板松开。
这里写图片描述
形状列表面板

如果要删除某个形状,只需要在Shapes面板上面选中要删除的形状,然后点击面板右下角的删除按钮即可。(图片的名字会默认为形状的名称,以便导出到文件最后在代码里面使用,所以一般使用合法标识符对图片命名。)
这里写图片描述
删除按钮

3、形状编辑面板(Editor)
主界面中央是用于编辑形状的面板,选中Shapes面板上面的形状后,该面板会显示对应的图片,开发者可以根据图片的轮廓进行描点,编辑出想要的形状。
这里写图片描述
形状编辑面板

形状编辑面板的顶部是一组形状编辑工具,从左往右依次是:圆形、多边形、魔术棒、左右对称、上下对称、顶点的x坐标值、顶点的y坐标值和一个删除按钮。

3.1、魔术棒
这里写图片描述
魔术棒顶点信息
在形状列表里面选择一个形状(我这里的carBody_1),这样便可以编辑carBody_1的形状了。先从魔术棒开始吧,点击魔术棒之后,编辑器会弹窗(如上图)显示自动描绘的形状顶点,直接点击右下角的“OK”按钮。物理编辑器根据图片的像素点自动生成了一个多边形(如下图)。
这里写图片描述
魔术棒生成的形状

3.2、多边形
虽然编辑器可以使用魔术棒自动生成多边形,但是这样形成的多边形顶点个数太多,会影响物理模拟的性能(计算量增加了),所以对于多边形,通常建议开发者使用多边形手动编辑(不用太过精确)。

点击多边形按钮,我们可以看到多生成了一个三角形(多边形默认为三角形),如下图:

这里写图片描述
多边形
新建形状后,原本使用魔术棒生成的形状会变灰掉,开发者可以点击灰掉的形状(形状变成红色选中状态),然后点击编辑面板右上角的删除按钮(y值右边的那个按钮)去除不要的形状。

生成的多边形默认在图片的左下角位置,开发者可以手动将其拖拽到合适的位置。然后,可以在形状相应的边线附近双击添加一个顶点(也可以右键创建一个顶点)。
这里写图片描述

编辑好的多边形
编辑好形状之后,可以选中一个顶点,然后顶部的x和y后面的文本框就会现实该顶点的坐标值,开发者可以手动输入进行微调(如上图)。

3.3、圆形

在形状列表选择wheel_1,然后点击形状编辑面板顶部的圆形按钮,这样就可以创建一个圆的形状。圆形在形状右边(3点钟位置)有一个顶点,拖拽它便可以调整圆形的大小。

3.4、形状镜像显示

如果要把编辑好的形状变成它的左右翻转镜像或者上下翻转的镜像,只需要点击一下左右对称或者上下对称按钮,这里就不再赘述(自己玩去,点个按钮反正很简单,不想写,任性–!)。

3.5、缩放编辑面板

由于编辑面板的大小受限,很多时候如果要编辑的图片比较大,或者想要描点更加精确,开发者可以使用形状编辑面板底部的缩放功能,默认是原图大小。

4、参数面板(Parameters)

参数面板分为:Exporter(导出)、Image parameters(图片参数)、Body parameters(刚体参数)和Fixture parameters(材质/夹具参数)四个部分。

在没有创建形状的时候,参数面板上面的部分参数是无法进行编辑的。

4.1、Exporter

Exporter用于设置导出的文件格式,默认为“AndEngine Exporter(XML)”,该选项是一个下拉选择列表,开发者可以根据使用情景选择合适的导出格式。这里我选择“Box2D generic(PLIST)”

4.2、Gloabl parameters(全局参数)

当Exporter设置为Box2D generic(PLIST)选项后,原本的Body parameters便没有了,反而多出了一个Gloabl parameters(全局参数),全局参数里面可以设置“PTM-Ratio”(Cocos2D与Box2D的度量单位的一个缩放系数)。

PTM-Ratio默认值是32,通常在Cocos2D中也是使用32,但是这边需要将其设置为64,否则在创建的时候会发现刚体过大(具体原因我也没有去深究)。

4.3、Image parameters

在Image parameters中,我们通常也只需要关注“Relative”这一项,其它的都可以不用管(看看就知道其它参数是什么意思)。

当Exporter设置为Box2D generic(PLIST)选项后,在形状编辑面板的左下角便会多出一个小圆圈(如下图)。

这里写图片描述
质心
这个圆圈表示的是刚体的质心(搓一点的人就把它当成中心点吧)。我们可以手动拖动这个小圆圈到想要设置的位置,通常是图片的中间,因为Cocos2D的精灵(Sprite)默认的锚点在中心点,这样设置方便进行物理世界和渲染世界的效果同步。

在拖动小圆圈的时候就会发现,Relative参数的对应的在变化,开发者可以直接在Relative后面的两个输入框里面把值改成0.5,这样就肯定居中了。

4.4、Fixture parameters

在材质参数这边一共有7个个参数要设置。

Density:密度,用于设置形状的密度,间接的影响刚体的质量。
Restitution:恢复系数,控制刚体发生碰撞时,能量的损失。0表示完全非弹性碰撞,1表示完全弹性碰撞。
Friction:摩擦系数,用于模拟形状的摩擦效果。
Is Sensor:是否为传感器类型,勾选时表示该形状发生碰撞时可以接收到碰撞信息,触发碰撞回调,但是没有碰撞的效果。
Id:刚体的id。
Group:碰撞组,用于碰撞检测,默认为0。
除了上述6个参数之外,在Group下面还有16个碰撞种群的设置,用于进行碰撞筛选:

Bit’s name:种群名称。
Cat:刚体所在的种群(可以多选),默认选择第一个。
Mask:与该形状发生碰撞的种群(可以多选),勾选后即表示当前形状可以与对应的种群的刚体发生碰撞。
这里写图片描述
参数设置

二、文件导出和保存
编辑结束后点击顶部工具栏的“Publish”按钮,进行导出,Publish导出的是plist格式,可以在Cocos2D中进行解析(关于如何使用该文件将在下一篇关于关节的篇幅中介绍)。

除此之外,我们还需要“CTRL + S”或者点击工具栏的“Save”按钮将文件保存成pes格式的文件,pes格式的文件可以使用PhysicsEditor直接打开,方便后续开发中进行修改。

物理编辑器的使用

在本系列博客的第一篇就介绍了如何使用Box2D内置的几种方式创建刚体的材质,然而我们在开发游戏的时候需要根据很多非常复杂的物体创建对应的形状,使用内置的多边形很难去描出合适的图形,所以Box2D物理游戏引擎支持外部导入文件进行物理刚体的创建。

1.1、根据物理编辑器文件创建物理刚体

首先使用PhysicsEditor物理编辑器将需要创建的材质信息编辑好,然后导出cocos2d支持的plist文件,假设我们将文件保存为“boxEdit.plist”并放在资源目录下的physics文件夹里面。

创建精灵

-- 创建车身精灵
self.car = D.img("common/cars/car_1/carBody_1.png"):p(480, 270):to(self)
-- 创建车轮精灵
self.w1 = D.img("common/cars/car_1/wheel_1.png"):p(430, 210):to(self)
self.w2 = D.img("common/cars/car_1/wheel_1.png"):p(530, 210):to(self)

接下来就要使用物理编辑器导出的文件了:

1、创建刚体

local bodyDef           = b2BodyDef()
-- 类型:静态(b2_staticBody),平台(b2_kinematicBody),动态(b2_dynamicBody)
bodyDef.type            = b2_dynamicBody
bodyDef.position        = b2Vec2(self.car.px() / 32, self.car:py() / 32)
bodyDef.angle           = math.rad(0)
-- 用户数据:存储用户的数据,可以是任何类型的数据。一般要求存储的数据的类型是一致的
bodyDef.userData        = self.car 
-- 创建一个刚体对象,根据刚体定义创建
local body          = world:CreateBody(bodyDef)

2、有了刚体,接下来就要创建材质,代码如下:

-- 加载physicsDesigner的.plist格式文件
GB2ShapeCache:sharedGB2ShapeCache():addShapesWithFile("physics/boxEdit.plist")
-- 把生成的刚体和形状绑在一起,key即图片名
GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(body, "carBody_1")

可以使用GB2ShapeCache:sharedGB2ShapeCache():addShapesWithFile(“plist文件名路径”)来加载物理编辑器导出的数据,然后通过GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(刚体对象, “精灵列表上的形状名称”)来给刚体绑定材质信息。
这里写图片描述
根据PhysicsEditor编辑的文件创建刚体材质
刷新模拟器就可以看到编辑的车身显示在场景中(要开启物理调试模式)。

3、使用同样的方式创建汽车的两个车轮

bodyDef.position        = b2Vec2(13.5, 8.5)
bodyDef.userData        = self.w1 
local bodyWheel1 = world:CreateBody(bodyDef)
GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(bodyWheel1, "wheel_1")
bodyDef.position        = b2Vec2(16.5, 8.5)
bodyDef.userData        = self.w2
local bodyWheel2 = world:CreateBody(bodyDef)
GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(bodyWheel2, "wheel_1")

刷新模拟器,可以看到如下画面:
这里写图片描述
根据PhysicsEditor编辑的文件创建刚体材质

二、物理更新
2.1、物理更新

在这之前,其实物理世界并没有开始进行物理模拟,那么我们可以使用下面这个代码来进行一次物理更新:

-- 更新[物理世界]
function M.tick()
    if not world then return end 

    -- 时间步、速度迭代、位置迭代
    local velocityIterations, positionIterations = 32, 32
    -- 物理引擎进行物理模拟,生成模拟后的数据
    world:Step(60, velocityIterations, positionIterations)
end

注意:这个函数里面的“world”就是之前创建的物理世界,所以我们可以将world定义成全局的,以保证可以在任何地方访问。

调用物理世界的Step()方法可以进行一次物理模拟,方法的第一个参数是时间步,第二个参数是速度迭代参数,第三个参数是位置迭代参数。

  • 时间步用于控制物理模拟的时间间隔,我们一般让其与渲染世界同步,所以设置为60,设备性能较低时可以设置为40
  • 速度迭代:迭代次数越高,模拟越逼真,但是越耗性能
  • 位置迭代:迭代次数越高,模拟越逼真,但是越耗性能

2.2、开启物理模拟

开启一个调度器来调用上面的这个函数,实现实时模拟:

local timer             = SC.open(self.tick, 1.0 / 60)

开启调度进行物理模拟的时候发现车身和车轮开始做只有落地运动,并且它们散架了,这是因为车身和车轮之间没有连接到一起,为了让车身和车轮组成一个整体,可以使用关节(约束)将他们连接到一起

三、关节(b2Joint)

关节又称之为“约束”,是用于限制刚体运动的一些条件,在box2D中,常用的关节有:鼠标关节、距离关节、移动关节、旋转关节等,其实这些关节的中文名称并非翻译的很准确,很多诸如《Cocos2d-JS 游戏开发》的书籍里面都不进行翻译,而是直接使用关节的类名来进行讲解。各种关节类型都派生自 b2JointDef。所有关节都连接两个不同的物体,可能其中一个是静态物体。如果你想浪费内存的话,那就创建一个连接两个静态物体的关节。可以为任何一种关节指定用户数据。很多关节定义需要你提供一些几何数据。一个关节常常需要一个锚点(anchor point)来定义。在 Box2D 中这点需要在局部坐标系中指定,这样,即便当前物体的变化违反了关节约束,关节还是可以被指定 —— 在游戏存取进度时这经常会发生。另外,有些关节定义需要默认的物体之间的相对角度。这样才能通过关节限制或固定的相对角来正确地约束旋转。初始化几何数据可能有些乏味。所以很多关节提供了初始化函数,消除了大部分工作。然而,这些初始化函数通常只应用于原型,在产品代码中应该直接地定义几何数据。这能使关节行为更加稳固。

1、关节描述(b2JointDef)

与创建刚体一样,创建关节也需要先创建关节的描述,在box2D中,关节描述b2JointDef的定义如下:

struct b2JointDef
{
    b2JointDef()
    {
        type = e_unknownJoint;
        userData = NULL;
        bodyA = NULL;
        bodyB = NULL;
        collideConnected = false;
    }

    /// The joint type is set automatically for concrete joint types.
    b2JointType type;

    /// Use this to attach application specific data to your joints.
    void* userData;

    /// The first attached body.
    b2Body* bodyA;

    /// The second attached body.
    b2Body* bodyB;

    /// Set this flag to true if the attached bodies should collide.
    bool collideConnected;
};

b2JointDef是一个结构体(struct)类型,包含如下五个属性:

  • type:关节类型。
  • userData:用户数据(可以存放任意数据)。
  • bodyA:被关节约束的第一个刚体对象。
  • bodyB:被关节约束的第二个刚体对象。
  • collideConnected:被约束的两个刚体是否可以发生碰撞,true表示可以发生碰撞。

1、鼠标关节(b2MouseJoint)

1.1、鼠标关节描述(b2MouseJointDef)

在创建鼠标关节之前,需要先创建对应的关节描述,鼠标关节描述b2MouseJointDef的定义如下:

struct b2MouseJointDef : public b2JointDef
{
    b2MouseJointDef()
    {
        type = e_mouseJoint;
        target.Set(0.0f, 0.0f);
        maxForce = 0.0f;
        frequencyHz = 5.0f;
        dampingRatio = 0.7f;
    }

    /// The initial world target point. This is assumed
    /// to coincide with the body anchor initially.
    b2Vec2 target;

    /// The maximum constraint force that can be exerted
    /// to move the candidate body. Usually you will express
    /// as some multiple of the weight (multiplier * mass * gravity).
    float32 maxForce;

    /// The response speed.
    float32 frequencyHz;

    /// The damping ratio. 0 = no damping, 1 = critical damping.
    float32 dampingRatio;
};

b2MouseJointDef继承自b2JointDef,它的type属性值为“e_mouseJoint”,指明使用该关节定义的关节是一个鼠标关节。

b2MouseJointDef相比于b2JointDef多出了如下几个属性:

maxForce:作用在刚体上的最大的力,力越大拖动刚体越轻松。
frequencyHz:响应速度,数值越高,关节响应的速度越快,关节看上去越坚固。
dampingRatio:阻尼率:值越大,关节运动阻尼越大。
典型情况下,关节频率(响应速度)要小于一半的时间步(time step)频率。比如每秒执行60次时间步, 关节的频率就要小于30赫兹。这样做的理由可以参考Nyquist频率理论。
影响关节的弹性(阻尼率无单位,典型是在0到1之间, 也可以更大。1是阻尼率的临界值, 当阻尼率为1时,没有振动。)

1.2、创建鼠标关节描述

-- 创建一个鼠标关节定义
local mouseJointDef             = b2MouseJointDef()
-- 要被鼠标关节约束的刚体
mouseJointDef.bodyA             = body
-- bodyB设置为世界的边缘刚体
mouseJointDef.bodyB             = edgeBody
mouseJointDef.frequencyHz       = 30
mouseJointDef.collideConnected  = true
mouseJointDef.maxForce          = 100
-- 关节的位置
mouseJointDef.target            = b2Vec2(11, 10)

可以使用b2MouseJointDef()函数创建一个鼠标关节定义,然后直接设置其属性值。值得注意的是,关节是用于连接两个刚体的,而鼠标关节的主要作用是可以使用鼠标来对刚体进行拖拽操作,作用的是一个刚体,所以我们可以将一个刚体设置为要拖拽的刚体对象,另一个一般设置为世界边缘刚体。

1.3、创建鼠标关节

-- 创建关节,并转换为b2MouseJoint类型
local mouseJoint = tolua.cast(world:CreateJoint(mouseJointDef), "b2MouseJoint")

物理世界对象的CreateJoint(jointDef)方法可以用于创建一个关节,其参数jointDef为要创建的关节对应的关节描述对象,该方法的返回值类型是b2Joint类型,可以通过tolua.cast(obj, typeName)方法来进行类型转换。

1.4、移动刚体

鼠标关节移动刚体的现方式是改变鼠标关节的位置,代码如下:

-- 设置关节的位置
mouseJoint:SetTarget(b2Vec2(10, 10))

只需要在触摸事件的移动回调中改变鼠标关节的位置即可实现刚体的拖拽

2、焊接关节(b2WeldJoint)

2.1、焊接关节描述(b2WeldJointDef)

struct b2WeldJointDef : public b2JointDef
{
    b2WeldJointDef()
    {
        type = e_weldJoint;
        localAnchorA.Set(0.0f, 0.0f);
        localAnchorB.Set(0.0f, 0.0f);
        referenceAngle = 0.0f;
        frequencyHz = 0.0f;
        dampingRatio = 0.0f;
    }

    /// Initialize the bodies, anchors, and reference angle using a world
    /// anchor point.
    void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor);

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The bodyB angle minus bodyA angle in the reference state (radians).
    float32 referenceAngle;

    /// The mass-spring-damper frequency in Hertz. Rotation only.
    /// Disable softness with a value of 0.
    float32 frequencyHz;

    /// The damping ratio. 0 = no damping, 1 = critical damping.
    float32 dampingRatio;
};

焊接关节的定义如上,其相比与b2JointDef增加的属性有:

localAnchorA:关节连接在刚体A上的局部锚点,或者就理解成是关节在刚体A上的连接点
localAnchorB:关节连接在刚体B上的局部锚点,或者就理解成是关节在刚体B上的连接点
referenceAngle:刚体B相对于刚体A的角度,使用弧度制表示
frequencyHz:响应速度,数值越高,关节响应的速度越快,关节看上去越坚固。
dampingRatio:阻尼率:值越大,关节运动阻尼越大。

2.2、创建焊接关节描述

-- 创建一个焊接关节定义
local weldJointDef              = b2WeldJointDef()
-- 初始化
weldJointDef:Initialize(bodyA, bodyB, bodyB:GetWorldCenter())
weldJointDef.frequencyHz        = 30
weldJointDef.collideConnected   = false

创建焊接关节的时候,设置关节约束的两个刚体需要通过调用焊接关节定义的Initialize()方法,该方法有三个参数,分别是:刚体A、刚体B和关节的锚点位置。

2.3、创建焊接关节

local weldJoint = tolua.cast(world:CreateJoint(weldJointDef), "b2WeldJoint")

3、距离关节(b2DistanceJoint)

3.1、距离关节定义(b2DistanceJointDef)

struct b2DistanceJointDef : public b2JointDef
{
    b2DistanceJointDef()
    {
        type = e_distanceJoint;
        localAnchorA.Set(0.0f, 0.0f);
        localAnchorB.Set(0.0f, 0.0f);
        length = 1.0f;
        frequencyHz = 0.0f;
        dampingRatio = 0.0f;
    }

    /// Initialize the bodies, anchors, and length using the world
    /// anchors.
    void Initialize(b2Body* bodyA, b2Body* bodyB,
                    const b2Vec2& anchorA, const b2Vec2& anchorB);

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The natural length between the anchor points.
    float32 length;

    /// The mass-spring-damper frequency in Hertz. A value of 0
    /// disables softness.
    float32 frequencyHz;

    /// The damping ratio. 0 = no damping, 1 = critical damping.
    float32 dampingRatio;
};

距离关节的定义如上,其相比与b2JointDef增加的属性有:

localAnchorA:关节连接在刚体A上的局部锚点,或者就理解成是关节在刚体A上的连接点。
localAnchorB:关节连接在刚体B上的局部锚点,或者就理解成是关节在刚体B上的连接点。
length:锚点之间的自然长度

3.2、创建距离关节

-- 创建一个距离关节定义
local distanceJointDef = b2DistanceJointDef()
distanceJointDef:Initialize(bodyA, bodyB, bodyA:GetWorldCenter(), bodyB:GetWorldCenter())
distanceJointDef.bodyA = bodyA
distanceJointDef.bodyB = bodyB
-- 距离关节的长度
distanceJointDef.length = 2
distanceJointDef.frequencyHz  = 30
-- 是否连续碰撞
distanceJointDef.collideConnected = false 
-- 创建关节
lcoal distanceJoint = tolua.cast(world:CreateJoint(distanceJointDef), "b2DistanceJoint")

创建距离关节同样需要调用Initialize()方法,具体参照焊接关节。

4、移动关节(b2PrismaticJoint)

4.1、移动关节定义(b2PrismaticJointDef)

struct b2PrismaticJointDef : public b2JointDef
{
    b2PrismaticJointDef()
    {
        type = e_prismaticJoint;
        localAnchorA.SetZero();
        localAnchorB.SetZero();
        localAxisA.Set(1.0f, 0.0f);
        referenceAngle = 0.0f;
        enableLimit = false;
        lowerTranslation = 0.0f;
        upperTranslation = 0.0f;
        enableMotor = false;
        maxMotorForce = 0.0f;
        motorSpeed = 0.0f;
    }

    /// Initialize the bodies, anchors, axis, and reference angle using the world
    /// anchor and unit world axis.
    void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor, const b2Vec2& axis);

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The local translation unit axis in bodyA.
    b2Vec2 localAxisA;

    /// The constrained angle between the bodies: bodyB_angle - bodyA_angle.
    float32 referenceAngle;

    /// Enable/disable the joint limit.
    bool enableLimit;

    /// The lower translation limit, usually in meters.
    float32 lowerTranslation;

    /// The upper translation limit, usually in meters.
    float32 upperTranslation;

    /// Enable/disable the joint motor.
    bool enableMotor;

    /// The maximum motor torque, usually in N-m.
    float32 maxMotorForce;

    /// The desired motor speed in radians per second.
    float32 motorSpeed;
};

距离关节的属性会相对多一点,其相比与b2JointDef增加的属性有:

localAnchorA:关节连接在刚体A上的局部锚点,或者就理解成是关节在刚体A上的连接点。
localAnchorB:关节连接在刚体B上的局部锚点,或者就理解成是关节在刚体B上的连接点。
localAxisA:在刚体A中的局部移动单元轴,就是刚体A滑动的方向。
referenceAngle:参考角度,刚体B减去刚体A的角度值。
enableLimit:是否启用限制。
lowerTranslation:移动的最小限制,与方向同向为正,反向为负。启用限制后才有效果。
upperTranslation:移动的最大限制,与方向同向为正,反向为负。启用限制后才有效果。
enableMotor:是否启动马达。
maxMotorForce:马达的最大扭力。
motorSpeed:马达速度。
4.2、创建移动关节(平移关节)

-- 创建一个移动关节的定义
local prismaticJointDef             = b2PrismaticJointDef()

if true then 
    -- 移动的方向,用矢量来表示可以移动的方向,零向量(0, 0)为任意方向
    local directVec     = b2Vec2(10, 0)
    -- 初始化关节
    prismaticJointDef:Initialize(bodyA, bodyB, bodyB:GetWorldCenter(), directVec)
end

prismaticJointDef.lowerTranslation  = -1.0
prismaticJointDef.upperTranslation  = 1.0
prismaticJointDef.enableLimit       = true
prismaticJointDef.collideConnected  = false 

-- 创建关节
local prismaticJoint = tolua.cast(world:CreateJoint(prismaticJointDef), "b2PrismaticJoint")

5、绳索关节(b2RopeJoint)

5.1、绳索关节定义(b2RopeJointDef)

struct b2RopeJointDef : public b2JointDef
{
    b2RopeJointDef()
    {
        type = e_ropeJoint;
        localAnchorA.Set(-1.0f, 0.0f);
        localAnchorB.Set(1.0f, 0.0f);
        maxLength = 0.0f;
    }

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The maximum length of the rope.
    /// Warning: this must be larger than b2_linearSlop or
    /// the joint will have no effect.
    float32 maxLength;
};

绳索关节其相比与b2JointDef增加的属性有:

localAnchorA:关节连接在刚体A上的局部锚点,或者就理解成是关节在刚体A上的连接点。
localAnchorB:关节连接在刚体B上的局部锚点,或者就理解成是关节在刚体B上的连接点。
maxLength:绳索的最大长度。

5.2、创建绳索关节

-- 创建一个绳索关节的定义
local ropeJointDef          = b2RopeJointDef()
-- 绳索的最大长度
ropeJointDef.maxLength      = 4
-- 关节连接的刚体A
ropeJointDef.bodyA          = bodyA
-- 关节连接的刚体B
ropeJointDef.bodyB          = bodyB
-- 关节连接在刚体A上的锚点,其他关节应该也有,默认在中央
ropeJointDef.localAnchorA   = b2Vec2(0.5, 0.5)
-- 关节连接在刚体B上的锚点,其他关节应该也有,默认在中央
ropeJointDef.localAnchorB   = b2Vec2(0.5, 0.5)
-- 是否连续碰撞 
ropeJointDef.collideConnected = true

-- 创建关节
local ropeJoint = tolua.cast(world:CreateJoint(ropeJointDef), "b2RopeJoint")

6、旋转关节(b2RevoluteJoint)

6.1、旋转关节定义(b2RevoluteJointDef)

struct b2RevoluteJointDef : public b2JointDef
{
    b2RevoluteJointDef()
    {
        type = e_revoluteJoint;
        localAnchorA.Set(0.0f, 0.0f);
        localAnchorB.Set(0.0f, 0.0f);
        referenceAngle = 0.0f;
        lowerAngle = 0.0f;
        upperAngle = 0.0f;
        maxMotorTorque = 0.0f;
        motorSpeed = 0.0f;
        enableLimit = false;
        enableMotor = false;
    }

    /// Initialize the bodies, anchors, and reference angle using a world
    /// anchor point.
    void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor);

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The bodyB angle minus bodyA angle in the reference state (radians).
    float32 referenceAngle;

    /// A flag to enable joint limits.
    bool enableLimit;

    /// The lower angle for the joint limit (radians).
    float32 lowerAngle;

    /// The upper angle for the joint limit (radians).
    float32 upperAngle;

    /// A flag to enable the joint motor.
    bool enableMotor;

    /// The desired motor speed. Usually in radians per second.
    float32 motorSpeed;

    /// The maximum motor torque used to achieve the desired motor speed.
    /// Usually in N-m.
    float32 maxMotorTorque;
};

旋转关节的定义和移动关节差不多,只是没有localAxisA属性,所以被旋转关节约束的两个刚体之间是可以随意方向移动的,将平移关节的移动方向设置为另向量b2Vec2(0, 0),那么平移关节的效果就和旋转关节一致了。

6.2、创建旋转关节

-- 创建一个旋转关节定义
local revoluteJointDef              = b2RevoluteJointDef()
revoluteJointDef:Initialize(bodyA, bodyB, bodyB:GetWorldCenter())
revoluteJointDef.bodyA              = bodyA
revoluteJointDef.bodyB              = bodyB
revoluteJointDef.lowerAngle         = 0
revoluteJointDef.upperAngle         = 0
-- 是否启用角度限制,类似手臂只能在一定角度内旋转一样。
revoluteJointDef.enableLimit        = false   
-- 马达的速度 
revoluteJointDef.motorSpeed         = 0.0       
-- 马达的最大扭矩
revoluteJointDef.maxMotorTorque     = 11110.0
-- 是否启用旋转马达,启用后关节会自动转动
revoluteJointDef.enableMotor        = true     
revoluteJointDef.frequencyHz        = 32
revoluteJointDef.collideConnected   = false 
-- 关节长度 
revoluteJointDef.length             = 0.1

-- 创建关节,并转换为b2RevoluteJoint类型
local revoluteJoint = tolua.cast(world:CreateJoint(revoluteJointDef), "b2RevoluteJoint")

7、摩擦关节

7.1、摩擦关节定义

struct b2FrictionJointDef : public b2JointDef
{
    b2FrictionJointDef()
    {
        type = e_frictionJoint;
        localAnchorA.SetZero();
        localAnchorB.SetZero();
        maxForce = 0.0f;
        maxTorque = 0.0f;
    }

    /// Initialize the bodies, anchors, axis, and reference angle using the world
    /// anchor and world axis.
    void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor);

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The maximum friction force in N.
    float32 maxForce;

    /// The maximum friction torque in N-m.
    float32 maxTorque;
};

摩擦关节相对于b2JointDef增加的属性是:

localAnchorA:关节在刚体A上面的作用点。
localAnchorB:关节在刚体B上面的作用点。
maxForce:关节的最大摩擦力。
maxTorque:摩擦关节最大扭力。

7.2、创建摩擦关节

-- 创建一个摩擦关节定义
local frictionJointDef = b2FrictionJointDef()
frictionJointDef:Initialize(bodyA, bodyB, bodyB:GetWorldCenter())
frictionJointDef.frequencyHz = 0.0
frictionJointDef.dampingRatio = 0.0
frictionJointDef.maxForce = 0.0

local frictionJoint = tolua.cast(world:CreateJoint(frictionJointDef), "b2FrictionJoint")

8、齿轮关节(b2GearJoint)

8.1、齿轮关节定义(b2GearJointDef)

struct b2GearJointDef : public b2JointDef
{
    b2GearJointDef()
    {
        type = e_gearJoint;
        joint1 = NULL;
        joint2 = NULL;
        ratio = 1.0f;
    }

    /// The first revolute/prismatic joint attached to the gear joint.
    b2Joint* joint1;

    /// The second revolute/prismatic joint attached to the gear joint.
    b2Joint* joint2;

    /// The gear ratio.
    /// @see b2GearJoint for explanation.
    float32 ratio;
};

齿轮关节的定义如上,其相比与b2JointDef增加的属性有:

joint1:齿轮关节上关连的第一个关节(旋转关节/平移关节)
joint2:齿轮关节上关连的第二个关节(旋转关节/平移关节)
ratio:关节比例(齿轮系数)

8.2、创建齿轮关节

-- 创建一个齿轮关节
local gearJointDef  = b2GearJointDef() 
-- 齿轮关节关联的关节1
gearJointDef.joint1 = jointA
-- 齿轮关节关联的关节2
gearJointDef.joint2 = jointB
gearJointDef.bodyA  = bodyA
gearJointDef.bodyB  = bodyB
-- 关节比例(齿轮系数)
gearJointDef.ratio  = 1 

local gearJointDef = tolua.cast(world:CreateJoint(gearJointDef), "b2GearJoint")

上面jointA和jointB是预先创建好的旋转关节或者平移关节,释放这两个关节时需要先释放齿轮关节,否则会引起空指针错误

9、滑轮关节(b2PulleyJoint)

9.1、滑轮关节定义(b2PulleyJointDef)

struct b2PulleyJointDef : public b2JointDef
{
    b2PulleyJointDef()
    {
        type = e_pulleyJoint;
        groundAnchorA.Set(-1.0f, 1.0f);
        groundAnchorB.Set(1.0f, 1.0f);
        localAnchorA.Set(-1.0f, 0.0f);
        localAnchorB.Set(1.0f, 0.0f);
        lengthA = 0.0f;
        lengthB = 0.0f;
        ratio = 1.0f;
        collideConnected = true;
    }

    /// Initialize the bodies, anchors, lengths, max lengths, and ratio using the world anchors.
    void Initialize(b2Body* bodyA, b2Body* bodyB,
                    const b2Vec2& groundAnchorA, const b2Vec2& groundAnchorB,
                    const b2Vec2& anchorA, const b2Vec2& anchorB,
                    float32 ratio);

    /// The first ground anchor in world coordinates. This point never moves.
    b2Vec2 groundAnchorA;

    /// The second ground anchor in world coordinates. This point never moves.
    b2Vec2 groundAnchorB;

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The a reference length for the segment attached to bodyA.
    float32 lengthA;

    /// The a reference length for the segment attached to bodyB.
    float32 lengthB;

    /// The pulley ratio, used to simulate a block-and-tackle.
    float32 ratio;
};

滑轮关节的定义如上,其相比与b2JointDef增加的属性有:

groundAnchorA:刚体A对应的那个滑轮的位置。
groundAnchorB:刚体B对应的那个滑轮的位置。
localAnchorA:关节在刚体A上面的作用点。
localAnchorB:关节在刚体B上面的作用点。
lengthA:滑轮到刚体A的线的长度。
lengthB:滑轮到刚体B的线的长度。
ratio:比例(关节传动时,滑轮上升和下降的两头的位移比例)。

9.2、创建滑轮关节

-- 创建一个滑轮关节定义
local pulleyJointDef    = b2PulleyJointDef() 
if true then 
    local function ccpToVecRatioIfnil(point, defaultVec2, offset)
        point   = point and M.ccpToVecRatio(point) or b2Vec2(default.x + offset.x, default.y + offset.y) 

        return point 
    end

    -- 滑轮绳子拉动的点
    local bodyWorldPointA   = bodyA:GetWorldCenter()
    -- 滑轮绳子拉动的点
    local bodyWorldPointB   = bodyB:GetWorldCenter()
    -- 刚体A对应的那个滑轮的位置
    local groundPointA      = b2Vec2(bodyWorldPointA.x, bodyWorldPointA.y + 2)
    -- 刚体B对应的那个滑轮的位置
    local groundPointB      = b2Vec2(bodyWorldPointB.x, bodyWorldPointB.y + 3)
    -- 比例(关节传动时,滑轮上升和下降的两头的位移比例)
    local ratio             = 1
    -- 初始化关节
    pulleyJointDef.Initialize(bodyA, bodyB, groundPointA, groundPointB, 
        bodyWorldPointA, bodyWorldPointB, ratio)
end
-- 绳子可拉动的最大长度
pulleyJointDef.maxLengthA = 5  
-- 绳子可拉动的最大长度
pulleyJointDef.maxLengthB = 5

local pulleyJoint = tolua.cast(world:CreateJoint(pulleyJointDef), "b2PulleyJoint")

10、轮子关节(b2WheelJoint)

10.1、轮子关节定义(b2WheelJointDef)

struct b2WheelJointDef : public b2JointDef
{
    b2WheelJointDef()
    {
        type = e_wheelJoint;
        localAnchorA.SetZero();
        localAnchorB.SetZero();
        localAxisA.Set(1.0f, 0.0f);
        enableMotor = false;
        maxMotorTorque = 0.0f;
        motorSpeed = 0.0f;
        frequencyHz = 2.0f;
        dampingRatio = 0.7f;
    }

    /// Initialize the bodies, anchors, axis, and reference angle using the world
    /// anchor and world axis.
    void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor, const b2Vec2& axis);

    /// The local anchor point relative to bodyA's origin.
    b2Vec2 localAnchorA;

    /// The local anchor point relative to bodyB's origin.
    b2Vec2 localAnchorB;

    /// The local translation axis in bodyA.
    b2Vec2 localAxisA;

    /// Enable/disable the joint motor.
    bool enableMotor;

    /// The maximum motor torque, usually in N-m.
    float32 maxMotorTorque;

    /// The desired motor speed in radians per second.
    float32 motorSpeed;

    /// Suspension frequency, zero indicates no suspension
    float32 frequencyHz;

    /// Suspension damping ratio, one indicates critical damping
    float32 dampingRatio;
};

轮子关节相对于b2JointDef增加的属性是:

localAnchorA:关节在刚体A上面的作用点。
localAnchorB:关节在刚体B上面的作用点。
localAxisA:在刚体A中的局部移动单元轴,就是刚体A滑动的方向。
enableMotor:是否启动马达。
maxMotorTorque:马达的最大转矩。
motorSpeed:马达速度。
frequencyHz:响应速度,数值越高,关节响应的速度越快,关节看上去越坚固。
dampingRatio:阻尼率:值越大,关节运动阻尼越大。
10.2、创建轮子关节

– 创建一个轮子关节定义
local wheelJointDef = b2WheelJointDef()
if true then
– 用坐标表示轮子轴的位置
local axis = b2Vec2(1.0, 0.0)
– 初始化关节
wheelJointDef:Initialize(bodyA, bodyB, bodyB:GetWorldCenter(), axis)
end
wheelJointDef.enableMotor = false
wheelJointDef.motorSpeed = 0.0
wheelJointDef.maxMotorTorque = 0.0
wheelJointDef.frequencyHz = 32
wheelJointDef.dampingRatio = 0.2

-- 创建关节 
local wheelJoint = tolua.cast(world:CreateJoint(wheelJointDef), "b2WheelJoint")

每个关节的用法基本一致,主要的差别都是创建关节定义的时候设置的参数不同而已,上文创建关节的代码里面只是随便设置了几个参数。读者可以使用b2WheelJoint关节将前文的汽车和车轮约束到一起。

三、接触(b2Contact)

在物理游戏中经常会需要实现碰撞检测的逻辑,box2D提供了b2Contact来实现碰撞。

1、接触检测

1.1、创建接触检测监听器

-- 创建接触侦听器
local listener = GB2ContactListener:new_local()

1.2、为接触监听注册回调句柄

-- 为接触注册侦听函数
listener:registerScriptHandler(function(contactType, contact, oldManifold)
    if contactType == GB2_CONTACTTYPE_BEGIN_CONTACT then 
        print("开始接触")
    elseif contactType == GB2_CONTACTTYPE_BEGIN_CONTACT then 
        print("结束接触")
    elseif contactType == GB2_CONTACTTYPE_BEGIN_CONTACT then 
        print("求解前")
    elseif contactType == GB2_CONTACTTYPE_BEGIN_CONTACT then 
        print("求解后")
    end 
end)

在接触回调里面如果做刚体的销毁工作可能会导致b2Contact对象引用的刚体变成一个空指针,从而造成游戏闪退。所以通常可以延迟一点进行销毁

1.3、给物理世界设置接触监听

-- 设置物理世界的接触侦听
world:SetContactListener(listener)

2、接触点的获取

刚体在接触的过程中,可以从接触对象(b2Contact)中获取接触点,代码如下:

    -- 定义一个b2WorldManifold对象,用来获取并存储碰撞点的全局坐标
    local manifold  = GB2Util:newWorldManifold()
    -- 通过GetWorldManifolde方法,计算出碰撞点的全局坐标,并存储到manifold变量中
    contact:GetWorldManifold(manifold)
    -- 获取碰撞点
    local point     = manifold.points[0]

四、渲染世界和物理世界的同步

物理世界只是负责物理效果的模拟,但是真正要反馈给玩家的是游戏的画面,也就是渲染世界

修改上文的tick()方法如下:

– 更新[物理世界]

function M.tick()
    if not world then return end 

    -- 时间步、速度迭代、位置迭代
    local velocityIterations, positionIterations = 32, 32
    -- 物理引擎进行物理模拟,生成模拟后的数据
    world:Step(M.getTimeStep(), velocityIterations, positionIterations)

    -- 同步物理世界和渲染世界
    local body = world:GetBodyList()
    while body do 
        if body:GetUserData() then
            local spr   = tolua.cast(body:GetUserData(), "CCSprite")
            local x, y  = body:GetPosition().x * PTM_RATIO, body:GetPosition().y * PTM_RATIO
            -- 更新位置、角度,同步物理世界和渲染世界
            spr:setPosition(ccp(x, y))
            spr:setRotation(-1 * PT.radians2degrees(body:GetAngle()))
        end
        -- 获得下一个body
        body = body:GetNext()
    end
end

实现物理世界和渲染世界的方式就是遍历所有的刚体,然后将刚体的用户数据(userData)绑定的精灵的位置和旋转角度与刚体进行同步

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值