Box2D C++ 教程-碰撞回调_box2d c++课程17-碰撞回调-程序员宅基地

技术标签: Box2D C++教程  

声明:本教程翻译自:Box2D C++ tutorials Collision callbacks,仅供学习参考。

  • 碰撞回调(Collision callbacks)

当物理场景中的物体移动或者彼此之间发生碰撞的时候,Box2D会为我们处理所有必要的碰撞监测,对于这一点我们不必过于担心。但是就整个游戏来说,偶尔我们需要在物体发生碰撞的时候触发一些事件。例如当游戏玩家触碰到怪物的时候,玩家应该over,等等。我们需要一种从物理引擎获取此信息的方法。

在画自己的图像这个话题中,我们在游戏场景中创建了一个物体作为研究对象,并在每一帧对物体当前位置做了查询操作,并对物体进行了渲染。之所以每帧对物体都进行渲染,是因为物体的位置/旋转每一帧似乎也在不停的变化。但是碰撞则有一点点不一样,它并不是每一帧都会发生,它的发生频率通常少于渲染帧率。所以如果还是每帧都去询问物理引擎某个物体是否发生碰撞,这就像是旅行当中,车中后座的孩子不停的问你:”我们到了吗?怎么还没到啊…“,而大部分时候得到的答案是:”还没有,马上就到”,所以如果每一帧都对物理世界中的实体进行遍历并不一定每次都能获得新状态的变化(除非物体确实有了新变化),而且这么做也是低效的。所以如果是开车远行的话,最好告诉车后的孩子们:”保持安静!当到达目的地的时候我会告诉你们的”。

在Box2D中我们就是通过这种方式进行方法回调的。当物体发生碰撞时,我们可以在这个方法中完成一些操作。当然了,事先我们不确定是哪两个物体发生碰撞,所以当物体真正发生碰撞的时候,我们需要把方法作为参数传递过去,这样我们就把方法交给了Box2D,就好像告诉Box2D说:”hi,有碰撞发生的时候,调用这个方法!”。

  • 回调时间(Callback timing)

在每次调用b2World::Step()方法时,将会对物理世界中实体之间的碰撞进行检测。就像我们在worlds话题中提到的那样,b2World::Step()方法推动了整个物理模拟过程,移动或旋转物体实体等。一旦检测到碰撞发生,程序就会回调你的方法来完成你指定的操作。当执行完指定的操作之后,会再次回到b2World::Step()方法执行剩下的程序,此刻你不应该做任何改变场景的操作,因为在同一时间步长中或许还有更多的碰撞发生。

pic

设置碰撞回调方法的过程和自定义debug draw类似。创建一个b2ContactListener类的子类,类中包含了一堆可以重写的虚方法,其中有一些方法我们会用到:

1
2
3
4
5
//b2ContactListener
// Called when two fixtures begin to touch
virtual void BeginContact(b2Contact* contact);
// Called when two fixtures cease to touch
virtual void EndContact(b2Contact* contact);

注意,这里也提到了一个检测碰撞结束的方法,后面我们会做有提到。

  • 回调信息(Callback information)

一旦你创建了b2ContactListener的子类,我们将会知道哪两个物理实体会发生碰撞,然后我们可以就可以根据自己的需要完成一些自定义的功能。但是等下….貌似并没有哪个物理实体支持BeginContact/EndContact方法,那么我们怎么如何知道到底是哪两个物体发生了碰撞呢?其实contact参数包含了所有我们想要的信息。其中最主要的一件事是想知道哪两个定制器(fixtures)发生了碰撞,我们可以通过下面这两个方法获得对应的定制器(fixtures)。

1
2
3
4
5
//b2Contact
// Get the first fixture in this contact
b2Fixture* GetFixtureA();
// Get the second fixture in this contact
b2Fixture* GetFixtureB();

但是貌似还是不知道是哪个物体…?定制器(fixture)类本身有一个GetBody()方法可以获得对应的物体,如果contact也有一个方法可以直接获得物体就好了,其实这么做并没有必要,实际中我发现碰撞是发生在定制器(fixture)之间的,不是物体之间。为了更好的说明之间的关系,下面这张图对于说明这一点可能会有所帮助:

pic

上图这个例子中,动态物体(Dynamic body)包括四个定制器(Fixture),当中的一个定制器和静态物体相接触,在BeginContact方法中,contact参数可以通过GetFixtrueA()方法返回其中一个定制器,GetFixtureB()方法可以返回另一个。但我们并不知道哪个是哪个,所以需要我们自己做判断。

  • 举例(Example)

作为本次话题的例子,我们会使用BeginContact/EndContact碰撞回调方法,在小球发生碰撞的时候改变它们的颜色。场景的构造方面,我们在画自己的图像这个话题中,讨论了在正确的位置画出笑脸。而此时,场景中只需要一个小球而已。

