AnvilKitAnvilKit

输入系统

输入系统 — 键盘、鼠标、操作映射、RenderApp 集成

anvilkit-input 提供跨平台的输入抽象,支持键盘和鼠标输入、逻辑操作映射(Action Map)系统,以及与渲染窗口事件循环集成的辅助工具。

InputPlugin(ECS 自动插件)

InputPlugin 包含在 DefaultPlugins 中,自动配置输入系统:

  • InputStateActionMap 初始化为 ECS 资源 — 游戏无需手动插入。
  • PreUpdate 调度中注册 action_map_update_system,确保操作状态在游戏逻辑运行前始终是最新的。

推荐入口: 最简单的输入启用方式是通过 AnvilKitApp::run(),它处理输入转发、帧生命周期和插件设置。本页后面描述的手动 RenderApp 集成是针对自定义事件循环的进阶/手动方式。

InputState

InputState 是一个 ECS Resource,用于追踪每帧的键盘和鼠标状态。它区分持续按下、首帧按下和首帧释放。

键盘 API

方法签名说明
press_key(&mut self, KeyCode)记录按键按下。如果该键之前未被按住,同时标记为"刚按下"。
release_key(&mut self, KeyCode)记录按键释放。如果该键之前被按住,标记为"刚释放"。
is_key_pressed(&self, KeyCode) -> bool当键被按住时返回 true(跨帧持续)。
is_key_just_pressed(&self, KeyCode) -> bool仅在按键首次按下的那一帧返回 true。
is_key_just_released(&self, KeyCode) -> bool仅在按键释放的那一帧返回 true。
pressed_keys(&self) -> &HashSet<KeyCode>所有当前被按住的键。

鼠标按钮 API

方法签名说明
press_mouse(&mut self, MouseButton)记录鼠标按钮按下。
release_mouse(&mut self, MouseButton)记录鼠标按钮释放。
is_mouse_pressed(&self, MouseButton) -> bool当按钮被按住时返回 true。
is_mouse_just_pressed(&self, MouseButton) -> bool仅在按下的那一帧返回 true。
is_mouse_just_released(&self, MouseButton) -> bool仅在释放的那一帧返回 true。

MouseButton 变体:LeftRightMiddle

鼠标位置/运动 API

方法签名说明
set_mouse_position(&mut self, Vec2)设置光标绝对位置(像素)。
mouse_position(&self) -> Vec2获取当前光标位置。
add_mouse_delta(&mut self, Vec2)累加原始鼠标运动增量。
mouse_delta(&self) -> Vec2获取本帧累计的增量(像素)。
add_scroll_delta(&mut self, f32)累加滚轮增量(行数)。
scroll_delta(&self) -> f32获取本帧累计的滚轮增量。

帧生命周期

方法签名说明
end_frame(&mut self)清除所有 just_pressedjust_releasedmouse_deltascroll_delta。每帧结束时调用一次。

用法示例

use anvilkit_input::input_state::{InputState, KeyCode, MouseButton};

fn player_system(input: Res<InputState>) {
    // 移动(按住)
    if input.is_key_pressed(KeyCode::W) {
        // 前进
    }
    if input.is_key_pressed(KeyCode::S) {
        // 后退
    }

    // 跳跃(刚按下——只触发一次)
    if input.is_key_just_pressed(KeyCode::Space) {
        // 跳跃
    }

    // 射击
    if input.is_mouse_pressed(MouseButton::Left) {
        // 开火
    }

    // 摄像机视角(用于 FPS 的原始增量)
    let delta = input.mouse_delta();
    camera_yaw   -= delta.x * sensitivity;
    camera_pitch -= delta.y * sensitivity;

    // 缩放
    let scroll = input.scroll_delta();
    fov -= scroll * 2.0;
}

ActionMap

ActionMap 将逻辑操作名称(字符串)映射到一个或多个物理 InputBinding。任一绑定的输入处于激活状态时,该操作即为激活。

InputBinding

pub enum InputBinding {
    Key(KeyCode),
    Mouse(MouseButton),
}

ActionState

变体is_active()is_just_pressed()is_just_released()
Inactivefalsefalsefalse
JustPressedtruetruefalse
Pressedtruefalsefalse
JustReleasedfalsefalsetrue

