C++游戏开发之旅 13

问题概述

上一章节中,我们完成了物理引擎和物理组件的构建,但是我们的物体似乎直接从地面上掉下去了,我们希望的物体应该停在地面上的,不应该穿过去,这章我们会完成如何教会引擎进行 碰撞检测

这节我们的目标:判断 哪两个物体在当前帧发生了重叠

我们将创建一系列的组件和类实现这个目标:

image-20260211122951661

模块 职责
Collider 定义碰撞形状(矩形、圆形)
ColliderComponent 将碰撞器附加到GameObject
collision 命名空间 提供碰撞检测算法
PhysicsEngine 在每帧检测所有碰撞对

1.碰撞形状(Collider)

首先,我们需要用数学方式描述一个物体的 物理边界,在2D游戏中,最常用的碰撞形状是矩形(AABB)圆形(Circle)

碰撞形状类型

形状 全称 特点 适用场景
AABB Axis-Aligned Bounding Box 轴对齐、计算快速 箱子、平台、建筑
Circle Circle Collider 圆形检测 球、圆状物

我们将创建一个Collider基类和及规格派生类来代表这些形状。

engine/physics/collider.h

#pragma once 
#include <glm/vec2.hpp>

namespace engine::physics {

enum class ColliderType {
    NONE,
    AABB,
    CIRCLE,
    // ...
};

// 抽象基类
// 所有具体的碰撞器都继承自这个类
class Collider {
protected: 
    glm::vec2 aabb_size_ = {0.0f, 0.0f};

public:
    virtual ~Collider() = default;
    virtual ColliderType getType() const = 0;

    void setAABBSize(const glm::vec2& size) { aabb_size_ = size;} // 最小包围盒
    
    const glm::vec2& getAABBSize() const { return aabb_size_;} // 获取最小包围盒

}

/**
 * @brief 轴对齐包围盒(Axis-Aligned Bounding Box)
 */
class AABBCollider final : public Collider {
private:
    glm::vec2 size_ = {0.0f, 0.0f};  // 包围盒大小 --- 和aabb_size_ 一样
public:
    /**
     * @brief 构造函数
     * @param size 包围盒大小
     */
    explicit AABBCollider(const glm::vec2& size) : size_(std::move(size)) {setAABBSize(size_); }
    ~AABBCollider() override = default;

    // get set
    ColliderType getType() const override { return ColliderType::AABB; }
    const glm::vec2& getSize() const { return size_; }
    void setSize(const glm::vec2& size) { size_ = size; }
};

/**
 * @brief 圆形碰撞器
 */
class CircleCollider final : public Collider {
private:
    float radius_ = 0.0f; // 半径
public:
    explicit CircleCollider(float radius) : radius_(radius) { setAABBSize({radius * 2.0f, radius * 2.0f}); }
    ~CircleCollider() override = default;

    ColliderType getType() const override { return ColliderType::CIRCLE; }
    float getRadius() const { return radius_; }
    void setRadius(float radius) { radius_ = radius; }
};

}
设计要点
要点 说明
抽象基类 Collider是抽象基类,定义了所有碰撞器的共有接口,如getType()
AABB尺寸 包含一个aabb_size_,代表能完全包围这个碰撞器的最小矩形尺寸,这在后续的优化中非常有用
具体实现 AABBColliderCircleCollider是具体的碰撞形状实现

为什么圆形也要使用AABB?

圆形碰撞器的AABB:

image-20260211164942018

我们可以先使用AABB检测过滤大部分不碰撞的情况,只对AABB重叠的情况再进行精确的圆形检测。

2.ColliderComponent:为GameObject添加碰撞体

PhysicsComponent类似,需要一个ColliderComponent来将一个Collider形状附加到GameObject上。

核心功能

  • 持有Collider --- 通过std::unique_ptr管理Collider的生命周期
  • 对齐方式 --- 通过offset_alignment_确定碰撞体相对于Transform的位置
  • 世界坐标转换 --- 提供getWorldAABB()方法,计算碰撞体在世界坐标系中的精确包围盒
  • 触发器支持 --- 支持is_trigger_标志,用于不产生物理响应的区域

