这几天给项目做了一个自动创建新兵种的工具,只需要提供符合规则格式的图片,即可一键自动生成新兵种战斗模型、布阵模型动画。

# 前言

工具:

  • Unity3D 2017.4.28f1
  • TexturePacker 3.0.9

# 战斗模型

我们项目这个版本 (2.2.0) 策划加了新兵种需求,根据兵种的进化情况,战斗中的兵种模型也是需要进行更换的。

图片

之前项目整个旧兵种也就固定的那几个,在拿到第一版的资源后,查了下并尝试了手动创建方式,还是有点麻烦。

手动操作步骤如下:

  • 复制一份老兵种
  • 复制对应美术路径兵种图片到新目录
  • 用 Texturepacker 打开,改生成图片、Json 描述名字
  • 手动打图集
  • 手动改名
  • 手动重新修改材质的图片引用
  • 手动将 json 转化为二进制文件
  • 手动修改兵种 Prefab 上的材质、二进制引用
  • 等...

# 布阵动画模型

另外,除战斗中兵种模型外,还有外部布阵界面的展示模型:

图片

布阵界面的动画模型,说是模型,实际上是 5 张不同动画的图片组成的帧动画,此处已有的功能结构使用了 Unity 自带的 Animation 创建动画,然后引用至 AnimatorController 进行播放使用。

图片

手动操作方式同样繁琐,手动机械化操作非常多。
其创建过程包括:

  • 放入新兵种图片
  • 打图集
  • 使用指定工具选定新图片及 Json 文本创建 Sprite 图集
  • 拖出布阵模型 Prefab,打开 Animation 界面创建对应名字动画
  • 动画设置为 10 帧,并选中之前打好的图集,每 0.1 秒手动拖拽一张图片形成动画
    图片
  • 保存动画,并将动画拖拽到 AnimatorController 中

# 问题

开始策划只给了几个兵种,就感觉手累,特别是新需求一下增加了数以十计的模型需求,后续版本还会继续增加:
图片

手动处理了几个,就感觉不能这样下去了!太过于机械化且浪费时间,何况第一波是 18 个,根据功能计划,下个版本第二波可能就是 36 个了!

于是就想整个自动化工具,最好是一键选择资源目录,可以一条龙处理的,以做到不管后面是 36 个还是 72 个,都可以自动化完成,避免手动重复操作。因此有了这个工具的开发。

# 实现

# 战斗模型

对于战斗模型,原理很简单:拿了一份兵种当做『源』,类似于 Prefab,然后添加新兵种时,复制一份,然后自动处理机械化相关操作。

主要需要处理的是复制后,所属为『新兵种』模型的各个文件引用关系。

其创建一个兵种的代码被封装为一个方法,批量创建时只需要获取路径,循环调用即可。



代码主要就是将上述手动步骤修改为自动步骤,Copy 文件,改名、该引用之类的代码就不贴了,以下是调用 TexturePacker 打新图集的方式:

public void DoCmd(string workingPath, string cmd, float defaulScale = 0.6f, float reduceScale = 0.05f, int retryCount = 5)
{
    float scale = defaulScale;
    int maxRetry = retryCount;
    while (DoCmd(_texturePackerCmdPath, workingPath, cmd + " --scale " + scale) != 0 && maxRetry > 0)
    {
        maxRetry--;
        scale -= reduceScale;
    }
    UnityEngine.Debug.Log(cmd + "  Scale:" + scale);
}
public int DoCmd(string fileName, string workingPath, string cmd)
{
    Process process = new Process();
    process.StartInfo.FileName = fileName;
    process.StartInfo.WorkingDirectory = workingPath;
    process.StartInfo.Arguments = cmd;
    //process.StartInfo.RedirectStandardInput = true;
    process.Start();
    //process.StandardInput.WriteLine(cmd);
    process.WaitForExit();
    return process.ExitCode;
}

代码通过每次调用 Texturepacker 进程结束标识,确定打出来的图集有没有问题?
这里的问题,主要是指图中要素过多,当前分辨率 (1024x1024) 装不下。
装不下怎么办呢?

看了下之前老的图集,是直接通过对图集进行部分缩放解决。

在此处自动化操作时,TexturePacker 打出来图集若出现此种情况,将会返回 10,若成功,则返回 0。

因此这里就可以针对 TexturePacker 进程结束的返回值进行额外判断,若返回非 0 ,则减少 Scale 参数并重试。

# 布阵模型动画创建

布阵相关处理,每次都是创建新的动画文件,并添加至 AnimatorController 引用。
那些 Copy 原图、打图集、改引用的代码就不说,都是常规操作。

主要创建动画相关代码如下:

