PhysicsEditorExporter for QuickCocos2dx 使用说明

前言

写这篇文章的时候,我用的是quick-cocos2d-x 2.2.1rc版本,最近quick将要升级2.2.3,但不影响使用。

在quick-cocos2d-x引擎目录的tool文件夹内,有个physics_editor_exporter,这东西是干嘛用的呢?

用过PhysicsEditor的人可能清楚,这个软件可以对图形进行顶点数据编辑,然后选择目标引擎导出数据(下图序号第三位置处)。

目前默认支持的引擎都有Cocos2D,Corona,Sparrow,LibGDX,AndEngine,Moai,Box2d/Nape-flash等。我们可以自定义导出数据格式,来让软件导出。而Physics_editor_export for quick-cocos2d-x就是在Corona模板(该引擎采用lua语言进行开发,导出的数据为.lua格式)的基础上进行修改而成。

使用方法

打开PhysicsEditor的安装目录,找到Resources/exporters,发现里面正是各个引擎的数据导出模板,我们将quick-cocos2d-x/tool/physics_editor_exporter目录下的quick_chipmunk文件夹拷贝进该目录,然后重启PhysicsEditor,就可以看到导出引擎数据列表中已经有了quick-cocos2d-x chipmunk这么一项。

关于如何使用PhysicsEditor这里不做介绍了,google下,关于如何使用的文章蛮多。

编辑好我们的图片顶点,并且设置合适的参数(包括弹性、摩擦力、质量等参数)后,我们选择Publish As选择目录导出我们的数据。(选择quick-cocos2d-x chipmunk,默认会保存为.lua)

读者可以打开保存的.lua进行查看,感兴趣的可以查看Physics_editor_export的模板与导出的.lua之间是如果对应解析导出的。这项工作由软件完成。

廖大的这个版本有点小问题,我进行了一些修改并附带了一个sample。

  1. 模板中的将要导出的fixture字段修改为更适合Chipmunk引擎的shape字段
  2. 修正了多边形定点按照scaleFactor计算的bug

Github地址:https://github.com/ChipmunkCommunityCN/quick-x_physics_editor_exporter

Screenshot:

sample说明

  • res/fruits.pes为PhysicsEditor工程文件
  • res/fruitsPhysicsData.lua为导出的数据文件
  • 西瓜、草莓、菠萝、葡萄我在PE设置了碰撞类型依次为1、2、3、4,并在MainScene.lua中对类型1、2的碰撞进行了监听,如果发生碰撞,会将他们移除掉。

额外的说明

  1. 多边形顶点数据必须是按照顺时针
  2. Chipmunk不支持凹多边形,只支持凸多边形

以上两点都跟物理引擎其中涉及到的算法相关,这里不做深究。

这两点不用担心,PhysicsEditor都为我们做好了工作,当我们编辑的图形顶点形成的是凹多边形时,它会将之计算拆分为若干的凸多边形组合,导出的数据完全满足上面提到的两条。朋友们如果看导出的.lua文件会发现,polygons里面如果有若干数组的话,正是凹多边形拆分而来的凸多边形数据。

最后

如果有问题,欢迎留言,HappyCoding:)

探讨Chipmunk2D碰撞查询

先借用《游戏引擎架构》一书中的一段话,来解释下物理引擎中的碰撞查询是用来干啥的。

碰撞检测的另一任务是回答有关游戏世界中碰撞体积的假想问题。例如:
1)若从玩家武器的的某方向射出子弹,若能击中目标,那目标是什么?
2)汽车从A点移动至B点是否会碰到任何障碍物?
3)找出玩家在给定半径范围内的所有敌人对象。
一般而言,这些操作成为碰撞查询(collision query)。最常用的查询类型是碰撞投射(collision cast),或简称作投射(cast)。投射用于判断,若放置某假想物体于碰撞世界,并沿光线或线段移动,是否会碰到世界中的物体。投射与正常的碰撞检测操作有别,因为投射的实体并不真正存在于碰撞世界,它完全不会影响世界中的其他物体。这就是为什么我们称,碰撞投射是回答关于世界中碰撞体的假想问题。

在阅读本文前,朋友们可以先瞅下官方文档中对碰撞查询的一些相关说明。

【Chipmunk2D中文手册】 猛击这里

1.最近点查询

最近点查询允许你检查离给定点一定距离内是否存在着形状,找到形状上离给定点最近的点或者找到离给定点最近的形状。

(截图来自ChipmunkDEMO中的Segment Query)

(截图来自cocos2dx3.0 demo中的RayCast 模式:any(任意))

(截图来自cocos2dx3.0 demo中的RayCast 模式:nearest(最近点))

(截图来自cocos2dx3.0 demo中的RayCast 模式:multiple(多个点))


未完待续