engine/component/collider_component.h

#pragma once 
#include "component.h"
#include "../physics/collider.h"
#include "../utils/alignment.h"
#include "../utils/math.h"
#include <memory>

namespace engine::component {
class TransformComponent;    
}

namespace engine::component {
/**
 * @brief 碰撞器组件
 * 
 * 持有Collider对象
 */
class ColliderComponent final : public Component{
friend class engine::object::GameObject;
private:
    TransformComponent* transform_ = nullptr; // 缓存的TransformComponent指针(非拥有)
    std::unique_ptr<physics::Collider> collider_;  // 持有Collider对象

    glm::vec2 offset_ = {0.0f,0.0f}; // 碰撞器相对于TransformComponent的偏移量,碰撞器左上角相对变换原点的偏移量
    utils::Alignment alignment_ = utils::Alignment::CENTER; //默认对齐方式,控制偏移量offset_

    bool isTrigger_ = false; // 是否为触发器(仅检测碰撞,不产生物理响应)
    bool isActive_ = true; // 是否激活碰撞器

public:
    /**
     * @brief 构造函数
     * @param collider 指向Collider实例的unique_ptr,拥有所有权
     * @param alignment 对齐方式,默认为CENTER
     * @param isTrigger 是否为触发器,默认为false
     * @param isActive 是否激活碰撞器,默认为true
     */
    explicit ColliderComponent(
        std::unique_ptr<physics::Collider> collider,
        utils::Alignment alignment = utils::Alignment::CENTER,
        bool isTrigger = false,
        bool isActive = true
    );

    /**
     * @brief 可以根据当前的对齐方式和collider_的尺寸计算偏移量offset_
     * @note 需要使用到transform_中的scale,所以在更新scale时,这力需要进行调用更新,
     * 类似的SpriteComponent也需要调用
     */
    void updateOffset();

    // getter and setter
    const TransformComponent* getTransform() const { return transform_; }
    const engine::physics::Collider* getCollider() const { return collider_.get(); }
    glm::vec2 getOffset() const { return offset_; }
    utils::Alignment getAlignment() const { return alignment_; }
    utils::Rect getWorldAABB() const; // 获取碰撞器尺寸,需要考虑类型
    bool isTrigger() const { return isTrigger_; }
    bool isActive() const { return isActive_; }

    void setOffset(const glm::vec2& offset) { offset_ = offset; }
    void setAlignment(utils::Alignment alignment); // 设置对齐方式,同时更新offset_
    void setTrigger(bool isTrigger) { isTrigger_ = isTrigger; }
    void setActive(bool isActive) { isActive_ = isActive; }

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

engine/component/collider_component.cpp

#include "collider_component.h"
#include "transform_component.h"
#include "../object/game_object.h"
#include <spdlog/spdlog.h>

namespace engine :: component{
    ColliderComponent::ColliderComponent(std::unique_ptr<physics::Collider> collider, utils::Alignment alignment, bool isTrigger, bool isActive)
    : collider_(std::move(collider)), alignment_(alignment), isTrigger_(isTrigger), isActive_(isActive)
    {
        if(!collider_){
            spdlog::error("传入的是个空的collider");
        }
    }

    void ColliderComponent::init()
    {
        if(!owner_){
            spdlog::error("collider组件的游戏对象为空");
        }
        transform_ = owner_->getComponent<component::TransformComponent>();

        if(!transform_){
            spdlog::error("collider组件的游戏对象没有transform组件");
            return;
        }
        updateOffset();
        
    }

