BlaXuan 发表于 2022-10-19 10:26

游戏开发笔记(九):tile map对象转Box2D物体

前两篇文章分别讨论了tile map在游戏中的批渲染和“编辑”处理功能,这里继续讨论tile map的第三个主题:如何将tile map对象转成Box2D物体。
Tiled软件并不提供某种标记功能,标注某个图形层瓦片(tile)为某个特定物体,相反它另起一个对象层,通过独立对象的方式提供非图像信息,实现渲染与逻辑分离。从识别的角度来看,不同的对象可以用不同的层进行分类,比如砖块,可以起一个Brick对象层,金币可以起一个Coin对象层。从现实需要的角度看,对象可以创建为点(point)、矩形(rectangle)、椭圆(ellipse)和多边形(polygon)。
除了点(point)一般作为提供位置信息用,其他形状对象都可以转变为对应的b2shape:矩形(rectangle)转变为b2PolygonShape(通过SetAsBox方法设为矩形),椭圆(ellipse)转变为b2CircleShape,多边形(polygon)转变为b2PolygonShape(闭合的情况下)或b2EdgeShape(不闭合的情况下)。
幸运的是,libtmx提供了这个对应关系的识别功能,标识了4类对象(obj_type):OT_SQUARE,OT_POLYGON,OT_POLYLINE,OT_ELLIPSE,由此我们可以轻松创建自己的映射类Map2BodyBuilder。
首先我们根据libtmx提供的解析接口,把最后的对象数据分类存放了起来:
void TilemapHelper::get_tilemap_objects(const char *name, tmx_object_group *objgr) {
   
    tmx_object *head = objgr->head;
   
    while (head) {
      if (head->visible) {
            if (head->obj_type == OT_SQUARE) {
                printf("object square(%f,%f) order %d \n",head->x,head->y,objgr->draworder);
                Color clr = PINK;
                clr.a *= 0.5f;
                EnvItem item = {{head->x,head->y},{head->width, head->height},4,clr};
                strcpy(item.name, name);
                gameItems.push_back(item);
            }
            else if (head->obj_type== OT_POLYGON) {
                printf("object OT_POLYGON,len %d\n",head->content.shape->points_len);
                MultPointsItem pItem;
                int i;
                for (i=0; i<head->content.shape->points_len; i++) {
                  auto px = head->x+head->content.shape->points;
                  auto py = head->y+head->content.shape->points;
                  pItem.vertices.push_back({px,py});
                }
                polygonItems.push_back(pItem);
            }
            else if (head->obj_type == OT_POLYLINE) {
                printf("object OT_POLYLINE, points len %d \n",head->content.shape->points_len);
                MultPointsItem plItem;
                int i;
                for (i=0; i<head->content.shape->points_len; i++) {
                  auto px = head->x+head->content.shape->points;
                  auto py = head->y+head->content.shape->points;
                  plItem.vertices.push_back({px,py});
                }
                polyLineItems.push_back(plItem);
            }
            else if (head->obj_type == OT_ELLIPSE) {
                //to do
            }
      }
      head = head->next;
    }
}
映射类Map2BodyBuilder,直接根据上面的数据创建Box2D世界里的物体:
//创建单个的Rectangle物体
void Map2BodyBuilder::BuildEnvItemObject(tmx_map *map, b2World *world, EnvItem *item)
{
    glm::vec2 pos = {item->position.x,item->position.y};
    glm::vec2 size = {item->size.x,item->size.y};
    b2BodyDef bd;
    int map_y = map->height-(pos.y+size.y)/map->tile_height;
    int py = map_y * map->tile_height + size.y/2.0f;
    float base = Global::ScreenHeight - (map->height)*map->tile_height;
    bd.position.Set((pos.x+size.x/2.0f)/PPM, (py+base)/PPM);
    b2Body* body = world->CreateBody(&bd);

    b2PolygonShape shape;
    shape.SetAsBox(size.x/2.0f/PPM, size.y/2.0f/PPM);
   
    b2FixtureDef fdef;
    fdef.shape = &shape;
    body->CreateFixture(&fdef);
}

