输入
Turian 的输入系统通过命名操作将物理设备与游戏逻辑解耦。你可以在 .inputactions 资源中定义操作——键盘、鼠标和手柄绑定都映射到同一个操作名称——你的脚本只需要问"跳跃操作是否按下?",而不需要问"是否按住了 Space?"。
快速开始
1. 创建输入操作资源
在资源浏览器中,右键单击 → 创建 → 输入操作。这会在所选文件夹中写入一个默认的 .inputactions 文件(ZON 格式)。
双击该文件打开输入操作编辑器。
2. 定义你的操作
编辑器显示一个操作列表。每个操作包含:
- 名称 —— 脚本中使用的字符串(例如
jump、move、look)。 - 类型 ——
button(开/关)、axis(1D -1…+1)或vector(2D,x/y)。 - 角色 —— 轴使用
Positive/Negative;向量使用Right/Left/Up/Down;按钮使用Buttons。每个角色最多可容纳 4 个绑定。
为每个绑定选择一个设备和一个代码:
| 设备 | 代码示例 |
|---|---|
key |
a、space、left_shift、left_ctrl |
mouse |
right、left、middle |
gamepad_button |
south、east、left_shoulder、left_trigger |
gamepad_axis |
left_x、left_y、right_x、right_y、left_trigger、right_trigger |
对于 gamepad_axis,+ 复选框选择该轴的正方向(例如摇杆向右、扳机按下)。取消勾选为负方向。
单击保存写入 ZON 文件并重新烹饪到项目中。
3. 打包资源
游戏加载器会自动发现项目中每一个 .inputactions 资源——无需代码。该资源在启动时应用到共享的 engine.Input。
在脚本中读取操作
脚本通过 update 接收 engine.Frame 上下文。frame.input 是一个 *const engine.Input:
const engine = @import("engine");
pub const PlayerController = struct {
pub const is_component = true;
speed: f32 = 5.0,
pub fn update(self: *@This(), frame: engine.Frame) void {
const input = frame.input;
const dt = frame.time.delta;
const t = frame.transform;
// 按钮操作——数字开/关
if (input.wasPressed("jump")) {
// 仅在上升沿触发
}
// 轴操作——1D 连续值
const throttle = input.axis("throttle"); // -1..+1
// 向量操作——2D(例如 WASD / 左摇杆)
const move = input.vector("move"); // .x = 右, .y = 前
t.position.x += move.x * self.speed * dt;
t.position.z += move.y * self.speed * dt;
}
};
操作查询 API
| 方法 | 返回值 | 含义 |
|---|---|---|
isPressed(name) |
bool |
操作值 ≥ 阈值(按住) |
wasPressed(name) |
bool |
在本帧上升至阈值以上 |
axis(name) |
f32 |
1D 值,-1…+1,带死区 |
vector(name) |
engine.Vector2 |
2D 值,限制在单位圆内 |
无论绑定是按键、鼠标按钮还是手柄摇杆,上述四个方法的运行方式都相同——操作类型仅决定哪种查询在语义上有意义。
原始设备查询
对于需要设备特定状态的 UI 或调试代码,frame.input 也暴露了原始查询:
// 键盘
if (input.isKeyDown(.space)) { ... }
if (input.wasKeyPressed(.a)) { ... }
// 鼠标
const delta = input.mouseDelta(); // 本帧的 .x / .y 像素
const wheel = input.wheelDelta(); // 本帧的滚动刻度
if (input.isMouseDown(.right)) { ... }
// 手柄
const lx = input.gamepadAxis(.left_x); // -1..+1
if (input.isGamepadButtonDown(.south)) { ... }
示例:自由飞行摄像机
示例项目提供了 FreeflyCamera.zig,它使用相同的操作名称通过键盘+鼠标和手柄驱动无碰撞摄像机:
move — WASD / 左摇杆
look_axis — 右摇杆(模拟,帧率无关)
ascend — Space / 手柄 A / 右扳机
descend — LShift / 手柄 B / 左扳机
boost — LCtrl / 左肩键
look — 按住鼠标右键使用鼠标增量观察
该脚本仅读取操作——从不提及键码。在 player-controls.inputactions 中重新绑定所有内容,无需修改 FreeflyCamera.zig。
运行时重新绑定
引擎暴露了两个函数,用于游戏中"按某个键重新绑定"的菜单:
// 在设置菜单的更新中:
if (frame.input.captureBinding()) |binding| {
// 用户按下了某个键——应用它
_ = frame.input.rebind("jump", .pos, 0, binding);
}
captureBinding() 返回第一个新按下的按键、鼠标按钮、手柄按钮或手柄轴(超过按下阈值)。rebind 通过操作名称、角色(.pos/.neg/.up/.down)和索引替换或附加绑定。
**持久化:**跨会话保存重新映射的绑定需要设置系统(问题 #13,尚未实现)。跟踪进度请查看问题 #42。
engine.Frame 参考
每个接受第二个参数的生命周期钩子都会接收 engine.Frame:
pub fn update(self: *@This(), frame: engine.Frame) void
pub fn start(self: *@This(), frame: engine.Frame) void
// awake、enable、disable、destroy 也接受 frame
| 字段 | 类型 | 含义 |
|---|---|---|
frame.time |
engine.Time |
增量时间、经过时间、帧计数器 |
frame.input |
*const engine.Input |
操作查询 + 原始设备状态 |
frame.transform |
*engine.Transform |
此对象的位置/旋转/缩放 |
frame.objects |
[]engine.SceneNode |
场景中的所有对象 |
frame.services |
*engine.Services |
用户注册的服务注册表 |
使用 frame.service(MyService) 进行类型化 ?*MyService 查找(在 frame.services 中)。
为了向后兼容,仍支持旧的
update(self, time: engine.Time)签名。