ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity 3D 与 Ionic 集成:Cordova 消息桥接环境搭建与避坑指南

Unity 3D 与 Ionic 集成:Cordova 消息桥接环境搭建与避坑指南

简介:这份资源面向需要在 Ionic 混合应用中嵌入 Unity 3D 内容的移动端开发者,重点解决 Android 与 iOS 双平台上 Unity 与 Ionic 之间的通信问题,并可选配合 Vuforia 插件实现 AR 场景。教程已在中等规模的 Unity 商业项目中验证,适合具备 Cordova、Objective-C++ 与 C# 基础的进阶开发者参考。压缩包共 21 个文件,约 424KB,包含 png 截图、mm 与 m 源文件、java 与 h 头文件、xml 配置、xcconfig 构建配置、json 与 js 脚本以及 cs 脚本等,覆盖 iOS 与 Android 两端桥接代码、插件声明和示例页面。已有 228 人学习下载。读者可从中获得完整的双端集成骨架、Unity 与 Ionic 互发消息的实现思路、Vuforia 接入的参考配置,以及按平台拆分的目录结构,便于快速对照搭建自己的混合 AR 应用。

1. UnityIonicIntegration 环境搭建:从 Unity 3D 到 Ionic 的消息通道

把 Unity 3D 场景塞进 Ionic 应用,还要让两边互相发消息,这事听起来像把两个不同星球的居民拉到一张桌子上吃饭。Unity 用 C# 和 Objective-C++ 跟原生层打交道,Ionic 跑在 WebView 里用 JavaScript 说话,中间隔着 Cordova 插件这座桥。我见过太多项目卡在第一步:Unity 导出的 Android 工程和 Ionic 的 Cordova 工程各跑各的,消息发出去石沉大海,或者 iOS 上编译直接报 Objective-C++ 符号找不到。这套环境适合做 AR 应用、3D 产品展示、教育类互动内容的开发者,尤其是已经用 Vuforia 做过识别、想把 Unity 场景嵌进现有 Ionic 壳子里的团队。装不好不会报致命错误,但消息通道是断的,你会在真机上看到 Unity 画面正常渲染、按钮点了没反应,查日志才发现桥接层根本没注册。

2. 环境准备:Unity、Ionic 与 Cordova 的版本对齐

2.1 三个核心组件的版本选择

这套集成方案对版本敏感,不是随便拉最新版就能跑通。Unity 这边,我一般会选 LTS 版本,比如 2021.3 或 2022.3,因为 Vuforia 插件对 Unity 版本有明确支持范围,太新的版本 Vuforia 还没跟上,太老的版本导出 Android 工程时 Gradle 模板又容易出问题。Ionic 侧,CLI 用 7.x 比较稳,底层还是 Cordova 而不是 Capacitor,因为这套集成方案的消息桥接是基于 Cordova 插件机制写的,Capacitor 的插件生命周期不一样,直接套会踩坑。Cordova 版本建议 11.x 或 12.x,Android 平台用 cordova-android@12,iOS 平台用 cordova-ios@7。

Node.js 版本别忽视,Ionic 7 要求 Node 16 以上,但我实测 Node 18 LTS 最稳,Node 20 在某些 Cordova 插件编译时会报 node-gyp 相关错误。Java 环境用 JDK 17,Android Gradle Plugin 8.x 强制要求 JDK 17,用 JDK 11 会在 sync 阶段直接失败。Xcode 这边,如果要做 iOS 集成,Xcode 15 配合 cordova-ios@7 是能跑通的组合,但要注意 Unity 导出的 iOS 工程里 Objective-C++ 代码对 Xcode 版本也有要求,太新的 Xcode 可能对某些旧版 Vuforia 的 bitcode 设置报错。

组件推荐版本最低要求备注
Unity2021.3 LTS / 2022.3 LTS2019.4需匹配 Vuforia 支持范围
Ionic CLI7.x6.x底层走 Cordova
Cordova12.x11.x不用 Capacitor
cordova-android12.x10.x对应 Android API 33
cordova-ios7.x6.x对应 iOS 15+
Node.js18 LTS16.x20.x 有兼容风险
JDK1711AGP 8.x 强制 17
Xcode15.x14.x需支持 Objective-C++