这节让我们来一起探讨下Chipmunk对碰撞过滤(collision filtering)的处理。碰撞过滤,顾名思义,就是要筛选出发生碰撞的一些刚体,将不会发生碰撞的刚体过滤出去,从而在后续回调中对碰撞进行处理。比如《AngryBird》里面,小鸟和箱子碰撞后,小鸟羽毛飞散、死亡,箱子爆破等的处理。

很多人更熟悉Box2D,为了更好的理解碰撞过滤,让我们先看瞅瞅Box2D是怎么实现碰撞过滤的,然后过渡到Chipmunk。

1.Box2D 碰撞过滤实现机制

在Box2D中,通过标志位和掩码的设计来实现碰撞过滤。其中有两个标志位和一个组别索引,分别是

  • categoryBits 类别标志位
  • maskBits 掩码标志位
  • groupIndex 组别索引

这三个属性在碰撞过滤机制中扮演着重要的角色。

过滤规则

  • 如果两个形状材质的组别索引相同为0,使用类别和掩码计算规则来确定是否碰撞
  • 如果两个形状材质的组别索引相同为正数,则直接确定为碰撞
  • 如果两个形状材质的组别索引相同为负数,则直接确定为不碰撞
  • 如果两个形状材质的组别索引不相同,使用类别和掩码计算规则来确定是否碰撞

额外的一些规则

  • 静态刚体的形状永远不会与其他静态刚体的形状发生碰撞
  • 同一刚体上的形状永远不会发生碰撞
  • 可以选择性的启用或者禁止被关节约束的刚体形状之间的碰撞

注:组别索引的过滤筛选要比类别和掩码标志位过滤筛选具有更高的优先级。

1
2
3
4
5
6
7
player1ShapeDef.filter.groupIndex = 1
player2ShapeDef.filter.groupIndex = 1
player3ShapeDef.filter.groupIndex = 2
player4ShapeDef.filter.groupIndex = -3
player5ShapeDef.filter.groupIndex = -3
player6ShapeDef.filter.groupIndex = 0
player7ShapeDef.filter.groupIndex = 0

根据上面的规则,我们知道

  • player1与player2碰撞
  • player4与player5不碰撞
  • player1与player3,player3与player4,player5与player7等等这些组别索引不同的形状材质,则要进一步根据类别和掩码计算来确定是否碰撞,后面我们马上会看到。
  • player6与player7组别索引相同为0,也要进一步根据类别和掩码计算来确定是否碰撞

类别标志位与掩码标志位的计算

Box2D支持16个类别,我们对于任何一种形状材质都可以设定类别标志位。通常我们可以用一个16进制来表示一个类别标志位,一共16位。比如0x0004,展开其实就是0x0000 0000 0000 0100

举个例子:

1
2
3
4
playerShapeDef.filter.categoryBits  = 0x0001
playerShapeDef.filter.maskBits = 0x0002
monsterShapeDef.filter.categoryBits = 0x0002
monsterShapeDef.filter.maskBits = 0x0001

计算规则:

  • 材质形状A的类别标志位材质形状B的掩码标志位进行”按位与”运算得到结果r1
  • 材质形状B的类别标志位材质形状A的掩码标志位进行”按位与”运算得到结果r2
  • r1与r2进行“逻辑与”,如果为true,则形状材质A与形状材质B则碰撞,false则不碰撞

我们根据上述规则得出结论,player与player之间不会碰撞,monster与monster之间也不会碰撞,但player与monster之间会发生碰撞。

2. Chipmunk2D 碰撞过滤实现

在Chipmunk中,一个shape具有grouplayer的属性,一起来看下在cpSpaceStep.c中的一个检测函数queryReject,即查询否定拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline cpBool
queryReject(cpShape *a, cpShape *b)
{

return (
// BBoxes must overlap
!cpBBIntersects(a->bb, b->bb)
// Don't collide shapes attached to the same body.
|| a->body == b->body
// Don't collide objects in the same non-zero group
|| (a->group && a->group == b->group)
// Don't collide objects that don't share at least on layer.
|| !(a->layers & b->layers)
// Don't collide infinite mass objects
|| (a->body->m == INFINITY && b->body->m == INFINITY)
);
}

根据上面的一些否定情况,我们总结出过滤规则:

  • 形状a与形状b的轴对齐包围盒如果没有发生碰撞,则不可能碰撞
  • 如果形状a和形状b同属于同一个刚体,则不会碰撞
  • 如果形状a和形状b在相同的非0组,则不会碰撞,同在0组,或者不相等则考虑碰撞
  • 如果形状a的层和形状b的层的按位与运算为0,即意味着不在一个“位面”上,则不会碰撞
  • 如果形状a和b从属的刚体的质量无限大,则不可能碰撞

