前言
美术做一个模型,附带很多动画,然后就创建和管理动画状态机,然后类似的模型可能有很多,也就是我们所理解的皮肤,低级的是换贴图,高级一点的换模型,如果模型比较多的话,美术要创建和管理很多相同的动画,重复性的劳动,这会就需要有个动画生成器了。
思路
我们可以根据美术的要求通过代码创建一个AnimatorController,但一旦美术修改什么需求我们就要跟着修改会比较麻烦,比较简便的是美术先创建一个动画控制器模板,然后接下来重复性的劳动我们就通过程序工具来解决。思路是copy模板,修改模板里面动画状态的Motion,指向当前模型的动画clip,Motion有的是简单的动画clip,这个直接替换没啥好说的,有的是混合树BlendTree,这个替换会碰到小坑,如果我们获取到混合树的Motion然后遍历里面的Children,替换每个child的Motion是替换不了的,这个我也是查看了BlendTree的源码才知道,源码里面Children的管理(增加和删除)都是通过覆盖的方式替换Children数组,但我也有通过替换Children数组的方式来实现还是没有替换成功,最后还是通过代码创建BlendTree的方式来替换BlendTree才成功了。
效果
代码
基于Odin插件的模式
1 | using Sirenix.OdinInspector; |
基于OnGUI的原生模式
OnGUI的模式是采用的Selection方式选择模板和目录。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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Windows.Forms;
using System;
using System.IO;
using UnityEditor.Animations;
public class AnimatorEditor : EditorWindow
{
string fileDirectory = string.Empty;
string assetDirectory = string.Empty;
bool recursion = false; //是否是递归模式
AnimatorController animatorControllerTemplate = null;
[UnityEditor.MenuItem("Tools/AnimatorEditor")]
private static void Open()
{
var window = EditorWindow.GetWindow(typeof(AnimatorEditor), true, "动画生成器", true);
window.Show();
}
void OnSelectionChange()
{
if (Selection.activeObject != null)
{
Debug.Log("选择物体:" + Selection.activeObject);
if (Selection.activeObject is AnimatorController)
{
animatorControllerTemplate = Selection.activeObject as AnimatorController;
Debug.Log("选择的物体是:" + animatorControllerTemplate.name);
}
if (Selection.activeObject is DefaultAsset) //选择目录
{
var asset = Selection.activeObject as DefaultAsset;
string[] strs = Selection.assetGUIDs;
string path = AssetDatabase.GUIDToAssetPath(strs[0]);
assetDirectory = path;
fileDirectory = Path.Combine(Environment.CurrentDirectory, path);
Debug.Log("选择的路径:" + path);
}
}
}
void OnGUI()
{
GUILayout.BeginHorizontal();
GUILayout.Label("选择Controller模板:");
GUILayout.Label(animatorControllerTemplate == null ? "" : animatorControllerTemplate.name);
GUILayout.EndHorizontal();
GUILayout.Space(5);
GUILayout.BeginHorizontal();
GUILayout.Label("选择生成的目录:");
GUILayout.Label(assetDirectory);
GUILayout.EndHorizontal();
GUILayout.Space(5);
GUILayout.BeginHorizontal();
GUILayout.Label("是否批量生成:");
recursion = EditorGUILayout.Toggle(recursion);
GUILayout.EndHorizontal();
GUILayout.Space(30);
GUILayout.BeginHorizontal();
if (GUILayout.Button("选择要生成的目录(递归遍历)"))
{
FolderBrowserDialog fbd = new FolderBrowserDialog();
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
if (fbd.ShowDialog() == DialogResult.OK)
{
fileDirectory = fbd.SelectedPath;
recursion = true;
}
Debug.Log("选择目录:" + fileDirectory);
}
if (GUILayout.Button("选择要生成的目录(单文件目录)"))
{
FolderBrowserDialog fbd = new FolderBrowserDialog();
fbd.RootFolder = Environment.SpecialFolder.MyComputer;
if (fbd.ShowDialog() == DialogResult.OK)
{
fileDirectory = fbd.SelectedPath;
recursion = false;
}
Debug.Log("选择目录:" + fileDirectory);
}
GUILayout.EndHorizontal();
GUILayout.Space(30);
if (GUILayout.Button("生成AnimatorController"))
{
Debug.Log("生成动画控制器");
CreateAnimatorAssets();
}
}
private void CreateAnimatorAssets()
{
if (!Directory.Exists(fileDirectory))
{
throw new Exception("目录不存在或者路径不存在");
}
if (animatorControllerTemplate == null)
{
Debug.LogError("没有选择动画模板");
return;
}
var animatorFilePath = AssetDatabase.GetAssetPath(animatorControllerTemplate);
var dirArray = fileDirectory.Split('\\');
var pathLastDirectoryName = dirArray[dirArray.Length - 1];
var animatorExtension = Path.GetExtension(animatorFilePath);
if (recursion)
{
var folders = Directory.GetDirectories(fileDirectory);
foreach (var folder in folders)
{
SingleFolderDispose(folder, animatorFilePath);
}
}
else
{
SingleFolderDispose(fileDirectory, animatorFilePath);
}
}
private void SingleFolderDispose(string folder, string animatorFilePath)
{
DirectoryInfo info = new DirectoryInfo(folder);
string folderName = info.Name;
var newAnimatorFilePath = Path.Combine(folder, folderName + Path.GetExtension(animatorFilePath));
File.Copy(animatorFilePath, newAnimatorFilePath, true);
AssetDatabase.Refresh();
AnalyzeAnimController(folder, newAnimatorFilePath);
AssetDatabase.Refresh();
var obj = LoadFbx(folder, newAnimatorFilePath);
PrefabUtility.SaveAsPrefabAsset(obj, string.Format("{0}/{1}.prefab", folder, folderName));
DestroyImmediate(obj);
}
private GameObject LoadFbx(string folder, string animatorFilePath)
{
//找到fbx,找到当前目录下名字不带@符号的fbx 进行实例化
FileInfo tempFile = null;
DirectoryInfo folderDirectoryInfo = new DirectoryInfo(folder);
var files = folderDirectoryInfo.GetFiles();
foreach (var fileInfo in files)
{
if (!fileInfo.Name.Contains("@") && fileInfo.Name.Contains(".fbx") && !fileInfo.Name.Contains(".meta")) //TODO:也可以根据floder名去找对应的fbx
{
tempFile = fileInfo;
break;
}
}
if (tempFile == null)
{
throw new Exception(string.Format("目录:{0} 没有找到不带@的fbx", folder));
}
var obj = Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>(GetAssetPath(tempFile.FullName))) as GameObject;
//找到controller
var controller = AssetDatabase.LoadAssetAtPath<RuntimeAnimatorController>(GetAssetPath(animatorFilePath));
obj.GetComponent<Animator>().runtimeAnimatorController = controller;
return obj;
}
private void AnalyzeAnimController(string floder, string controllerPath)
{
var assetPath = GetAssetPath(controllerPath);
var animatorController = AssetDatabase.LoadAssetAtPath<AnimatorController>(assetPath);
string animationFolder = Path.GetDirectoryName(assetPath) + "\\animations";
animationFolder.Replace("\\", "/");
//animatorController的Parameters不需要修改
//遍历所有的layer
for (int i = 0; i < animatorController.layers.Length; i++)
{
var layer = animatorController.layers[i];
AnimatorStateMachine sm = layer.stateMachine;
RecursionAnalyzeAnimatorStateMachine(sm, animationFolder);
}
}
private string GetAssetPath(string fullPath)
{
var strs = fullPath.Split(new string[] { "Assets" }, StringSplitOptions.None);
var assetPath = "Assets" + strs[strs.Length - 1];
assetPath.Replace("\\", "/");
return assetPath;
}
private void RecursionAnalyzeAnimatorStateMachine(AnimatorStateMachine stateMachine, string animationFlolder)
{
//遍历states
for (int i = 0; i < stateMachine.states.Length; i++)
{
var animatorState = stateMachine.states[i];
var motion = animatorState.state.motion;
if (motion != null)
{
if (motion is BlendTree)
{
BlendTree bt = motion as BlendTree;
ChildMotion[] childMotions = new ChildMotion[bt.children.Length];
for (int j = 0; j < bt.children.Length; j++)
{
var childMotion = bt.children[j];
var motionClip = GetAnimationClip(childMotion.motion.name, animationFlolder);
if (motionClip == null)
{
Debug.LogError("没有找到" + motion.name + "的动画控制器");
}
else
{
Debug.Log(string.Format("Name:{0} Motion:{1}", animatorState.state.name, childMotion.motion)); //根据名字找到对应的prefab 然后找出里面的动画文件加载
//childMotion.motion = (Motion)motionClip;
//var newChildMotion = new ChildMotion() { motion = motionClip, cycleOffset = childMotion.cycleOffset, mirror = childMotion.mirror, directBlendParameter = childMotion.directBlendParameter, position = childMotion.position, threshold = childMotion.threshold, timeScale = childMotion.timeScale };
//childMotion = newChildMotion;
childMotions[j] = new ChildMotion() { motion = (Motion)motionClip, cycleOffset = childMotion.cycleOffset, mirror = childMotion.mirror, directBlendParameter = childMotion.directBlendParameter, position = childMotion.position, threshold = childMotion.threshold, timeScale = childMotion.timeScale };
}
}
//bt.children = childMotions;
BlendTree newBt = new BlendTree()
{
blendParameter = bt.blendParameter,
blendParameterY = bt.blendParameterY,
blendType = bt.blendType,
hideFlags = bt.hideFlags,
maxThreshold = bt.maxThreshold,
minThreshold = bt.minThreshold,
name = bt.name,
useAutomaticThresholds = bt.useAutomaticThresholds,
children = childMotions,
};
animatorState.state.motion = newBt;
}
else
{
animatorState.state.motion = null;
var motionClip = GetAnimationClip(motion.name, animationFlolder);
if (motionClip == null)
{
Debug.LogError("没有找到" + motion.name + "的动画控制器");
}
else
{
animatorState.state.motion = (Motion)motionClip;
Debug.Log(string.Format("Name:{0} Motion:{1}", animatorState.state.name, motion));
}
}
}
}
//遍历substatemachine
for (int j = 0; j < stateMachine.stateMachines.Length; j++)
{
var stateMachines = stateMachine.stateMachines[j];
RecursionAnalyzeAnimatorStateMachine(stateMachines.stateMachine, animationFlolder);
}
}
private AnimationClip GetAnimationClip(string motionName, string animationFolder)
{
var motionNameExt = motionName.Substring(motionName.IndexOf("_"));
DirectoryInfo directoryInfo = new DirectoryInfo(animationFolder);
FileInfo tempFileInfo = null;
var files = directoryInfo.GetFiles("*.FBX", SearchOption.AllDirectories);
for (int i = 0; i < files.Length; i++)
{
if (files[i].Name.EndsWith(motionNameExt + ".FBX")) //有可能是Robert01_gun_jump_start 对应的Robert01@Robert01_gun_jump
{
tempFileInfo = files[i];
break;
}
}
if (tempFileInfo != null)
{
var datas = AssetDatabase.LoadAllAssetsAtPath(GetAssetPath(tempFileInfo.FullName));
if (datas.Length == 0)
{
Debug.Log(string.Format("Can't find clip in {0}", tempFileInfo.FullName));
return null;
}
foreach (var data in datas)
{
if (!(data is AnimationClip))//如果不是动画文件则跳过
continue;
var newClip = data as AnimationClip;
return newClip;
}
}
else
{
Debug.LogError("没有找到对应的动画FBX:" + motionName);
}
return null;
}
}
打开弹框
打开弹框可以用Unity内置的System.Windows.Forms.dll的api来打开,将其放在Plugins下,打开方法:1
2
3
4
5
6
7
8
9
10public void OpenFile()
{
OpenFileDialog dialog = new OpenFileDialog();
dialog.Filter = "exe files (*.exe)|*.exe"; //过滤文件类型
dialog.InitialDirectory = "D:\\"; //定义打开的默认文件夹位置,可以在显示对话框之前设置好各种属性
if (dialog.ShowDialog() == DialogResult.OK)
{
Debug.Log(dialog.FileName);
}
}
纯代码创建控制器
1 | using System; |
碰到的坑
据反馈,新生成的动画控制器在关闭Unity之后,重新打开会发现新生成的Animaor出问题了,motion丢失,但测试下来直接将Clip赋给Motin没有问题,子状态机这种情况也没有问题,唯独BlendTree有问题,我对比新生成的Animator文件跟模板Animator文件对比发现FileID=0,也就是说BlendTree文件并没有保存下来,但BlendTree又不像动画Clip那样我们能直接看到,经过查看Animator文件的数据会发现BlendTree信息写在Animator中,也就是BT并没有保存下来,那么如何保存代码修改的Animation的BlendTree呢?我看到Unity论坛有人碰到类似的问题,https://forum.unity.com/threads/how-to-save-the-animation-blend-tree-created-by-script.480320/,感谢题主!解决方法就是:
经过这段代码,会将BlendTree的信息写入到Animator中,
这样问题就解决了!
工程下载
https://gitee.com/dingxiaowei/AnimatorGenerator