ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unity WebGL 中文输入法候选框跟随与全屏兼容实战

Unity WebGL 中文输入法候选框跟随与全屏兼容实战

简介:该资源面向在 Unity WebGL 项目中需要处理中文输入与输入法定位的开发者,重点解决浏览器环境下中文输入法无法正常唤起、输入框与光标位置错位,以及全屏切换后输入法窗口定位失效等问题,适合具备一定 Unity 与 JavaScript 交互基础的中级开发者参考。压缩包共 19 个文件,约 17.4MB,包含 html 入口页、css 样式、js 脚本、json 配置、unityweb 构建产物及 png 图标等,覆盖 WebGL 构建输出与页面层交互所需的主要文件类型。已有 940 人学习下载。资源通过 JavaScript 与 Unity C# 脚本通信激活浏览器输入法,并监听 canvas 鼠标事件动态调整输入法窗口位置,同时处理全屏 API 切换后的定位更新;配套说明文档与构建目录结构便于读者对照理解实现思路,可作为中文输入与输入法跟随功能的完整参考示例。

1. 从一次真机联调翻车说起:Unity WebGL 里的中文输入到底卡在哪

去年帮某团队做一个浏览器端的三维展示 Demo,本地编辑器里中文输入一切正常,打包成 WebGL 丢到浏览器里,输入框一敲拼音就露馅:候选词框飘在页面左上角,跟输入框完全对不上;切到全屏模式后更离谱,输入框直接消失,光标还在闪但字打不进去。这类问题在 Unity WebGL 项目里几乎是必经之路,尤其是需要用户填表单、搜索、聊天的场景。这份资源就是一个专门解决该问题的 Unity WebGL 中文输入 Demo,核心做两件事:让输入法候选框跟随输入框位置,以及在浏览器全屏状态下保持输入可用。适合正在做 WebGL 端交互表单、又不想自己从零啃 IME 事件桥接的开发者,拿来就能对照改。

2. 项目定位与技术选型:为什么绕不开 JavaScript 桥接

2.1 这个 Demo 解决的真实痛点

Unity 打包成 WebGL 后,运行在浏览器的 Canvas 里,Unity 自己的 UI 输入组件(InputField / TMP_InputField)在桌面浏览器上对英文数字没问题,但一旦涉及中文输入法,就会撞上两层墙。第一层是 IME 组合输入(composition)事件:拼音还没上屏时的那串字母、候选词列表,属于浏览器输入法的中间态,Unity 的输入系统默认拿不到完整状态。第二层是 DOM 与 Canvas 的坐标错位:输入法候选框由浏览器根据当前聚焦的 DOM 元素位置来绘制,而 Unity 的输入框画在 Canvas 里,浏览器根本不知道那个“框”在哪,于是候选框只能飘到页面角落。

这个 Demo 的思路很直接:在 Canvas 上层叠一个透明的 HTML input 元素,把 Unity 输入框的屏幕坐标同步给它,让浏览器认为用户正在操作一个真实的 DOM 输入框,从而把 IME 候选框画到正确位置。全屏问题则是另一条线——浏览器全屏 API 触发后,页面布局和坐标系会变化,透明 input 的定位需要重新计算,否则就会出现“全屏后输入框消失”的现象。

2.2 技术栈与目录结构

项目是标准的 Unity 工程,核心逻辑分两块:C# 侧负责 UI 事件与坐标上报,JavaScript 侧负责 DOM 元素管理与 IME 事件回传。常见目录结构大致如下(不同 Unity 版本略有差异,以实际工程为准):

Assets/ Scripts/ WebGLInput.cs // C# 侧入口,挂到输入框上 WebGLInputManager.cs // 单例管理,协调多个输入框 Plugins/ WebGLInput.jslib // JavaScript 桥接层 Scenes/ Demo.unity // 演示场景 ProjectSettings/ ProjectSettings.asset // 需确认 WebGL 相关设置

Plugins下的.jslib是 Unity WebGL 调用 JavaScript 的标准方式,C# 里用[DllImport("__Internal")]声明外部函数,构建时 Unity 会把 jslib 合并进生成的页面脚本。这个结构的好处是桥接层独立,升级 Unity 版本时改动面小;坏处是 jslib 里的调试信息不会出现在 Unity 控制台,排查要靠浏览器 DevTools,这一点后面避坑章节会细说。

2.3 为什么不用 Unity 官方方案