    void ColliderComponent::updateOffset()
    {
        if(!collider_) return;

        glm::vec2 collider_size = collider_->getAABBSize();
        if(collider_size.x <= 0 || collider_size.y <= 0){
            offset_ = glm::vec2(0,0);
            return;
        }
        auto scale = transform_->getScale();

        switch(alignment_){
            case utils::Alignment::TOP_LEFT: offset_ = scale * glm::vec2(0,0); break;
            case utils::Alignment::TOP_CENTER: offset_ = scale * glm::vec2(-collider_size.x / 2, 0); break;
            case utils::Alignment::TOP_RIGHT: offset_ = scale * glm::vec2(-collider_size.x, 0); break;
            case utils::Alignment::CENTER_LEFT: offset_ = scale * glm::vec2(0, -collider_size.y / 2); break;
            case utils::Alignment::CENTER: offset_ = scale * glm::vec2(-collider_size.x / 2, -collider_size.y / 2); break;
            case utils::Alignment::CENTER_RIGHT: offset_ = scale * glm::vec2(-collider_size.x, -collider_size.y / 2); break;
            case utils::Alignment::BOTTOM_LEFT: offset_ = scale * glm::vec2(0, -collider_size.y); break;
            case utils::Alignment::BOTTOM_CENTER: offset_ = scale * glm::vec2(-collider_size.x / 2, -collider_size.y); break;
            case utils::Alignment::BOTTOM_RIGHT: offset_ = scale * glm::vec2(-collider_size.x, -collider_size.y); break;
            case utils::Alignment::NONE:  break;
            default: break;
        }

    }

    utils::Rect ColliderComponent::getWorldAABB() const
    {
        if(!transform_ || !collider_){
            return {glm::vec2(0,0), glm::vec2(0,0)};
        }
        // 获得最小包围盒左上角的坐标
        const glm::vec2 top_left = transform_->getPosition() + offset_;
        // 包围盒大小
        const glm::vec2 ori_size = collider_->getAABBSize();
        const glm::vec2  scale = transform_->getScale();
        const glm::vec2 size = ori_size * scale;
        return {top_left, size};

    }

    void ColliderComponent::setAlignment(utils::Alignment alignment)
    {
        alignment_ = alignment;
        if(transform_ && collider_)
            updateOffset();
    }
}

关键方法解析

