2026/11/4 12:20:54

Unity WebGL中文输入实战:JS桥接与全屏适配方案

Unity WebGL中文输入实战:JS桥接与全屏适配方案 简介这份资源面向需要在 Unity WebGL 项目中实现中文文本输入的开发者重点解决浏览器环境下输入法无法唤起、输入框不跟随光标以及全屏模式下定位错乱等常见问题。压缩包共 19 个文件约 17.4MB包含 html 入口页、css 样式、js 交互脚本、unityweb 构建产物以及 png 图标素材等结构上覆盖了 WebGL 构建输出与页面模板的完整组成。已有 940 人学习下载说明该方案在同类需求中具备一定参考价值。资源通过 JavaScript 与 Unity C# 的相互调用激活浏览器输入法并借助鼠标事件动态调整输入法窗口位置使光标移动时输入框能够同步跟随同时兼顾全屏切换后的定位更新。读者可据此理解 JSIL 通信机制、输入法兼容性处理与延迟加载思路并参考其中的错误处理与样式定制方式快速搭建可运行的中文输入示例工程。1. 需求先立住Unity WebGL 里那个让人抓狂的中文输入框做过 Unity WebGL 项目的人大概都遇到过同一个场景游戏在浏览器里跑起来了英文输入一切正常可一旦玩家切到中文输入法输入框要么完全没反应要么字符乱码要么输入法候选框飘在屏幕左上角跟输入位置对不上。更难受的是全屏之后输入法候选框直接跑到屏幕外面玩家根本看不见自己在打什么。这不是玄学是 WebGL 平台下 Unity 的输入事件和浏览器原生输入法之间的桥接问题。这份方案要解决的就是三件事让 WebGL 构建里能正常输入中文、让输入法候选框跟随输入位置、让全屏状态下输入依然可用。适合正在做 WebGL 项目、需要交付一个能真正跑通中文输入 Demo 的开发者也适合拿它当课程设计或毕设里「跨平台输入适配」模块的参考实现。2. 技术选型与总体设计为什么不能只靠 Input Field2.1 原生 Input Field 在 WebGL 下的真实短板Unity 自带的 Input Field 组件在编辑器里和桌面平台表现良好但 WebGL 构建后它的输入依赖 Unity 自己维护的隐藏输入框和浏览器事件的映射。问题出在中文输入法的组合输入阶段输入法在选词之前会有一串「组合中」的字符这部分内容 Unity 的默认输入通道拿不到完整状态于是出现候选词丢失、上屏顺序错乱。全屏模式下更麻烦浏览器全屏后 Unity 画布尺寸变化隐藏输入框的位置没有同步更新输入法候选框就失去了锚点。我一般会先明确一个判断这不是靠调 Input Field 的某个属性就能修好的问题需要在浏览器层接管输入事件再把结果回传给 Unity。也就是说方案的核心是「JS 侧输入代理 Unity 侧接收渲染」的双向通道而不是在 C# 里硬扛。2.2 三层结构页面层、桥接层、表现层整套方案按职责拆成三层边界清晰后面排查问题也容易定位。层级运行位置核心职责关键产出页面层浏览器 DOM承载真实 input 元素、监听 composition 事件、处理全屏切换组合输入文本、上屏文本、光标坐标桥接层Unity WebGL 模板 JS把 DOM 事件转成 Unity 可调用的消息把 Unity 的请求转成 DOM 操作SendMessage 调用、坐标换算表现层Unity C#接收文本、驱动 UI 显示、请求聚焦与失焦、同步输入框位置输入框视觉、光标、候选框锚点页面层用的是一个真实挂载在 DOM 上的 input 元素而不是完全依赖 Unity 的隐藏输入框。这个 input 默认透明、尺寸跟随 Unity 输入框的屏幕位置玩家看到的是 Unity 渲染的输入框实际敲键盘的是这个 DOM 元素。桥接层负责把 compositionstart、compositionupdate、compositionend 三个事件里的文本通过 SendMessage 送到 C#同时把 Unity 侧算好的输入框屏幕坐标回写给 DOM让输入法候选框有正确的锚点。表现层只做两件事显示文本、告诉桥接层「我现在在哪、要不要聚焦」。2.3 全屏适配的关键坐标换算与事件重绑全屏不是简单调一个 requestFullscreen 就完事。进入全屏后画布的 CSS 尺寸、设备像素比、浏览器视口都会变输入框的屏幕坐标必须重新计算。方案里把坐标换算统一收口到一个函数输入是 Unity 世界坐标或 UI 局部坐标输出是相对视口的 CSS 像素坐标换算时同时考虑 canvas 的 boundingRect 和 devicePixelRatio。全屏切换时监听 fullscreenchange 事件触发一次坐标重算和输入框位置同步避免候选框还停在旧位置。提示坐标换算不要在两处各写一份页面层和 C# 侧各算各的必然对不上。统一由 C# 算出逻辑坐标桥接层只做一次线性映射出问题时只需要检查这一个函数。3. 桥接层实现JS 输入代理与 Unity 消息通道3.1 DOM 输入元素的创建与组合事件监听页面层的核心是一个动态创建的 input挂在 body 下样式上做成透明且不抢焦点视觉。下面这段是桥接层里最关键的监听逻辑组合输入和上屏输入分开处理。// 创建代理输入元素透明覆盖在 Unity 输入框位置 function createProxyInput() { const el document.createElement(input); el.type text; el.style.position absolute; el.style.opacity 0; // 视觉交给 Unity 渲染 el.style.zIndex 9999; el.style.border none; el.style.outline none; el.style.background transparent; document.body.appendChild(el); // 组合输入阶段输入法还没上屏文本暂存 el.addEventListener(compositionstart, () { isComposing true; }); el.addEventListener(compositionupdate, (e) { // 把组合中的文本实时回传用于 Unity 侧预览 sendToUnity(OnCompositionUpdate, e.data); }); el.addEventListener(compositionend, (e) { isComposing false; // 上屏文本才是最终结果 sendToUnity(OnCompositionEnd, e.data); }); // 非组合输入英文、数字直接走 input 事件 el.addEventListener(input, (e) { if (!isComposing) { sendToUnity(OnInputText, e.target.value); } }); return el; }这段代码的设计意图是把「组合中」和「已上屏」两种状态分开。compositionupdate 里拿到的 e.data 是候选词还没确认时的临时文本用来做输入预览compositionend 里的 e.data 才是玩家最终选定的内容。如果只监听 input 事件中文输入法在组合阶段的行为在不同浏览器里不一致很容易出现重复字符或者丢字。isComposing 这个标志位就是用来挡住组合期间的 input 事件避免同一段文本被送两次。sendToUnity 的实现依赖 Unity 提供的全局对象写法如下// 统一的消息出口gameInstance 由 Unity 模板注入 function sendToUnity(methodName, payload) { if (typeof gameInstance undefined || !gameInstance) return; gameInstance.SendMessage(WebGLInputBridge, methodName, payload || ); }参数说明methodName 对应 C# 侧挂载在 WebGLInputBridge 这个 GameObject 上的公开方法名payload 是字符串。SendMessage 只支持单个字符串或数值参数所以复杂结构要在两侧约定好序列化格式这里文本场景直接用字符串即可。3.2 聚焦、失焦与输入框位置同步输入框位置同步是候选框跟随的前提。C# 侧在每帧或者输入框状态变化时把输入框在屏幕上的矩形传给桥接层桥接层设置 DOM input 的 left、top、width、height。下面是对外暴露的同步接口。// 由 Unity 调用更新代理输入框的位置与尺寸 function syncInputRect(x, y, w, h) { if (!proxyInput) return; proxyInput.style.left x px; proxyInput.style.top y px; proxyInput.style.width w px; proxyInput.style.height h px; } // 由 Unity 调用请求聚焦触发浏览器输入法 function focusInput() { if (!proxyInput) return; proxyInput.focus(); } // 由 Unity 调用主动失焦收起输入法 function blurInput() { if (!proxyInput) return; proxyInput.blur(); }坐标 x、y 是相对浏览器视口左上角的 CSS 像素值w、h 是输入框的显示尺寸。这里有个血泪经验不要直接把 Unity 的 UI 局部坐标传过来必须先经过 canvas 的 boundingRect 换算。因为 Unity 画布在页面里可能不是从 (0,0) 开始还有可能被外层容器加了 padding 或缩放直接传局部坐标会让候选框整体偏移一个固定距离看起来就像「候选框总是偏左上」。3.3 全屏切换时的重绑逻辑全屏切换会改变视口尺寸必须重新同步一次位置并且要处理浏览器可能因为全屏而重建输入焦点的情况。// 监听全屏变化重算并同步输入框位置 document.addEventListener(fullscreenchange, () { const isFull !!document.fullscreenElement; // 通知 Unity 当前全屏状态由 C# 侧决定是否重算布局 sendToUnity(OnFullscreenChanged, isFull ? 1 : 0); // 延迟一帧再同步等画布尺寸稳定 requestAnimationFrame(() { if (typeof syncFromUnity function) { syncFromUnity(); // 回调 Unity 侧重新下发矩形 } }); });这里用 requestAnimationFrame 包一层是因为全屏切换后画布的 CSS 尺寸更新有延迟立刻读 boundingRect 可能拿到旧值。延迟一帧再让 Unity 重新下发矩形能避开大部分「全屏后候选框位置错乱」的问题。OnFullscreenChanged 回传后C# 侧可以决定是否重新计算 UI 布局比如全屏后输入框本身的位置也可能变化。4. Unity 侧实现文本接收、渲染与输入框状态管理4.1 桥接脚本的挂载与消息接收C# 侧需要一个固定名字的 GameObject 来接收 SendMessage方案里叫 WebGLInputBridge挂一个同名脚本。脚本里暴露三个公开方法分别对应组合更新、上屏、全屏变化。using UnityEngine; using UnityEngine.UI; public class WebGLInputBridge : MonoBehaviour { public InputField targetField; // 绑定的输入框 public RectTransform inputRect; // 用于计算屏幕位置 // 组合输入中的预览文本 public void OnCompositionUpdate(string text) { if (targetField ! null) targetField.text text; // 实时预览不触发提交 } // 输入法上屏的最终文本 public void OnCompositionEnd(string text) { if (targetField ! null) { targetField.text text; targetField.caretPosition text.Length; } } // 全屏状态变化 public void OnFullscreenChanged(string flag) { bool isFull flag 1; // 全屏后布局可能变化触发一次位置重算 SyncInputPosition(); } }targetField 是场景里实际显示文本的 InputFieldinputRect 用来取输入框的屏幕矩形。OnCompositionUpdate 里直接改 text 是为了让玩家在选词过程中看到预览但要注意这时候不能触发 onValueChanged 的业务逻辑否则每敲一个候选词都会触发一次提交。通常的做法是在 InputField 的 onValueChanged 里判断一个「是否正在组合」的标志组合期间只更新显示不跑业务。4.2 屏幕坐标计算与下发位置同步的触发点在 C# 侧因为只有 Unity 知道输入框在画布里的真实位置。下面这个方法把 UI 局部坐标换算成视口坐标再调 JS。using UnityEngine; using System.Runtime.InteropServices; public class WebGLInputBridge : MonoBehaviour { [DllImport(__Internal)] private static extern void syncInputRect(float x, float y, float w, float h); [DllImport(__Internal)] private static extern void focusInput(); public Canvas targetCanvas; public RectTransform inputRect; public void SyncInputPosition() { if (inputRect null || targetCanvas null) return; // 取输入框四角在世界空间的坐标 Vector3[] corners new Vector3[4]; inputRect.GetWorldCorners(corners); // 转成屏幕坐标左下为原点 Vector2 screenMin RectTransformUtility.WorldToScreenPoint( targetCanvas.worldCamera, corners[0]); Vector2 screenMax RectTransformUtility.WorldToScreenPoint( targetCanvas.worldCamera, corners[2]); // 转成浏览器视口坐标左上为原点Y 轴翻转 float x screenMin.x; float y Screen.height - screenMax.y; float w screenMax.x - screenMin.x; float h screenMax.y - screenMin.y; syncInputRect(x, y, w, h); } }这段的关键在 Y 轴翻转。Unity 的屏幕坐标原点在左下浏览器视口原点在左上所以 y 要用 Screen.height 减去上边界的屏幕 y。w 和 h 直接取对角差值。GetWorldCorners 返回的顺序是左下、左上、右上、右下corners[0] 和 corners[2] 正好是对角取这两个点就够了。如果输入框有旋转或者非矩形裁剪这套换算会失真所以方案里约定输入框保持轴对齐复杂形状用外层容器包一层矩形来算。4.3 输入框生命周期与焦点管理输入框不是一直聚焦的玩家点一下才进入输入状态点外面就失焦。C# 侧用一个状态机管理三种状态空闲、聚焦、组合中。点击输入框时调 focusInput 让 DOM 元素拿到焦点同时把状态切到聚焦收到 compositionstart 对应的信号切到组合中compositionend 后回到聚焦点击空白区域调 blurInput 回到空闲。public enum InputState { Idle, Focused, Composing } private InputState state InputState.Idle; public void OnInputFieldClicked() { if (state InputState.Idle) { focusInput(); state InputState.Focused; } } public void OnClickOutside() { if (state ! InputState.Idle) { blurInput(); state InputState.Idle; } }状态机的好处是避免重复聚焦和重复失焦。浏览器对 focus 的调用有节流短时间内连续调 focus 可能导致输入法状态异常用状态挡一层能减少这类问题。另外失焦时要主动把组合中的临时文本清掉或者提交具体看业务方案里默认失焦即提交当前上屏文本避免玩家以为输入了其实丢了。5. 避坑与排查WebGL 中文输入最容易翻车的五个点5.1 候选框位置偏移一个固定距离现象输入法候选框总是出现在输入框左上方或右下方一段固定距离怎么调都对不齐。原因通常是坐标换算时混用了两套原点比如 C# 传了 UI 局部坐标JS 侧又按视口坐标处理或者 canvas 有外层容器的偏移没算进去。解决方式是统一收口C# 只输出视口坐标JS 侧不再做任何二次偏移排查时在 syncInputRect 里打印传入值和 canvas.getBoundingClientRect() 对比一眼就能看出差在哪。5.2 中文上屏后字符重复或丢字现象打「你好」上屏变成「你你好」或者只剩「好」。原因是组合期间的 input 事件和 compositionend 都被处理了同一段文本走了两条路径。解决方式是在 input 监听里用 isComposing 挡住组合期间的输入只让 compositionend 负责最终上屏。如果项目里还监听了 keydown 做特殊处理也要在组合期间跳过否则回车选词会被当成提交。5.3 全屏后输入法完全失效现象非全屏能输入一进全屏输入法就不弹或者弹了没反应。原因多半是全屏切换导致 DOM 焦点丢失代理 input 被浏览器移出了焦点链。解决方式是在 fullscreenchange 之后延迟一帧重新调 focusInput并且检查代理 input 是否还在 document.body 下。有些浏览器全屏时会重建渲染层input 元素如果挂在被全屏的容器内部可能被隐藏方案里统一挂在 body 下就是为了避开这个问题。5.4 移动端浏览器组合事件不触发现象桌面浏览器正常手机浏览器上中文输入没有预览、上屏也异常。原因是移动端输入法的 composition 事件行为和桌面不同部分浏览器在软键盘下只发 input 不发 composition。解决方式是在桥接层做能力检测如果一段时间内没收到 composition 事件退化为直接监听 input 并做去重。这个降级逻辑要单独测不能假设桌面行为能直接搬到移动端。5.5 SendMessage 目标对象找不到现象控制台报错说找不到接收方法的 GameObject输入完全没反应。原因是桥接脚本挂载的 GameObject 名字和 JS 里写的不一致或者场景切换时对象被销毁了。解决方式是给桥接对象加 DontDestroyOnLoad名字用常量统一管理JS 侧调用前先判断 gameInstance 是否存在。排查时在浏览器控制台手动调一次 gameInstance.SendMessage 看是否有响应能快速区分是桥接没通还是逻辑没跑。6. 验收与答辩怎么证明这套输入方案真的能用验收这套方案不能只看「能打出中文」这一条要按场景拆成可检查的清单。我一般会准备一张验收表逐项过一遍答辩时直接拿这张表讲比口头描述有说服力。验收项操作步骤通过标准基础中文输入聚焦输入框切换中文输入法输入「测试」并选词上屏文本正确显示无重复无丢字组合预览输入拼音但不选词观察输入框能看到组合中的临时文本选词后正确替换候选框跟随在不同屏幕位置各放一个输入框分别聚焦输入候选框始终贴近当前输入框不偏移全屏输入进入全屏后重复基础中文输入输入法正常弹出候选框位置正确失焦提交输入到一半点击空白区域已上屏文本保留组合中文本按约定处理多浏览器在至少两种主流浏览器各跑一遍行为一致无控制台报错答辩时容易被问到的两个点提前准备好答案。一个是「为什么不用 Unity 自带的输入」回答要落到组合输入事件在 WebGL 下拿不到完整状态而不是笼统说「有 bug」。另一个是「全屏坐标怎么保证准」回答要能说清 boundingRect 加 devicePixelRatio 的换算链路最好现场打开控制台打印一次坐标对比。演示顺序建议先非全屏走一遍完整输入再切全屏重复一次最后点空白失焦三步把核心场景覆盖完时间控制在两分钟内。有个习惯我从那以后一直保留任何涉及浏览器原生能力的桥接方案验收前一定先在目标浏览器里手动跑一遍最小闭环确认 SendMessage 通、坐标对、事件触发顺序符合预期再往上叠业务逻辑。这套输入方案看着不复杂但翻车点全在浏览器和 Unity 的交界处先把桥接层单独验通后面省下的排查时间远超这点准备成本。希望帮到你。本文还有配套的精品资源点击获取