C++游戏开发之旅 14

问题概述:

我们现在已经完成了让物理引擎可以检测到物体间的重叠,现在,是时候让这些碰撞产生实际效果了,这章我们将实现碰撞解析(Collision Resolution),首先解决最核心的需求:让动态游戏对象能够与静态的瓦片地图层(如地面、墙壁)发生真实的物理交互,而不是直接穿过。

章节目标:

  • 定义实体瓦片 --- 在Tiled中标记哪些瓦片是可碰撞的
  • 读取瓦片属性 --- 让引擎理解瓦片solid属性
  • 瓦片碰撞检测 --- 高效检测动态对象与瓦片的碰撞
  • 轴分离解析 --- 分别处理X和Y轴的碰撞
  • 速度控制 --- 通过物理引擎统一管理运动

1.在Tiled中定义"实体"

首先,我们的物理引擎,需要知道哪些瓦片是“实体”(Solid),哪些只是背景。Tiled的自定义属性(Custom Properties)功能就是为此而生的。

流程:

  • 打开图块集 --- 在Tiled中,打开你的图块集(.tsj文件)
  • 选择瓦片 --- 选择一个你希望作为“实体”的瓦片
  • 添加属性 --- 在左侧的“自定义属性”面板中,右键添加新属性
  • 设置属性 --- 将属性类型设置bool,名称设置为solid,并勾选其值(设置为true
  • 保存图块集

image-20260212194416992

JSON结构

保存后,这个信息会被记录在JSON文件中,类似于:

"tiles":[
        {
         "id":26,
         "properties":[
                {
                 "name":"solid",
                 "type":"bool",
                 "value":true
                }]
        }, 
        {
         "id":28,
         "properties":[
                {
                 "name":"solid",
                 "type":"bool",
                 "value":true
                }]
        }, 
    //...

这样就对特定的瓦片进行了标记。

2.读取瓦片类型(LevelLoader)

现在,我们需要让LevelLoader能够读取并理解这个solid属性

解析思路:

Tiled地图解析思路 ---> 通过data数组中的gid查找所需信息 ---> 获取图块集中的"tiles"数组 ---> 查找对应tile的"properties"数组 ---> 读取"solid"属性值 ---> 设置TileInfo的TileType

瓦片类型枚举

首先定义瓦片类型:

// engine/component/tilelayer_component.h
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) {}
};

}

engine/scene/level_loader.h

// 这里需要完成的任务就是区分出特殊类型的瓦片
namespace engine::component {
struct TileInfo;
enum class TileType;// 前向声明
}

class LevelLoader final {
// ...
private:
    //
    /**
     * @brief 单个tile的json数据
     * @note 通过properties获取瓦片类型
     */
    component::TileType getTileType(const nlohmann::json& tile_json);
    /**
     * @brief 传入的是整个瓦片集json数据,单个的瓦片数据有一个id(local_id),一个properties
     */
    component::TileType getTileTypeById(const nlohmann::json& tileset_json, int local_id);
    // 获取瓦片数据集中的属性
    component::TileInfo getTileInfoByGid(int gid);
}

engine/scene/level_loader.cpp

// ...
component::TileType LevelLoader::getTileType(const nlohmann::json &tile_json)
{
    if(tile_json.contains("properties") && tile_json["properties"].is_array()){
        for(const auto& property : tile_json["properties"]){
            if(property.contains("name") && property["name"] == "solid"){
                bool is_solid = property.value("value", false);
                return is_solid ? component::TileType::SOLID : component::TileType::NORMAL;
            }
        }
    }
    return component::TileType::NORMAL;
}

component::TileType LevelLoader::getTileTypeById(const nlohmann::json &tileset_json, int local_id)
{
    if(tileset_json.contains("tiles")){
        const auto tiles_json = tileset_json["tiles"];
        for(auto& tile_json : tiles_json){
            if(tile_json.contains("id") && tile_json["id"] == local_id){
                return getTileType(tile_json);
            }
        }
    }
    return component::TileType::NORMAL;
}

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);
        auto tile_type = getTileTypeById(tileset_json, localgid);
        return component::TileInfo(sprite, tile_type);
    } 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);
                auto tile_type = getTileType(tile_data);
                return component::TileInfo(sprite, tile_type);
            }
        }
    }

    return component::TileInfo();
}

