完成瓦片层碰撞解析

This commit is contained in:
Ziyu
2025-06-16 14:44:47 +08:00
parent ddb02723f5
commit a120565132
8 changed files with 205 additions and 17 deletions
+4 -2
View File
@@ -10,10 +10,12 @@
- 总共有“地图大小”个瓦片,放入容器vector中
- 每个瓦片包含数据:Sprite,Type(例如solid类型)
- 可能引用多个图块集,因此可先*载入并保存每个图块集的数据*(载入函数),其它瓦片层(以及对象层)也能继续引用
- 通过data数组中的gid查找所需信息(查找函数),填充瓦片vector
- 通过data数组中的gid查找所需信息(查找函数,能获取自定义属性),填充瓦片vector
- 创建包含 TileLayerComponent 的游戏对象(持有瓦片vector)
- 如果 type : objectgroup 载入对象层
- 对象数据在"objects"字段中,每个对象都创建一个游戏对象
- 关注的要点:gid查找源图片信息,layer数据中确认目标位置(TransformComponent)信息
- 如果没有gid,则代表自己绘制的形状(可能是碰撞盒、触发器等,未来按需处理)
- 如果没有gid,则代表自己绘制的形状(可能是碰撞盒、触发器等,未来按需处理)
- 自定义属性的获取方法:图块集中 获取 "tiles" --> 获取 "properties"
@@ -3,6 +3,7 @@
#include "../core/context.h"
#include "../render/renderer.h"
#include "../render/camera.h"
#include "../physics/physics_engine.h"
#include <spdlog/spdlog.h>
namespace engine::component {
@@ -54,6 +55,13 @@ void TileLayerComponent::render(engine::core::Context& context) {
}
}
void TileLayerComponent::clean()
{
if (physics_engine_){
physics_engine_->unregisterCollisionLayer(this);
}
}
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::warn("TileLayerComponent: 瓦片坐标越界: ({}, {})", pos.x, pos.y);
+7 -1
View File
@@ -12,6 +12,10 @@ namespace engine::core {
class Context;
}
namespace engine::physics {
class PhysicsEngine;
}
namespace engine::component {
/**
* @brief 定义瓦片的类型,用于游戏逻辑(例如碰撞)。
@@ -47,6 +51,7 @@ private:
glm::vec2 offset_ = {0.0f, 0.0f}; ///< @brief 瓦片层在世界中的偏移量 (瓦片层通常不需要缩放及旋转,因此不引入Transform组件)
// offset_ 最好也保持默认的0,以免增加不必要的复杂性
bool is_hidden_ = false; ///< @brief 是否隐藏(不渲染)
engine::physics::PhysicsEngine* physics_engine_ = nullptr; ///< @brief 物理引擎的指针, clean()函数中可能需要反注册
public:
TileLayerComponent() = default;
@@ -92,13 +97,14 @@ public:
void setOffset(const glm::vec2& offset) { offset_ = offset; } ///< @brief 设置瓦片层的偏移量
void setHidden(bool hidden) { is_hidden_ = hidden; } ///< @brief 设置是否隐藏(不渲染)
void setPhysicsEngine(engine::physics::PhysicsEngine* physics_engine) {physics_engine_ = physics_engine; }
protected:
// 核心循环方法
void init() override;
void update(float, engine::core::Context&) override {}
void render(engine::core::Context& context) override;
void clean() override;
};
} // namespace engine::component
+109 -8
View File
@@ -3,6 +3,7 @@
#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 <algorithm>
#include <spdlog/spdlog.h>
@@ -22,6 +23,19 @@ void PhysicsEngine::unregisterComponent(engine::component::PhysicsComponent* com
spdlog::trace("物理组件注销完成。");
}
void PhysicsEngine::registerCollisionLayer(engine::component::TileLayerComponent *layer)
{
layer->setPhysicsEngine(this); // 设置物理引擎指针
collision_tile_layers_.push_back(layer);
spdlog::trace("碰撞瓦片图层注册完成。");
}
void PhysicsEngine::unregisterCollisionLayer(engine::component::TileLayerComponent* layer) {
auto it = std::remove(collision_tile_layers_.begin(), collision_tile_layers_.end(), layer);
collision_tile_layers_.erase(it, collision_tile_layers_.end());
spdlog::trace("碰撞瓦片图层注销完成。");
}
void PhysicsEngine::update(float delta_time) {
// 每帧开始时先清空碰撞对列表
collision_pairs_.clear();
@@ -42,14 +56,8 @@ void PhysicsEngine::update(float delta_time) {
pc->velocity_ += (pc->getForce() / pc->getMass()) * delta_time;
pc->clearForce(); // 清除当前帧的力
// 更新位置:S += v * dt
auto* tc = pc->getTransform();
if (tc) {
tc->translate(pc->velocity_ * delta_time);
}
// 限制最大速度:v = min(v, max_speed)
pc->velocity_ = glm::clamp(pc->velocity_, -max_speed_, max_speed_);
// 处理瓦片层碰撞(速度和位置的更新移入此函数)
resolveTileCollisions(pc, delta_time);
}
// 处理对象间碰撞
checkObjectCollisions();
@@ -84,4 +92,97 @@ void PhysicsEngine::checkObjectCollisions()
}
}
void PhysicsEngine::resolveTileCollisions(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 (world_aabb.size.x <= 0.0f || world_aabb.size.y <= 0.0f) return;
// -- 检查结束, 正式开始处理 --
auto tolerance = 1.0f; // 检查右边缘和下边缘时,需要减1像素,否则会检查到下一行/列的瓦片
auto ds = pc->velocity_ * delta_time; // 计算物体在delta_time内的位移
auto new_obj_pos = obj_pos + ds; // 计算物体在delta_time后的新位置
// 遍历所有注册的碰撞瓦片层
for (auto* layer : collision_tile_layers_) {
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)); // 获取x方向瓦片坐标
// y方向坐标有两个,右上和右下
auto tile_y = static_cast<int>(floor(obj_pos.y / tile_size.y));
auto tile_type_top = layer->getTileTypeAt({tile_x, tile_y}); // 右上角瓦片类型
auto tile_y_bottom = static_cast<int>(floor((obj_pos.y + obj_size.y - tolerance) / tile_size.y));
auto tile_type_bottom = layer->getTileTypeAt({tile_x, tile_y_bottom}); // 右下角瓦片类型
if (tile_type_top == engine::component::TileType::SOLID || tile_type_bottom == engine::component::TileType::SOLID) {
// 撞墙了!速度归零,x方向移动到贴着墙的位置
new_obj_pos.x = tile_x * layer->getTileSize().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)); // 获取x方向瓦片坐标
// y方向坐标有两个,左上和左下
auto tile_y = static_cast<int>(floor(obj_pos.y / tile_size.y));
auto tile_type_top = layer->getTileTypeAt({tile_x, tile_y}); // 左上角瓦片类型
auto tile_y_bottom = static_cast<int>(floor((obj_pos.y + obj_size.y - tolerance) / tile_size.y));
auto tile_type_bottom = layer->getTileTypeAt({tile_x, tile_y_bottom}); // 左下角瓦片类型
if (tile_type_top == engine::component::TileType::SOLID || tile_type_bottom == engine::component::TileType::SOLID) {
// 撞墙了!速度归零,x方向移动到贴着墙的位置
new_obj_pos.x = (tile_x + 1) * layer->getTileSize().x;
pc->velocity_.x = 0.0f;
}
}
// 轴分离碰撞检测:再检查Y方向是否有碰撞 (x方向使用初始值obj_pos.x)
if (ds.y > 0.0f) {
// 检查底部碰撞,需要分别测试左下和右下角
auto bottom_left_y = new_obj_pos.y + obj_size.y;
auto tile_y = static_cast<int>(floor(bottom_left_y / tile_size.y));
auto tile_x = static_cast<int>(floor(obj_pos.x / tile_size.x));
auto tile_type_left = layer->getTileTypeAt({tile_x, tile_y}); // 左下角瓦片类型
auto tile_x_right = static_cast<int>(floor((obj_pos.x + obj_size.x - tolerance) / tile_size.x));
auto tile_type_right = layer->getTileTypeAt({tile_x_right, tile_y}); // 右下角瓦片类型
if (tile_type_left == engine::component::TileType::SOLID || tile_type_right == engine::component::TileType::SOLID) {
// 到达地面!速度归零,y方向移动到贴着地面的位置
new_obj_pos.y = tile_y * layer->getTileSize().y - obj_size.y;
pc->velocity_.y = 0.0f;
}
}
else if (ds.y < 0.0f) {
// 检查顶部碰撞,需要分别测试左上和右上角
auto top_left_y = new_obj_pos.y;
auto tile_y = static_cast<int>(floor(top_left_y / tile_size.y));
auto tile_x = static_cast<int>(floor(obj_pos.x / tile_size.x));
auto tile_type_left = layer->getTileTypeAt({tile_x, tile_y}); // 左上角瓦片类型
auto tile_x_right = static_cast<int>(floor((obj_pos.x + obj_size.x - tolerance) / tile_size.x));
auto tile_type_right = layer->getTileTypeAt({tile_x_right, tile_y}); // 右上角瓦片类型
if (tile_type_left == engine::component::TileType::SOLID || tile_type_right == engine::component::TileType::SOLID) {
// 撞到天花板!速度归零,y方向移动到贴着天花板的位置
new_obj_pos.y = (tile_y + 1) * layer->getTileSize().y;
pc->velocity_.y = 0.0f;
}
}
}
// 更新物体位置,并限制最大速度
tc->setPosition(new_obj_pos);
pc->velocity_ = glm::clamp(pc->velocity_, -max_speed_, max_speed_);
}
} // namespace engine::physics
+8
View File
@@ -5,6 +5,7 @@
namespace engine::component {
class PhysicsComponent;
class TileLayerComponent;
}
namespace engine::object {
@@ -19,6 +20,7 @@ namespace engine::physics {
class PhysicsEngine {
private:
std::vector<engine::component::PhysicsComponent*> components_; ///< @brief 注册的物理组件容器,非拥有指针
std::vector<engine::component::TileLayerComponent*> collision_tile_layers_; ///< @brief 注册的碰撞瓦片图层容器
glm::vec2 gravity_ = {0.0f, 980.0f}; ///< @brief 默认重力值 (像素/秒^2, 相当于100像素对应现实1m)
float max_speed_ = 500.0f; ///< @brief 最大速度 (像素/秒)
@@ -37,8 +39,14 @@ public:
void registerComponent(engine::component::PhysicsComponent* component); ///< @brief 注册物理组件
void unregisterComponent(engine::component::PhysicsComponent* component); ///< @brief 注销物理组件
// 如果瓦片层需要进行碰撞检测则注册。(不需要则不必注册)
void registerCollisionLayer(engine::component::TileLayerComponent* layer); ///< @brief 注册用于碰撞检测的 TileLayerComponent
void unregisterCollisionLayer(engine::component::TileLayerComponent* layer);///< @brief 注销用于碰撞检测的 TileLayerComponent
void update(float delta_time); ///< @brief 核心循环:更新所有注册的物理组件的状态
void checkObjectCollisions(); ///< @brief 检测并处理对象之间的碰撞,并记录需要游戏逻辑处理的碰撞对。
/// @brief 检测并处理游戏对象和瓦片层之间的碰撞。
void resolveTileCollisions(engine::component::PhysicsComponent* pc, float delta_time);
// 设置器/获取器
void setGravity(const glm::vec2& gravity) { gravity_ = gravity; } ///< @brief 设置全局重力加速度
+32 -2
View File
@@ -193,6 +193,34 @@ void LevelLoader::loadObjectLayer(const nlohmann::json& layer_json, Scene& scene
}
}
engine::component::TileType LevelLoader::getTileType(const nlohmann::json &tile_json)
{
if (tile_json.contains("properties")) {
auto& properties = tile_json["properties"];
for (auto& property : properties) {
if (property.contains("name") && property["name"] == "solid") {
auto is_solid = property.value("value", false);
return is_solid ? engine::component::TileType::SOLID : engine::component::TileType::NORMAL;
}
// TODO: 可以在这里添加更多的自定义属性处理逻辑
}
}
return engine::component::TileType::NORMAL;
}
engine::component::TileType LevelLoader::getTileTypeById(const nlohmann::json &tileset_json, int local_id)
{
if (tileset_json.contains("tiles")) {
auto& tiles = tileset_json["tiles"];
for (auto& tile : tiles) {
if (tile.contains("id") && tile["id"] == local_id) {
return getTileType(tile);
}
}
}
return engine::component::TileType::NORMAL;
}
engine::component::TileInfo LevelLoader::getTileInfoByGid(int gid)
{
if (gid == 0) {
@@ -229,7 +257,8 @@ engine::component::TileInfo LevelLoader::getTileInfoByGid(int gid)
static_cast<float>(tile_size_.y)
};
engine::render::Sprite sprite{texture_id, texture_rect};
return engine::component::TileInfo(sprite, engine::component::TileType::NORMAL); // 目前只完成渲染,以后再考虑瓦片类型
auto tile_type = getTileTypeById(tileset, local_id); // 获取瓦片类型(只有瓦片id,还没找具体瓦片json)
return engine::component::TileInfo(sprite, tile_type);
} else { // 这是多图片的情况
if (!tileset.contains("tiles")) { // 没有tiles字段的话不符合数据格式要求,直接返回空的瓦片信息
spdlog::error("Tileset 文件 '{}' 缺少 'tiles' 属性。", tileset_it->first);
@@ -258,7 +287,8 @@ engine::component::TileInfo LevelLoader::getTileInfoByGid(int gid)
static_cast<float>(tile_json.value("height", image_height))
};
engine::render::Sprite sprite{texture_id, texture_rect};
return engine::component::TileInfo(sprite, engine::component::TileType::NORMAL); // 目前只完成渲染,以后再考虑瓦片类型
auto tile_type = getTileType(tile_json); // 获取瓦片类型(已经有具体瓦片json了)
return engine::component::TileInfo(sprite, tile_type);
}
}
}
+17
View File
@@ -3,9 +3,11 @@
#include <glm/vec2.hpp>
#include <nlohmann/json.hpp>
#include <map>
#include <optional>
namespace engine::component {
struct TileInfo;
enum class TileType;
}
namespace engine::scene {
@@ -36,6 +38,21 @@ private:
void loadTileLayer(const nlohmann::json& layer_json, Scene& scene); ///< @brief 加载瓦片图层
void loadObjectLayer(const nlohmann::json& layer_json, Scene& scene); ///< @brief 加载对象图层
/**
* @brief 根据瓦片json对象获取瓦片类型
* @param tile_json 瓦片json数据
* @return 瓦片类型
*/
engine::component::TileType getTileType(const nlohmann::json& tile_json);
/**
* @brief 根据(单一图片)图块集中的id获取瓦片类型
* @param tileset_json 图块集json数据
* @param local_id 图块集中的id
* @return 瓦片类型
*/
engine::component::TileType getTileTypeById(const nlohmann::json& tileset_json, int local_id);
/**
* @brief 根据全局 ID 获取瓦片信息。
* @param gid 全局 ID。
+20 -4
View File
@@ -5,6 +5,7 @@
#include "../../engine/component/sprite_component.h"
#include "../../engine/component/physics_component.h"
#include "../../engine/component/collider_component.h"
#include "../../engine/component/tilelayer_component.h"
#include "../../engine/physics/physics_engine.h"
#include "../../engine/scene/level_loader.h"
#include "../../engine/input/input_manager.h"
@@ -25,6 +26,16 @@ void GameScene::init() {
engine::scene::LevelLoader level_loader;
level_loader.loadLevel("assets/maps/level1.tmj", *this);
// 注册"main"层到物理引擎
auto* main_layer = findGameObjectByName("main");
if (main_layer) {
auto* tile_layer = main_layer->getComponent<engine::component::TileLayerComponent>();
if (tile_layer) {
context_.getPhysicsEngine().registerCollisionLayer(tile_layer);
spdlog::info("注册\"main\"层到物理引擎");
}
}
// 创建 test_object
createTestObject();
@@ -93,15 +104,20 @@ void GameScene::TestObject()
{
if (!test_object_) return;
auto& input_manager = context_.getInputManager();
auto* pc = test_object_->getComponent<engine::component::PhysicsComponent>();
if (!pc) return;
if (input_manager.isActionDown("move_left")) {
test_object_->getComponent<engine::component::TransformComponent>()->translate(glm::vec2(-1, 0));
pc->velocity_.x = -100.0f;
} else {
pc->velocity_.x *= 0.9f;
}
if (input_manager.isActionDown("move_right")) {
test_object_->getComponent<engine::component::TransformComponent>()->translate(glm::vec2(1, 0));
pc->velocity_.x = 100.0f;
} else {
pc->velocity_.x *= 0.9f;
}
if (input_manager.isActionPressed("jump")) {
test_object_->getComponent<engine::component::PhysicsComponent>()->setVelocity(glm::vec2(0, -400));
pc->velocity_.y = -400.0f;
}
}