制作 Unity Animation 事件编辑器

Unity 的人物动作模型fbx文件中一般会有一个或多个 Animation 动画,为了配合音效,我们需要给 Animation 添加事件,Unity自带的Inspector可以实现简单的事件编辑,不过缺点是不能预览动作和音效事件的配合。因此可以自己制作工具来实现在运行时的事件编辑器。

事件读取

在加载人物模型之后,可以获取到身上挂的Animator脚本,从而读取到人物所有的AnimationClip:

1
AnimationClip[] animationClips = CurUnit.animator.runtimeAnimatorController.animationClips;

Animation的基本信息有以下内容:

1
2
3
animationClip.name //clip名称 string
animationClip.averageDuration //持续时间(秒) float
animationClip.events //事件数组 AnimationEvent[]

新增事件

给指定的animationClip新增一个调用函数为SoundEvent的事件,持续时间为1s,string参数为”hit”。

1
2
3
4
5
AnimationEvent evt = new AnimationEvent();
evt.functionName = "SoundEvent";
evt.time = float.Parse(1);
evt.stringParameter = "hit";
animationClip.AddEvent(evt);

修改事件

不能直接修改 animationClip 的 events数组,需要先取出来,修改完再赋值回去。因此编辑时需要自己记住下标eventIndex 。

1
2
3
4
5
6
7
AnimationEvent[] events = animationClip.events;
AnimationEvent evt = new AnimationEvent();
evt.functionName = "SoundEvent";
evt.time = float.Parse(1);
evt.stringParameter = "hit";
events[eventIndex] = evt;
animationClip.events = events;

由于是运行时添加,游戏中会立即增加这个时间,这时播放对于的 animation 动作就可以预览最终效果了。不过这样关闭游戏不会保存到文件,因为数据没有写入到 AssetDatabase。

回退修改

刚才修改了事件,虽然没保存,但是必须重启游戏才能恢复,这样很麻烦。实际上我们可以通过重新导入来实现 Revert 功能。

1
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(animationClip));

一行代码即可搞定。

保存事件

先尝试Unity官方文档所说的方式:

1
AnimationUtility.SetAnimationEvents(animationClip, animationClip.events);

看起来很容易,一行代码就搞定了,关掉游戏再运行也没问题,可是git却没有检测到任何变化,这是怎么回事?

其实网上有不少吐槽这个接口的讨论了,这个接口只会把信息保存到本地缓存中,并没有真的写入到文件里,所以实际上需要自己来做保存这件事。

保存事件要先知道 animation 的 event 信息记录到哪里,实际上不是在 anim 文件中,而是其对应的 meta 文件中。

因此要想保存到文件,要根据读取的 animationClip 找到 AssetPath ,再找到对应数据的 m_ClipAnimations 标签,这里就是所有的AnimationProperty,最后遍历就可以做我们想做的事情了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//找到当前的animationClip的路径
string assetPath = AssetDatabase.GetAssetPath(animationClip);
ModelImporter modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
//读取文件序列化数据 实际就是meta里的数据
SerializedObject serializedObject = new SerializedObject(modelImporter);
//找到所有animation
SerializedProperty clipAnimations = serializedObject.FindProperty("m_ClipAnimations");
if (clipAnimations == null || clipAnimations.arraySize == 0)
{
modelImporter.clipAnimations = modelImporter.defaultClipAnimations;
serializedObject = new SerializedObject(modelImporter);
clipAnimations = serializedObject.FindProperty("m_ClipAnimations");
}
for (int i = 0; i < clipAnimations.arraySize; i++)
{
SerializedProperty clipAnimationProperty = clipAnimations.GetArrayElementAtIndex(i);
if (clipAnimationProperty.displayName == "你要找的animationClip的名字")
{
//找到其events
SerializedProperty eventsProperty = clipAnimationProperty.FindPropertyRelative("events");
//清空事件
eventsProperty.ClearArray();
//重新写入
SerializedProperty eventProperty = eventsProperty.GetArrayElementAtIndex(index);
//数据中的时间是相对于总时间的0到1的小数 而不是以秒为单位的时间 所以要转换一下
eventProperty.FindPropertyRelative("time").floatValue = Time2Percent(evt.time);
eventProperty.FindPropertyRelative("functionName").stringValue = evt.functionName;
eventProperty.FindPropertyRelative("floatParameter").floatValue = evt.floatParameter;
eventProperty.FindPropertyRelative("intParameter").intValue = evt.intParameter;
eventProperty.FindPropertyRelative("data").stringValue = evt.stringParameter;
//应用
serializedObject.ApplyModifiedProperties();
//重新读取
AssetDatabase.ImportAsset(assetPath);
}
}
//最终刷新一下AssetDatabase
AssetDatabase.Refresh();

总结

通过上面的方法就可以在运行时编辑animation的事件了,这个工具相比于Unity Inspector的功能可以更强大,可以实时预览事件在游戏中的效果,尤其是在编辑音效时,就体现出预览的重要性了。