C++游戏开发之旅 10

问题概述:

上一章,我们实现了将Tiled中的背景图层渲染到了屏幕上,为关卡铺设了远景。现在,我们要开始构建玩家能够站立、行走、跳跃的实体世界。这章我们继续深入LevelLoader,实现对瓦片层(Tiled Layer)的解析和加载。瓦片层是Tiled地图的核心,是由一个网格单元组成的二维数组,每个单元格可以放置一个来自图块集(Tileset)的瓦片。这比简单的图片层要复杂。

1.Tiled瓦片系统核心概念

首先我们需要知道Tiled是如何进行组织瓦片数据的。

image-20260201190421230

  • 地图大小:height × width 91 × 29

  • 瓦片大小:tileheight × tilewidth 16 × 16

  • 地图像素大小:height × tileheight × width × tilewidth = 91 × 16 × 29 × 16

  • Data数组:指定每个地图区域放入什么图块

    • 长度为地图大小
    • 数值为瓦片索引(0代表无瓦片)
  • 瓦片索引(gid):全局唯一,计算方式 gid = firstgid + 图集内id

核心术语

  • 图块集(Tileset) - 包含多个瓦片的资源集合,可以是一张包含所有瓦片的大地图,也可以是多个独立图片的集合。Tiled会将图块集的数据保存在一个单独的.tsj文件中。

  • 全局ID(GID) - Tiled会给一个地图中所有图块集里的每一个瓦片分配一个从1开始、独一无二的ID,这就是GID。

  • firstgid - 在地图文件.tmj中,每个引用的图块集中都有一个firstgid属性。它表示这个图块集的第一个瓦片在整个地图中的GID是多少。

  • 局部ID - 一个瓦片在所属图块集中的内部ID

  • 换算关系 - LID = GID - firstgid,通过公式,我们可以从地图文件数据中读到一个GID时,就可以判断出它属于哪一个图块集,并且知道是该图块集中的第几个瓦片。

Tiled地图解析思路

解析地图文件流程:

1.加载图块集
  • 遍历地图文件中的tilesets数组
  • 根据source字段找到对应的.tsj文件并解析
  • 将其数据与firstgid一起缓存起来
2.加载图层
  • 遍历layers数组
  • 如果是tilelayer:
    • 遍历该层的data数组,里面存放了每个网格单元的GID
    • 对于每一个GID,使用缓存的图块集数据来查找它对应的图片、源矩形和自定义属性
    • 将这些信息组装成一个TileInfo对象
    • 所有TileInfo对象被存入一个std::vector
    • 最后,创建一个包含这个vector的TileLayerComponent游戏对象

2.TileLayerComponent:瓦片世界的容器

我们通过新建一个组件来持有和渲染整个瓦片层。

tilelayer_component.h

#pragma once
#include "component.h"
#include "../render/sprite.h"
#include <vector>
#include <glm/vec2.hpp>

namespace engine::core {
class Context;
}


namespace engine::component {

enum class TileType{
    EMPTY,
    NORMAL,
    SOLID,
    // ...
};

struct TileInfo {
    render::Sprite sprite;  // 精灵视图
    TileType type;          // 瓦片类型
    TileInfo(render::Sprite sprite = render::Sprite(), TileType type = TileType::EMPTY) : sprite(std::move(sprite)), type(type) {}
};

/**
 * @brief 瓦片层组件
 * 用于存储瓦片信息,并可以渲染瓦片层
 */
class TileLayerComponent final : public Component {
    friend class engine::object::GameObject;
private:
    glm::ivec2 map_size_;   // 地图尺寸(瓦片数)
    glm::ivec2 tile_size_;  // 单个瓦片尺寸
    std::vector<TileInfo> tiles_;   // 存储所有瓦片信息 以行主序存储
    glm::vec2 offset_ = {0.0f,0.0f}; // 瓦片层一般不会旋转或是缩放,不引入Transform,一般也不会有偏移 保持0就行
    bool is_hidden_ = false;

public:
    TileLayerComponent() = default;