2.2 创建 Ionic 工程并锁定 Cordova 平台

先建 Ionic 工程,用 blank 模板就行,别选 Angular 或 React 的复杂模板,后面集成 Unity 跟前端框架关系不大,越简单越容易排查。

# 安装 Ionic CLI 和 Cordova npm install -g @ionic/cli cordova # 创建 blank 模板工程 ionic start UnityIonicDemo blank --type=angular --capacitor=false # 进入工程目录 cd UnityIonicDemo # 确认 Cordova 集成已启用(Ionic 7 默认可能用 Capacitor) ionic integrations enable cordova # 添加 Android 和 iOS 平台 cordova platform add android@12 cordova platform add ios@7

这里--capacitor=false是关键,Ionic 7 新建工程默认走 Capacitor,但我们的 Unity 消息桥接依赖 Cordova 的插件执行机制,所以必须显式禁用 Capacitor 并启用 Cordova 集成。cordova platform add的时候指定版本号,避免拉到不兼容的大版本。添加完平台后,检查config.xml里有没有<engine name="android" spec="^12.0.0" />这样的锁定,没有的话手动补上。

2.3 Unity 导出 Android 工程的参数配置

Unity 这边,打开你的 3D 项目,切到 Android 平台,在 Build Settings 里勾选 Export Project 而不是 Build And Run。导出前检查 Player Settings 里的几个关键项:Scripting Backend 选 IL2CPP,Target Architectures 勾选 ARM64 和 ARMv7,Minimum API Level 设成 24 以上。如果用了 Vuforia,在 Vuforia Configuration 里确认 Android 的 Camera 权限和 AR 相机设置已经配好。

导出后的 Android 工程目录结构大概是这样的:unityLibrary模块放 Unity 运行时和你的场景代码,launcher模块是 Android 壳。我们要的是unityLibrary里的classes.jar和libs下的 so 库,以及src/main/java里的 UnityPlayerActivity。这些后面要合并到 Cordova 工程的 Android 平台里。

iOS 侧类似,Unity 导出 Xcode 工程后,你会得到UnityFramework.framework和Classes目录下的 Objective-C++ 文件。iOS 集成比 Android 麻烦一点,因为 Cordova 的 iOS 工程结构跟 Unity 导出的 Xcode 工程需要手动合并,不能像 Android 那样直接拷模块。

3. 集成 Unity 到 Cordova 工程:Android 与 iOS 双平台操作

3.1 Android 平台合并 Unity 模块

Android 的合并思路是把 Unity 导出的unityLibrary作为一个模块引入 Cordova 工程的 Android 项目。Cordova 生成的 Android 工程在platforms/android下,用 Android Studio 打开这个目录,然后在settings.gradle里加上 Unity 模块的引用。

// platforms/android/settings.gradle include ':app' include ':unityLibrary' project(':unityLibrary').projectDir = new File('../../unity-export/android/unityLibrary')

然后在app模块的build.gradle里加依赖:

// platforms/android/app/build.gradle dependencies { implementation project(':unityLibrary') implementation fileTree(dir: project(':unityLibrary').projectDir.toString() + '/libs', include: ['*.jar']) }

这两步做完,sync 一下 Gradle,如果报unityLibrary里的build.gradle用了旧版 AGP 语法,需要手动改一下,把compile换成implementation,把buildToolsVersion删掉让根工程统一管理。常见做法是在unityLibrary/build.gradle顶部加上apply from: '../config.gradle'之类的统一配置,但最省事的还是直接改。

接着把 Unity 的AndroidManifest.xml里的 Activity 声明合并到 Cordova 的app/src/main/AndroidManifest.xml里。Unity 的UnityPlayerActivity需要改成继承CordovaActivity或者用一个中间 Activity 来承载,这样才能让 Cordova 的 WebView 和 Unity 的 GLSurfaceView 共存。我一般会新建一个UnityCordovaActivity,在onCreate里先初始化 Cordova 的 WebView,再启动 UnityPlayer。

3.2 iOS 平台合并 UnityFramework

