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。
将碰撞相关的数据集中起来统一管理的好处有很多:
- 如果我们想要扩展更多的碰撞层级,只需要简单添加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,就可以直接在这里评论。