  • updateOffset() --- 根据对齐方式计算碰撞器相对于Transform的偏移量
  • getWorldAABB() --- 关键方法:计算碰撞体在世界坐标系中的精确包围盒,为碰撞检测提供数据

这个组件持有Collider(通过std::unique_ptr管理其生命周期),并像SpriteComponent一样,通过offset_alignment_来确定碰撞体相对于TransformComponent的位置。

3.碰撞检测算法(collision.h/cpp)

我们将所有纯粹的数学碰撞检测函数放到一个独立的collision命名空间下,这让我们的代码更加清晰

检测算法概述

检测类型 算法 复杂度
AABB vs AABB 分离轴定理 O(1)
Circle vs Circle 圆心距离比较 O(1)
AABB vs Circle 最近点检测 O(1)

engine/physics/collision.h

#pragma once
#include <glm/vec2.hpp>
#include "../utils/math.h"

namespace engine::component {
class ColliderComponent;
}

namespace engine::collision {
/**
 * @brief 检查两个碰撞器组件是否发生碰撞
 * @param a 第一个碰撞器组件
 * @param b 第二个碰撞器组件
 * @return 如果发生碰撞,返回true,否则返回false
 */
bool checkCollision(const component::ColliderComponent& a, const component::ColliderComponent& b);

/**
 * @brief 检查两个圆形是否重叠
 * @param a_center a的圆心坐标
 * @param a_radius a的半径
 * @param b_center b的圆心坐标
 * @param b_radius b的半径
 * @return 如果两个圆形重叠,返回true,否则返回false
 */
bool checkCircleOverlap(
    const glm::vec2& a_center, float a_radius,
    const glm::vec2& b_center, float b_radius
);
/**
 * @brief 检查两个AABB是否重叠
 * @param a_pos a的左上角坐标
 * @param a_size a的尺寸
 * @param b_pos b的左上角坐标
 * @param b_size b的尺寸
 * @return 如果两个AABB重叠,返回true,否则返回false
 */
bool checkAABBOverlap(
    const glm::vec2& a_pos, const glm::vec2& a_size,
    const glm::vec2& b_pos, const glm::vec2& b_size
);

/**
 * @brief 检查两个矩形是否重叠
 * @param a 矩形a
 * @param b 矩形b
 * @return 如果两个矩形重叠,返回true,否则返回false
 */
bool checkRectOverlap(const utils::Rect& a, const utils::Rect& b);

/**
 * @brief 检查点是否在圆形内
 * @param point 点的坐标
 * @param center 圆心的坐标
 * @param radius 圆的半径
 */
bool checkPointInCircle(const glm::vec2& point, const glm::vec2& center, float radius);

}

engine/physics/collision.cpp

#include "collision.h"
#include "../component/collider_component.h"
#include "../component/transform_component.h"

namespace engine::collision{
    bool checkCollision(const component::ColliderComponent &a, const component::ColliderComponent &b)
    {
        // 获取两个碰撞体的信息
        auto a_collider = a.getCollider();
        auto b_collider = b.getCollider();
        auto a_transform = a.getTransform();
        auto b_transform = b.getTransform();

        // 先进行AABB的粗略碰撞检测--最小包围盒是否碰撞
        auto a_size = a_collider->getAABBSize() * a_transform->getScale();
        auto b_size = b_collider->getAABBSize() * b_transform->getScale();
        auto a_pos = a_transform->getPosition() + a.getOffset() ;
        auto b_pos = b_transform->getPosition() + b.getOffset();
        // a位置的碰撞盒的左上角位置,a的碰撞盒的尺寸
        if(!checkAABBOverlap(a_pos, a_size, b_pos, b_size)){
            return false;
        }

        // 精确碰撞检测--具体形状的碰撞检测
        if(a_collider->getType() == physics::ColliderType::AABB && b_collider->getType() == physics::ColliderType::AABB){
            return true;
        } // Circle vs Circle
        else if(a_collider->getType() == physics::ColliderType::CIRCLE && b_collider->getType() == physics::ColliderType::CIRCLE){
            auto a_center = a_pos + a_size / 2.f;
            auto b_center = b_pos + b_size / 2.f;
            float a_radius = a_size.x / 2.f;
            float b_radius = b_size.x / 2.f;
            return checkCircleOverlap(a_center, a_radius, b_center, b_radius);
        } // Circle vs AABB
        else if(a_collider->getType() == physics::ColliderType::CIRCLE && b_collider->getType() == physics::ColliderType::AABB){
            auto a_center = a_pos + a_size / 2.f;
            auto a_radius = a_size.x / 2.f;
            auto nearest_point = glm::clamp(a_center, b_pos, b_pos + b_size);
            return checkPointInCircle(nearest_point, a_center, a_radius);
        } // AABB vs Circle
        else if(a_collider->getType() == physics::ColliderType::AABB && b_collider->getType() == physics::ColliderType::CIRCLE){
            auto b_center = b_pos + b_size / 2.f;
            auto b_radius = b_size.x / 2.f;
            auto nearest_point = glm::clamp(b_center, a_pos, a_pos + a_size);
            return checkPointInCircle(nearest_point, b_center, b_radius);
        }
        return false;
    }

    bool checkCircleOverlap(const glm::vec2 &a_center, float a_radius, const glm::vec2 &b_center, float b_radius)
    {
        auto length = glm::length(a_center - b_center);
        return length < (a_radius + b_radius);
    }

    bool checkAABBOverlap(const glm::vec2 &a_pos, const glm::vec2 &a_size, const glm::vec2 &b_pos, const glm::vec2 &b_size)
    {
        if(a_pos.x > b_pos.x + b_size.x || a_pos.y > b_pos.y + b_size.y ||
           a_pos.x + a_size.x < b_pos.x || a_pos.y + a_size.y < b_pos.y) {
            return false;
        }
        return true;
    }

    bool checkRectOverlap(const utils::Rect &a, const utils::Rect &b)
    {
        return checkAABBOverlap(a.position, a.size, b.position, b.size);
    }

    bool checkPointInCircle(const glm::vec2 &point, const glm::vec2 &center, float radius)
    {
        auto length = glm::length(point - center);
        return length < radius;
    }
}
核心算法:两阶段检测

checkCollision函数采用两阶段检测