这个是Chipmunk2D里面的碰撞机制,看起来和Box2D不太一样,啊哈?Cocos2dX对物理引擎进行了封装,碰撞过滤的实现和这里的方式却有所不同。封装的碰撞过滤接近了Box2D碰撞过滤的思路。让我们再来看下。

CCPhysicsShape/CCPhysicsBody类里有三个重要的属性,分别是

  • categoryBitmask

类别掩码,该掩码定义了刚体形状属于的类别。Chipmunk支持32种类别。通过对刚体或刚体形状设定categoryBitmask与contactTestBitmask,将两者按位与运算,我们便可以指定游戏中的哪些刚体之间可以有相互作用,并在相互作用后并进行后续的通知。(该通知直接影响到preSolve、postSolve、seperate等回调是否被调用)

默认值为0xFFFFFFFF

注意:相互作用并不等于就会产生碰撞反应,如传感器(sensor)就是一例。

  • contactTestBitmask

接触测试掩码,该掩码定义了哪些类别的刚体可以与本刚体(或刚体形状)产生相互作用。在物理空间中,每个刚体的类别掩码(categoryBitmask)会和其他刚体的接触测试掩码(contactTestBitmask)进行按位与运算,如果结果为非0值,便会产生一个PhysicsContact对象,并作为参数传入到physics world的代理方法内。为了性能考虑,我们只会设定我们关注的相互作用的的掩码。

默认值为0x00000000

  • collisionBitmask

碰撞掩码,该掩码定义了哪些类别的刚体可以与本刚体(或刚体形状)发生碰撞。当刚体彼此接触的时候,可能会发生碰撞反应。此时该刚体的碰撞掩码(collisionBitmask)会与另外一个刚体的类别(categoryBitmask)进行按位与运算,如果结果为非0值,该刚体就会受到碰撞影响。每个刚体都可以选择是否要受到碰撞影响。例如,你可以通过设定碰撞掩码来避免碰撞计算带来的刚体速度的改变。

默认值为0xFFFFFFFF

另外值得一提的是,封装后的CCPhysicsShape和CCPhysicsBody的group属性和Chipmunk2D的group对过滤规则的影响不一样!!!这里要注意下。上面总结的第三条是Chimunk2D的group的过滤规则,但在Cocos2DX封装之下的group,却采取了和Box2D一样的group过滤规则,即

  • 如果两个形状材质的组别索引相同为正数,则直接确定为碰撞
  • 如果两个形状材质的组别索引相同为负数,则直接确定为不碰撞
  • 组别索引的过滤筛选要比掩码过滤筛选具有更高的优先级。

之前我以为这是官方的一个bug,提过一个Issule给官方团队,见这里https://github.com/cocos2d/cocos2d-x/pull/6148。官方解释的原因是对物理引擎的封装要隐藏掉具体的使用哪个引擎的细节,而更关心的是友好的api,性能和功能性,另外一方面是对于有SpriteKit开发经验的开发者要更友好点。解释可以接受,但感觉怪怪的,这里的封装建构在Chipmunk2D之上,但group的过滤却是Box2D的规则。换个角度想,如果不叫group,或许更好接受点。

关于在Cocos2DX v3.x里面如何理解Chipmunk2D的碰撞过滤,可以参考这个简单的demo

思考:为什么ball1与ball2不碰撞,box1与ball1、ball2不碰撞,box2与ball1、ball2碰撞?改变他们的group会怎么样?对他们的一些掩码重新赋值会怎么样?朋友们可以尝试着设定不同的掩码来观察,方便理解其中的规则。

欢迎朋友们关注这个基础概念demo的项目,在学习过程的测试demo可以提交个pull request过来,一起来丰富这个项目。

参考

欢迎朋友们交流,HappyCoding:)

1 ShoeBox 简介

ShoeBox是一个基于AdobeAIR实现的免费跨平台的工具。这个工具使用拖放、剪切板的工作流程方式,能够很方便的处理游戏图片、创建位图字体等。

  • 支持引擎

2 功能概括介绍

ShoeBox虽小,五脏俱全。作者做这个工具足见是用了心,如果各位想捐款,官网右上角:)。写这篇文字的时候,笔者用的版本是最新版3.4.2。官网的一些图片说明不少是老版本的,注意下就可以了。

Sprites

  • 打包位图(Sprite Sheet)

这个功能非常强大。我们在游戏制作中,经常利用TexturePacker等工具,将一些碎图图片合成一个精灵表单(spritesheet),好处不言而喻。ShoeBox的这个功能,在一些方面并没有TP工具那么强大,但却有自己的特色功能。TA可以将多个图片,SWF(AS1,AS2,AS3版本)动画或者GIF动画合成到一个纹理图上,强大吧,支持SWF/GIF哦。

  • 读取精灵(Read Sprites)