    /**
     * 构造函数
     * @param map_size 地图尺寸(瓦片数)
     * @param tile_size 单个瓦片尺寸
     * @param tiles 瓦片信息
     */
    TileLayerComponent(glm::ivec2 map_size, glm::ivec2 tile_size, std::vector<TileInfo>&& tiles);

    /**
     * @brief 获取瓦片信息
     * @param pos 瓦片坐标(0 <= x < map_size_.x, 0 <= y < map_size_.y)
     * @return const TileInfo* 指向瓦片信息的指针,如果坐标超出范围则返回nullptr
     */
    const TileInfo* getTileInfoAt(glm::ivec2 pos) const;

    /**
     * @brief 获取瓦片类型
     * @param pos 瓦片坐标(0 <= x < map_size.x, 0 <= y < map_size.y)
     * @return TileType 瓦片类型,如果坐标超出范围则返回TileType::EMPTY
     */
    TileType getTileTypeAt(glm::ivec2 pos) const;

    /**
     * @brief 根据世界坐标获取瓦片类型
     * @param world_pos 世界坐标
     * @return TileType 瓦片类型,如果坐标超出范围则返回TileType::EMPTY
     */
    TileType getTileTypeAtWorldPos(const glm::vec2& world_pos) const;

    // getter and setter
    const glm::ivec2& getTileSize() const { return tile_size_; }
    const glm::ivec2& getMapSize() const { return map_size_; }
    glm::vec2 getWorldSize() const { return glm::vec2(map_size_.x * tile_size_.x, map_size_.y * tile_size_.y); }
    
    bool isHidden() const { return is_hidden_; }
    void setHidden(bool hidden) { is_hidden_ = hidden; }

protected:
    void init() override;
    void update(float, engine::core::Context&) override{}
    void render(engine::core::Context&) override;

};

}
核心设计说明
  • TileType --- 表示瓦片的类型,这个在碰撞的时候需要知道,比如SOLID,表示地表、墙的瓦片类型。

  • TileInfo --- 结构体,包含单个瓦片的所有信息

    • 表示视图渲染的Sprite
    • 用于逻辑判断的TileType
  • tiles_ --- 使用一个std::vector数组来以行主序的方式存放瓦片层的所有瓦片信息TileInfo

  • render() --- 遍历tiles_,计算每个瓦片在世界中的位置,调用Renderer::drawSprite绘制

tilelayer_component.cpp

#include "tilelayer_component.h"
#include "../core/context.h"
#include "../render/renderer.h"
#include <spdlog/spdlog.h>

namespace engine::component {
    TileLayerComponent::TileLayerComponent(glm::ivec2 map_size, glm::ivec2 tile_size, std::vector<TileInfo> &&tiles)
    : map_size_(map_size), tile_size_(tile_size), tiles_(std::move(tiles)) 
    {
        if(tiles_.size() != static_cast<size_t>(map_size_.x * map_size_.y)){
            spdlog::error("瓦片层组件瓦片数量不匹配");
            tiles_.clear();
            map_size_ = {0,0};
        }
        spdlog::trace("瓦片层组件创建");
    }
    
    const TileInfo *TileLayerComponent::getTileInfoAt(glm::ivec2 pos) const
    {
        if(pos.x < 0 || pos.x >= map_size_.x || pos.y < 0 || pos.y >= map_size_.y){
            spdlog::error("超出地图大小索引");
            return nullptr;
        }
        return &tiles_[static_cast<size_t>(pos.y * map_size_.x + pos.x)];
    }

    TileType TileLayerComponent::getTileTypeAt(glm::ivec2 pos) const
    {
        if (const auto *tile_info = getTileInfoAt(pos)) {
            return tile_info->type;
        }
        return TileType::EMPTY;
    }

    TileType TileLayerComponent::getTileTypeAtWorldPos(const glm::vec2 &world_pos) const
    {
        glm::vec2 relative_position = world_pos - offset_;

        int tile_x = static_cast<int>(std::floor(relative_position.x / tile_size_.x));
        int tile_y = static_cast<int>(std::floor(relative_position.y / tile_size_.y));

        return getTileTypeAt(glm::ivec2(tile_x, tile_y));
    }

