准备

导入思路

  • 2d游戏一般都会打atlas,atlas里有每个图片的rect信息,导出工具可以将此信息导出。
  • unity的sprite importer支持代码处理sprite,可以直接设置spritePivot,使用导出的offset信息,可以直接批量处理所有图片。
  • 使用代码生成Animation Clip。
  • 使用代码生成Animator Controller。
修改 SpritePivot 参考
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.IO;

namespace Rpg.Importer
{
    public class DNFSpriteImporter
    {
        public static void ApplyTextures(Texture[] textures, OffsetData[] offsetDatas, TextureImporterSettings textureSettings, bool useOffsetInfo)
        {
            if (textures.Length <= 0 || offsetDatas.Length <= 0 || offsetDatas.Length < textures.Length)
            {
                Debug.LogError($"offset is not equal! textures : {textures.Length} , offsetDatas : {offsetDatas.Length}");
                return;
            }

            try
            {
                AssetDatabase.StartAssetEditing();

                for (int i = 0; i < textures.Length; i++)
                {
                    var path = AssetDatabase.GetAssetPath(textures[i]);
                    var texture = textures[i];

                    EditorUtility.DisplayProgressBar("import img", path, (float)i / textures.Length);

                    TextureImporter import = AssetImporter.GetAtPath(path) as TextureImporter;
                    if (textureSettings == null)
                    {
                        textureSettings = new TextureImporterSettings();
                    }

                    import.ReadTextureSettings(textureSettings);

                    textureSettings.textureType = TextureImporterType.Sprite;

                    textureSettings.npotScale = TextureImporterNPOTScale.None;
                    textureSettings.sRGBTexture = true;
                    textureSettings.spritePixelsPerUnit = 100;
                    textureSettings.alphaSource = TextureImporterAlphaSource.FromInput;
                    textureSettings.spriteMode = (int)SpriteImportMode.Single;
                    textureSettings.spriteMeshType = SpriteMeshType.FullRect;


                    textureSettings.mipmapEnabled = false;
                    textureSettings.wrapMode = TextureWrapMode.Repeat;
                    textureSettings.filterMode = FilterMode.Point;

                    if (useOffsetInfo)
                    {
                        textureSettings.spriteAlignment = (int)SpriteAlignment.Custom;
                        textureSettings.spritePivot = new Vector2(
                            0.5f - (float)(offsetDatas[i].OffsetX + texture.width / 2) / texture.width,
                            0.5f + (float)(offsetDatas[i].OffsetY + texture.height / 2) / texture.height
                        );
                    }
                    else
                    {
                        textureSettings.spriteAlignment = (int)SpriteAlignment.Center;
                    }

                    import.SetTextureSettings(textureSettings);
                    AssetDatabase.ImportAsset(path);
                }

                AssetDatabase.Refresh();
            }
            finally
            {
                AssetDatabase.StopAssetEditing();

                EditorUtility.ClearProgressBar();
            }
        }
    }

    public class BatDNFSpriteImporterWindow : EditorWindow
    {
        [MenuItem("DNF/Bat_DNFSpriterImporter")]
        private static void Init()
        {
            var window = EditorWindow.GetWindow<BatDNFSpriteImporterWindow>();
            window.Show();
        }

        public bool useOffsetInfo = true;
        public List<string> paths = new List<string>();

        private Vector2 m_ScrollPos;
        private void OnGUI()
        {
            EditorGUILayout.HelpBox("1. Drag folder(s) which contains sprites and info.json to the paths label.\n2. Click process button.", MessageType.Info);

            var dragArea = GUILayoutUtility.GetRect(0f, 35f, GUILayout.ExpandWidth(true));
            EditorGUI.LabelField(dragArea, "Paths : drag folder(s) here.");

            m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos);
            for (int i = 0; i < paths.Count; i++)
            {
                EditorGUILayout.BeginHorizontal();
                EditorGUILayout.LabelField($"{i.ToString()}. ", GUILayout.Width(18));
                paths[i] = EditorGUILayout.TextField(paths[i]);
                EditorGUILayout.EndHorizontal();
            }
            EditorGUILayout.EndScrollView();

            if (Event.current.type == EventType.DragUpdated && dragArea.Contains(Event.current.mousePosition))
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
            }
            else if (Event.current.type == EventType.DragExited && dragArea.Contains(Event.current.mousePosition))
            {
                var dragPaths = DragAndDrop.paths;
                if (DragAndDrop.paths != null && dragPaths.Length > 0)
                {
                    for (int i = 0; i < dragPaths.Length; i++)
                    {
                        if (!paths.Contains(dragPaths[i]))
                            paths.Add(dragPaths[i]);
                    }
                }
            }