iOS 这边,把 Unity 导出的UnityFramework.framework拖进 Cordova 工程的 Xcode 项目里,在 Build Phases 的 Link Binary With Libraries 里加上。然后要把 Unity 的Classes目录下的.mm和.h文件加到 Cordova 插件的源码里,因为消息桥接的 Objective-C++ 代码就在这些文件里。

// 在 Cordova 插件的 .mm 文件里引入 Unity 接口 #import <UnityFramework/UnityFramework.h> #import "UnityUtils.h" // 获取 UnityFramework 实例 UnityFramework* ufw = [UnityFramework getInstance]; [ufw runUI];

iOS 上最容易翻车的是 bitcode 设置。Unity 导出的 framework 可能带 bitcode,而 Cordova 工程默认不开,需要在 Build Settings 里把 Enable Bitcode 设成 NO,两边保持一致。另外UnityFramework.framework的 Embed 选项要选 Embed & Sign,不然真机跑起来会报 dyld 找不到符号。

3.3 消息桥接层的注册与初始化

消息通道的核心是一个 Cordova 插件,它负责在 JavaScript 和 Unity 的 C# 之间转发消息。Android 侧用UnityPlayer.UnitySendMessage发消息给 Unity,用UnityPlayerActivity里的回调接收 Unity 发来的消息。iOS 侧用UnityFramework的sendMessageToGOWithName方法。

// www/UnityBridge.js var exec = require('cordova/exec'); var UnityBridge = { sendMessage: function(gameObject, method, message, success, error) { exec(success, error, 'UnityBridge', 'sendMessage', [gameObject, method, message]); }, receiveMessage: function(callback) { exec(callback, null, 'UnityBridge', 'registerReceiver', []); } }; module.exports = UnityBridge;

这个 JS 模块暴露两个方法:sendMessage往 Unity 发,receiveMessage注册一个回调接收 Unity 主动推过来的消息。exec的第三个参数UnityBridge是插件名,第四个参数是 action 名,后面数组是参数。Android 和 iOS 的原生实现里分别处理这两个 action,Android 用UnityPlayer.UnitySendMessage,iOS 用UnityFramework的对应方法。

4. 避坑与排查:消息不通、编译失败、真机崩溃的常见原因

4.1 消息发出去 Unity 收不到

现象是 Ionic 里调了sendMessage,Unity 的 C# 脚本里OnMessageReceived方法没被触发。原因通常是 GameObject 名字对不上,或者方法名大小写不一致。Unity 的UnitySendMessage要求 GameObject 必须在场景里处于激活状态,而且方法必须是public void且无参数或单字符串参数。解决方法是先在 Unity 里把接收消息的 GameObject 名字打印出来,确认跟 JS 传过去的一致,然后检查方法签名。

4.2 Android 编译报 duplicate class 或 manifest merger failed

现象是 Gradle sync 或 build 时报Duplicate class或者Manifest merger failed。原因是 Unity 导出的unityLibrary里带了 AndroidX 或 support 库的旧版本,跟 Cordova 工程的依赖冲突。解决办法是在app/build.gradle里加configurations.all { resolutionStrategy { force 'androidx.core:core:1.9.0' } }强制统一版本,或者在AndroidManifest.xml的<application>标签里加tools:replace="android:allowBackup"之类的属性覆盖。

4.3 iOS 真机运行崩溃在 UnityFramework 加载

现象是 Xcode 编译通过,真机跑起来一启动就闪退,日志显示dyld: Library not loaded: @rpath/UnityFramework.framework/UnityFramework。原因是 framework 没有正确嵌入。解决方法是到 Build Phases 的 Embed Frameworks 里确认UnityFramework.framework在列表里,Destination 选 Frameworks,Code Sign On Copy 勾上。如果还是不行,检查Runpath Search Paths里有没有@executable_path/Frameworks。

4.4 Vuforia 相机在 Ionic WebView 上层显示黑屏

现象是 Unity 场景里的 Vuforia AR 相机画面是黑的,但 3D 模型能正常渲染。原因是 Cordova 的 WebView 盖在了 Unity 的 GLSurfaceView 上面,相机预览层被遮挡。解决办法是在 Android 侧把 WebView 的背景设成透明,并且调整 View 的 Z 轴顺序,让 Unity 的 SurfaceView 在 WebView 之上。iOS 侧类似,需要调整UnityFramework的rootView层级。

