新项目中使用了unity的动画控制器animator,写了以下几个小脚本。
1.导入fbx并拆分其中的动画,修改fbx导入设置。
2.导出fbx中的动画到指定目录,生成独立的Animation Clip.
3.动态创建及修改Animator Controller.
1.美术把fbx提交svn的同时配上一个txt,里面指定哪一帧到哪一帧为哪个动作。脚本根据这个配置去拆分动作,并保存fbx.这样就不需要美术同学手动去拆分动作,而且可以在unity中方便地预览指定的动作。
code:
fbx_bone_1001.txt
idle 80 140 loop
fire 330 340 loop
walk 1850 1886 loop
run 430 446 loop
dead 680 724
attack 1290 1320
FbxAnimListPostprocessor.cs
// FbxAnimListPostprocessor.cs : Use an external text file to import a list of
// splitted animations for FBX 3D models.
//
// Put this script in your "Assets/Editor" directory. When Importing or
// Reimporting a FBX file, the script will search a text file with the
// same name and the ".txt" extension.
// File format: one line per animation clip "firstFrame-lastFrame loopFlag animationName"
// The keyworks "loop" or "noloop" are optional.
// Example:
// idle 80 140 loop
// dead 680 724
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;
using System;
using UnityEditor.Animations;
public class FbxAnimListPostprocessor : AssetPostprocessor
{
public void OnPreprocessModel()
{
if (Path.GetExtension(assetPath).ToLower() == ".fbx"
&& !assetPath.Contains("@"))
{
try
{
// Remove 6 chars because dataPath and assetPath both contain "assets" directory
string fileAnim = Application.dataPath + Path.ChangeExtension(assetPath, ".txt").Substring(6);
StreamReader file = new StreamReader(fileAnim);
string sAnimList = file.ReadToEnd();
file.Close();
//if (EditorUtility.DisplayDialog("FBX Animation Import from file",
//fileAnim, "Import", "Cancel"))
{
System.Collections.ArrayList List = new ArrayList();
ParseAnimFile(sAnimList, ref List);
ModelImporter modelImporter = assetImporter as ModelImporter;
modelImporter.splitAnimations = true;
modelImporter.clipAnimations = (ModelImporterClipAnimation[])
List.ToArray(typeof(ModelImporterClipAnimation));
// 根据项目需要可选
/* modelImporter.motionNodeName = "";
modelImporter.importMaterials = false;
modelImporter.animationRotationError = 0.5f;
modelImporter.animationPositionError = 0.1f;
modelImporter.animationScaleError = 0.5f;
modelImporter.animationType = ModelImporterAnimationType.Generic; */
//EditorUtility.DisplayDialog("Imported animations",
// "Number of imported clips: "
// + modelImporter.clipAnimations.GetLength(0).ToString(), "OK");
}
}
catch { }
// (Exception e) { EditorUtility.DisplayDialog("Imported animations", e.Message, "OK"); }
}
}
public static void ParseAnimFile(string sAnimList, ref System.Collections.ArrayList List)
{
Regex regexString = new Regex(" *(?\\w+) *(?[0-9]+) *(?[0-9]+) *(?(loop|noloop|.))",
RegexOptions.Compiled | RegexOptions.ExplicitCapture);
Match match = regexString.Match(sAnimList, 0);
while (match.Success)
{
ModelImporterClipAnimation clip = new ModelImporterClipAnimation();
if (match.Groups["firstFrame"].Success)
{
clip.firstFrame = System.Convert.ToInt32(match.Groups["firstFrame"].Value, 10);
}
if (match.Groups["lastFrame"].Success)
{
clip.lastFrame = System.Convert.ToInt32(match.Groups["lastFrame"].Value, 10);
}
if (match.Groups["loop"].Success)
{
clip.loop = match.Groups["loop"].Value == "loop";
clip.loopTime = match.Groups["loop"].Value == "loop";
clip.loopPose = match.Groups["loop"].Value == "loop";
}
if (match.Groups["name"].Success)
{
clip.name = match.Groups["name"].Value;
}
List.Add(clip);
match = regexString.Match(sAnimList, match.Index + match.Length);
}
}
}
引用网址:http://wiki.unity3d.com/index.php/FbxAnimListPostprocessor
改了一下正则,是由于美术3dmax中导出的txt是 idle 80 140 这种格式的。
2.把在fbx中拆分好的动画导出到指定目录下,生成一个个独立的Animation Clip,每个clip单独打assetbundle,这样可以使得更新某个动作时可以尽可能少地更新资源。
code
AnimClipExtract.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine.UI;
using UnityEditor;
using System.Linq;
public class AnimClipExtract
{
[MenuItem("Tools/ExtractAnim")]
static void ExtractAnimClipTool()
{
Object[] objs = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);
var fbxPaths = objs.Select(x => AssetDatabase.GetAssetPath(x))
.Where(x => x.ToLower().EndsWith("fbx"));
if(fbxPaths.Count() == 0)
Debug.LogError("未选择fbx文件;请至少选中一个fbx文件!");
fbxPaths.ToList().ForEach(ExtractAnimClip);
}
public static void ExtractAnimClip(string fbxPath)
{
if(!fbxPath.ToLower().EndsWith("fbx"))
{
Debug.LogError(fbxPath + " 不是有效的FBX文件");
return;
}
if (fbxPath.Contains(Application.dataPath))
fbxPath = "Assets" + "/" + fbxPath.Substring(Application.dataPath.Length+1);
Object[] assets = AssetDatabase.LoadAllAssetsAtPath(fbxPath);
if(assets.Length == 0)
{
Debug.LogError(fbxPath + " 读取FBX文件失败;");
return;
}
try
{
string fbxName = Path.GetFileNameWithoutExtension(fbxPath);
string caption1 = "AnimClipExtractTool - " + fbxName + ".fbx";
EditorUtility.DisplayProgressBar(caption1, fbxPath + "分析中", 0);
int start = fbxName.ToLower().IndexOf("fbx_bone_");
start = start == -1 ? 0 : start + "fbx_bone_".Length;
string Dir1 = "Art/AnimClip";
Directory.CreateDirectory(Application.dataPath + "/" + Dir1);
string Dir2 = Dir1 + "/" + fbxName.Substring(start);
string extractDirectory = Application.dataPath + "/" + Dir2;
Directory.CreateDirectory(extractDirectory);
string[] fileArray = Directory.GetFiles(extractDirectory, "*.anim", SearchOption.AllDirectories);
foreach(string filePath in fileArray)
{
File.Delete(filePath);
}
string extractDir = "Assets" + "/" + Dir2;
//这里先生成到tmp目录再拷贝覆盖,是因为直接生成到目标目录meta文件的guid会变
string tmpDir = "Assets/Art/AnimClip/tmp/";
Directory.CreateDirectory(tmpDir);
var clipAssets = assets.Where(a => a is AnimationClip);
int i = 0;
foreach (var item in clipAssets)
{
var clone = Object.Instantiate(item);
//AssetDatabase.CreateAsset(clone, extractDir + "/" + item.name + ".anim");
AssetDatabase.CreateAsset(clone, tmpDir + item.name + ".anim");
string srcFile = Application.dataPath + "/" + "Art/AnimClip/tmp/" + item.name + ".anim";
string destFile = Application.dataPath + "/" + Dir2 + "/" + item.name + ".anim";
File.Copy(srcFile, destFile, true);
EditorUtility.DisplayProgressBar(caption1, "导出" + item.name + ".anim", i++ / clipAssets.Count());
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
Debug.Log("导出位置:" + extractDir + " 导出数量:" + i + " anim导出完成 " + fbxPath);
}
finally
{
EditorUtility.ClearProgressBar();
}
}
}
例:选中一个名叫fbx_bone_1001.fbx的文件,使用Tools\ExtractAnim,会在Assets/Art/AnimClip下生成1001的文件夹,并生成Animation Clip生成到该目录下。
3.做好第一个单位的Animator Controller之后,后面的单位以这个为模版生成自己的controller.对于已经存在的controller,会使用第2步生成的Animation Clip根据名字替换controller中每一个节点的motion
code
UpdateAnimatorController.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.Animations;
using System.IO;
using System.Collections;
public class UpdateAnimatorController
{
static void GenerateController(string fbxPath)
{
string fbxName = Path.GetFileNameWithoutExtension(fbxPath);
string startStr = "fbx_bone_";
if (fbxName.StartsWith(startStr))
{
int start = startStr.Length;
string unitName = fbxName.Substring(start);
string unitId = unitName.Substring(0, unitName.IndexOf('_'));
string controllerName = unitId + ".controller";
string controllerPath = "Art/AnimController/" + controllerName;
string controllerAssetPath = "Asset/" + controllerPath;
string controllerFullPath = Application.dataPath + "/" + controllerPath;
if (!File.Exists(controllerFullPath))
{
File.Copy(Application.dataPath + "/" + "Art/AnimController/1001.controller", controllerFullPath);
}
if (File.Exists(controllerFullPath))
{
List clipList = new List();
clipList.Clear();
string animClipFolder = Application.dataPath + "/" + "Art/AnimClip/" + unitId;
if (Directory.Exists(animClipFolder))
{
DirectoryInfo directory = new DirectoryInfo(animClipFolder);
FileInfo[] files = directory.GetFiles("*.anim", SearchOption.AllDirectories);
foreach (FileInfo file in files)
{
string animPath = file.ToString();
string animAssetPath = animPath.Substring(animPath.IndexOf("Assets"));
AnimationClip clip = AssetDatabase.LoadAssetAtPath(animAssetPath);
if (clip != null)
{
clipList.Add(clip);
}
}
}
AnimatorController animController = AssetDatabase.LoadAssetAtPath(controllerAssetPath);
for (int i = 0; i < animController.layers.Length; i++)
{
AnimatorStateMachine stateMachine = animController.layers[i].stateMachine;
UpdateAnimator(clipList, stateMachine);
}
}
}
else
{
Debug.Log("fbx name not valid. ");
}
}
static void UpdateAnimator(List newClips, AnimatorStateMachine stateMachine)
{
for (int i = 0; i < stateMachine.states.Length; i++)
{
ChildAnimatorState childState = stateMachine.states[i];
if (childState.state.motion == null)
{
if (childState.state.name.CompareTo("New State") == 0 || childState.state.name.CompareTo("empty") == 0)
continue;
Debug.LogWarning(" UpdateAnimatorController Null : " + childState.state.name + ",layer name: " + stateMachine.name);
continue;
}
if (childState.state.motion.GetType() == typeof(AnimationClip))
{
for (int j = 0; j < newClips.Count; j++)
{
if (newClips[j].name.CompareTo(childState.state.motion.name) == 0)
{
childState.state.motion = (Motion)newClips[j];
break;
}
}
}
else if (childState.state.motion.GetType() == typeof(UnityEditor.Animations.BlendTree))
{
UnityEditor.Animations.BlendTree tree = (UnityEditor.Animations.BlendTree)childState.state.motion;
BlendTreeType treeType = tree.blendType;
ChildMotion[] childMotionArray = tree.children;
for (int k = 0; k < childMotionArray.Length; k++)
{
if (childMotionArray[k].motion.GetType() == typeof(AnimationClip))
{
for (int j = 0; j < newClips.Count; j++)
{
if (newClips[j].name.CompareTo(childMotionArray[k].motion.name) == 0)
{
childMotionArray[k].motion = (Motion)newClips[j];
break;
}
}
}
else if (childMotionArray[k].motion.GetType() == typeof(UnityEditor.Animations.BlendTree))
{
Debug.LogError("You need to change it!");
}
}
tree.children = childMotionArray;
}
}
for (int i = 0; i < stateMachine.stateMachines.Length; i++)
{
UpdateAnimator(newClips, stateMachine.stateMachines[i].stateMachine);
}
}
}
感谢浏览,有问题请留言。