为Ball类添加布尔成员变量,来记录当前小球是否发生碰撞,当有contacts参数发生变化的时候,物理引擎会会通知我们,并进行方法回调。在渲染方法中,我们会根据contacts参数状态来对颜色进行设置。

1
2
3
4
5
6
7
8
9
10
11
12
//Ball class member variable
bool m_contacting;
//in Ball class constructor
m_contacting = false;
//Ball class functions
void startContact() {
            m_contacting = true; }
void endContact() {
            m_contacting = false; }
//in Ball::render
if ( m_contacting )
    glColor3f(1,0,0);//red
else
    glColor3f(1,1,1);//white

我们也可以在Ball类型的构造函数中,把物体UserData数据设置成小球对象自身的数据:

1
2
//in Ball constructor
body->SetUserData( this ); //set this Ball object in the body's user data

现在,当contact状态改变的时候,希望Box2D能够通知我们。创建一个b2ContactListener子类,实现BeginContact/EndContact方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyContactListener : public b2ContactListener
{
           
    void BeginContact(b2Contact* contact) {
           
        //check if fixture A was a ball
        void* bodyUserData = contact->GetFixtureA()->GetBody()->GetUserData();
        if ( bodyUserData )
            static_cast<Ball*>( bodyUserData )->startContact();
        //check if fixture B was a ball
        bodyUserData = contact->GetFixtureB()->GetBody()->GetUserData();
        if ( bodyUserData )
            static_cast<Ball*>( bodyUserData )->startContact();
    }
    void EndContact(b2Contact* contact) {
           
        //check if fixture A was a ball
        void* bodyUserData = contact->GetFixtureA()->GetBody()->GetUserData();
        if ( bodyUserData )
            static_cast<Ball*>( bodyUserData )->endContact();
        //check if fixture B was a ball
        bodyUserData = contact->GetFixtureB()->GetBody()->GetUserData();
        if ( bodyUserData )
            static_cast<Ball*>( bodyUserData )->endContact();
    }
};

乍看起来好像写了很多代码,其实仔细看会发现很多都是重复的。首先我们检查物体的定制器中的UserData字段是否有值,如果有,一定是ball,如果没有,那么一定是静态物体的墙。对于这样一个简单的场景来说,做起来还算容易,但是对真正的游戏开发来说,实现起来恐怕要会复杂一些。不管怎么说,既然我们决定把定制器附加到一个Ball类对象上,并且把UserData指针指向Ball对象,而且还可以适当的调用改变一些状态的方法。注意我们需要对定制器A和B都做同样的操作,因为两个可能都是小球。现在,可以在Box2D世界中使用这些方法进行碰撞操作,我们可以使用SetContactListener方法,此方法可以设置一个指向b2ContactListener对象的指针,所以我们需要一个类的实例,这个需求很容易满足,只要在你的新类中声明一个全局变量就ok了。

1
2
3
4
//at global scope
MyContactListener myContactListenerInstance;
//in FooTest constructor
m_world->SetContactListener(&myContactListenerInstance);

现在,当小球和墙接触的时候,就会变成红色,离开墙的时候,就会变回白色。

pic

看起来已经ok了,特别是对于这个简单场景来说。但是方法中还存在着一个问题,用鼠标强制把小球推到墙角,这时小球和两面墙都有接触。现在让小球沿着一面墙进行滑动,离开墙角。小球会变成白色,即便它现在仍然和一面墙有所接触!当小球离开墙角的时候到底发生了什么呢?当一面墙不再接触的一刹那,EndContact被触发了,小球的m_contacting变量被设置成了false,幸运的是,这比较容易发现,由于此处使用布尔值简单的记录on/off状态,我们只需要使用一个整型值代替,以此来增加/减少当前接触的定制器(fixture)的数量。

1
2
3
4
5
6
7
8
9
10
11
12
//replace old boolean variable m_contacting in Ball class
int m_numContacts;
//in Ball constructor
m_numContacts = 0;
//new implementation for contact state change
void startContact() {
            m_numContacts++; }
void endContact() {
            m_numContacts--; }
//altered rendering code
if ( m_numContacts > 0 )
    glColor3f(1,0,0);//red
else
    glColor3f(1,1,1);//white

现在再进行一次测试,问题应该就被解决了。现在添加一堆球,把它们推到角落,进一步验证一下此次的修改。注意当物体不停的蹦跳着互相碰撞的时候,发生接触的时候可能会产生短暂的闪烁现象。如果你在运行游戏逻辑的时候出现这种情况,这没有什么意外的,注意这一点。

pic

