Oh!Coder

Coding Life

Box2D C++ 教程-跳跃问题

| Comments

声明:本文翻译自Box2D C++ tutorial-The ‘can I jump’ question,仅供学习参考。

在地上是我的特性吗?

如果曾经遇到了关于Box2D的问题,多半可能有“我如何告诉我的游戏角色当前是站在地上的”。我想这个问题如此频繁的冒出来的首要原因,是因为类似回调方法可以得到接触信息有关-回调方法可以抛出与当前物体自身不同的相关信息,或许还因为可以获知当前接触到什么,什么是需要程序员自己实现而不能用库方法编译生成的。不管是上面哪一种情况,其实我们已经在之前的话题中有所覆盖。碰撞回调话题中处理了开始和结束接触事件。传感器话题中我们展示了一个持续跟踪“当前接触”链表的例子。

那么接下来我们要讨论什么呢?呃,我在猜当很多人碰到这个问题的时候都会找到这个页面,所以我把之前讨论的两个话题放到了文章的开始位置为的是能够更好的有个了解。之前我还想过或许比较好的办法是对这些技术做一些改动然后完成一个示范,这或许也是遇到问题的朋友通常所希望的,就像是一个完整的游戏总比在之前话题中所使用的不完整的战斗场景要好(译者注:我的理解是一个完整的例子要比之前话题中随便搭建一个局部场景要好)。

我们将会看到一个非常高效的方法来解决这个问题,而且不会太复杂。玩家角色的身体由一个主要的定制器来展示,另一个传感定制器附加到下边,当脚下有东西的时候用来进行检测。脚下的传感器用来感知所接触到的世界中其它定制器,因为这样我们可以直接感知玩家脚下并且范围不会太大,在任何情况下,我们通过玩家脚下的传感器来判断当前情景下玩家是否会直接跳起。接下来我们可以实现几个小想法,这些只是一个大概的想法不会太详细。

因为之前的一些概念我们已经覆盖过了,这里只是做一些不同的设置,我们将会由浅入深的逐步来实现玩家的双脚下面是否有东西,以及玩家站立在什么东西之上。场景中将会有两种不同大小的盒子,通过玩家感知所站立的盒子的不同,来调整玩家的弹跳力。

防止在半空中跳跃

是的,正如你所猜的,我们准备使用跳跃话题中所使用的脚本(译者注:这里的脚本是场景的意思不是编程脚本)。把那里的代码重新拽过来,然后在构造方法中创建一些在场景中跳跃的物体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//add some bodies to jump around on
{
    //body definition
    b2BodyDef myBodyDef;
    myBodyDef.position.Set(-5,5);
    myBodyDef.type = b2_dynamicBody;

    //shape definition
    b2PolygonShape polygonShape;
    polygonShape.SetAsBox(1, 1); //a 2x2 rectangle

    //fixture definition
    b2FixtureDef myFixtureDef;
    myFixtureDef.shape = &polygonShape;
    myFixtureDef.density = 1;

    for (int i = 0; i < 5; i++)
    {
        b2Fixture* fixture = m_world->CreateBody(&myBodyDef)->CreateFixture(&myFixtureDef);
        fixture->SetUserData( (void*)1 );//tag square boxes as 1
    }

    //change size
    polygonShape.SetAsBox(0.5, 1); //a 1x2 rectangle

    for (int i = 0; i < 5; i++)
    {
        b2Fixture* fixture = m_world->CreateBody(&myBodyDef)->CreateFixture(&myFixtureDef);
        fixture->SetUserData( (void*)2 );//tag smaller rectangular boxes as 2
    }
}

至此我们得到了一个漂亮但普通的场景,不过重点是这里我们有两个不同类型的物体,不同的形状以及不同的user data标签。在程序中user data不会引用其它物体,只是一个简单的数字标签,然后我们可以在碰撞回调中指定是哪类物体正在发生碰撞。

pic

现在在构造方法开始的地方,我们需要对’玩家’盒子做两处变动。首先我们让其变高一点,以此更容易进行识别,然后我们为其添加脚部传感定制器,user data的标签设置成3。你可以进行局部修改,下面是玩家身体清晰的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//player body
{
    //body definition
    b2BodyDef myBodyDef;
    myBodyDef.type = b2_dynamicBody;
    myBodyDef.fixedRotation = true;

    //shape definition for main fixture
    b2PolygonShape polygonShape;
    polygonShape.SetAsBox(1, 2); //a 2x4 rectangle

    //fixture definition
    b2FixtureDef myFixtureDef;
    myFixtureDef.shape = &polygonShape;
    myFixtureDef.density = 1;

    //create dynamic body
    myBodyDef.position.Set(0, 10);
    m_body = m_world->CreateBody(&myBodyDef);

    //add main fixture
    m_body->CreateFixture(&myFixtureDef);

    //add foot sensor fixture
    polygonShape.SetAsBox(0.3, 0.3, b2Vec2(0,-2), 0);
    myFixtureDef.isSensor = true;
    b2Fixture* footSensorFixture = m_body->CreateFixture(&myFixtureDef);
    footSensorFixture->SetUserData( (void*)3 );
}