  • 粗略检测(AABB过滤)--- 如果AABB都不重叠的话那么就直接返回false,该方式可以过滤大部分不碰撞的情况,节省了昂贵的计算资源
  • 精确检测 --- 根据Collider的类型进行精确检测:
    • AABB vs AABB 直接返回true,第一阶段检测过了
    • Circle vs Circle 圆心距离 < 半径和,distance(c1,c2) < r1 + r2
    • AABB vs Circle 最近点检测,找到AABB上离圆心最近的点,检查是否在圆内

最邻近点可以通过clamp函数获取,在检查这个点是否在AABB内(glm::clamp(邻近点,AABB左上角,AABB右下角)

4.在PhysicsEngine中集成碰撞检测

现在,PhysicsEngine不仅要更新物体位置,还要负责在更新后进行物体间的碰撞检测

engine/physics/physics_engine.h(新增)

#include <utility> // std::pair
class PhysicsEngine{
private:
    std::vector<std::pair<engine::object::GameObject*, engine::object::GameObject*>> collision_pairs_; // 保存碰撞的物体对>>
public:
    void update(float delta_time);
    const auto& getCollisionPairs() const {return collision_pairs_};
private:
    void checkObjectCollisions();
}

engine/physics/physics_engine.cpp(新增)

void PhysicsEngine::update(float delta_time) {
    // 1. 每帧开始时清空上一帧的碰撞记录
    collision_pairs_.clear();

    // 2. 更新所有物理组件的位置(与上一课相同)
    for (auto* pc : components_) {
        // ... (更新速度和位置) ...
    }

    // 3. 执行碰撞检测
    checkObjectCollisions();
}

void PhysicsEngine::checkObjectCollisions(){
    // 两层循环遍历所有包含物理组件的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));
            }
        }
    }
}

方法解析

  • 清空碰撞记录 --- collision_pairs_.clear(),每帧开始时清空上一帧的碰撞记录
  • 更新位置 --- 遍历所有PhysicsComponent,应用物理规则更新位置
  • 检测碰撞 --- checkObjectCollisions(),执行碰撞检测
checkObjectCollisions优化
for(size_t i = 0; i < components_.size(); ++i){
    for(size_t j = i+1; j < components_.size(); ++i){
        // 检测 i 和 j 之间的碰撞
    }
}

优化

  • 内存循环j = i + 1:避免重复检测(A vs B和B vs A)
  • 避免自我检测(A vs A)
  • 时间复杂度:O(n$^2$)

性能考虑:对于对象层物体不多的情况,O(n$^2$)暴力求解是可以接受的。

碰撞对的存储
collision_pairs_.emplace_back(obj_a,obj_b);

如果collision::checkCollision返回true,就将这对发生碰撞的GameObject指针存入collision_pairs_

职责分离
  • 物理引擎:报告碰撞事件
  • 上层逻辑:决定如何响应碰撞

5.检测碰撞

我们在GameScene中创建两个物体:一个受重力影响的箱子(AABB碰撞体)和一个静止的箱子(圆形碰撞体),并给其加上ColliderComponent

game/scene/game_scene.cpp

void GameScene::createTestObject()
{
    std::unique_ptr<engine::object::GameObject> gameobj = std::make_unique<engine::object::GameObject>("test1");
    // 添加组件进行测试
    gameobj->addComponent<engine::component::TransformComponent>(glm::vec2(200.0f,100.0f));
    gameobj->addComponent<engine::component::SpriteComponent>(
        "assets/textures/Props/big-crate.png",
        context_.getResourceManager(),
        engine::utils::Alignment::CENTER
    );
    gameobj->addComponent<engine::component::PhysicsComponent>(&context_.getPhysicsEngine(),1.0f);
    gameobj->addComponent<engine::component::ColliderComponent>(
        std::make_unique<engine::physics::AABBCollider>((glm::vec2(32)))
    );
    addGameObject(std::move(gameobj));

    auto gameobj1 = std::make_unique<engine::object::GameObject>("test2");
    gameobj1->addComponent<engine::component::TransformComponent>(glm::vec2(200.0f,200.0f));
    gameobj1->addComponent<engine::component::SpriteComponent>(
        "assets/textures/Props/big-crate.png",
        context_.getResourceManager(),
        engine::utils::Alignment::CENTER
    );
    gameobj1->addComponent<engine::component::PhysicsComponent>(&context_.getPhysicsEngine(),1.0f,false);
    gameobj1->addComponent<engine::component::ColliderComponent>(
        std::make_unique<engine::physics::AABBCollider>((glm::vec2(32)))
    );
    addGameObject(std::move(gameobj1));
}