读取一个精灵表单或者位图字体集,并将他们单独导出为精灵图片。要求图片文件和查找索引文本文件(即图片描述数据文件)。

  • 提取精灵(Extract Sprites)

上图是我拿FlappyBird的精灵表单做的试验。

检测具有alpha通道的图片的精灵图片并一次标上序号导出。这个功能可以在你丢失了图片描述文件的情况下,用作精灵表单反向导出精灵图片。同一个物体如果在图片上有透明间隙,会被表上多个序号,这里不是很理想,朋友们如果要用这个功能的话注意下。

  • 设定锚点(Sprite Pivots)

这个我们在Cocos2D里面用的不多,starling里面倒是有设置pivot的。

GUI

  • 位图字体(Bitmap Font)

这个就是我们今天介绍的重点了。见后面的详细介绍。

  • 拆分PSD文件(Split PSD)

这个功能绝对是美术人员的福利。强烈建议朋友们推荐给自己公司的MM们,给她们介绍下这个ShoeBox,并握着她们的小手手,手把手教她们如何使用这个功能。如果美术是GG,不管了。我不知道其他美术是怎么做的,作为非美术的我,但经常使用Photoshop,我在导出图片的时候,如果图层过多,通过隐藏其他图层,将图层的图片一个个单独导出,其中还要涉及裁剪或者新建文档导出。麻烦不?当然了。所以,这个功能能高效的导出PSD中的图片。

  • Slice 9

顾名思义,这个功能处理的是九宫格图片。因为在Cocos2D中有自己的九宫图类,这里就不介绍了。

Animation

  • 动画帧(Ani Frames)

将GIF或者SWF动画导出为帧序列图。异常强大!将上面的GIF动画图片拖放到AniFrames上后,保存会导出该动画的序列帧。SWF同理。这个功能通常用来解析资源。

  • 帧表单(FrameSheet)

这个功能集合了动画帧序列导出(AniFrames)以及打包精灵图片(Pack Sprites)功能,将GIF或者SWF动画文件拖放到上面后,可以保存为一个精灵表单,内容包含了动画的帧序列图。

Bitmaps

  • JPNG位图(JPNG Bitmap)

将32位RGBA位图转成24位的JPG格式图,附带一个alpha通道组。引擎解析需要加载JPG图片,然后通过复制右边的alpha通道并应用到左侧的颜色区域来重新组成32位的RGBA图。貌似这种做法现在很少用了。

  • Mask Image

将JPG图片进行遮罩和羽化,合成了一张复杂的图。还不清楚这个具体怎么使用。

Misc

  • Flickr Upload
  • Clean Up Text
  • Mac to PC url

这三个游戏基本用不到。就不介绍了。想要进一步了解的,可以深入官网探个究竟。

3 言归正传 创建位图字体

通常情况下,Windows下我们可以使用免费的Bitmap Font Generator,mac下可以使用付费的Glyph Designer。有了这款跨平台的ShoeBox,美术人员可以完全按照自己的设计,做出一套位图字体,支持中文。

工作流程

1.右键打开设置面板,在Txt Chars输入你要制作的字体,如我这里输入abcdefghijklmnopqrstuvwxyz这些字体,最后给大家呈现一个cocos quick的美术效果。FollowMe!

2.按住BitmapFont图标2s左右,会复制我们的字符集到剪切板上,这时候打开Photoshop,新建一个文件,用文本输入工具或者热键T点击画布,然后粘帖剪切板上的字符到画布上。如下图所示

字体毫无美感可言,不是么?

3.下面是发挥设计能力的时候了。让美术尽情设计字体吧。我随便给文字加了个描边、外发光、投影,如下图

4.保存刚才的设计,导出为PNG图片。这里需要注意下,在设计的时候字体的间距不要太靠近,否则ShoeBox在后面识别的过程,会将太过于靠近的字体识别为一个字体。

5.拖放上步保存的图片到BitmapFont图标上,这时候会有流动的虚线表示可以拖放有效。如果你拖个JPG图片的,是无效的。

6.见证奇迹的时刻到了!

在设置面板的 Message 一栏里面,我们输入cocos quick,设定字符间距(KerningValue)为合适的值比如-2,空格宽度(TxtSpaceWidth)为5,然后apply应用确定,可以看到文字的预览效果。

7.参数调整合适后,保存字体(SaveFont),会在原始图片目录下生成一个PNG图片和FNT数据文件。这时候引擎就可以直接使用这两个文件了。

如果大家觉得看着文字麻烦,可以先看下视频。

Happy Designing!

作为暴雪资深迷,一口气买了3本小说,《巨龙的黄昏》《战争之潮》主要是围绕《大地的裂变》资料片,分别以萨尔、吉安娜为主线讲述了大地的裂变所发生的故事,而《部落的暗影》以巨魔领袖暗影猎手沃金、熊猫人陈·风暴烈酒、祝踏岚以及人类提尔森为主线讲述了发生在潘达利亚的传奇。工作累了阅读放松下,美哉!

