输入系统
输入系统 — 键盘、鼠标、操作映射、RenderApp 集成
anvilkit-input 提供跨平台的输入抽象,支持键盘和鼠标输入、逻辑操作映射(Action Map)系统,以及与渲染窗口事件循环集成的辅助工具。
InputPlugin(ECS 自动插件)
InputPlugin 包含在 DefaultPlugins 中,自动配置输入系统:
- 将
InputState和ActionMap初始化为 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 变体:Left、Right、Middle。
鼠标位置/运动 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_pressed、just_released、mouse_delta 和 scroll_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() |
|---|---|---|---|
Inactive | false | false | false |
JustPressed | true | true | false |
Pressed | true | false | false |
JustReleased | false | false | true |
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 | 当状态为 JustPressed 或 Pressed 时返回 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_releasedGamepadButton 变体
| 按钮 | 说明 |
|---|---|
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)来填充GamepadState。AutoInputPlugin仅处理键盘/鼠标。
手柄事件转发:
RenderApp和AnvilKitApp事件循环现在会在存在 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-render 的 RenderApp 时,三个辅助方法会自动将操作系统窗口事件桥接到 ECS 的 InputState 资源中。使用自定义 ApplicationHandler 的游戏可以直接调用这些方法。
| 方法 | 类型 | 签名 | 说明 |
|---|---|---|---|
forward_input | static | (app: &mut App, event: &WindowEvent) | 将键盘、鼠标按钮、光标位置和滚轮事件转换为 InputState 调用。 |
forward_device_input | static | (app: &mut App, event: &DeviceEvent) | 将原始 DeviceEvent::MouseMotion 转换为 InputState::add_mouse_delta。用于 FPS 风格的摄像机控制。 |
tick | instance | (&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,后者会在窗口边界处被裁剪。