            if (GUILayout.Button("Clear all paths"))
            {
                paths.Clear();
            }

            useOffsetInfo = EditorGUILayout.Toggle("使用info文件", useOffsetInfo);

            EditorGUILayout.Space();
            if (GUILayout.Button("Process"))
            {
                List<Texture> texArray = new List<Texture>(48);
                TextureImporterSettings settings = new TextureImporterSettings();

                for (int i = 0; i < paths.Count; i++)
                {
                    texArray.Clear();

                    var texDirs = Directory.GetFiles(paths[i], "*.png", SearchOption.AllDirectories);
                    var strDirs = Directory.GetFiles(paths[i], "*.json", SearchOption.AllDirectories);

                    Debug.Log("texDirs length : " + texDirs.Length + " , strDirs length : " + strDirs.Length);

                    for (int k = 0; k < texDirs.Length; k++)
                    {
                        texArray.Add(AssetDatabase.LoadAssetAtPath<Texture>(texDirs[k]));
                    }
                    texArray.Sort((a, b) => int.Parse(a.name) - int.Parse(b.name));

                    var offsets = JsonConvert.DeserializeObject<List<OffsetData>>(AssetDatabase.LoadAssetAtPath<TextAsset>(strDirs[0]).text);

                    // Debug.Log($"textures : {texArray.Count} , offsetDatas : {offsets.Count}");
                    // for (int k = 0; k < texArray.Count; k++)
                    // {
                    //     var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GetAssetPath(texArray[k]));
                    //     Debug.Log(
                    //         $"[id : {texArray[k].name}] \t [offset : ({offsets[i].x} , {offsets[i].y})] \t " +
                    //             $"[pivot : ({ 0.5f - (float)(offsets[i].OffsetX + sprite.rect.width / 2) / sprite.rect.width} , " +
                    //             $"{ 0.5f + (float)(offsets[i].OffsetY + sprite.rect.height / 2) / sprite.rect.height})]");
                    // }
                    DNFSpriteImporter.ApplyTextures(texArray.ToArray(), offsets.ToArray(), settings, useOffsetInfo);
                }
            }
        }
    }

    [System.Serializable]
    public class OffsetData
    {
        const int AddOffsetX = 229;
        const int AddOffsetY = 333;

        public int x;
        public int y;

        public int OffsetX { get { return x - AddOffsetX; } }
        public int OffsetY { get { return y - AddOffsetY; } }
    }
}
生成 Animation Clip 参考
using Saro.Paperdoll;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace Rpg
{
    public class DnfAnimationCreator : EditorWindow
    {
        [MenuItem("DNF/Animation Creator")]
        static void DoIt()
        {
            GetWindow<DnfAnimationCreator>();
        }

        private string m_OutputPath = "Assets/Arts/Animator/AnimationCreateByScripts/clip new.anim";
        private float m_FrameRate = 12f;
        private SpritePack m_SpritePack;
        private Paperdoll m_Paperdoll;

        private void OnGUI()
        {
            m_OutputPath = EditorGUILayout.TextField("OutputPath", m_OutputPath);
            m_SpritePack = EditorGUILayout.ObjectField("SpritePack", m_SpritePack, typeof(SpritePack), false) as SpritePack;
            m_Paperdoll = EditorGUILayout.ObjectField("Actor(PaperDoll)", m_Paperdoll, typeof(Paperdoll), true) as Paperdoll;

            if (m_SpritePack && m_Paperdoll)
            {
                if (GUILayout.Button("create aniamtion"))
                {
                    try
                    {
                        var clip = new AnimationClip();
                        clip.frameRate = m_FrameRate;
                        clip.legacy = false;

                        var category = m_SpritePack.m_SpriteCategories[0];
                        var hashKeys = category.hashes;
                        var animatedKeys = new Keyframe[category.sprites.Length];
                        for (int i = 0; i < hashKeys.Length; i++)
                        {
                            var hash = hashKeys[i];
                            animatedKeys[i] = new Keyframe
                            {
                                time = 1f / m_FrameRate * i,
                                value = PaperdollUtility.ConvertIntToFloat(hash),
                            };
                        }
                        var curve = new AnimationCurve(animatedKeys);

                        for (int i = 0; i < animatedKeys.Length; i++)
                        {
                            AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
                            AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
                        }

                        var paths = new List<string>();

                        m_Paperdoll.equipSlots.ForEach(slot =>
                            {
                                slot.PaperdollParts.ForEach(p =>
                                {
                                    var path = p.name;
                                    var cur = p.transform;
                                    while (cur.parent)
                                    {
                                        cur = cur.parent;

                                        if (cur == m_Paperdoll.transform) break;

                                        path = cur.name + "/" + path;
                                    }

                                    paths.Add(path);
                                });
                            }
                        );

                        for (int i = 0; i < paths.Count; i++)
                        {
                            var animatedBindings = EditorCurveBinding.FloatCurve(paths[i], typeof(PaperdollPart), "m_SpriteHash");
                            AnimationUtility.SetEditorCurve(clip, animatedBindings, curve);
                        }

                        AssetDatabase.CreateAsset(clip, m_OutputPath);
                        AssetDatabase.Refresh();
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogError(e);
                    }
                }
            }
        }
    }
}
生成 Animator Controller 参考
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using UnityEditor.Animations;

