cocos2dx-lua物理引擎碰撞检测

前言

在游戏中模拟真实的物理世界是比较麻烦的,通常都会交给物理引擎来做,比较知名的物理引擎有Box2D和Chipmunk。

在Cocos2d-x 2.x中,游戏直接使用物理引擎,引擎提供一个简单的CCPhysicsSprite,处理了物理引擎的body与CCSprite的关系,而物理引擎的其他元素并没有和引擎对应起来,游戏需要选择直接调用Box2D或Chipmunk的API来处理逻辑。然而直接使用物理引擎是比较复杂的,物理引擎的接口参数繁多、复杂、需要开发人员对物理引擎和Coco2d-x都很了解,才能把两者融合得很好。
这种情况在Cocos2d-x 3.x中有了改变,全新的Physics Integration,把Chipmunk和Box2D封装到引擎内部,开发者不必关心底层具体用的是哪个物理引擎,也不用直接调用物理引擎的接口。
Cocos2d-x 3.x默认使用Chipmunk作为内部物理引擎。
Physics Integration 做了以下深度融合:
(1)物理世界被融入到Scene中,即当创建一个场景时,可以指定这个场景是否使用物理引擎。
(2)Node自带body属性,也就是Sprite自带body属性。
(3)对物理引擎的Body(cc.PhysicsBody),Shape(cc.PhysicsShape),Contact(cc.PhysicsContact),Joint(cc.PhysicsJoint),World(cc.PhysicsWold)进行封装抽象,使用更加简单。
(4)更简单的碰撞检测监听:EventListenerPhysicsContact.

创建带物理世界的scene

使用下面方法创建带物理世界的scene:

1
2
3
local MainScene = class ("MainScene",function()
return display.newScene("MainScene",{physics = 2})
end)

创建后,self:getPhysicsWorld()用来获取场景绑定的物理世界对象。
PhysicsWorld()默认是带重力的,大小为Vect(0.0f,-98.0f),也可以通过setGravity()方法来改变重力值,如下:
1
self:getPhysicsWorld():setGravity(cc.p(0,-100)) 

在调试物理世界中物体运动模拟时,可以使用PyhsicsWorldsetDebugDrawMask()来开启调试模式。它能把物理世界中不可见的shape, joint和contact都可视化。当调试结束需要发布游戏的时候,需要把该debug开关关闭
关闭DEBUG,传入参数cc.PhysicsWorld.DEBUGDRAW_NONE

创建物理边界

在物理世界中,所以物体均受重力的影响。物理引擎提供StaticShape创建一个不受重力影响的形状,在Cocos2d-x 2.x中,我们需要了解物理引擎的StaticShape相关的各种参数来完成边界设置。而在Cocos2d-x 3.x中,由cc.PhysicsBody创建边界,然后由Node添加到场景,addChild内部自动碗好吃呢个边界添加到物理世界,Node在这里起中介作用。
代码如下:

1
2
3
4
5
6
local size = display.size
local body = cc.PhysicsBody:createEdgeBox(size,cc.PHYSICSBODY_MATERIAL_DEFAULT,2)
local edgeNode = display.newNode()
edgeNode:setPosition(size.width/2,size.height/2)
edgeNode:setPhysicsBody(body)
self:addChild(edgeNode)

cc.PhysicsBody包含很多工程方法,createEdgeBox创建一个不受重力影响的矩形边界,参数含义依次是:
(1)矩形区域大小,这里设置为屏幕大小。
(2)设置材质,可选参数,默认为PHYSICSBODY_ MATERIAL DEFAULT
(3)边线宽度,可选参数,默认为1
然后我们创建一个Node,把刚刚创建的body附加到Node上,并设置好Node的position为屏幕中心点。最后把Node添加到scene。
Node的addChild方法,在Cocos2d-x 3. x中有对物理body做处理,它会自动把Node的body设置到scene的PhysicsWorld上去。
PhysicsBody中的工程方法,针对参数设置的body大小,会自动创建对应的PhysicsBody和一个PhysicsShape, 这也是通常情况下,直接使用物理引擎创建一个body需要做的事情。Cocos2d-x 3. x的Physics Integration 极大地简化了使用物理引擎的代码量。

创建受重力作用的sprite

在Cocos2d-x 3.0中创建一个受重力作用的Sprite也很简单。首先来看代码:

