Oh!Coder

Coding Life

Box2D C++ 教程-用户数据

| Comments

声明:本教程翻译自:Box 2D C++ turorials - User data,仅供学习参考。

  • 用户数据(User data)

在上一个话题中,我们看到在游戏实体中存储物理对象的的用处。有时候情况相反也许也很有用-在游戏中,一个物理实体指针存储游戏实体。在Box2D中这叫做user data,这是一个能够让你存储有用数据的指针。下面这些类都有这个功能:

-b2Body
-b2Fixture
-b2Joint

Box2D不会在意具体内容是什么,而且不会对内容有任何操作。Box2D只是进行存储,然后当你想调用这些数据的时候可以随时获取。上面所提到的类都有下面这两个方法:

1
2
3
//in b2Body, b2Fixture, b2Joint
void SetUserData(void* data);
void* GetUserData();

在后面的话题中,物体(bodies)和定制器(fixtures)里设置用户数据(user data)会非常有用。那么让我们实现一个简单的例子,摸索一下其中的敲门。在本次例子中我们将实现上一个话题中所做的(获取游戏实体的渲染位置和速度),但是我们不会在Ball类中存储物理实体的指针。相反,我们会为每一个物理实体存储与之对应的Ball对象的指针数据,然后在每次遍历的时候我们会为ball对象的变量更新数据信息。注意,这不是一个真正的项目任务,这只用来做演示例子。

为了能够访问物理实体的用户数据,我们需要遍历场景中的所有物理实体。从某种意义上来说,这并不是一个循环,只不过是通过物理实体的链表来实现的而已。我们可以通过b2World::GetBodyList()来获取遍历的起始元素。

那么,以上一个话题中的源代码作为起点并对其进行一些小的修改。首先,取出Ball类中m_body成员变量的所有相关引用,然后修改构造函数添加创建物理实体,并设置用户数据(user data)为Ball类自身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Ball(b2World* world, float radius) {
    m_radius = radius;
    //set up dynamic body
    b2BodyDef myBodyDef;
    myBodyDef.type = b2_dynamicBody;
    myBodyDef.position.Set(0, 20);
    b2Body* body = world->CreateBody(&myBodyDef);

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

    //add circle fixture
    b2CircleShape circleShape;
    circleShape.m_p.Set(0, 0);
    circleShape.m_radius = m_radius;
    b2FixtureDef myFixtureDef;
    myFixtureDef.shape = &circleShape;
    myFixtureDef.density = 1;
    body->CreateFixture(&myFixtureDef);
}

下一步,为Ball类添加一些成员变量,来存储一些我们需要的物理信息:

1
2
3
b2Vec2 m_position;
float m_angle;
b2Vec2 m_linearVelocity;

…就像之前那样,在调用m_body->GetPosition(),m_body->GetAngle()以及m_body->GetLinearVelocity()的地方通过简单的调用新创建的成员变量来更新小球的位置信息,例如在renderAtBodyPosition代码段中,设置位置大体看起来像这样:

1
2
glTranslatef( m_position.x, m_position.y, 0 );
glRotatef( m_angle * RADTODEG, 0, 0, 1 );

运行它,你会发现我们又退回到最近话题中所提到的状态,那就是小球在(0,0)点渲染,而且没有旋转。

pic

然后修改Step()方法,在Test::Step()调用之后渲染小球之前,我们需要从物理实体中获取数据来更新小球的位置。为了完成这个功能,我们需要遍历物理世界中的所有实体,然后获取必要的状态变量position/angle/velocity值并对Ball对象进行赋值:

1
2
3
4
5
6
7
8
9
10
11
12
b2Body* b = m_world->GetBodyList();//get start of list
while ( b != NULL ) {
    //obtain Ball pointer from user data
    Ball* ball = static_cast<Ball*>( b->GetUserData() );
    if ( ball != NULL ) {
        ball->m_position = b->GetPosition();
        ball->m_angle = b->GetAngle();
        ball->m_linearVelocity = b->GetLinearVelocity();
    }
    //continue to next body
    b = b->GetNext();
}

这样,就可以实现最开始的效果。再次提醒一下,并不建议你用这种方式,以渲染物体为目的来关联游戏中的实体,这里只是作为用户数据的演示例子。如果是以渲染为目的,之前提到的方法更容易实现并且易于管理。推动物理实体的信息更新,并不能给我们带来多大的帮助,因为毕竟我们会一直渲染,不管物理模拟的世界里发生了什么。

那么什么情况下我们会用到用户数据(user data)这个特性呢?当我们不能正确预测物理引擎正在发生什么的时候,用户数据(user data)特性会更有用,例如定制器(fixture)什么时候发生碰撞,物体碰到了什么以及反作用力将会有多大等等。其他有用的地方包括射线或者使用AABB来找到相交的定制器(以此从定制器的用户数据中找到相关的游戏实体)。

  • 设置复杂的用户数据

既然用户数据(user data)接受void类型指针,任何类型的指针都可以设置成void类型指针来存储在用户数据(user data)中,这可以是一个简单的数字,一个现有的指针(例如上面的例子)或者是一个具有复杂信息的相关物理对象的指针。这里有几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//setting and retrieving an integer
int myInt = 123;
body->SetUserData( (void*)myInt );
...later...
int udInt = (int)body->GetUserData();

//setting and retrieving a complex structure
struct bodyUserData {
    int entityType;
    float health;
    float stunnedTimeout;
};
bodyUserData* myStruct = new bodyUserData;
myStruct->health = 4;//set structure contents as necessary
body->SetUserData(myStruct);
...later...
bodyUserData* udStruct = (bodyUserData*)body->GetUserData();

在每一个项目中,你应该使用同一类型来设置用户数据(user data)。例如,如果你像第一个例子那样,为定制器的用户数据设置了整数,那么所有其他的定制器也应该设置成整数。如果你为部分定制器设置了整数,其他定制器设置了结构,那么在通过进行遍历检索的时候,通过GetUserData()方法获取的数据,你就很难确定是整数还是结构,就很难做出相应的处理(碰撞回调话题将会解释为什么你不能够每次都知道物体或者定制器发生碰撞,并及时进行处理)。

大部分应用都会为用户数据(user data)设置成包含多个成员变量的结构,Box2D不会在删除物体(body)/定制器(fixture)/连接器(joint)的时候连同用户数据也一并删除。所以你需要记得,当不再使用这些数据的时候自行手动进行删除。

上面所提到的例子中的结构都是确定的成员变量,只是给出了有限的可用属性值,如果你的游戏中有不同的实体类型,这么做看起来并不灵活。在碰撞回调话题中的‘真实场景(real scenarios)’部分会有一个例子,说明了为真实情况中经常使用的类继承方式来满足有多个实体类型的需求。

Comments