Unity WebGL中文输入法跟随与全屏支持完整方案
2026/9/8 8:20:42 网站建设 项目流程

简介:面向Unity WebGL开发者的中文输入与输入法跟随Demo资源,直接解决Web端Unity应用输入中文不便、输入法弹窗位置错乱、全屏状态下面板无法跟随等常见问题。资源包共19个文件,压缩后大小约17.4MB,其中包含HTML入口文件、JavaScript交互逻辑、CSS样式表、Unity WebGL构建数据(.unityweb格式)及多张UI预览图,方便了解页面加载到构建运行的整体过程。当前已有934人学习下载,适合希望快速补齐WebGL中文输入能力的中高级Unity开发者参考。通过研究该Demo,可以掌握Unity C#脚本与浏览器JavaScript之间的通信方式,理解输入法跟随功能如何利用浏览器事件动态调整输入法窗口位置。同时,项目中对全屏模式切换、输入法样式定制、浏览器兼容性及异常处理均有涉及,能帮助开发者在实际项目中减少踩坑,提升WebGL应用的中文输入体验。 不算少见,你在网上搜“Unity WebGL 中文输入”,能找到一大堆人卡在同一道坎上:浏览器里输入框一聚焦,英文敲得飞快,一切到中文输入法,拼音倒是出来了,候选框却不知道飘到哪里去,或者干脆输不进去。等项目接上全屏需求的瞬间,问题更上头——输入法弹窗飞到了屏幕角落,Unity 这边的输入框和系统输入法完全对不上号。标题里的“Dome”应该是“Demo”的手误,但这三个痛点是真的:Unity WebGL 中文输入、输入法跟随、全屏支持,属于做网页端 Unity 项目绕不开的硬骨头。这篇文章就把我从踩坑到落地的一套方案完整拆开讲,内容包括底层原理、jslib 注入、坐标换算、全屏 API 联动,以及我在真实项目里记录下的高频问题和排查思路。无论你是刚转 WebGL 的新手,还是已经在 H5 游戏里被输入法折磨过几轮的开发者,这篇都值得你花十分钟读完再收藏。

1. 先把问题说清楚:Unity WebGL 中文输入到底难在哪

1.1 输入的底层机制:Unity 的“假输入框”和浏览器的“真输入法”

很多人第一次在 Unity WebGL 里做输入时都会愣一下:Unity 编辑器里跑得好好的 InputField,怎么到了浏览器里就只能敲英文了?要理解这个问题,得先搞清楚 Unity WebGL 里的输入框本质是什么。

Unity 编辑器或者桌面平台上,InputField 是引擎内部直接处理的 UI 控件,系统输入法会把候选窗口直接挂到原生窗口上,所以中文输入天然可用。但 WebGL 构建出来的 Unity 应用跑在浏览器里,引擎的 UI 画在 WebGL Canvas 上,它其实是一个被浏览器当成“一块画布”的东西。InputField 显示出来的光标、文字全是引擎自己画上去的,不是一个真正的 HTML<input>元素,浏览器输入法根本不认识它。

这里的关键在于输入法的工作流程。中文输入法的核心是 composition(组合输入):你按下拼音字母,输入法进入预编辑状态,候选框出现,选完字才触发真正的 input 事件提交文本。这个过程中,composition 事件是由浏览器在“能承受 IME 的输入框元素”上分发的。Unity 的 Canvas 不能接收 IME 的 composition 事件,所以你在 Unity 的 WebGL 输入框里打拼音,浏览器不知道把候选框放哪,Unity 更拿不到组合过程中的文本,最后的表现就是中文根本进不去,或者进去一团乱码。

1.2 三个需求其实是一条链路:输入、跟随、全屏不能分开做

很多教程会把这问题拆成三篇讲:先是中文输入怎么处理,再讲输入法跟随怎么调,最后聊全屏支持。实际做项目你就会发现,这三件事根本拆不开——它们共享同一套坐标体系和事件链路。