1
2
3
4
5
6
7
8
9
local oneSprite = display.newSprite("game/basketball/image/basketball.png")
-- local oneBody = cc.PhysicsBody:createBox(oneSprite:getContentSize(),cc.PHYSICSBODY_MATERIAL_DEFAULT,cc.p(0,0)) --矩形刚体
local oneBody = cc.PhysicsBody:createCircle(25,cc.PHYSICSBODY_MATERIAL_DEFAULT,cc.p(0,0)) --圆形刚体
oneBody:setContactTestBitmask(0xFFFFFFFF)
oneBody:applyImpulse(cc.p(0,10000))
oneSprite:setPhysicsBody(oneBody)
oneSprite:setPosition(x,y)
self:addChild(oneSprite)
--onBody:setGravityEnable(false) --刚体不受重力作用

首先创建-一个sprite,然后用cc.PhysicsBody:createBox()创建一个矩形的body附加在sprite上。createBox 接受三个参数如下:
(1)参数1,cc. size类型,表示矩形的长宽。
(2)参数2,cc.PhysicsMaterial类型,表示物理材质属性,可选参数,默认为cc. PHYSICSBODY MATERIAL DEFAULT。手动创建材质方法如下:
1
cc. PhysicsMaterial(density, restitution, friction)

其中第一个参数表示密度,第二个参数表示反弹力,第三个参数表示摩擦力。
(3)参数3,cc.p类型,表示body与中心点的偏移量,可选参数,默认为cc. p(0,0)。类似地,可以用下面的方法创建圆形body:
1
cc. PhysicsBody:createCircle(radius,material,offset)

不同于矩形的创建,第一个参数是园的半径,第二、三个参数的作用同createBox一样。

碰撞检测

在Cocos2d-x 中,事件派发机制做了重构,所有事件均由事件派发器统一管理。物理引擎的碰撞事件也不例外,下面的代码注册碰撞begin回调函数。

1
2
3
4
5
6
7
8
9
10
11
--监听碰撞
local function onContactBegin(contact)
local tag = contact:getShapeA():getBody():getNode():getTag()
-- print(tag) --碰撞后的回调事件
return true
end

local contactListener = cc.EventListenerPhysicsContact:create()
contactListener:registerScriptHandler(onContactBegin,cc.Handler.EVENT_PHYSICS_CONTACT_BEGIN)
local eventDispatcher = cc.Director:getInstance():getEventDispatcher()
eventDispatcher:addEventListenerWithFixedPriority(contactListener,1)

碰撞检测的所有事件由cc.EventListenerPhysicsContact的实例来监听,这些事件有如下几类。
(1) cc. Handler. EVENT_ PHYSICS_ _CONTACT_ BEGIN,在碰撞刚发生时,触发这个事件,并且在此次碰撞中只会被调用一次。可以通过返回true 或者false 来决定物体是否发生碰撞。需要注意的是,当这个事件的回调函数返回flase 时, EVENT _ PHYSICS_CONTACT_ PRESOLVEEVENT_ PHYSICS_ CONTACT_ POSTSOLVE 将不会被触发,但EVENT_ PHYSICS_ CONTACT_ SEPERATE必定会触发。
(2) cc. Handler. EVENT_ PHYSICS_ CONTACT_ PRESOLVE, 发生在碰撞的每个step,可以通过调用cc.PhysicsContactPreSolve的成员函数来改变碰撞处理的一些参数设定,比如弹力和阻力等。同样可以通过返回true或者false来决定物体是否发生碰撞。
(3)cc. Handler. EVENT_ PHYSICS_ CONTACT_ POSTSOLVE,发生在碰撞计算完毕的每个step,可以在此做一些碰撞的后续处理,比如安全的移除某个物体等。
(4) cc. Handler. EVENT_ PHYSICS_ CONTACT_ SEPERATE,发生在碰撞结束两物体分离时,同样只会被调用一次。它与onContactBegin必定是成对出现的。
监听器设置完毕,需要加入到引擎导演的事件分发器中。
默认情况下,物理引擎中的物体都不发出碰撞事件,也就是上面的代码中的onContactBegin永远不会调用到。为了解决这个问题,首先需要了解cc. PhysicsBody的三个mask。
(1) CategoryBitmask body,类别掩码,32位整型,也就是可以有32 个不同的类别。默认值为0xFFFFFFFF
(2) ContactTestBitmask,当两个物体接触时,用一个物体的CategoryBitmask与另一.个物体的ContactTestBitmask做逻辑运算,不为0时引擎才会新建PhysicsContact对象,发送碰撞事件。ContactTestBitmask的设计是为了优化性能,并不是所有物体之间的碰撞我们都关心,所以这个ContactTestBitmask的默认值为0x00000000
(3)CollisionBitmask刚体碰撞掩码,当两个物体接触后,用—个物体的CollisionBitmask与另一个物体的CategoryBitmask做逻辑运算,不为0时才能发生刚体碰撞,默认值为0xFFFFFFFF
上面的解释说明了每个掩码的作用,而掩码之间的相互作用可归纳如下:
(1)CategoryBitmask,是其他两个掩码比较的基础。
(2) CategoryBitmask & ContactTestBitmask,决定是否发送事件消息。
(3) CategoryBitmask & CollisionBitmask,决定是否产生刚体反弹效果。
(4) ContactTestBitmaskCollisionBitmask,互相之间没有联系。

