使用GLFW处理同时按下多个键的输入

4

我正在尝试使用glfwSetKeyCallback功能创建基本相机运动。问题在于它无法处理同时输入多个按键,例如W和A应该以左上方向对角线方式移动。相反,它表现得好像只知道最后一个输入的按键一样。所以假设我按下A向左移动,它就向左移动,然后我按下W向左上方对角线移动,它就“忘记”了A被按下并向上移动。

float cameraSpeed = 0.02f;
    if (key == GLFW_KEY_ESCAPE && (action == GLFW_PRESS || action == GLFW_REPEAT))
        glfwSetWindowShouldClose(window, GLFW_TRUE);
    if (key == GLFW_KEY_W && (action == GLFW_PRESS || action == GLFW_REPEAT))
        Game::GetInstance()->cameraY += cameraSpeed;
    if (key == GLFW_KEY_A && (action == GLFW_PRESS || action == GLFW_REPEAT))
        Game::GetInstance()->cameraX -= cameraSpeed;
    if (key == GLFW_KEY_S && (action == GLFW_PRESS || action == GLFW_REPEAT))
        Game::GetInstance()->cameraY -= cameraSpeed;
    if (key == GLFW_KEY_D && (action == GLFW_PRESS || action == GLFW_REPEAT))
        Game::GetInstance()->cameraX += cameraSpeed;

我想到的一个解决方案是创建自己的布尔型键盘输入表,并仅使用GLFW函数设置它们,例如:

if (key == GLFW_KEY_A && action == GLFW_PRESS)
        // set 'A' to true;
if (key == GLFW_KEY_A && action == GLFW_RELEASE)
        // set 'A' to false;

然后我想在完全不同的函数/类/其他中对其进行操作。但这对我来说似乎不太干净。有什么好的解决方法吗?


你在处理GLFW_PRESSGLFW_RELEASE时有什么具体的困难吗? - user7860670
不,它们在只有一个键按下时可以正常工作。但是当我按下两个键(比如W和A)时,它应该向左上方移动(因为这些键一直被按下,所以使用GLFW_REPEAT),但实际上只有最后按下的键被记住了。 - Krzysztof Kwiecień
你是说第一个按键没有接收到GLFW_RELEASE吗? - user7860670
我根本没有释放第一个键,即使我这样做了,我也会得到GLFW_RELEASE。 编辑:问题在于当我接收到第二个键的GLFW_PRESS时,我不再接收第一个键的GLFW_REPEAT。 - Krzysztof Kwiecień
只有最后按下并保持的键才会收到GLFW_REPEAT。这没有任何问题。实际上根本不需要处理GLFW_REPEAT。您可以处理GLFW_PRESSGLFW_RELEASE,并将相机位置调整为与当前按下的键对应的方向。请注意,您不应等待键事件来更新相机位置,就像您现在所做的那样。 - user7860670
显示剩余2条评论
2个回答

7

我也遇到了同样的问题,决定使用回调函数来设置一组键状态。只有最后一个按下的键才会发送 GLFW_REPEAT 信号。我的回调函数直接委托给了一个Input类,但这并不重要:它们将所有参数传递给该类。

void key_callback(GLFWwindow *win, int key, int scancode, int action, int mods){
    Input *input = (Input*)glfwGetWindowUserPointer(win);
    input->keys(win, key, scancode, action, mods);
}

在Input类中定义了键盘按键数组:

bool pressed[KEYS]; // KEYS = 349 (last GLFW macro, GLFW_KEY_MENU = 348)

实际处理方法中,我还处理需要窗口指针的特殊键:

Input::keys(GLFWwindow *win, int key, int scancode, int action, int mods){
    if(key == GLFW_UNKNOWN) return; // Don't accept unknown keys
    if(action == GLFW_PRESS)
        pressed[key] = true;
    else if(action == GLFW_RELEASE)
        pressed[key] = false;

    switch(key){
    case GLFW_KEY_EXCAPE:
        if(action == GLFW_PRESS)
            glfwSetWindowShouldClose(win, true);
    }
}

然后我会在每次渲染操作后的主循环中使用 Input::handle(void) 处理每个按键,用法如下:Input in(); in.handle()

void Input::handle(void){        // Things to do every frame
    for(int i = 0; i < KEYS; i++){
        if(!pressed[i]) continue;  // Skip if not pressed
        switch(i){
        case GLFW_KEY_SPACE:
            cam->ascend(1.0);    // Ascend with camera
        }
    }
}

我计划加入一个时间常数来解决帧率变化的问题,但这就是要点。

结果:即使按下多个键,相机在所有方向上都可以平稳移动。


1
感谢分享你的解决方案。与其每帧循环遍历350多个可能定义的键,其他人可能更喜欢仅检查单个键并执行相关操作。例如,如果(pressed [GLFW_KEY_SPACE]),则cam->ascend(1.0)。 - Michael Whinfrey
@MichaelWhinfrey 哦,那是一个聪明的优化,非常感谢! - Felix

1
我实现了自己版本的键盘处理类:

enum EVENT{
    PRESS = 1,
    RELEASE = 2,
    REPEAT = 4,
    NONE = 8
};

static std::map<int, EVENT> event_map = {{GLFW_PRESS, PRESS}, {GLFW_RELEASE, RELEASE}};

class Input{
public:
    Input() {}
    Input(GLFWwindow *w) : window(w) {}
    void set_window(GLFWwindow *w) {window = w;};

void add_key_input(int key, std::function<void(int, float)> f, int events){
    // if (key == -1) {std::cout << "undefinet key : " << key << '\n'; return;}
    keys[key] = KEY(f, event_map[events]); 
}   

void process(float delta){
    for (auto& [key, info] : keys)  {
        int e = glfwGetKey(window, key);
        std::cout << key << " " << e << " " << info.action << " " << info.event << " ";

        if      (e == GLFW_RELEASE &&  info.action == NONE)                             info.action = NONE;
        else if (e == GLFW_RELEASE &&  (info.action == PRESS || info.action == REPEAT)) info.action = RELEASE;
        else if (e == GLFW_PRESS   &&  (info.action == PRESS || info.action == REPEAT)) info.action = REPEAT;
        else if (e == GLFW_PRESS   && (info.action == NONE || info.action == RELEASE))  info.action = PRESS;

        std::cout << info.action << "\n";     
        if (info.event & info.action) {
            info.callback(key, delta);
        }
    }
}

private:
    struct KEY{
        KEY():action(RELEASE) {}
        KEY(std::function<void(int, float)> f, EVENT e): callback(f), action(RELEASE) ,event(e) {}


    std::function<void(int, float)> callback;
    EVENT action;
    EVENT event;
};

GLFWwindow *window;
std::map<int, KEY> keys;
};

使用它。初始化:

key_input.set_window(window);
key_input.add_key_input(GLFW_KEY_ESCAPE, [this](int key, float delta){glfwSetWindowShouldClose(this->window, true);}, PRESS);

在主循环中:
glfwPollEvents();
key_input.process(delta_time); // in my case delta time is time in secs since last frame

您可以将 std::function<void(int, float)> f, EVENT e> (在我的项目中使用的函数) 替换为 std::any,并传递任何类型的函数,或尝试使用 std::invoke 和 template<typename Args&...>。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接