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

智能推荐

如何配置DNS服务的正反向解析_dns反向解析-程序员宅基地

文章浏览阅读3k次,点赞3次,收藏13次。root@server ~]# vim /etc/named.rfc1912.zones #添加如下内容,也可直接更改模板。[root@server ~]# vim /etc/named.conf #打开主配置文件,将如下两处地方修改为。注意:ip地址必须反向书写,这里文件名需要和反向解析数据文件名相同。新建或者拷贝一份进行修改。nslookup命令。_dns反向解析

设置PWM占空比中TIM_SetCompare1,TIM_SetCompare2,TIM_SetCompare3,TIM_SetCompare4分别对应引脚和ADC通道对应引脚-程序员宅基地

文章浏览阅读2.5w次,点赞16次,收藏103次。这个函数TIM_SetCompare1,这个函数有四个,分别是TIM_SetCompare1,TIM_SetCompare2,TIM_SetCompare3,TIM_SetCompare4。位于CH1那一行的GPIO口使用TIM_SetCompare1这个函数,位于CH2那一行的GPIO口使用TIM_SetCompare2这个函数。使用stm32f103的除了tim6和tim7没有PWM..._tim_setcompare1

多线程_进程和线程,并发与并行,线程优先级,守护线程,实现线程的四种方式,线程周期;线程同步,线程中的锁,Lock类,死锁,生产者和消费者案例-程序员宅基地

文章浏览阅读950次,点赞33次,收藏19次。多线程_进程和线程,并发与并行,线程优先级,守护线程,实现线程的四种方式,线程周期;线程同步,线程中的锁,Lock类,死锁,生产者和消费者案例

在 Linux 系统的用户目录下安装 ifort 和 MKL 库并配置_在linux系统的用户目录下安装ifort和mkl库并配置-程序员宅基地

文章浏览阅读2.9k次。ifort 编译器的安装ifort 编译器可以在 intel 官网上下载。打开https://software.intel.com/content/www/us/en/develop/tools/oneapi/components/fortran-compiler.html#gs.7iqrsm点击网页中下方处的 Download, 选择 Intel Fortran Compiler Classic and Intel Fortran Compiler(Beta) 下方对应的版本。我选择的是 l_在linux系统的用户目录下安装ifort和mkl库并配置

使用ftl文件生成图片中图片展示无样式,不显示_ftl格式pdf的样式调整-程序员宅基地

文章浏览阅读689次,点赞7次,收藏8次。些项目时需要一个生成图片的方法,我在网上找到比较方便且适合我去设置一些样式的生成方式之一就是使用Freemarker,在对应位置上先写好一个html格式的ftl文件,在对应位置用${参数名}填写上。还记得当时为了解决图片大小设置不上,搜索了好久资料,不记得是在哪看到的需要在里面使用width与height直接设置,而我当时用style去设置,怎么都不对。找不到,自己测试链接,准备将所有含有中文的图片链接复制一份,在服务器上存储一份不带中文的文件。突然发现就算无中文,有的链接也是打不开的。_ftl格式pdf的样式调整

orin Ubuntu 20.04 配置 Realsense-ROS_opt/ros/noetic/lib/nodelet/nodelet: symbol lookup -程序员宅基地

文章浏览阅读1.5k次,点赞6次,收藏12次。拉取librealsense。_opt/ros/noetic/lib/nodelet/nodelet: symbol lookup error: /home/admin07/reals

随便推点

操作系统精选习题——第四章_系统抖动现象的发生由什么引起的-程序员宅基地

文章浏览阅读3.4k次,点赞3次,收藏29次。一.单选题二.填空题三.判断题一.单选题静态链接是在( )进行的。A、编译某段程序时B、装入某段程序时C、紧凑时D、装入程序之前Pentium处理器(32位)最大可寻址的虚拟存储器地址空间为( )。A、由内存的容量而定B、4GC、2GD、1G分页系统中,主存分配的单位是( )。A、字节B、物理块C、作业D、段在段页式存储管理中,当执行一段程序时,至少访问()次内存。A、1B、2C、3D、4在分段管理中,( )。A、以段为单位分配,每._系统抖动现象的发生由什么引起的

UG NX 12零件工程图基础_ug-nx工程图-程序员宅基地

文章浏览阅读2.4k次。在实际的工作生产中,零件的加工制造一般都需要二维工程图来辅助设计。UG NX 的工程图主要是为了满足二维出图需要。在绘制工程图时,需要先确定所绘制图形要表达的内容,然后根据需要并按照视图的选择原则,绘制工程图的主视图、其他视图以及某些特殊视图,最后标注图形的尺寸、技术说明等信息,即可完成工程图的绘制。1.视图选择原则工程图合理的表达方案要综合运用各种表达方法,清晰完整地表达出零件的结构形状,并便于看图。确定工程图表达方案的一般步骤如下:口分析零件结构形状由于零件的结构形状以及加工位置或工作位置的不._ug-nx工程图

智能制造数字化工厂智慧供应链大数据解决方案(PPT)-程序员宅基地

文章浏览阅读920次,点赞29次,收藏18次。原文《智能制造数字化工厂智慧供应链大数据解决方案》PPT格式主要从智能制造数字化工厂智慧供应链大数据解决方案框架图、销量预测+S&OP大数据解决方案、计划统筹大数据解决方案、订单履约大数据解决方案、库存周转大数据解决方案、采购及供应商管理大数据模块、智慧工厂大数据解决方案、设备管理大数据解决方案、质量管理大数据解决方案、仓储物流与网络优化大数据解决方案、供应链决策分析大数据解决方案进行建设。适用于售前项目汇报、项目规划、领导汇报。

网络编程socket accept函数的理解_当在函数 'main' 中调用 'open_socket_accept'时.line: 8. con-程序员宅基地

文章浏览阅读2w次,点赞38次,收藏102次。在服务器端,socket()返回的套接字用于监听(listen)和接受(accept)客户端的连接请求。这个套接字不能用于与客户端之间发送和接收数据。 accept()接受一个客户端的连接请求,并返回一个新的套接字。所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数_当在函数 'main' 中调用 'open_socket_accept'时.line: 8. connection request fa

C#对象销毁_c# 销毁对象及其所有引用-程序员宅基地

文章浏览阅读4.3k次。对象销毁对象销毁的标准语法Close和Stop何时销毁对象销毁对象时清除字段对象销毁的标准语法Framework在销毁对象的逻辑方面遵循一套规则,这些规则并不限用于.NET Framework或C#语言;这些规则的目的是定义一套便于使用的协议。这些协议如下:一旦销毁,对象不可恢复。对象不能被再次激活,调用对象的方法或者属性抛出ObjectDisposedException异常重复地调用对象的Disposal方法会导致错误如果一个可销毁对象x 包含或包装或处理另外一个可销毁对象y,那么x的Disp_c# 销毁对象及其所有引用

笔记-中项/高项学习期间的错题笔记1_大型设备可靠性测试可否拆解为几个部分进行测试-程序员宅基地

文章浏览阅读1.1w次。这是记录,在中项、高项过程中的错题笔记;https://www.zenwu.site/post/2b6d.html1. 信息系统的规划工具在制订计划时,可以利用PERT图和甘特图;访谈时,可以应用各种调查表和调查提纲;在确定各部门、各层管理人员的需求,梳理流程时,可以采用会谈和正式会议的方法。为把企业组织结构与企业过程联系起来,说明每个过程与组织的联系,指出过程决策人,可以采用建立过程/组织(Process/Organization,P/O)矩阵的方法。例如,一个简单的P/O矩阵示例,其中._大型设备可靠性测试可否拆解为几个部分进行测试