namespace Rpg
{
    public class DnfAnimatorBuilder : EditorWindow
    {
        [MenuItem("DNF/AnimatorBuilder")]
        private static void ShowWindow()
        {
            var window = GetWindow<DnfAnimatorBuilder>();
            window.titleContent = new GUIContent("AnimatorBuilder");
            window.Show();
        }

        public List<AnimationClip> animClips = new List<AnimationClip>();

        public string savePath = "Assets/Arts/Animator/";
        public string animatorName = "Test.controller";

        private Vector2 m_ScrollPos;

        private void OnGUI()
        {
            EditorGUILayout.HelpBox("1. Drag folder(s) to paths label.\n2. Click process button.", MessageType.Info);

            savePath = EditorGUILayout.TextField(savePath);
            animatorName = EditorGUILayout.TextField(animatorName);

            var dragArea = GUILayoutUtility.GetRect(0f, 35f, GUILayout.ExpandWidth(true));
            EditorGUI.LabelField(dragArea, "Animation Clips : drag animation(s) here.");

            m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos);
            for (int i = 0; i < animClips.Count; i++)
            {
                EditorGUILayout.BeginHorizontal();
                EditorGUILayout.LabelField($"{i.ToString()}. ", GUILayout.Width(18));
                EditorGUILayout.ObjectField(animClips[i], typeof(AnimationClip), false);
                EditorGUILayout.EndHorizontal();
            }
            EditorGUILayout.EndScrollView();

            if (Event.current.type == EventType.DragUpdated && dragArea.Contains(Event.current.mousePosition))
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
            }

            else if (Event.current.type == EventType.DragExited && dragArea.Contains(Event.current.mousePosition))
            {
                var dragPaths = DragAndDrop.objectReferences;
                if (DragAndDrop.paths != null && dragPaths.Length > 0)
                {
                    for (int i = 0; i < dragPaths.Length; i++)
                    {
                        var animClip = dragPaths[i] as AnimationClip;
                        if (animClip)
                        {
                            if (!animClips.Contains(animClip))
                            {
                                animClips.Add(animClip);
                            }
                        }
                    }
                }
            }

            if (GUILayout.Button("Clear all animations"))
            {
                animClips.Clear();
            }

            EditorGUILayout.Space();
            if (GUILayout.Button("Process"))
            {
                BuildAnimator(savePath + animatorName, animClips);
            }
        }

        private void BuildAnimator(string controllerPath, List<AnimationClip> clips)
        {
            /**************************************************************
            /* create animator controller
            /**************************************************************/
            var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);

            var baseLayer = controller.layers[0].stateMachine;

            baseLayer.anyStatePosition = new Vector3(0f, 0f);
            baseLayer.entryPosition = new Vector3(0f, 50f);
            baseLayer.exitPosition = new Vector3(1000f, 0f);

            /**************************************************************
            /* create states
            /**************************************************************/
            var nullstate = baseLayer.AddState("null", new Vector2(0f, 100f));
            baseLayer.defaultState = nullstate;

            var startPos = new Vector3(400f, 0f);
            for (int i = 0; i < clips.Count; i++)
            {
                var state = baseLayer.AddState(clips[i].name, startPos);
                state.motion = clips[i];

                var transition = state.AddExitTransition();
                transition.hasExitTime = true;
                transition.exitTime = 1f;
                transition.duration = 0f;

                startPos.y += 50f;
            }

            AssetDatabase.Refresh();
        }
    }
}

小技巧

  1. AssetDatabase.StartAssetEditing 能加速批量导入。