三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity跨平台二维码识别实战:基于ZXing.NET的扫码登录与优化

Unity跨平台二维码识别实战:基于ZXing.NET的扫码登录与优化

1. 项目概述

最近在做一个Unity项目,需要实现一个扫码登录的功能,核心就是二维码的识别。市面上方案很多,但综合考虑成熟度、稳定性和上手成本,我最终还是选择了ZXing这个老牌的开源库。ZXing(Zebra Crossing)在Java和.NET生态里名气很大,用它来处理二维码识别,可以说是“站在巨人的肩膀上”。不过,把ZXing集成到Unity里,特别是要处理好跨平台(PC、移动端)的摄像头调用和图像处理,还是有不少细节需要注意的。这篇文章,我就把自己从零搭建Unity二维码识别功能,基于ZXing.NET实现的全过程,包括踩过的坑和优化心得,完整地梳理一遍。无论你是想给游戏加个扫码兑换礼包的功能,还是为应用实现一个扫码登录的入口,这篇实战指南应该都能帮到你。

2. 核心思路与技术选型

2.1 为什么选择ZXing.NET?

在做技术选型时,我主要对比了三种主流方案:纯自己写识别算法、使用专门的Unity Asset Store插件、以及集成ZXing这类成熟的开源库。

自己写算法首先被排除了,二维码的编解码规范(QR Code Specification)相当复杂,涉及纠错码、掩码、格式信息等,从头实现不仅周期长,而且识别率和鲁棒性很难保证,属于“重复造轮子”且造不好的那种。

Asset Store里确实有不少优秀的二维码插件,它们通常封装得很好,提供拖拽式的组件和友好的编辑器界面。但问题在于,第一是成本,商业插件需要付费;第二是灵活性,插件的核心代码往往是DLL形式,难以进行深度定制或问题排查;第三是长期维护,插件的更新可能跟不上Unity版本或目标平台的迭代。

ZXing.NET则完美避开了这些问题。首先,它是完全开源且免费的,拥有庞大的社区和长期的维护历史,代码质量有保障。其次,它是一个纯粹的.NET库,以DLL形式引入Unity项目后,我们可以完全掌控其调用过程,从图像预处理到结果解析的每一个环节都可以介入和优化。最后,它的识别核心经过了海量测试,识别速度和准确率都非常可靠。对于需要将二维码功能深度集成到业务逻辑中的项目来说,ZXing.NET提供了最佳的可控性和灵活性。

2.2 整体架构设计

我们的目标是在Unity中实现一个完整的二维码识别流程,它需要包含以下核心模块:

  1. 摄像头管理模块:负责打开/关闭设备摄像头,获取实时的视频流数据(WebCamTexture),并将其渲染到UI上供用户预览。
  2. 图像捕捉与预处理模块:定时或按需从视频流中抓取一帧图像。这一帧图像可能需要经过预处理,例如缩放、灰度化、二值化或对比度增强,以提高后续识别的成功率。
  3. ZXing识别核心模块:将预处理后的图像数据(通常是Color32[]byte[])传递给ZXing库的BarcodeReader进行解码。
  4. 结果处理与反馈模块:解析ZXing返回的Result对象,提取出二维码中的文本信息。然后根据业务逻辑进行后续操作,比如跳转链接、解析JSON数据、更新UI提示等,同时给用户提供视觉或听觉的识别成功反馈。

整个数据流可以概括为:摄像头硬件 -> WebCamTexture -> 图像帧 -> 预处理 -> ZXing解码器 -> 文本结果 -> 业务逻辑。这个流程看起来简单,但每个环节都有优化点,后面会详细展开。

3. 环境准备与ZXing集成

3.1 创建Unity项目与导入ZXing.unity.dll