    void TileLayerComponent::init()
    {
        if(!owner_) {
            spdlog::error("瓦片层组件没有拥有者");
            return;
        }
        spdlog::trace("瓦片层组件初始化完成");
    }
    
    void TileLayerComponent::render(engine::core::Context& context)
    {
        if(tile_size_.x <= 0 || tile_size_.y <= 0) {
            return;
        }

        for (int y = 0; y < map_size_.y; ++y) {
            for (int x = 0; x < map_size_.x; ++x) {
                int index = static_cast<size_t>(y * map_size_.x + x);
                if (tiles_[index].type != TileType::EMPTY) {
                    const auto &tile_info = tiles_[index];
                    // 计算左上角的位置
                    glm::vec2 position = {
                        offset_.x + static_cast<float>(x) * tile_size_.x,
                        offset_.y + static_cast<float>(y) * tile_size_.y
                    };
                    // 如果遇到瓦片大小不一致的情况,则调整位置
                    if(tile_info.sprite.getSourceRect()->h != tile_size_.y){
                        position.y -= (tile_info.sprite.getSourceRect()->h - static_cast<float>(tile_size_.y));
                    }
                    context.getRenderer().drawSprite(context.getCamera(), tile_info.sprite, position);
                }
            }
        }
    }

}

3.LevelLoader添加载入瓦片层的函数

接下来需要为LevelLoader添加加载图块集和瓦片层的能力。

engine/scene/level_loader.h

#pragma once
#include <string>
#include <nlohmann/json.hpp>
#include <glm/vec2.hpp>
#include <map>

namespace engine::component{
struct TileInfo;
}

namespace engine::scene {
class Scene;

class LevelLoader final {
private:
    std::string map_path_; // 关卡地图路径
    glm::ivec2 map_size_; // 关卡地图大小 瓦片填充个数
    glm::ivec2 tile_size_; // 关卡地图中每个瓦片的大小
    std::map<int,nlohmann::json> tileset_data_; // 关卡地图中的瓦片数据,需要将瓦片数据集给先缓存下来
    // 对应的是firstgid --- 瓦片数据

public:
    LevelLoader() = default;

    /**
     * @brief 加载关卡数据到指定的 Scene 对象对
     * @param map_path 关卡文件路径
     * @param scene 需要加载关卡数据的 Scene 对象
     * @return 加载成功返回 true,否则返回 false
     */
    bool loadLevel(const std::string& map_path, Scene& scene);

private:
    void loadImageLayer(const nlohmann::json& json, Scene& scene);    
    void loadTileLayer(const nlohmann::json& json, Scene& scene);
    void loadObjectLayer(const nlohmann::json& json, Scene& scene);

    // 获取瓦片数据集中的属性
    component::TileInfo getTileInfoByGid(int gid);

    // 加载瓦片数据集
    void loadTilesets(const std::string& tileset_path, int firstgid);

    /**
     * 解析路径,将地图路径和相对路径合并
     * 地图路径:"assets/maps/level1.tmj"
     * 相对路径:"../textures/Layers/back.png"
     * 结果路径:"assets/maps/../textures/Layers/back.png
     *  maps/../ 抵消了
     */
    // 显然这部分获得绝对路径是有点问题,这只是对于地图文件了,后面比如瓦片数据集文件获取就需要处理,这里进行修改以适配该情况
    std::string resolvePath(const std::string& image_path);
    std::string resolvePath(const std::string& relative_path, const std::string& file_path);

};
}

关键更新说明

新增成员变量
  • **map_size_ , tile_size_ ** --- 存储地图的全局信息
  • **std::map<int, nlohmann::json> tileset_data_ ** --- 缓存所有图块集的数据,以键值对的形式{firstgid, json对象},
新增方法
  • getTileInfoByGid() --- 根据GID获取瓦片信息
  • loadTileset() --- 加载图块集数据,将图块集数据放到std::map<int, nlohmann::json> tileset_data_
  • loadTileLayer() --- 加载瓦片层

engine/scene/level_loader.cpp

