You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用winit检测按键与鼠标移动?Rust相机控制实现求助

使用winit实现WASD+鼠标控制相机

一、核心思路

要实现相机控制,核心是三点:

  • 跟踪WASD按键的按住状态,控制相机前后左右平移
  • 捕获鼠标并获取相对移动量,控制相机视角旋转
  • 基于帧时间差更新相机状态,保证不同帧率下速度一致

二、分步实现

1. 定义输入状态结构体

先封装一个结构体跟踪按键和鼠标的状态,方便帧更新时读取:

use winit::event::{ElementState, MouseButton, VirtualKeyCode, WindowEvent};

#[derive(Default)]
struct InputState {
    // WASD按键按住状态
    w_pressed: bool,
    a_pressed: bool,
    s_pressed: bool,
    d_pressed: bool,
    // 鼠标相对移动量(每帧重置)
    mouse_delta: (f32, f32),
    // 是否锁定鼠标
    cursor_locked: bool,
}

2. 处理窗口事件,更新输入状态

在事件循环中捕获键盘、鼠标事件,实时更新输入状态:

fn handle_event(event: &WindowEvent, input_state: &mut InputState, window: &winit::window::Window) {
    match event {
        // 键盘按键按下/抬起事件
        WindowEvent::KeyboardInput { input, .. } => {
            let is_pressed = input.state == ElementState::Pressed;
            if let Some(key) = input.virtual_keycode {
                match key {
                    VirtualKeyCode::W => input_state.w_pressed = is_pressed,
                    VirtualKeyCode::A => input_state.a_pressed = is_pressed,
                    VirtualKeyCode::S => input_state.s_pressed = is_pressed,
                    VirtualKeyCode::D => input_state.d_pressed = is_pressed,
                    VirtualKeyCode::Escape => {
                        // 按ESC切换鼠标锁定状态
                        input_state.cursor_locked = !input_state.cursor_locked;
                        window.set_cursor_grab(input_state.cursor_locked).unwrap();
                        window.set_cursor_visible(!input_state.cursor_locked);
                    }
                    _ => {}
                }
            }
        }
        // 鼠标移动事件(仅锁定时处理相对位移)
        WindowEvent::MouseMotion { delta, .. } => {
            if input_state.cursor_locked {
                input_state.mouse_delta = (delta.0 as f32, delta.1 as f32);
            }
        }
        // 左键点击锁定鼠标(可选操作)
        WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => {
            if !input_state.cursor_locked {
                input_state.cursor_locked = true;
                window.set_cursor_grab(true).unwrap();
                window.set_cursor_visible(false);
            }
        }
        _ => {}
    }
}

3. 定义相机结构体与更新逻辑

用glam库简化3D数学计算,封装相机的位置、朝向和更新逻辑:

use glam::{Vec3, Mat4};

struct Camera {
    position: Vec3,
    yaw: f32,     // 左右旋转角度(偏航角)
    pitch: f32,   // 上下旋转角度(俯仰角)
    move_speed: f32,
    look_speed: f32,
}

impl Camera {
    fn new(init_pos: Vec3) -> Self {
        Self {
            position: init_pos,
            yaw: -90.0, // 默认朝向-Z轴(屏幕前方)
            pitch: 0.0,
            move_speed: 5.0,
            look_speed: 0.1,
        }
    }

    // 获取相机视图矩阵(用于渲染)
    fn view_matrix(&self) -> Mat4 {
        let front = self.get_front_vector();
        Mat4::look_at_rh(self.position, self.position + front, Vec3::Y)
    }

    // 计算相机前方向向量
    fn get_front_vector(&self) -> Vec3 {
        let yaw_rad = self.yaw.to_radians();
        let pitch_rad = self.pitch.to_radians();
        Vec3::new(
            yaw_rad.cos() * pitch_rad.cos(),
            pitch_rad.sin(),
            yaw_rad.sin() * pitch_rad.cos(),
        ).normalize()
    }

    // 每帧更新相机状态
    fn update(&mut self, input: &mut InputState, delta_time: f32) {
        // 处理WASD平移
        let front = self.get_front_vector();
        let right = front.cross(Vec3::Y).normalize();
        let speed = self.move_speed * delta_time;

        if input.w_pressed { self.position += front * speed; }
        if input.s_pressed { self.position -= front * speed; }
        if input.a_pressed { self.position -= right * speed; }
        if input.d_pressed { self.position += right * speed; }

        // 处理鼠标旋转
        if input.cursor_locked {
            let (dx, dy) = input.mouse_delta;
            self.yaw += dx * self.look_speed;
            self.pitch -= dy * self.look_speed; // 反转Y轴,符合直觉

            // 限制俯仰角,避免相机翻转
            self.pitch = self.pitch.clamp(-89.0, 89.0);
        }

        // 重置鼠标位移,避免下一帧重复处理
        input.mouse_delta = (0.0, 0.0);
    }
}

4. 整合到主事件循环

将上述模块整合到winit的主循环中,完成完整流程:

fn main() {
    let event_loop = winit::event_loop::EventLoop::new();
    let window = winit::window::WindowBuilder::new()
        .with_title("WASD Mouse Camera")
        .build(&event_loop)
        .unwrap();

    let mut input_state = InputState::default();
    let mut camera = Camera::new(Vec3::new(0.0, 0.0, 5.0));
    let mut last_frame = std::time::Instant::now();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = winit::event_loop::ControlFlow::Poll;

        match event {
            winit::event::Event::WindowEvent { event, window_id } if window_id == window.id() => {
                if matches!(event, WindowEvent::CloseRequested) {
                    *control_flow = winit::event_loop::ControlFlow::Exit;
                }
                handle_event(&event, &mut input_state, &window);
            }
            winit::event::Event::MainEventsCleared => {
                // 计算帧时间差,保证移动速度稳定
                let now = std::time::Instant::now();
                let delta_time = now.duration_since(last_frame).as_secs_f32();
                last_frame = now;

                // 更新相机状态
                camera.update(&mut input_state, delta_time);

                // 这里可以添加渲染逻辑,比如使用camera.view_matrix()
                println!("Pos: {:?}, Yaw: {:.1}, Pitch: {:.1}", camera.position, camera.yaw, camera.pitch);

                window.request_redraw();
            }
            winit::event::Event::RedrawRequested(_) => {
                // 渲染代码写在此处
            }
            _ => {}
        }
    });
}

三、依赖配置

在Cargo.toml中添加所需依赖:

[dependencies]
winit = "0.29"
glam = "0.24"

四、关键注意事项

  • 鼠标锁定:调用set_cursor_grab(true)后,鼠标会被限制在窗口内,此时的MouseMotion事件返回相对位移,适合控制视角
  • 帧时间差:用delta_time缩放移动/旋转速度,避免帧率波动导致速度不稳定
  • 俯仰角限制:将俯仰角限制在-89°到89°之间,防止相机翻转后视图异常
  • 退出控制:按ESC键可切换鼠标锁定状态,方便退出相机控制模式

内容的提问来源于stack exchange,提问作者Question Asker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 19:05:08