函数说明

  • getTileType() --- 从单个tile的 JSON 中解析properties数组,提取solid属性
  • getTileTypeById() --- 根据 local_id 在图块集的tiles数组中查找对应的tile,然后调用getTileType()
  • getTileInfoByGid() --- 更新后在创建TileInfo时能正确设置TileType

添加了两个工具辅助函数来解析properties数组,并更新getTileInfoByGid,使其在创建TileInfo时能正确设置TileType

3.物理引擎与瓦片层交互

PhysicsEngine需要知道哪些TileLayerComponent是需要参与碰撞检测的,需要为此添加注册和注销机制。

engine/physics/physics_engine.h

namespace engine::component {
    class PhysicsComponent;
    class TileLayerComponent; // 前向声明
}

class PhysicsEngine {
private:
    std::vector<engine::component::TileLayerComponent*> collision_tileLayer_; //新增
public:
    // 添加注册和注销瓦片层 / 和之前添加PhyscisComponent一样
    void registerCollisionTileLayer(engine::component::TileLayerComponent* tileLayer); // 注册碰撞瓦片图层
    void unregisterCollisionTileLayer(engine::component::TileLayerComponent* tileLayer); // 注销碰撞瓦片图层 
private:
    // 需要一个处理瓦片碰撞解析的函数 游戏对象和瓦片的碰撞
    void resolveTileCollision(engine::component::PhysicsComponent* pc, float delta_time);
}

如何进行注册?

我们可以在场景初始化的时候进行

// src/game/scene/game_scene.cpp 注册图层
void GameScene::init()
{
    // 加载场景
    engine::scene::LevelLoader loader;
    loader.loadLevel("assets/maps/level1.tmj", *this);

    // 添加测试对象
    createTestObject();

    auto* tile_obj = findGameObjectByName("main");
    if(tile_obj){
        auto* tilelayer = tile_obj->getComponent<engine::component::TileLayerComponent>();
        context_.getPhysicsEngine().registerCollisionTileLayer(tilelayer);
    }
    // 完成后,调用父类的init
    Scene::init();
}

注册流程:

场景初始化 ---> 查询需要注册的地图图层 GameObject ---> 获取 TileLayerComponent ---> 注册到PhysicsEngine ---> PhysicsEngine 维护 collision_tileLayer_ 列表

还有需要在清除的时候进行反注册,和前面存储PhysicsComponent一样

// TileLayerComponent 类中 新增
physics::PhysicsEngine* physics_engine_ = nullptr;  // 缓存物理引擎指针
void clean() override;
void TileLayerComponent::clean()
{
    if(physics_engine_){
        physics_engine_->unregisterCollisionTileLayer(this);
    }
}

// 调用注册时就set一下
// PhysicsEngine类中
void PhysicsEngine::registerCollisionTileLayer(engine::component::TileLayerComponent *tileLayer)
{
    if(tileLayer) {
        tileLayer->setPhysicsEngine(this); // 设置物理指针
        collision_tileLayer_.push_back(tileLayer);
    }
    spdlog::trace("碰撞层注册成功");
}

4.轴分离碰撞解析算法

这是章节的重点。当一个物体将要移动到一个新位置时,我们如何判断其是否会撞上瓦片,如何阻止?

我们将使用一种经典高效的方式,轴分离碰撞解析(Axis-Separated Collision Resolution)

核心思想

将物体的移动分解为 水平(X轴)垂直(Y轴)两个独立的部分:

  • 先处理 X 轴碰撞(x位置是obj_new_pos.x,y位置还是原位置的obj_pos.y),校正其位置
  • 然后处理Y轴碰撞(y位置是obj_new_pos.y,x位置还是原位置的obj_pos.x),校正位置

以上顺序可以调换,甚至可以并行;我们可以看下示意图:

image-20260215110552940