ActionMap API

方法签名说明
new() -> Self创建空的操作映射。
add_binding(&mut self, action: &str, InputBinding)将输入绑定到命名操作。每个操作支持多个绑定。
update(&mut self, &InputState)根据当前输入重新计算所有操作状态。每帧在输入事件之后调用一次。
action_state(&self, action: &str) -> ActionState获取操作的完整状态。未知操作返回 Inactive
is_action_active(&self, action: &str) -> bool当状态为 JustPressedPressed 时返回 true。
is_action_just_pressed(&self, action: &str) -> bool仅在激活帧返回 true。
is_action_just_released(&self, action: &str) -> bool仅在停用帧返回 true。
get_bindings(&self, action: &str) -> Option<&[InputBinding]>查看当前绑定。
clear_bindings(&mut self, action: &str)移除某个操作的所有绑定。
apply_overrides(&mut self, overrides: HashMap<String, String>)应用来自 Settings 的按键绑定覆盖。每个条目将操作名称映射到按键名称字符串,替换该操作的现有绑定。

解析辅助函数

函数签名说明
InputBinding::from_key_name(name: &str) -> Option<InputBinding>将按键名称字符串(如 "W""Space""MouseLeft")解析为 InputBinding
KeyCode::from_name(name: &str) -> Option<KeyCode>将按键名称字符串解析为 KeyCode。不区分大小写。
use anvilkit_input::action_map::{InputBinding, ActionMap};
use anvilkit_input::input_state::KeyCode;

// Parse key names from config
let binding = InputBinding::from_key_name("Space").unwrap();
let key = KeyCode::from_name("W").unwrap();

// Apply overrides from Settings
let mut overrides = HashMap::new();
overrides.insert("jump".into(), "Space".into());
overrides.insert("shoot".into(), "MouseLeft".into());
action_map.apply_overrides(overrides);

用法示例

use anvilkit_input::prelude::*;
use anvilkit_input::action_map::InputBinding;

// 初始化(通常在 Startup 中)
fn setup_actions(mut commands: Commands) {
    let mut map = ActionMap::new();

    map.add_binding("move_forward", InputBinding::Key(KeyCode::W));
    map.add_binding("move_forward", InputBinding::Key(KeyCode::Up));
    map.add_binding("move_back",    InputBinding::Key(KeyCode::S));
    map.add_binding("move_back",    InputBinding::Key(KeyCode::Down));
    map.add_binding("jump",         InputBinding::Key(KeyCode::Space));
    map.add_binding("shoot",        InputBinding::Mouse(MouseButton::Left));
    map.add_binding("aim",          InputBinding::Mouse(MouseButton::Right));

    commands.insert_resource(map);
}

// 每帧更新(PreUpdate)
fn update_actions(input: Res<InputState>, mut actions: ResMut<ActionMap>) {
    actions.update(&input);
}

// 游戏逻辑(Update)
fn game_logic(actions: Res<ActionMap>) {
    if actions.is_action_active("move_forward") {
        // 前进
    }
    if actions.is_action_just_pressed("jump") {
        // 开始跳跃
    }
    if actions.is_action_just_released("shoot") {
        // 释放蓄力射击
    }
}

手柄支持

GamepadState 追踪已连接的手柄、按钮和模拟轴:

use anvilkit_input::prelude::*;

let mut gamepad = GamepadState::default();

// 连接事件
gamepad.connect(0);
assert!(gamepad.is_connected(0));

// 按钮输入
gamepad.press_button(0, GamepadButton::South);
assert!(gamepad.is_button_pressed(0, GamepadButton::South));

// 模拟轴(范围:-1.0 到 1.0)
gamepad.set_axis(0, GamepadAxis::LeftStickX, 0.75);
let value = gamepad.axis_value(0, GamepadAxis::LeftStickX);

gamepad.end_frame(); // 清除 just_pressed/just_released

GamepadButton 变体

按钮说明
South / East / West / North面板按钮(A/B/X/Y 或 ×/○/□/△)
DPadUp/Down/Left/Right方向键
LeftShoulder / RightShoulder肩键(L1/R1)
LeftTrigger / RightTrigger扳机(L2/R2)
LeftThumb / RightThumb摇杆按下(L3/R3)
Start / Select菜单按钮