首先,创建一个新的Unity项目(这里我使用的是2022.3 LTS版本,比较稳定)。ZXing.NET的集成非常简单,我们不需要从源码编译。

  1. 获取ZXing.unity.dll:访问ZXing.Net的GitHub Release页面(例如https://github.com/micjahn/ZXing.Net/releases),下载最新的稳定版本。解压后,在netstandard2.0netstandard2.1文件夹中(根据你的Unity .NET兼容性级别选择),找到zxing.unity.dll文件。这个DLL是专门为Unity的运行时环境编译的。
  2. 导入Unity:在Unity项目的Assets文件夹下,创建一个Plugins文件夹(如果不存在),然后将zxing.unity.dll直接拖入。Unity会自动识别并导入它。你可以在Inspector窗口中确认其平台设置,通常保持默认(Any Platform)即可。

注意:有些教程可能会让你导入整个ZXing.Net的NuGet包或源代码,但对于Unity来说,直接使用官方提供的unity.dll是最省事且兼容性最好的方式。自己编译可能会遇到命名空间或依赖问题。

3.2 基础场景搭建

我们需要两个简单的UI界面:

  • 一个用于显示摄像头预览的RawImage
  • 一个用于开始/停止扫描的Button
  • 一个用于显示识别结果的Text组件

在场景中创建一个Canvas,然后按需布置这些UI元素。将RawImage的锚点设置为拉伸全屏,以便适配不同分辨率。给开始扫描的按钮挂上一个空的脚本,我们稍后来填充事件。

4. 核心代码实现与解析

接下来是重头戏,我们将一步步实现摄像头调用、图像捕捉和ZXing识别。

4.1 摄像头控制与预览

Unity提供了WebCamTexture类来访问摄像头。我们的第一步是获取设备并开启预览。

using UnityEngine; using UnityEngine.UI; public class QRCodeScanner : MonoBehaviour { public RawImage cameraPreview; // 用于显示摄像头画面的UI public Text resultText; // 用于显示识别结果的UI private WebCamTexture webCamTexture; private bool isScanning = false; void Start() { // 初始化时可以先不打开摄像头,等待用户点击按钮 // 检查设备是否有摄像头 if (WebCamTexture.devices.Length == 0) { Debug.LogError("未找到摄像头设备!"); resultText.text = "未找到摄像头"; return; } } public void StartScanning() { if (isScanning) return; // 通常使用第一个找到的摄像头,对于手机,这可能是后置摄像头 // 更健壮的做法是遍历设备,寻找名称中包含"back"或分辨率更高的 WebCamDevice device = WebCamTexture.devices[0]; // 设置一个合适的分辨率,过高会影响性能,过低会影响识别率 webCamTexture = new WebCamTexture(device.name, 640, 480, 30); cameraPreview.texture = webCamTexture; cameraPreview.GetComponent<AspectRatioFitter>().aspectRatio = (float)webCamTexture.width / webCamTexture.height; webCamTexture.Play(); isScanning = true; resultText.text = "正在扫描..."; } public void StopScanning() { if (webCamTexture != null && webCamTexture.isPlaying) { webCamTexture.Stop(); } isScanning = false; cameraPreview.texture = null; resultText.text = "已停止"; } }

这里有几个关键点:

  • 设备选择WebCamTexture.devices[0]不一定总是你想要的摄像头。在移动设备上,可能需要遍历设备列表,通过device.namedevice.isFrontFacing来判断前后置摄像头。
  • 分辨率与帧率new WebCamTexture(device.name, width, height, fps)。分辨率不宜过高,640x480或1280x720对于二维码识别完全足够,且能保证流畅度。帧率30fps也绰绰有余。
  • 画面拉伸:摄像头画面的宽高比可能和RawImage不一致,导致画面变形。使用AspectRatioFitter组件可以保持画面比例,将其挂在RawImage上,并在代码中动态设置aspectRatio

4.2 集成ZXing进行识别

现在,摄像头画面已经出来了,我们需要定时抓取画面并交给ZXing去识别。在Update方法中定时执行识别是一种简单粗暴但有效的方式。

首先,在脚本开头引入ZXing的命名空间:using ZXing;。然后声明一个BarcodeReader实例。

private BarcodeReader barcodeReader; private float interval = 0.2f; // 识别间隔,200毫秒一次 private float timer = 0f; void Start() { // ... 其他初始化 barcodeReader = new BarcodeReader(); // 可以设置一些解码选项,提高识别成功率 barcodeReader.Options = new DecodingOptions { PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE }, // 只识别QR码 TryHarder = true, // 尝试更努力地解码(更耗时) // 设置字符集,确保中文等不乱码 CharacterSet = "UTF-8" }; // 也可以使用AutoRotate,自动尝试旋转图像识别 barcodeReader.AutoRotate = true; } void Update() { if (!isScanning || webCamTexture == null || !webCamTexture.isPlaying) return; timer += Time.deltaTime; if (timer >= interval) { timer = 0f; ScanFrame(); } } private void ScanFrame() { try { // 获取当前帧的像素数据 Color32[] color32s = webCamTexture.GetPixels32(); // 调用ZXing进行解码 Result result = barcodeReader.Decode(color32s, webCamTexture.width, webCamTexture.height); if (result != null) { // 识别成功! Debug.Log($"识别到二维码: {result.Text}"); resultText.text = result.Text; // 触发成功事件,例如停止扫描、播放音效、处理结果等 OnScanSuccess(result.Text); } } catch (System.Exception ex) { Debug.LogWarning($"识别过程中出现异常: {ex.Message}"); // 这里不要轻易停止扫描,可能只是单帧图像问题 } } private void OnScanSuccess(string resultStr) { // 停止扫描,避免重复识别 StopScanning(); // 可以播放一个“嘀”的音效 // AudioSource.PlayClipAtPoint(successClip, Vector3.zero); // 根据结果进行业务处理,例如: // - 如果是URL,用Application.OpenURL打开 // - 如果是JSON字符串,解析后处理 // - 显示一个成功提示框 Debug.Log($"处理结果: {resultStr}"); }

核心解析

  1. barcodeReader.Decode(Color32[] data, int width, int height):这是最关键的调用。ZXing接受一个颜色数组和图像的宽高。WebCamTexture.GetPixels32()正好提供了Color32[]
  2. 识别间隔:在Update中每帧都识别会带来巨大的性能开销。设置一个间隔(如0.2秒)既能保证响应速度,又不会让手机发烫。
  3. 错误处理Decode方法可能会因为图像质量太差等原因抛出异常,一定要用try-catch包裹起来,防止程序崩溃。
  4. 识别成功后的处理:一旦识别成功,通常需要立即停止扫描(StopScanning),并触发后续业务逻辑。这里我抽象了一个OnScanSuccess方法,你可以在这里接入你的游戏逻辑。

4.3 图像预处理优化(进阶)

在光线不佳、二维码距离较远或存在轻微畸变的情况下,直接识别成功率可能会下降。这时,对从摄像头获取的图像进行预处理就非常有效。ZXing本身有一定的容错能力,但我们可以帮它一把。

一个简单而有效的预处理流程是:灰度化 -> 二值化。这能显著提升黑白对比度,让二维码的轮廓更清晰。

我们可以创建一个静态工具类来处理:

using UnityEngine; public static class ImagePreprocessor { /// <summary> /// 将Color32数组转换为灰度字节数组,并进行简单二值化 /// </summary> public static byte[] ConvertToBinaryBuffer(Color32[] colors, int width, int height, int threshold = 128) { byte[] buffer = new byte[width * height]; for (int i = 0; i < colors.Length; i++) { // 计算灰度值 (标准公式: 0.299*R + 0.587*G + 0.114*B) int grayScale = (int)(colors[i].r * 0.299f + colors[i].g * 0.587f + colors[i].b * 0.114f); // 二值化 buffer[i] = (byte)(grayScale < threshold ? 0 : 255); // 0为黑,255为白 } return buffer; } /// <summary> /// 使用LuminanceSource包装预处理后的数据,供ZXing使用 /// </summary> public static RGBLuminanceSource CreateLuminanceSourceFromBuffer(byte[] grayBuffer, int width, int height) { // 这里需要将一维的灰度字节数组转换为ZXing需要的格式 // 注意:RGBLuminanceSource期望的是RGB或RGBA字节数组。 // 我们的灰度buffer是单通道,需要转换成“伪”RGB格式(R=G=B=灰度值) byte[] rgbBuffer = new byte[grayBuffer.Length * 3]; for (int i = 0; i < grayBuffer.Length; i++) { int baseIndex = i * 3; byte gray = grayBuffer[i]; rgbBuffer[baseIndex] = gray; // R rgbBuffer[baseIndex + 1] = gray; // G rgbBuffer[baseIndex + 2] = gray; // B } // 创建LuminanceSource,告诉ZXing这是RGB24格式的数据 return new RGBLuminanceSource(rgbBuffer, width, height, RGBLuminanceSource.BitmapFormat.RGB24); } }

然后,修改我们的ScanFrame方法,使用预处理后的数据:

private void ScanFrame() { try { Color32[] color32s = webCamTexture.GetPixels32(); int width = webCamTexture.width; int height = webCamTexture.height; // --- 图像预处理 --- byte[] binaryBuffer = ImagePreprocessor.ConvertToBinaryBuffer(color32s, width, height); var luminanceSource = ImagePreprocessor.CreateLuminanceSourceFromBuffer(binaryBuffer, width, height); // --- 预处理结束 --- // 使用预处理后的源进行解码 Result result = barcodeReader.Decode(luminanceSource); if (result != null) { OnScanSuccess(result.Text); } } catch (System.Exception ex) { Debug.LogWarning($"识别异常: {ex.Message}"); } }

实操心得:阈值threshold的选择是关键。128是一个中间值。在光线暗的环境下,可以尝试降低阈值(如100);在强光或反光下,可以提高阈值(如160)。更高级的做法是使用自适应阈值算法(如OTSU),但这会带来额外的计算开销,需要权衡。对于大多数室内场景,固定阈值128配合TryHarder = true已经能取得不错的效果。

5. 多平台适配与性能调优

Unity项目最终要发布到不同平台,每个平台在摄像头权限、线程处理和性能表现上都有差异。

5.1 Android与iOS的权限处理

在移动端,访问摄像头需要用户授权。Unity 2021及之后的版本提供了NativeCameraNativeGallery类似的API,但更通用的做法是在启动时或点击扫描按钮时检查并请求权限。

对于Android:需要在AndroidManifest.xml中添加摄像头权限。你可以通过Unity的Player Settings(Player -> Android -> Publishing Settings -> Build -> Custom Main Manifest)来添加,确保包含:

<uses-permission android:name="android.permission.CAMERA" />

在代码中,可以使用UnityEngine.Android.Permission来请求:

if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) { Permission.RequestUserPermission(Permission.Camera); // 需要处理用户拒绝的情况 }

对于iOS:同样需要在Player Settings(Player -> iOS -> Camera Usage Description)中填写摄像头使用描述。代码层面的权限请求,Unity会在底层自动处理,但你需要确保描述文本清晰合理,否则应用商店审核可能被拒。

一个健壮的启动逻辑应该是:检查平台 -> 检查权限 -> 如果未授权则请求 -> 在权限回调中初始化扫描功能。

5.2 性能优化要点

  1. 降低识别分辨率:我们不需要用摄像头的原生全高清分辨率来识别二维码。可以在创建WebCamTexture时使用较低的分辨率(如640x480),或者在GetPixels32()之后,将图像缩放至一个固定的、较小的尺寸(如256x256)再交给ZXing识别。这能大幅减少需要处理的数据量。

    private Texture2D scaledTexture; // ... 在初始化时创建 scaledTexture = new Texture2D(256, 256, TextureFormat.RGBA32, false); private void ScanFrame() { // 获取原图 Color32[] originalPixels = webCamTexture.GetPixels32(); // 缩放(这里使用简单的双线性缩放,有更高要求可用Graphics.CopyTexture) // 注意:这是一个简化示例,实际缩放需要更复杂的逻辑或使用辅助方法库。 // 可以考虑使用 `Texture2D.GetRawTextureData` 和 `Graphics.ConvertTexture` 进行高效缩放。 // 为了示例清晰,此处省略具体缩放实现代码块。 // 假设我们有一个工具方法 FastResize Color32[] resizedPixels = ImageUtils.FastResize(originalPixels, webCamTexture.width, webCamTexture.height, 256, 256); // 使用缩放后的数据进行识别 Result result = barcodeReader.Decode(resizedPixels, 256, 256); }
  2. 控制识别频率:我们已经通过interval做了基础控制。更进一步,可以设计一个“智能间隔”:连续多次识别失败后,适当增加间隔以节省性能;检测到画面剧烈晃动(通过陀螺仪或图像差异)时,暂停识别。

  3. 后台线程识别BarcodeReader.Decode()是一个计算密集型操作,在主线程执行可能导致卡顿。理想情况下,应该将图像数据发送到另一个线程进行解码,完成后再将结果回调给主线程。Unity的C# Job SystemSystem.Threading.Tasks可以用于此目的,但需要注意线程间数据传递的安全性。

    // 伪代码,示意后台线程思路 private void ScanFrameAsync() { Color32[] frameData = webCamTexture.GetPixels32().Clone(); // 必须复制数据,因为WebCamTexture会更新 Task.Run(() => { Result result = barcodeReader.Decode(frameData, width, height); if (result != null) { // 使用Unity主线程调度器来回调,因为UI操作必须在主线程 MainThreadDispatcher.Enqueue(() => OnScanSuccess(result.Text)); } }); }

    注意:多线程编程会增加复杂度,需要处理好数据同步和生命周期管理。如果识别频率不高(如0.2秒一次)且图像尺寸较小,在主线程执行带来的卡顿感可能微乎其微。建议先实现基础版本,性能测试遇到瓶颈时再考虑引入多线程。

  4. 及时释放资源:在OnDisableOnDestroy方法中,务必停止摄像头并释放相关纹理。

    void OnDestroy() { StopScanning(); if (scaledTexture != null) Destroy(scaledTexture); }

6. 实战问题排查与经验总结

在实际开发中,你肯定会遇到各种各样的问题。下面是我踩过的一些坑和解决方案。

6.1 常见问题速查表

问题现象可能原因解决方案
识别不出任何二维码1. 摄像头权限未获取。
2.WebCamTexture没有成功播放。
3. 图像数据未正确传递给ZXing(如宽高参数错误)。
4. 二维码尺寸在画面中太小或太大。
1. 检查并请求摄像头权限,在真机上测试。
2. 检查webCamTexture.isPlaying,确保设备名正确。
3. 打印webCamTexture.width/height,确认与传入Decode的参数一致。
4. 引导用户将二维码置于取景框中央,并占画面足够比例(如1/3到1/2)。
识别率低,时好时坏1. 环境光线太暗或反光。
2. 摄像头对焦不准(移动端)。
3. 识别频率太高,CPU占用满导致图像处理延迟。
1. 增加图像预处理(二值化),动态调整阈值。
2. 尝试在移动端触发自动对焦(需平台特定代码)。
3. 增加识别间隔(如0.3秒),或降低识别图像分辨率。
识别出乱码1. 二维码内容包含非ASCII字符(如中文),但字符集设置不正确。
2. ZXing版本与Unity .NET版本不兼容。
1. 确保barcodeReader.Options.CharacterSet = "UTF-8"
2. 确认使用的zxing.unity.dll版本与项目.NET兼容性级别匹配。
在编辑器里正常,打包后失效1. DLL平台设置错误。
2. 移动端权限描述文件缺失或错误。
3. 代码中使用了编辑器特有的API。
1. 检查zxing.unity.dll的Inspector,确保包含目标平台(如Android, iOS)。
2. 检查AndroidManifest和iOS的Info.plist中权限配置。
3. 使用#if UNITY_EDITOR预处理指令隔离编辑器代码。
画面卡顿或延迟高1. 每帧都调用GetPixels32()Decode(),性能开销大。
2. 图像分辨率过高。
3. UI布局或渲染复杂。
1. 严格使用计时器控制识别频率。
2. 降低WebCamTexture创建时的分辨率,或缩放识别用图。
3. 使用性能分析器(Profiler)定位瓶颈,优化Canvas。

6.2 独家避坑技巧

  1. “预热”ZXing:第一次实例化BarcodeReader和调用Decode可能会比较慢。可以在场景加载后、用户点击扫描前,提前创建好BarcodeReader实例,并用一张小的测试二维码图片进行一次解码,让JIT编译和初始化过程提前完成。

  2. 动态调整扫描框:在UI上绘制一个半透明的扫描框(中间镂空),引导用户将二维码对准该区域。识别时,可以只截取扫描框区域的图像数据进行解码,而不是全屏图像,这能减少数据量并排除背景干扰。

  3. 处理模糊与运动模糊:移动中扫描很容易拍糊。可以在ScanFrame中增加一个简单的图像清晰度判断:计算图像梯度(例如拉普拉斯算子的方差),如果清晰度低于某个阈值,则跳过本帧识别,并提示用户“请保持手机稳定”。

  4. 结果去重与验证:由于是连续识别,很可能在短时间内对同一个二维码解码多次。这可能导致业务逻辑被重复触发。一个简单的解决方案是:在OnScanSuccess中,将本次识别到的文本与上一次成功的文本进行比较,如果相同且在短时间内(如1秒),则忽略此次结果。

  5. 备选识别方案:对于某些极端情况(如严重形变、部分遮挡),ZXing可能无法识别。可以考虑集成一个备用的识别库,或者将图像上传到服务器端进行更强大的算法识别。这属于提升体验的进阶方案。

7. 功能扩展与封装

基础功能跑通后,我们可以考虑将其封装成更易用、更强大的组件或工具类。

7.1 封装成可复用的Scanner组件

将摄像头管理、ZXing识别、结果回调、UI控制等逻辑封装到一个QRCodeScanner组件中。通过Unity事件(UnityEvent)或C#事件(event Action<string>)来暴露扫描成功、失败、状态变化等回调,让业务逻辑脚本只需监听事件即可,实现解耦。

using UnityEngine; using UnityEngine.Events; using ZXing; public class QRCodeScanner : MonoBehaviour { public UnityEvent<string> OnQRCodeScanned; // 扫描成功事件 public UnityEvent OnScanStarted; public UnityEvent OnScanStopped; // ... 内部变量 (webCamTexture, barcodeReader等) public void StartScan() { // 启动扫描逻辑 OnScanStarted?.Invoke(); } public void StopScan() { // 停止扫描逻辑 OnScanStopped?.Invoke(); } private void HandleScanResult(string result) { // 内部处理,如去重验证 if (IsValidNewResult(result)) { OnQRCodeScanned?.Invoke(result); } } // ... 其他私有方法 }

7.2 支持静态图片识别

除了实时摄像头,我们可能还需要识别相册中的图片或项目内的纹理。可以扩展一个静态方法:

public static string DecodeFromTexture2D(Texture2D texture) { BarcodeReader reader = new BarcodeReader(); reader.Options = new DecodingOptions { PossibleFormats = new List<BarcodeFormat> { BarcodeFormat.QR_CODE } }; Result result = reader.Decode(texture.GetPixels32(), texture.width, texture.height); return result?.Text; }

7.3 生成二维码功能

ZXing同样可以用于生成二维码。我们可以提供一个工具方法,根据输入的字符串生成对应的Texture2D,方便在UI上显示。

public static Texture2D GenerateQRCodeTexture(string text, int width = 256, int height = 256, Color? codeColor = null) { BarcodeWriter writer = new BarcodeWriter(); writer.Format = BarcodeFormat.QR_CODE; writer.Options = new EncodingOptions { Width = width, Height = height, Margin = 1, PureBarcode = false }; writer.Options.Hints[EncodeHintType.CHARACTER_SET] = "UTF-8"; Color32[] colorData = writer.Write(text); Texture2D tex = new Texture2D(width, height); tex.SetPixels32(colorData); tex.Apply(); // 如果需要自定义颜色,可以在这里遍历像素进行替换 if (codeColor.HasValue) { Color32 targetColor = codeColor.Value; Color32[] pixels = tex.GetPixels32(); for (int i = 0; i < pixels.Length; i++) { if (pixels[i].r < 128) // 简单判断是否为黑色模块 { pixels[i] = targetColor; } } tex.SetPixels32(pixels); tex.Apply(); } return tex; }

把这个方法挂载到一个按钮上,就能在Unity Editor里快速生成测试用的二维码图片了,非常方便调试识别功能。

整个流程走下来,从导入DLL到实现稳定可用的跨平台二维码识别,核心在于理解WebCamTextureColor32[]的数据流,并妥善处理好ZXing的集成与调用。性能优化和异常处理是保证用户体验的关键。希望这篇超详细的实战记录,能让你在实现自己的Unity二维码功能时少走弯路。

← 返回列表