简单讲一下,可以看到,物体斜着向右下移动,跑到了瓦片层中,我们会进行位移的轴分离,也就是两个颜色较深的红框(x轴分离)和绿框(y轴分离),然后进行碰撞检测,分别检测两个点,如果碰撞,就会将其移动到网格的边界处,最终的期望位置(引擎中是左上角)就是分离后的(期望x,期望y)。

检测点说明

移动方向 检测点 原因
向下位移 左下角+右下角 检测脚下的瓦片
向上移动 左上角+右上角 检测头顶的瓦片
向右移动 右上角+右下角 检测右侧的瓦片
向左移动 左上角+左下角 检测左侧的瓦片
瓦片坐标计算

世界坐标 ---> 瓦片坐标

tile_x = floor(world_x / tile_size_x)

tile_y = floor(world_y / tile_size_y)

示例:

物体位置 = (135,250)

瓦片尺寸 = (32,32)

瓦片坐标 = (floor(135/32),floor(250/32)) = (4,7)

engine/physics/physics_engine.cpp

#include "physics_engine.h"
#include "spdlog/spdlog.h"
#include <glm/common.hpp> // for glm::clamp
#include "../component/physics_component.h"
#include "../component/transform_component.h"
#include "../component/collider_component.h"
#include "../component/tilelayer_component.h"
#include "../object/game_object.h"
#include "collision.h"

namespace engine::physics {

    void PhysicsEngine::registerComponent(engine::component::PhysicsComponent *component)
    {
        if(component) {
            components_.push_back(component);
        }
        spdlog::trace("物理组件注册成功");
    }

    void PhysicsEngine::unregisterComponent(engine::component::PhysicsComponent *component)
    {
        // erase-remove 惯用法移除
        auto it = std::remove(components_.begin(), components_.end(), component);
        components_.erase(it, components_.end());
        spdlog::trace("物理组件注销成功");
    }

    void PhysicsEngine::registerCollisionTileLayer(engine::component::TileLayerComponent *tileLayer)
    {
        if(tileLayer) {
            tileLayer->setPhysicsEngine(this); // 设置物理指针
            collision_tileLayer_.push_back(tileLayer);
        }
        spdlog::trace("碰撞层注册成功");
    }

    void PhysicsEngine::unregisterCollisionTileLayer(engine::component::TileLayerComponent *tileLayer)
    {
        // erase-remove 惯用法移除
        auto it = std::remove(collision_tileLayer_.begin(), collision_tileLayer_.end(), tileLayer);
        collision_tileLayer_.erase(it, collision_tileLayer_.end());
        spdlog::trace("碰撞层注销成功");
    }

    void PhysicsEngine::update(float delta_time)
    {
        // 先清空collision_pairs_
        collision_pairs_.clear();

        // 更新所有物理组件位置
        for (auto* pc : components_) {
            if(!pc || !pc->isEnabled()) {
                continue;
            }

            // 应用重力 F = g * m
            if(pc->isUseGravity()) {
                pc->addForce(gravity_ * pc->getMass());
            }

            // 更新速度
            pc->velocity_ += (pc->getForce() / pc->getMass()) * delta_time;
            pc->clearForce(); // 清除当前帧的力
            // auto* tc = pc->getTransform();
            // if(tc) {
            //     tc->translate(pc->velocity_ * delta_time);
            // }
            
            resolveTileCollision(pc, delta_time);
        }

        // 处理对象间的碰撞
        checkObjectCollision();

    }

