Axmol 3.x 输入系统重构:从 Touch/Mouse 到统一 Pointer,再到现代 InputField

本文基于 Axmol PR #3173:Refactor InputSystem 整理。

这不是一次简单的类名调整,而是 Axmol 3.x 输入、事件、UI 文本输入与平台窗口层的一次系统性重构。

在游戏引擎里,"输入系统"经常是最容易被低估、也最容易长期背负历史包袱的模块。

早期移动端时代,触摸事件是主角;桌面端则长期围绕鼠标、键盘和窗口消息展开。随着跨平台需求越来越复杂,同一个 UI 控件可能同时运行在 Windows、macOS、Linux、Android、iOS、WASM、WinRT 上;同一套交互逻辑还要支持鼠标、触摸、触控笔、滚轮、输入法、剪贴板、文本选择、组合键、焦点切换等行为。

Axmol PR #3173 的目标,就是把这些分散在不同历史路径里的输入逻辑重新收束起来:

  • 新增 ax::ui::InputField,提供现代 UI 文本输入组件;
  • InputDelegate 和平台驱动输入派发替代旧的 IMEDelegate / IMEDispatcher 流程;
  • 新增 InputSystem,作为键盘、Pointer、Mouse、Scroll、Touch、文本输入等事件的统一归一化层;
  • PointerEvent / PointerEventListener 重构旧的 TouchEventTouchEventMouse 以及鼠标/触摸监听器分裂模型;
  • 重构 EventDispatcher 的 pointer 路由,使 pointer stream 可以通过 capture ownership 进行稳定派发;
  • 对核心事件类做 noun-first 命名调整;
  • RenderView 命名更清晰,公共部分重命名为 RenderViewCore 与平台特定 RenderView-*(类名简化为 RenderView);
  • 统一 UI 源文件命名,弱化旧的 UI* 文件名前缀;
  • 更新 cpp-tests、lua-tests、fairygui、sceneio、templates、extensions、Lua bindings、ImGui 集成与多平台文本输入路径。

这类改动对引擎内部非常深,但对使用者而言,核心变化可以概括为一句话:

Axmol 正在从"鼠标和触摸各自为政"的历史模型,迁移到"统一输入系统 + 统一 Pointer 事件 + 现代文本输入控件"的新模型。


为什么要重构输入系统?

旧输入模型最大的问题,不是不能用,而是"分裂"。

典型表现包括:

  1. Touch 和 Mouse 是两条不同路径

    移动端写 EventTouch,桌面端写 EventMouse,UI 控件内部经常需要把两套逻辑分别适配一遍。随着触控笔、桌面触摸屏、浏览器 Pointer Events 等场景出现,这种二分法越来越难覆盖真实设备。

  2. 文本输入与普通事件系统割裂

    旧的 IME 流程更偏向"文本输入专用通道",和键盘事件、控件焦点、平台输入法、剪贴板、选择区等行为之间缺少统一抽象。实现一个现代输入框时,很容易出现平台分支过多、事件边界不清晰的问题。

  3. 事件命名和文件命名带有历史包袱

    EventKeyboardEventCustomEventFocus 这类命名来自早期 cocos2d-x 体系,能看懂,但不够符合现代 C++ 类命名直觉。UI 目录下大量 UI* 文件名也让类名与模块结构显得不够清爽。

  4. 平台视图层职责过重

    RenderView 同时承载公共逻辑和平台差异,随着各平台输入处理越来越复杂,拆分公共 core 与平台实现就变得很有必要。

PR #3173 的价值,正是在这些问题之间做了一次统一设计。


新增 InputSystem:输入事件的归一化入口

这次重构中,InputSystem 是非常关键的一环。

它的定位不是"又一个事件类",而是一个中心化的输入归一化层,用来承接来自平台层的原始输入,并转换成引擎内部更一致的事件模型。