void GameScene::update(float delta_time) {
    Scene::update(delta_time);
    TestCollisionPairs(); // 每帧检查并打印碰撞对
}


void GameScene::testCollisionPair()
{
    for (auto& pair : context_.getPhysicsEngine().getCollisionPairs()){
        spdlog::info("{}和{}发生碰撞",pair.first->getName(),pair.second->getName());
    }
}

编译运行

image-20260212133653441

可以看到两个箱子发生了碰撞,这证明我们的碰撞检测系统已经成功运行了

系统架构总结

GameObject ---添加---> ColliderComponent(持有Collider) ---提供---> getWorldAABB() (获得世界坐标下的包围盒) ---用于---> collision::checkCollision()(检测算法) ---调用---> PhysicsEngine::checkObjectCollisions() ---> 记录碰撞对 collsion_pairs_ ---> 上层逻辑查询/碰撞解析

完成任务:

  • 定义碰撞形状:AABB 和 Circle 两种基本形状
  • 创建碰撞组件:ColliderComponent管理碰撞器
  • 实现检测算法:两个阶段检测优化性能
  • 集成到物理引擎PhysicsEngine统一管理碰撞检测
  • 碰撞对记录:为碰撞解析做准备

遇到的问题

1.const 关键字使用方面的疑问

// ①返回值的const  ②返回值的引用  ③函数后的const
const glm::vec2& getForce() const {return force_;}

const glm::vec2& ,限定返回的引用是只读的,调用方不能通过这个引用修改force_;

函数后的const,限定这个成员函数是只读的,函数内部不能修改类的任何非mutable成员,(force_不能改),这会让force_变成const属性。在const成员函数中,类的成员变量(比如force_)会被编译器视为const类型 —— 即force_的实际类型是const glm::vec2,这是后续所有写法是否合法的核心前提。

const glm::vec2& getForce() const //合法,force_是const glm::vec2,返回const glm::vec2& ---> 引用是const的force_,类型匹配,调用方无法进行修改
glm::vec2& getForce() const //编译报错,返回是非const引用,相当于把const对象绑定到非const上,违法
const glm::vec2 getForce() const //合法,返回的是值拷贝,把const的force_拷贝一份返回,类型匹配,调用方拿到的是拷贝,不会影响原force_
glm::vec2 getForce() const //合法,将const对象拷贝到非const的对象是合法的,调用方也是无法修改原值的

总结:函数后的const使函数内不能改成员,且force_变为const;返回glm::vec2&(非const引用)无法绑定const对象;去掉引用(返回值拷贝)会有拷贝开销;const glm::vec2&合法高效安全

2.加深理解值传递和const引用两种参数传递方式的差异

explicit AABBCollider(const glm::vec2& size) : size_(size) {setAABBSize(size_); }
explicit AABBCollider(glm::vec2 size) : size_(std::move(size)) {setAABBSize(size_); }

方式1:const glm::vec2&引用绑定,形参size直接绑定到传入的实参,不拷贝,不移动实参,只是"借用"实参的内存地址。成员初始化size_(size)拷贝构造,因为sizeconst引用,不能通过std::move转移其资源(const对象移动会破坏原对象的只读语义),所以只能把size指向的glm::vec2内容完整拷贝到成员变量size_中。(1次拷贝构造)

方式2:glm::vec2 size值接收,调用构造函数时,实参会先拷贝/移动到局部形参size中,传入左值,形参size是拷贝构造;传入右值,移动构造,转移资源,几乎无开销;成员初始化:size_(std::move(size))移动构造,把局部形参size资源直接转移到size_中;通过 std::move 减少拷贝开销,形参 size 被移动后会变成 “有效但未定义” 状态(但因为是局部变量,出了构造函数就销毁,无副作用)(左值:1次拷贝构造,1次移动构造;右值:2次移动构造(1次形参size移动临时对象,成员初始化时1次std::move()移动形参size))