Unity 较新版本对 WebGL 输入有过一些改进,但在中文 IME 跟随和全屏兼容上,官方组件的覆盖并不完整,尤其是候选框定位这块,仍然需要项目自己接管。自己写桥接的代价是要处理 compositionstart / compositionupdate / compositionend 三个事件的时序,还要处理移动端浏览器软键盘弹出导致的视口变化。这个 Demo 把这些脏活封装好了,对二次开发来说,站在它的肩膀上改比从零写要省很多时间。

3. 核心链路拆解:从点击输入框到候选框跟随的完整流程

3.1 C# 侧:输入框注册与坐标上报

C# 侧的核心职责是告诉 JavaScript“我现在有一个输入框,它在屏幕上的位置是哪里”。典型实现是在输入框获得焦点时注册,失去焦点时注销,并在每帧或位置变化时更新坐标。下面是一段贴近该 Demo 思路的示意代码:

using UnityEngine; using UnityEngine.UI; using System.Runtime.InteropServices; public class WebGLInput : MonoBehaviour { // 声明 jslib 中的外部函数,仅在 WebGL 平台生效 #if UNITY_WEBGL && !UNITY_EDITOR [DllImport("__Internal")] private static extern void RegisterInput(int id, float x, float y, float w, float h); [DllImport("__Internal")] private static extern void UpdateInputRect(int id, float x, float y, float w, float h); [DllImport("__Internal")] private static extern void UnregisterInput(int id); #endif private InputField inputField; private int inputId; private RectTransform rectTransform; void Awake() { inputField = GetComponent<InputField>(); rectTransform = GetComponent<RectTransform>(); inputId = GetInstanceID(); } void OnEnable() { inputField.onValueChanged.AddListener(OnValueChanged); } // 获得焦点时注册,把当前屏幕矩形传给 JS public void OnSelect() { #if UNITY_WEBGL && !UNITY_EDITOR Vector3[] corners = new Vector3[4]; rectTransform.GetWorldCorners(corners); // 世界坐标转屏幕坐标,注意 WebGL 下 y 轴方向与 DOM 相反 Vector2 min = RectTransformUtility.WorldToScreenPoint(null, corners[0]); Vector2 max = RectTransformUtility.WorldToScreenPoint(null, corners[2]); RegisterInput(inputId, min.x, Screen.height - max.y, max.x - min.x, max.y - min.y); #endif } void Update() { // 位置可能因滚动、动画变化,持续同步 #if UNITY_WEBGL && !UNITY_EDITOR if (inputField.isFocused) { Vector3[] corners = new Vector3[4]; rectTransform.GetWorldCorners(corners); Vector2 min = RectTransformUtility.WorldToScreenPoint(null, corners[0]); Vector2 max = RectTransformUtility.WorldToScreenPoint(null, corners[2]); UpdateInputRect(inputId, min.x, Screen.height - max.y, max.x - min.x, max.y - min.y); } #endif } private void OnValueChanged(string value) { // 值变化时同步回 Unity 输入框(JS 侧也会回调,这里做兜底) } }

这段代码的关键点有三个。第一,#if UNITY_WEBGL && !UNITY_EDITOR条件编译必须加,否则在编辑器里运行会因为找不到__Internal而报错。第二,坐标转换里Screen.height - max.y这一步是血泪经验——Unity 屏幕坐标原点在左下,DOM 原点在左上,不做翻转候选框会上下颠倒。第三,Update里持续同步而不是只在OnSelect时同步一次,是因为输入框可能位于可滚动列表里,用户滚动时位置会变,只同步一次候选框就会错位。

3.2 JavaScript 侧:透明 input 与 IME 事件回传

jslib 侧要做的事更细:创建一个绝对定位的透明 input,跟随 C# 传来的矩形,监听 composition 事件,把最终文本回传给 Unity。示意实现如下:

// WebGLInput.jslib mergeInto(LibraryManager.library, { $inputMap: {}, RegisterInput: function (id, x, y, w, h) { var el = document.createElement('input'); el.type = 'text'; el.style.position = 'absolute'; el.style.left = x + 'px'; el.style.top = y + 'px'; el.style.width = w + 'px'; el.style.height = h + 'px'; el.style.opacity = '0'; // 透明但可聚焦 el.style.zIndex = '9999'; el.style.border = 'none'; el.style.outline = 'none'; document.body.appendChild(el); inputMap[id] = el; // 组合输入结束,把上屏文本回传 Unity el.addEventListener('compositionend', function (e) { SendMessage('WebGLInputManager', 'OnIMECommit', e.data); }); el.addEventListener('input', function (e) { // 非 IME 输入(英文、数字)走这里 SendMessage('WebGLInputManager', 'OnInput', e.target.value); }); el.focus(); }, UpdateInputRect: function (id, x, y, w, h) { var el = inputMap[id]; if (!el) return; el.style.left = x + 'px'; el.style.top = y + 'px'; el.style.width = w + 'px'; el.style.height = h + 'px'; }, UnregisterInput: function (id) { var el = inputMap[id]; if (el && el.parentNode) el.parentNode.removeChild(el); delete inputMap[id]; } });

opacity: 0而不是display: none是必须的,后者会让元素无法聚焦,IME 事件也就不会触发。compositionend里拿到的e.data是最终上屏的中文文本,input事件则覆盖英文和数字场景,两条路径分开处理能避免重复上屏。SendMessage是 Unity 提供的全局函数,第一个参数是挂载脚本的 GameObject 名,第二个是方法名,第三个是参数,这里要求场景里有一个名为WebGLInputManager的对象接收回调。

3.3 全屏模式下的坐标重算

全屏问题的根源在于浏览器全屏 API 触发后,document.body的尺寸和滚动位置会变化,之前算好的绝对坐标可能失效。处理方式是在全屏状态变化时重新触发一次坐标同步。常见做法是监听fullscreenchange事件,通知 Unity 侧刷新所有已注册输入框的矩形:

document.addEventListener('fullscreenchange', function () { // 通知 Unity 重新上报所有输入框坐标 SendMessage('WebGLInputManager', 'OnFullscreenChanged', document.fullscreenElement ? 1 : 0); });

C# 侧收到通知后,遍历所有活跃输入框重新调用UpdateInputRect。这里有个容易忽略的点:全屏切换有动画过渡,立即重算可能拿到中间态尺寸,稳妥做法是延迟一两帧再同步,或者在全屏事件后连续同步几帧。Demo 里如果只做了一次同步,在部分浏览器上仍会出现短暂错位,二次开发时值得补上。

4. 跑起来与改起来:环境配置、构建步骤与参数调整

4.1 环境准备与工程导入

拿到工程后,第一步是确认 Unity 版本。WebGL 输入相关的 API 在不同版本间有差异,建议用工程标注的版本打开,没有标注就用近两年的 LTS 版本。导入后检查ProjectSettings里的 WebGL 平台是否已切换,Player Settings中确认WebGL模板和压缩格式。如果工程里带了Plugins/WebGLInput.jslib,不需要额外安装包,Unity 构建时会自动处理。

构建流程是标准的:File > Build Settings选 WebGL,Switch Platform,然后Build。构建产物是一个包含index.html的文件夹,必须通过本地服务器访问,直接双击打开会因为跨域和文件协议限制导致 jslib 加载失败。常见做法是用 Python 起一个静态服务:

# 在构建产物目录下执行 python -m http.server 8080 # 浏览器访问 http://localhost:8080

如果项目用了 Brotli 或 Gzip 压缩,本地服务器需要支持对应响应头,否则会卡在加载界面。简单起见,调试阶段可以在Player Settings > Publishing Settings里把压缩格式设为 Disabled,产物大一点但省去配置服务器的麻烦。

4.2 关键参数与配置项说明

配置项位置作用调整建议
透明 input 的 zIndexjslib控制叠层顺序需高于 Canvas,低于弹窗层
坐标同步频率C# Update决定跟随灵敏度每帧同步最稳,性能敏感可降频
全屏重算延迟jslib / C#规避过渡动画错位延迟 2 到 3 帧
输入框透明度jslib隐藏 DOM 元素保持 0,不要用 display none
压缩格式Player Settings影响加载调试期可关闭

这张表里最容易被忽略的是 zIndex。如果页面里还有自定义的加载遮罩或提示弹层,透明 input 的层级设错会导致它被盖住,表现就是“点了输入框没反应”,实际是焦点被上层元素拦截了。排查时用 DevTools 的 Elements 面板看元素层级最快。

4.3 二次开发时的改造入口

想把这个 Demo 用到自己的项目里,改造点集中在三处。第一处是输入框类型,Demo 通常只演示单行InputField,如果要支持多行或 TMP_InputField,需要把 C# 侧的组件引用换成对应类型,jslib 里的input元素改成textarea。第二处是事件回传,SendMessage依赖固定的 GameObject 名,接入现有项目时要改成自己的管理器命名,或者改用 C# 事件订阅的方式解耦。第三处是样式,透明 input 的字体大小、行高如果和 Unity 输入框不一致,光标位置会有细微偏差,调font-size和line-height对齐即可。

5. 避坑与排查:五条真机联调踩出来的经验

5.1 编辑器里正常、打包后输入框没反应

现象是编辑器内一切正常,WebGL 构建后点击输入框无任何反应。原因通常是条件编译没写对,[DllImport("__Internal")]在非 WebGL 平台会抛异常,如果没加#if UNITY_WEBGL && !UNITY_EDITOR,编辑器里走的是空实现或报错被吞掉,打包后反而暴露。解决是检查所有外部函数声明和调用点是否都被条件编译包裹,构建后打开浏览器控制台看有没有__Internal相关报错。

5.2 候选框位置上下颠倒或偏移

现象是中文输入时候选词框出现在输入框上方很远或下方很远。原因是 Unity 屏幕坐标与 DOM 坐标的 y 轴方向相反,漏了Screen.height - y的翻转,或者用了Input.mousePosition这类已经带偏移的坐标。解决是统一用RectTransformUtility.WorldToScreenPoint取角点,再手动翻转 y,并且确认 Canvas 的 Render Mode 是 Screen Space Overlay,其他模式下坐标换算还要考虑相机。

5.3 全屏后输入框消失

现象是点击全屏按钮后,输入框视觉上还在,但点击无响应、候选框不出现。原因是全屏切换后 DOM 布局变化,透明 input 的绝对坐标落在了可视区外,或者被全屏元素遮挡。解决是监听fullscreenchange重新同步坐标,并检查全屏容器的z-index和overflow设置,确保透明 input 挂在document.body而不是某个会被全屏替换的容器里。

5.4 中文上屏重复或丢字

现象是输入一段中文后,Unity 输入框里出现两遍,或者最后一个字丢失。原因是compositionend和input事件都触发了回传,两条路径没有去重;丢字则可能是compositionend触发时 Unity 侧还没准备好接收。解决是在 JS 侧用一个标志位区分 IME 输入和普通输入,compositionend后短暂忽略紧随的input事件,C# 侧的回调方法里做空值和重复值判断。

5.5 移动端软键盘弹出后页面错位

现象是在手机浏览器上,软键盘弹出把页面顶上去,输入框和候选框全部错位。原因是软键盘改变了视口高度,window.innerHeight变化但 Unity 的Screen.height没有及时更新。解决是监听resize和visualViewport的resize事件,重新触发坐标同步,必要时在软键盘弹出时临时调整 Canvas 尺寸。这块在 Demo 里未必覆盖完整,移动端项目要额外验证。

6. 二次开发进阶:把 Demo 改造成可复用的输入组件

把这份 Demo 直接塞进项目只能算跑通,要真正好用,得把它抽成一个可复用的组件。我的做法是定义一个IWebGLInputAdapter接口,把注册、注销、坐标同步、文本回传四个动作抽象出来,InputField和TMP_InputField各写一个实现,管理器只依赖接口。这样后续换 UI 框架时,桥接层不用动。

public interface IWebGLInputAdapter { int Id { get; } Rect GetScreenRect(); // 返回 DOM 坐标系下的矩形 void OnTextCommitted(string text); void OnFocusChanged(bool focused); }

管理器在OnIMECommit回调里根据 id 找到对应适配器,把文本分发过去,避免所有输入框共用一个静态回调导致的串台。验证方法是同时放三个输入框,快速切换焦点并输入中文,观察文本是否落到正确的框里、候选框是否跟随当前焦点。这个测试能暴露大部分 id 管理和焦点同步的问题。

另一个值得做的改造是坐标同步的节流。每帧调用UpdateInputRect在输入框多的时候会有性能开销,可以只在矩形实际变化时同步,用一个缓存的上次矩形做比较。改完之后在低端机上对比帧率,通常能看出差别。

还有一个容易翻车的点:全屏切换和输入框注册的时序。如果用户在全屏动画过程中点击输入框,注册时拿到的坐标可能是过渡态,候选框会偏一下再回正。我的处理是在注册后连续三帧强制同步一次,用一个小协程收尾。从那以后我每次接 WebGL 输入相关的需求,都会先在真机浏览器上把「输入中文、切全屏、再输入、滚动列表后输入」这四步走一遍,再谈功能开发。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表