跳转至

世界空间 UI 输入设计

English。

目标是在世界 UI 前打开透明输入 Screen,使玩家可以自由移动系统光标。 默认准心模式与自由光标模式共用后续平面命中和事件路由。 接口、单位、接入示例与验收命令见 API。

flowchart LR
    C[准心:viewport 中心] --> P[屏幕坐标反投影]
    S[透明 Screen:GUI 光标] --> P
    P --> R[渲染视图空间射线]
    R --> H[已有射线与 UI 平面相交]
    H --> L[逆 UI 变换:局部坐标]
    L --> N[最近有效矩形]
    N --> I[hover / button capture / keyboard focus]
    I --> G[GuiEventListener 与子控件]

坐标空间与渲染时序

设输入层光标为 (x,y),逻辑 viewport 为 (W,H),归一化设备坐标为 nx = 2x/W - 1、ny = 1 - 2y/H。用本帧实际投影矩阵 P 的逆矩阵反投影 (nx,ny,-1) 和 (nx,ny,0),透视除法后得到射线起点与第二点,两点之差归一化得到方向。 选择内部深度点可以兼容无限远投影;正交相机也能使用同一流程。

手绘图中的视点、屏幕平面和 UI 平面由这条射线联系起来。投影矩阵包含 FOV、宽高比、 光学缩放及渲染时的 bobbing;不需要另估屏幕到眼睛的距离,也不需要给鼠标偏移套一个固定系数。 没有目标平面或其他命中约束时,屏幕上的一点只确定一条射线,无法唯一确定一个世界点。

世界平面的完整局部到视图矩阵记为 M。相交时射线和平面都在视图空间,命中点再经 M^-1 还原成 UI 像素坐标。M 包含 host model-view、相机平移、UI 的旋转/缩放/位置; 这样避免将世界射线与已减相机位置的平面混用。也避免在命中后重复减去 UI 半宽高。

内置 backend 每帧先复制投影和各个可交互面板的最终变换,再结束这一帧的输入更新。 输入事件在两帧之间使用最近一次呈现的快照,点击前重新计算当前位置,避免只能点击上一帧的 hover。 自由光标不动但视角/面板变化时,帧结束仍重算 hover 和捕获中的局部位置。 离屏相机不覆盖玩家主画面的输入快照。

画布与深度

世界平面只负责一次纹理合成。面板内部的控件、字形和阴影先在独立 RGBA+depth 画布中使用 原版 GUI 正交投影组合,保留 0.03 GUI-Z 等细小层级差。直接把整个 GUI 深度范围压缩为 0.1 世界格,会使文字和阴影在透视深度缓冲中量化为相同值,出现条纹、缺块和边框闪烁。 GUI 深度端点外预留 1 单位,包含 MAX_GUI_Z + 0.03 的阴影层;所有合成顶点固定为局部 Z=0。 因此极端 GUI Z 不进入世界变换,面板位置及命中平面保持一致。

画布清为透明黑,原版 GUI 混合后 RGB 已预乘 alpha;世界合成使用 ONE / ONE_MINUS_SRC_ALPHA,避免重复乘 alpha 形成黑边。平面保留场景深度测试但不写深度。 绘制作用域恢复 FBO、viewport、投影、混合/深度/裁剪与纹理状态。面板删除、跨世界和客户端 关闭时释放画布,尺寸改变时替换附件。面板内的裁剪使用画布高度,而非主窗口高度。

输入所有权

hover 是射线当前命中的最近面板;键盘焦点是最后点击的面板;button capture 则按鼠标按钮分别记录 接受按下事件的面板。三者可以不同。例如按下面板 A 后拖到 B,B 可以成为 hover,A 仍接收拖拽和松键。 拖拽增量由连续两次投影后的 UI 局部坐标相减得到,不使用屏幕像素增量。

鼠标离开矩形后仍可与其无限延伸的平面相交,捕获因此不会因越界丢失。射线暂时无法与平面相交时, 保留最后有效坐标;关闭、移除或失焦时也将 release 发给最初的目标。键盘同样记录接收 key-down 的目标, 切换焦点时补发 release,防止 key-up 落到其他面板。重复移动通知不重复发送零增量拖拽。

输入层不复制世界 UI,也不把面板作为二维 Screen 再绘制一遍。Minecraft 的 Screen 生命周期负责 释放/重新捕获鼠标与清空游戏按键;透明 Screen 负责吞掉输入并转给路由器。世界模拟继续运行。 世界显示、平面求交和输入截获分别维护,后续可以把已有方块遮挡或其他求交器接入同样的射线来源。

正常准心模式另有临时键盘所有权:沿最后点击面板的嵌套焦点链找到可编辑文本控件才接管。 KeyboardHandler 的客户端 HEAD 注入在原版更新 KeyMapping、处理聊天和调试快捷键之前消费事件; 后置按键通知不足以阻止这些副作用。接管开始和结束都清空游戏按键,鼠标捕获与当前 Screen 不变。 Escape 递归清除子控件焦点,补发目标持有的松键,并记录仍按住的物理键,防止旧 repeat 在退出后 变成移动输入。失焦时可能缺少原生 release,故新的 press 会清除旧记录并正常进入游戏。

范围

当前接受 Screen 或有明确矩形尺寸的 GuiEventListener;默认沿用几何平面交互,不同步服务端状态。 点击服务器控制的业务应由面板回调发包,并由服务器验证权限、距离和业务状态。 世界面板的注册生命周期属于调用者;输入层的退出只清理输入状态。