它覆盖的范围包括:

  • keyboard;
  • pointer;
  • mouse;
  • scroll;
  • touch;
  • text input;
  • IME 相关输入;
  • 平台特定输入状态转换。

过去很多平台层可能直接把 touch、mouse、keyboard 事件推给不同的 dispatcher 路径。这样做短期简单,但长期会让上层控件感知到底层设备差异。

新的方向是:

text 复制代码
平台原始输入
    ↓
InputSystem 归一化
    ↓
KeyboardEvent / PointerEvent / text input delegate
    ↓
EventDispatcher / UI 控件 / ImGui / Lua bindings

这样做的好处是,上层 UI 不需要过度关心输入来自鼠标、触摸还是触控笔,而是优先关心"这是一个 pointer down/move/up/cancel/scroll"或者"这是一次文本插入/删除/提交"。

这和 Web 标准中的 Pointer Events 思路类似:不是让每种设备都有一套完全独立的业务模型,而是抽象出一个足够稳定的 pointer 层。


PointerEvent:统一 Touch 与 Mouse 的基础

PR #3173 的另一个核心是 PointerEventPointerEventListener

旧模型里,触摸和鼠标通常是这样的:

cpp 复制代码
EventListenerTouchOneByOne
EventListenerTouchAllAtOnce
EventListenerMouse
EventTouch
EventMouse
Touch

这些类在历史上很自然,但当一个控件既要支持移动端触摸,又要支持桌面鼠标拖拽,还要支持滚轮、悬停、捕获、取消事件时,代码就会变得非常割裂。

新模型围绕 pointer 重新组织:

cpp 复制代码
PointerEvent
PointerEventListener

它能够统一表达:

  • pointer down;
  • pointer move;
  • pointer up;
  • pointer cancel;
  • pointer scroll;
  • pointer id;
  • button / pressed state;
  • primary pointer;
  • 坐标;
  • 事件 phase;
  • pointer capture 状态。

对于 UI 控件而言,这意味着以后更推荐围绕 pointer 编写交互逻辑,而不是分别维护 touch 和 mouse 两套路径。


Pointer Capture:让一次拖拽始终有归属

在复杂 UI 中,一个常见问题是:

pointer down 在控件 A 上发生,但 move/up 时坐标已经离开 A,事件应该发给谁?

旧式 hit-test 模型如果每次 move 都重新命中,拖拽体验就容易不稳定。例如按钮按下后移出按钮、滑块拖动时超出控件边界、ScrollView 拖动过程中子控件层级变化,都可能导致事件归属混乱。

这次 EventDispatcher 的 pointer routing 引入 capture ownership 思路:一次 pointer stream 可以被某个目标捕获,后续 move/up/cancel 会继续派发给捕获者,而不是每次都重新找命中对象。

这对 UI 来说非常关键:

  • 按钮按下后,release 逻辑不会轻易丢失;
  • ScrollView 拖动可以更稳定;
  • 输入框选择文本、拖动光标等交互更容易实现;
  • 多点触摸场景中,每个 pointer id 的归属可以更清晰;
  • cancel 事件可以被统一处理,避免状态残留。

简化理解就是:

text 复制代码
pointer down 命中控件
    ↓
控件获得 pointer stream 的处理权
    ↓
后续 move/up/cancel 继续回到该控件
    ↓
stream 结束后释放 capture

这比"每一帧重新 hit-test 再派发"更符合真实交互语义。


ax::ui::InputField:新的现代 UI 文本输入控件

这次 PR 中最直观的新能力,是新增了 ax::ui::InputField

过去 Axmol 里存在 TextFieldTTFui::TextFieldui::TextFieldExEditBox 等多条文本输入路径。它们各自服务过不同阶段的需求,但对于现代 UI 输入框来说,能力边界并不理想。

