跳转至

03-碰撞管理器实现与工厂方法设计

大到游戏引擎,小到我们特化的游戏框架中,该如何存访这些碰撞外形的数据,并统一进行处理和优化呢?这是一个很关键的话题。
在前面我们的游戏中,碰撞逻辑十分简单,它们散落在各种游戏对象中,而没有统一进行管理。但随着游戏场景的复杂,我们希望给角色添加场景交互时,这种实现思路便不太容易灵活地进行开发工作了。究其原因,是碰撞逻辑所需的具体数据,被存放在了不同的游戏对象内部,如果我们需要对它们进行处理,就需要引入彼此的引用,让游戏程序中对象的依赖关系变得非常混乱。
所以与上节资源管理器的原理相似,我们便可以将碰撞箱这些数据存放在一处。这样不同对象只需要存放对应碰撞箱的引用,通过回调函数执行碰撞发生后的行为。

1 碰撞层类型

我们首先创建collision_layer.h头文件,添加碰撞层的枚举类定义。

enum class CollisionLayer
{
    None,
    Player,
    Enemy
};

它用来描述某个碰撞箱自身所处的碰撞层级,或可以发生碰撞的目标层级。

2 碰撞箱

紧接着,我们便可以创建collision_box.h头文件

class CollisionBox
{
private:
    Vector2 size;
    Vector2 position;
    bool enable = true;
    std::function<void()> on_collide;
    CollisionLayer layer_src = CollisionLayer::None;
    CollisionLayer layer_dst = CollisionLayer::None;

private:
    CollisionBox() = default;
    ~CollisionBox() = default;
};

需要注意的是,碰撞箱的构造函数和析构函数都被定义为了私有成员,这就意味着我们不能自行随意创建碰撞箱对象。这是因为游戏中所有的碰撞箱对象,都应该由碰撞管理器进行统一管理。
一个不被注册到碰撞管理器的碰撞箱对象,没有任何意义,所以也不应该被创建。

这样,我们便需要在头文件开始处添加CollisionManager的前向声明。并将CollisionManager定义为友元,来允许管理器访问私有构造函数和析构函数。

class CollisionManager;

class CollisionBox
{
    friend class CollisionManager;

private:
    Vector2 size;
    Vector2 position;
    bool enable = true;
    std::function<void()> on_collide;
    CollisionLayer layer_src = CollisionLayer::None;
    CollisionLayer layer_dst = CollisionLayer::None;

private:
    CollisionBox() = default;
    ~CollisionBox() = default;
};

除此之外,我们还需要添加下面这几个公共方法,来方便对碰撞箱对象进行各种属性的设置。
由于碰撞的检测机制由碰撞管理器接管,所以我们只需要对大部分字段仅提供set接口即可。

public:
    void set_enable(bool flag)
    {
        enable = flag;
    }

    void set_layer_src(CollisionLayer layer)
    {
        layer_src = layer;
    }

    void set_layer_dst(CollisionLayer layer)
    {
        layer_dst = layer;
    }

    void set_on_collide(std::function<void()> on_collide)
    {
        this->on_collide = on_collide;
    }

    void set_size(const Vector2& size)
    {
        this->size = size;
    }

    const Vector2& get_size() const
    {
        return size;
    }

    void set_position(const Vector2& position)
    {
        this->position = position;
    }

这里我们依然使用AABB型碰撞箱,也就是说碰撞的外形始终是一个不进行任何旋转操作的矩形。这是代码实现最简单且性能最佳的实现思路。

3 碰撞管理器

终于,我们可以正式开始碰撞管理器的实现了。不过先让我们了解一种新的设计思想

3.1 工厂方法

工厂方法允许我们使用一套顶级的接口,来创建和销毁对象。这可以看作是封装思想的一种实践。
我们对外屏蔽了对象创建和销毁时的细节。这种设计也在很多知名的程序库中被广泛使用。
比如在SDL中,我饿们使用SDL_CreateTexture等函数来创建可以被渲染的纹理对象,使用SDL_DestroyTexture来释放纹理图像资源,而不是直接使用c风格的malloc和free。对于SDL_Surface这种结构也是同样的接口设计思想,使用SDL_FreeSurface进行统一的释放。

3.2 定义头文件

来到我们的项目代码中,创建collision_manager头文件,并添加管理器的单例类定义。

class CollisionManager
{
public:
    static CollisionManager *instance();

private:
    static CollisionManager *manager;

    std::vector<CollisionBox *> collision_box_list;

private:
    CollisionManager();
    ~CollisionManager();
};

游戏中所有的碰撞箱对象都需要存放在collision_box_list这个vector中进行更相信检测。

那么我们就需要额外提供两个工厂方法,用于在外部借助管理器创建和销毁碰撞箱。
同时,为了方便在主循环中进行碰撞处理逻辑的调用以及调试,我们提供了process_collide和on_debug_render两个方法

CollisionBox* create_collision_box();
void destroy_collision_box(CollisionBox *collision_box);

void process_collide();
void on_debug_render();

3.3 实现代码

之后便是创建collision_manager.cpp源文件.添加单例模式的基础函数实现

CollisionManager *CollisionManager::manager = nullptr;

CollisionManager* CollisionManager::instance()
{
    if(!manager)
        manager = new CollisionManager();
    return manager;
}

