主题
第一人称鼠标捕获与相对视角输入
GameGraph.InputSystem.GamePointerCapture 为当前游戏视口提供鼠标捕获。它适合第一人称观察、建造和桌面自由视角:捕获后隐藏光标,游戏通过相对位移转动镜头;打开背包或菜单时释放鼠标。
需要客户端能力版本 5,对应 ClientRuntimeFeature.RelativeMouseCapture。这是桌面鼠标输入能力;移动端触屏继续使用触摸或摇杆。能力版本说明接口契约存在,不保证当前设备、窗口和视口一定接受捕获请求,必须处理 RequestCapture() 返回 false。
三个状态
| 状态 | 含义 | 下一步 |
|---|---|---|
Released | 未请求捕获,鼠标用于普通 UI | 关闭菜单后调用 RequestCapture() |
WaitingForClick | 请求已接受,等待用户点击当前游戏视口 | 用户按下左键后尝试进入捕获 |
Captured | 当前游戏已获得捕获,接收相对移动 | E 打开菜单,或 Esc/失焦释放实际捕获 |
RequestCapture() 不会立即夺取系统鼠标。首次左键点击用于进入捕获,由宿主消费,不能同时作为攻击、挖掘或 UI 点击;后续点击才进入游戏逻辑。不要在每次 OnPointerButtonDown 中反复调用它。
Esc 和窗口失焦把实际捕获释放到 WaitingForClick。恢复焦点后仍要重新点击,不会自动抓回鼠标。主动 ReleaseCapture() 则取消整个请求,进入 Released,适合背包、暂停菜单和其它需要操作 UI 的状态。停止游戏、停止 PIE 或宿主移除当前游戏时,原生宿主负责释放,项目仍应正常退订自己的事件。
状态通知在帧末派发。游戏结束后 WASM 可能已经关闭,因此不保证再收到 StateChanged(..., GameEnded);在 Game.OnGameEnd 或自己的 Dispose 路径中完成清理,不要等待这个状态事件。
最小接入
在客户端游戏开始、框架服务就绪后检查一次能力,并保留结果供当前游戏使用:
csharp
#if CLIENT
using System.Numerics;
using GameCore.ClientEnvironment;
using GameGraph.InputSystem;
bool supported = ClientRuntime.Current.Supports(
ClientRuntimeFeature.RelativeMouseCapture);
float yaw = 0;
float pitch = 0;
void OnRelativeMoved(Vector2 delta)
{
const float radiansPerUnit = 0.000025f;
yaw -= delta.X * radiansPerUnit;
pitch = Math.Clamp(pitch - delta.Y * radiansPerUnit, -1.45f, 1.45f);
// 将 yaw/pitch 应用到项目自己的客户端镜头。
}
if (supported)
{
GamePointerCapture.RelativeMoved += OnRelativeMoved;
bool accepted = GamePointerCapture.RequestCapture();
// accepted 为 false 时显示当前设备或视口暂不支持捕获的提示。
}
// supported 为 false 时显示更新客户端或使用其它输入方式的提示。
#endifRelativeMoved 的参数是本次派发累计位移,X 向右为正、Y 向下为正;它不是速度,不要再次乘 deltaTime。位移采用当前视口的输入逻辑单位,不承诺未经处理的硬件原始数据。仅捕获生效时才有该事件,不需要用绝对光标坐标相减,也不要在边缘自行把鼠标移回中心。
鼠标灵敏度没有跨设备统一的常数。鼠标 DPI、系统指针设置、窗口缩放和宿主输入路径都可能改变一个输入单位代表的物理移动距离。示例的 0.000025 弧度/输入单位(约 0.00143 度/输入单位)是当前桌面视口的保守起点,正式游戏应把灵敏度做成玩家设置,并按实际设备体验校准。
相对位移只在当前客户端派发。多人游戏仍需按自己的协议同步视角或操作,并由服务端验证攻击、距离和目标;不要把每个鼠标事件直接当成权威网络指令。
E 打开背包,按钮返回游戏
保留一个 menuOpen 状态。E 按键忽略自动重复,切换时使用以下顺序:
csharp
void SetMenuOpen(bool open)
{
menuOpen = open;
if (!supported) return;
if (open) GamePointerCapture.ReleaseCapture();
// 在这里更新背包面板 Visible。
if (!open)
{
bool accepted = GamePointerCapture.RequestCapture();
// 显示“点击游戏画面继续”,或处理请求被拒绝。
}
}返回游戏按钮调用 SetMenuOpen(false)。关闭按钮的这次点击只负责关闭菜单;用户随后再点击游戏画面进入捕获。Esc 由宿主作为安全释放键处理,不要依赖游戏一定收到该键才释放。
订阅 StateChanged 可显示当前状态和原因,例如 Captured、Escape、FocusLost、HostUnavailable、CaptureFailed。状态以宿主报告为准,不能在 RequestCapture() 返回 true 后自行写成已捕获。
销毁控制器时退订事件;游戏仍在运行时销毁控制器,还应调用 ReleaseCapture()。旧客户端不要调用新 API,包括 State getter。框架只负责让内部初始化跳过不支持的事件,不会为主动调用自动生成兼容输入。
可选示例与验证清单
模板 code_sample/src/PointerCaptureSample/ 提供可选模式 PointerCaptureSample:简单方块场景、固定位置视角、E 菜单、返回按钮、状态和左键计数。在 GlobalConfig.cs 中设置 GameDataGlobalConfig.TestGameMode 选择这个模式;它不会修改其他示例的默认模式。编译需使用包含本 API 的 SDK 和依赖,运行需匹配的客户端。
使用支持能力版本 5 的客户端,按项目实际使用的独立客户端、Editor PIE 或多视口环境检查以下行为:
| 环境/操作 | 应观察到的结果 |
|---|---|
| 桌面独立客户端进入样例 | WaitingForClick;第一次左键只捕获,攻击计数不增加 |
| 捕获后移动鼠标并连续点击 | 镜头持续转动,不被屏幕边缘限制;后续左键增加计数 |
| E 打开背包、点击返回按钮 | 打开后为 Released;关闭后为 WaitingForClick,需再点击一次捕获 |
| Esc、Alt-Tab、窗口失焦后恢复 | 释放实际捕获;恢复焦点不自动抓回;再次捕获的点击不计入攻击 |
| PIE 中点击游戏画面外的编辑器 UI | 不允许从非本游戏视口的点击进入捕获 |
| 捕获时停止 PIE,再次进入 | 编辑器恢复普通光标;新游戏需要重新请求并点击 |
| 多客户端/多视口切换 | 相对输入只发给当前捕获游戏;不把光标状态留在已停用的游戏 |
| 老客户端及触屏移动端 | 提示更新或其它输入方式;不因初始化新事件导致启动失败,不把触摸当成相对鼠标 |