    void PhysicsEngine::resolveTileCollision(engine::component::PhysicsComponent *pc, float delta_time)
    {
        auto obj = pc->getOwner();
        if(!obj) return;
        auto* tc = obj->getComponent<engine::component::TransformComponent>();
        auto* cc = obj->getComponent<engine::component::ColliderComponent>();
        // 如果碰撞器组件是触发器说明其只会产生碰撞检测但是不会发生碰撞物理效果
        if(!tc || !cc || !cc->isActive() || cc->isTrigger()) return; 
        // 检查碰撞盒是否有效
        auto world_aabb = cc->getWorldAABB();
        auto obj_pos = world_aabb.position;
        auto obj_size = world_aabb.size;
        if(obj_size.x <= 0.0f || obj_size.y <= 0.0f) return;
        // 前置检查完成

        float tolerance = 0.001f; // 容差,防止检测偏移
        auto ds = pc->velocity_ * delta_time; // 计算在当前帧内移动的距离
        auto new_obj_pos = obj_pos + ds; // 计算新的位置

        for(auto* layer : collision_tileLayer_){
            if(!layer) continue;
            auto tile_size = layer->getTileSize();
            // 轴分离碰撞检测,先检测x轴(y轴使用原位置obj_pos.y)
            if(ds.x > 0.0f){
                // 向右移动,检测右上和右下角两个点
                auto right_top_x = new_obj_pos.x + obj_size.x;
                auto tile_x = static_cast<int>(floor(right_top_x / tile_size.x));
                // y方向检测2个点位
                auto tile_y = static_cast<int>(floor(obj_pos.y / tile_size.y));
                auto tile_top_type = layer->getTileTypeAt({tile_x, tile_y}); // 右上角瓦片类型
                auto tile_bottom_y = static_cast<int>(floor((obj_pos.y + obj_size.y - tolerance) / tile_size.y));
                auto tile_bottom_type = layer->getTileTypeAt({tile_x, tile_bottom_y}); // 右下角瓦片类型
                if(tile_top_type == engine::component::TileType::SOLID || tile_bottom_type == engine::component::TileType::SOLID)
                {   // x方向也就是右侧两个点检测到碰撞,需要调整位置
                    new_obj_pos.x = tile_x * tile_size.x - obj_size.x; // 期望位置需要调整
                    pc->velocity_.x = 0.0f;
                }
            } else if(ds.x < 0.0f){
                // 向左移动,检测左上和左下角两个点
                auto left_top_x = new_obj_pos.x;
                auto tile_x = static_cast<int>(floor(left_top_x / tile_size.x));
                // y方向检测2个点位
                auto tile_y = static_cast<int>(floor(obj_pos.y / tile_size.y)); // 左上角瓦片类型
                auto tile_top_type = layer->getTileTypeAt({tile_x, tile_y});
                auto tile_bottom_y = static_cast<int>(floor((obj_pos.y + obj_size.y - tolerance) / tile_size.y));
                auto tile_bottom_type = layer->getTileTypeAt({tile_x, tile_bottom_y}); // 左下角瓦片类型
                if(tile_top_type == engine::component::TileType::SOLID || tile_bottom_type == engine::component::TileType::SOLID)
                {   // x方向也就是左侧两个点检测到碰撞,需要调整位置
                    new_obj_pos.x = (tile_x + 1) * tile_size.x; // 期望位置需要调整
                    pc->velocity_.x = 0.0f;
                }
            }

            // y轴方向检测
            if(ds.y > 0.0f){
                // 向下移动,检测左下和右下角两个点
                auto left_bottom_y = new_obj_pos.y + obj_size.y;
                auto tile_y = static_cast<int>(floor(left_bottom_y / tile_size.y));
                // x方向检测2个点位
                auto tile_x = static_cast<int>(floor(obj_pos.x / tile_size.x));
                auto tile_left_type = layer->getTileTypeAt({tile_x, tile_y}); // 左下角瓦片类型
                auto tile_right_x = static_cast<int>(floor((obj_pos.x + obj_size.x - tolerance) / tile_size.x));
                auto tile_right_type = layer->getTileTypeAt({tile_right_x, tile_y}); // 右下角瓦片类型
                if(tile_left_type == engine::component::TileType::SOLID || tile_right_type == engine::component::TileType::SOLID)
                {   // y方向也就是下侧两个点检测到碰撞,需要调整位置
                    new_obj_pos.y = tile_y * tile_size.y - obj_size.y; // 期望位置需要调整
                    pc->velocity_.y = 0.0f;
                }
            } else if(ds.y < 0.0f){
                // 向上移动,检测左上和右上角两个点
                auto left_top_y = new_obj_pos.y;
                auto tile_y = static_cast<int>(floor(left_top_y / tile_size.y));
                // x方向检测2个点位
                auto tile_x = static_cast<int>(floor(obj_pos.x / tile_size.x));
                auto tile_left_type = layer->getTileTypeAt({tile_x, tile_y}); // 左上角瓦片类型
                auto tile_right_x = static_cast<int>(floor((obj_pos.x + obj_size.x - tolerance) / tile_size.x));
                auto tile_right_type = layer->getTileTypeAt({tile_right_x, tile_y}); // 右上角瓦片类型
                if(tile_left_type == engine::component::TileType::SOLID || tile_right_type == engine::component::TileType::SOLID)
                {   // y方向也就是上侧两个点检测到碰撞,需要调整位置
                    new_obj_pos.y = (tile_y + 1) * tile_size.y; // 期望位置需要调整
                    pc->velocity_.y = 0.0f;
                }
            }
        }
        // 更新位置,限制速度
        tc->setPosition(new_obj_pos);
        pc->velocity_ = glm::clamp(pc->velocity_, -max_speed_, max_speed_);
    }

