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

智能推荐

linux里面ping www.baidu.com ping不通的问题_linux桥接ping不通baidu-程序员宅基地

文章浏览阅读3.2w次,点赞16次,收藏90次。对于这个问题我也是从网上找了很久,终于解决了这个问题。首先遇到这个问题,应该确认虚拟机能不能正常的上网,就需要ping 网关,如果能ping通说明能正常上网,不过首先要用命令route -n来查看自己的网关,如下图:第一行就是默认网关。现在用命令ping 192.168.1.1来看一下结果:然后可以看一下电脑上面百度的ip是多少可以在linux里面ping 这个IP,结果如下:..._linux桥接ping不通baidu

android 横幅弹出权限,有关 android studio notification 横幅弹出的功能没有反应-程序员宅基地

文章浏览阅读512次。小妹在这里已经卡了2-3天了,研究了很多人的文章,除了低版本api 17有成功外,其他的不是channel null 就是没反应 (channel null已解决)拜托各位大大,帮小妹一下,以下是我的程式跟 gradle, 我在这里卡好久又没有人可问(哭)![image](/img/bVcL0Qo)public class MainActivity extends AppCompatActivit..._android 权限申请弹窗 横屏

CNN中padding参数分类_cnn “相同填充”(same padding)-程序员宅基地

文章浏览阅读1.4k次,点赞4次,收藏6次。valid padding(有效填充):完全不使用填充。half/same padding(半填充/相同填充):保证输入和输出的feature map尺寸相同。full padding(全填充):在卷积操作过程中,每个像素在每个方向上被访问的次数相同。arbitrary padding(任意填充):人为设定填充。..._cnn “相同填充”(same padding)

Maven的基础知识,java技术栈-程序员宅基地

文章浏览阅读790次,点赞29次,收藏28次。手绘了下图所示的kafka知识大纲流程图(xmind文件不能上传,导出图片展现),但都可提供源文件给每位爱学习的朋友一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长![外链图片转存中…(img-Qpoc4gOu-1712656009273)][外链图片转存中…(img-bSWbNeGN-1712656009274)]

getFullYear()和getYear()有什么区别_getyear和getfullyear-程序员宅基地

文章浏览阅读469次。Date对象取得年份有getYear和getFullYear两种方法经 测试var d=new Date;alert(d.getYear())在IE中返回 2009,在Firefox中会返回109。经查询手册,getYear在Firefox下返回的是距1900年1月1日的年份,这是一个过时而不被推荐的方法。而alert(d.getFullYear())在IE和FF中都会返回2009。因此,无论何时都应使用getFullYear来替代getYear方法。例如:2016年用 getFullYea_getyear和getfullyear

Unix传奇 (上篇)_unix传奇pdf-程序员宅基地

文章浏览阅读182次。Unix传奇(上篇) 陈皓 了解过去,我们才能知其然,更知所以然。总结过去,我们才会知道我们明天该如何去规划,该如何去走。在时间的滚轮中,许许多的东西就像流星一样一闪而逝,而有些东西却能经受着时间的考验散发着经久的魅力,让人津津乐道,流传至今。要知道明天怎么去选择,怎么去做,不是盲目地跟从今天各种各样琳琅满目前沿技术,而应该是去 —— 认认真真地了解和回顾历史。 Unix是目前还在存活的操作系_unix传奇pdf

随便推点

ACwing 哈希算法入门:_ac算法 哈希-程序员宅基地

文章浏览阅读308次。哈希算法:将字符串映射为数字形式,十分巧妙,一般运用为进制数,进制据前人经验,一般为131,1331时重复率很低,由于字符串的数字和会很大,所以一般为了方便,一般定义为unsigned long long,爆掉时,即为对 2^64 取模,可以对于任意子序列的值进行映射为数字进而进行判断入门题目链接:AC代码:#include<bits/stdc++.h>using na..._ac算法 哈希

VS配置Qt和MySQL_在vs中 如何装qt5sqlmysql模块-程序员宅基地

文章浏览阅读952次,点赞13次,收藏27次。由于觉得Qt的编辑界面比较丑,所以想用vs2022的编辑器写Qt加MySQL的项目。_在vs中 如何装qt5sqlmysql模块

【渝粤题库】广东开放大学 互联网营销 形成性考核_画中画广告之所以能有较高的点击率,主要由于它具有以下特点-程序员宅基地

文章浏览阅读1k次。选择题题目:下面的哪个调研内容属于经济环境调研?()题目:()的目的就是加强与客户的沟通,它是是网络媒体也是网络营销的最重要特性。题目:4Ps策略中4P是指产品、价格、顾客和促销。题目:网络市场调研是目前最为先进的市场调研手段,没有任何的缺点或不足之处。题目:市场定位的基本参数有题目:市场需求调研可以掌握()等信息。题目:在开展企业网站建设时应做好以下哪几个工作。()题目:对企业网站首页的优化中,一定要注意下面哪几个方面的优化。()题目:()的主要作用是增进顾客关系,提供顾客服务,提升企业_画中画广告之所以能有较高的点击率,主要由于它具有以下特点

爬虫学习(1):urlopen库使用_urlopen the read operation timed out-程序员宅基地

文章浏览阅读1k次,点赞2次,收藏5次。以爬取CSDN为例子:第一步:导入请求库第二步:打开请求网址第三步:打印源码import urllib.requestresponse=urllib.request.urlopen("https://www.csdn.net/?spm=1011.2124.3001.5359")print(response.read().decode('utf-8'))结果大概就是这个样子:好的,继续,看看打印的是什么类型的:import urllib.requestresponse=urllib.r_urlopen the read operation timed out

分享读取各大主流邮箱通讯录(联系人)、MSN好友列表的的功能【升级版(3.0)】-程序员宅基地

文章浏览阅读304次。修正sina.com/sina.cn邮箱获取不到联系人,并精简修改了其他邮箱代码,以下就是升级版版本的介绍:完整版本,整合了包括读取邮箱通讯录、MSN好友列表的的功能,目前读取邮箱通讯录支持如下邮箱:gmail(Y)、hotmail(Y)、 live(Y)、tom(Y)、yahoo(Y)(有点慢)、 sina(Y)、163(Y)、126(Y)、yeah(Y)、sohu(Y) 读取后可以发送邮件(完..._通讯录 应用读取 邮件 的相关

云计算及虚拟化教程_云计算与虚拟化技术 教改-程序员宅基地

文章浏览阅读213次。云计算及虚拟化教程学习云计算、虚拟化和计算机网络的基本概念。此视频教程共2.0小时,中英双语字幕,画质清晰无水印,源码附件全课程英文名:Cloud Computing and Virtualization An Introduction百度网盘地址:https://pan.baidu.com/s/1lrak60XOGEqMOI6lXYf6TQ?pwd=ns0j课程介绍:https://www.aihorizon.cn/72云计算:概念、定义、云类型和服务部署模型。虚拟化的概念使用 Type-2 Hyperv_云计算与虚拟化技术 教改