#include "level_loader.h"
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <fstream>
#include <filesystem>
#include "scene.h"
#include "../core/context.h"
#include "../resource/resource_manager.h"
#include "../object/game_object.h"
#include "../component/transform_component.h"
#include "../component/parallax_component.h"
#include "../component/tilelayer_component.h"


namespace engine::scene {
    bool LevelLoader::loadLevel(const std::string &map_path, Scene &scene)
    {
        map_path_ = map_path;
        // 1.加载地图文件
        std::ifstream file(map_path_);
        if (!file.is_open()) {
            spdlog::error("打开地图文件失败: {}", map_path_);
            return false;
        }

        // 2.解析地图文件
        nlohmann::json json_data;
        try{
            file >> json_data;
        } catch (const nlohmann::json::parse_error &e) {
            spdlog::error("解析地图文件失败: {}", e.what());
            return false;
        }

        // 3.获取地图的基本信息
        map_size_ = glm::vec2(json_data.value("width", 0),json_data.value("height", 0));
        tile_size_ = glm::vec2(json_data.value("tilewidth", 0),json_data.value("tileheight", 0));

        // 4.加载图块集
        if(json_data.contains("tilesets") && json_data["tilesets"].is_array()){
            for (const auto& tileset_json : json_data["tilesets"]) {
                if(tileset_json.contains("source") && tileset_json.contains("firstgid") &&
                tileset_json["source"].is_string() && tileset_json["firstgid"].is_number_integer()){
                    auto source = tileset_json["source"].get<std::string>();
                    auto firstgid = tileset_json["firstgid"]; // 不使用.get<int>() 也可以,因为支持隐式转换
                    auto tileset_path = resolvePath(source, map_path_); // 得到瓦片集的绝对路径
                    loadTilesets(tileset_path, firstgid);
                }
            }
        }

        // 5.加载图层
        if(!json_data.contains("layers") || !json_data["layers"].is_array() ){
            spdlog::error("地图文件缺少图层信息或者没有数组layers, {}", map_path_);
            return false;
        }
        for (const auto& layer_json : json_data["layers"]) {
            std::string layer_type = layer_json.value("type", "none");
            if(!layer_json.value("visible",true)){
                spdlog::debug("图层{}不可见,跳过", layer_json.value("name", "none"));
                continue;
            }
            if(layer_type == "imagelayer"){
                loadImageLayer(layer_json, scene);
            } else if (layer_type == "tilelayer") {
                loadTileLayer(layer_json, scene);
            } else if (layer_type == "objectgroup") {
                loadObjectLayer(layer_json, scene);
            } else {
                spdlog::warn("未知的图层类型: {}", layer_type);
            }
        }
        spdlog::info("加载地图文件成功: {}", map_path_);
        return true;
    }
    
    void LevelLoader::loadImageLayer(const nlohmann::json &layer_json, Scene &scene)
    {   // 获取相对路径,这里会自动处理\/
        const auto& image_path = layer_json.value("image", "");
        if (image_path.empty()) {
            spdlog::warn("图层{}缺少图片路径", layer_json.value("name", "none"));
            return;
        }
        /* 我们知道,resourceManager中的getTexture是通过绝对路径来获取图片的,我们需要将相对路径
        转换为绝对路径以此去适配它 */
        const std::string& texuture_id = resolvePath(image_path);
        if (texuture_id.empty()) {
            spdlog::warn("图片路径{}解析失败", image_path);
            return;
        }
        // 获取图片层的名字
        auto name = layer_json.value("name", "none");
        // 获取图片层的偏移量
        glm::vec2 offset = glm::vec2(layer_json.value("offsetx", 0), layer_json.value("offsety", 0));
        // 获取视差因子
        glm::vec2 scroll_factor = glm::vec2(layer_json.value("parallaxx", 1.0f), layer_json.value("parallaxy", 1.0f));
        // 是否平铺
        glm::bvec2 repeat = glm::bvec2(layer_json.value("repeatx", false), layer_json.value("repeaty", false));

        // 创建一个游戏对象 注意顺序,最先添加的组件是Transform
        auto obj = std::make_unique<object::GameObject>(name);
        obj->addComponent<component::TransformComponent>(offset);
        obj->addComponent<component::ParallaxComponent>(texuture_id,scroll_factor,repeat);
        scene.addGameObject(std::move(obj));
        spdlog::info("加载图片层成功: {}", name);
    }
    