    void PhysicsEngine::checkObjectCollision()
    {
        // 遍历所有包含物理组件的GameObject
        // 从获取PhysicsComponent开始 ---> 然后得到GameObject ---> 判断ColliderComponent
        for(size_t i = 0; i < components_.size(); ++i) {
            auto* pc_a = components_[i];
            if(!pc_a || !pc_a->isEnabled()) continue;
            auto obj_a = pc_a->getOwner();
            if(!obj_a) continue;
            auto* cc_a = obj_a->getComponent<engine::component::ColliderComponent>();
            if(!cc_a || !cc_a->isActive()) continue; // 碰撞器组件存在且激活

            for(size_t j = i + 1; j < components_.size(); ++j) {
                auto* pc_b = components_[j];
                if(!pc_b || !pc_b->isEnabled()) continue;
                auto obj_b = pc_b->getOwner();
                if(!obj_b) continue;
                auto* cc_b = obj_b->getComponent<engine::component::ColliderComponent>();
                if(!cc_b || !cc_b->isActive()) continue; // 碰撞器组件存在且激活

                // 检查碰撞
                if(collision::checkCollision(*cc_a, *cc_b)) {
                    // 记录碰撞对
                    collision_pairs_.emplace_back(std::make_pair(obj_a, obj_b));
                }
            }
        }
    }

}

算法检测步骤说明:

  • 计算物体的当前位置与期望位置(如果无碰撞障碍)

  • 分离轴向的移动和碰撞

    • X轴:x坐标为期望位置;y坐标为当前位置,检测x方向碰撞并调整x坐标、速度
    • Y轴:y坐标为期望位置;x坐标为当前位置,检测y方向碰撞并调整y坐标、速度
  • 确定物体最终位置

  • 计算角点坐标,获取坐标对应图块属性

  • PhysicsEngine中注册带碰撞的瓦片层

代码步骤:

先是一些保护性的检测确定 ---> 计算潜在的位置移动 obj_new_pos = obj_pos + pc->velocity_ * delta_time ---> 分别进行x、y轴的分离检测,更新x、y轴方向的速度和位置 ---> 应用最终位置

细节说明:
1.Tolerance(容差)
float tolerance = 0.001f; // 容差,防止检测偏移
auto tile_bottom_y = static_cast<int>(floor((obj_pos.y + obj_size.y - tolerance) / tile_size.y));

防止浮点数精确问题导致物体卡在两个瓦片之间

2.位置校正公式
// 向下碰撞
new_obj_pos.y = tile_y * tile_size.y - obj_size.y;
// 向右碰撞
new_obj_pos.x = tile_x * tile_size.x - obj_size.x; 

tile_y * tile_size.y:瓦片顶部世界坐标;需要减去物体尺寸,得到物体左上角在的位置

3.速度清零
pc->velocity_.y = 0; //落地时清零垂直速度
pc->velocity_.x = 0; //撞墙时清零水平速度

5.切换到速度控制

最后,让物理引擎 接管运动,改变测试逻辑,不修改Transform位置,进而修改PhysicsComponent的速度。