3.对于计算碰撞检测时对于Transform组件中scale的疑问

这里我在自己编写的时候老是容易忘记这个细节部分,在碰撞器组件ColliderComponent中更新偏移的部分、Collision.cpp中checkCollision部分也是,这是个遗留的细节问题,当然也出现在SpriteComponent中的更新偏移部分,不然这样设想一下,比如我们加载一张图片,这张图片有点大,比如64×64px,我想要调整一下,那么就需要通过Transform进行调整,这个GameObject会使用到如下组件:

  • TransformComponent --- position、scale、rotation
  • SpriteComponent

那么这个图片原大小64×64px会存在SpriteComponent组件类中的成员变量sprite_size_=glm::vec2(64,64)中,我们建立的TransformComponent组件中scale默认都是(1.0,1.0);我想要调整,我想将图片缩小一半,那么我需要将scale设置成(0.5,0.5);但是调整后会牵扯到对齐方式,这是肯定的,如果原本是以中心对齐方式CENTER,那么SpriteComponent中的offset_就是offset_ = glm::vec2(-sprite_size_.x/2,-sprite_size_.y/2) * scale = (-32,-32);我们的SpriteComponent中的offset_偏移是始终和TransformComponent中的scale挂钩的,这得注意,如果图片缩小一半,会导致scale缩小一半,那么牵扯到的偏移也应该缩小一半变成(-16,-16)

同样的,类比于碰撞组件ColliderComponent中的偏移offset_,这里的collider_size类似于sprite_size_原始图片的大小,也同样需要scale进行位移偏移计算。

void ColliderComponent::updateOffset()
{
    if(!collider_) return;

    auto collider_size = collider_->getAABBSize();
    if(collider_size.x <= 0 || collider_size.y <= 0){
        offset_ = glm::vec2(0,0);
        return;
    }
    auto scale = transform_->getScale();

    switch(alignment_){
        case utils::Alignment::TOP_LEFT: offset_ = scale * glm::vec2(0,0); break;
        case utils::Alignment::TOP_CENTER: offset_ = scale * glm::vec2(-collider_size.x / 2, 0); break;
        case utils::Alignment::TOP_RIGHT: offset_ = scale * glm::vec2(-collider_size.x, 0); break;
        case utils::Alignment::CENTER_LEFT: offset_ = scale * glm::vec2(0, -collider_size.y / 2); break;
        case utils::Alignment::CENTER: offset_ = scale * glm::vec2(-collider_size.x / 2, -collider_size.y / 2); break;
        case utils::Alignment::CENTER_RIGHT: offset_ = scale * glm::vec2(-collider_size.x, -collider_size.y / 2); break;
        case utils::Alignment::BOTTOM_LEFT: offset_ = scale * glm::vec2(0, -collider_size.y); break;
        case utils::Alignment::BOTTOM_CENTER: offset_ = scale * glm::vec2(-collider_size.x / 2, -collider_size.y); break;
        case utils::Alignment::BOTTOM_RIGHT: offset_ = scale * glm::vec2(-collider_size.x, -collider_size.y); break;
        case utils::Alignment::NONE:  break;
        default: break;
    }

}

4.对于AABB和Circle,也就是矩形和圆之间的碰撞检测不太理解。

这个点其实很简单,我们会计算圆心和矩形之间的最邻近点,只要这个点到圆心的距离小于圆的半径,那么就重叠了,发生碰撞。这个点可能在边上,可能在角上,也可能在内部,那如何计算呢?通过clamp钳制的方法将圆心这个点限制在AABB矩形区域。因为AABB是轴对齐,所以边都是平行于x/y轴的,那么对圆心坐标钳制后得到的点就是AABB上 唯一、离圆心最近的点

posted @ 2026-02-18 21:31  wenyiGamecpp  阅读(47)  评论(0)    收藏  举报