新的 InputField 目标是成为 UI 系统中的现代输入组件,支持:

  • 光标渲染;
  • 文本选择;
  • 选择区高亮;
  • 复制、剪切、粘贴;
  • 移动端键盘弹出后自动顶 UI;
  • password mode;
  • password mask character;
  • max length;
  • placeholder;
  • placeholder color;
  • text color;
  • cursor color;
  • read-only;
  • 单行与多行布局;
  • UTF-8 字符计数;
  • 文本换行与 line metrics;
  • IME 集成;
  • 键盘事件处理;
  • pointer hit-test;
  • 长按回调;
  • 内部 dirty flag 更新管线。

从接口设计看,InputField 不再只是一个"能输入文字的 Label",而是一个真正的可编辑文本控件。

另外彻底重构的 InputField 解决了 WebAssembly 中文输入问题:此前在 WebAssembly (Wasm) 环境下存在无法输入中文的问题,这是由于旧版控件无法正确处理浏览器输入法的事件。重构后的 InputField 重写了事件处理逻辑,当在 Wasm 平台运行时,会自动挂接一个隐形的 <input> 元素捕获输入法事件,并与引擎中的文本显示同步。因此,用户可以在浏览器中顺畅地输入中文、日文等多字节字符,输入法候选框也会如原生应用般正常显示,极大提升了东亚语言用户的开发体验。

例如它内部需要维护:

text 复制代码
_inputText
_placeholder
_cursorIndex
_selectionStart / _selectionEnd
_charLimit
_multilineEnabled
_passwordEnabled
_dirtyFlags

还要处理:

text 复制代码
文本内容变化
    ↓
UTF-8 offset cache 重建
    ↓
line metrics 重建
    ↓
Label 几何更新
    ↓
cursor 坐标更新
    ↓
selection highlight 重绘

因此它引入了细粒度 dirty flag pipeline,例如:

  • DIRTY_CHAR_OFFSETS
  • DIRTY_LINE_METRICS
  • DIRTY_TEXT_GEOMETRY
  • DIRTY_CURSOR
  • DIRTY_SELECTION
  • DIRTY_TEXT
  • DIRTY_ALL

这能避免每次输入都粗暴重建所有状态,让文本输入这种高频交互更可控。


IMEDelegateInputDelegate

旧的 IME 模型中,文本输入主要围绕 IMEDelegate / IMEDispatcher 展开。这个设计在早期足够实用,但当输入框需要处理选择区、剪贴板、组合键、平台输入法、软键盘、硬键盘、多行文本等需求时,仅仅把"输入法事件"作为一条独立通道就不够了。

新的 InputDelegate 更像是输入目标的抽象接口:

text 复制代码
平台输入系统
    ↓
InputSystem
    ↓
当前 InputDelegate
    ↓
InputField / EditBox / 其它文本输入目标

这样文本输入不再是孤立的 IME 管线,而是和平台事件、键盘事件、pointer focus、UI 控件状态一起被纳入统一模型。

对引擎维护者来说,这能减少大量平台分支的重复逻辑。

对使用者来说,最终效果是输入框行为更稳定、更接近原生平台体验。


平台文本输入路径的更新

PR #3173 同时更新了多个平台的文本输入路径。

比较典型的是 Android:

  • 新增 AxmolInputConnection
  • 新增 AxmolKeyboardTracker
  • 更新 Java 层 AxmolActivityAxmolEngineAxmolPlayerAxmolSurfaceViewGLAxmolSurfaceViewVK 等输入相关路径。

这说明 Android 输入不再只是简单传递 key event,而是更完整地接入了系统输入法连接、软键盘状态、文本编辑上下文。

iOS/macOS 方面,EditBox 文件被移动到更清晰的目录结构:

text 复制代码
axmol/ui/EditBox/EditBoxImpl-ios.mm
axmol/ui/EditBox/EditBoxImpl-mac.mm
axmol/ui/EditBox/iOS/...
axmol/ui/EditBox/Mac/...

WASM 方面也更新了 EditBox 支持,使其适配新的输入流。