pic

注意:让脚部传感器成为一个真正的传感器并不是必须的,对于获取碰撞事件它不是必须存在的,但是这提供了一些优势。

  • 第一,既然我们知道脚步传感器在玩家下方,我们知道发生碰撞就意味着玩家站到了某个东西上。如果我们只获取玩家身体的碰撞事件,那么我们只能感知来自墙和天花板的碰撞事件,所以在我们确定是地面碰撞之前我们需要检查碰撞来自哪个方向。
  • 第二,当玩家移动的时候,特别是玩家在斜坡上移动的时候身体会产生大量的震动从而快速引发大量连续的开始/结束接触事件。即便身体已经明显离开了地面,但这仍然可能让身体发生跳跃,这一点是不是让人觉得很讨厌?在这种情况下如果使用脚部传感器就会有平滑和连续的接触检测,因为即便身体有小的反弹,脚部的传感器仍然会于地面接触。
  • 第三,添加足部传感器并于身体的定制器分开意味着在不同情况下可以为这两部分是独立分开的。比如说,组部传感器比身体更窄,这就意味着当你站在一个摇摇欲坠很窄的物体上的时候将不能跳跃,当然了,这些都可以很简单的通过改变大小,形状或者传感器的位置来进行调节。

现在我们需要知道当我们跳起来的时候,我们需要知道当前有多少定制器与脚部传感器有接触。添加一个全局变量来保存这个值:

1
2
3
4
5
//global scope
int numFootContacts;

//in constructor
numFootContacts = 0;

然后像碰撞回调话题讲到的方法一样,我们需要进行碰撞回调。不一样的是这次我们要把定制器的user data替换成物体的user data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyContactListener : public b2ContactListener
{
    void BeginContact(b2Contact* contact) {
        //check if fixture A was the foot sensor
        void* fixtureUserData = contact->GetFixtureA()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            numFootContacts++;
        //check if fixture B was the foot sensor
        fixtureUserData = contact->GetFixtureB()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            numFootContacts++;
    }

    void EndContact(b2Contact* contact) {
        //check if fixture A was the foot sensor
        void* fixtureUserData = contact->GetFixtureA()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            numFootContacts--;
        //check if fixture B was the foot sensor
        fixtureUserData = contact->GetFixtureB()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            numFootContacts--;
    }
};

不要忘记类的实例化,然后使用SetContactLisener方法告诉Box2D世界进行方法回调。

为了实际使用这些信息,我们需要在脚部传感器没有接触任何东西的时候忽略任何尝试的跳跃。在Keyboard方法中,你应该在switch模块中添加类似相应的代码:

1
if ( numFootContacts < 1 ) break;

既然我们完成了跳跃状态,让我们在屏幕上清晰的显示出来:

1
2
3
//in Step function
m_debugDraw.DrawString(5, m_textLine, "Can I jump here? %s", numFootContacts>0?"yes":"no");
m_textLine += 15;

现在运行测试,你应该能够看到当脚部传感器接触状态发生改变的时候跳跃状态也能相应的正确变化,而且当玩家离开地面的时候跳跃状态也能得到保证。Great!那么差不多和前面提到的类似仍然需要做一些调整。

跳起之后立即防止再一次跳跃

当使用ApplyForce或者ApplyLinearImpulse进行跳跃之后,如果你一直按下跳跃按键,你会发现虽然不像前面那么糟,不过玩家仍然会跳的非常高。原因很简单,虽然跳跃已经开始并且身体也在向上移动,但是身体离地面很近的时候,按键状态其实已经触发了很多次,所以在很短的时间范围内力/冲量进行了叠加。为此我们要做一个计数,以此尽可能的做频率限制,等等。如果玩家按下了跳跃按键,至少在100ms内不应该再次跳跃。

要实现这个功能通常需要一个快速的timer记录按下键盘的时间,以及按下之后当前的时间。你可以从网上找到针对不同平台的定时器,最新的Box2D也有包括。既然我们当前使用的Box2D版本,v2.1.2没有任何计时器,那我们就用帧速做为一个基本计数器。让我们每1/4秒内阻止玩家再次跳跃,也就是60Hz内有15次跳跃这足够让脚步传感器离开地面了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//class member variable
int m_jumpTimeout;

//in class constructor
m_jumpTimeout = 0;

//in Step()
m_jumpTimeout--;

//in Keyboard routine (only showing the impulse version here as an example)
case 'l':
    if ( numFootContacts < 1 ) break;
    if ( m_jumpTimeout > 0 ) break;
    //ok to jump
    m_body->ApplyLinearImpulse( b2Vec2(0, m_body->GetMass() * 10), m_body->GetWorldCenter() );
    m_jumpTimeout = 15;
    break;

根据地面类型防止跳跃

上面谈到的方法是为了防止玩家已经跳起的状态下再次发生跳跃。那么当我们站到某个确定的物体上的时候允许玩家进行跳跃应该怎么办呢?或者站到某个物体上的时候跳跃的更高,等等。在场景中,让我们试一下当玩家站在大盒子上的时候允许发生跳跃。为了实现这个功能,我们需要一个记录当前所站在物体上的一个链表,当玩家按下跳跃按键的时候对Keyboard方法进行调用。可以在BeginContact方法当中向这个链表中添加最新的定制器,然后在EndContact方法中进行移除,就像传感器话题中的雷达一样。

