
简介这份资源面向需要在 Ionic 混合应用中嵌入 Unity 3D 内容的移动开发者重点解决 Android 与 iOS 双端集成及两者间消息通信的问题并已在带 Vuforia 插件的中等规模 Unity 商业项目中验证适合具备 Cordova、Objective-C 与 C# 基础的进阶开发者参考。压缩包共 21 个文件、约 424KB包含 png 界面截图、mm 与 m 原生实现、java 与 h 安卓桥接、cs 通信脚本、xml 插件配置、xcconfig 与 json 工程配置及 jar 依赖等覆盖双端桥接所需的关键模块。目前已有 228 人学习下载。读者可从中获得 Unity 与 Ionic 互调消息的完整实现思路、原生层与 C# 层的对接方式以及 Vuforia 场景下的集成参考便于快速搭建 AR 类混合应用骨架并排查跨平台通信问题。1. 把 Unity 塞进 Ionic 壳里这套桥接方案到底解决什么问题如果你做过 AR 项目大概率遇到过这种拧巴的需求业务界面用 Ionic 写跨平台、迭代快、表单和列表都顺手但一到 3D 渲染、AR 识别、模型交互又必须交给 Unity。两个世界各自跑得好好的一旦要拼进同一个 App问题就来了——WebView 里的 JavaScript 和原生层的 Unity 怎么对话Android 和 iOS 的桥接方式还不一样Vuforia 插件又要额外处理生命周期。这套 UnityIonicIntegration 方案核心就是解决「Ionic 壳 Unity 3D 内容」在 Android 和 iOS 上的双向消息传递问题并且已经在 Vuforia 插件场景下验证过。适合谁适合手里有 Ionic 项目、又必须嵌入 Unity AR/3D 模块的移动端开发者尤其是做工业巡检、教育演示、展厅导览这类需要「业务界面 3D 视图」混合的团队。2. 桥接原理与工程结构为什么不能直接 iframe 嵌 Unity2.1 两种运行时的边界在哪里Ionic 跑在 WebView 里本质是一个浏览器环境Unity 导出 Android/iOS 后是一个原生 Activity 或 ViewController两者不在同一个渲染管线里。很多人第一反应是用 iframe 或者 WebView 加载 Unity WebGL 构建这在桌面浏览器上勉强能跑但在移动端 AR 场景下基本不可行——WebGL 构建体积大、性能差Vuforia 的摄像头和传感器能力也拿不到。所以正确的做法是Unity 作为原生模块嵌入Ionic 通过 Cordova 插件暴露的 JS 接口与原生层通信原生层再转发给 Unity。这个结构里有三层JS 层Ionic 页面、Cordova 插件层Java/Objective-C、Unity 层C#。消息从 JS 发出经过插件层序列化最终由 Unity 的UnitySendMessage或自定义回调接收反向则从 Unity 调用原生方法再通过插件回调到 JS。2.2 工程目录怎么摆一个典型的混合工程结构如下Android 和 iOS 的 Unity 导出内容分别放在对应平台目录下project-root/ ├── ionic-app/ # Ionic 前端工程 │ ├── src/ │ └── cordova-plugin-unity/ # 自定义 Cordova 插件 │ ├── src/ │ │ ├── android/ │ │ │ └── UnityBridgePlugin.java │ │ └── ios/ │ │ └── UnityBridgePlugin.m │ ├── www/ │ │ └── unity-bridge.js │ └── plugin.xml ├── unity-project/ # Unity 工程 │ ├── Assets/ │ │ └── Scripts/ │ │ └── NativeBridge.cs │ └── Plugins/ │ ├── Android/ │ └── iOS/ └── platforms/ ├── android/ │ └── unityLibrary/ # Unity 导出的 Android 模块 └── ios/ └── UnityFramework/ # Unity 导出的 iOS 框架这里的关键点是Unity 导出时选择「Export as Android Library」或「Export as iOS Framework」而不是直接导出 APK 或 Xcode 工程。这样 Ionic 的 Cordova 构建才能把 Unity 模块作为依赖引入。2.3 Cordova 插件如何暴露接口插件层是整个方案的核心。以 Android 为例UnityBridgePlugin.java需要继承CordovaPlugin并实现execute方法处理 JS 调用public class UnityBridgePlugin extends CordovaPlugin { private static final String TAG UnityBridge; private static UnityPlayer unityPlayer; private static CallbackContext pendingCallback; Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) { if (sendToUnity.equals(action)) { String message args.optString(0); // 通过 UnityPlayer 的 UnitySendMessage 发送到指定 GameObject UnityPlayer.UnitySendMessage(NativeBridge, OnMessageFromJS, message); callbackContext.success(sent); return true; } if (initUnity.equals(action)) { // 初始化 Unity 视图并嵌入当前 Activity cordova.getActivity().runOnUiThread(() - { unityPlayer new UnityPlayer(cordova.getActivity()); // 将 UnityPlayer 的 View 添加到布局中 FrameLayout container cordova.getActivity().findViewById(R.id.unity_container); container.addView(unityPlayer.getView()); }); callbackContext.success(init); return true; } return false; } // Unity 调用此静态方法回传消息到 JS public static void onUnityMessage(String message) { if (pendingCallback ! null) { pendingCallback.success(message); pendingCallback null; } } }逻辑说明execute是 Cordova 插件的入口JS 调用cordova.exec时会路由到这里。UnitySendMessage是 Unity 提供的原生接口第一个参数是 Unity 场景中的 GameObject 名称第二个是挂载脚本的方法名第三个是字符串参数。注意UnitySendMessage只能传字符串复杂数据结构需要自己序列化成 JSON。参数说明action是 JS 侧指定的操作名args是参数数组callbackContext用于异步回调。initUnity里的R.id.unity_container需要在 Android 布局文件中预先定义通常是一个FrameLayout。2.4 iOS 侧的 Objective-C 桥接iOS 的桥接稍微复杂一些因为 Unity 导出的是 framework需要用 Objective-C 来调用 Unity 的 C 接口// UnityBridgePlugin.m #import Cordova/CDV.h #import UnityFramework/UnityFramework.h interface UnityBridgePlugin : CDVPlugin property (nonatomic, strong) UnityFramework *ufw; end implementation UnityBridgePlugin - (void)initUnity:(CDVInvokedUrlCommand *)command { // 加载 UnityFramework self.ufw [UnityFramework getInstance]; [self.ufw setDataBundleId:com.unity3d.framework]; [self.ufw runUI]; // 将 Unity 视图添加到当前 ViewController UIView *unityView [self.ufw appController].window.rootViewController.view; [self.viewController.view addSubview:unityView]; CDVPluginResult *result [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; } - (void)sendToUnity:(CDVInvokedUrlCommand *)command { NSString *message [command.arguments objectAtIndex:0]; // 调用 Unity 的 SendMessage 接口 [self.ufw sendMessageToGOWithName:NativeBridge functionName:OnMessageFromJS message:[message UTF8String]]; CDVPluginResult *result [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; } end逻辑说明UnityFramework是 Unity 导出 iOS 框架时的入口类getInstance获取单例runUI启动 Unity 渲染循环。sendMessageToGOWithName对应 Android 的UnitySendMessage参数含义一致。注意 iOS 上 Unity 视图的层级管理需要小心如果直接 addSubview 到根视图可能会遮挡 Ionic 的 WebView通常需要控制显示时机。参数说明setDataBundleId指定 Unity 资源包的 bundle identifier必须与 Unity 导出时设置的一致。command.arguments是 JS 传入的参数数组这里取第一个作为消息内容。3. 从零跑通一次消息往返JS 到 Unity 再回来3.1 Ionic 侧调用封装在 Ionic 项目里先封装一个简单的服务来调用 Cordova 插件// unity-bridge.service.ts import { Injectable } from angular/core; declare var cordova: any; Injectable({ providedIn: root }) export class UnityBridgeService { private isReady false; initUnity(): Promisestring { return new Promise((resolve, reject) { cordova.exec( (res) { this.isReady true; resolve(res); }, (err) reject(err), UnityBridge, // 插件名对应 plugin.xml 中的 feature name initUnity, // action [] // 参数 ); }); } sendToUnity(message: string): Promisestring { return new Promise((resolve, reject) { if (!this.isReady) { reject(Unity not initialized); return; } cordova.exec(resolve, reject, UnityBridge, sendToUnity, [message]); }); } }逻辑说明cordova.exec的五个参数分别是成功回调、失败回调、插件名、action 名、参数数组。插件名必须与plugin.xml里feature nameUnityBridge保持一致否则会报Class not found。参数说明message是字符串如果需要传对象在调用前JSON.stringifyUnity 侧再解析。3.2 Unity 侧接收与回传Unity 工程里需要一个挂载在名为NativeBridge的 GameObject 上的脚本// NativeBridge.cs using UnityEngine; using System.Runtime.InteropServices; public class NativeBridge : MonoBehaviour { // iOS 原生方法声明 [DllImport(__Internal)] private static extern void onUnityMessage(string message); // Android 原生方法声明 private AndroidJavaClass unityBridgePlugin; void Start() { if (Application.platform RuntimePlatform.Android) { unityBridgePlugin new AndroidJavaClass(com.example.unitybridge.UnityBridgePlugin); } } // 由原生层调用接收 JS 消息 public void OnMessageFromJS(string message) { Debug.Log(Received from JS: message); // 处理消息后回传 SendToJS(Unity received: message); } // 发送消息回 JS public void SendToJS(string message) { if (Application.platform RuntimePlatform.Android) { unityBridgePlugin.CallStatic(onUnityMessage, message); } else if (Application.platform RuntimePlatform.IPhonePlayer) { onUnityMessage(message); } } }逻辑说明OnMessageFromJS是原生层通过UnitySendMessage调用的方法方法名必须完全匹配。SendToJS里 Android 通过AndroidJavaClass反射调用 Java 静态方法iOS 通过DllImport调用 Objective-C 的 C 函数。参数说明com.example.unitybridge.UnityBridgePlugin是 Java 类的完整包名需要与插件工程一致。iOS 的onUnityMessage需要在.mm文件里用extern C声明。3.3 完整往返流程验证在 Ionic 页面里调用async testRoundTrip() { await this.unityBridge.initUnity(); const response await this.unityBridge.sendToUnity(hello from ionic); console.log(Unity replied:, response); }如果一切正常控制台会先打印 Unity 的Debug.Log然后收到Unity received: hello from ionic。这一步跑通说明桥接链路是通的。4. 避坑与排查那些让我熬夜的桥接问题4.1 Unity 视图黑屏或白屏现象Ionic 页面正常但嵌入的 Unity 区域一片黑或白没有渲染内容。原因最常见的是 Unity 导出时没有勾选正确的 Graphics API或者 Android 上UnityPlayer的 View 没有正确添加到布局层级里。iOS 上则可能是UnityFramework的runUI调用时机不对在 ViewController 还没加载完就调用了。解决Android 检查build.gradle里的minSdkVersion是否与 Unity 导出设置一致通常 Unity 2019 以上要求 22。iOS 确保在viewDidLoad之后再调用runUI并且 Unity 视图的 frame 要手动设置不能依赖自动布局。4.2 消息发送后 Unity 收不到现象JS 调用sendToUnity返回成功但 Unity 的OnMessageFromJS没有触发。原因UnitySendMessage的第一个参数是 GameObject 名称如果场景里没有这个对象或者对象在消息发送时还没加载消息就会丢失。另一个常见原因是 GameObject 名称拼写错误Unity 对大小写敏感。解决在 Unity 场景里确保NativeBridge这个 GameObject 在启动时就存在可以放在DontDestroyOnLoad的根对象上。发送消息前先确认 Unity 已经完成初始化可以在Start方法里回传一个 ready 信号给 JS。4.3 iOS 编译报错 Undefined symbols现象Xcode 编译时提示Undefined symbols for architecture arm64: _onUnityMessage。原因DllImport(__Internal)声明的方法在 Objective-C 侧没有用extern C导出C 的名称修饰导致链接器找不到符号。解决在.mm文件里这样写extern C void onUnityMessage(const char* message) { // 转发到 Cordova 插件 [[UnityBridgePlugin sharedInstance] sendMessageToJS:[NSString stringWithUTF8String:message]]; }注意文件扩展名必须是.mm而不是.m否则 C 代码无法编译。4.4 Vuforia 摄像头在 Ionic 页面切换后失效现象第一次进入 Unity AR 视图正常退出再进入后摄像头黑屏或 Vuforia 报错。原因Vuforia 的摄像头生命周期与 Unity 的UnityPlayer绑定如果退出时没有正确暂停 Unity再次进入时摄像头资源被占用。解决在 Ionic 页面ionViewWillLeave里调用插件的pauseUnity方法Android 上执行unityPlayer.pause()iOS 上调用[self.ufw pause:YES]。重新进入时再resume。这个坑在 Vuforia 场景下尤其明显因为摄像头是独占资源。5. 进阶技巧让桥接更稳的几个习惯5.1 消息协议设计别只传裸字符串一开始我图省事JS 和 Unity 之间直接传字符串结果消息类型一多就乱套。后来改成固定 JSON 格式{ type: command, action: rotateModel, payload: { angle: 90 }, callbackId: cb_001 }Unity 侧解析type和action做路由callbackId用于异步回传时匹配请求。这样即使后续加消息类型也不用改桥接层代码。5.2 生命周期同步页面切换时暂停 UnityIonic 的页面栈和 Unity 的渲染循环是两套生命周期。如果不做同步用户切到其他页面时 Unity 还在后台跑耗电且容易出状态问题。我一般会在 Cordova 插件里暴露pauseUnity和resumeUnity两个方法在 Ionic 的ionViewWillEnter和ionViewWillLeave里调用。Android 侧public boolean execute(String action, JSONArray args, CallbackContext callbackContext) { if (pauseUnity.equals(action)) { if (unityPlayer ! null) unityPlayer.pause(); callbackContext.success(); return true; } if (resumeUnity.equals(action)) { if (unityPlayer ! null) unityPlayer.resume(); callbackContext.success(); return true; } // ... }iOS 侧对应[self.ufw pause:YES]和[self.ufw pause:NO]。5.3 验证桥接是否正常的检查清单每次集成完新版本我会按这个顺序过一遍检查项预期结果失败时看哪里插件注册cordova plugin list能看到 UnityBridgeplugin.xml 的 feature nameUnity 初始化调用 initUnity 后视图出现Android logcat / Xcode consoleJS 到 UnityUnity 日志打印收到消息UnitySendMessage 的 GameObject 名Unity 到 JSIonic 控制台收到回调静态方法是否被正确调用页面切换退出再进入 AR 正常pause/resume 是否成对调用这张表我贴在显示器边上每次发版前跑一遍能省掉很多「玄学」问题。5.4 一个容易忽略的细节线程问题UnitySendMessage必须在主线程调用如果你在 Cordova 的异步回调里直接调可能会崩。Android 上我一般用cordova.getActivity().runOnUiThread包一层iOS 上确保在主队列 dispatch。这个坑不常出现但一旦出现就是随机崩溃很难查。从那以后我每次集成桥接层都强制走一遍「初始化 → 发消息 → 收回调 → 切页面 → 再回来」的完整流程确认没有资源泄漏和线程问题才继续写业务。希望帮到你。本文还有配套的精品资源点击获取