//创建所有的Polyline物体
void Map2BodyBuilder::BuildAllPolylineObjects(b2World *world)
{
    for (int i = 0; i < TilemapHelper::polyLineItems.size(); i++)
    {
      MultPointsItem *item = &TilemapHelper::polyLineItems;
      if(item->vertices.size()<3){
            b2BodyDef bd;
            b2Body* edge = world->CreateBody(&bd);
            b2EdgeShape shape;
            for(int j = 1; j < item->vertices.size();j++ ){
                auto prePosY = (Global::ScreenHeight-item->vertices.y)/PPM;
                auto currPosY = (Global::ScreenHeight-item->vertices.y)/PPM;
                auto width = item->vertices.x - item->vertices.x;
                b2Vec2 v0((item->vertices.x-width)/PPM, prePosY);
                b2Vec2 v1( item->vertices.x/PPM, prePosY);
                b2Vec2 v2( item->vertices.x/PPM, currPosY);
                b2Vec2 v3((item->vertices.x+width)/PPM, currPosY);
                shape.SetOneSided(v0, v1, v2, v3);
            }
            b2FixtureDef fdef;
            fdef.filter.categoryBits = Player::OBJECT_BIT;
            fdef.friction = 1.0f;
            fdef.density = 0.0f;
            fdef.shape = &shape;
            edge->CreateFixture(&fdef);
      }
    }
}

//创建所有的Polygon物体
void Map2BodyBuilder::BuildAllPolygonObjects(b2World *world)
{
    for (int i = 0; i < TilemapHelper::polygonItems.size(); i++)
    {
      MultPointsItem *item = &TilemapHelper::polygonItems;
      b2BodyDef bd;
      b2Body* polygon = world->CreateBody(&bd);
      b2Vec2 vertices;
      int size = (int)item->vertices.size();
      for(int j = 0; j < size;j++ ){
            vertices.Set(item->vertices.x/PPM, (Global::ScreenHeight-item->vertices.y)/PPM);
      }
      b2PolygonShape shape;
      shape.Set(vertices, size);
      b2FixtureDef fdef;
      fdef.filter.categoryBits = Player::OBJECT_BIT;
      fdef.friction = 1.0f;
      fdef.density = 0.0f;
      fdef.shape = &shape;
      polygon->CreateFixture(&fdef);
    }
}
OK,映射转换工作完成!
记录一些要注意的问题:
在Tiled软件创建对象层物体,默认情况下你可以拖拽鼠标在任意位置创建任意大小的物体,但这种“自由”我们往往不需要,毕竟手工拉线创建一个Rectangle对象与一排砖块(tile图形对象)完全对齐是很费劲(几乎办不到)的事,这是机器该干的事!解决这个问题,只需要在菜单中把 'View' ->'Snaping '-> 'Snap To grid' 勾选上。这时候你无论如何拉线,创建的对象都会与格子边缘对齐,这对于创建类马里奥的平台游戏特别好用!


第二个,关于b2EdgeShape类,经常用于类似下图的情况:


人物落在上面会被托住,人物往上跳跃不会被挡住,即所谓的collision one-sided(单边碰撞)。
b2EdgeShape提供了这样的设置函数:
void b2EdgeShape::SetOneSided        (const b2Vec2 &         v0,
                                 const b2Vec2 &         v1,
                                 const b2Vec2 &         v2,
                                 const b2Vec2 &         v3
)       
为什么需要四个位置点b2Vec2,一段线段不是两个位置点就可以了吗?实际上,v1,v2才是真正的edge vertices,v0,v3是用来辅助碰撞计算的,用来实现collision one-sided。v0,v3往往需要自己计算出来。见Map2BodyBuilder::BuildAllPolylineObjects中的代码:
auto width = item->vertices.x - item->vertices.x;
b2Vec2 v0((item->vertices.x-width)/PPM, prePosY);
b2Vec2 v1( item->vertices.x/PPM, prePosY);
b2Vec2 v2( item->vertices.x/PPM, currPosY);
b2Vec2 v3((item->vertices.x+width)/PPM, currPosY);
shape.SetOneSided(v0, v1, v2, v3);
全部代码见SuperMarioEx。
页: [1]
查看完整版本: 游戏开发笔记(九):tile map对象转Box2D物体