    void LevelLoader::loadTileLayer(const nlohmann::json &json, Scene &scene)
    {
        if(!json.contains("data") || !json["data"].is_array()){
            spdlog::error("图层{}数据异常", json.value("name","none"));
            return;
        }

        // TileInfo Vector (瓦片数量 = 地图宽度 * 地图高度)
        std::vector<engine::component::TileInfo> tiles;
        tiles.reserve(map_size_.x * map_size_.y);

        // 获取data
        const auto& data = json["data"];

        for (const auto& gid : data) {
            tiles.push_back(getTileInfoByGid(gid));
        }

        // 添加游戏对象
        auto name = json.value("name", "none");
        auto obj = std::make_unique<object::GameObject>(name);
        obj->addComponent<component::TileLayerComponent>(map_size_, tile_size_, std::move(tiles));
        scene.addGameObject(std::move(obj));
        spdlog::info("加载瓦片层成功: {}", name);

    }
    
    void LevelLoader::loadObjectLayer(const nlohmann::json &json, Scene &scene)
    {
    }

    component::TileInfo LevelLoader::getTileInfoByGid(int gid)
    {
        if(gid == 0){
            return component::TileInfo();
        }
        // 需要找比gid小的最大的firstgid
        auto it = tileset_data_.upper_bound(gid);
        if(it == tileset_data_.begin()){
            spdlog::error("找不到对应的图块集");
            return component::TileInfo();
        }
        --it; // 退一步
        const auto& tileset_json = it->second; // 获取图块集json对象
        auto localgid = gid - it->first; // 获取相对gid

        // 这里需要考虑两种情况,一种是图块集是单个图片,一种是图块集是多个图片拼接的
        if(tileset_json.contains("image")){ // 单个图片,image属性在外边
            //获取图片路径
            const auto& textureid = resolvePath(tileset_json["image"].get<std::string>(), tileset_json["file_path"].get<std::string>());
            // 可以计算出图片网格中的坐标
            auto coordinate_x = localgid % tileset_json["columns"].get<int>();
            auto coordinate_y = localgid / tileset_json["columns"].get<int>();
            // 确定源矩形
            SDL_FRect texture_rect = {
                static_cast<float>(coordinate_x * tile_size_.x),
                static_cast<float>(coordinate_y * tile_size_.y),
                static_cast<float>(tile_size_.x),
                static_cast<float>(tile_size_.y)
            };
            engine::render::Sprite sprite(textureid, texture_rect);
            return component::TileInfo(sprite,engine::component::TileType::NORMAL);
        } else { // 多图片情况 image路径在tiles里
            for(auto& tile_data : tileset_json["tiles"]){
                if(tile_data["id"] == localgid){
                    // 获取图片路径
                    const auto& textureid = resolvePath(tile_data["image"].get<std::string>(), tileset_json["file_path"].get<std::string>());
                    // 多图片获取的是整张图片 但你还是得确定源矩形
                    auto image_width = tile_data.value("imagewidth", 0);
                    auto image_height = tile_data.value("imageheight", 0);
                    SDL_FRect texture_rect = {
                        static_cast<float>(tile_data.value("x", 0)),
                        static_cast<float>(tile_data.value("y", 0)),
                        static_cast<float>(tile_data.value("width", image_width)),
                        static_cast<float>(tile_data.value("height", image_height))
                    };
                    engine::render::Sprite sprite(textureid, texture_rect);
                    return component::TileInfo(sprite,engine::component::TileType::NORMAL);
                }
            }
        }

        return component::TileInfo();
    }

