这几天给项目做了一个自动创建新兵种的工具,只需要提供符合规则格式的图片,即可一键自动生成新兵种战斗模型、布阵模型动画。
# 前言
工具:
- 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 进行修复并添加一些子单位功能后,最终界面如下:
- 创建一个:选中一个目录,该目录下存在『红』、『蓝』文件夹,若同时存在『布阵』文件夹,则同时会创建布阵动画 (可通过勾选 不生成布阵模型 取消)。
- 批量创建:选中一个目录,该目录下存在一系列子目录,子目录格式与上述 『创建一个』 格式一致,该选项会自动批量执行。
- 创建一个 (布阵动画):选中一个目录,该目录下存在『布阵』文件夹,程序根据其内图片生成布阵动画。
- 批量创建 (布阵动画):格式与 『批量创建』 一致,批量创建布阵动画
- 重打图集 (布阵):直接重打图集,不会生成动画
- 图集修复 (布阵):若最终图集出现错误 (如精灵分割错误),可以点击重新生成
- 重定向动画引用:这是在打完新图集,生成新动画后发现老动画出现问题而增加的选项 —— 当时是由于图集使用图片进行额外优化了分辨率大小导致。该选项可以根据目录下的动画文件名字,重新去图集中获取对应图片生成新的动画及引用,一般没有使用需求。