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 坐标指向其 左下角
引擎 --- TransformComponent的position 指向其 左上角

坐标转换
因此,在读取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 - 创建对象并添加组件,然后加到场景中。
对象层中对象实际渲染大小和源矩形大小对比如下:


关键说明
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.最终效果

我们的LevelLoader已经是一个 功能完备的模块 了,可以加载图片层、瓦片层、对象层。
6.系统架构总结
完整数据驱动流程:
Tiled 导出 JSON 格式的关卡文件(level1.tmj)---> LevelLoader 解析, 图片层(ParallaxComponent)、瓦片层(TileLayerComponent)、对象层(多个GameObject(Transform + Sprite))---> 添加到游戏场景中。

浙公网安备 33010602011771号