ECS框架-捕获与中断信号传递

捕获与中断信号传递

现在我们首先要解决上一节的问题,就是防止信号的穿透。

当我们压入多个场景的时候,每个场景在init()都订阅了同一个动作;那么当你再次触发这个动作的时候,所有场景的回调都会触发(因为它们都连接在同一个信号上)。

我们希望输入信号只被当前场景处理,或者允许某个回调"捕获并中断"后续的传播。

image-20260322210610529

目标

  • 了解信号穿透的原因,多个场景都订阅了同一个输入动作
  • 学会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做了统一化处理,防止轮询的方式,并且顺手完成了将逻辑坐标保存为成员变量,保证一帧最多调用一次,完成了缓存优化

posted @ 2026-03-23 20:42  wenyiGamecpp  阅读(44)  评论(0)    收藏  举报