private void CreateFormationAnimationClip(string fileName, string formationBlueTex)
{
    string shortName = fileName.Replace("_", "");
    string animPath = FileUtil.GetProjectRelativePath(Path.Combine(_troopformationPath, "an/" + shortName + ".anim"));
    string animControllerPath = FileUtil.GetProjectRelativePath(Path.Combine(_troopformationPath, "buzhen_ctr.controller"));
    if (File.Exists(animPath))
        File.Delete(animPath);
    // 创建动画片段
    AnimationClip clip = new AnimationClip();
    List<Sprite> sprites = LoadFormationSprite(formationBlueTex, "_" + fileName + "_idle_");
    EditorCurveBinding curveBinding = new EditorCurveBinding();
    curveBinding.type = typeof(UnityEngine.UI.Image);
    curveBinding.path = "";
    curveBinding.propertyName = "m_Sprite";
    ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[sprites.Count];
    for (int i = 0; i < keyframes.Length; i++)
    {
        keyframes[i] = new ObjectReferenceKeyframe();
        keyframes[i].time = i / 10f;
        keyframes[i].value = sprites[i];
    }
    clip.frameRate = 10;
    AnimationClipSettings clipSettings = AnimationUtility.GetAnimationClipSettings(clip);
    clipSettings.loopTime = true;
    AnimationUtility.SetAnimationClipSettings(clip, clipSettings);
    AnimationUtility.SetObjectReferenceCurve(clip, curveBinding, keyframes);
    AssetDatabase.CreateAsset(clip, animPath);
    // 添加至动画控制器
    AnimatorController animatorController = AssetDatabase.LoadAssetAtPath<AnimatorController>(animControllerPath);
    List<AnimatorState> stateRemoveTmp = new List<AnimatorState>();
    foreach (var item in animatorController.layers[0].stateMachine.states)
    {
        if (item.state.name == shortName) stateRemoveTmp.Add(item.state);
    }
    foreach (var item in stateRemoveTmp)
    {
        animatorController.layers[0].stateMachine.RemoveState(item);
    }
    AnimatorState state = animatorController.AddMotion(clip, 0);
    state.name = shortName;
    state.motion = clip;
    state.speed = 0.5f;
    //animatorController.layers[0].stateMachine.AddState(state, Vector3.up * animatorController.layers[0].stateMachine.states.Length * 50);
    //animatorController.layers[0].stateMachine.AddEntryTransition(state);
    AssetDatabase.SaveAssets();
    //AssetDatabase.Refresh();
}

最终生成效果如下:

图片

# 代码为 AnimatorController 添加动画的注意点

在使用编辑器对 AnimatorController 进行操作,添加新动画状态时,需要调用 AnimatorController 内置函数 AddMotion,直接传入 AnimationClip,该方法会自动创建一个 AnimatorState 并返回其示例:

AnimatorState state = animatorController.AddMotion(clip, 0);
state.name = shortName;
state.motion = clip;
state.speed = 0.5f;

该方法创建的动画状态才能被正确保存至文件。

不能使用 New 的方式去创建,如下:

AnimatorState state = new AnimatorState();
state.name = shortName;
state.motion = clip;
state.speed = 0.5f;
animatorController.layers[0].stateMachine.AddState(state, Vector3.up * animatorController.layers[0].stateMachine.states.Length * 50);

这种方式看起来确实也能添加,但是仅限于『当前』,因为它不会改动到 AnimatorController 文件 (至少经过个人尝试,Unity2017.4.28f1 中是如此),相当于在缓存中生成了一样,当重启 Unity 后,所做修改都会失效。

# 使用方式

# 最终界面

所有功能完成后,再加上发现 Bug 进行修复并添加一些子单位功能后,最终界面如下:

图片

  • 创建一个:选中一个目录,该目录下存在『红』、『蓝』文件夹,若同时存在『布阵』文件夹,则同时会创建布阵动画 (可通过勾选 不生成布阵模型 取消)。
  • 批量创建:选中一个目录,该目录下存在一系列子目录,子目录格式与上述 『创建一个』 格式一致,该选项会自动批量执行。
  • 创建一个 (布阵动画):选中一个目录,该目录下存在『布阵』文件夹,程序根据其内图片生成布阵动画。
  • 批量创建 (布阵动画):格式与 『批量创建』 一致,批量创建布阵动画
  • 重打图集 (布阵):直接重打图集,不会生成动画
  • 图集修复 (布阵):若最终图集出现错误 (如精灵分割错误),可以点击重新生成
  • 重定向动画引用:这是在打完新图集,生成新动画后发现老动画出现问题而增加的选项 —— 当时是由于图集使用图片进行额外优化了分辨率大小导致。该选项可以根据目录下的动画文件名字,重新去图集中获取对应图片生成新的动画及引用,一般没有使用需求。