CollisionManager::CollisionManager() = default;
CollisionManager::~CollisionManager() = default;

create和destroy方法的实现也没有太复杂的内容

CollisionBox* CollisionManager::create_collision_box()
{
    CollisionBox *collision_box = new CollisionBox();
    collision_box_list.push_back(collision_box);
    return collision_box;
}

void CollisionManager::destroy_collision_box(CollisionBox* collision_box)
{
    collision_box_list.erase(std::remove(collision_box_list.begin(), collision_box_list.end(), collision_box),
                             collision_box_list.end());
    delete collision_box;
}

接下来便是实现处理碰撞的核心部分了,我们遍历取出碰撞箱列表中的每一个对象,首先检查它是否启用了碰撞,并且确保碰撞目标的层级不为空,随后检测它与除自身之外其他启用碰撞的同层级对象的碰撞。
这里检测矩形相交性的算法是经典的坐标投影法,分别计算水平和数值的左右边界,作差后与宽或高的和相比较。

void CollisionManager::process_collide()
{
    for(CollisionBox * collision_box_src : collision_box_list) 
    {
        if (!collision_box_src->enable || collision_box_src->layer_dst==CollisionLayer::None)
            continue;

        for (CollisionBox* collision_box_dst : collision_box_list)
        {
            if(!collision_box_dst->enable || collision_box_src == collision_box_dst
                || collision_box_src->layer_dst != collision_box_dst->layer_src)
                continue;

            bool is_collide_x = (std::max(collision_box_src->position.x + collision_box_src->size.x / 2,
                                    collision_box_dst->position.x + collision_box_dst->size.x / 2)
                            - std::min(collision_box_src->position.x - collision_box_src->size.x / 2,
                                    collision_box_dst->position.x - collision_box_dst->size.x / 2)
                            <= collision_box_src->size.x + collision_box_dst->size.x);
            bool is_collide_y = (std::max(collision_box_src->position.y + collision_box_src->size.y / 2,
                                    collision_box_dst->position.y + collision_box_dst->size.y / 2)
                            - std::min(collision_box_src->position.y - collision_box_src->size.y / 2,
                                    collision_box_dst->position.y - collision_box_dst->size.y / 2)
                            <= collision_box_src->size.y + collision_box_dst->size.y);

            if (is_collide_x && is_collide_y && collision_box_dst->on_collide)
                collision_box_dst->on_collide();
        }
    }
}

下面来解释一下layer_src和layer_dst的设计用途,对于玩家刀剑的攻击碰撞箱,它不需要产生受击的逻辑,所以layer_src字段可以设置为None,而因为需要与敌人发生碰撞,所以layer_dst需要设置为Enemy,对应的敌人的受击碰撞箱,仅需要检测伤害来源,所以layer_src必须设置为Enemy,而layer_dst则可以设置为None。
Pasted image 20260309153004.png

将碰撞相关的数据集中起来统一管理的好处有很多:

  • 如果我们想要扩展更多的碰撞层级,只需要简单添加CollisonLayer的更多值就可以了。而不需要修改CollisionBox和CollisonManager的类定义。

说一个超前一点的设计,如果我们需要让一个碰撞箱可以对多个层级的目标产生碰撞。比如某些极管可以同时对敌人和玩家产生伤害。这时便可以将CollisionLayer的比对变成按位运算,每一位对应着不同的层级。

比如用001000表示敌人,用000001表示玩家,则用layer_dst=001001表示敌人和玩家都会受击。

  • 甚至说当我们的场景巨大无比。不得不进行空间优化以减少碰撞的相交性运算时,便可以只修改CollisionManager内部存储碰撞箱对象的数据结构。譬如将列表变为四叉树进行管理。而不需要就该更上层的游戏对象内容。
  • 又或者我们为碰撞箱的create接口提供更多参数,来支持创建形状各异的碰撞箱。
    上面这些内容都是可以在现有的程序架构上拓展和自由发挥的。

最后就是on_debug_render方法的实现了,我们根据碰撞箱对象当前是否处于启用状态,选择不同的颜色在窗口上绘制矩形线框。
这与先前在《植物明星大乱斗》时的设计十分相似,可以帮助我们在开发的过程中十分直观地调试修改抽象地碰撞数据。

void CollisionManager::on_debug_render()
{
    for(CollisionBox* collision_box : collision_box_list)
    {
        setlinecolor(collision_box->enable ? RGB(255, 195, 195) : RGB(115, 115, 115));
        rectangle((int)(collision_box->position.x - collision_box->size.x / 2),
                  (int)(collision_box->position.y - collision_box->size.y / 2),
                  (int)(collision_box->position.x + collision_box->size.x / 2),
                  (int)(collision_box->position.y + collision_box->size.y / 2));
    }
}

4 总结

在这一节中,我们实现了碰撞箱和碰撞管理器的类,提供了create和destroy工厂方法来创建游戏的碰撞箱,将这些数据留存在了碰撞管理器中,而不是散落在后续编写的各种游戏对象内.方便进行统一的管理和优化.

和之前实现的资源管理器相似,碰撞管理器也是一个使用单例模式思想设计的类.碰撞检测的核心时双层循环判断2D平面内矩形相交性. 并提供了调试渲染的接口, 来将抽象的数据可是化在窗口上.。

评论

如果你已登录 GitHub,就可以直接在这里评论。