今天看到咱们quick群里有人在问如何实现多重继承,想想自己好像还真没用过lua中的多重继承,确切的说没用过。但如果真用到多重继承的时候,我会想想是不是非得用多重继承,总觉得多重继承有点“重”。好了废话不说,查阅手册,研究了下。看来在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
local function search(k, tables)
for i, v in ipairs(tables) do
if v[k] then
return v[k]
end
end
return nil
end

-- 这里实现多重继承,参数arg为多重父类表
function createClassFrom(...)
-- c为返回值的目标表,为实现多重继承的子类表
local c = {}
local parents = {...}
setmetatable(c, {__index = function(t, k)
return search(k, parents)
end})

function c:new(o)
o = o or {}
setmetatable(o, {__index = c})
return o
end

return c
end

-- 人 吃饭
Human = {name = "human"}
function Human:eat()
print("human eat")
end

-- 程序员 写代码
Programmer = {name = "coder"}
function Programmer:doProgramming()
print("do coding")
end

-- 女程序员 继承 人和程序员
-- 性别女
FemaleProgrammer = createClassFrom(Human, Programmer)
local femaleCoder = FemaleProgrammer:new({sex = "female", canBear = function() print("Female: can give birth a baby!") end})
femaleCoder:eat() -- human eat
femaleCoder:doProgramming() -- do coding

上面代码难点在于理解lua中__index的用法。我们在lua中实现继承的时候,会用到这个__index。我们再次看看这个__index究竟是怎么回事。元表的__index可以是一个表,也可以是一个函数。

1. __index是个表的情况

__index是个表的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local Test = { group = "quick" }
function Test:new(o)
o = o or {}
setmetatable(o, {__index = Test})
return o
end

function Test:getName()
return self.name
end

function Test:setName(name)
self.name = name
end

local a = Test:new({name = "Just a test"})
print(a:getName()) -- Just a test
print(a.group) -- quick

当表a调用自身所没有的方法( getName() )或者属性(group)的时候, lua会通过getmetatable(a)得到a的元表{index = Test}, 而该元表的`index`是个表Test,则lua会在这个表Test中看看是否有缺少的域方法(“getName”)以及属性(group),如果找到了则会调用表Test的方法或者属性。

2. __index 是函数的情况

__index 是函数的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local Test = { }
Test.prototype = { group = "quick",
qq = "284148017",
company = "chukong",
getName = function() return "Just a test" end}

function Test:new(o)
o = o or {}
setmetatable(o, {__index = function(table, key)
return Test.prototype[key]
end})
return o
end

local a = Test:new()
print(a:getName()) -- Just a test
print(a.qq) -- 284148017
print(a.company) -- chukong

当表a调用自身所没有的方法(getName)或者属性(qq/company)的时候, lua会通过getmetatable(a)得到a的元表
{__index = function(table, key) return Test.prototype[key] end}, 而该元表的__index是个函数,该函数的实参依次为正在调用方法、属性的表a以及表a中缺失的方法名或属性(键值key),lua会将这两个实参传入进去并调用__index指向的函数。

例如:

  • a:getName()时,就会调用a的元表的__index方法,将自身a以及”getName”键名依次传入进去,返回了Test.prototype["getName"]该函数,lua会自动调用该函数,所以a:getName()会返回Just a test
  • a.qq时,就会调用a的元表的__index方法,将自身a以及”qq”键名依次传入进去,返回了Test.prototype["qq"]284148017

相关阅读

最后

如果代码有问题或者其他疑问,欢迎一起探讨学习
Happy Coding:)

Effective C++

=====================================

改善程序与设计的55个具体做法

55 Specific Ways to Improve Your Programs and Designs

