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

智能推荐

使用UmcFramework和unimrcpclient.xml连接多个SIP设置的配置指南及C代码示例

在多媒体通信领域,MRCP(Media Resource Control Protocol)协议被广泛用于控制语音识别和合成等媒体资源。UniMRCP是一个开源的MRCP实现,提供了客户端和服务端的库。UmcFramework是一个基于UniMRCP客户端库的示例应用程序框架,它帮助开发者快速集成和测试MRCP客户端功能。本文将详细介绍如何使用UmcFramework和unimrcpclient.xml配置文件连接到多个SIP设置,以及如何用C代码进行示例说明。

java.net.ProtocolException: Server redirected too many times (20)-程序员宅基地

文章浏览阅读3k次。报错:java.net.ProtocolException: Server redirected too many times (20)1.没有检查到cookie,一直循环重定向。解决:CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));URL url = new URL(url); ..._java.net.protocolexception: server redirected too many times (20)

springboot启动报错 Failed to scan *****/derbyLocale_ja_JP.jar from classloader hierarchy_failed to scan from classloader hierarchy-程序员宅基地

文章浏览阅读4.1k次。问题这是部分报错信息2019-07-11 14:03:34.283 WARN [restartedMain][DirectJDKLog.java:175] - Failed to scan [file:/D:/repo/org/apache/derby/derby/10.14.2.0/derbyLocale_ja_JP.jar] from classloader hierarchyjava...._failed to scan from classloader hierarchy

MATLAB-ones函数_matlab中ones函数-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏7次。在MATLAB中,ones函数用于创建一个指定大小的由1组成的矩阵或数组。_matlab中ones函数

解决PS等软件出现应用程序无法正常启动(0xc000007b)_photoshop应用程序无法正常启动0xc000007b。请单击“确认”关闭应用程序。-程序员宅基地

文章浏览阅读3.9w次,点赞2次,收藏9次。  在使用电脑办公过程中,安装应用程序时难免遇到无法安装或者无法正常启动的问题,这对我们使用电脑带来了诸多不便。那遇到应用程序无法正常启动的问题要如何解决呢?相信大家肯定都是十分疑问的,每次都是只能忍痛重新安装软件。今天,小编就和大家探讨下应用程序无法正常启动的解决方法,帮助大家排忧解难。0xc000007b电脑图解1  第一种方案:SFC检查系统完整性来尝试修复丢失文件  1、打开电脑搜索输入cmd.exe,选择以管理员身份运行,跳出提示框时选择继续。0xc000007b电脑图解2_photoshop应用程序无法正常启动0xc000007b。请单击“确认”关闭应用程序。

oracle介质恢复和实例恢复的异同-程序员宅基地

文章浏览阅读396次。1、概念 REDO LOG是Oracle为确保已经提交的事务不会丢失而建立的一个机制。实际上REDO LOG的存在是为两种场景准备的:实例恢复(INSTANCE RECOVERY);介质恢复(MEDIA RECOVERY)。 实例恢复的目的是在数据库发生故障时,确保BUFFER CACHE中的数据不会丢失,不会造成数据库的..._oracle 实例恢复和介质恢复

随便推点

轻松搭建CAS 5.x系列(5)-增加密码找回和密码修改功能-程序员宅基地

文章浏览阅读418次。概述说明CAS内置了密码找回和密码修改的功能; 密码找回功能是,系统会吧密码重置的连接通过邮件或短信方式发送给用户,用户点击链接后就可以重置密码,cas还支持预留密码重置的问题,只有回答对了,才可以重置密码;系统可配置密码重置后,是否自动登录; 密码修改功能是,用户登录后输入新密码即可完成密码修改。安装步骤`1. 首先,搭建好cas sso server您需要按..._修改cas默认用户密码

springcloud(七) feign + Hystrix 整合 、-程序员宅基地

文章浏览阅读141次。之前几章演示的熔断,降级 都是 RestTemplate + Ribbon 和RestTemplate + Hystrix ,但是在实际开发并不是这样,实际开发中都是 Feign 远程接口调用。Feign + Hystrix 演示:  eruka(略)order 服务工程:  pom.xml<?xml version="1.0" encoding="U..._this is order 服务工程

YOLOv7如何提高目标检测的速度和精度,基于优化算法提高目标检测速度-程序员宅基地

文章浏览阅读3.4k次,点赞35次,收藏43次。学习率是影响目标检测精度和速度的重要因素之一。合适的学习率调度策略可以加速模型的收敛和提高模型的精度。在YOLOv7算法中,可以使用基于余弦函数的学习率调度策略(Cosine Annealing Learning Rate Schedule)来调整学习率。

linux中进程退出函数:exit()和_exit()的区别_linux结束进程可以用哪些函数,它们之间有何区别?-程序员宅基地

文章浏览阅读4k次,点赞4次,收藏9次。 linux中进程退出函数:exit()和_exit()的区别(1)_exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。(2)调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin, stdout, stderr ...). exit函数是在_exit..._linux结束进程可以用哪些函数,它们之间有何区别?

sqlserver55555_sqlserver把小数点后面多余的0去掉-程序员宅基地

文章浏览阅读134次。select 5000/10000.0 --想变成0.5select 5500/10000.0 --想变成0.55select 5550/10000.0 --想变成0.555select 5555/10000.0 --想变成0.5555其结果分别为:0.5000000 0.5500000 0.5550000 0.5555000一、如果想去掉数字5后面多余的0 ,需要转化一下:selec..._sql server 去小数 0

Angular6 和 RXJS6 的一些改动_angular6,requestoptions改成了什么-程序员宅基地

文章浏览阅读3.1k次。例一:import { Injectable } from '@angular/core';import { Observable } from 'rxjs';import { User } from "./model/User";import { map } from 'rxjs/operators';import { Http, Response, Headers, RequestOp..._angular6,requestoptions改成了什么

推荐文章

热门文章

相关标签