HappyCoding:] ChildhoodAndy

思维世界的发展,在某种意义上说,就是对惊奇的不断摆脱。——爱因斯坦

Posts match “ cocos2dx ” tag:

详解QuickCocos2dX状态机模式设计

| Comments


目录

注:写这篇文章的时候,笔者所用的是quick-cocos2d-x 2.2.1rc版本。

quick状态机

状态机的设计,目的就是为了避免大量状态的判断带来的复杂性,消除庞大的条件分支语句,因为大量的分支判断会使得程序难以修改和扩展。但quick状态机的设计又不同设计模式的状态模式,TA没有将各个状态单独划分成单独的状态类,相反根据js、lua语言的特点,特别设计了写法,使用起来也比较方便。

quick框架中的状态机,是根据javascript-state-machine重新设计改写而成,同时sample/statemachine范例也是根据js版demo改写而来。该js库现在是2.2.0版本。基于js版的README.md,结合廖大的lua版重构,我针对状态机的使用做了点说明,如果有不对的地方,感谢指出:)。

推荐大家在理解的时候结合sample/statemachine范例进行理解,注意player设置成竖屏模式,demo里面的按钮在横屏模式下看不见。

sample图示


用法

创建一个状态机

local fsm = StateMachine.new()
-- (注:和demo不同的是,demo采用组件形式完成的初始化)

fsm:setupState({
    initial = "green",
    events  = {
            {name = "warn",  from = "green",  to = "yellow"},
            {name = "panic", from = "green",  to = "red"   },
            {name = "calm",  from = "red",    to = "yellow"},
            {name = "clear", from = "yellow", to = "green" },
    }
})

之后我们就可以通过

  • fsm:doEvent("start")-从"none"状态转换到"green"状态
  • fsm:doEvent("warn")-从"green"状态转换到"yellow"状态
  • fsm:doEvent("panic")-从"green"状态转换到"red"状态
  • fsm:doEvent("calm")-从"red"状态转换到"yellow"状态
  • fsm:doEvent("clear")-从"yellow"状态转换到"green"状态

同时,

  • fsm:isReady()-返回状态机是否就绪
  • fsm:getState()-返回当前状态
  • fsm:isState(state)-判断当前状态是否是参数state状态
  • fsm:canDoEvent(eventName)-当前状态如果能完成eventName对应的event的状态转换,则返回true
  • fsm:cannotDoEvent(eventName)-当前状态如果不能完成eventName对应的event的状态转换,则返回true
  • fsm:isFinishedState()-当前状态如果是最终状态,则返回true
  • fsm:doEventForce(name, ...)-强制对当前状态进行转换

单一事件的多重from和to状态

如果一个事件允许我们从多个状态(from)转换到同一个状态(to), 我们可以通过用一个集合来构建from状态。如下面的"rest"事件。但是,如果一个事件允许我们从多个状态(from)转换到对应的不同的状态(to),那么我们必须将该事件分开写,如下面的"eat"事件。

local fsm = StateMachine.new()
fsm:setupState({
    initial = "hungry",
    events  = {
            {name = "eat",  from = "hungry",     to = "satisfied"},
            {name = "eat",  from = "satisfied",  to = "full"},
            {name = "eat",  from = "full",       to = "sick"   },
            {name = "rest", from = {"hungry", "satisfied", "full", "sick"},  to = "hungry"},
    }
})

在设置了事件events之后,我们可以通过下面两个方法来完成状态转换。

  • fsm:doEvent("eat")
  • fsm:doEvent("rest")

rest事件的目的状态永远是hungry状态,而eat事件的目的状态取决于当前所处的状态。

注意1:如果事件可以从任何当前状态开始进行转换,那么我们可以用一个通配符*来替代from状态。如rest事件,我们可以写成{name = "rest", from = "*", to = "hungry"}

注意2:上面例子的rest事件可以拆分写成4个,如下:

{name = "rest", from = "hungry",    to = "hungry"},
{name = "rest", from = "satisfied", to = "hungry"},
{name = "rest", from = "full",      to = "hungry"},
{name = "rest", from = "sick",      to = "hungry"}

