
1. 项目概述与核心价值最近在做一个混合应用项目需要把Unity做的3D模型和交互场景无缝嵌入到Flutter的App里并且两边还得能“对话”。比如用户在Flutter界面点个按钮Unity里的角色要动起来反过来Unity里发生了一个事件比如捡到道具Flutter的UI状态也得实时更新。这听起来像是把两个完全不同的“世界”打通一开始我也觉得头大Unity用C#Flutter用Dart怎么通信经过一番折腾最终用flutter-unity-view-widget这个插件漂亮地解决了问题。这不仅仅是简单地把Unity视图塞进去关键是实现了稳定、高效的双向消息传递让Flutter和Unity真正成为了一个有机整体。如果你也在面临类似的混合开发需求比如用Flutter构建App的主框架和UI用Unity来承载核心的3D展示、AR体验或者复杂的游戏逻辑那么这篇文章就是为你准备的。我会从为什么选这个方案讲起一步步拆解集成步骤、通信原理并分享我踩过的坑和总结的实战技巧。整个过程不需要你精通底层原生开发但需要对Flutter和Unity的基本开发流程有所了解。最终你将能构建一个可以双向“喊话”的Fltter-Unity混合应用。2. 技术选型与方案对比为什么是flutter-unity-view-widget在决定使用flutter-unity-view-widget之前我们其实评估过几种常见的Flutter与Unity集成方案。2.1 常见方案对比第一种是纯原生桥接。也就是分别在Android用Java/Kotlin和iOS用Swift/Objective-C端写原生代码把Unity作为一个Activity或UIViewController嵌入然后通过Method Channel与Flutter通信。这种方式最灵活但工作量巨大需要维护三端Flutter、Android、iOS的通信代码一致性很难保证调试起来更是噩梦。第二种是使用Unity as a Library。Unity官方提供了将Unity项目导出为Android.aar库或iOS.framework的方案。然后你在原生端集成这个库再通过Flutter插件与Flutter通信。这比第一种方案规范一些但依然绕不开编写和维护大量的原生桥接代码对于主要做Flutter开发的团队来说心智负担很重。第三种也就是我们选择的flutter-unity-view-widget插件方案。它是一个社区维护的Flutter插件核心价值在于它封装了上述第二种方案中最繁琐的原生桥接部分提供了一个现成的Flutter WidgetUnityWidget和一套Dart API。开发者几乎可以像使用普通Flutter组件一样使用Unity视图并通过简单的API调用完成双向通信。它抽象得足够好让我们能聚焦于业务逻辑而不是底层通信的细节。2.2 flutter-unity-view-widget的核心优势开发体验友好提供纯Dart侧的APIFlutter开发者无需深入编写Android/iOS原生代码即可完成集成和通信。双向通信开箱即用插件内置了基于Platform Channels的通信机制提供了UnityWidgetController来发送消息到Unity也暴露了从Unity回调到Flutter的接口。平台一致性一套Dart代码同时在Android和iOS上运行避免了双端原生代码不一致的问题。活跃的社区虽然非官方但该插件在GitHub上star数不少Issues和讨论比较活跃遇到问题有地方可查。当然它也有局限。比如对Unity版本和导出设置有一定要求深度定制原生行为时可能仍需介入原生代码。但对于绝大多数需要“嵌入Unity视图并通信”的场景它无疑是目前最平衡、最高效的选择。注意插件的兼容性是需要关注的第一点。务必查看插件pub.dev页面或GitHub仓库的README确认其支持的Flutter和Unity版本。我这次用的是flutter-unity-view-widget: ^6.0.0搭配Unity 2021.3 LTS和Flutter 3.16运行稳定。3. 环境准备与Unity项目导出万事开头难集成的第一步往往卡在环境配置和项目导出上。这部分我会详细说明每一步的操作和背后的原因。3.1 环境与工具清单确保你的开发环境包含以下工具且版本尽量匹配Flutter环境安装Flutter SDK并配置好环境变量。通过flutter doctor检查确保Android和iOS的开发环境Android Studio/Xcode、模拟器或真机都已就绪。Unity Hub Unity编辑器安装Unity Hub并通过它安装一个Long-Term Support (LTS) 版本的Unity如2021.3或2022.3。LTS版本更稳定社区插件兼容性更好。记住你安装的Unity版本号后续导出设置需要。IDE用于Flutter开发的IDEVS Code或Android Studio和用于Unity开发的IDEVisual Studio或JetBrains Rider。3.2 创建与准备Unity项目新建或打开现有Unity项目。如果你的Unity项目比较复杂建议先新建一个简单的测试项目比如只包含一个立方体和一段简单脚本用于验证通信流程。关键脚本UnityMessageManager。flutter-unity-view-widget依赖一个名为UnityMessageManager的C#脚本来处理通信。你通常需要从插件的示例工程中找到这个脚本文件可能在Assets/FlutterUnityIntegration/Scripts/目录下并将其复制到你Unity项目的Assets目录中。这是通信的基石没有它消息无法被Unity接收和处理。编写测试通信的C#脚本。在Unity中创建一个新的C#脚本例如CommunicationTester.cs并将其挂载到场景中的某个游戏对象上如主摄像机。这个脚本将负责接收Flutter发来的消息以及向Flutter发送消息。// CommunicationTester.cs using UnityEngine; using System; // 为了使用Action public class CommunicationTester : MonoBehaviour { // 1. 接收来自Flutter的消息 // 方法名“OnFlutterMessage”可以被自定义但需要与Flutter端发送时指定的方法名一致。 public void OnFlutterMessage(string message) { Debug.Log($Unity 收到来自Flutter的消息: {message}); // 这里可以根据消息内容执行不同的Unity逻辑 if (message rotate) { // 例如让某个物体旋转 GameObject.Find(Cube).transform.Rotate(0, 90, 0); } } // 2. 向Flutter发送消息 public void SendMessageToFlutter() { // 通过UnityMessageManager.Instance.SendMessageToFlutter发送消息 // 第一个参数是“消息主题”第二个参数是“消息内容”字符串 UnityMessageManager.Instance.SendMessageToFlutter(onUnityEvent, Unity说按钮被点击了); } // 可以在Unity的Update中测试或者由某个UI按钮触发SendMessageToFlutter void Update() { if (Input.GetMouseButtonDown(0)) // 点击屏幕时触发 { SendMessageToFlutter(); } } }3.3 导出Unity项目为Android/iOS库这是最关键也最容易出错的一步。打开Build Settings在Unity编辑器中点击File - Build Settings。切换平台在Platform列表中选择Android或iOS然后点击Switch Platform。这个过程可能会花费一些时间。Android导出关键设置Texture Compression如果你要发布到Google Play考虑到设备兼容性可以选择ETC2支持OpenGL ES 3.0或使用ASTC。对于开发和测试可以保持默认。Minimum API Level设置与你Flutter项目中android/app/build.gradle里minSdkVersion一致或更低的版本。Scripting Backend选择IL2CPP这是目前更推荐的方式兼容性和性能更好。Target Architectures勾选ARMv7和ARM64。只勾选ARM64可能会丢失一部分老旧设备支持。Export Project这是最重要的一步不要直接点击Build And Run。你需要勾选底部的Export Project复选框。点击Export选择一个空文件夹例如unity_android_export来保存导出的Android工程。iOS导出关键设置切换平台到iOS。Target SDK选择Device SDK。Architecture选择ARM64。同样勾选Export Project。点击Export选择一个空文件夹例如unity_ios_export保存导出的Xcode工程。实操心得为什么必须勾选Export Project因为flutter-unity-view-widget插件需要的是Unity导出的完整原生工程而不是一个最终的.apk或.ipa包。插件脚本会在后续步骤中自动从这个导出工程里提取必要的库和资源文件并集成到你的Flutter项目中。直接Build出来的安装包是没法被集成的。4. Flutter项目集成与基础嵌入现在我们转向Flutter一侧将导出的Unity工程集成进来。4.1 创建Flutter项目并添加依赖flutter create flutter_unity_demo cd flutter_unity_demo打开pubspec.yaml文件在dependencies下添加flutter-unity-view-widget。dependencies: flutter: sdk: flutter flutter_unity_widget: ^6.0.0 # 请使用当时的最新稳定版本然后运行flutter pub get获取依赖。4.2 集成Unity导出工程Android端在Flutter项目的android目录下创建一个新的目录例如unityLibs。路径flutter_unity_demo/android/unityLibs。将之前Unity导出的Android工程文件夹unity_android_export整个复制到unityLibs目录下。最终结构类似flutter_unity_demo/android/unityLibs/unity_android_export。修改Flutter项目的android/settings.gradle文件在末尾添加以下代码告诉Gradle包含这个Unity模块// android/settings.gradle include :unityLibs:unity_android_export project(:unityLibs:unity_android_export).projectDir new File(unityLibs/unity_android_export)修改Flutter项目的android/app/build.gradle文件在dependencies块中添加对Unity模块的依赖// android/app/build.gradle dependencies { implementation project(:unityLibs:unity_android_export) // ... 其他依赖 }4.3 集成Unity导出工程iOS端iOS端的集成相对更“自动化”一些但前提是必须在Mac电脑上进行。将之前Unity导出的iOS工程文件夹unity_ios_export整个复制到Flutter项目的ios目录下。最终结构类似flutter_unity_demo/ios/unity_ios_export。打开终端进入Flutter项目的ios目录运行pod install。flutter-unity-view-widget插件的iOS端代码配置了Podfile它会自动识别unity_ios_export目录并将其作为原生依赖集成进来。常见问题排查如果pod install失败常见原因是导出的iOS工程路径不对或者CocoaPods版本问题。确保unity_ios_export目录直接放在ios文件夹下并且其包含Unity-iPhone.xcodeproj文件。可以尝试pod repo update更新仓库或升级CocoaPods。4.4 在Flutter页面中嵌入Unity视图完成上述集成后就可以在Dart代码中使用UnityWidget了。// lib/main.dart (部分代码) import package:flutter/material.dart; import package:flutter_unity_widget/flutter_unity_widget.dart; class UnityDemoScreen extends StatefulWidget { const UnityDemoScreen({super.key}); override StateUnityDemoScreen createState() _UnityDemoScreenState(); } class _UnityDemoScreenState extends StateUnityDemoScreen { UnityWidgetController? _unityWidgetController; // 这个回调在Unity视图创建好后触发提供了控制器 void _onUnityCreated(UnityWidgetController controller) { this._unityWidgetController controller; // 可以在这里向Unity发送初始消息 // controller.postMessage(GameObjectName, MethodName, Argument); } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(Flutter Unity 通信Demo)), body: Column( children: [ // 关键UnityWidget Expanded( child: UnityWidget( onUnityCreated: _onUnityCreated, onUnityMessage: _onUnityMessage, // 监听来自Unity的消息 fullscreen: false, ), ), // Flutter侧的按钮用于触发向Unity发送消息 Padding( padding: const EdgeInsets.all(20.0), child: ElevatedButton( onPressed: _sendMessageToUnity, child: const Text(让Unity中的立方体旋转), ), ), ], ), ); } // 发送消息到Unity void _sendMessageToUnity() { if (_unityWidgetController ! null) { // postMessage 参数说明 // 参数1: Unity场景中的GameObject名称 // 参数2: 挂载在该GameObject上的脚本的公共方法名 // 参数3: 传递给该方法的参数字符串 _unityWidgetController!.postMessage( Main Camera, // GameObject名我们脚本挂在了主摄像机上 OnFlutterMessage, // 方法名对应C#脚本中的 public void OnFlutterMessage(string message) rotate, // 参数对应C#方法中的 message 变量 ); } else { print(Unity控制器未就绪); } } // 接收来自Unity的消息 void _onUnityMessage(dynamic message) { // message 通常是一个字符串格式可以是简单的字符串也可以是JSON print(Flutter 收到来自Unity的消息: $message); // 在这里可以更新Flutter的UI状态例如显示一个SnackBar ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(Unity说: $message)), ); } }运行Flutter项目 (flutter run)你应该能看到Unity的3D场景被嵌入到Flutter页面中点击Flutter按钮Unity中的立方体会旋转。点击Unity场景模拟我们C#脚本中Update里的鼠标点击Flutter底部会弹出SnackBar显示消息。至此基础的双向通信链路已经打通5. 双向通信机制深度解析与实战优化上一节我们实现了最基本的通信但实际项目中的需求远比这复杂。我们需要深入理解通信机制并对其进行封装和优化。5.1 通信原理剖析flutter-unity-view-widget的通信底层基于Flutter的Platform Channels。简单理解它建立了一条Flutter(Dart)与原生平台(Java/Kotlin, Swift/ObjC)之间的“管道”而原生平台又与Unity通过C#接口建立了另一条“管道”。消息在这三条链路中传递Dart - Platform Channel - Native (Android/iOS) - Unity C#。Flutter - Unity当你在Dart中调用_unityWidgetController.postMessage(...)时插件会通过Method Channel调用原生代码原生代码再通过UnityPlayer.UnitySendMessage这个Unity提供的原生接口将消息转发给指定的Unity GameObject和方法。Unity - Flutter当在Unity C#中调用UnityMessageManager.Instance.SendMessageToFlutter(...)时UnityMessageManager脚本会通过原生插件回调到Flutter的UnityWidget最终触发我们在Dart中注册的onUnityMessage回调。5.2 复杂数据传递从字符串到JSON我们之前的例子只传递了简单的字符串rotate。实际应用中我们需要传递结构化数据。解决方案是使用JSON。Flutter端发送复杂数据void _sendComplexDataToUnity() { if (_unityWidgetController ! null) { final command { action: updatePlayer, data: { health: 85, position: {x: 10.5, y: 0, z: 20.3}, inventory: [sword, potion] } }; // 将Map转换为JSON字符串 final jsonString jsonEncode(command); _unityWidgetController!.postMessage( GameManager, // 假设有一个管理GameObject OnJsonMessage, jsonString, // 发送JSON字符串 ); } }Unity C#端解析JSON 你需要安装并使用一个JSON库比如Unity自带的Newtonsoft.Json通过Package Manager安装或者UnityEngine.JsonUtility功能较简单。using Newtonsoft.Json; // 使用Newtonsoft.Json using System; [System.Serializable] // 如果使用JsonUtility需要这个标签 public class PlayerData { public string action; public PlayerDataInner data; } [System.Serializable] public class PlayerDataInner { public int health; public Vector3 position; // 注意JsonUtility不能直接序列化Vector3需要自定义结构体 public Liststring inventory; } public void OnJsonMessage(string jsonMessage) { Debug.Log($收到JSON消息: {jsonMessage}); // 使用Newtonsoft.Json反序列化 var playerData JsonConvert.DeserializeObjectPlayerData(jsonMessage); if (playerData.action updatePlayer) { int health playerData.data.health; // ... 处理数据 } // 或者使用UnityEngine.JsonUtility (需要定义可序列化类) // PlayerData data JsonUtility.FromJsonPlayerData(jsonMessage); }5.3 通信封装与状态管理直接在UI层调用postMessage和解析onUnityMessage会导致代码混乱。更好的做法是进行封装。创建通信服务类// lib/services/unity_communication_service.dart import package:flutter_unity_widget/flutter_unity_widget.dart; class UnityCommunicationService { UnityWidgetController? _controller; final _unityMessageStreamController StreamControllerString.broadcast(); StreamString get unityMessageStream _unityMessageStreamController.stream; void setController(UnityWidgetController controller) { _controller controller; } void sendCommand(String gameObject, String method, String message) { _controller?.postMessage(gameObject, method, message); } void sendJsonCommand(String gameObject, String method, MapString, dynamic data) { sendCommand(gameObject, method, jsonEncode(data)); } // 供UnityWidget回调 void onUnityMessage(dynamic message) { _unityMessageStreamController.add(message.toString()); } void dispose() { _unityMessageStreamController.close(); } }与状态管理结合如Provider// 在Provider中持有ServiceUI通过Provider监听消息并发送命令 class UnityProvider extends ChangeNotifier { final UnityCommunicationService _unityService UnityCommunicationService(); String _lastUnityEvent ; UnityCommunicationService get service _unityService; String get lastUnityEvent _lastUnityEvent; UnityProvider() { // 监听Unity消息流并更新状态 _unityService.unityMessageStream.listen((message) { _lastUnityEvent 收到: $message; notifyListeners(); // 通知UI更新 _handleUnityMessage(message); // 处理消息逻辑 }); } void _handleUnityMessage(String message) { // 解析消息并可能触发其他Provider或逻辑 print(处理消息: $message); } override void dispose() { _unityService.dispose(); super.dispose(); } }这样你的UI代码将变得非常清晰只需要调用Provider.ofUnityProvider(context).service.sendCommand(...)即可。5.4 性能与内存管理注意事项UnityWidget生命周期当包含UnityWidget的页面被销毁时如pop务必确保Unity视图被正确释放。UnityWidgetController提供了dispose()方法应在State的dispose生命周期中调用。override void dispose() { _unityWidgetController?.dispose(); super.dispose(); }消息频率避免在每一帧如Flutter的build方法或Unity的Update中高频次发送消息这会给通信通道带来压力。对于连续的状态同步如角色位置可以考虑在Unity端累积数据以较低频率如每秒10次批量发送或者只在状态发生显著变化时发送。纹理与分辨率在Unity导出时注意纹理压缩格式和分辨率设置过高的分辨率会导致内存占用激增在移动设备上可能引起OOM内存溢出。尤其是在嵌入多个Unity视图或与其他高内存消耗功能共存时。后台处理当App进入后台时Unity引擎默认会暂停。你需要根据业务需求决定是否允许Unity在后台运行。这通常需要在原生端进行额外配置flutter-unity-view-widget插件可能提供相关的选项或需要你自己扩展原生代码。6. 常见问题排查与调试技巧实录集成过程中你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方法。6.1 Unity视图黑屏或无法加载症状Flutter页面中UnityWidget区域一片黑或者一直显示加载中。排查步骤检查集成步骤这是最常见的原因。请严格按照第3、4节的步骤确认Unity导出工程是否放在了Flutter项目正确的路径下android/unityLibs/和ios/以及Gradle和CocoaPods的配置是否正确。重新运行flutter pub get和pod installiOS。检查Unity导出设置确认Unity导出时勾选了Export Project并且选择了正确的目标平台Android/iOS。查看日志运行flutter run -v查看详细日志或者在Android Studio/Xcode中运行并查看Logcat/Console输出寻找Unity或原生插件相关的错误信息。权限问题Android检查android/app/src/main/AndroidManifest.xml确保有必要的权限如互联网权限如果Unity内容需要联网。flutter-unity-view-widget通常会自动添加所需权限但最好确认一下。简单场景测试使用一个极其简单的Unity场景如默认的蓝色天空盒和一个立方体重新导出并集成排除是复杂场景导致的问题。6.2 消息发送后Unity无反应症状Flutter端调用postMessage没有报错但Unity端收不到消息或者对应的方法没有执行。排查步骤确认GameObject名称和方法名这是最容易出错的地方。postMessage的第一个参数是Unity场景中真实存在的、激活的GameObject的名称区分大小写。第二个参数是该GameObject上挂载的脚本中的一个public方法名。请打开Unity编辑器在Hierarchy面板中确认GameObject的名字在Inspector面板中确认脚本和方法名。检查UnityMessageManager确保UnityMessageManager脚本被正确地放到了Unity项目的Assets目录下并且场景中至少有一个激活的GameObject挂载了它或者有脚本引用了它。它是消息接收的枢纽。使用Unity编辑器调试在Unity编辑器中运行游戏查看Console窗口是否有来自Flutter的消息日志。确保你C#脚本中的调试代码如Debug.Log被执行了。参数类型匹配postMessage的第三个参数必须是字符串。如果你需要传数字或其他类型请先转换为字符串在Unity端再解析。6.3 从Unity发送消息Flutter收不到症状Unity端调用SendMessageToFlutter但Flutter端的onUnityMessage回调没有触发。排查步骤检查Flutter端监听确认创建UnityWidget时onUnityMessage回调函数被正确设置。检查消息格式SendMessageToFlutter发送的消息会被转换为字符串传递给Flutter。确保发送的内容是有效的。检查Flutter Widget生命周期确保接收消息的Flutter Widget及其State在消息到达时仍然处于活动状态没有被销毁。如果消息发送时页面已经pop了自然接收不到。可以考虑使用全局的、生命周期更长的服务如之前封装的UnityCommunicationService来监听消息。使用打印调试在Flutter的onUnityMessage回调里第一行就打印日志确认回调是否被触发。6.4 iOS构建失败CocoaPods相关症状在iOS目录下运行pod install失败错误信息可能与UnityFramework相关。解决方案确保导出的iOS工程路径正确且包含UnityFramework。尝试清理Pod缓存pod cache clean --all然后删除ios/Podfile.lock和ios/Pods目录再重新运行pod install。检查Flutter项目的ios/Podfile文件确保没有与其他插件冲突的配置。flutter-unity-view-widget的安装说明通常会要求你在Podfile中添加use_frameworks!和platform :ios, 11.0或更高的配置。如果遇到UnityFramework找不到头文件等错误可能需要手动在Xcode中检查Framework Search Paths和Header Search Paths。不过如果按照插件文档操作通常不会遇到此问题。6.5 真机调试与发布构建真机调试在真机上测试是必须的模拟器可能无法正常运行Unity内容。确保你的Android手机开启了开发者选项和USB调试iOS手机设置了信任开发者证书。发布构建Android运行flutter build apk --release或flutter build appbundle --release。如果构建失败检查android/app/build.gradle中是否启用了混淆minifyEnabled。Unity的代码可能被混淆破坏建议在初期关闭混淆或仔细配置混淆规则proguard-rules.pro来保留Unity和插件相关的类。发布构建iOS运行flutter build ipa --release。需要在Xcode中配置正确的签名Signing Capabilities。特别注意Unity导出的iOS项目可能包含Bitcode。Flutter默认不支持Bitcode。你需要在Xcode中将UnityFramework和你的Flutter App Target的Build Settings中的Enable Bitcode设置为NO。最后保持耐心善用搜索引擎和flutter-unity-view-widget的GitHub Issues页面你遇到的问题很可能别人已经遇到并解决了。记住从最简单的“Hello World”式通信开始逐步增加复杂度是调试这类混合集成问题最有效的方法。当你看到Flutter的UI和Unity的3D世界流畅交互时所有的折腾都是值得的。