举个很现实的例子:你给 Unity 的 Canvas 上盖了一层透明的 HTML input 来接输入法的 composition 事件,位置也对准了 Unity 输入框。这时候用户点了全屏按钮,Unity 的 Canvas 尺寸瞬间变化,你那层 HTML input 如果还用原来的 left/top,下一秒就会偏到十万八千里。输入法候选框或者你的透明输入层一旦位置偏移,用户每敲一个字都要怀疑自己是不是打错了位置。

所以在方案设计的第一天,我就要求自己把这三件事放到一起考虑:输入层跟随 Unity 输入框的坐标,而坐标换算的基准是当前 canvas 在浏览器视口中的 CSS 位置和尺寸;全屏事件发生后,这个基准必须重新计算。所有逻辑都围绕“坐标映射”这一根线展开,后面每个环节都会反复遇到它。

2. 方案选型:为什么我放弃插件,用原生覆盖层

2.1 现成插件的坑

聊中文输入,网上第一个跳出来的就是 Unity 官方维护的 webgl-input-plugins,或者各种第三方输入的 JS 插件。我一开始也图省事直接用,结果在真实项目里被坑了好几次。

官方插件的问题不是不能用,而是覆盖不到所有场景。它对中文输入的默认处理策略是“拦截 InputField 的焦点事件,再把文本注入回 Unity”,这个思路没错,但当你想要定制输入法跟随候选框位置、或者把它嵌套进 iframe 播放页、或者对接抖音/微信小程序的 webview 时,插件的封装反而成了阻碍。很多这类插件为了兼容性,位置计算用的是 fixed 定位,一旦全屏或者外层页面滚动,定位立刻失效。

第三方插件更不可控,有些已经几年不维护,和最新的 Unity 版本构建出来的 JS 模块交互时会直接报_unityInstance is not defined或者Module.ccall is not a function之类的错,排查成本比自己做一套还高。我后来想通了:中文输入这套东西本质上就是在和浏览器 API 打交道,不如直接写一层轻量 JS 嵌入 Unity WebGL 模板,把控制权握在自己手里。

2.2 覆盖层方案的总体思路

我最终采用的思路可以概括为一句话:在 Unity 的 canvas 上方盖一个透明、绝对定位的 HTML<input>,接管输入法事件,拿到中文文本后再回填给 Unity 的 InputField。

这个透明输入层只在 Unity 输入框处于激活状态时出现,平时pointer-events设为none,绝不干扰游戏操作。一旦 InputField 通过点击获得焦点,JS 侧就把这个透明 input 显示出来并让它聚焦,用户的所有键盘输入、输入法组合、候选框弹出,全部发生在这个真实的 HTML 元素上。当汉字被选中、composition 结束,JS 捕获最终文本,通过 Unity 对外暴露的接口注入回引擎侧,随后隐藏透明层,让 Unity 继续作为 UI 的主人。

这套方案的优势很明显:输入法候选框的跟随位置天然正确,因为浏览器会为真实 input 元素自动调整候选框;中英文切换完全交给系统输入法处理,不用在 JS 里做语言判断;遇到全屏、滚动、iframe 缩放这些场景时,我可以自己控制透明层的定位策略,不会被插件逻辑卡死。当然代价也很直接——所有坐标换算都要自己写,这就是下面几节要讲的核心内容。

3. 一步一步实现中文输入支持

3.1 jslib 注入:给 Unity 安一个“外部输入通道”

Unity WebGL 的构建产物是一个 HTML5 应用,你要在 C# 和浏览器 JS 之间通信,标准做法是写一个.jslib文件放在Assets/Plugins/WebGL目录下。Unity 会自动把它合并进最终构建的 JS 模块里,C# 侧通过[DllImport("__Internal")]声明外部方法即可调用。

我先给一个最小可用的 jslib 骨架,里面包含三个基础能力:绑定透明输入元素、控制显隐、获取输入内容。