完整代码

下面是main.lua文件的完整代码,单击屏幕任意一点会创建一个精灵,精灵之间相互碰撞后产生回调事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
local MainScene = class ("MainScene",function()
return display.newScene("MainScene",{physics = 2})
end)

function MainScene:ctor()
self:getPhysicsWorld():setGravity(cc.p(0,-100)) --重力
self:getPhysicsWorld():setDebugDrawMask(cc.PhysicsWorld.DEBUGDRAW_ALL)
local size = display.size
local body = cc.PhysicsBody:createEdgeBox(size,cc.PHYSICSBODY_MATERIAL_DEFAULT,2)
local edgeNode = display.newNode()
edgeNode:setPosition(size.width/2,size.height/2)
edgeNode:setPhysicsBody(body)
self:addChild(edgeNode)
--监听碰撞
local function onContactBegin(contact)
local tag = contact:getShapeA():getBody():getNode():getTag()
-- print(tag) --碰撞后的回调事件
return true
end

local contactListener = cc.EventListenerPhysicsContact:create()
contactListener:registerScriptHandler(onContactBegin,cc.Handler.EVENT_PHYSICS_CONTACT_BEGIN)
local eventDispatcher = cc.Director:getInstance():getEventDispatcher()
eventDispatcher:addEventListenerWithFixedPriority(contactListener,1)

self:getPhysicsWorld():setDebugDrawMask(cc.PhysicsWorld.DEBUGDRAW_ALL)
local function onTouchBegan( touch, event )
return true
end
local function onTouchEnded( touch, event )
local location = touch:getLocation() --获取鼠标的位置
local event_x = location["x"] or 0
local event_y = location["y"] or 0
self:addSprite(event_x,event_y)
end
local function onTouchMoved(touch, event)
end
local listener1 = cc.EventListenerTouchOneByOne:create() --创建一个单点事件监听
listener1:setSwallowTouches(false) --是否向下传递
listener1:registerScriptHandler(onTouchBegan,cc.Handler.EVENT_TOUCH_BEGAN )
listener1:registerScriptHandler(onTouchMoved,cc.Handler.EVENT_TOUCH_MOVED )
listener1:registerScriptHandler(onTouchEnded,cc.Handler.EVENT_TOUCH_ENDED )
local eventDispatcher = self:getEventDispatcher()
eventDispatcher:addEventListenerWithSceneGraphPriority(listener1, self) --分发监听事件
end
function MainScene:addSprite(x,y)
local oneSprite = display.newSprite("game/basketball/image/basketball.png")
-- local oneBody = cc.PhysicsBody:createBox(oneSprite:getContentSize(),cc.PHYSICSBODY_MATERIAL_DEFAULT,cc.p(0,0)) --矩形刚体
local oneBody = cc.PhysicsBody:createCircle(25,cc.PHYSICSBODY_MATERIAL_DEFAULT,cc.p(0,0)) --圆形刚体
oneBody:setContactTestBitmask(0xFFFFFFFF)
oneBody:applyImpulse(cc.p(0,10000))
oneSprite:setPhysicsBody(oneBody)
oneSprite:setPosition(x,y)
oneSprite:setTag(101)
self:addChild(oneSprite)
end


function MainScene:showWithScene(transition, time, more)
self:setVisible(true)
local scene = self
display.runScene(scene, transition, time, more)
return self
end

return MainScene

