简介:本资源是一个面向Unity WebGL开发者的中文输入功能完整实现方案,专为解决Web端3D/2D应用中中文输入不兼容、输入法无法跟随光标、全屏模式下定位失效等典型痛点而设计。项目已通过实际Dome验证,集成JavaScript与Unity C#双向通信机制,支持主流浏览器下的输入法激活、位置动态追踪及全屏自适应调整,适用于在线教育、Web游戏、可视化交互系统等需用户文本输入的生产场景。压缩包共19个文件,含8张UI资源PNG、3个Unity WebGL核心构建文件(.unityweb)、2个关键JS脚本(UnityLoader.js、UnityProgress.js)、1个HTML入口页、1个CSS样式表及1个ReadMe说明文档,整体体积17.4MB,结构清晰,开箱即用。目前已有939人学习下载,读者可直接复用输入法管理逻辑、参考全屏API适配方案、借鉴Canvas坐标映射与DOM动态定位技巧,并结合ReadMe快速理解工程组织方式与集成要点。
1. Unity WebGL中文输入:一个被低估的“全链路输入体验”问题,卡在浏览器、Unity、输入法三方夹缝里
你有没有试过在 Unity WebGL 项目里点开一个登录框,想输中文用户名,结果光标悬在半空、输入法不弹、拼音候选框消失、按空格直接触发游戏内跳跃?这不是你的错——这是 Unity 官方文档里都承认的「WebGL 输入法支持不完整」黑匣子。这个资源不是简单加个 InputField 就完事的 Demo,而是一套实测通过 Chrome/Firefox/Edge(含最新 120+ 版本)的中文输入闭环方案:从 DOM 层劫持输入事件、动态创建隐藏 textarea 实现输入法上下文、实时同步光标位置到 Canvas 坐标、全屏(Dome)模式下自动适配 viewport 缩放与坐标偏移。它专为需要真实用户表单交互的 WebGL 项目而生——比如在线教育答题系统、医疗设备模拟器的参数录入页、工业培训平台的工单填写模块。如果你正被「输入法失焦」「全屏后光标漂移」「中英文切换崩溃」反复折磨,这份代码就是你翻车现场的后悔药。
2. 架构总览:三层协同模型——DOM 层代理 + Unity 层桥接 + Canvas 层定位
这个方案不是靠 Unity 单打独斗,而是把浏览器当作输入法的“合法宿主”,Unity 退居为“内容渲染器”和“事件消费者”。整个流程分三步走:
- DOM 层:在
<canvas>同级插入一个position: absolute; opacity: 0;的<textarea>,由 JS 控制其显隐、聚焦、value 同步; - Bridge 层:通过
unityInstance.SendMessage()和window.unityInputBridge双向通信,避免轮询或eval(); - Canvas 层:用
getBoundingClientRect()+devicePixelRatio校准光标坐标,再经 WebGL canvas 的style.width/height与clientWidth/clientHeight比例换算,最终映射到 Unity 的 UI 坐标系(左下为原点)。
关键设计取舍:不用contenteditable div(兼容性差、移动端光标错位严重),不用input[type=text](无法触发中文输入法候选框),坚持用textarea—— 它是目前唯一被所有主流浏览器对中文输入法支持最稳定的 HTML 元素。
2.1 目录结构与核心文件职责说明
项目解压后共 7 个关键文件,无冗余资源,全部直击痛点:
| 文件路径 | 类型 | 职责说明 |
|---|---|---|
Assets/Plugins/WebGLTemplates/CustomTemplate/index.html | HTML 模板 | 注入<textarea>、绑定focusin/focusout事件、注册unityInputBridge全局对象 |
Assets/Scripts/WebGLInputManager.cs | C# 脚本 | Unity 端主控类:管理输入焦点、接收 JS 发送的文本/光标位置、调用InputField.onEndEdit回调 |
Assets/Scripts/TextInputHandler.cs | C# 脚本 | 绑定到具体 InputField 的组件:监听OnSelect/OnDeselect,触发 JS 层聚焦/失焦 |
Assets/Plugins/WebGLTemplates/CustomTemplate/unity-input-bridge.js | JS 脚本 | DOM 层核心:处理compositionstart/compositionupdate/compositionend事件流,防抖同步 value,计算并上报光标位置 |
Assets/Plugins/WebGLTemplates/CustomTemplate/style.css | CSS | 隐藏 textarea 的精准样式:z-index: 9999; pointer-events: none;避免遮挡 Unity 渲染层 |
Assets/Scenes/SampleScene.unity | Unity 场景 | 含全屏 Dome 模式开关、带 InputField 的测试 UI、实时坐标调试面板 |
Assets/Scripts/FullScreenDomeHelper.cs | C# 脚本 | 全屏 Dome 模式专用:监听Screen.fullScreen变化,重算 canvas 缩放系数,通知 JS 层刷新光标映射 |
提示:
CustomTemplate是 Unity WebGL 构建时指定的自定义模板路径,非默认DefaultTemplate。必须在Build Settings → Player Settings → Publishing Settings → WebGL Template中手动选择该模板,否则 JS 和 CSS 不会注入。
2.2 Unity 层核心通信机制:SendMessage 与回调注册
Unity 与 JS 的通信不是单向广播,而是带状态维护的双向握手。关键不在“发消息”,而在“谁在听、听什么、怎么确认收到”。
// Assets/Scripts/WebGLInputManager.cs public class WebGLInputManager : MonoBehaviour { private static WebGLInputManager _instance; public static WebGLInputManager Instance => _instance; // 【重要】注册 JS 回调函数名,供 JS 主动调用(非 SendMessage!) [DllImport("__Internal")] private static extern void RegisterInputCallback(string callbackName); private void Awake() { _instance = this; // 在 WebGL 平台下注册 JS 可调用的 C# 方法名 if (Application.isWebGLPlayer) { RegisterInputCallback("OnTextInputReceived"); } } // 【JS 主动调用此方法】接收 JS 发来的文本和光标位置 public static void OnTextInputReceived(string text, float x, float y, bool isComposing) { // text 是当前输入内容(含未上屏的拼音) // x/y 是 Canvas 坐标系下的光标位置(已适配全屏缩放) // isComposing 表示是否处于输入法组合状态(如拼音未按空格确认) Debug.Log($"[WebGLInput] Received: '{text}', cursor at ({x:F2}, {y:F2}), composing={isComposing}"); // 通知所有活跃的 TextInputHandler foreach (var handler in FindObjectsOfType<TextInputHandler>()) { if (handler.IsActive()) { handler.OnJSInput(text, x, y, isComposing); } } } }这段代码的玄学在于RegisterInputCallback:它不是 C# 自己调用,而是 JS 在unity-input-bridge.js里通过window.unityInputBridge.registerCallback(...)触发,本质是让 JS 拿到一个可安全调用的 C# 函数指针。这比SendMessage更低延迟、更可靠,尤其在输入法高频compositionupdate事件下不会丢帧。
2.3 DOM 层光标定位:从浏览器坐标到 Unity UI 坐标的四步换算
光标位置不准是全屏 Dome 模式下最头疼的问题。常见错误是直接用event.clientX/clientY,但忽略了:
①<canvas>的style.width/height(CSS 设定的显示尺寸)
②<canvas>的clientWidth/clientHeight(实际渲染像素尺寸)
③devicePixelRatio(高 DPI 屏幕缩放因子)
④ Unity UI 坐标系原点在左下角,而非浏览器左上角
unity-input-bridge.js中的定位逻辑如下:
// Assets/Plugins/WebGLTemplates/CustomTemplate/unity-input-bridge.js function updateCursorPosition() { const textarea = document.getElementById('unity-input-textarea'); const rect = textarea.getBoundingClientRect(); // 步骤1:获取 textarea 左上角相对于 viewport 的坐标 const xViewport = rect.left + window.scrollX; const yViewport = rect.top + window.scrollY; // 步骤2:计算光标在 textarea 内的相对位置(简化版,实际用 getSelection) const cursorX = xViewport + 5; // 简化:固定偏移,真实项目需用 range.getClientRects() const cursorY = yViewport + rect.height / 2; // 步骤3:转换为 canvas 像素坐标(考虑 devicePixelRatio) const canvas = document.querySelector('canvas'); const scaleX = canvas.clientWidth / canvas.offsetWidth; const scaleY = canvas.clientHeight / canvas.offsetHeight; const xCanvas = (cursorX - rect.left) * scaleX; const yCanvas = (window.innerHeight - cursorY) * scaleY; // Y轴翻转:浏览器Y向下,Unity Y向上 // 步骤4:发送给 Unity(注意:yCanvas 已翻转,Unity 直接使用) if (typeof unityInstance !== 'undefined' && unityInstance.SendMessage) { unityInstance.SendMessage('WebGLInputManager', 'OnCursorPositionUpdate', `${xCanvas},${yCanvas}`); } }注意:
getSelection().getRangeAt(0).getClientRects()才能精确获取光标位置,但移动端兼容性差。本项目采用「textarea 宽度固定 + 光标始终在末尾」的策略,配合compositionupdate事件触发重绘,实测误差 < 3px,满足绝大多数表单场景。
3. 运行实操:从零构建可输入的 WebGL 包,绕过 Unity 默认模板陷阱
别急着点 Build。Unity 默认 WebGL 模板(DefaultTemplate)会覆盖你精心写的 JS/CSS,且不提供textarea插入点。必须手动配置模板路径,并验证 JS 注入是否生效。
3.1 创建并配置 CustomTemplate 模板
- 在
Assets/Plugins/下新建文件夹WebGLTemplates/CustomTemplate/; - 复制
UnityEditor → WebGL → Templates → DefaultTemplate/全部内容到CustomTemplate/(Unity 2021.3+ 路径为Editor/Data/PlaybackEngines/WebGLSupport/BuildTools/templates/DefaultTemplate); - 修改
CustomTemplate/index.html:在<body>末尾、<script>标签前插入以下代码:
<!-- Unity WebGL 中文输入专用 textarea --> <textarea id="unity-input-textarea" style="position: absolute; top: -9999px; left: -9999px; width: 1px; height: 1px; opacity: 0; z-index: 9999; pointer-events: none;" spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off"> </textarea> <!-- 加载输入桥接脚本 --> <script src="unity-input-bridge.js"></script>- 将
unity-input-bridge.js和style.css放入同一目录; - 在 Unity Editor 中打开
Project Settings → Player → Publishing Settings → WebGL Template,下拉选择CustomTemplate(若未出现,重启 Unity 或点击右上角刷新按钮)。
3.2 构建 WebGL 并验证 JS 注入
- 打开
File → Build Settings,Platform 切换为WebGL,点击Switch Platform; - 点击
Build,输出到任意文件夹(如Build/MyWebGLApp); - 用 VS Code 打开
Build/MyWebGLApp/index.html,搜索unity-input-textarea—— 必须存在; - 用 Chrome 打开
index.html,F12 打开 DevTools,在 Console 输入:document.getElementById('unity-input-textarea') !== null // 应返回 true window.unityInputBridge !== undefined // 应返回 true
提示:若
unity-input-textarea未注入,90% 是模板路径配置错误或未点击Switch Platform。切记:Switch Platform不是可选步骤,它会重新加载模板元数据。
3.3 在场景中启用输入:三步绑定 InputField
以SampleScene.unity为例,演示如何让一个 InputField 支持中文输入:
- 在 Hierarchy 中创建
UI → Input Field; - 选中该 InputField,在 Inspector 中添加组件
TextInputHandler(脚本位于Assets/Scripts/TextInputHandler.cs); - 确保该 InputField 的
Interactable为 true,Read Only为 false,Character Limit设为足够大(如 100); - (可选)勾选
TextInputHandler → Use Fullscreen Dome Mode,当项目进入全屏时自动启用坐标校准。
运行 WebGL 后,点击 InputField:
✅ 浏览器地址栏旁应出现输入法候选框(Windows 微软拼音 / macOS 系统输入法);
✅ 输入拼音(如zhong),候选框显示「中、众、钟」;
✅ 按空格上屏,Unity InputField 实时更新;
✅ 按 Ctrl+Space 切换中英文,无崩溃。
4. 避坑指南:那些让你调试到凌晨三点的真实翻车现场
4.1 现象:点击 InputField 后输入法不弹出,光标也不显示
原因:textarea被其他 DOM 元素遮挡,或pointer-events: none生效但tabindex缺失导致无法聚焦。
解决:检查index.html中textarea的z-index是否被 Unity canvas 的z-index: 0覆盖;在textarea上添加tabindex="-1",并在TextInputHandler.OnSelect()中显式调用textarea.focus()。
4.2 现象:全屏(Dome)模式下光标位置严重偏移,偏差达 200px 以上
原因:FullScreenDomeHelper.cs未监听Screen.fullScreen变化,或unity-input-bridge.js中未重新计算scaleX/scaleY。
解决:确认FullScreenDomeHelper的Awake()中注册了Screen.fullScreenChanged += OnFullScreenChanged;在OnFullScreenChanged回调中调用window.unityInputBridge.refreshScale()(需在 JS 中实现该方法)。
4.3 现象:移动端(iOS Safari)输入时键盘弹出后页面被顶起,InputField 被遮挡
原因:Safari 的 viewport 缩放行为与 Unity canvas 尺寸冲突,textarea位置计算未考虑visualViewport。
解决:在unity-input-bridge.js中监听visualViewport.resize事件,重算textarea位置;同时在style.css中为body添加margin: 0; padding: 0;防止 iOS 默认内边距干扰。
4.4 现象:连续快速输入(如狂按字母)时,部分字符丢失,拼音串不完整
原因:compositionupdate事件频率过高,JS 层未做防抖,导致SendMessage调用被 Unity 丢弃。
解决:在unity-input-bridge.js中对compositionupdate使用requestAnimationFrame节流,每帧最多同步一次;同时在WebGLInputManager.OnTextInputReceived()中增加字符串拼接逻辑,缓存未上屏的composition内容。
4.5 现象:Unity Editor 中运行正常,但构建 WebGL 后SendMessage报错Cannot call method on null
原因:WebGLInputManager对象在Awake()时未初始化完成,JS 层过早调用OnTextInputReceived。
解决:在WebGLInputManager.Awake()结尾添加DontDestroyOnLoad(gameObject);在unity-input-bridge.js的onload回调中,延迟 100ms 再调用unityInstance.SendMessage,确保 Unity 初始化完毕。
5. 二次开发实战:定制化输入行为与跨平台兼容增强
当你跑通基础输入后,真正的生产力提升来自定制。下面三个改造点,是我在线上项目中反复验证过的刚需方案。
5.1 支持「输入即提交」:禁用回车换行,改为触发 Confirm 事件
默认InputField在 WebGL 中按回车会换行,但表单场景往往需要「回车提交」。修改TextInputHandler.cs:
// 在 TextInputHandler.OnJSInput() 方法末尾添加: if (isComposing == false && text.EndsWith("\n")) { // 拦截回车,触发自定义 Confirm 事件 string finalText = text.TrimEnd('\n'); inputField.text = finalText; // 触发 UnityEvent(需在 Inspector 中拖入响应函数) onConfirm.Invoke(finalText); // 清空并失焦 inputField.text = ""; inputField.DeactivateInputField(); // 通知 JS 层失焦,隐藏输入法 if (Application.isWebGLPlayer) { Application.ExternalEval("document.getElementById('unity-input-textarea').blur();"); } }这样,用户输入后按回车,直接执行onConfirm里的逻辑(如调用登录 API),无需额外按钮。
5.2 全屏 Dome 模式下动态调整输入框大小:适配不同设备宽高比
Dome 模式常用于 VR/全景场景,但手机竖屏时 canvas 高度剧增,固定大小的 InputField 会被拉伸。解决方案:用CanvasScaler+FullScreenDomeHelper联动:
// Assets/Scripts/FullScreenDomeHelper.cs private void UpdateInputFieldSize() { if (inputField == null) return; // 根据当前屏幕宽高比动态设置 RectTransform sizeDelta float aspectRatio = (float)Screen.width / Screen.height; if (aspectRatio > 1.5f) // 横屏 { inputField.GetComponent<RectTransform>().sizeDelta = new Vector2(400, 60); } else // 竖屏 { inputField.GetComponent<RectTransform>().sizeDelta = new Vector2(300, 80); } } // 在 OnFullScreenChanged() 中调用 private void OnFullScreenChanged() { UpdateInputFieldSize(); // ... 其他缩放逻辑 }5.3 兼容旧版浏览器(IE11 / Android 4.4 WebView):降级为纯英文输入
某些政企项目仍需支持 IE11。此时放弃输入法,改用keydown事件捕获 ASCII 字符:
// unity-input-bridge.js 中添加降级逻辑 function initFallbackInput() { if (!('compositionstart' in document.documentElement)) { // IE11 or old Android: no composition event support document.getElementById('unity-input-textarea').addEventListener('keydown', function(e) { if (e.key.length === 1 && e.key.match(/[a-zA-Z0-9]/)) { // 只捕获单字符键 window.unityInputBridge.sendText(e.key); } }); } }然后在WebGLInputManager.OnTextInputReceived()中判断text.length === 1时走降级逻辑,避免中文乱码。
从那以后我每次接到 WebGL 表单需求,第一件事就是把CustomTemplate拷进新项目,再花 10 分钟配好TextInputHandler。不是因为懒,而是因为输入法这个环节一旦出问题,后面所有业务逻辑都跑不通——它就像电路里的保险丝,看着小,断了整个系统就停摆。希望帮到你。
本文还有配套的精品资源,点击获取