回调

quick的状态机支持4种特定事件类型的回调:

  • onbeforeEVNET- 在特定事件EVENT开始前被激活
  • onleaveSTATE - 在离开旧状态STATE时被激活
  • onenterSTATE - 在进入新状态STATE时被激活
  • onafterEVENT - 在特定事件EVENT结束后被激活

注解:编码时候,EVENT/STATE应该被替换为特定的名字

为了便利起见,

  • onenterSTATE可以简写为onSTATE
  • onafterEVENT可以简写为onEVENT

所以假如要使用简写的话,为了避免onSTATEonEVENT的STATE/EVENT被替换成具体的名字后名字相同引起问题,to状态和name名字尽量不要相同。比如

-- 角色开火

{name = "fire",   from = "idle",    to = "fire"}
--假如使用简写

--onSTATE --- onfire

--onEVENT --- onfire,回调会引起歧义。


--如果不使用简写

--则onenterSTATE --- onenterfire

--onafterEVENT --- onafterfire

另外,我们可以使用5种通用型的回调来捕获所有事件和状态的变化:

  • onbeforeevent- 在任何事件开始前被激活
  • onleavestate - 在离开任何状态时被激活
  • onenterstate - 在进入任何状态时被激活
  • onafterevent - 在任何事件结束后被激活
  • onchangestate - 当状态发生改变的时候被激活

注解:这里是任何事件、状态, 小写的event、state不能用具体的事件、状态名字替换。

回调参数

所有的回调都以event为参数,该event为表结构,包含了

  • name 事件名字
  • from 事件表示的起始状态
  • to 事件表示的目的状态
  • args 额外的参数,用来传递用户自定义的一些变量值