void GameScene::testObj()
{
    auto obj = findGameObjectByName("test1");
    if(context_.getInputManager().isActionDown("move_left")) {
        obj->getComponent<engine::component::PhysicsComponent>()->velocity_.x = -100.0f;
    } else { // 模拟摩擦
        obj->getComponent<engine::component::PhysicsComponent>()->velocity_.x *= 0.9f;
    }
    if(context_.getInputManager().isActionDown("move_right")) {
        obj->getComponent<engine::component::PhysicsComponent>()->velocity_.x = 100.0f;
    } else { // 模拟摩擦
        obj->getComponent<engine::component::PhysicsComponent>()->velocity_.x *= 0.9f;
    }
    if(context_.getInputManager().isActionPressed("jump")) {
        obj->getComponent<engine::component::PhysicsComponent>()->velocity_.y = -400.0f;
    }

}

控制方式对比

方式 旧方式 新方式
移动 直接修改Transform.position 设置Physics.velocity
跳跃 设置速度 设置速度
停止 摩檫力模拟(velocity *= 0.9f)
优势 简单 物理引擎统一管理、自动处理碰撞、更真实的物理效果

编译运行

image-20260215113849380

箱子可以停止,并且更真实

章节总结

完整碰撞解析流程

Tiled 编辑器 设置solid属性 --- JSON文件 LevelLoader读取 --- TileLayerComponent 存储TileInfo(包含TileType) --- 注册到PhysicsEngine --- 每帧物理更新 --- 速度计算潜在位移处理碰撞(轴分离) --- 校正位置、速度清零 --- 更新 Transform.position

算法优势

  • 轴分离 --- 分别处理 X和Y轴,清晰的碰撞方向,自然的滑墙效果
  • 高效检测 --- 只检测相关的几个瓦片,O(1)而非O(n),适合大地图
  • 精确定位 --- 直接计算瓦片坐标,无需遍历所有瓦片
  • 容差检测处理 --- 避免浮点数误差,更稳定的碰撞

性能分析

对象层碰撞:O(n2) --- 适用少量对象

瓦片层碰撞:O(1) --- 每个物体只检测周围瓦片

地图规模:1000+ 瓦片

检测次数:约4个瓦片/物体

性能:快

遇到的问题

1.我在测试的时候发现一个奇怪的现象,就是刚开始的时候,物体直接飞走了。

因为物体直接消失了,所以我就定位到了物理引擎的update函数也就是在PhysicsEngine::update(float delta_time),我通过打印delta_time发现这个值很大,不是正常帧数的间隔时间(正常是0.0167s,而呈现的是0.96s左右),远远超过,为什么会这样?后面定位到Time类模块去了,发现到了问题。

void PhysicsEngine::update(float delta_time)
{
    // 先清空collision_pairs_
    collision_pairs_.clear();

    // 更新所有物理组件位置
    for (auto* pc : components_) {
        if(!pc || !pc->isEnabled()) {
            continue;
        }

        // 应用重力 F = g * m
        if(pc->isUseGravity()) {
            pc->addForce(gravity_ * pc->getMass());
        }

        // 更新速度
        pc->velocity_ += (pc->getForce() / pc->getMass()) * delta_time;
        pc->clearForce(); // 清除当前帧的力
        // auto* tc = pc->getTransform();
        // if(tc) {
        //     tc->translate(pc->velocity_ * delta_time);
        // }

        resolveTileCollision(pc, delta_time);
    }

    // 处理对象间的碰撞
    checkObjectCollision();

}

看到Time.cpp

void Time::update()
{
    frame_start_time_ = SDL_GetTicksNS();
    double current_delta_time = static_cast<double>(frame_start_time_ - last_time_) / 1.0e9;
    if(target_frame_time_ > 0.0){ // 若设置目标帧率,则限制帧率;否则delta_time_ = current_delta_time;
        limitFrameRate(current_delta_time);
    }else {
        delta_time_ = current_delta_time;
    }
    last_time_ = SDL_GetTicksNS();  // 记录离开 update 时的时间戳
}

