由于自身的懒惰,一年多没写博客了。。十分愧疚,回想来还是写博客才能将知识更好的牢记并发现问题
前几天工作需要做一个自动生成动画组件的工具类,美术一般只会将做好的动画模型发给你,如果每个模型都手动自己新建Animator再绑定的话 是十分浪费时间的,所以必须得实现脚本自动化。
根据近几天参考的几篇博客,自己写了一个工具类,下面进入正题
[MenuItem("Tools/自动生成动画控制器")]
public static void AutoAllCreateAnimatorControll()
在该函数中,将要进行三步操作
首先我们来实现第一步,新建一个函数名为ExportAllAnimationClips
public static void ExportAllAnimationClips()
{
//检查是否存在animationClips存放的目录
if (Directory.Exists(Application.dataPath + PathTools._AnimationClipsDir) == false) //此处的PathTools._AnimationClipsDir为你自己想存放AnimationClip的路径
{
Directory.CreateDirectory(Application.dataPath + PathTools._AnimationClipsDir);
}
DirectoryInfo directoryInfo = new DirectoryInfo(PathTools._FBXDir);//PathTools._FBXDir是之前存放了模型动画的文件夹
FileInfo[] fileInfos = directoryInfo.GetFiles();
foreach (FileInfo fileInfo in fileInfos)
{
if (fileInfo.Extension == ".fbx")
{
string fileName = fileInfo.Name.Substring(0, fileInfo.Name.IndexOf("."));//获取模型文件的名称
string pathName = fileInfo.FullName.Substring(fileInfo.FullName.IndexOf("Assets\\"));//获取模型文件的Unity的路径
AnimationClip animationClip = AssetDatabase.LoadAssetAtPath(pathName);//获取模型文件中的AnimationClip组件
if (animationClip != null)
{
//以下是验证文件命名的方法 可以根据自己项目来定
string[] names = fileName.Split('_');
if (names.Length != 2)
{
Debug.LogError(fileName + "文件命名有问题,请检查");
continue;
}
int param1, param2, param3 = 0;
if (names[0] != "Model" || int.TryParse(names[1], out param1) == false)
{
Debug.LogError(fileName + "文件命名有问题,请检查");
continue;
}
//创建动画片段
AnimationClip clipTemp = new AnimationClip();
EditorUtility.CopySerialized(animationClip, clipTemp);
var targetDir = "Assets" + PathTools._AnimationClipsDir + "/";
AssetDatabase.CreateAsset(clipTemp, targetDir + fileName + ".anim"); //统一命名
}
}
}
AssetDatabase.Refresh();
}
代码中的路径你可以自己定义一个枚举或者类来编写
public class PathTools
{
//最后生成的AnimatorController目录
public static string _AnimaControlDir = @"Assets/FightingUnityChan_FreeAsset/FightingUnityChan_FreeAsset/Animator/";
//FBX目录
public static string _FBXDir = Application.dataPath + @"/FightingUnityChan_FreeAsset/FightingUnityChan_FreeAsset/Animations";
//clips目录
public static string _AnimationClipsDir = @"/FightingUnityChan_FreeAsset/FightingUnityChan_FreeAsset/NewAnimations";
}
这样第一部就完成了,如果直接执行代码的话 会发现你的项目中已经生成了包含animationClip的文件夹
第一步完成之后,我们的第二步是对AnimationClip文件进行压缩,这一步是否需要可以根据你的项目具体情况而定,如果动画文件过多导致内存紧缺的话不妨尝试,压缩的主要目的是2点
1.去除骨骼动画中的Scale信息(如果你用到了scale数据则不要进行这一步 否则会出问题)
2.减小骨骼点信息的float存储精度
如果想更详细了解的话可以浏览这篇文章,我这里就不多做讲解了,直接贴上代码
public static void CompressAllAnimationClips()
{
//逻辑跟之前差不多
DirectoryInfo directoryInfo = new DirectoryInfo(Application.dataPath + PathTools._AnimationClipsDir);
FileInfo[] fileInfos = directoryInfo.GetFiles();
foreach (FileInfo fileInfo in fileInfos)
{
if (fileInfo.Extension == ".anim")
{
//string fileName = fileInfo.Name.Substring(0, fileInfo.Name.IndexOf(".")); ;
string pathName = fileInfo.FullName.Substring(fileInfo.FullName.IndexOf("Assets\\"));
//获取之前导出的animationClip
AnimationClip animationClip = AssetDatabase.LoadAssetAtPath(pathName);
if (animationClip != null)
{
//复制一份进行压缩动画
var clip = UnityEngine.Object.Instantiate(animationClip) as AnimationClip;
CompressAnim(clip);
EditorUtility.CopySerialized(clip, animationClip);
EditorUtility.SetDirty(animationClip);
}
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
//压缩单个animation 创建新的animation
public static void CompressAnim(AnimationClip clip)
{
ReduceScaleKey(clip);
ReduceFloatPrecision(clip);
}
//去掉骨骼动画scale
private static void ReduceScaleKey(AnimationClip clip)
{
EditorCurveBinding[] curves = AnimationUtility.GetCurveBindings(clip);
for (int j = 0; j < curves.Length; j++)
{
EditorCurveBinding curveBinding = curves[j];
if (curveBinding.propertyName.ToLower().Contains("scale"))
AnimationUtility.SetEditorCurve(clip, curveBinding, null);
}
}
//浮点精度保留3位
private static void ReduceFloatPrecision(AnimationClip clip)
{
EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(clip);
AnimationClipCurveData[] curves = new AnimationClipCurveData[curveBindings.Length];
for (int index = 0; index < curves.Length; ++index)
{
curves[index] = new AnimationClipCurveData(curveBindings[index]);
curves[index].curve = AnimationUtility.GetEditorCurve(clip, curveBindings[index]);
}
foreach (AnimationClipCurveData curveDate in curves)
{
var keyFrames = curveDate.curve.keys;
for (int i = 0; i < keyFrames.Length; i++)
{
var key = keyFrames[i];
key.value = float.Parse(key.value.ToString("f3"));
key.inTangent = float.Parse(key.inTangent.ToString("f3"));
key.outTangent = float.Parse(key.outTangent.ToString("f3"));
keyFrames[i] = key;
}
curveDate.curve.keys = keyFrames;
clip.SetCurve(curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve);
}
}
压缩过后可以发现animationClip的大小减少了,最后我们可以进行AnimatorController的生成了
//根据压缩过的clip,创建AnimatorController
public static void CreateAnimatorControllerByClip()
{
//遍历压缩过的所有animtionClip
DirectoryInfo directoryInfo = new DirectoryInfo(Application.dataPath + PathTools._AnimationClipsDir);
FileInfo[] files = directoryInfo.GetFiles("*.anim", SearchOption.AllDirectories);
string animatorName = "";
List tempList = new List();//这里用到一个数据结构,里面包含一个string和一个int类型
foreach (var file in files)
{
string fileName = file.Name.Substring(0, file.Name.IndexOf("."));
string pathName = file.FullName.Substring(file.FullName.IndexOf("Assets\\"));
string[] names = fileName.Split('_');
if (names.Length != 2 || names[0] != "Model")
{
continue;
}
if (animatorName.Equals(""))
{
animatorName = names[0];
}
int conditionValue = int.Parse(names[1]);
AnimationNameAndCondition temp = new AnimationNameAndCondition(pathName, conditionValue); //将名字和序列存放进list中
tempList.Add(temp);
}
if (tempList.Count == 0)
{
Debug.LogError("Animation获取失败");
return;
}
CreateAnimatorController(PathTools._AnimaControlDir, animatorName, tempList);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("Animator已生成");
}
///
/// 创建动画控制器
///
///
///
///
private static AnimatorController CreateAnimatorController(string animationControlDir, string animatorName, List animationList)
{
//创建animator
var animatorPath = animationControlDir + animatorName + ".controller";
AnimatorController animatorController = AssetDatabase.LoadAssetAtPath(animatorPath);
if (animatorController == null)
{
animatorController = AnimatorController.CreateAnimatorControllerAtPath(animationControlDir + animatorName + ".controller");
}
//设定一个名为Action的Int类型Parameter用作动画切换的标志
bool isExist = false;
var parameters = animatorController.parameters;
foreach (var param in parameters)
{
if (param.name == "Action")
{
isExist = true;
}
}
if (isExist == false)
{
animatorController.AddParameter("Action", AnimatorControllerParameterType.Int);
}
//获取base layer
AnimatorControllerLayer animatorControllerLayer = animatorController.layers[0];
AnimatorStateMachine animatorStateMachine = animatorControllerLayer.stateMachine;
//按condition排序
animationList.Sort((left, right) =>
{
if (left.Condition > right.Condition)
return 1;
else if (left.Condition == right.Condition)
return 0;
else
return -1;
});
//开始添加状态
bool enterState = false;
foreach (var animationPath in animationList)
{
AnimationClip animationClip = AssetDatabase.LoadAssetAtPath(animationPath.Path);
if (!animationClip)
continue;
//添加一个状态
AnimatorState state = null;
string stateName = animatorName + "_" + animationPath.Condition;
var childState = animatorStateMachine.states;
foreach (var animatorState in childState)//遍历是否之前存在状态
{
if (animatorState.state.name == stateName)
{
state = animatorState.state;
break;
}
}
//不存在状态,则添加一个
if (state == null)
{
state = animatorStateMachine.AddState(stateName);
}
state.motion = animationClip;//加入状态机中
//第一个状态默认连接enter
if (enterState == false)
{
var enterStates = animatorStateMachine.entryTransitions;
if (enterStates.Length == 0)
{
animatorStateMachine.AddEntryTransition(state);
}
enterState = true;
}
//所有状态连接anystate
AnimatorStateTransition stateTransition = null;
var anyTransitions = animatorStateMachine.anyStateTransitions;
foreach (var transitions in anyTransitions)//同上的遍历
{
if (transitions.destinationState.name == stateName)
{
stateTransition = transitions;
break;
}
}
if (stateTransition == null)
{
stateTransition = animatorStateMachine.AddAnyStateTransition(state);
//将之前命名下划线后的序列作为动作切换的值
stateTransition.AddCondition(AnimatorConditionMode.Equals, animationPath.Condition, "Action");
}
stateTransition.hasExitTime = false;
stateTransition.canTransitionToSelf = false;
}
EditorUtility.SetDirty(animatorController);
return animatorController;
}
这段代码中用到的数据结构主要是存放condition的值,根据我们之前的命名,下划线后的数字就对应了动画切换的值
public class AnimationNameAndCondition
{
public string Path;
public int Condition;
public AnimationNameAndCondition(string path, int condition)
{
Path = path;
Condition = condition;
}
}
最终我们运行脚本,就可以得到一个AnimatorController了
此时你就可以在运行时代码中获取到Animator组件并调用setinteger切换动画了!
该工具在项目实际运用中还有很多待优化的地方。比如这里只针对了一个文件夹的动画模型,事实项目中是有很多动画模型需要生成的,所以还需要再文件夹下面进行遍历,然后根据项目的需求进行命名。而且根据项目需求也可以不用SetInteger这种切换方法,使用SetTrigger或者SetBool再加入对应的命名枚举同样可以做到。
这里只讲到了生成AnimatorController的过程,之后还需要将组建绑定到prefab上,这个过程也是需要自动化的,目前只想到这么多了,之后有新的改进或者想法会继续补充!如果有问题请指正,谢谢!
参考:http://www.xuanyusong.com/archives/2811
https://www.cnblogs.com/yaoh/p/5149568.html