ECS框架-捕获与中断信号传递
捕获与中断信号传递
现在我们首先要解决上一节的问题,就是防止信号的穿透。
当我们压入多个场景的时候,每个场景在init()都订阅了同一个动作;那么当你再次触发这个动作的时候,所有场景的回调都会触发(因为它们都连接在同一个信号上)。
我们希望输入信号只被当前场景处理,或者允许某个回调"捕获并中断"后续的传播。

目标
- 了解信号穿透的原因,多个场景都订阅了同一个输入动作
- 学会EnTT的
sigh::collect()搜集回调返回值,并在满足条件时终止传播 - 调整输入回调签名为
bool():用返回值表示是否已经处理 - 调整完善输入模块,避免轮询处理
1.设计可中断信号
前面我们对于输入管理器中的动作状态信号是这样设置的,一个动作对应3个sigh(按下、持续、释放),回调的类型是void():不返回任何信息,也就无法表达处理完毕的信息了
所以我们需要调整一下,把信号改成bool(),返回true表示事件已被处理,应该终止继续传播;返回false表示未处理,应该继续让其他订阅者尝试处理
这样我们可以解决“穿透”问题:场景栈顶(当前场景)的回调返回true,表示事件被处理,后续的回调即使存在,也会被中断不再执行。
EnTT信号是先绑定后调用的模式,恰好符合场景栈,新场景后绑定先响应。
2.EnTT collect() 返回值控制传播
现在我们需要修改一下inputmanager中的actions_to_func_,将原本的void改为bool,之后修改注册事件处理函数为bool。
std::unordered_map<std::string, std::array<entt::sigh<bool()>,3>> actions_to_func_;
// onAction
entt::sink<entt::sigh<bool()>> InputManager::onAction(std::string_view action_name, ActionState action_state)
{
return actions_to_func_[std::string(action_name)].at(static_cast<int>(action_state));
}
// 3.触发回调
for(auto& [action_name_id, state] : action_states_){
if(state != ActionState::INAVTIVE){
// 且有绑定回调函数
if(auto it = actions_to_func_.find(action_name_id); it != actions_to_func_.end()){
it->second.at(static_cast<int>(state)).collect([](bool result){return result;});
}
}
}
EnTT 的 sigh 支持 collect:它会依次调用所有回调,并把每个回调的返回值传给一个收集器(collector)。当 collector 返回 true 时,停止继续分发。
输入管理器内部触发回调就通过collect而非publish实现,返回true就停止
it->second.at(static_cast<int>(state)).collect([](bool result){return result;}); //collect需要函数对象 → 让你能自定义如何根据处理状态决定后续流程
3.统一输入管理器的Quit流程和鼠标逻辑坐标优化
我们的输入管理中关于退出还是通过轮询的方式进行的,我们同样可以将其设定为发送一个退出信号,在processEvent()进行修改,我们新添加一个quit函数,触发回调
// 2.根据SDL_Event更新动作状态 这里需要注意如果某个键按下不动时,并不会生成SDL_Event
SDL_Event event;
while(SDL_PollEvent(&event)){
processEvent(event);
}
// 新增触发回调,在处理SDL_EVENT_QUIT事件的时候触发
void InputManager::quit()
{
dispatcher_->trigger<engine::utils::QuitEvent>(); // 触发退出事件
}
对于鼠标的逻辑坐标也可以进行相应的修改,保存为成员变量,一帧中最多执行一次,只有在满足特定的事件时执行
void InputManager::processEvent(const SDL_Event &e)
{
switch(e.type){
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:{
SDL_Scancode scancode = e.key.scancode;
bool is_down = e.key.down;
bool is_repeat = e.key.repeat;
auto it = input_to_actions_.find(scancode);
if(it != input_to_actions_.end()){ // 找到对应的按键响应
const std::vector<std::string>& actions = it->second;
for(const std::string& action : actions){
updateActionState(action,is_down,is_repeat);
}
}
break;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:{
Uint8 mouse_button = e.button.button;
bool is_down = e.button.down;
auto it = input_to_actions_.find(mouse_button);
if(it != input_to_actions_.end()){
const std::vector<std::string>& actions = it->second;
for(const std::string& action : actions){
updateActionState(action,is_down,false);
}
}
// 更新鼠标位置
mouse_position_ = {e.button.x,e.button.y};
SDL_RenderCoordinatesFromWindow(sdl_renderer_, mouse_position_.x, mouse_position_.y, &logical_mouse_position_.x, &logical_mouse_position_.y);
break;
}
case SDL_EVENT_MOUSE_MOTION:{
mouse_position_ = {e.motion.x,e.motion.y};
SDL_RenderCoordinatesFromWindow(sdl_renderer_, mouse_position_.x, mouse_position_.y, &logical_mouse_position_.x, &logical_mouse_position_.y);
break;
}
case SDL_EVENT_QUIT:{
spdlog::trace("输入管理器:收到退出事件");
quit();
break;
}
default:
break;
}
}
最后,在GameApp类中的handleInput函数中就可以将原本轮询的方式替换为输入的update了
void GameApp::handleEvents()
{
input_manager_->update();
scene_manager_->handleInput();
}
章节总结
这节,我们通过修改输入管理器的动作回调函数从void升级到bool,实现了捕获与中断的能力,通过collect代替publish,收集回调的返回值,解决了信号的穿透;同时对输入管理器中的quit做了统一化处理,防止轮询的方式,并且顺手完成了将逻辑坐标保存为成员变量,保证一帧最多调用一次,完成了缓存优化

浙公网安备 33010602011771号