首先,使用 display.newPhysicsScene作为MainScene的父类,创建一个带物理世界的MainScene。
在MainScene:ctor中依次做了下面的初始化工作:
(1)修改物理世界的重力,重力是从cc. p(0,0)到setGravity()参数点之间的向量。
(2)用cc. PhysicsBody: createEdgeBox在屏幕四周创建物理边界,然后通过节点添加到场景中,它不受重力的影响。
(3)注册EVENT_ PHYSICS_ CONTACT_ BEGIN事件的回调函数。
注: onContactBegin需要return true,否则物体碰撞后不发生刚体反弹。

(4)打开物理世界的调试模式,可以在屏幕上看到物理边界以及刚体的框架。
(5)注册触摸事件,每次触摸事件到来都会在触摸点创建一个刚体精灵。
MainScene: addSprite完成精灵的创建以及初始化:
(1)xxxx.png是个圆形的图片,通过这种图片创建精灵。
(2) cc. PhysicsBody: createCircle创建一个圆形的刚体,注意到它采用了radius作为第一个参数,这样刚体就能完全吻合图片的形状。
(3)setContactTestBitmask修改精灵的接触检测掩码,这样精灵之间碰撞就能发出事件。
(4)我们还用applyImpulse为刚体施加了一个向,上的瞬时冲力,这样精灵创建后会先向上飞,再掉落下来。applyImpulse是个很有用的接口,在物理世界中,用这个接口来改变物体的运动轨迹,而不是用传统的setPos,否则物理世界的运动将不可预期。
(5)setPhysicsBody把物理刚体和精灵都绑定在一起。
(6)设置精灵的初始坐标并添加到场景上。
快速单机屏幕创建多个精灵,发现它们互相弹开了,这是由于刚体的弹力作用,然后由于受到重力的影响,最终他们都凋落到屏幕下方。
设置刚体属性,参数分别是密度,碰撞系数,摩擦力

1
cc.PhysicsMaterial(1,1,0)

效果图

在这里插入图片描述

掩码说明

  categoryBitmask:

分类掩码,定义了物体属于哪个分类。场景中的每个物理刚体可以被赋值一个多达32位的值(因为categoryBitmask为int型),每个对应32位掩码中的每一位,你在你的游戏中定义掩码值。结合collisionBitMask和contactTestBitMask属性, 你可以定义哪些物理刚体相互作用并且你的游戏何时接受这些相互作用的通知。默认值为0xFFFFFFFF(所有位都被设置)。

contactTestBitmask:

接触测试掩码,定义哪些刚体分类可以与本刚体产生相互作用的通知。当两个刚体在同一个空间,即物理世界中,每个刚体的分类掩码会和其他刚体的接触测试掩码进行逻辑与的运算。如果任意一个比较结果为非零值,产生一个PhysicsContact对象并且传递到物理世界协议中,这里协议指我们的监听器对应的回调函数。 为了最好的性能,仅设置你感兴趣的接触测试掩码中的位,也就是说通过设置接触测试掩码,你可以决定发生碰撞后,回调函数是否有响应。默认值为0x00000000(所有位都被清除)。

collisionBitmask:

碰撞掩码,定义了哪些物理刚体分类可以和这个物理刚体发生碰撞。当两个物理刚体相互接触时,可能发生碰撞。这个刚体的碰撞掩码和另一个刚体的分类掩码进行逻辑与运算比较。如果结果是一个非零值,这个刚体会发生碰撞。每个刚体独立选择接受与哪个刚体发生碰撞。例如,你可以使用此掩码来忽略那些对于本刚体的速度有影响的刚体碰撞,也就是说你可以使用此掩码使得本刚体与某些刚体碰撞不会对本刚体产生影响。默认值为0xFFFFFFFF(所有位都被设置)。

从上面三个掩码的说明中,我们可以做一个小结。假设刚体A的接触测试掩码和碰撞掩码已知,刚体B的分类掩码决定了能否和A进行碰撞和在碰撞的前提下能否发出PhysicsContact对象触发回调函数。如果B的分类掩码与A的碰撞掩码做逻辑与运算的结果为0,则不会发生碰撞,因此也不会继续和A的接触测试掩码进行逻辑与运算。如果B的分类掩码与A的碰撞掩码做逻辑与运算的结果非0,则发生碰撞,并且B的分类掩码继续与A的接触测试掩码做逻辑与运算,如果结果非0,则发出PhysicsContact对象触发回调函数。