我最早用上 Animation Recorder,是被一个"录制玩家操作并回放"的需求逼的。关卡编辑器要做 Undo、角色在不同存档位要能复现某一段动作轨迹、训练关要展示"标准操作"给玩家看——这些场景都不是单纯播放一个预先做好的动画,而是要把运行过程中任意时刻、任意对象的位移和旋转记下来,存成一份可复用的动画资产。
当时第一反应是自己写采样工具,每帧记录 Transform 的 localPosition 和 localRotation,然后手动生成 AnimationClip。数据量一大就犯难:几千帧的曲线要自己处理了,关键帧稀疏化要做,还要处理 root motion、归一化时间轴、动画压缩。写完用起来也凑合,但总觉得把 Unity 自家动画系统的底层轮子又重新造了一遍,代码和边界情况都塞满了一个编辑器窗口。
后来切到 Animation Recorder,才意识到这玩意儿不是"帮你记录几个 Transform 值"那么简单,它本身就是一条从场景数据到 AnimationClip 资产的完整流水线。这篇文章是我把运行时和编辑器两条录制链路反复用了几个月后整理出来的实现思路,重点讲清楚两套模式各自的调用逻辑、录制数据在底层怎么组织,以及实际工程里最容易踩的那些坑。
1. 为什么说 Animation Recorder 不是"录 Transform 的录屏工具"
很多人第一次看到 Animation Recorder 的名字,会下意识以为是"把场景里的动作用录屏方式捕获下来"。其实它捕获的不是像素,而是属性曲线。它做的事情本质上是:在一段时间内持续采样指定对象的指定属性,把变化过程编码成 Unity 动画系统的曲线数据,最终产出标准的 AnimationClip 资源。
这个定位非常关键。因为"属性采样"和"录屏"在底层逻辑上有根本区别:
- 录屏拿到的是画面帧,只能回看,不能反解出精确数值。
- 动画曲线拿到的是属性随时间变化的函数,可以直接丢给 Animator、Interpolation、Timeline、代码复用,甚至还可以再编辑。
换句话说,Animation Recorder 是把"某一段时间里发生了什么"转译成"动画系统能理解的语言"。这也是它特别适合做动画工具链中间层的原因。比如:
- 关卡编辑器录一段机关运动轨迹,存成 clip 后用于剧情回放。
- 在场景里手动 k 或者逐步拖动物体,最终录成一段可复用动画。
- 把外部传入的数据流(比如传感器姿态、PLC 控制的轴运动)驱动到模型上,用 Recorder 记录状态,方便离线回放和验证。
- 实机操作流程录制,生成任务指引动画,比人工 K 帧效率高太多。
这里面有个隐含逻辑:既然最终产物是 AnimationClip,那么你能录制的对象就不局限于骨骼动画。只要属性可以被 Unity 的动画系统识别并绑定,比如 Transform 的 position/rotation/scale、材质球颜色、组件数值,都可以是录制目标。我在数字孪生项目里甚至录过整条传送带的启停状态,通过记录一个自定义组件的运行状态值,拿到 clip 后反向驱动动画,比写一堆控制状态机要清爽得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 运行时录制:代码接入的完整流程与原理
运行时录制解决的核心问题是:游戏或模拟器在真正运行过程中,动态产生的动作和状态变化,怎么变成可保存、可复制、可再次播放的动画资产。这里面最典型的需求就是"录制玩家操作回放"或者"从逻辑状态生成动画片段"。
2.1 最小可用示例:先让录制转起来
Unity 编辑器模式下,运行时录制的最小实现可以用 UnityEditor.AnimationRecorder 类来完成。这个类在 UnityEditor 命名空间下,所以要放在 Editor 脚本里,或者用 #if UNITY_EDITOR 包起来。核心调用大概长这样:
csharp复制using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
public static class RuntimeRecordDemo
{
public static void StartRecord(GameObject target, string savePath)
{
// 手动指定录制目标
var settings = ScriptableObject.CreateInstance<AnimationRecorderSettings>();
settings.AnimationClip = new AnimationClip();
settings.AnimationClip.name = "RecordedClip";
settings.Bindings = AnimationRecorder.CalculateBindings(target);
var recorder = new AnimationRecorder();
recorder.Settings = settings;
recorder.Bindings = settings.Bindings;
// 开始录制
recorder.StartRecording(target);
// 录制一定时间后手动停止,或者用协协程控制时长
// recorder.StopRecording();
// AssetDatabase.CreateAsset(settings.AnimationClip, savePath);
}
}
这个 AnimationRecorder 的用法有几点值得说清楚:
-
AnimationRecorder.CalculateBindings(target)会遍历目标物体上所有可绑定属性,生成一组绑定条件。这个函数很强大,它会自动把 Transform、Animator 下的骨骼层级、挂在物体上的可录制组件全都扫一遍。但对于一些自定义组件,它不一定能识别到,需要自己手动往绑定列表里塞EditorCurveBinding。 -
StartRecording传入的目标对象会被持续采样。采样频率默认跟帧回调相关,这意味着你的录制精度受游戏帧率影响。如果游戏掉帧,录出来的曲线会出现关键帧密度不均匀的问题。 -
录制期间不能销毁或禁用在绑定列表中的对象,否则绑定失效,后续录制的数据会断。
-
停止录制之后并不会自动保存资源,必须自己调用
AssetDatabase.CreateAsset把 clip 写入工程目录。很多人卡在这一步,觉得录完没有产出,其实只是没落盘。
2.2 录制模式的选择:为什么不能一概用 Update
运行时录制摄到什么节奏由录制模式决定。在 AnimationRecorderSettings 里有一个字段是 Recorded 'isPlaying'、Recorded 'isTimeline'、Recorded 'manual' 这类模式选项。我实际感受下来:
- 如果录的对象是骨骼动画或者 Animator 驱动的动作,必须在帧率稳定时采样。
isPlaying模式会跟随编辑器的播放状态回调,比较适合。 - 如果录的对象是带物理属性的刚体,比如自由落体、碰撞反弹,那么和 FixedUpdate 对齐会更稳。这种情况下可以手动在
FixedUpdate里推进录制采样,而不是依赖默认回调。 - 如果录的是 Timeline 驱动的序列动画,用 Timeline 模式会让 clip 的时间轴和 Timeline 播放位置精确对齐。
所以不要无脑 Start。先在脑子过一遍:目标物体的动作用什么系统驱动?Animator?物理?Timeline?自己在外层写逻辑赋值?确认驱动源之后,再选录制节奏,否则会出现"录下来动画播放时和现场效果不一致"的问题。
2.3 手动控制采样时长的方案
想要精确录制"从第 0.5 秒到第 2.5 秒这一段",可以自己做个包装器,用一个 MonoBehaviour 来管理录制的启停:
csharp复制public class RuntimeRecorderController : MonoBehaviour
{
AnimationRecorder recorder;
AnimationRecorderSettings settings;
GameObject target;
public void Begin(GameObject target, string clipName)
{
this.target = target;
settings = ScriptableObject.CreateInstance<AnimationRecorderSettings>();
settings.AnimationClip = new AnimationClip();
settings.AnimationClip.name = clipName;
settings.Bindings = AnimationRecorder.CalculateBindings(target);
recorder = new AnimationRecorder
{
Settings = settings,
Bindings = settings.Bindings
};
recorder.StartRecording(target);
}
public void EndAndSave(string assetPath)
{
recorder.StopRecording();
AssetDatabase.CreateAsset(settings.AnimationClip, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
这里有个重要的经验:StartRecording 之前最好手动把目标物体的状态重置到起始姿态,否则录制出来的 clip 开头会"跳切"到当前帧的状态。比如你想录一段从 A 点走到 B 点的动作,先初始化到 A 点,再开始录,最后停在 B 点,这样 clip 的首尾都是干净明确的。
3. 编辑器录制:从场景操作到动画资产的完整链路
编辑器录制是指开发者或 TA 在编辑器里对物体进行手动拖动、旋转、调整参数时,把这些操作过程记录成动画。这个功能在制作过场动画、机关动画、或者"摆出你想要的姿态再录下来"的工作流中非常好用。
3.1 录制面板的核心配置项
Unity 自带的 Animation Recorder 窗口(Window > Animation > Animation Recorder)里几个关键字段值得留意:
- Target:要录制的 GameObjects。可以是单个物体,也可以是一组物体,但一组物体时绑定列表会线性叠加,所以录制时选中物体越多,生成 clip 的曲线越多。
- Recording Mode:区分
Recorded、Manual等选项。Manual模式下你会用代码里主动调用Update推进录制进度,适合配合自定义编辑器窗口做局部录制。 - Clip Name / Destination:生成 clip 的名称和保存路径。路径建议用
Assets/开头,否则资源写不进去。 - Bindings:点开可以细看这个录制任务会记录哪些属性。你可以在录制前手动增删。
设置好之后,"Start Recording" 按钮按下,编辑器会进入类似 Play Mode 的准备状态,或者直接在编辑模式下开始采样(取决于 Unity 版本和 Recorder 版本的行为)。一旦开始录制,你在 Scene 视图里拖动对象,所有位移都会被写入曲线。停止之后,clip 出现在指定目录,可以直接拖到 Animation 窗口查看曲线。
3.2 编辑模式录制经常忽略的细节
编辑器录制常见的问题有两个,都和"编辑器本身不是严格时间驱动"有关。
第一个问题是录制开始和结束的时机不精确。手动点击开始按钮,到你松开鼠标之间的时间,受你手速影响很大。如果你需要精确到小数秒的时间边界,更好的方案是用短线脚本控制,而不是全靠手点面板。
第二个问题是录制过程中如果切了窗口焦点,或者打开了某个需要重新编译的脚本,录制会中断。这听起来像是小事,但项目一大,脚本随时可能被编译,稍不留神你精心录到一半的动画就断了。所以我习惯在录制前把所有需要改的 C# 先写好并且确认编译通过,再开始录。
3.3 用 EditorScript 控制编辑器录制的起始和保存
和我一样偏好脚本化的同学,可以用编辑器脚本完整控制整条链路。假设你要写一个工具窗口,点击一个按钮就开始录制场景中选中的物体,再点另一个按钮停止并保存:
csharp复制using UnityEditor;
using UnityEngine;
public class ClipRecorderWindow : EditorWindow
{
GameObject target;
string clipName = "NewClip";
string savePath = "Assets/Recordings/";
AnimationRecorder recorder;
[MenuItem("Tools/Clip Recorder Window")]
public static void OpenWindow()
{
GetWindow<ClipRecorderWindow>("Clip Recorder");
}
void OnGUI()
{
target = (GameObject)EditorGUILayout.ObjectField("Target", target, typeof(GameObject), true);
clipName = EditorGUILayout.TextField("Clip Name", clipName);
savePath = EditorGUILayout.TextField("Save Path", savePath);
if (GUILayout.Button("Start"))
{
StartRecording();
}
if (GUILayout.Button("Stop And Save"))
{
StopAndSave();
}
}
void StartRecording()
{
if (target == null) return;
var settings = ScriptableObject.CreateInstance<AnimationRecorderSettings>();
settings.AnimationClip = new AnimationClip();
settings.AnimationClip.name = clipName;
settings.Bindings = AnimationRecorder.CalculateBindings(target);
settings.RecordingMode = AnimationRecordingMode.Manual; // 手动推进
recorder = new AnimationRecorder
{
Settings = settings,
Bindings = settings.Bindings
};
recorder.StartRecording(target);
}
void StopAndSave()
{
if (recorder == null) return;
recorder.StopRecording();
var clip = recorder.Settings.AnimationClip;
if (!AssetDatabase.IsValidFolder(savePath))
AssetDatabase.CreateFolder("Assets", "Recordings");
AssetDatabase.CreateAsset(clip, savePath + clipName + ".anim");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
recorder = null;
}
}
这段代码把编辑器录制变成了一个完全可控的工具行为。更有意思的是,AnimationRecordingMode.Manual 可以让"开始录制"之后不是立刻采样,而是由你自己在编辑器脚本的其他逻辑里调用 recorder.TakeSnapshot() 之类的推进方法。这种模式特别适合把录制动作嵌进美术操作流程:比如美术先摆好起始姿态,点一下标记关键帧,再摆下一个姿态,再点一下,最终生成一段逐帧定义的动画效果。
4. 录制数据的底层结构:曲线、绑定与关键帧的组织方式
不管是从运行时录制还是编辑器录制,最终数据都会落在两个概念上:绑定(Binding)和曲线(Curve)。把所有表面 API 剥开,Recorder 在底层做的工作就是把"物体 + 属性"映射到"曲线的关键帧列表"。
4.1 理解 EditorCurveBinding 与 AnimationClip 的对应关系
EditorCurveBinding 有四个关键字段:path(从根物体到目标物体的相对路径,比如 "Armature/LeftArm/Hand")、type(组件类型,比如 Transform)、propertyName(属性名,比如 m_LocalPosition.x)、还有 isPPtrCurve(是否是对象引用曲线,通常处理材质或 Animator 参数时会出现)。
当 Recorder 对目标 GameObject 做完整的绑定计算时,它本质上是在生成大量这样的 EditorCurveBinding,每一个绑定最终都会在 AnimationClip 中对应一条或者一组曲线。举个例子,一个带骨骼层级的人形角色的 Transform 绑定会有几十上百条,因为每一节骨骼的 position/rotation/scale 都是单独的绑定项。
调用 AnimationUtility.GetEditorCurve(clip, binding) 可以获取某条绑定对应的 AnimationCurve,进一步检查关键帧数量、曲线形态和数据稀疏程度。这在验证录制是否完整时很有用。
4.2 绑定的选取策略:全量绑定还是精细裁剪?
大多数情况下,直接 CalculateBindings 生成的绑定数量是过剩的。比如你只关心某个手臂的旋转,但绑定会把角色全身都扫进去。这样的话录出来的 clip 会非常庞大,关键帧数量爆炸,而且容易采集到不需要的噪声。
我建议在正式录制前做一个裁剪步骤,在绑定列表里按需保留:
csharp复制settings.Bindings = AnimationRecorder.CalculateBindings(target)
.Where(binding => binding.path.StartsWith("Armature/LeftArm")
|| binding.type == typeof(Transform))
.ToArray();
你甚至可以把常用角色的"可录制绑定预置"做成一份 ScriptableObject 配置,录制时按角色类型加载对应的绑定集合,而不是每次临时算。这个思路在做批量动画录制管线时尤其管用——同一套角色模板可以批量录制几十个不同动作,每次用同一份绑定配置去录,生成的动画资产在结构上能保持一致。
4.3 关键帧稀疏化:为什么不能无脑保留所有采样
当录制时间较长且帧率较高时,比如录制了 60 秒、每秒 60 帧,那就有 3600 个采样点。如果每帧每个属性都在变化,clip 文件会迅速膨胀。Unity 动画系统本身支持曲线压缩,但压缩不是万能的,尤其是对于带随机波动的高频数据,压缩后精度下降会很明显。
比较稳妥的做法是:录制时保持原始采样,然后在保存前用 AnimationUtility.CompressAnimationClip 对 clip 做强压缩,或者自己写一个关键帧减稀逻辑,把相邻差值小于某一阈值的采样合并。具体来说:
- 如果一段曲线在某个时间段内数值几乎不变,可以把中间的关键帧去掉,只留首尾。
- 如果某个旋转轴在录制期间一直为 0,可以直接整段曲线的关键帧数量降到最小。
- 对于带噪声的数据,先做平滑滤波再录制,比录制完再平滑更可控。
我在录一段传送带连续运转动画时就遇到过,录了十分钟的曲线,其实大部分时间轴的旋转值都是稳定递增,压缩后也才几百个关键帧,但如果不去管,初始 clip 可能有几万个采样。所以"录制后检查关键帧密度"应该成为流程的固定环节。
5. 我在实际项目中踩过的坑与绕过方案
写文章永远比实操顺利。下面这几个坑,是我在真实项目里一个字一个字踩出来的,有些甚至不是文档里能翻到的。
5.1 录制 Humanoid 角色时丢失 root motion 的问题
用 Animation Recorder 录制人形角色动作时,如果目标对象挂载了 Animator 且 Avatar 配置为 Humanoid,经常出现录出来的 clip 在 Animator 上播放时没有位移效果的情况。原因在于 Humanoid 动画的 root motion 依赖骨骼绑定数据,而 Recorder 在计算绑定时并不会自动将 root motion 骨骼的位移标记为必须录制。
解决思路有两种。第一种是录制前在绑定列表里强制加入 root 相关的 Transform 路径,比如 "Root"、"Hips" 或者 "Armature" 的绑定,确保 root 位移也进了曲线。第二种是直接在录制时选择 Generic 模式的 Avatar,从源头上绕开 Humanoid 的 root motion 特殊处理。
我在做角色动作录制功能时,最终采用的是第二种方式:录制前生成一个临时替身 GameObject,把原始角色作为一个子物体包进去,录制这个替身(Generic 模式)。录制出来的动画再作为通用动画处理。这样反而更可控,因为不需要猜 Humanoid 系统会怎么解释这些曲线。
5.2 手动 Start 但忘记 Stop,导致编辑器状态残留
AnimationRecorder 内部持有目标对象和绑定关系,如果只调用 StartRecording 而没有在合适的时机调用 StopRecording,编辑器会持续采样,造成明显卡顿。更麻烦的是,即使你切了场景,这件事可能还挂在内存里。
我的习惯是:在录制工具类里封装一个 try-finally 式的停止逻辑,确保任何异常路径都能正确地 Stop。编辑器里可以用 EditorApplication.update 做兜底,检查当前是否还在录制状态,如果发现已经离开预期录制时段,自动停止。
5.3 录制时长与 clip 长度的粒度问题
如果你录制了 0 到 1.0 秒的内容,AnimationClip 的 stopTime 默认是 1.0 秒。但如果你在 0.8 秒时停止了录制,有些版本 Recorder 产出的 clip 长度会变成 0.8 或 1.0,不一定是预期值。为了统一处理,可以在保存前用 AnimationClipSettings 调整 startTime/stopTime,并把 loopTime 设置成你想要的值。
我自己通常都会做一个标准化的收尾步骤:在保存到 Asset 之前强制设置 clip 的帧率、循环行为和首尾边界,确保后续在 Timeline 或者 Animator 里用的 clip 都是符合项目规范的。
6. 性能、存储与扩展玩法
Animation Recorder 除了基本的记录功能,还可以被改造成很多更强大的工具链。最后一个章节聊聊性能侧和扩展方向。
6.1 延长录制时长的技巧:断点续录与分段拼接
录制过程中如果时间太长,没法在一个 clip 里全部搞定,可以考虑分段录制。先录 0 到 10 秒,保存为 clipA;再录 10 到 20 秒,保存为 clipB;最后用 AnimationClip 的曲线合并方法(AnimationUtility.SetEditorCurve 配合 AnimationUtility.GetEditorCurve 进行逐条合并)把两条曲线拼接成一条完整曲线。
这个技巧特别适合长时间动作捕捉的场景。比如某个机械设备要持续运行 10 分钟,中间不能停机,但你不想让录制脚本一直持有这么多内存,就可以每 30 秒落盘一次,最后再做拼接合并。
6.2 录制带有随机性波动的数据时,先平滑再录
如果你的数据源是传感器噪声,比如 IMU 姿态、PLC 返回的轴位置,录制前最好先滤波再输入。因为 Recorder 只负责采样,它不会对数据进行平滑,也不会有任何降噪逻辑。如果你直接录原始噪声,最终生成动画播放时会显得抖动明显,而且动画文件被迫记录了大量微小波动的关键帧,尺寸和性能都受影响。
我比较常用的方式是:在录制目标对象和目标数据之间加一个轻量的"数据平滑层",也就是把原始值经过低通滤波后再写入 Transform,录制时采集的是平滑后的值。这个逻辑在运行时录制中尤其重要,因为很多外部设备的数据刷新率并不等于帧率。
6.3 与 Timeline、Animator、Playable 的联动
录制出来的 clip 最直接的使用场景是拖进 Timeline 轨道的 Animation Track。需要注意的是,Timeline 对 clip 的首尾边界和循环方式有和普通 Animator 不同的解释,最好在保存前就把 AnimationClipSettings.loopTime 明确设置。
另一个扩展玩法是把录制的 clip 和 Playable API 结合起来,在运行时动态创建 AnimationClipPlayable 或者 AnimationMixerPlayable,把一段预先录制好的动画混合进当前游戏状态。这样你不再需要等动画师手动 K 出某些特殊动作,而是直接从真实操作中"捕捉"到这段动作,转成可播放的数据。这个能力对构建 AI 行为树、动作生成管线、自动化测试回放这些场景都非常有用。
6.4 录制工具的系统化:配置驱动、批量执行和日志追踪
当 Animation Recorder 只是手动工具时,你不需要考虑复杂工程化。但一旦要支撑批量动作录制,一定要做成配置驱动。我用过的方案大概长这样:
- 用自定义 ScriptableObject 描述每个录制任务:目标物体的生成方式、绑定的筛选规则、录制时长、采样目标路径、clip 命名规范、压缩方式。
- 用一个 Editor 批处理窗口,选取一组配置,逐个执行录制、保存、压缩、校验。
- 每执行一步,在 Console 里输出关键指标:绑定数量、采样帧数、clip 文件大小、压缩后关键帧数量。
这样做完,整个录制流程就不再是"我对着面板手动操作",而变成了一条可以重复执行的自动化流水线。哪怕是需要在一个月后重新录制全部动画,也不需要任何额外的手工操作。
最后再分享一个小技巧:录制前把 Time.captureFramerate 或者 Application.targetFrameRate 锁定到一个固定值,比如 30 或者 60,比自然帧率下录制稳定得多,因为 Recorder 采样的推进逻辑与时序关系非常密切,帧率一动,时间戳和帧之间的映射就会乱。固定帧率后再录,曲线上的关键帧分布均匀,后面做剪辑、压缩、拼接都省心很多。
