news 2026/10/30 5:06:28

Unity WebGL中文输入解决方案:光标跟随与输入法兼容

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity WebGL中文输入解决方案:光标跟随与输入法兼容

简介:本资源是一个面向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 = '';"); #endif

4.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 小时无效调试。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 5:05:53

C#构建B站动态抽奖:WBI签名、规则链与可复现随机

简介&#xff1a;BiliRaffle 是一款面向 B 站动态抽奖场景的 C# 桌面工具组件&#xff0c;适合希望了解或二次开发抽奖流程的开发者与进阶用户。它围绕动态抽奖的登录、界面交互与抽奖逻辑组织代码&#xff0c;采用 .NET Framework 4.5 及以上环境&#xff0c;可配合相关 Bilib…

作者头像 李华
网站建设 2026/10/30 5:05:37

数据分析师笔试实战:脏数据清洗与业务归因分析

简介&#xff1a;本资源是一套面向数据分析求职者与转行学习者的笔试真题实战资料包&#xff0c;聚焦企业招聘中高频考察的数据清洗、SQL查询、统计推断、可视化解读及基础建模等核心能力。资源共41个文件&#xff0c;涵盖20张真题截图&#xff08;含前海数据、东莞数据等真实企…

作者头像 李华
网站建设 2026/10/30 5:05:36

求佛(原创诗)

法门的香火吹不散的是五十六个民族的花我匍匐在石阶的尽头看信仰的光一次次触动信念的心佛像始终伫立掌心向上托举的是众生的希望善恶的法门中是一念佛心边怒目金刚的陪伴-求佛愿祖国强大为根本愿国家统一的信念愿保一方平安的心愿愿这个饱经沧桑的国家像钢铁般坚固始终团结在中…

作者头像 李华
网站建设 2026/10/30 5:05:31

ISEK-AI认知脚手架:绕过AI新手黑洞的学习路径设计

简介&#xff1a;ISEK-AI人工智能资源是一套面向AI开发者与研究者的开源项目实践包&#xff0c;聚焦Agent-to-Agent&#xff08;A2A&#xff09;智能体协同通信机制&#xff0c;适用于分布式AI系统设计、多智能体协作开发及开源社区贡献实践等场景。资源共118个文件&#xff0c…

作者头像 李华
网站建设 2026/10/30 5:05:21

毕业论文写作 AI 工具箱:按选题到定稿整理的基础版 + 进阶版清单

这篇分享面向正在准备毕业论文的本科生、研究生。我们围绕一个非常典型的交付任务来选工具&#xff1a;从确定选题、搭建大纲、整理文献和数据&#xff0c;到完成正文、图表、引用标注、格式排版与降重改稿&#xff0c;最终提交一篇符合学校规范的毕业论文。 不同阶段适合的工…

作者头像 李华
网站建设 2026/10/30 5:04:30

从多尺度耦合到量产合规:电池工程链路的选型方法与验收口径

电池工程链路能否闭环&#xff0c;取决于 1D 系统仿真、CFD 热管理、结构耐久与物理测试四类模型之间是否共享同一套几何、材料与边界定义&#xff0c;使任一环节的变更都能被其他环节感知。本文给出五条判据&#xff0c;并说明每条判据对应的能力与验收口径。一、为什么电池工…

作者头像 李华