简介:本资源是一个面向Unity WebGL开发者的中文输入功能完整实现Demo,专为解决Web端3D/2D应用中中文输入不兼容、输入法悬浮窗无法跟随光标、全屏模式下定位失效等典型痛点而设计,适用于中高级Unity开发者及跨平台WebGL项目集成场景。压缩包共19个文件,含8张UI与进度图(如webgl-logo.png、fullscreen.png)、3个Unity WebGL核心构建文件(.unityweb)、2个关键JS脚本(UnityLoader.js、UnityProgress.js)、1个HTML入口页(index.html)、1个CSS样式文件(style.css)及ReadMe说明文档等,整体体积17.4MB,结构清晰,便于快速集成与调试。已有939人学习下载。读者可直接复用其JavaScript与C#双向通信逻辑、输入法动态定位算法、全屏API适配方案,并参考ReadMe中的实践要点与优化建议,快速落地支持中文输入的生产级WebGL应用。
1. Unity WebGL中文输入:不是加个InputField就完事,而是让拼音输入法在浏览器里真正“跟得上光标”
你有没有试过在 Unity WebGL 构建的网页游戏或交互式应用里放一个 InputField,然后用中文输入法打字——结果发现:光标卡在开头不动、候选框飘到屏幕左上角、按空格选词后只出乱码、全屏(Dome)模式下输入框直接消失?这不是你代码写错了,是 Unity WebGL 的文本输入机制和浏览器原生输入法之间存在一层看不见的“协议断层”。这个项目就是为了解决这个被大量开发者忽略的底层兼容问题而生:它不依赖任何第三方插件或外部 JS 库封装,而是通过深度定制 Unity 的 WebGL 模块通信链路 + 原生 DOM 元素劫持 + 输入法状态同步三重手段,实现了真正的中文输入法跟随(包括候选框位置实时绑定光标、中英文状态同步、回车/ESC/Tab 等控制键透传),并完整支持 WebGL 全屏(Dome)模式下的输入框生命周期管理。适合所有正在用 Unity 做 Web 端教育工具、表单系统、AI 对话界面、或需要用户实时输入中文内容的 WebGL 项目的开发者——尤其当你已经卡在“能输但输不对”“能显示但不跟随”“一全屏就失联”这三道墙前超过两小时。
2. 技术栈与架构设计:为什么必须绕开 Unity 默认 InputField,又不能全盘交给 JS?
Unity WebGL 的输入处理本质是“双线程模拟”:C# 逻辑运行在 Emscripten 编译后的 WebAssembly 线程,而浏览器 UI(包括输入法)完全运行在主线程。Unity 官方 InputField 在 WebGL 下默认使用<input type="text">作为底层载体,但它做了三件危险的事:
- 光标位置硬编码:把光标坐标固定映射到 Canvas 左上角,无视实际 InputField 在 UI 中的 RectTransform 位置;
- 输入法状态黑匣子:不监听
compositionstart/compositionupdate/compositionend事件,导致无法感知拼音输入过程,只能拿到最终字符串; - 全屏劫持失效:Dome 全屏时 Canvas 被置顶缩放,但
<input>元素仍留在 document.body 原始层级,Z-index 和 transform 完全脱节。
本项目彻底弃用 Unity 自带 InputField 渲染逻辑,改用「C# 控制权移交 + DOM 元素动态注入 + 事件桥接」架构:
2.1 核心模块划分:四个不可拆分的协作单元
Unity 层:InputBridge.cs
提供静态方法ShowInputAt(RectTransform rect, string initText),接收 UI 元素的 RectTransform 并触发 JS 注入;暴露OnInputSubmit(string)、OnCompositionUpdate(string)等回调委托,供 C# 业务逻辑消费输入流。JS 层:webgl-input-manager.js
独立于 Unity Loader 加载,在document.body创建<div class="webgl-input-overlay">作为输入容器;监听window.resize、document.fullscreenchange、window.orientationchange,动态计算 Canvas 实际缩放比和偏移量;用getBoundingClientRect()+getComputedStyle()实时反推光标在 Canvas 坐标系中的像素位置。DOM 层:input-proxy 元素
不用<input>,而用<textarea contenteditable="true">+style="position: absolute; z-index: 9999;",规避 input 的默认样式干扰;设置font-family、font-size、line-height与 Unity TextMeshPro 字体完全一致,保证光标高度匹配。通信层:Unity ↔ JS 双向桥接
使用unityInstance.SendMessage("GameObjectName", "MethodName", data)触发 C# 回调;JS 层通过Module.SendMessage向 Unity 发送结构化 JSON 字符串(含type: "submit" | "composition" | "focus" | "blur"+text,cursorX,cursorY,scaleX,scaleY)。
提示:这个架构的关键在于“控制权让渡”——Unity 不再试图渲染输入框,而是告诉 JS:“我要在这儿输中文,你来管光标和输入法,我只收结果”。
2.2 关键参数设计:为什么 scale 和 offset 必须每帧重算?
Canvas 在 WebGL 中的缩放不是静态 CSS transform,而是由WebGLContext的canvas.width/height与 CSSwidth/height比值决定,且在 Dome 全屏、移动端横竖屏切换、浏览器缩放时会动态变化。若只在初始化时读取一次scaleX = canvas.clientWidth / canvas.width,后续全屏后该值立即失效。
项目采用「每帧采样 + 防抖更新」策略:
- JS 层注册
requestAnimationFrame循环,每 16ms 读取canvas.getBoundingClientRect()和getComputedStyle(canvas); - 计算真实缩放因子:
const rect = canvas.getBoundingClientRect(); const scaleX = rect.width / canvas.width; const scaleY = rect.height / canvas.height; const offsetX = rect.left - document.documentElement.scrollLeft; const offsetY = rect.top - document.documentElement.scrollTop; - 仅当
Math.abs(scaleX - lastScaleX) > 0.01时才触发 DOM 元素重定位,避免高频 layout thrashing。
2.3 输入法状态同步:composition 事件如何映射到 C# 逻辑?
中文输入法的核心阶段是compositionstart→compositionupdate(拼音输入中)→compositionend(选词完成)。项目将这三个事件统一桥接到 C# 的OnCompositionUpdate(string)回调,并约定:
compositionupdate时传入当前拼音串(如"nihao");compositionend时传入最终汉字(如"你好");- 若用户用空格/回车选词,则
compositionend后紧跟着input事件,此时再触发OnInputSubmit(string)。
这样 C# 层可实现:
private void OnCompositionUpdate(string pinyinOrText) { if (IsComposing) { // 显示拼音悬浮提示,或高亮输入框背景 compositionText.text = $"[拼音] {pinyinOrText}"; } else { // 直接显示最终文字 inputText.text = pinyinOrText; } }3. 运行环境准备与构建配置:三步确认 WebGL 输入通道已打通
Unity WebGL 构建不是“点一下 Build 就完事”,输入功能依赖底层模块加载顺序和导出配置。以下步骤缺一不可,否则你会看到控制台报Module is not defined或SendMessage failed。
3.1 Unity Editor 设置:必须关闭的两个默认选项
Player Settings → Publishing Settings → Compression Format
必须设为Disabled(禁用压缩)。原因:LZ4 压缩会破坏 JS 模块的Module全局对象初始化时机,导致webgl-input-manager.js在Module尚未就绪时尝试调用Module.SendMessage,引发静默失败。Player Settings → Other Settings → Configuration → Scripting Backend
必须为IL2CPP(非 Mono)。Mono 在 WebGL 下对跨线程字符串传递有编码缺陷,中文字符易出现\uFFFD替换符;IL2CPP 通过 Emscripten 的 UTF-8 字符串桥接更稳定。
注意:若你已在用 Mono 构建且无法切换,必须在 JS 层手动做 UTF-8 解码:
decodeURIComponent(escape(str)),但此方案在长文本下性能极差,不推荐。
3.2 构建后文件注入:把 JS 文件塞进正确位置
Unity WebGL 构建输出目录结构如下:
Build/ ├── TemplateData/ │ ├── index.html ← 主入口 HTML │ └── style.css ├── MyGame.js ← Unity 主 JS 模块 ├── MyGame.wasm ← WebAssembly 二进制 └── ...你需要将webgl-input-manager.js注入到TemplateData/index.html的<head>末尾,且必须在MyGame.js加载之前:
<head> <!-- 其他 meta、link --> <script src="webgl-input-manager.js"></script> <!-- 放在这里 --> <script src="Build/MyGame.js"></script> </head>提示:不要用
<script defer>或async,必须阻塞式加载,确保webgl-input-manager.js的全局函数在 Unity 初始化前已注册。
3.3 浏览器兼容性验证:哪些浏览器真能跑通?
| 浏览器 | 中文输入法支持 | 全屏(Dome)支持 | 备注 |
|---|---|---|---|
| Chrome 115+ | ✅ 完整 | ✅ | 推荐主力测试环境 |
| Edge 115+ | ✅ | ✅ | 基于 Chromium,行为一致 |
| Safari 16.5+ | ⚠️ 仅基础输入 | ❌ 全屏后光标错位 | Safari 对getBoundingClientRect()在全屏 Canvas 下返回值异常,需额外 patch |
| Firefox 115+ | ✅ | ⚠️ 偶发候选框偏移 | 需开启dom.webcomponents.enabled = true |
验证方法:打开构建后的index.html,按 F12 打开 DevTools → Console,输入:
// 应返回 true typeof window.WebGLInputManager !== 'undefined' // 应返回当前 Canvas 缩放信息 window.WebGLInputManager.getCanvasScale()4. 避坑指南:那些让你调试到凌晨三点的典型翻车现场
4.1 现象:输入框显示了,但光标永远停在第一行开头,拼音候选框固定在左上角
原因:RectTransform 的anchoredPosition未转换为 Canvas 像素坐标,或 JS 层未正确读取rectTransform.rect的宽高。Unity 的anchoredPosition是相对于锚点的偏移,而 DOM 定位需要绝对像素值。
解决:在InputBridge.ShowInputAt()中,必须调用rectTransform.TransformPoint(Vector3.zero)获取世界坐标,再用Camera.main.WorldToScreenPoint()转为屏幕坐标,最后减去 Canvas 的rectTransform.position得到相对 Canvas 左上角的像素偏移。JS 层接收的是(x, y, width, height)四元组,而非anchoredPosition。
4.2 现象:全屏(Dome)后输入框消失,或点击无响应
原因:Dome 全屏时 Unity 会调用canvas.webkitRequestFullscreen()(Chrome)或canvas.requestFullscreen()(标准),但webgl-input-manager.js未监听fullscreenchange事件,导致 overlay<div>的z-index被 Canvas 的全屏层覆盖。
解决:在 JS 初始化时绑定:
document.addEventListener('fullscreenchange', () => { const overlay = document.querySelector('.webgl-input-overlay'); if (document.fullscreenElement === canvas) { overlay.style.zIndex = '99999'; // 高于 Unity 全屏层 overlay.style.pointerEvents = 'auto'; } else { overlay.style.zIndex = '9999'; overlay.style.pointerEvents = 'none'; } });4.3 现象:中文输入后显示为方块()或乱码(ãº)
原因:Unity C# 字符串在跨线程传递时被错误编码。Unity WebGL 默认用 UTF-16 编码字符串,但 JS 层Module.SendMessage期望 UTF-8。若未启用 IL2CPP 或未在 JS 层做解码,就会出现字节错位。
解决:
- 强制使用 IL2CPP(见 3.1);
- 在 JS 的
onMessageFromUnity回调中,对data字段做 UTF-8 解码(Unity 会自动转义):function decodeUTF8(str) { try { return decodeURIComponent(escape(str)); } catch (e) { return str; // fallback } }
4.4 现象:移动端 Safari 上输入法弹出后,Canvas 被顶起,页面滚动
原因:iOS Safari 的输入法弹出会触发window.scrollTo(0,0),且 Canvas 的style.position = 'fixed'无法阻止 body 滚动。
解决:在InputBridge.ShowInputAt()调用前,锁定 body 滚动:
#if UNITY_WEBGL && !UNITY_EDITOR Application.ExternalEval("document.body.style.overflow = 'hidden';"); #endif并在OnInputBlur()回调中恢复:
#if UNITY_WEBGL && !UNITY_EDITOR Application.ExternalEval("document.body.style.overflow = '';"); #endif4.5 现象:连续快速点击多个 InputField,光标位置错乱或输入框不消失
原因:JS 层 overlay<div>是单例复用,未做防重复创建和销毁清理。若前一个输入框未blur()就点击下一个,focus()事件会叠加触发。
解决:在 JS 层维护状态机:
let currentInputId = null; function showInput(id, x, y, w, h) { if (currentInputId && currentInputId !== id) { hideInput(); // 先销毁上一个 } currentInputId = id; // ... 创建 overlay } function hideInput() { const overlay = document.querySelector('.webgl-input-overlay'); if (overlay) overlay.remove(); currentInputId = null; }5. 核心代码逐行解析:从 ShowInputAt 到光标精准跟随的完整链路
我们以最典型的场景切入:用户点击 UI 上一个按钮,弹出输入框,光标需精确落在按钮右侧 10px、垂直居中位置。以下是 C# 与 JS 协同的最小可行链路。
5.1 C# 层:InputBridge.cs 的 ShowInputAt 方法
public static class InputBridge { private static readonly string INPUT_GAMEOBJECT_NAME = "InputManager"; /// <summary> /// 在指定 RectTransform 位置显示输入框,支持全屏/Dome 模式 /// </summary> /// <param name="rect">目标 UI 元素的 RectTransform</param> /// <param name="initText">初始文本(用于编辑场景)</param> /// <param name="isPassword">是否密码模式(隐藏字符)</param> public static void ShowInputAt(RectTransform rect, string initText = "", bool isPassword = false) { // 1. 获取 Canvas 组件(必须存在) Canvas canvas = rect.GetComponentInParent<Canvas>(); if (!canvas || !canvas.isRootCanvas) { Debug.LogError("InputBridge: RectTransform must be under a root Canvas!"); return; } // 2. 将 RectTransform 的 anchoredPosition 转为 Canvas 像素坐标 Vector2 localPos; RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, rect.TransformPoint(Vector3.zero), out localPos); // 3. 减去 Canvas 左上角偏移,得到相对 Canvas 的像素坐标 Vector2 canvasPos = localPos - canvas.GetComponent<RectTransform>().position; // 4. 计算输入框尺寸(宽高取 rect 的 rect.size,加 10px 右侧留白) float width = rect.rect.width + 10f; float height = rect.rect.height; // 5. 构造参数对象并发送给 JS var args = new { x = canvasPos.x, y = canvasPos.y, width = width, height = height, text = initText, isPassword = isPassword, canvasId = canvas.gameObject.GetInstanceID() // 用于 JS 端识别 Canvas }; // 6. 调用 JS 全局函数(注意:函数名必须与 webgl-input-manager.js 中一致) Application.ExternalEval($"WebGLInputManager.showInput({JsonConvert.SerializeObject(args)});"); } }关键说明:
RectTransformUtility.WorldToScreenPoint是核心,它把 UI 元素的世界坐标转为屏幕坐标,绕过了anchoredPosition的锚点陷阱;canvas.GetComponent<RectTransform>().position是 Canvas 在屏幕上的绝对位置,必须减去才能得到“相对于 Canvas”的坐标;Application.ExternalEval是 Unity WebGL 最底层的 JS 调用方式,比SendMessage更快,且不依赖 GameObject 存在;JsonConvert.SerializeObject将 C# 对象序列化为 JSON 字符串,JS 层可直接JSON.parse(),避免字符串拼接 XSS 风险。
5.2 JS 层:webgl-input-manager.js 的 showInput 函数
// 全局单例管理器 window.WebGLInputManager = { overlay: null, canvas: null, scale: { x: 1, y: 1 }, offset: { x: 0, y: 0 }, // 初始化:查找 Canvas 并绑定事件 init: function() { this.canvas = document.querySelector('canvas'); // Unity WebGL 默认 canvas if (!this.canvas) return; // 绑定 resize 和 fullscreen 事件 window.addEventListener('resize', () => this.updateScale()); document.addEventListener('fullscreenchange', () => this.updateScale()); // 首次计算 this.updateScale(); }, updateScale: function() { const rect = this.canvas.getBoundingClientRect(); this.scale.x = rect.width / this.canvas.width; this.scale.y = rect.height / this.canvas.height; this.offset.x = rect.left - window.pageXOffset; this.offset.y = rect.top - window.pageYOffset; }, showInput: function(params) { // 1. 销毁旧 overlay(防重复) if (this.overlay) this.hideInput(); // 2. 创建新 overlay this.overlay = document.createElement('div'); this.overlay.className = 'webgl-input-overlay'; this.overlay.contentEditable = true; this.overlay.style.cssText = ` position: absolute; left: ${params.x}px; top: ${params.y}px; width: ${params.width}px; height: ${params.height}px; font-family: "Arial", sans-serif; /* 必须与 Unity TextMeshPro 字体一致 */ font-size: 14px; line-height: ${params.height}px; overflow: hidden; outline: none; z-index: 99999; pointer-events: auto; `; // 3. 设置初始文本(密码模式用 ● 替代) if (params.isPassword) { this.overlay.textContent = '●'.repeat(params.text.length); } else { this.overlay.textContent = params.text; } // 4. 插入 body 并聚焦 document.body.appendChild(this.overlay); this.overlay.focus(); // 5. 绑定输入事件(composition 和 input) this.overlay.addEventListener('compositionstart', (e) => { this.sendToUnity('compositionstart', { text: e.data }); }); this.overlay.addEventListener('compositionupdate', (e) => { this.sendToUnity('compositionupdate', { text: e.data }); }); this.overlay.addEventListener('compositionend', (e) => { this.sendToUnity('compositionend', { text: e.data }); }); this.overlay.addEventListener('input', (e) => { this.sendToUnity('input', { text: e.target.textContent }); }); // 6. 绑定回车/ESC 等快捷键 this.overlay.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.sendToUnity('submit', { text: e.target.textContent }); this.hideInput(); } else if (e.key === 'Escape') { e.preventDefault(); this.hideInput(); } }); }, sendToUnity: function(type, data) { // 构造消息对象 const msg = { type: type, text: data.text || '', timestamp: Date.now() }; // 发送给 Unity(假设 Module 已就绪) if (typeof Module !== 'undefined' && Module.SendMessage) { try { Module.SendMessage('InputManager', 'OnInputEvent', JSON.stringify(msg)); } catch (e) { console.warn('Send to Unity failed:', e); } } }, hideInput: function() { if (this.overlay) { this.overlay.remove(); this.overlay = null; } } }; // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', () => { WebGLInputManager.init(); });关键说明:
updateScale()每次resize或fullscreenchange都重新计算,确保缩放因子实时准确;showInput中left/top直接使用 C# 传来的像素值,但注意:这些值是“Canvas 坐标系”下的,而document.body是文档坐标系,所以必须靠updateScale()动态补偿;contentEditable="true"替代<input>,获得更自由的样式控制和事件粒度;sendToUnity使用JSON.stringify将消息转为字符串,Unity C# 层用JsonUtility.FromJson<InputEvent>(json)解析,类型安全;e.preventDefault()在keydown中阻止浏览器默认行为(如回车换行),确保控制权完全交还 Unity。
5.3 C# 层:接收 JS 消息的 InputManager.cs
public class InputManager : MonoBehaviour { // 定义消息结构体,与 JS sendToUnity 的 JSON 字段严格对应 [Serializable] public struct InputEvent { public string type; // "submit" | "compositionupdate" | "blur" public string text; // 当前输入内容 public long timestamp; // 时间戳(可选,用于防抖) } // 业务逻辑回调(可由其他脚本订阅) public static Action<string> OnInputSubmit; public static Action<string> OnCompositionUpdate; // Unity WebGL 必须暴露的公共方法,供 JS 调用 public void OnInputEvent(string json) { try { InputEvent evt = JsonUtility.FromJson<InputEvent>(json); switch (evt.type) { case "submit": OnInputSubmit?.Invoke(evt.text); break; case "compositionupdate": OnCompositionUpdate?.Invoke(evt.text); break; case "blur": // 输入框失去焦点,可触发 UI 收起等逻辑 break; } } catch (System.Exception e) { Debug.LogWarning($"InputManager: Parse JSON failed - {e.Message}"); } } }关键说明:
JsonUtility.FromJson是 Unity 内置的轻量 JSON 解析器,比Newtonsoft.Json更快,且无需额外 DLL;OnInputSubmit和OnCompositionUpdate是静态事件,任何脚本都可订阅,例如:void Start() { InputManager.OnInputSubmit += HandleSubmit; InputManager.OnCompositionUpdate += HandleComposition; } void HandleSubmit(string text) { Debug.Log($"Final input: {text}"); // 更新 UI 文本、提交表单等 }
6. 二次开发实战:如何扩展支持语音输入、历史记录、自定义键盘
这个项目不是“用完即弃”的 Demo,而是一个可生长的输入基础设施。我把它用在某高校的在线实验平台中,学生需在 WebGL 环境中输入化学方程式、电路参数、甚至手写公式识别结果。以下是三个真实落地的扩展方向,附可直接抄作业的代码片段。
6.1 扩展语音输入:接入 Web Speech API,让麦克风按钮一键说话
Web Speech API 的SpeechRecognition在 Chrome/Edge 中已稳定支持,只需在 JS 层增加语音识别模块,并与现有输入框联动。
JS 层修改(webgl-input-manager.js 新增):
// 语音识别实例(全局单例) WebGLInputManager.speechRec = null; WebGLInputManager.startSpeechRecognition = function() { if (!('webkitSpeechRecognition' in window)) { alert('Speech recognition not supported in this browser.'); return; } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; this.speechRec = new SpeechRecognition(); this.speechRec.continuous = false; this.speechRec.interimResults = false; this.speechRec.lang = 'zh-CN'; this.speechRec.onresult = (event) => { const transcript = event.results[0][0].transcript; // 将语音结果插入当前输入框 if (this.overlay && document.activeElement === this.overlay) { this.overlay.textContent = transcript; this.sendToUnity('submit', { text: transcript }); } }; this.speechRec.onerror = (event) => { console.error('Speech error', event.error); }; this.speechRec.start(); }; // 在 showInput 中添加语音按钮 WebGLInputManager.showInput = function(params) { // ... 原有代码 // 创建语音按钮(绝对定位在输入框右端) const micBtn = document.createElement('button'); micBtn.innerHTML = '🎤'; micBtn.style.cssText = ` position: absolute; right: 5px; top: 50%; transform: translateY(-50%); width: 24px; height: 24px; border: none; background: #4CAF50; color: white; border-radius: 50%; cursor: pointer; `; micBtn.onclick = () => WebGLInputManager.startSpeechRecognition(); this.overlay.appendChild(micBtn); };C# 层调用:在需要语音输入的 UI 按钮上挂脚本:
public void OnClickMicButton() { // 触发 JS 语音识别 Application.ExternalEval("WebGLInputManager.startSpeechRecognition();"); }血泪经验:语音识别必须在用户手势(click/touch)后触发,否则浏览器会静默拒绝。所以
onclick事件是刚需,不能用setTimeout自动启动。
6.2 扩展输入历史:本地存储最近 10 条输入,按 ↑ ↓ 键切换
很多表单场景需要“上次输过什么”,比如命令行式交互、SQL 查询输入。利用localStorage实现轻量历史管理。
JS 层新增 history 模块:
WebGLInputManager.history = { maxItems: 10, items: [], load: function() { const saved = localStorage.getItem('webgl-input-history'); this.items = saved ? JSON.parse(saved) : []; }, save: function(text) { if (!text.trim()) return; // 去重:如果已存在,移到最前 const idx = this.items.indexOf(text); if (idx > -1) this.items.splice(idx, 1); // 插入开头 this.items.unshift(text); // 截断 if (this.items.length > this.maxItems) { this.items.pop(); } localStorage.setItem('webgl-input-history', JSON.stringify(this.items)); }, getPrevious: function(currentIndex) { if (this.items.length === 0) return ''; const idx = Math.min(currentIndex, this.items.length - 1); return this.items[idx]; } }; // 在 showInput 中初始化 history WebGLInputManager.showInput = function(params) { // ... 原有代码 WebGLInputManager.history.load(); // 绑定上下键 this.overlay.addEventListener('keydown', (e) => { if (e.key === 'ArrowUp') { e.preventDefault(); const prev = WebGLInputManager.history.getPrevious(0); if (prev) this.overlay.textContent = prev; } }); };C# 层保存历史(在OnInputSubmit回调中):
void HandleSubmit(string text) { // 保存到 JS history Application.ExternalEval($"WebGLInputManager.history.save('{text.Replace("'", "\\'")}');"); }6.3 扩展自定义软键盘:为触控设备提供数字/符号专用键盘
移动端没有物理键盘,需弹出自定义软键盘。项目已预留keyboardType参数,可扩展为number、email、url等模式。
C# 层增强 ShowInputAt:
public enum KeyboardType { Default, Number, Email, Url } public static void ShowInputAt(RectTransform rect, string initText = "", bool isPassword = false, KeyboardType keyboardType = KeyboardType.Default) { var args = new { x = canvasPos.x, y = canvasPos.y, width = width, height = height, text = initText, isPassword = isPassword, keyboardType = keyboardType.ToString().ToLower() // "number" }; Application.ExternalEval($"WebGLInputManager.showInput({JsonConvert.SerializeObject(args)});"); }JS 层根据 keyboardType 渲染不同键盘:
WebGLInputManager.showInput = function(params) { // ... 创建 overlay // 根据类型插入软键盘 DOM if (params.keyboardType === 'number') { const keypad = document.createElement('div'); keypad.innerHTML = ` <div class="keypad"> <button onclick="WebGLInputManager.insertChar('1')">1</button> <button onclick="WebGLInputManager.insertChar('2')">2</button> <!-- ... 0-9, ., ← --> </div> `; document.body.appendChild(keypad); } }; WebGLInputManager.insertChar = function(char) { if (this.overlay && document.activeElement === this.overlay) { const sel = window.getSelection(); const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(char)); this.overlay.focus(); } };从那以后我每次做 WebGL 输入需求,都会先检查三件事:IL2CPP 是否启用、webgl-input-manager.js 是否在 index.html head 中且早于 MyGame.js、Canvas 的 Render Mode 是否为 Screen Space - Overlay。这三道关卡卡住 90% 的问题,省下至少 4 小时无效调试。希望帮到你。
本文还有配套的精品资源,点击获取