这些变化的共同目标是:让平台差异尽量留在平台层,而不是泄漏到 UI 控件和业务代码中。


RenderView 命名统一,更清晰

PR 中将,重新划分类命名和职责:

cpp 复制代码
RenderViewCore
RenderView-android
RenderView-ios
RenderView-pc (for win32, wasm, linux, macos)
RenderView-winrt
RenderView-wasm

输入系统和窗口系统关系非常密切。鼠标坐标、触摸坐标、屏幕缩放、窗口焦点、IME 窗口位置、软键盘弹出、滚轮事件等,都离不开平台 view。

如果 RenderView 同时承载公共逻辑和所有平台差异,长期会越来越难维护。重组后可以形成更清晰的结构:

text 复制代码
RenderViewCore
    负责跨平台公共状态和公共行为

RenderView-*
    负责平台窗口、平台输入、平台消息桥接

这对后续维护 Win32、WinRT、Android、iOS、macOS、Linux、WASM 都更友好。


事件类命名:从 EventKeyboardKeyboardEvent

这次还对核心事件类做了 noun-first 命名调整,例如:

text 复制代码
EventAcceleration -> AccelerationEvent
EventController   -> ControllerEvent
EventCustom       -> CustomEvent
EventFocus        -> FocusEvent
EventKeyboard     -> KeyboardEvent

对应 listener 也同步调整。

这类改名看似只是风格问题,但对一个现代 C++ 引擎来说很重要。KeyboardEventEventKeyboard 更符合英语直觉和常见 API 命名习惯,也能让代码阅读更顺畅。

旧命名强调"这是一个 Event 的某种类型",新命名强调"这是一个键盘事件对象"。

在类型系统中,这通常更自然。


UI 文件命名清理:去掉历史 UI* 包袱

Axmol 的 UI 模块长期继承了 cocos2d-x 的命名传统,很多文件带 UI 前缀,例如:

text 复制代码
UIButton.cpp
UILayout.cpp
UITabControl.cpp
UIText.cpp

这次 PR 对 UI 源文件和类名做了进一步清理,例如:

text 复制代码
UIButton.cpp      -> Button.cpp
UILayout.cpp      -> LayoutGroup.cpp
UITabControl.cpp  -> TabView.cpp

这种变化的意义在于:

  • 文件名更短;
  • 类名更贴近现代 UI 组件命名;
  • ax::ui 命名空间已经表达了 UI 语义,不需要文件名再重复 UI
  • 后续新组件如 InputField 可以自然融入。

这会带来一定迁移成本,但对 Axmol 3.x 来说,趁大版本重构清理历史命名是合理的。


ImGui、FairyGUI、sceneio、Lua bindings 的同步适配

输入系统重构不是只改 core 就结束了。真正困难的是生态层同步。

PR #3173 涉及:

  • ImGui integration;
  • FairyGUI;
  • sceneio;
  • cpp-tests;
  • lua-tests;
  • live2d-tests;
  • templates;
  • Lua 自动绑定;
  • Lua 手写 binding conversion;
  • 各种扩展模块。

其中 ImGui 尤其重要。ImGui 对输入事件非常敏感,需要准确接收:

  • mouse position;
  • mouse button;
  • wheel;
  • key down/up;
  • text input;
  • modifier keys;
  • focus state。

如果底层输入模型改变,ImGui backend 必须同步更新,否则会出现点击、拖拽、滚轮、快捷键或文本输入异常。

Lua bindings 也同样关键。Axmol 不只是 C++ 引擎,Lua 用户也需要访问新的事件类、PointerEvent、InputField 和 renamed classes。因此绑定层必须跟着更新,否则 C++ API 和脚本 API 会断裂。


测试覆盖:从 UITextField 到 UIInputFieldTest

PR 中还替换了旧的 UITextField / UITextFieldEx cpp-tests,新增 UIInputFieldTest 覆盖。