local fsm = StateMachine.new()
fsm = fsm:setupState({
        initial = "green",
        events  = {
                {name = "warn",  from = "green",  to = "yellow"},
                {name = "panic", from = "green",  to = "red"   },
                {name = "calm",  from = "red",    to = "yellow"},
                {name = "clear", from = "yellow", to = "green" },
        },
        callbacks = {
            onbeforestart = function(event) print("[FSM] STARTING UP") end,
            onstart       = function(event) print("[FSM] READY") end,
            onbeforewarn  = function(event) print("[FSM] START   EVENT: warn!") end,
            onbeforepanic = function(event) print("[FSM] START   EVENT: panic!") end,
            onbeforecalm  = function(event) print("[FSM] START   EVENT: calm!") end,
            onbeforeclear = function(event) print("[FSM] START   EVENT: clear!") end,
            onwarn        = function(event) print("[FSM] FINISH  EVENT: warn!") end,
})
fsm:doEvent("warn", "some msg")

如上例子,fsm:doEvent("warn", "some msg")中的some msg作为额外的参数字段args结合name from to被添加到event,此时

event = {
    name = "warn",
    from = "green",
    to   = "yellow",
    args = "some msg"
}

event表正是回调函数的参数。

回调顺序

用{name = "clear", from = "red", to = "green"}举例,我画个示意图来说明

注意:之前的onbeforeEVENT,这里EVENT就被具体替换为clear,于是是onbeforeclear,而onbeforeevent类似的通用型则不用替换。

  • onbeforeclear - clear事件执行前的回调
  • onbeforeevent - 任何事件执行前的回调
  • onleavered - 离开红色状态时的回调
  • onleavestate - 离开任何状态时的回调
  • onentergreen - 进入绿色状态时的回调
  • onenterstate - 进入任何状态时的回调
  • onafterclear - clear事件完成之后的回调
  • onafterevent - 任何事件完成之后的回调
3种影响事件响应的方式
  1. onbeforeEVENT方法中返回false来取消事件
  2. onleaveSTATE方法中返回false来取消事件
  3. onleaveSTATE方法中返回ASYNC来执行异步状态转换

异步状态转换

有时候,我们需要在状态转换的时候执行一些异步性代码来确保不会进入新状态直到代码执行完毕。
举个例子来说,假如要从一个menu状态转换出来,或许我们想让TA淡出?滑出屏幕之外?总之执行完动画再进入game状态。

我们可以在onleavestate或者onleaveSTATE方法里返回StateMachine.ASYNC,这时状态机会被挂起,直到我们使用了event的transition()方法。

...
onleavered    = function(event)
                self:log("[FSM] LEAVE   STATE: red")
                self:pending(event, 3)
                self:performWithDelay(function()
                    self:pending(event, 2)
                    self:performWithDelay(function()
                        self:pending(event, 1)
                        self:performWithDelay(function()
                            self.pendingLabel_:setString("")
                            event.transition()
                        end, 1)
                    end, 1)
                end, 1)
                return "async"
            end,
...            

提示:如果想取消异步事件,可以使用event的cancel()方法。


初始化选项

  • 状态机的初始化选项一般根据我们游戏需求来决定,quick状态机提供了几个简单的选项。 在默认情况下,如果你没指定initial状态,状态机会指定当前状态为none状态,所以需要定义一个能将none状态转换出去的事件。
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
        events  = {
            {name = "startup", from = "none",   to = "green" },
            {name = "panic",   from = "green",  to = "red"   },
            {name = "calm",    from = "red",    to = "green"}
        }
    })
    echoInfo(fsm:getState()) -- "none"
    
    fsm:doEvent("start")
    echoInfo(fsm:getState()) -- "green"
    
  • 如果我们特别指定了initial状态,那么状态机在初始化的时候会自动创建startup事件,并且被执行。
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
        initial = "green",
        events  = {
            -- 当指定initial状态时,这个startup事件会被自动创建,所以可以不用写这一句 {name = "startup", from = "none",   to = "green" },
    
            {name = "panic",   from = "green",  to = "red"   },
            {name = "calm",    from = "red",    to = "green"}
        }
    })
    echoInfo(fsm:getState()) -- "green"
    
  • 我们也可以这样指定initial状态:
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
        initial = {state = "green", event = "init"},
        events  = {
            {name = "panic",   from = "green",  to = "red"   },
            {name = "calm",    from = "red",    to = "yellow"}
        }
    })
    echoInfo(fsm:getState()) -- "green"
    
  • 如果我们想延缓初始化状态转换事件的执行,我们可以添加defer = true
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
        initial = {state = "green", event = "init", defer = true},
        events  = {
            {name = "panic",   from = "green",  to = "red"   },
            {name = "calm",    from = "red",    to = "green"}
        }
    })
    echoInfo(fsm:getState()) -- "none"
    
    fsm:doEvent("init")
    echoInfo(fsm:getState()) -- "green"
    

异常处理

在默认情况下,如果我们尝试着执行一个当前状态不允许转换的事件,状态机会抛出异常。如果选择处理这个异常,我们可以定义一个错误事件处理。在quick中,发生异常的时候StateMachine:onError_(event, error, message)会被调用。

local fsm = StateMachine.new()
fsm:setupState({
    initial = "green",
    events  = {
            {name = "warn",  from = "green",  to = "yellow"},
            {name = "panic", from = "green",  to = "red"   },
            {name = "calm",  from = "red",    to = "green"},
            {name = "clear", from = "yellow", to = "green" },
    }
})
fsm:doEvent("calm") -- fsm:onError_会被调用,在当前green状态下不允许执行calm事件

本文如果有写的不对的地方,还请大家指出,交流学习:)
如果朋友们有关于状态机的使用心得,也非常欢迎分享。

走进COCOS2DX的物理世界

| Comments

走进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中有个宏开关,

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

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

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

  • CCNode类:

成员属性

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

成员方法

#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类:
#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相关资料(保持更新)

详解Cocos2DX中Chipmunk碰撞过滤

| Comments

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

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

1.Box2D 碰撞过滤实现机制

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

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

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

过滤规则

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

额外的一些规则

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

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

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

举个例子:

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,即查询否定拒绝。

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:)