var IMEBridge = { // 初始化:创建透明 input,挂到 canvas 的父节点下 BindInputElement: function (canvasId) { var canvas = document.getElementById(canvasId); if (!canvas) return; container = canvas.parentNode; overlayInput = document.createElement('input'); overlayInput.type = 'text'; overlayInput.style.position = 'absolute'; overlayInput.style.opacity = '0'; // 透明但不隐藏,隐藏会导致输入法驱逐 overlayInput.style.pointerEvents = 'none'; overlayInput.autocomplete = 'off'; overlayInput.setAttribute('autocorrect', 'off'); overlayInput.setAttribute('autocapitalize', 'off'); overlayInput.setAttribute('spellcheck', 'false'); container.appendChild(overlayInput); overlayInput.addEventListener('compositionstart', function () { composing = true; }); overlayInput.addEventListener('compositionend', function (e) { composing = false; onCompositionEnd(e.target.value); }); overlayInput.addEventListener('input', function (e) { if (!composing) onCompositionEnd(e.target.value); }); }, // 让透明输入层获得焦点 FocusOverlay: function () { if (overlayInput) overlayInput.focus(); }, // 清空并隐藏 ClearOverlay: function () { if (overlayInput) overlayInput.value = ''; if (overlayInput) overlayInput.style.display = 'none'; } }; mergeInto(LibraryManager.library, IMEBridge);

mergeInto是 Unity 默认的 jslib 合并方式,这个写法在 2020 到 6000.x 系列构建里都能用。jslib 文件本质就是普通 JS,但要注意LibraryManager.library这个内部对象是 Unity 构建系统约定好的,不要动它,只往里挂你的方法就行。

3.2 C# 侧的事件绑定与回填

jslib 写完,C# 侧要做的就是三件事:加载时绑定、输入框聚焦时唤起、JS 回传文本时写入。

using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; public class WebGLInputBridge : MonoBehaviour { [DllImport("__Internal")] private static extern void BindInputElement(string canvasId); [DllImport("__Internal")] private static extern void FocusOverlay(); [DllImport("__Internal")] private static extern void ClearOverlay(); [DllImport("__Internal")] private static extern string GetOverlayValue(); private InputField currentInput; private void Start() { #if UNITY_WEBGL && !UNITY_EDITOR BindInputElement(GetCanvasId()); #endif } public void OnInputFieldFocused(InputField field) { currentInput = field; #if UNITY_WEBGL && !UNITY_EDITOR FocusOverlay(); #endif } public void OnInputFieldExit(InputField field) { currentInput = null; #if UNITY_WEBGL && !UNITY_EDITOR ClearOverlay(); #endif } // JS 侧通过 SendMessage 回调这个方法 public void ReceiveIMEComposition(string text) { if (currentInput == null) return; currentInput.text = text; currentInput.caretPosition = text.Length; } }

这里我特意把OnInputFieldExit也做好了:点击 Unity 输入框之外的地方,要立刻隐藏透明层并把焦点还给 Canvas,否则用户想用键盘 WASD 操作游戏,按下去全敲进了透明 input 里,游戏直接卡死,这是新手最容易被偷袭的坑。

3.3 叠加层与 Unity 输入框的联动逻辑

代码写完,联动逻辑是决定好不好用的关键。我的建议是不要只要看到 Unity 的 InputField 获得焦点就无脑唤起透明层,而是加一个开关:只有标记为“允许中文输入”的输入框才唤起覆盖层,比如只对昵称、聊天、搜索框开启;对玩家输入的密码、ID 等非中文场景,让 Unity 原生处理就行,这样能少很多兼容性问题。

还有一个细节值得单独说:透明层的display属性不要用none来隐藏。display: none会让元素彻底脱离渲染树,如果用户切换到其它窗口再切回来,浏览器可能会忘记之前的状态,再聚焦时输入法没法被正确唤起。更稳妥的做法是用visibility: hiddenopacity: 0,同时让pointer-events: none,这样元素虽然在视觉上不可见,但它始终在文档流里,聚焦和输入法唤起行为都正常。这个细节是我在某次做到一半时突然发现“怎么输入法偶尔弹不出来了”,追了半天才定位到的。

4. 输入法跟随:坐标换算和 composition 处理

4.1 屏幕坐标到 CSS 坐标的换算

透明输入层能不能精准覆盖住 Unity 里的那个 InputField,本质是坐标换算问题。Unity 的 UI 坐标系以左下角为原点,Canvas 采用 Screen Space - Overlay 模式时,RectTransform.position给的是世界坐标,数值上等于屏幕像素坐标。而浏览器 DOM 的坐标以左上角为原点,所以 y 轴必须翻转。

先说最简单的情况:Unity 的 Game 视图就是浏览器里 canvas 的完整显示区,没有额外 DOM 遮挡,scale 为 1,设备像素比 devicePixelRatio 只有 1。此时换算公式是:

cssX = unityX cssY = containerHeight - unityY - inputHeight

其中containerHeight是 canvas 父容器的 CSS 高度,inputHeight是透明输入层自身的高度,减掉它是因为 input 的定位用的是左上角,而我们拿到的是 Unity 输入框矩形底边的位置。

真实项目里 canvas 会被 CSS 缩放,所以我在 jslib 里维护一组缓存变量,每次换算前先实时读取canvas.getBoundingClientRect(),把 Unity 的屏幕坐标除以 canvas 宽度、再乘以当前 CSS 显示宽度,避免硬编码。给出完整换算代码大概长这样:

function getCanvasRect() { var rect = canvas.getBoundingClientRect(); return { left: rect.left, top: rect.top, width: rect.width, height: rect.height, scaleX: rect.width / canvas.width, scaleY: rect.height / canvas.height }; } function setOverlayPosition(unityX, unityY, unityWidth, unityHeight) { var r = getCanvasRect(); var cssX = r.left + unityX * r.scaleX; var cssTop = r.top + (canvas.height - unityY - unityHeight) * r.scaleY; overlayInput.style.left = cssX + 'px'; overlayInput.style.top = cssTop + 'px'; overlayInput.style.width = unityWidth * r.scaleX + 'px'; overlayInput.style.height = unityHeight * r.scaleY + 'px'; }

注意这里用的是canvas.width而不是canvas.clientWidthcanvas.width是 Unity 构建时设置的绘图缓冲区宽高,和引擎内的逻辑分辨率保持一致;canvas.clientWidth是 CSS 显示宽高,两者在页面被缩放时不一样。我见过有人拿后者做分母,结果透明层永远对不准 Unity 输入框,一查发现就是混用了两个宽度,这是最容易犯的低级错误。

4.2 光标级跟随:别只跟输入框左上角

输入法跟随做到“输入框位置对”只是及格,要做到“候选框跟着光标走”才算优秀。如果透明层只是压在输入框上,你在输入框中间打字,输入法的候选框仍然会默认弹在透明层左上角,视觉上就会错位。

要做到光标级跟随,Unity 侧需要给出当前光标(caret)所在位置的字符坐标。Unity 的TextGenerator可以拿到字符矩形,我封装了一个 C# 方法,在每次 InputField 内容变化或光标移动时,把光标对应的屏幕坐标传给 JS:

private Vector2 GetCaretScreenPosition() { if (currentInput == null) return Vector2.zero; var textGen = currentInput.textComponent.cachedTextGeneratorForLayout; int caret = currentInput.caretPosition; if (textGen.characterCount > 0 && caret < textGen.characterCount) { var charRect = textGen.charactersVisible[caret].topLeft; float x = currentInput.textComponent.transform.position.x + charRect.x; float y = currentInput.textComponent.transform.position.y + charRect.y; return new Vector2(x, y); } return currentInput.textComponent.transform.position; }

这是进阶写法,如果你的功能优先级不高,也可以用简化版:把透明层定到输入框左下角,把 input 字号调成和 Unity 输入框相近,这样候选框即使偏一点也还能忍。但一旦上了全屏,2 个像素的偏移都会被缩放放大成明显错位,所以我还是建议把光标级跟随做完整。JS 侧在composition事件的 diaglog 触发时实时调用setOverlayPosition即可。

4.3 密码框、多行框、TMP 的特殊处理

输入框形态复杂时,这套方案要把特殊情况逐磨透。密码框需要把overlayInput.type改成'password',否则用户输密码,浏览器自动带上“明文预览”和“保存密码”的鸡肋提示,安全上的体验也很减分。多行输入框则应该把透明层改成<textarea>,同时高度调成撑满输入框,否则长段内容换行后全挤在一行。

TextMeshPro 的处理也要单独留意。TMP 和 UI.Text 的文本生成器 API 不同,用cachedTextGeneratorForLayout时需要确认你引用了正确的类型,而且 TMP 对富文本标签<color>这类内容也会渲染到字符列表中,取值之前最好调用TMP_TextUtilities.GetCursorIndexFromPosition这类方法做一次坐标反查。

不管哪种形态,透明输入层的字体样式尽量和 Unity 输入框保持一致,特别是font-sizeline-height,不然输入法候选框的参考位置会不同,就会有“文字输入进去之后显示偏上一个像素”的诡异感觉,这种问题通常很难排查,因为肉眼几乎看不出但截图对比就能发现。

5. 全屏支持:不是点两下 API 那么简单

5.1 全屏 API 的正确姿势

浏览器全屏的 API 本身并不复杂:canvas.requestFullscreen()让 canvas 进入全屏,document.exitFullscreen()退出,document.fullscreenchange监听状态变化。但放到 Unity WebGL 场景里,有三个坑必须提前处理。

第一个坑是手势限制。浏览器出于安全策略,requestFullscreen()必须由用户的点击、触摸等手势事件直接触发,如果你在 Unity C# 侧通过UnityAction回调、或者在异步延时之后再调用 JS 的全屏方法,浏览器会把请求视为“非手势触发”,直接拒绝。解决办法是:Unity 侧只负责标记“用户想全屏”,真正的全屏调用要在 JS 侧的手势事件回调里发起,或者至少让 JS 方法在同一个调用栈内完成requestFullscreen()

第二个坑是全屏元素的选择。如果你调用的是document.documentElement.requestFullscreen(),整个页面包括 Unity 的 loading bar 都会进全屏,副作用是页面布局可能塌掉。更推荐直接让 Unity 的 canvas 元素全屏,这样它自动铺满屏幕,Unity 的 Game 视图就正好占满全屏。不过注意,如果 canvas 外面还有其他 UI 元素,比如聊天框、角落的 log,它们就看不到了,遇到这种项目可以把外层的容器元素拿去全屏。

第三个坑是 iframe。这点在抖音、微信小程序 webview 或第三方页游平台里尤其常见:iframe 内的 canvas 想全屏,父页面必须在 iframe 标签上加上allow="fullscreen",否则你去调requestFullscreen(),控制台会安静地报一个Fullscreen permission denied,用户体验就是“点了全屏键没反应”。这种问题不在自己的代码里,但排查起来特别费时。

5.2 全屏后输入法跟随的联动调整

全屏后 canvas 的尺寸从浏览器页面的几百像素突然变成整个屏幕,透明输入层的坐标基准也彻底变了。在 jslib 里监听fullscreenchange事件,把之前缓存的 canvas 尺寸强制清空,下次打开输入框时重新走一遍getCanvasRect()就能自动适配新尺寸。这个操作听着简单,但一定要记得触发一次重新定位,否则上次留下的透明层位置在全屏后的第一帧还是旧的,用户如果在这个瞬间点了一下输入框,候选框会从屏幕角落闪一下再跳到正确位置,体验很违和。

我在实际项目里还在全屏切换后干了另一件事:强制把透明层隐藏一帧,然后在下一次 Unity 聚焦输入框时再唤醒。理由是 WebGL 在全屏切换的瞬间,浏览器会把 canvas 内部 buffer 重建,这个过程中如果透明层还在 DOM 上聚焦,个别浏览器会候选框闪烁甚至卡住,隐藏再唤醒可以在视觉上规避这个问题。这个策略不一定对所有人的项目都有必要,但保持输入层状态干净是没错的。

5.3 DPR 与性能:别让全屏变成幻灯片

全屏后 canvas 的尺寸变成屏幕分辨率,高性能显卡的机器上还好,集成显卡或手机上,画一个大分辨率的 WebGL 场景就是灾难,帧率会直线掉。这里有两个手段配合使用。

一个是设备像素比(devicePixelRatio)的处理。Unity WebGL 默认会按 CSS 像素和目标 DPR 来设置 canvas buffer,如果你的项目对画质要求不是特别高,我建议在全屏状态下手动把 DPR 上限设为 1.5 甚至 1,对应到 Unity 里就是在 C# 侧调用Screen.SetResolution(width, height, false)降分辨率,必要时把androidApplication相关的质量档位同步调低。另一个是锁定帧率,Unity WebGL 没有直接暴力的Application.targetFrameRate完全锁死整个 pipeline,但你可以在 C# 侧每隔一段时间检查canvas.width的变化,在全屏状态下做一次性能采样,超过阈值就把GetComponent<Camera>().allowHDR关掉或者降一下后处理特效。这和输入法本身没有直接关系,但全屏体验是完整功能的一部分,如果全屏后卡到输入法弹窗都要 3 秒才响应,那用户的第一反应一定是你的功能做坏了。

6. 常见问题排查实录

6.1 高频问题与解决方案

下面表格里是我在这套方案落地过程中真实遇到过的坑,按照出现频率从高到低排列。

症状根因解决办法
中文输入后 Unity 输入框乱码JS 回传时文本编码不一致,C# 侧按 UTF-8 读取jslib 里直接用字符串,不要在 JS 里手动转编码;接收端统一用Application.ExternalCallSendMessage传字符串
输入法候选框闪到屏幕左下角透明 input 没有正确聚焦,或坐标没有更新检查FocusOverlay是否被调用;绑定聚焦事件时用input.focus()并主动preventDefault掉 Unity 的 Click
全屏后透明层位置偏了fullscreenchange没触发重定位在事件里强制置空缓存坐标,下一次定位时重新取getBoundingClientRect()
iOS Safari 里输入法组合结束不触发compositionend部分 iOS 版本单指拼音进入候选状态会比较特殊在 blur 事件里兜底,把当前的overlayInput.value全部提交给 Unity
微信内置 webview 里虚拟键盘把透明层顶到屏幕外虚拟键盘弹出后 visualViewport 高度变化监听window.visualViewport的 resize 事件,重算透明层位置并适当上移
iframe 嵌套项目全屏无响应iframe 缺少allow="fullscreen"属性让父页面在 iframe 标签上加allow="fullscreen",并确认requestFullscreen是接到 canvas 上

6.2 几个容易被忽略的细节

再补充几个不容易在测试阶段发现、但上线后一定会被用户骂的细节。

透明输入层的opacity: 0在个别浏览器上会连 caret 光标一起隐藏,导致用户不知道焦点在哪。我的做法是不把 opacity 设为 0,而是设成一个极小值比如0.001,视觉上已经不可见,但浏览器的 IME 光标仍然存在,输入法候选框也不会因为元素“不可见”而产生异常行为。这个技巧看起来有点取巧,实测下来非常稳。

如果你的项目要同时嵌入多个 Unity 实例,那么 jslib 里的全局变量就是灾难。每个实例的 canvas、overlayInput 都要按实例 ID 或者 canvasId 存进一个 map 里,事件回调也要带上实例标识。我在做一个计算器 H5 和一个小游戏同页展示时就被狠狠坑过一次,输入法文本从 A 实例串到了 B 实例的输入框,排查了很久才意识到全局变量被第二个绑定方法覆盖了。

最后提醒一下,不要想着把这套 DOM 覆盖层方案直接套到微信小游戏上。微信小游戏环境没有真正的 DOM 和 input 元素,只能用输入法适配插件或者自绘键盘,方案完全是另一个体系。曾有同事把 WebGL 版的 JS 插件原封不动搬过去,构建后运行直接报错,因为document.createElement在小游戏运行时环境里是不存在的。做不同平台,方案要重新评估。

说实话,Unity WebGL 的中文输入、输入法跟随和全屏支持,单独拆开看每一项都不算难,但把它们组合到真实项目里,要考虑的边界情况真的很多。我个人在做完这一整套方案后最大的体会是:不要怕自己造轮子,尤其是这种跨引擎和浏览器边界的场景,理解了底层机制,你才能在任何平台、任何浏览器、任何诡异的 WebView 环境下临危不乱。上面这些代码和排查记录是我在多个项目里反复验证过的,直接拿去用就行,遇到新问题再回来对照坐标换算和事件处理的思路,基本都能找到突破口。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询