让我们再举一个例子,来示范一下碰撞对象之间是如何进行关联的。到目前为止,我们举的例子都是在说一个球是否发生了碰撞,现在让我们来看看这个球到底和谁发生了碰撞。下面,让我们创建一个只有一个红球的场景,当它碰撞到别的小球的时候,就把红色传递给另一个小球,同时自己变成白色,以此来标记“这个球”当前是哪一个。

为Ball类添加一个布尔值变量,以此来标记当前是不是“这个球”,创建一个全局方法,来完成传递颜色变化的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//in Ball class
bool m_imIt;
//in Ball constructor
m_imIt = false;
//in Ball::render
if ( m_imIt )
    glColor3f(1,0,0);//red
else
    glColor3f(1,1,1);//white  
//in global scope
void handleContact( Ball* b1, Ball* b2 ) {
           
    bool temp = b1->m_imIt;
    b1->m_imIt = b2->m_imIt;
    b2->m_imIt = temp;
}

这次在接触(contact)监听器(listener)回调中,我们只要实现BeginContact方法就行:

1
2
3
4
5
6
7
8
void BeginContact(b2Contact* contact) {
           
    //check if both fixtures were balls
    void* bodyAUserData = contact->GetFixtureA()->GetBody()->GetUserData();
    void* bodyBUserData = contact->GetFixtureB()->GetBody()->GetUserData();
    if ( bodyAUserData && bodyBUserData )
        handleContact( static_cast<Ball*>( bodyAUserData ),
                       static_cast<Ball*>( bodyBUserData ) );
}

最后,我们需要设置一下第一个红色小球:

1
2
//at end of FooTest constructor
balls[0]->m_imIt = true;

为了更清楚的看到发生了什么,或许你想去掉重力作用,这样可以让小球运动时间更长一点,或者把小球的恢复系数(restitution)设置的更高一点,这样可以长时间在不用手来重新推动小球。

pic

  • 真实场景(Real scenarios)

这是一个非常基础的范例,但是这个思路可以在游戏中实现很多有趣的逻辑,比如玩家撞到怪兽身上,等等。在复杂的场景中,可以在不同的实体之间发生碰撞,或许你想在定制器(fixtures)的用户数据(User Data)中设置一些你想关联的实体类型。一个可能的方法就是直接把Ball类型的指针放到用户数据(User Data)中,当然你可以设置一个通用的父类,其它类都继承自此通用类,父类中可以有一个虚方法返回类型的实例,类似于这样:

1
2
3
4
5
6
7
8
class Entity
{
           
    virtual int getEntityType() = 0;
};
class Ball : public Entity
{
           
    int getEntityType() {
            return ET_BALL; }
}

可以在碰撞回调中使用这个方法监测碰撞实体的类型,并进行类型转换。

另外可以做dynamic类型转换。老实说并没有一个我特别喜欢的方法,因为不管沿用哪种方法都要用许多if/else判断,看起来都很臃肿。如果有人有其它好的方法,希望能够告知大家!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Const_Gong/article/details/51464583

智能推荐

python执行javascript脚本文件_在Python中执行javascript-程序员宅基地

文章浏览阅读785次。在使用python抓取网页的过程中,有的时候需要执行某些简单的javascript,以获得自己需要的内容,例如执行js里面的document.write或者document.getElementById等。自己解析js代码显然有点吃力不讨好,因此最好能找到一些可以解析执行js的python库。google之可以找到三个候选者,分别是微软的ScriptControl,v8的python移植PyV8,..._wxpython运行javascript脚本

Ubuntu13.04(64bit)下用Wine安装百度云、360云、微云_wine 百度云-程序员宅基地

文章浏览阅读1.1w次,点赞4次,收藏2次。首先用安装好 wine 1.6.2: sudo a_wine 百度云

Linux磁盘格式化_linux格式化磁盘-程序员宅基地

文章浏览阅读1.7w次,点赞3次,收藏41次。Linux磁盘格式化_linux格式化磁盘

Zigbee联盟基础知识普及-程序员宅基地

文章浏览阅读1.2k次。做智能家居,还要了解ZigBee联盟,不是做好产品就好吗,与ZigBee联盟有什么关系?在具体说明这个问题之前,我们不妨先来了解一下ZigBee。ZigBee实际是一种短距离、低功耗的无线通信技术,名称来源于 ZigZag——一种蜜蜂的肢体语言。当蜜蜂新发现一片花丛后会用特殊舞蹈来告知同伴发现的食物种类及位置等信息,是蜜蜂群体间一种简单、高效的传递信息..._zigbee联盟

分段线性插值法matlab,matlab实现lagrange插值和分段线性插值-程序员宅基地