void Time::limitFrameRate(float current_delta_time)
{
    if(current_delta_time < target_frame_time_){
        double time_to_wait = static_cast<double>(target_frame_time_ - current_delta_time);
        Uint64 time_to_wait_ns = static_cast<Uint64>(time_to_wait * 1.0e9);
        SDL_DelayNS(time_to_wait_ns);
        delta_time_ = static_cast<double>(SDL_GetTicksNS() - last_time_) / 1.0e9;
    } 
    else { // 若当前帧时间超过目标帧时间,则直接使用当前帧时间
        delta_time_ = current_delta_time;
    }
}

什么问题呢?发现游戏刚开始的时候,会有一个较长时间的delta_time,所以会程序会运行到当前帧时间超过目标帧时间,则直接使用当前帧时间的else里面,所以说会导致一个较大的delat_time间隔时间,因此,刚开始物理引擎更新的时候会有一个较大的速度,而且第一次更新的末尾才限制了速度,所以说会有一个超大的位移,看上去就是物体直接飞跑了。

那么如何解决呢?很简单,可以在game_app的run函数加一个判断。

// 游戏循环
while(is_running_){
    time_->update();
    float delta_time = time_->getDeltaTime();
    if(delta_time > 0.05f) continue;; // 如果delta_time过大,直接返回
    input_manager_->update(); // 更新输入管理器

    handleEvents();
    update(delta_time);
    render();

    // spdlog::info("delta_time: {}", delta_time);
}

2.发现物体不受重力引擎影响,而是直接从左上角飞走了。

这个问题出在锚点对齐方式那部分,我在创建GameObject测试对象的时候,Sprite精灵组件使用的是CENTER对齐方式,而我的碰撞Collider使用的也是CENTER对齐方式,这会有什么问题?比如物体大小是(32,32),transform变换组件位置(100,100),很明显,两个组件中会有偏移offset_,应该都是(-16,-16),这样会出现一个问题,在物理引擎中的PhysicsEngine::resolveTileCollision()函数中有一个获得位置的函数auto world_aabb = cc->getWorldAABB();,这里得到的是左上角的位置以及一个大小,那左上角的位置就是(84,84)了,那么后面的tc->setPosition(new_obj_pos);不就会出现从原来(100,100)跑到(84,84)了,因为原位置obj_pos是(84,84)这个点,obj_new_pos点会在其附近,更新位置后就发生了向左上角移动(16,16)这个现象了,下一次的点就是(84,84)-(16,16)=(68,68)又会向左上角移动。所以我们的锚点的设置得清楚。将这两个都设置为NONE就不会有问题了。

3.对于检测偏差减去tolerance的疑问?

我其实刚开始不清楚为什么只有右边缘和下边缘需要进行一个tolerance的调整,其实这个问题并不复杂,首先我们需要明白floor()的特性,floor(x)的作用返回不大于x的最大整数,而且瓦片网格是遵循左闭右开的规则的。比如tile_size=16,第0列:0<=x<32(x=32不属于第0列,属于第1列),第0行同理;那么floor()+左闭右开会导致不同边缘的结果差异,左边缘/上边缘 --- floor()计算是正确的。

以x=32为例子:

  • 如果一个物体的第2列左边界是32,这很好,没什么问题,这个物体的左上角确实在第2列
  • 但是如果一个物体的右边缘是32的话,这就不行了,因为由于左闭右开,会将物体错认为是处在第3列

同理y方向也是,因此在处理下/右边缘的时候需要注意这个点,我们会减一个容差防止检测越界。

4.最后想讲一讲Tiled文件的一些内容信息

首先是瓦片集文件(tileset.tsj),这个文件只会记录有自定义配置(properties / 碰撞盒 / 动画)的瓦片(tile),如果没有自定义属性是不会出现在这个tsj文件中的;

地图文件(.tmj),这个文件记录地图中实际用到的所有瓦片ID,所以说两者的地块数量自然是不一致,地图文件中有一个全局id(gid)存储瓦片的信息,会联系其他瓦片集并记录firstid,进而与瓦片集中的localid进行一个联系。

5.最后一个细节问题,在写getTileType()的时候返回TileType,没写return,这太不应该了,当时还奇怪,怎么没有SOLID的地块

return is_solid ? component::TileType::SOLID : component::TileType::NORMAL;
posted @ 2026-02-19 20:42  wenyiGamecpp  阅读(53)  评论(0)    收藏  举报