    void LevelLoader::loadTilesets(const std::string &tileset_path, int firstgid)
    {
        std::ifstream file(tileset_path);
        if (!file.is_open()) {
            spdlog::error("打开图块集文件失败: {}", tileset_path);
            return;
        }

        // 1.解析图块集文件
        nlohmann::json json_data;
        try {
            file >> json_data;
        } catch (const nlohmann::json::parse_error &e) {
            spdlog::error("解析图块集文件失败: {}", e.what());
            return;
        }
        // 2.获取图块集的基本信息
        // 将路径放进去,为了后面解析图片路径需要,此时这个json对象中有一个路径,也就是文件所在的绝对路径
        json_data["file_path"] = tileset_path;
        tileset_data_[firstgid] = std::move(json_data);
        spdlog::info("图块集{}加载成功, firstgid:{}", tileset_path, firstgid);

    }

    std::string LevelLoader::resolvePath(const std::string &image_path)
    {
        try {
            // 这里可以获得地图map_path_的父路径assets/maps/level1.tmj ---> assets/maps/
            auto map_dir = std::filesystem::path(map_path_).parent_path();
            // 合并路径,获得绝对路径
            auto abs_path = map_dir / image_path;
            // 规范化处理
            abs_path = std::filesystem::canonical(abs_path);
            return abs_path.string();
        } catch (const std::filesystem::filesystem_error &e) {
            spdlog::error("解析路径失败: {}", e.what());
            return "";
        } 
    }

    std::string LevelLoader::resolvePath(const std::string &relative_path, const std::string &file_path)
    {
        try {
            // 获取文件所在的父路径
            auto file_dir = std::filesystem::path(file_path).parent_path();
            // 合并路径,获得绝对路径、
            auto abs_path = file_dir / relative_path;
            // 规范化处理
            abs_path = std::filesystem::canonical(abs_path);
            return abs_path.string();
        } catch (const std::filesystem::filesystem_error &e) {
            spdlog::error("解析路径失败: {}", e.what());
            return "";
        }
    }
}

4.渲染细节

我们发现游戏在渲染瓦片时,非常容易在瓦片之间出现细微的裂缝,这是因为GPU默认使用线性插值(SDL_SCALEMODE_LINEAR)来缩放纹理,导致瓦片边缘的像素与临近的透明像素“混合”,从而产生半透明的边缘。

解决方案:最近邻插值

