C++游戏开发之旅 11

问题背景

现在我们已经成功加载了静态世界的图片层和瓦片层。现在,是时候将 动态生命注入关卡了。这章我们将完成LevelLoader的最后一部分功能:解析并加载 对象层(Object Layer)

对象层的作用

对象层在Tiled中用于放置独立的、非网格对齐的实体,例如:

对象类型 说明 示例
玩家出生点 定义玩家开始位置 PlayerSpawn
敌人 各种敌对生物 Enemy,Boss
可收集物品 道具和奖励 Coin,Health,PowerUp
触发区域 不可见的交互区域 Checkpoint,Dialog Trigger
装饰对象 非瓦片的装饰物 Sign,Crate,Plant

对象层 vs 瓦片层

特性 瓦片层 对象层
对齐方式 严格网格对齐 自由位置
数据结构 二维数组 对象列表
用途 地形、平台 实体、触发器
GameObject 一个GameObject(整个图层) 每个对象一个GameObject

1.Tiled对象层解析思路

对象层在JSON中的结构与前两者不同,包含一个objects数组,每个元素都是一个独立的对象。

JSON结构示例

{
    // ...
    "layers": [
        // ...
        {
            "draworder": "topdown",
            "id": 5,
            "name": "Objects",
            "objects": [
                {
                    "gid": 149,
                    "height": 29,
                    "id": 1,
                    "name": "player_spawn",
                    "rotation": 0,
                    "type": "PlayerSpawn",
                    "visible": true,
                    "width": 29,
                    "x": 48,
                    "y": 288
                },
                // ... more objects ...
            ],
            "opacity": 1,
            "type": "objectgroup",
            // ...
        }
    ]
}

解析流程

Tiled 地图解析 ---> 加载图块集、解析图片层和瓦片层 ---> 载入对象层(type:“objectgroup”)

---> 每个对象都是一个GameObject

相关说明

  • gid --- 通过gid查找源图片信息(和瓦片层一样)
  • 位置和尺寸 --- 通过x,y,width,height,rotation确定TransformComponent的信息
  • 自定义形状 --- 如果没有gid,则代表是策划在编辑器中直接绘制的形状(如矩形、圆形),这些通常用于碰撞盒、触发器等

2.坐标系转换

在解析对象层时,有一个小细节,Tiled对象层的坐标系与我们引擎的坐标系不同!

坐标系差异

Tiled --- 对象的 y 坐标指向其 左下角

引擎 --- TransformComponentposition 指向其 左上角

image-20260207173758825


坐标转换

因此,在读取Tiled的y坐标后,需要进行转换:

engine_y = tile_y - obj_height

e.g

比如上面的小狐狸对象,其目前在Tiled中位置是(0,32),但是在引擎中的位置应该是(0,0)。

Tiled的y坐标在底部,我们需要向上移动height的距离才可以得到左上角的顶部坐标。

3.升级LevelLoader以支持对象层的载入

实现loadObjectLayer方法。

engine/scene/level_loader.cpp

// 补充
void LevelLoader::loadObjectLayer(const nlohmann::json &json, Scene &scene)
{
    if(!json.contains("objects") || !json["objects"].is_array()){
        spdlog::error("json对象{}结构异常,缺少object属性", json.value("name","none"));
        return;
    }

    const auto& objects = json["objects"];

    for(const auto& object:objects){
        auto gid = object.value("gid", 0);
        if(gid == 0){
            // 表示自定义形状
            // todo
        } else {
            // gid 存在,代表一个带图像的对象
            auto tile_info = getTileInfoByGid(gid);
            if(tile_info.sprite.getTextureId().empty()){
                spdlog::error("gid为{}的瓦片没有图像纹理",gid);
                continue;
            }
            // 获取Transform相关信息
            // 获取位置,Tiled里面是左下角的位置
            auto position = glm::vec2(object.value("x", 0.0f), object.value("y", 0.0f));
            // 获取渲染的区域
            auto dst_size = glm::vec2(object.value("width", 0.0f), object.value("height", 0.0f));
            // 转化为引擎中的位置就是左上角的位置
            position = glm::vec2(position.x, position.y - dst_size.y);
            auto rotation = object.value("rotation", 0.0f); // 旋转角度

            // TransformComponent除了position和rotation,还需要一个scale
            // 我们可以通过渲染区域和源矩形的宽高比来计算scale
            auto src_size_opt = tile_info.sprite.getSourceRect();
            if(!src_size_opt.has_value()){
                spdlog::error("gid为{}的瓦片没有图像纹理",gid);
                continue;
            }
            glm::vec2 src_size = glm::vec2(src_size_opt->w, src_size_opt->h);
            glm::vec2 scale = dst_size / src_size; // 得到缩放倍数

            const std::string& obj_name = object.value("name", "none");
            // 创建游戏对象
            auto obj = std::make_unique<object::GameObject>(obj_name);
            // 添加组件
            obj->addComponent<component::TransformComponent>(position, scale, rotation);
            obj->addComponent<component::SpriteComponent>(std::move(tile_info.sprite), scene.getContext().getResourceManager());

            scene.addGameObject(std::move(obj));
            spdlog::info("加载对象 {} 完成", obj_name);
        }

    }

}

