Unity Animator Controller相关脚本集

新项目中使用了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 = "<Root Transform>";

                    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(" *(?<name>\\w+) *(?<firstFrame>[0-9]+) *(?<lastFrame>[0-9]+) *(?<loop>(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<AnimationClip> clipList = new List<AnimationClip>();
                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<AnimationClip>(animAssetPath);
                        if (clip != null)
                        {
                            clipList.Add(clip);
                        }
                    }
                }
                AnimatorController animController = AssetDatabase.LoadAssetAtPath<AnimatorController>(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<AnimationClip> 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);
        }
    }
}

感谢浏览,有问题请留言。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容