我们需要告诉SDL使用使用最近邻插值(SDL_SCALEMODE_NEAREST,这种模式会选择最接近的像素颜色,保持像素画的锐利边缘。

更新engine/resource/texture_manager.cpp
SDL_Texture *TextureManager::loadTexture(const std::string &file_path)
{
    // 首先检查是否已经加载过
    auto it = textures_.find(file_path);
    if(it != textures_.end()){
        return it->second.get();
    }
    // 没有加载过,则加载
    spdlog::debug("加载纹理:{}", file_path);
    SDL_Texture* raw_texture = IMG_LoadTexture(sdl_renderer_, file_path.c_str());
    if(!raw_texture){
        spdlog::error("TextureManager 加载纹理失败->文件路径 {} 异常,错误信息 {}", file_path,SDL_GetError());
        return nullptr;
    }

    // 设置纹理缩放模式为最近邻
    if(!SDL_SetTextureScaleMode(raw_texture, SDL_SCALEMODE_NEAREST)){
        spdlog::warn("无法设置纹理缩放模式为最近邻模式");
    }

    // 将纹理放入缓存
    textures_.emplace(file_path, std::unique_ptr<SDL_Texture, SDLTextureDeleter>(raw_texture));
    spdlog::debug("TextureManager 加载纹理成功->文件路径 {}", file_path);
    return raw_texture;
}

5.最终效果

GameScene的代码几乎不需要改变,其会自动处理新增的瓦片层逻辑。

运行结果

image-20260207155618931

image-20260207155729742

将图片缩放模式设置为最近邻后效果好了很多!

本章总结

  • 完成了TileLayerComponent组件的设计,我们有一个TileInfo,有一个sprite和tiletype的成员变量,组件中有一个vector数组,存放TileInfo,实现对于瓦片层的渲染

  • 了解Tiled瓦片系统的核心概念(GID、Tileset、LID等等)

  • 升级了LevelLoader以支持图块集和瓦片层加载,修改了resolvePath()函数

  • 解决了像素画瓦片渲染的缝隙问题

遇到问题

1.在加载图块集数据的时候,没有将文件的路径放到tile_data_map容器的json对象当中。

在加载图块集数据的时候,我们是从地图文件中去找的,有一个sourcefirstgid,这个source图块集文件相对地图文件的路径,因此,我们通过resolvePath()函数获得绝对路径,这个得到的路径就算图块集的路径,有了这个路径后,就可以往tileset_data_中存储数据了,当然我们还需要将这个路径存放到json对象中。因为后面我们会从图块集数据中读取图片路径,这个resolvePath()函数需要一个相对路径(图块集中图片所在的相对路径),一个文件路径(图块集当前文件的路径,也就是存的路径)。

2.nlohmann::json的不完整定义问题

问题出现在level_loader.h,起因是我在头文件中是这样引入的#include <nlohmann/json_fwd.hpp>,然后我的LevelLoader中有一个成员变量std::map<int,nlohmann::json> tileset_data_,这是存储瓦片集数据的,对应于一个 firstgid 和 json 对象数据,然而,我的构造函数LevelLoader()=default写在了头文件里,所以在构造函数初始化的时候肯定会报错,因为std::map初始化的时候需要知道完整的nlohmann::json的定义,但是你并没有引入#include <nlohmann/json.hpp>的完整定义,所以报错了,这是个小细节问题。

3.程序运行遇到了Cannot access value of empty optional的问题,这是为什么?

因为在level_loader.cpp中的getTileInfoByGid函数处理多图片情况我是这样写的:

else { // 多图片情况 image路径在tiles里
    for(auto& tile_data : tileset_json["tiles"]){
        if(tile_data["id"] == localgid){
            // 获取图片路径
            const auto& textureid = resolvePath(tile_data["image"].get<std::string>(), tileset_json["file_path"].get<std::string>());
            // 多图片获取的是整张图片
            engine::render::Sprite sprite(textureid);
            return component::TileInfo(sprite,engine::component::TileType::NORMAL);
        }
    }
}

可以看到我的Sprite类在进行初始化时,源矩形没有传参数,默认是std::nullopt,这会造成TileLayerComponent.cpp中的render函数出现异常判断,可以看到下面的代码

if (tiles_[index].type != TileType::EMPTY) {
    const auto &tile_info = tiles_[index];
    // 计算左上角的位置
    glm::vec2 position = {
        offset_.x + static_cast<float>(x) * tile_size_.x,
        offset_.y + static_cast<float>(y) * tile_size_.y
    };
    // 如果遇到瓦片大小不一致的情况,则调整位置
    if(tile_info.sprite.getSourceRect()->h > tile_size_.y){
        position.y -= (tile_info.sprite.getSourceRect()->h - static_cast<float>(tile_size_.y));
    }
    context.getRenderer().drawSprite(context.getCamera(), tile_info.sprite, position);
}

因为可能出现瓦片图大于网格瓦片的情况,我们需要对精灵图进行调整,也就是瓦片信息的精灵源矩形大于网格瓦片大小,tile_info.sprite.getSourceRect()->h > tile_size_.y,这个时候 y 方向要减去瓦片网格size的高,但是基于前面的代码,你获取的时候这个源矩形就为std::nullopt了,就报错了。

4.渲染render函数错位了

image-20260207110732391

if(tile_info.sprite.getSourceRect()->h > tile_size_.y){ // > 得改成 !=
position.y -= (tile_info.sprite.getSourceRect()->h - static_cast<float>(tile_size_.y));
}
context.getRenderer().drawSprite(context.getCamera(), tile_info.sprite, position);

我太fw了啊,只是考虑了瓦片图片大于网格瓦片的大小,而如果小于网格瓦片也会造成错位,比如精灵的源矩形是15×10,那么希望其在16×16的瓦片网格中y方向的位置应该是6,也就是一个网格中的下部分。

posted @ 2026-02-11 12:12  wenyiGamecpp  阅读(71)  评论(0)    收藏  举报