最近新加入了一个项目,特效是使用序列帧图片实现的。既然如此,我就准备写点代码根据导出的序列帧图片,网上搜索了一下,发现了雨松MOMO大神的自动生成AnimationClip的文章:
Unity2D研究院之自动生成动画、AnimationController、Prefab(一)
只不过这篇文章已经是两年前的版本了,里面的代码在Unity5里有很多已经不可用了。这篇文章的评论区有朋友写出了一些Unity5需要改动的地方,需要的朋友可以自己翻一下,文章底部也会给出我的完整代码。
这里记录下我遇到的几个问题及解决方法:
原因:TexturePacker导出图集时有进行透明度裁剪,又没有保留原图的位置信息
解决方案:在TexturePacker设置中将TrimMode设置为Crop, keep position而非Crop, flush
position
原因:Image里面是使用RectTransform的sizeDelta属性,也就是Inspector面板里的width及height来控制宽高的。锚点则是使用RectTransform的pivot属性。这两个值都要单独设置,且不能有曲线动画。
解决方案:生成AnimationClip时,计算好每张图片的sizeDeltaX\sizeDeltaY\pivotX\pivotY,记录到动画片段里,且设置动画每帧的tangentMode为Constant即可。嗯,好像是解决了问题的样子,但是怎么动画还在抖???这就要看问题3了。
这个问题就要慢慢说一下了,百度和Google都搜索不到相关的文章(也可能是我搜索的思路不对。。。毕竟英文不是很熟。。。)自己来查又几乎一点头绪都没有。
先上个图给大家看一下:
可以看到在我导出的.anim文件里,已经设置好了每帧应该显示的sprite图片,注意下Element5里设置的是名为yaoshi10006的图。但是我在Animation动画编辑器里看到的却是这样:
奇了个怪。。我之前一直以为是我导出.anim动画文件时每帧的sprite写入的有问题,这才发现原来不是。然后我想在Animation编辑器里去更改这一帧对应的图片,但无论我改成什么图片都会被还原回去,无法保存。后来一通乱点之后才发现,我可以删除这第3帧的关键帧,重新增加这一帧的关键帧后,就可以设置图片,也可以保存了,动画也就正常了。。。
经过这一通乱点之后,就确认了一定是.anim动画文件的问题。但是如我上图所示,sprite图片设置是对的,那又是哪里有问题呢?我将代码自动生成的.anim额外保存了一份,然后与我修改好的这一份.anim文件用对比工具进行文字对比,发现了一件有意思的事情:
可以看到,原自动生成的动画文件第5帧记录的time是0.20833334,而修改后的动画文件第5帧的time是0.20833333,就是因为这么一点差异,导致了第5帧的图片没有产生变化,依然使用了第4帧的图片。既然知道了问题出在哪里,就容易解决了不是,回头看下生成AnimationClip时这个time是如何设置的:
float frameTime = 1/clip.frameRate;
for (int i = 0; i < sprites.Length; i++)
{
Sprite sprite = sprites[i];
keyFrames[i] = new ObjectReferenceKeyframe();
keyFrames[i].time = frameTime * i;
keyFrames[i].value = sprite;
}
一看time是一个float对象,难道会是由于精度问题所引起的?赶紧将代码进行了如下改动:
double frameTime = 1d / 24d;
for (int i = 0; i < sprites.Length; i++)
{
Sprite sprite = sprites[i];
keyFrames[i] = new ObjectReferenceKeyframe();
keyFrames[i].time = (float)(frameTime * i);
keyFrames[i].value = sprite;
}
到此,终于修复了动画中sprite图片错乱的问题。总算是有一个解决方法了吧,也不知道是否有更加合适的解决方案,如果有欢迎跟我说。。。下面是完整的自动生成动画文件的代码,其中的大部分思路均引用自雨松MOMO大神的文章。
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine.UI;
///
/// 根据sprites图集自动生成序列帧动画
///
public class BuildAnimationEditor : Editor
{
[MenuItem("Assets/BuildSpriteRenderAnimation")]
public static void BuildSpriteRenderAnimation()
{
BuildAnimation(true, typeof(SpriteRenderer));
}
[MenuItem("Assets/BuildImageAnimation")]
public static void BuildImageAnimation()
{
BuildAnimation(true, typeof(Image));
}
public static void BuildAnimation(bool isLoop, System.Type curveBindingType)
{
UnityEngine.Object[] arr = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.TopLevel);
foreach (Object obj in arr)
{
string path = AssetDatabase.GetAssetPath(obj);
Sprite[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(path)
.Select(x => x as Sprite).Where(x => x != null).ToArray();
if (sprites == null || sprites.Length == 0)
continue;
string directoryName = Path.GetDirectoryName(path);
string animationName = Path.GetFileNameWithoutExtension(path);
if (curveBindingType == typeof (Image))
animationName = "UI-" + animationName;
//这里是否循环的参数我写死了为true,可以根据自己的需要进行修改.
AnimationClip clip = BuildAnimationClip(directoryName, animationName, sprites, true, curveBindingType);
List clips = new List();
clips.Add(clip);
AnimatorController controller = BuildAnimationController(clips, directoryName, animationName);
BuildPrefab(directoryName, animationName, controller, sprites[0], curveBindingType);
}
}
static AnimationClip BuildAnimationClip(string directoryPath, string animationName, Sprite[] sprites, bool isLoop, System.Type curveBindingType)
{
AnimationClip clip = new AnimationClip();
#if !UNITY_5
AnimationUtility.SetAnimationType(clip, ModelImporterAnimationType.Generic);
#endif
EditorCurveBinding curveBinding = new EditorCurveBinding();
curveBinding.type = curveBindingType;
curveBinding.path = "";
curveBinding.propertyName = "m_Sprite";
ObjectReferenceKeyframe[] keyFrames = new ObjectReferenceKeyframe[sprites.Length];
//动画帧率,这里统一用24帧
clip.frameRate = 24f;
//这里的keyFrames[i].time是float类型,而float的精度会影响到sprite的切换,解决方法就是使用double类型计算time。。。。
//动画长度是按秒为单位,1/10就表示1秒切10张图片,根据项目的情况可以自己调节
double frameTime = 1d / 24d;
for (int i = 0; i < sprites.Length; i++)
{
Sprite sprite = sprites[i];
keyFrames[i] = new ObjectReferenceKeyframe();
keyFrames[i].time = (float)(frameTime * i);
keyFrames[i].value = sprite;
}
if (curveBindingType == typeof(Image))
{
//如果是Image动画,要额外设置宽高、pivot等属性
BuildImageAnimationClip(clip, sprites, frameTime);
}
//有些动画我希望天生它就动画循环
if (isLoop)
{
SerializedObject serializedClip = new SerializedObject(clip);
AnimationClipSettings clipSettings = new AnimationClipSettings(serializedClip.FindProperty("m_AnimationClipSettings"));
clipSettings.loopTime = true;
serializedClip.ApplyModifiedProperties();
}
AnimationUtility.SetObjectReferenceCurve(clip, curveBinding, keyFrames);
AssetDatabase.CreateAsset(clip, directoryPath + "/" + animationName + ".anim");
AssetDatabase.SaveAssets();
return clip;
}
private static void BuildImageAnimationClip(AnimationClip clip, Sprite[] sprites, double frameTime)
{
EditorCurveBinding sizeDelta_x_curveBinding = new EditorCurveBinding();
sizeDelta_x_curveBinding.type = typeof(RectTransform);
sizeDelta_x_curveBinding.path = "";
sizeDelta_x_curveBinding.propertyName = "m_SizeDelta.x";
EditorCurveBinding sizeDelta_y_curveBinding = new EditorCurveBinding();
sizeDelta_y_curveBinding.type = typeof(RectTransform);
sizeDelta_y_curveBinding.path = "";
sizeDelta_y_curveBinding.propertyName = "m_SizeDelta.y";
EditorCurveBinding pivot_x_curveBinding = new EditorCurveBinding();
pivot_x_curveBinding.type = typeof(RectTransform);
pivot_x_curveBinding.path = "";
pivot_x_curveBinding.propertyName = "m_Pivot.x";
EditorCurveBinding pivot_y_curveBinding = new EditorCurveBinding();
pivot_y_curveBinding.type = typeof(RectTransform);
pivot_y_curveBinding.path = "";
pivot_y_curveBinding.propertyName = "m_Pivot.y";
Keyframe[] sizeDelta_x_keyframes = new Keyframe[sprites.Length];
Keyframe[] sizeDelta_y_keyframes = new Keyframe[sprites.Length];
Keyframe[] pivot_x_keyframes = new Keyframe[sprites.Length];
Keyframe[] pivot_y_keyframes = new Keyframe[sprites.Length];
for (int i = 0; i < sprites.Length; i++)
{
sizeDelta_x_keyframes[i] = new Keyframe((float)(frameTime * i), sprites[i].rect.width);
sizeDelta_y_keyframes[i] = new Keyframe((float)(frameTime * i), sprites[i].rect.height);
float x_pivot = sprites[i].pivot.x / sprites[i].rect.width;
float y_pivot = sprites[i].pivot.y / sprites[i].rect.height;
pivot_x_keyframes[i] = new Keyframe((float)(frameTime * i), x_pivot);
pivot_y_keyframes[i] = new Keyframe((float)(frameTime * i), y_pivot);
}
AnimationCurve sizeDelta_x_curve = new AnimationCurve(sizeDelta_x_keyframes);
AnimationCurve sizeDelta_y_curve = new AnimationCurve(sizeDelta_y_keyframes);
AnimationCurve pivot_x_curve = new AnimationCurve(pivot_x_keyframes);
AnimationCurve pivot_y_curve = new AnimationCurve(pivot_y_keyframes);
for (int i = 0; i < sprites.Length; i++)
{
AnimationUtility.SetKeyLeftTangentMode(sizeDelta_x_curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(sizeDelta_x_curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyLeftTangentMode(sizeDelta_y_curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(sizeDelta_y_curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyLeftTangentMode(pivot_x_curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(pivot_x_curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyLeftTangentMode(pivot_y_curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(pivot_y_curve, i, AnimationUtility.TangentMode.Constant);
}
AnimationUtility.SetEditorCurve(clip, sizeDelta_x_curveBinding, sizeDelta_x_curve);
AnimationUtility.SetEditorCurve(clip, sizeDelta_y_curveBinding, sizeDelta_y_curve);
AnimationUtility.SetEditorCurve(clip, pivot_x_curveBinding, pivot_x_curve);
AnimationUtility.SetEditorCurve(clip, pivot_y_curveBinding, pivot_y_curve);
}
static AnimatorController BuildAnimationController(List clips, string directoryName, string name)
{
AnimatorController animatorController = AnimatorController.CreateAnimatorControllerAtPath(directoryName + "/" + name + ".controller");
AnimatorControllerLayer layer = animatorController.layers[0];
AnimatorStateMachine asm = layer.stateMachine;
foreach (AnimationClip newClip in clips)
{
AnimatorState state = asm.AddState(newClip.name);
state.motion = newClip;
AnimatorStateTransition trans = asm.AddAnyStateTransition(state);
trans.hasExitTime = false;
}
AssetDatabase.SaveAssets();
return animatorController;
}
static void BuildPrefab(string directoryName, string goName, AnimatorController animatorCountorller, Sprite defaultSprite, System.Type curveBindingType)
{
//生成Prefab 添加一张预览用的Sprite
GameObject go = new GameObject(goName);
if (curveBindingType == typeof (SpriteRenderer))
{
SpriteRenderer spriteRender = go.AddComponent();
spriteRender.sprite = defaultSprite;
spriteRender.sortingLayerName = "Effect";
}
else if (curveBindingType == typeof (Image))
{
Image image = go.AddComponent();
image.sprite = defaultSprite;
image.rectTransform.sizeDelta = new Vector2(defaultSprite.rect.width, defaultSprite.rect.height);
float x_pivot = defaultSprite.pivot.x / defaultSprite.rect.width;
float y_pivot = defaultSprite.pivot.y / defaultSprite.rect.height;
image.rectTransform.pivot = new Vector2(x_pivot, y_pivot);
}
Animator animator = go.AddComponent();
animator.runtimeAnimatorController = animatorCountorller;
animator.cullingMode = AnimatorCullingMode.CullCompletely;
PrefabUtility.CreatePrefab(directoryName + "/" + goName + ".prefab", go);
DestroyImmediate(go);
}
class AnimationClipSettings
{
SerializedProperty m_Property;
private SerializedProperty Get(string property) { return m_Property.FindPropertyRelative(property); }
public AnimationClipSettings(SerializedProperty prop) { m_Property = prop; }
public float startTime { get { return Get("m_StartTime").floatValue; } set { Get("m_StartTime").floatValue = value; } }
public float stopTime { get { return Get("m_StopTime").floatValue; } set { Get("m_StopTime").floatValue = value; } }
public float orientationOffsetY { get { return Get("m_OrientationOffsetY").floatValue; } set { Get("m_OrientationOffsetY").floatValue = value; } }
public float level { get { return Get("m_Level").floatValue; } set { Get("m_Level").floatValue = value; } }
public float cycleOffset { get { return Get("m_CycleOffset").floatValue; } set { Get("m_CycleOffset").floatValue = value; } }
public bool loopTime { get { return Get("m_LoopTime").boolValue; } set { Get("m_LoopTime").boolValue = value; } }
public bool loopBlend { get { return Get("m_LoopBlend").boolValue; } set { Get("m_LoopBlend").boolValue = value; } }
public bool loopBlendOrientation { get { return Get("m_LoopBlendOrientation").boolValue; } set { Get("m_LoopBlendOrientation").boolValue = value; } }
public bool loopBlendPositionY { get { return Get("m_LoopBlendPositionY").boolValue; } set { Get("m_LoopBlendPositionY").boolValue = value; } }
public bool loopBlendPositionXZ { get { return Get("m_LoopBlendPositionXZ").boolValue; } set { Get("m_LoopBlendPositionXZ").boolValue = value; } }
public bool keepOriginalOrientation { get { return Get("m_KeepOriginalOrientation").boolValue; } set { Get("m_KeepOriginalOrientation").boolValue = value; } }
public bool keepOriginalPositionY { get { return Get("m_KeepOriginalPositionY").boolValue; } set { Get("m_KeepOriginalPositionY").boolValue = value; } }
public bool keepOriginalPositionXZ { get { return Get("m_KeepOriginalPositionXZ").boolValue; } set { Get("m_KeepOriginalPositionXZ").boolValue = value; } }
public bool heightFromFeet { get { return Get("m_HeightFromFeet").boolValue; } set { Get("m_HeightFromFeet").boolValue = value; } }
public bool mirror { get { return Get("m_Mirror").boolValue; } set { Get("m_Mirror").boolValue = value; } }
}
}