这几天在阅读这本书,确实是本很棒的书,想起以前自己栽的坑,后悔没有早一点阅读。先摘抄了下这55条做法。

  1. 视 C++ 为一个语言联邦
  2. 尽量以 const, enum, inline 替换 #define
  3. 尽可能使用 const
  4. 确定对象被使用前已被
  5. 了解 C++ 默默编写并调用哪些函数
  6. 若不想使用编译器自动生成的函数,就该明确拒绝
  7. 为多态基类声明 virtual 析构函数
  8. 别让异常逃离析构函数
  9. 绝不在构造和析构过程中调用 virtual 函数
  10. 令 operator= 返回一个 reference to *this
  11. 在 operator= 中处理“自我赋值”
  12. 复制对象时勿忘每一个成分
  13. 以对象管理资源
  14. 在资源管理类中小心 coping 行为
  15. 在资源管理类中提供对原始资源的访问
  16. 成对使用 new 和 delete 时要采取相同形式
  17. 以独立语句将 newed 对象置入智能指针
  18. 让接口容易被正确使用,不易被误用
  19. 设计 class 犹如设计 type
  20. 宁以 pass-by-reference-to-const 替换 pass-by-value
  21. 必须返回对象时,别妄想返回其 reference
  22. 将成员变量声明为 private
  23. 宁以 non-member、non-friend 替换 member 函数
  24. 若所有参数皆需类型转换,请为此采用 non-member 函数
  25. 考虑写出一个不抛异常的 swap 函数
  26. 尽可能延后变量定义式的出现时间
  27. 尽量少做转型动作
  28. 避免返回 handles 指向对象内部成分
  29. 为“异常安全”而努力是值得的
  30. 透彻了解 inlining 的里里外外
  31. 将文件间的编译依存关系降至最低
  32. 确定你的 public 继承塑模出 is-a 关系
  33. 避免遮掩继承而来的名称
  34. 区分接口继承和实现继承
  35. 考虑 virtura 函数以外的其他选择
  36. 绝不重新定义继承而来的 not-virtual 函数
  37. 绝不重新定义继承而来的缺省参数值
  38. 通过复合塑模出 has-a 或 “根据某物实现出”
  39. 明智而审慎地使用 private 继承
  40. 明智而审慎地使用多重继承
  41. 了解隐式接口和编译期多态
  42. 了解 typename 的双重意义
  43. 学习处理模板化基类内的名称
  44. 将与参数无关的代码抽离 templates
  45. 运用成员函数模板接受所有兼容类型
  46. 需要类型转换时请为模板定义非成员函数
  47. 请使用 traits classes 表现类型信息
  48. 认识 template 元编程
  49. 了解 new-handler 的行为
  50. 了解 new 和 delete 的合理替换时机
  51. 编写 new 和 delete 时需固守常规
  52. 写了 placement new 也要写 placement delete
  53. 不要轻忽编译器的警告
  54. 让自己熟悉包括 TR1 在内的标准程序库
  55. 让自己熟悉 Boost

这一节让我们来理解下Chipmunk2D中的销关节约束。

首先看文档中的一些解释

1
2
3
cpPinJoint *cpPinJointAlloc(void)
cpPinJoint *cpPinJointInit(cpPinJoint *joint, cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2)
cpConstraint *cpPinJointNew(cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2)

a和b是被连接的两个刚体,anchr1和anchr2是这两个刚体的锚点。当关节被创建的时候距离便被确定,如果你想要设定一个特定的距离,使用setter函数来重新设定该值。

getter/setter函数

  • cpVect cpPinJointGetAnchr1(const cpConstraint *constraint)
  • void cpPinJointSetAnchr1(cpConstraint *constraint, cpVect value)
  • cpVect cpPinJointGetAnchr2(const cpConstraint *constraint)
  • void cpPinJointSetAnchr2(cpConstraint *constraint, cpVect value)
  • cpFloat cpPinJointGetDist(const cpConstraint *constraint)
  • void cpPinJointSetDist(cpConstraint *constraint, cpFloat value)

在Cocos2DX中,销关节被封装成了PhysicsJointDistance,我们先来看看该类头文件。

PhysicsJointDistance类

PhysicsJointDistance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Set the fixed distance with two bodies */
class PhysicsJointDistance : public PhysicsJoint
{
public:
static PhysicsJointDistance* construct(PhysicsBody* a, PhysicsBody* b, const Point& anchr1, const Point& anchr2);

float getDistance() const;
void setDistance(float distance);

protected:
bool init(PhysicsBody* a, PhysicsBody* b, const Point& anchr1, const Point& anchr2);

protected:
PhysicsJointDistance() {}
virtual ~PhysicsJointDistance() {}
};

PhysicsJointDistance这个名字和销关节的工作机制还是很贴切的。(在Box2D中也有距离关节,b2DistanceJoint)。当关节被创建的时候,刚体a和刚体b的锚点距离就被定了下来。假如后面我们要对该距离进行修改,可以通过setDistance()方法来设定距离值。当设定的值不等于刚体之前的锚点间距时,我们会发现画面上刚体锚点的间距会发生突变,那是Chipmunk在按照你新设定的间距在修正。

1.注意:anchr1anchr2针对的是刚体a和刚体b的局部坐标系。

2.注意:PhysicsJointPin可不是销关节,其实是枢轴关节(PivotJoint)!PhysicsJointDistance才是我们这里说的销关节。

为了更好的理解销关节的工作机制,我做了几个演示来说明下:

图示1

图示2(多个销关节对绳索的模拟)

图示3

图示3 Github地址

走进COCOS2DX的物理世界


Box2D作者

Chipmunk2D作者