测试场景包括:

  • basic input;
  • max length;
  • password mode;
  • line wrapping;
  • TTF rendering;
  • BMFont rendering;
  • placeholder color;
  • text color。

这说明新的 InputField 不是只提供接口,而是已经开始围绕真实 UI 行为建立测试样例。

对输入框来说,测试非常重要,因为它不是静态渲染控件,而是一个状态机:

text 复制代码
focus
    ↓
attach IME
    ↓
insert text
    ↓
move cursor
    ↓
select text
    ↓
copy/cut/paste
    ↓
delete backward
    ↓
detach IME

任何一个状态处理不完整,都可能导致平台间行为不一致。


其它构建与三方库调整

除了输入系统主体,这个 PR 还包含一些工程侧调整。

CI 中加入 clang-format 前置检查

build.yml 中新增 format-check job,并让多个平台构建 job 依赖它。这意味着格式检查可以更早失败,避免浪费后续多平台构建资源。

同时 clang-format.yml 被改造成可以通过 workflow_call 被复用,并保留:

  • workflow_dispatch 手动触发;
  • issue comment /clang-format 触发;
  • check_only
  • create_pr
  • auto_commit

这种设计可以让格式检查既能作为主构建流水线的一环,也能作为独立维护工具使用。

GLFW Wayland 选项显式关闭

3rdparty/CMakeLists.txt 中,在 Linux GLFW 构建选项里,当 AX_ENABLE_WAYLAND 未启用时,显式加入:

cmake 复制代码
GLFW_BUILD_WAYLAND OFF

这类改动看起来很小,但对构建可重复性很有帮助。

显式关闭比"默认不启用"更稳,尤其在三方库默认选项变化或 CMake cache 残留时,可以减少不可预期行为。

macOS 特殊快捷键补偿

GLFW Cocoa backend 增加了 performKeyEquivalent: 处理,用于捕获某些在 keyDown: 之前就被系统消费的组合键,例如:

  • Control + Tab;
  • Control + Escape;
  • Command + Period。

这类快捷键在 macOS 上经常会被 AppKit 提前处理。如果引擎希望上层仍能收到这些事件,就需要在更早的 responder hook 中补发。

这体现出输入系统重构中一个很现实的问题:跨平台输入不是简单封装 API,而是要理解每个平台消息机制的细节。


迁移影响:旧代码需要关注什么?

对于 Axmol 3.x 用户,这次 PR 会带来一些迁移点。

1. 事件类改名

旧代码中如果使用:

cpp 复制代码
EventKeyboard
EventCustom
EventFocus
EventController
EventAcceleration

需要迁移到:

cpp 复制代码
KeyboardEvent
CustomEvent
FocusEvent
ControllerEvent
AccelerationEvent

对应 listener 也需要同步替换。

2. Touch / Mouse 逻辑建议迁移到 Pointer

如果业务代码同时支持桌面和移动端,建议逐步从旧 touch/mouse listener 迁移到 pointer listener。

旧写法通常是:

cpp 复制代码
// touch path
// mouse path

新方向应当是:

cpp 复制代码
// pointer down / move / up / cancel / scroll

这样可以减少重复逻辑,也更适合未来触控笔、桌面触摸屏、浏览器输入等场景。

3. UI 文本输入优先考虑 InputField

如果需要更现代的 UI 文本输入控件,可以优先关注:

cpp 复制代码
ax::ui::InputField

特别是需要:

  • 光标;
  • 选择;
  • 剪贴板;
  • 密码模式;
  • max length;
  • 多行;
  • IME;

这些能力时,InputField 会比旧的 TextField 路径更符合未来方向。

4. 自定义控件需要关注 pointer capture

如果自定义控件依赖拖拽、按压、滑动、滚轮,需要重新审视事件生命周期:

text 复制代码
down -> move -> up
down -> move -> cancel
scroll