文章浏览阅读1.7k次。《matlab实现lagrange插值和分段线性插值》由会员分享,可在线阅读,更多相关《matlab实现lagrange插值和分段线性插值(4页珍藏版)》请在人人文库网上搜索。1、数值分析作业姓名:虞驰程题目:函数:fx=11+x2在-5,5上,取n=10,对其进行分段线性插值和拉格朗日插值,在Matlab中实现且绘图。Matlab实现:首先定义函数f,在Matlab中用function.m文件编...

DAY26:GetShell专题_lzcms_v1.1.5后台文件上传getshell-程序员宅基地

文章浏览阅读1k次。BEES企业网站管理系统 getshell实验、海洋影视网站-getshell实验、Lzcms_v1.1.5 后台文件上传getshell、phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)、骑士CMS模版注入+文件包含getshell、Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)、远程文件包含getshell实战靶场(云演环境)、emlog备份数据库漏洞..._lzcms_v1.1.5后台文件上传getshell

随便推点

MMlab实验室AI实战营-人体姿态估计与MMPose_人体姿态估计直接回归方法流程-程序员宅基地

文章浏览阅读432次。当骨骼发生变化时,特定关节点的位置变化对人体表面不同顶点(Vertex)的位置变化有不同影响,在混合蒙皮技术里,这种影响是由不同的权重实现的。两个阶段的检测共用了一个图像特征网络,设计了Spatial Transform Network(STN)模块,从完整的特征图中裁剪出单人对应的图像特征,用于后续关键点检测。设计思路:准去的姿态估计需要结合不同尺度的信息:局部信息(检测不同的身体组件)、全局信息(建模组件之间的关系,在大尺度变形、遮挡时也可以准确推断出姿态)绝对坐标:各关节点在相机坐标系中的坐标;_人体姿态估计直接回归方法流程

Pytorch Dataloader 模块源码分析(二):Sampler / Fetcher 组件及 Dataloader 核心代码-程序员宅基地

文章浏览阅读712次。总算写完了 DataLoader 部分,总结这一块的源码主要是因为公司最近用到了相关的业务,需要对 Dataset 和 DataLoader 进行改造,因此认真读了这一部分的源码。总而言之,Dataset 和 DataLoader 模块为整个 Pytorch 提供了通用的数据加载和预处理接口,整体代码有很高的鲁棒性。如果说这个模块还有什么可以改进的地方,主要就在于 I/O 的部分,Dataset 在实现 shuffle 操作时,加载数据使用的是随机 I/O,这会大幅降低 I/O。...

Asp类型判断及数组打印-程序员宅基地

文章浏览阅读504次。ASP类型判断TypeName 函数返回一个字符串,提供有关变量的 Variant 子类型信息。TypeName(varname)必选项的 varname 参数,可以是任何变量。返回值TypeName 函数返回值如下:值 描述Byte 字节值Integer 整型值Long 长整型值Single 单精度浮点值Double 双精度浮点值Currency 货币值Dec...

Adroid Studio 2022.3.1 版本配置greendao提示无法找到_plugin with id 'org.greenrobot.greendao' not found-程序员宅基地

文章浏览阅读647次。之前报错,主要就是id 'org.greenrobot.greendao'这个包无法找到,找不到的原因就是org.greenrobot:greendao-gradle-plugin:3.3.1需要添加到整个项目模块的配置文件build.gradle(module)中,我一直在app配置文件。在配置greendao的过程中,在网上寻找各种方法去配置都显示错误:Plugin with id 'org.greenrobot.greendao' not found.中的添加的,所以一直无法找到。_plugin with id 'org.greenrobot.greendao' not found

esxi查看许可过期_解决Vsphere Client 60天过期问题-程序员宅基地

文章浏览阅读2.7k次。步骤:使用Vcenter client登陆试图---系统管理---许可---许可证秘钥右键----管理vsphere许可证秘钥,然后一步步添加。注册机:见百度云盘VMWare:vSphere6企业版参考序列号HV4WC-01087-1ZJ48-031XP-9A843NF0F3-402E3-MZR80-083QP-3CKM24F6FX-2W197-8ZKZ9-Y31ZM-1C3LZJZ2E9-6D..._esxi7过期了怎么办

CMake_cmake_module_path-程序员宅基地

文章浏览阅读2k次。查看变量cmake --help-variable CMAKE_MODULE_PATH常用变量CMAKE_MODULE_PATH查看变量后发现该变量默认为空,需要自己定义。这个变量用来定义自己的cmake模块所在的路径。如果工程比较复杂,有可能会自己编写一些cmake模块,这些cmake模块是随工程发布的,为了让cmake在处理CMakeLists.txt时找到这些模块,你需要通过SET指令将cmake模块路径设置一下。比如SET(CMAKE_MODULE_PATH,${PROJECT_SOUR_cmake_module_path