
前几天有个朋友在群里问Unity 游戏要上线应用商店商店需要演示视频有没有能在移动端直接录屏、还能顺手生成 GIF 的插件我第一反应就是 Natcorder。这个插件我用了快两年录屏、拍照、GIF 三件事全都能干而且不需要 GPU ReadbackCPU 直接编码兼容性比同类方案好不少。如果你也在做移动端录屏需求或者想给游戏加个保存精彩瞬间的功能这篇应该能帮你少踩几个坑。标题里写的是录频但实际开发里我们一般叫录屏或者屏幕录制核心就是把 Unity 的渲染画面编码成视频文件。Natcorder 最实用的地方在于它不像原生方案那样需要写 Android 和 iOS 两套代码一套 C# 接口全搞定。下面我按自己的实战顺序从选型、原理到三个功能的完整实现把踩过的坑和优化经验一起写出来。1. 为什么移动端录屏我最终选了 Natcorder先说结论不管你是要录屏、拍照还是生成 GIFNatcorder 都能在纯 Unity 环境下完成不需要额外接入原生 SDK。这对中小团队和个人开发者特别友好因为它意味着你只需要维护一份代码就可以同时覆盖 Android 和 iOS。1.1 移动端录屏方案对比RenderTexture、原生 API 与 NatcorderUnity 里做录屏常见的路子有三条RenderTexture Texture2D.ReadPixels 逐帧读回思路简单但性能消耗极大。每次 ReadPixels 都是一次 GPU 到 CPU 的同步拷贝在移动端很容易造成明显的掉帧。录 720p 都会卡更别提 1080p 了。Android MediaProjection / iOS ReplayKit 原生方案性能好但需要写原生代码还要处理生命周期、权限弹窗、不同机型兼容。如果你想快速做一个跨平台的功能模块这条路的成本是成倍增加的。Natcorder 插件方案它在底层封装了不同平台的原生编码器但在 Unity 层暴露的是统一的 C# 接口。原理上不再走 GPU Readback而是直接通过底层 API 获取原生纹理句柄进行编码速度快很多。同时它还内置了音频录制功能可以同步录系统声音和麦克风声音。1.2 Natcorder 的底层原理CPU 编码与原生纹理句柄Natcorder 最大的亮点在于它绕过了把像素从 GPU 拷贝到 CPU这个瓶颈。传统的 ReadPixels 方案是GPU 渲染完一帧 → 拷贝到 CPU 内存 → CPU 编码写入文件。这个过程每一帧都会造成 GPU 停顿移动端尤其明显。Natcorder 的做法是拿到当前帧在 GPU 上的原生纹理指针然后交给底层的硬件编码器直接处理整个流程不经过 CPU 的像素拷贝。这就是为什么它在移动端依然能保持比较好的帧率表现也是我最终选择它的核心原因。注意Natcorder 最新版本是 2.xAPI 和 1.x 相比有一定改动。下文所有代码均基于 2.x 版本如果你在用旧版本请注意字段名的差异。2. 先搞懂 Natcorder 的工作机制再动手Natcorder 的操作逻辑其实很有规律概括起来就是四个字配置、开始、喂帧、结束。但这里面的细节如果不搞清楚很容易写出录出来是黑屏或者视频没有声音这种问题。2.1 IClock 与 IRecorder两个核心抽象Natcorder 把录制的整个过程抽象成两部分IClock负责统一管理录制过程中的时间戳。你需要传入一个时间源给 RecorderRecorder 会按照这个时钟的节奏去接收帧数据。Natcorder 自带RealtimeClock它从创建开始就以真实时间进行计时。如果你希望在录屏过程中视频时间流逝和真实世界完全同步比如 1 秒真实时间对应 1 秒视频时间RealtimeClock 就是首选。IRecorder负责把每一帧图像/音频数据编码到文件中。不同的编码格式对应不同的 Recorder 类型。录 MP4 用MP4Recorder录 GIF 用GIFRecorder拍照用JPGRecorder。它们的配合关系是这样的你创建一个 Recorder传入一个 IClock然后在游戏循环中不断把当前帧的图像以及采集到的音频喂给 Recorder最后调用DisposeRecorder 会完成编码并生成最终文件。2.2 音频录制的关键AudioRecorder 与音频适配器如果你只是录画面可能觉得音频部分可有可无。但实际上应用商店的演示视频、玩家分享的精彩操作没有声音体验感会大打折扣。Natcorder 在音频录制上的设计也很统一也是通过AudioRecorderIClock的组合把OnAudioFilterRead或AudioListener的数据喂进 Recorder。这里有一个比较关键的细节Unity 的音频回调是在音频线程中触发的Recorder 的Append方法需要跨线程调用。Natcorder 在底层做了线程安全处理只要你不是在极端情况下频繁创建销毁 Recorder基本不会遇到崩溃问题。不过我还是建议你在正式项目里做好音频回调的判空保护尤其是在切换场景时。2.3 录制文件去哪了路径与持久化录屏完成后的文件Natcorder 会写入应用的持久化路径。iOS 上通常就是 App 沙盒的 Documents 或 Library 目录Android 上则是应用私有目录也可能是公共 Movies/Pictures 目录取决于你的配置。如果你需要让玩家在相册里直接看到录好的视频就需要在 Android 上触发一次媒体扫描或者把文件复制到公共目录。Natcorder 本身不负责这件事需要你自己接原生逻辑或引入第三方插件来搞定。我在项目里通常只是把文件路径返回给上层由运营层决定是否要弹分享面板。3. 手写录屏模块从 CameraRecorder 到 MP4 文件接下来是重点。我们直接写代码实现录屏功能。这里我会分步骤拆解先改造工程结构再给出完整代码最后讲几个容易忽略的坑。3.1 从零搭建录制脚本CameraRecorder 完整实现先看这个脚本的核心结构。我用的是 Natcorder 2.x 的CameraRecorder它可以直接附加在 Camera 上自动采集相机的画面。核心代码如下using System.Collections; using System.IO; using UnityEngine; using NatSuite.Recorders; using NatSuite.Recorders.Clocks; using NatSuite.Recorders.Inputs; public class ScreenRecorder : MonoBehaviour { [Header(录制设置)] public int videoWidth 1280; public int videoHeight 720; public int frameRate 30; public int bitRate 2_000_000; // 2Mbps private CameraRecorder cameraRecorder; private AudioRecorder audioRecorder; private RealtimeClock clock; private MP4Recorder mp4Recorder; private bool isRecording; public void StartRecording() { if (isRecording) return; // 1. 创建真实时间时钟 clock new RealtimeClock(); // 2. 创建 MP4 编码器支持最高 1080p mp4Recorder new MP4Recorder( videoWidth, videoHeight, frameRate, bitRate ); // 3. 创建 CameraRecorder采集主相机画面 cameraRecorder new CameraRecorder( mp4Recorder, clock, Camera.main ); // 4. 需要录音就创建 AudioRecorder audioRecorder new AudioRecorder( mp4Recorder, clock, AudioSettings.outputSampleRate, (int)AudioSettings.speakerMode ); isRecording true; } public void StopRecording() { if (!isRecording) return; // 结束录制CameraRecorder 和 AudioRecorder 会各自停止 cameraRecorder.Dispose(); audioRecorder.Dispose(); // 完成编码拿到文件路径 var filePath mp4Recorder.FinishWriting(); Debug.Log($录制完成文件路径{filePath}); isRecording false; } private void Update() { if (isRecording cameraRecorder ! null) { // 手动提交画面帧CameraRecorder 会在 OnRenderImage 中自动提交 // 这里主要是确保在非渲染管线场景下也有帧数据 cameraRecorder.CommitFrame(); } } }这里有一个细节要说明CameraRecorder内部其实已经通过OnRenderImage自动提交帧了。我这里的CommitFrame只是为了一些特殊情况做保障比如渲染管线被改、或者相机不渲染时日常使用中你会发现即使不写 Update 里的提交也能正常工作。但写上总归更稳妥。3.2 为什么用 MP4Recorder 而不是别的格式Natcorder 支持多种 Recorder最常用的就是MP4Recorder和GIFRecorder。视频文件用 MP4 几乎是唯一合理选择原因是MP4 是 H.264/HEVC 编码的标准容器Android、iOS 系统级播放器原生支持MP4 有硬件编码加速功耗比 GIF 小很多MP4 可以同时包含视频轨和音频轨GIF 不行如果你只是做保存到相册这种需求MP4 是完全够用的。GIF 的真正用途通常是聊天表情包、论坛发帖、或者看重体积短小的场景。3.3 切换录制目标的技巧从 MP4 到 GIF 的 Recorder 封装因为 MP4 和 GIF 在 Natcorder 中都是 Recorder 的实现所以只要你把上层代码泛化到IRecorder层级就可以非常方便地在两种录制模式之间切换。private IRecorder recorder; private IClock activeClock; public void StartRecording(VideoFormat format) { activeClock new RealtimeClock(); if (format VideoFormat.MP4) recorder new MP4Recorder(1280, 720, 30, 2_000_000); else if (format VideoFormat.GIF) recorder new GIFRecorder(320, 240, 12, 1); // 后续统一使用 recorder.AppendFrame / recorder.FinishWriting }这个泛化思路很重要它会让你后续扩展代码时省很多事。比如以后想支持 H.265你只需要 new 出对应的 Recorder 类型即可不用动上层逻辑。3.4 隐藏的坑相机朝向、分辨率比例和内存占用录屏最容易出现的问题就是录出来的视频方向不对、画面被拉伸、以及内存突然涨了一大截。相机朝向移动端竖屏游戏比较常见。如果你的游戏是竖屏如 1080x1920录制分辨率也设置成 1080x1920即宽高比为 9:16录出来的视频就是正的。如果你设置成了 1920x1080会导致画面被旋转 90 度或者裁切。解决方法是根据屏幕方向动态设置录制分辨率。分辨率比例我建议录制分辨率和屏幕显示分辨率保持一致。如果你强制设置一个不同宽高比的 ResolutionNatcorder 会直接截取中间区域而不是等比缩放。我在项目里是这么处理的int screenWidth Screen.width; int screenHeight Screen.height; // 保证录出来的视频也是竖屏 int recordWidth screenHeight screenWidth ? 720 : 1280; int recordHeight screenHeight screenWidth ? 1280 : 720;内存占用MP4 编码器在初始化时会分配一定量的缓冲。分辨率越大缓冲越大内存占用也越高。在低端 Android 机上一次录 10 分钟的 1080p 视频内存峰值可能到 300MB 以上风险不小。所以除非必要不要无脑上 2K、4K 录制。提示在实际项目里我更推荐把录制分辨率控制在 720p并主动设置 bitRate 在 2-4Mbps。这个档位在绝大多数移动端设备上都能获得不错的画质和体积平衡也几乎不会对游戏性能产生明显影响。4. 拍照与 GIF 动图两个小而实用的扩展功能录屏是最基础的能力但项目里真正频繁被用到的往往是拍照和 GIF 这两个轻量功能。前者用于生成静态分享图后者用于快速产出低体积的动态素材。两个功能都基于 Natcorder实现起来也不难但细节各有讲究。4.1 拍照实现JPGRecorder 与 ReadPixels 的正确打开方式很多人一看到Natcorder 能拍照就以为会像录屏一样直接截取当前帧。其实 Natcorder 的拍照实现本质上是走了一个简化版的帧提交流程再用 JPGRecorder 编码成 JPG 文件。但 JPGRecorder 并不是视频编码器它的核心接口是Readback原理上和录屏有所不同。如果你想手动实现拍照功能也可以不走 JPGRecorder直接用 Unity 的Texture2D.ReadPixels加EncodeToJPG。但要注意ReadPixels必须在相机渲染后立刻调用并且需要设置ReadPixels的 Rect 和屏幕朝向一致。Natcorder 的JPGRecorder则帮你处理了这些底层细节。using NatSuite.Recorders; public void TakeScreenshot() { // 这里用 JPGRecorder 只是为了证明 Natcorder 支持 var jpgRecorder new JPGRecorder(); // 拍照逻辑从当前相机画面读取像素 StartCoroutine(CaptureScreenshot(jpgRecorder)); } private IEnumerator CaptureScreenshot(JPGRecorder jpgRecorder) { yield return new WaitForEndOfFrame(); var texture ScreenCapture.CaptureScreenshotAsTexture(); // 如果你不转存 JPGRecorder可以直接用 texture.EncodeToJPG() byte[] jpgBytes texture.EncodeToJPG(); File.WriteAllBytes(Application.persistentDataPath /capture.jpg, jpgBytes); Destroy(texture); jpgRecorder.Dispose(); }上面的写法走的是 Unity 原生ScreenCapture流程最简单适合大多数拍照需求。JPGRecorder 在 Natcorder 2.x 中主要面向需要把 JPG 送入统一 Recorder 管线的场景日常拍照我反而不建议绕道。4.2 GIF 动图制作定制化输出尺寸与延迟GIF 的实现和视频类似但有几个参数特别重要尺寸、帧率、延迟。尺寸GIF 是索引色每帧存储成本很高尺寸过大会让文件膨胀得厉害。常规的分享场景320x240 到 480x360 就足够了。帧率GIF 一般不追求高帧率8-12 FPS 在人眼视觉上已经很连贯而且低帧率可以直接减少帧数量显著降低文件大小。延迟值GIF 的播放节奏由帧延迟控制Natcorder 的GIFRecorder会基于IClock自动设置延迟所以你只需要控制提交帧的节奏即可。using NatSuite.Recorders; using NatSuite.Recorders.Clocks; var gifClock new RealtimeClock(); // 320x24012 FPS延迟 1越低越快 var gifRecorder new GIFRecorder(320, 240, 12, 1); // 假设你有一个相机画面源 var gifInput new CameraInput(gifRecorder, gifClock, Camera.main); // 开始录 3 秒 StartCoroutine(RecordGifForSeconds(gifRecorder, gifInput, 3f));GIFRecorder构造函数的第四个参数就是延迟。这个值你不需要手动去算它遵循 GIF 规范的单位单位是 1/100 秒1 表示每帧 0.01 秒。但实际使用时如果发现生成的 GIF 播放速度过快可以适当调高这个值比如 5 或 8。我通常习惯把延迟设为 1配合 12 FPS 用起来观感最自然。4.3 GIF 录制中的相机输入避免黑屏的三个检查点GIF 录制最容易翻车的场景就是录完发现是黑屏。我排查下来95% 的原因出在以下三个点CameraInput 是否在 LateUpdate 中提交帧GIFRecorder 本身不会主动抓帧它需要你手动把帧喂进去。CameraInput 会自动处理前提是你别把它附着在一个被禁用/没有 RenderTexture 的相机上。相机 culling mask 是否为空如果相机只渲染 Nothing 层那采集到的纹理自然是全黑。屏幕方向是否切换在竖屏和横屏切换时之前的 RenderTexture 会被释放导致后续帧采集失败。解决办法是在 OnRectTransformDimensionsChange 或方向变更回调里重建采集器。提示录制 GIF 时如果相机使用的是 HDR 渲染管线需要确保 RenderTexture 的格式是 ARGB32 或 RGBA32否则会导致颜色异常。5. 移动端真机踩坑从包体到性能的实战记录计划很完美但现实总是会在真机上给你一巴掌。以下是我在移动端接入 Natcorder 后遇到的一些高频问题以及对应的解决经验。5.1 性能开销到底有多大一局 6 分钟的实测数据我在一个中等特效的中型手游项目里做过测试机型为骁龙 870 平台的 Android 手机录制设置为 720p、30 FPS、2Mbps。结论是CPU 占用增加约 10%-15%主要是视频编码的软件部分和音频采集的开销。帧率下降约 2-4 帧在个别复杂场景会掉 5 帧以上但日常副本完全可接受。内存增加约 80-120MB其中包括编码器缓冲和纹理拷贝的临时内存。温度上升速度加快长时间录制半小时以上手机会明显发热。所以我的建议是不要把录屏功能直接开放给所有玩家常驻开启而是做成主动点击开始/结束的按钮。这样既满足了分享需求又避免了后台常驻录制的性能损失。5.2 压后台后录制的行为你应该知道的限制Android 和 iOS 在 App 退到后台后渲染会被系统暂停。如果你此时没有处理录屏会直接写入一坨黑帧或卡在最后一帧。Natcorder 不会自动感知前后台切换你需要自己在OnApplicationPause里做处理private void OnApplicationPause(bool pauseStatus) { if (pauseStatus isRecording) { // 暂停录音但不要立即 FinishWriting因为可能用户马上切回来 audioRecorder.Dispose(); // 或者实现一个 Pause 逻辑 } else if (!pauseStatus isRecording) { // 恢复录音重新创建音频适配器 audioRecorder new AudioRecorder(mp4Recorder, clock, AudioSettings.outputSampleRate, (int)AudioSettings.speakerMode); } }在 iOS 上退后台后如果超过一定时间没回到前台系统可能直接杀掉 App。这种情况下录制的临时文件可能会残留需要再启动时清理掉。5.3 安卓与 iOS 的差异权限、路径与相册可见性Android 权限如果你要让录制的文件出现在相册需要申请WRITE_EXTERNAL_STORAGEAndroid 10 以下或使用 MediaStore APIAndroid 10。Natcorder 本身只管写文件到指定路径不负责相册注册。iOS 相册iOS 需要申请NSPhotoLibraryAddUsageDescription权限并且写完后要调PHPhotoLibrary保存。Natcorder 也不会自动做这一步需要你自己接一下。路径差异iOS 沙盒路径每次启动可能变化所以不要把录制路径写死在内存里建议每次从Application.persistentDataPath读取拼接。5.4 提升录制体验的额外技巧分辨率、码率与首帧延迟这里分享几个我在实际项目里总结的优化点首帧延迟优化MP4Recorder 创建后创建编码器需要一定时间表现为点了录制按钮要过一两秒才开始录。解决办法是采用预热策略在玩家进入战斗前预创建 Recorder等真正开录时直接 Append。我自己测试预热后首帧编码时间几乎为 0。码率自适应如果你想尽量保证画质可以根据当前网络或设备性能调整码率而不是拍死一个固定值。通常 720p 30FPS 在 1.5-4Mbps 之间都算合理。分帧采集如果你的游戏画面经常超过 30FPS比如跑到 60FPS可以用frameRate参数告诉编码器期望帧率编码器会按这个节奏抽帧不会导致视频文件变成慢动作或快动作。5.5 应该避开的坑纹理销毁、重复提交与长视频文件下面这几个问题是我在不同客户项目里都见过的属于高频翻车点纹理销毁顺序用CameraInput录屏时如果中途释放了 RenderTexture但没有同步移除 CameraInput会导致下一帧提交空纹理录出的视频会出现花屏或黑块。正确做法是先 Dispose 掉输入源再释放 RenderTexture。重复提交如果同一个 Recorder 被两个 CameraInput 同时使用比如主相机和 UI 相机都加了组件会导致画面撕裂或文件损坏。解决办法是统一用一个CameraRecorder或者手动控制以免重复 Append。长视频文件MP4 录制超过 30 分钟部分 Android 机型会出现文件头损坏导致无法播放的情况。实际上这是 FAT32 或某些多媒体库的 4GB 文件大小限制。解决办法是按时长自动分段录制每 20 分钟结束当前 Recorder创建一个新的并拼接文件列表。6. 从录屏到产品化完整流程与优化经验如果你只是演示用录屏功能能跑通就够用了。但要在正式游戏里做成一个玩家可用、体验良好的功能还有不少细节要打磨。6.1 如何把录屏从功能变成产品好用的分享功能通常包含这些环节录制前告诉玩家即将录制的时长上限比如最长 30 秒避免无意识录太久产生超大文件。录制中显示一个明确的录制状态 UI红点计时并提供停止并保存和取消录制两个操作。录制后生成预览图或视频预览让玩家决定是否保存/分享。分享到社交平台iOS 用UIActivityViewControllerAndroid 用 Intent 发送文件。这里可能需要额外的原生桥接插件。Natcorder 只负责生成文件上面这些流程都需要你自建。我见过有的项目把预览和分享都做了有的只做了录制完自动弹系统分享面板。就看你的运营需求。6.2 降低文件大小的方法为何你的 GIF 比视频还大有一个很容易踩的坑你觉得 GIF 肯定比视频小结果录出来 5MB 的 GIF同场景视频才 2MB。原因在于 GIF 是索引色不支持高效压缩而且逐帧存储全图。优化方法只有三板斧缩小尺寸320x240 起步不要超过 480x360。降低帧率8 FPS 足够12 FPS 是上限。减少延迟值延迟值越小播放节奏越快看起来越流畅但文件也会偏大需要在两者间取平衡。如果这样压缩后文件仍然太大那就别用 GIF 了改用短 MP4 视频体验会好很多。6.3 录制模块的架构拆分与游戏主循环解耦最后聊一下产品级架构。我不建议把录屏逻辑跟游戏业务代码耦合在一起而是单独做一个RecordingManager通过事件对外广播录制状态。public static class RecordingManager { public static event System.Actionstring OnRecordingFinished; public static event System.Actionfloat OnRecordingTimeUpdated; private static ScreenRecorder recorder; public static void Start() { recorder new GameObject(ScreenRecorder).AddComponentScreenRecorder(); recorder.StartRecording(); } public static void Stop() { recorder.StopRecording(); Object.Destroy(recorder.gameObject); } }这样一个全局管理器可以在战斗、剧情、UI 界面上任意调用只要你做好开始和结束的配对不重复触发即可。实际项目中我一般还会在 UI 上绑定快捷键比如同时按两个键触发录制隐藏调试面板方便测试。6.4 你的真正需求是录屏 拍照 GIF还是只要其中两个在接入 Natcorder 前建议先明确你到底需要几个能力。很多项目一开始说录屏、拍照、GIF 全都要但做完了发现玩家用得最多的只有录屏拍照和 GIF 的调用率极低。如果你追求最小成本那只需录屏MP4就够了。拍照可以直接用 Unity 的ScreenCapture.CaptureScreenshotAsTextureGIF 也可以等真有需求再加上。Natcorder 好就好在它是一个全流程打通的基础能力你随时可以在已有工程上增加新的 Recorder 类型不需要动底层架构。从工程角度来看我的建议是先花一个晚上把录屏跑通再对照官方 Demo 把拍照和 GIF 的代码分别看看理解它们的参数差异然后按需接入。当你把这三个能力都沉淀成一套统一的录制管理器后后续任何业务方要精彩时刻录制回放截图分享你都能很快给出方案。