关于物理引擎,概括性的介绍,朋友们可以看下物理引擎-百度百科物理引擎分类-维基百科。Box2D和Chipmunk2D这两个物理引擎精确的说是刚体物理仿真库,这里的物理就是刚体动力学。而我们在谈到这物理引擎的时候,经常会听到刚体(Rigid body)。那什么是刚体?刚体就是在任何力的作用下,体积和形状都不发生改变的物体。动力学又是什么?动力学是计算刚体在受力作用下随时间移动并相互作用的一个过程。Box2D和Chipmunk2D两个引擎的建构理论基础当然基于此。

作为物理引擎来讲,两个物理引擎在一些基本概念上都是一致的,有的只是说法不同而已。如Box2D中将物理世界称之为world,而Chipmunk则称之为space,其实描述的都是碰撞/物理世界,即physics world。一些概念如shape-形状,body-刚体,contact-接触,joint-关节等都一样。

在Cocos2dX中,包括了这两个物理引擎库。开发者可以自由的选择使用哪个物理引擎。但我想不少开发者都在纠结使用哪一个。1000个读者就有1000个哈姆雷特,每个人看法不同,最后的决定也不同。那两大引擎究竟大的区别在哪?这里有几个链接相信大家看了后会有自己的判断。

1. 哪个更容易学习?

这个取决于你。50%的开发者认为Box2D比Chipmunk2D要容易,而50%的却认为Chipmunk更容易。我想这是在国外的情况。在国内,Box2D的资料相对Chipmunk更多点,如果对两个引擎都是新接触的话,很明显国内开发者的博客、文档资料、论坛甚至包括一些box2d的游戏源码会让你学起box2d更为容易一些。我以前做as开发的时候用了一下box2d,但只是皮毛,那时候学起来就觉得丰富的文档资料、前人的博客经验分享对学习有着莫大的帮助。最近在学习Chipmunk的时候,谷歌+百度,发现国内这方面资料确实不多。发起Chipmunk中文官方文档的翻译也是我逼不得已的一个举动,一方面想填补国内该引擎中文官方文档的空白,一方面也是方便自己学习。最近文档刚翻译完,这里非常感谢泰然组以及folk进行斧正错误的一些朋友们。文档可见Chipmunk2D中文手册-github。顺便提一下,Chipmunk中文交流社区欢迎朋友们加入进来,无论是对项目的folk还是关注,提Issue,这都是你学习Chipmunk最好的方式。这里将会发展成国内最好的一个Chipmunk2D资料分享以及学习平台。以后千万不要告诉妈妈说,Chipmunk文档资料太少了,不好学。

2. 性能比较呢?

这里有数据说话

在很多测试中Chipmunk的性能要明显优于Box2D。对于对性能不做太大要求的游戏,我们完全可以忽略这点差异。

3. 还有其他差异吗?

有。Box2D支持“bullet”,也就是我们说的高速移动物体(形象地称为“子弹”)。高速移动的物体在一瞬间可能会错误的穿越过一些物体(被称为“隧道效应”),box2d通过连续碰撞检测来防止这种情况发生。而Chipmunk没有支持这个特性。

另外最显著也是我们常常谈论的,这俩引擎的关节有些不一样。是的。他们有的作用原理是一样的,只是命名不同,如box2d中的旋转关节(RevoluteJoint)就好比Chipmunk中的枢轴关节(PivotJoint)。而有些如box2d中的摩擦力关节(FrictionJoint),滑轮关节(PulleyJoint)等Chipmunk中并没有,而Chipmunk中的一些阻尼弹簧(DampedSpring),简单马达(SimpleMotor)等Box2d中并没有。但不用担心,通过关节的一些合理组合,两个物理引擎基本上都可以模拟出彼此的任何关节。

还有很多其他差异,读者可以看上面讨论的4个链接。这里就不详细介绍了。

4. Box2D是用C++写的,更面向对象,Chipmunk是c写的,面向对象使用起来不方便

这个就不用再纠结了。下面我们来看看Cocos2dX关于物理引擎部分都为我们做了什么。

Cocos2dX对Chipmunk进行了一些封装,封装工作做了下面几个工作。

1.针对Chipmunk设计良好的c接口进行了一些封装,在cocos2dx v3.0beta2版本中封装的物理库目录如下

  • 物理世界 CCPhysicsWorld.h
  • 物理世界信息 CCPhysicsWorldInfo_chipmunk.h
  • 刚体 CCPhysicsBody.h
  • 刚体信息 CCPhysicsBodyInfo_chipmunk.h
  • 形状 CCPhysicsShape.h
  • 形状信息 CCPhysicsShapeInfo_chipmunk.h
  • 约束关节 CCPhysicsJoint.h
  • 约束关节信息 CCPhysicsJointInfo_chipmunk.h
  • 接触 CCPhysicsContact.h
  • 接触信息 CCPhysicsContactInfo_chipmunk.h

  • 帮助类 CCPhysicsHelper_chipmunk.h, 包括了一些常用的静态转换函数, 主要指的是cocos2dx中的运算类型和Chipmunk的运算类型的一些互转,如cpv2point,cpv2size,cpfloat2float,rect2cpbb等以及反向转换函数。