执行流程

  • 遍历objects数组中的每一个object,处理每个独立对象,一个object对应一个GameObject
  • 通过gid来判断是否要调用getTileInfoByGid,因为有些object没有gid,就表示不带图像的点或是区域
  • 获取TransformComponent的相关属性,从object的 JSON 数据中读取x,y,width,height,rotation
  • 坐标转换,Tiled中图块位置从左下角开始计算的,而引擎中的 sdl 渲染是从左上角开始的,需要通过position.y = position.y - dst_size.y转换
  • 计算缩放scale,TransformComponent组件还有一个scale大小属性,我们从json对象中获得的dst_size是渲染的大小,而从图块集中可以得到源矩形的大小,那么scale = dst_size / src_size
  • 创建对象并添加组件,然后加到场景中。

对象层中对象实际渲染大小和源矩形大小对比如下:

image-20260207201040293

image-20260207201113139

关键说明

1.不同的gid情况
if(gid == 0){
    // 自定义形状
    // 用于碰撞盒、触发器
    
} else{
    // 带图像的对象
    // 从图块集中获取精灵信息
} 
    
2.缩放计算

dst_size: Tiled 中设置的对象尺寸(可调整的)

src_size: 原始瓦片图片的尺寸

scale = dst_size / src_size : 计算缩放比例

3.高效传递 Sprite
game_object -> addComponent<SpriteComponent>(
	std::move(tile_info.sprite), // 右值引用
    scene.getContext().getResourceManager()
);

4.SpriteComponent的优化

为了能高效地将LevelLoader中创建的Sprite对象传递给SpriteComponent,我们添加了一个新的构造函数,接收一个右值引用Sprite

// sprite_component.h中新增
SpriteComponent(
    engine::render::Sprite&& sprite,
    engine::resource::ResourceManager& resource_manager,
    engine::utils::Alignment alignment = engine::utils::Alignment::NONE
);

// sprite_component.cpp
SpriteComponent::SpriteComponent(engine::render::Sprite&& sprite, engine::resource::ResourceManager &resource_manager, engine::utils::Alignment alignment)
:resource_manager_(&resource_manager),sprite_(std::move(sprite)),alignment_(alignment)
{
    if(!resource_manager_){
        spdlog::critical("创建SpriteComponent时,resource_manager_为空!此组件无效!");
    }
    spdlog::trace("创建SpriteComponent,texture_id:{}",sprite_.getTextureId());
}
  • 移动语义 --- 使用std::move转移 Sprite 所有权,避免深拷贝
  • 性能优化 --- 大量对象加载时性能提升明显

5.最终效果

image-20260207213319984

我们的LevelLoader已经是一个 功能完备的模块 了,可以加载图片层、瓦片层、对象层。

6.系统架构总结

完整数据驱动流程:

Tiled 导出 JSON 格式的关卡文件(level1.tmj)---> LevelLoader 解析, 图片层(ParallaxComponent)、瓦片层(TileLayerComponent)、对象层(多个GameObject(Transform + Sprite))---> 添加到游戏场景中。

posted @ 2026-02-12 18:32  wenyiGamecpp  阅读(56)  评论(0)    收藏  举报