4.5 消息回调在 iOS 上只触发一次

现象是receiveMessage注册的回调第一次能收到 Unity 消息,后面就不触发了。原因是 iOS 的UnityFramework在发送消息时用了sendMessageToGOWithName,但 Cordova 插件的回调 block 被释放了。解决办法是在插件里把回调 block 用strong引用持有,或者改用NSNotificationCenter做消息中转,让 Unity 发通知、插件监听通知再转发给 JS。

5. 验证消息通道:从 JS 到 C# 再回来的完整链路测试

装完不是看编译过不过,而是看消息能不能双向跑通。我一般会写一个最小验证场景:Ionic 页面上放一个按钮,点击后往 Unity 发一个字符串,Unity 收到后把字符串反转再发回 Ionic,Ionic 弹窗显示反转后的结果。这个链路走通,说明桥接层、插件注册、原生转发、Unity 接收和回传全部正常。

先在 Unity 场景里建一个空 GameObject,命名UnityBridgeReceiver,挂上这个 C# 脚本:

// UnityBridgeReceiver.cs using UnityEngine; public class UnityBridgeReceiver : MonoBehaviour { // 供 Android/iOS 原生层调用的方法,必须是 public void 且参数为 string public void OnMessageFromIonic(string message) { Debug.Log("收到 Ionic 消息: " + message); // 反转字符串 char[] charArray = message.ToCharArray(); System.Array.Reverse(charArray); string reversed = new string(charArray); // 回传给 Ionic SendMessageToIonic(reversed); } void SendMessageToIonic(string message) { // Android 用 UnityPlayer.UnitySendMessage,iOS 用 UnityFramework 的对应方法 // 这里统一走一个平台判断 #if UNITY_ANDROID && !UNITY_EDITOR using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity")) { activity.Call("onUnityMessage", message); } #elif UNITY_IOS && !UNITY_EDITOR // iOS 侧通过 Objective-C++ 桥接回传 UnitySendMessageToIonic(message); #endif } #if UNITY_IOS && !UNITY_EDITOR [System.Runtime.InteropServices.DllImport("__Internal")] private static extern void UnitySendMessageToIonic(string message); #endif }

这段 C# 里,OnMessageFromIonic是原生层通过UnitySendMessage调用的入口,方法名和 GameObject 名必须跟 JS 侧传过来的一致。反转逻辑只是为了让验证结果肉眼可辨。回传部分 Android 走AndroidJavaClass反射调用 Activity 的方法,iOS 走DllImport调用 Objective-C++ 里暴露的 C 函数。

Ionic 侧在页面里这样调:

// home.page.ts import { Component } from '@angular/core'; import { UnityBridge } from '../plugins/UnityBridge'; @Component({ selector: 'app-home', templateUrl: 'home.page.html', }) export class HomePage { result: string = ''; constructor() { // 注册接收 Unity 回传消息的回调 UnityBridge.receiveMessage((message: string) => { this.result = message; console.log('收到 Unity 回传:', message); }); } sendToUnity() { // 往 Unity 的 UnityBridgeReceiver 发消息 UnityBridge.sendMessage( 'UnityBridgeReceiver', 'OnMessageFromIonic', 'hello', () => console.log('发送成功'), (err) => console.error('发送失败', err) ); } }

receiveMessage在构造函数里注册一次,后面 Unity 每次回传都会触发这个回调。sendMessage的四个参数分别是 GameObject 名、方法名、消息内容、成功回调和失败回调。点击按钮后,如果一切正常,页面上会显示olleh。

验证的时候按这个顺序排查:先看 Chrome 的 console 有没有发送成功,没有的话查 Cordova 插件有没有正确注册;有的话看 Android Studio 的 Logcat 或 Xcode 的控制台有没有 Unity 的收到 Ionic 消息: hello,没有的话查 GameObject 名字和方法签名;有收到但没回传,查回传路径的原生方法有没有被调用;回传了但 JS 没收到,查receiveMessage的回调是不是被释放了。

从那以后我每次集成完这套环境,都会先跑一遍这个反转字符串的链路,确认双向通了再往里面塞业务逻辑。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进