GamepadAxis 变体

说明
LeftStickX / LeftStickY左摇杆
RightStickX / RightStickY右摇杆
LeftTrigger / RightTrigger模拟扳机(0.0 到 1.0)

注意: winit 0.30 移除了内置手柄支持。需要单独的后端(如 gilrs)来填充 GamepadStateAutoInputPlugin 仅处理键盘/鼠标。

手柄事件转发: RenderAppAnvilKitApp 事件循环现在会在存在 gilrs 后端时,将手柄连接、按钮和轴事件转发到 GamepadState 中。这消除了在游戏代码中手动更新 GamepadState 的需要。

ActionId(零分配查询)

在热循环中使用 ActionId 避免字符串哈希开销:

let mut map = ActionMap::new();
map.add_binding("jump", InputBinding::Key(KeyCode::Space));
let jump_id = map.register_action("jump");

// 游戏循环中 — 无堆分配
if map.is_action_active_by_id(jump_id) {
    player.jump();
}
方法说明
register_action(name) -> ActionId为命名动作分配数字 ID(幂等)
is_action_active_by_id(id) -> bool零分配激活检查
action_state_by_id(id) -> ActionState零分配完整状态查询

轴向绑定

通过键盘或手柄轴的模拟输入:

use anvilkit_input::prelude::*;

let mut map = ActionMap::new();

// 键盘:A/D → -1.0 / +1.0
map.bind_axis("move_x", AxisBinding::KeyboardAxis {
    negative: KeyCode::A,
    positive: KeyCode::D,
});

// 手柄模拟摇杆
map.bind_axis("move_x", AxisBinding::GamepadAxis(GamepadAxis::LeftStickX));

// 查询合并值(所有绑定中绝对值最大的)
let x = map.axis_value("move_x", &input, gamepad.as_ref());

同时按下 A 和 D 时,互相抵消为 0.0。

RenderApp 集成(进阶/手动)

大多数游戏应使用 AnvilKitApp::run() 而非手动配置 RenderApp。以下部分适用于需要完全自定义事件循环的游戏。

使用 anvilkit-renderRenderApp 时,三个辅助方法会自动将操作系统窗口事件桥接到 ECS 的 InputState 资源中。使用自定义 ApplicationHandler 的游戏可以直接调用这些方法。

方法类型签名说明
forward_inputstatic(app: &mut App, event: &WindowEvent)将键盘、鼠标按钮、光标位置和滚轮事件转换为 InputState 调用。
forward_device_inputstatic(app: &mut App, event: &DeviceEvent)将原始 DeviceEvent::MouseMotion 转换为 InputState::add_mouse_delta。用于 FPS 风格的摄像机控制。
tickinstance(&mut self, app: &mut App)运行一帧:更新 DeltaTime,调用 app.update(),然后调用 InputState::end_frame()

典型自定义事件循环

use anvilkit_render::RenderApp;
use winit::event::{WindowEvent, DeviceEvent};

// 在你的 ApplicationHandler 实现中:

fn window_event(&mut self, event_loop, window_id, event: WindowEvent) {
    // 让引擎处理输入记录
    RenderApp::forward_input(&mut self.app, &event);

    // 处理游戏特定的窗口事件
    match &event {
        WindowEvent::CloseRequested => event_loop.exit(),
        _ => {}
    }
}

fn device_event(&mut self, event_loop, device_id, event: DeviceEvent) {
    // 用于 FPS 摄像机的原始鼠标运动
    RenderApp::forward_device_input(&mut self.app, &event);
}

fn about_to_wait(&mut self, event_loop) {
    // 标准的每帧生命周期:DeltaTime -> app.update() -> end_frame()
    self.render_app.tick(&mut self.app);
    self.window.request_redraw();
}

鼠标锁定

FPS 游戏通常锁定光标并使用原始增量:

use winit::window::CursorGrabMode;

// 将光标锁定在窗口内
window.set_cursor_grab(CursorGrabMode::Confined)
    .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked))
    .unwrap();
window.set_cursor_visible(false);

// 使用 DeviceEvent::MouseMotion(由 forward_device_input 转发)
// 而非 CursorMoved,后者会在窗口边界处被裁剪。

目录