尤其要注意:

  • pointer down 后是否需要 capture;
  • move 时是否只处理当前 captured pointer;
  • up/cancel 时是否释放状态;
  • 多 pointer id 是否独立处理;
  • hover 与 pressed move 是否区分。

这次重构的长期意义

PR #3173 最大的意义,不是"新增了一个输入框",也不是"把类名改得更现代"。

它真正做的是把 Axmol 的输入基础设施向现代跨平台模型推进了一大步。

旧模型更像是:

text 复制代码
TouchEvent
MouseEvent
KeyboardEvent
IMEDelegate
EditBox
TextField
平台 view
各自处理

新模型更接近:

text 复制代码
平台输入
    ↓
InputSystem
    ↓
统一事件模型
    ↓
PointerEvent / KeyboardEvent / InputDelegate
    ↓
UI / ImGui / Lua / extensions

这会带来几个长期收益:

  1. 控件实现更统一

    Button、ScrollView、Slider、InputField、FairyGUI 控件等,都可以基于 pointer 事件表达交互,不再重复 touch/mouse 两套逻辑。

  2. 平台差异更可控

    Android 输入法、iOS/macOS 文本控件、WASM DOM 输入、WinRT pointer 事件等差异可以尽量收敛在平台层和 InputSystem 中。

  3. 未来扩展更容易

    触控笔、hover、复杂快捷键、多窗口、游戏手柄与 UI 焦点联动等能力,都更容易接入统一模型。

  4. API 更现代

    KeyboardEventPointerEventInputFieldRenderViewCore 这些名字更符合现代 C++ 引擎的直觉。

  5. Axmol 3.x 可以摆脱更多 cocos2d-x 历史包袱

    大版本最适合做这种破坏性但必要的整理。越早统一,后续维护成本越低。


结语

输入系统是引擎里最接近"真实设备"和"用户手感"的模块。

它不像渲染后端那样容易被 benchmark 量化,也不像资源管理那样容易用数据结构解释,但它直接决定了一个按钮是否好点、一个输入框是否好用、一个拖拽是否稳定、一个快捷键是否符合平台习惯。

Axmol PR #3173 通过 InputSystemPointerEventInputDelegateInputField,把过去分散的输入路径重新组织起来。这是一次典型的 Axmol 3.x 风格重构:不只是修补旧接口,而是趁大版本机会把底层模型重新理顺。

短期看,它会带来迁移成本。

长期看,它会让 Axmol 的 UI、输入法、Pointer、脚本绑定和平台适配都站在更稳的基础上。

如果说 Axmol 2.x 更多是在继承和演进 cocos2d-x 的成熟架构,那么 Axmol 3.x 正在做的,就是把这些历史架构逐步改造成更适合现代跨平台应用和游戏开发的形态。

PR #3173 正是其中非常关键的一步。

相关推荐
Zwarwolf2 小时前
Godot零散知识点项目汇总
游戏引擎·godot
游乐码5 小时前
Unity基础(十四)场景异步加载
unity·游戏引擎
mxwin5 小时前
Unity Shader URP:法线在空间变换上的特殊性
unity·游戏引擎·shader
charlee446 小时前
Unity在安卓端如何调试输出信息
android·unity·adb·游戏引擎·真机调试
TCW11218 小时前
Minetest游戏引擎源代码解析
游戏引擎
虹科网络安全19 小时前
艾体宝产品|Arango AutoGraph 如何重构企业的知识图谱
人工智能·重构·知识图谱
上海锝秉工控1 天前
省线型增量编码器:用“减法思维“重构工业控制的未来
网络·人工智能·重构
一锅炖出任易仙1 天前
创梦汤锅学习日记day32
学习·ai·游戏引擎
Hy行者勇哥1 天前
2026 IT技术全景:算力超级周期下的三重重构与不可能三角,尤其需要关注“芯片自己设计自己”的情况
人工智能·重构