另外,你可以把numFootContacts变量分离成两个,以此分开记录当前站立的大盒子和小盒子-但是这种实现方式有一定的局限性,比如你又有了一个第三种类型的盒子,你还得添加一个变量来记录,诸如此类。这里为了此次展示,我们将会使用更有效的方法来记录当前脚下的盒子们,因为这个方法是一个长远的有效解决办法,而且还有一个原因是后面我还会用到脚下站立的盒子。

添加一个类的成员变量来保存当前脚下的盒子们。你可以用一个链表存储物体,但是我会用它来存储定制器,因为你要知道在真实的游戏中我们可以站在一个静态的由很多定制器组成的’地面’物体上,我们可能只会设置当中的某个并让其分离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//global variable (include <set>)
set<b2Fixture*> fixturesUnderfoot;

//revised implementation of contact listener
class MyContactListener : public b2ContactListener
{
    void BeginContact(b2Contact* contact) {
        //check if fixture A was the foot sensor
        void* fixtureUserData = contact->GetFixtureA()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            fixturesUnderfoot.insert(contact->GetFixtureB());//A is foot so B is ground
        //check if fixture B was the foot sensor
        fixtureUserData = contact->GetFixtureB()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            fixturesUnderfoot.insert(contact->GetFixtureA());//B is foot so A is ground
    }

    void EndContact(b2Contact* contact) {
        //check if fixture A was the foot sensor
        void* fixtureUserData = contact->GetFixtureA()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            fixturesUnderfoot.erase(contact->GetFixtureB());//A is foot so B is ground
        //check if fixture B was the foot sensor
        fixtureUserData = contact->GetFixtureB()->GetUserData();
        if ( (int)fixtureUserData == 3 )
            fixturesUnderfoot.erase(contact->GetFixtureA());//B is foot so A is ground
    }
};

注意:只有接触监听器部分做了改动,对counter的操作,我们把insert/erase替换成了incrementing/decrementing。set容器可以方便的保存相同的物体添加两次,而且擦出的话也很方便。

现在在Keyboard方法内部,我们不只是检测脚下站立物体的数量,还要检测它们是什么类型。当站立的物体只是一个小的盒子的时候,我们想要防止跳跃。这也就意味着我们要检测fixtureUnderfoot链表,如果其中有定制器的标签是1(大盒子)或者NULL)(静态的’地面’定制器),那么跳跃应该被允许。由于这个可跳跃检测方法有点长,我们又想在多个地方分别调用,为了保证其自身的整洁,我将把这段逻辑单独放到一个方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//class function
bool CanJumpNow()
{
    if ( m_jumpTimeout > 0 )
        return false;
    set<b2Fixture*>::iterator it = fixturesUnderfoot.begin();
    set<b2Fixture*>::iterator end = fixturesUnderfoot.end();
    while (it != end)
    {
        b2Fixture* fixture = *it;
        int userDataTag = (int)fixture->GetUserData();
        if ( userDataTag == 0 || userDataTag == 1 ) //large box or static ground
            return true;
        ++it;
    }
    return false;
}

使用set容器有一个不方便的地方就是,你需要遍历整个容器…

使用这个方法替换上面检测numFootContacts的方法,然后准备好运行。运行程序,然后测试一下,只有在大盒子或一般静态地面上才能够跳起。

在环境中添加一些反应

那么上面我们涵盖了一些在特定环境下,防治跳跃的基本办法。做为最后一个小的展示,当玩家跳起来之后,给所站立的盒子一个向下的作用。这里就体现出了使用链表比只使用一个整型数来记录盒子的优势。

当玩家跳起来之后,在键盘方法中你可以像下面这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//in Keyboard
//kick player body upwards
b2Vec2 jumpImpulse(0, m_body->GetMass() * 10);
m_body->ApplyLinearImpulse( jumpImpulse, m_body->GetWorldCenter() );
m_jumpTimeout = 15;

//kick the underfoot boxes downwards with an equal and opposite impulse
b2Vec2 locationOfImpulseInPlayerBodyCoords(0, -2);//middle of the foot sensor
b2Vec2 locationOfImpulseInWorldCoords = m_body->GetWorldPoint(locationOfImpulseInPlayerBodyCoords);
set<b2Fixture*>::iterator it = fixturesUnderfoot.begin();
set<b2Fixture*>::iterator end = fixturesUnderfoot.end();
while (it != end)
{
    b2Fixture* fixture = *it;
    fixture->GetBody()->ApplyLinearImpulse( -jumpImpulse, locationOfImpulseInWorldCoords );
    ++it;
}

现在,当然你从大盒子上跳起的时候,可以看下盒子的反应。当然了,这个场景并不能非常好的说明这一点,如果大盒子在静态的地面上就看不到这个效果-试着跳到一个大盒子下面有其它盒子的上面,然后再次跳起,你会看到大盒子会有一点点变化。

Comments