2.在extensions/physics-nodes文件夹下,还存在着两个类。分别是CCPhysicsSprite.hCCPhysicsDebugNode.h

CCPhysicsSprite.h物理精灵类:通过设置预处理宏CC_ENABLE_CHIPMUNK_INTEGRATION(开启Chipmunk迭代)或者CC_ENABLE_BOX2D_INTEGRATION(开启Box2D迭代)来启用相应的引擎。

如果尝试启用两个,则会抛出"Either Chipmunk or Box2d should be enabled, but not both at the same time"错误提示。开发者启用哪个引擎,则应该调用该引擎对应的此类成员函数,如果尝试调用另外一个未启用引擎对应的成员函数,则会抛出断言,“兄弟,别乱调!”

CCPhysicsDebugNode.h调试节点类:这个类继承于DrawNode,用来调试用。将space空间传入,这个类会遍历出空间中所有的形状(包括圆形、线段、多边形等)、约束关节(销关节、滑动关节、枢轴关节等等)进行绘制,并配合不同的颜色来标识。详细信息可查看该类。

3.不要以为只有前两条就完了,前面的封装是封装好了,但引擎的数据模拟和图形渲染是独立开的,那最后一步当然是对两者进行绑定。也就意味着将刚体和我们的node节点进行绑定,熟悉box2d的都知道,bodyDef.userData = someDrawNode, 刚体定义的用户数据指针会指向我们的可视精灵,当模拟中发生刚体旋转、缩放、位移时,要同步的对可视精灵进行旋转、缩放、位移。这样我们就会看到一个仿真的物理世界。

那Cocos2dX如何做的?

首先在ccConfig.h中有个宏开关,

1
/** Use physics integration API */
#ifndef CC_USE_PHYSICS
#define CC_USE_PHYSICS 1
#endif

如果我们需要用到物理引擎,设置为1,不需要的话,设置为0。

其次在CCNode,CCSprite以及CCScene中都是以CC_USE_PHYSICS启用为前提做了下面一些工作。

  • CCNode类:

成员属性

1
#if CC_USE_PHYSICS
    PhysicsBody* _physicsBody;        ///< the physicsBody the node have
#endif

成员方法

1
#if CC_USE_PHYSICS
    /**
     *   set the PhysicsBody that let the sprite effect with physics
     */
    void setPhysicsBody(PhysicsBody* body);

    /**
     *   get the PhysicsBody the sprite have
     */
    PhysicsBody* getPhysicsBody() const;

    /**
     *   update rotation and position from physics body
     */
    virtual bool updatePhysicsTransform();

#endif

设置自身的PhysicsBody成员属性,同时还包括位置、角度等同步的一些操作

  • CCSprite类:和CCNode类似,多了一个dirty设置,跟图形绘制相关。
  • CCScene类:
1
#if CC_USE_PHYSICS
public:
    virtual void addChild(Node* child, int zOrder, int tag) override;
    virtual void update(float delta) override;
    inline PhysicsWorld* getPhysicsWorld() { return _physicsWorld; }
    static Scene *createWithPhysics();
protected:
    bool initWithPhysics();
    void addChildToPhysicsWorld(Node* child);

    PhysicsWorld* _physicsWorld;
#endif

平常我们通过Scene::create()来创建一个普通的场景,而创建一个物理世界的场景只需Scene::createWithPhysics()就可以了,非常容易不是么?这里虚函数addChild的重写,会递归将该node以及子node的刚体都加入到空间(物理世界)中。

总的来说,Cocos2dX 3.0版本对物理引擎做的工作使得我们开发物理效果类游戏更方便,追根溯源,对Chipmunk的封装起到了关键作用。后面我会慢慢更新一些对Chipmunk的基本概念的理解、如何使用的一些文章以及demo演示。

题外话:当QuickCocos2DX同步到3.0时,物理引擎部分也会相应导出绑定到lua,加之廖大团队对之进一步的封装完善,你会发现到时用起lua写物理游戏,岂是一个爽字了的。有朋友指出在quick-cocos2d-x中没有box2d,确实cocos2dx官方团队一直没对Box2D做luabinding,不过论坛有个朋友做了这样的工作,自己没测试过,链接地址:http://www.cocos2d-x.org/forums/11/topics/3181?r=40339

欢迎大家一起交流。Happy coding!


BOX2D相关资料(保持更新)

Chipmunk2D相关资料(保持更新)