XNA动画的实现4——保存动画数据
在上一篇文章中,我们实现了关键帧类实现了复杂的动画,这些复杂动画的数据都是在程序中手动生成的,为了能够重用这些数据,有必要实现下述功能:将动画数据保存为磁盘上的一个文件,使用时只要从磁盘上读取这个文件构成动画数据类,就可以在内存中使用它了。
要做到这一点,只需使用.NET框架的文件IO功能将动画数据文件串行化。
首先是关键帧数据类Keyframe.cs,代码如下:
/// <summary> /// 关键帧类,表示一个动画片段(AnimationClip)中一个指定时刻的状态(包含位置、旋转、缩放信息), /// 多个关键帧组成一个动画片段。 /// </summary> public class Keyframe { /// <summary> /// 位置 /// </summary> public Vector3 Position{ get;set;} /// <summary> /// 旋转 /// </summary> public Vector3 Rotation{ get;set;} /// <summary> /// 缩放 /// </summary> public Vector3 Scale{ get;set;} /// <summary> /// 此关键帧离开动画开始时刻的时间,单位为秒 /// </summary> public double Time{ get;set;} /// <summary> /// 创建一个关键帧 /// </summary> public Keyframe(Vector3 Position, Vector3 Rotation, Vector3 Scale, double Time) { this.Position = Position; this.Rotation = Rotation; this.Scale = Scale; this.Time = Time; } // 必须要有一个不带参数的构造函数用于串行化 private Keyframe() { } }
这个代码与上一篇文章几乎是一样的,但有三点不同:
1. 各个属性由原来的只读变成可写,这样做破坏了类的封装性,让用户可以设置属性值,从而导致错误。但是要让这个类可串行化,属性必须可写。
2. Time属性由原来的TimeSpan类型变成了double类型。这是因为.NET框架无法串行化TimeSpan类型,导出为空值,所以只能换种类型。下一篇文章的方法可以解决这个问题
3. 必须要有一个不带参数的构造函数,否则串行化过程会报错。
然后是动画数据类AnimationClip.cs,你需要将数据和方法分离,因此,相对于上一篇文章,这个类删除了方法,只保留了数据,变得简单多了。代码如下:
/// <summary> /// 动画片段类,这个类保存一个关键帧集合和动画持续时间。 /// </summary> public class AnimationClip { /// <summary> /// 动画片段的播放长度 /// </summary> public double Duration { get; set; } /// <summary> /// 关键帧集合 /// </summary> public List<Keyframe> Keyframes { get; set; } /// <summary> /// 创建一个动画片段 /// </summary> /// <param name="Keyframes">关键帧集合</param> public AnimationClip(List<Keyframe> Keyframes) { this.Keyframes = Keyframes; //对关键帧根据时间先后进行排序 Keyframes.Sort(CompareKeyframeTimes); // 动画播放的时间就是最后一个关键帧的Time属性 this.Duration = Keyframes[Keyframes.Count - 1].Time; } /// <summary> /// 创建一个动画片段,数据是从一个xml文件加载的 /// </summary> public AnimationClip(string fileName) { // ---------------------------- // 下述代码应该还有改进空间 // --------------------------- AnimationClip animationData; // 将xml文件反串行化获取动画数据 using (XmlReader reader = XmlReader.Create(fileName)) { XmlSerializer serializer = new XmlSerializer(typeof(AnimationClip)); animationData = (AnimationClip)serializer.Deserialize(reader); } this.Keyframes = animationData.Keyframes; //对关键帧根据时间先后进行排序 Keyframes.Sort(CompareKeyframeTimes); // 动画播放的时间就是最后一个关键帧的Time属性 this.Duration = Keyframes[Keyframes.Count - 1].Time; } // 必须要有一个不带参数的构造函数用于串行化 private AnimationClip(){} // 按照关键帧的时间属性升序排序的Comparison方法 int CompareKeyframeTimes(Keyframe a, Keyframe b) { return a.Time.CompareTo(b.Time); } }
从代码中我们可以看出,首先我们在AnimationClip中添加了一个表示动画持续时间的Duration属性,它实际上就是关键帧数组中最后一个的时间Time属性,但将它提取出来可以简化后面的操作。 而且我们还编写了一个以动画数据名称为参数的构造函数,在这个构造函数中,我们使用了IO功能从磁盘的动画数据xml文件中读取了信息构建了AnimationClip类。 而原来的AnimationClip类中的方法移至一个新的类中,我命名为AnimationPlayer,它负责计算变换矩阵,控制动画的播放。代码如下:
/// <summary> /// 控制动画播放的类,这个类负责计算变换矩阵,还可以调整动画播放的速度 /// </summary> public class AnimationPlayer { private AnimationClip animationClip; // 动画片段 TimeSpan startTime, endTime; // 动画播放开始时刻和结束时刻 TimeSpan elapsedTime; // 动画当前已经播放的时间 int currentKeyframeIndex; // 动画当前播放的关键帧索引 bool loop; // 动画是否循环 float playbackRate = 1.0f; // 动画播放倍率,1表示正常速度播放 LerpMode lerpMode; // 动画数据的插值模式 Vector3 position, rotation, scale; // 当前时刻的位置、旋转和缩放 /// <summary> /// 动画是否已经播放完成 /// </summary> public bool Done { get; private set; } /// <summary> /// 动画是否处于暂停状态 /// </summary> public bool Paused { get; set; } /// <summary> /// 变换矩阵 /// </summary> public Matrix Transform { get; private set; } /// <summary> /// 创建一个动画播放类 /// </summary> /// <param name="setAnimationClip">要播放的动画片段</param> public AnimationPlayer(AnimationClip setAnimationClip) { this.animationClip = setAnimationClip; } /// <summary> /// 更新动画数据 /// </summary> public void Update(GameTime gameTime) { if (animationClip == null || Done) return; if (Paused) return; TimeSpan time = gameTime.ElapsedGameTime; // 调整动画播放速度 if (playbackRate != 1.0f) time = TimeSpan.FromSeconds(time.TotalSeconds * playbackRate); elapsedTime += time; // 进行插值操作 updateTransforms(); } void updateTransforms() { // 如果动画播放的时间已经超过指定的时间间隔... while (elapsedTime >= (endTime - startTime)) { // 如果是循环动画,则将动画播放时间回退到0 if (loop) { elapsedTime -= (endTime - startTime); currentKeyframeIndex = 0; } // 否则,将播放时间进行截取 else { Done = true; elapsedTime = endTime - startTime; break; } } // 读取关键帧集合 IList<Keyframe> keyframes = animationClip.Keyframes; // 首先获取离开整个动画开始时刻的时间,然后根据它获取当前关键帧的索引 // 最后获取从当前帧开始的动画播放时间,这个才能根据这个时间进行插值运算 double totalTime = (elapsedTime+ startTime).TotalSeconds; while (keyframes[currentKeyframeIndex + 1].Time < totalTime ) currentKeyframeIndex++; totalTime -= keyframes[currentKeyframeIndex].Time; // 获取播放到当前关键帧至下一个关键帧之间的位置,介于0和1之间 float amt = (float)((totalTime) / (keyframes[currentKeyframeIndex + 1].Time - keyframes[currentKeyframeIndex].Time)); // 根据插值模式对位置进行插值运算 if (lerpMode == LerpMode.Linear) position = Vector3.Lerp(keyframes[currentKeyframeIndex].Position, keyframes[currentKeyframeIndex + 1].Position, amt); else position = catmullRom3D( keyframes[wrap(currentKeyframeIndex - 1, keyframes.Count - 1)].Position, keyframes[wrap(currentKeyframeIndex, keyframes.Count - 1)].Position, keyframes[wrap(currentKeyframeIndex + 1, keyframes.Count - 1)].Position, keyframes[wrap(currentKeyframeIndex + 2, keyframes.Count - 1)].Position, amt); // 曲线运动中的旋转和缩放仍然是线性插值 rotation = Vector3.Lerp(keyframes[currentKeyframeIndex].Rotation, keyframes[currentKeyframeIndex + 1].Rotation, amt); scale = Vector3.Lerp(keyframes[currentKeyframeIndex].Scale, keyframes[currentKeyframeIndex + 1].Scale, amt); // 计算变换矩阵 Transform = Matrix.CreateScale(scale) * Matrix.CreateFromYawPitchRoll(rotation.Y, rotation.X, rotation.Z) * Matrix.CreateTranslation(position); } Vector3 catmullRom3D(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float amt) { return new Vector3( MathHelper.CatmullRom(v1.X, v2.X, v3.X, v4.X, amt), MathHelper.CatmullRom(v1.Y, v2.Y, v3.Y, v4.Y, amt), MathHelper.CatmullRom(v1.Z, v2.Z, v3.Z, v4.Z, amt)); } // 辅助方法,将输入的"value"参数在[0, max]范围内循环 int wrap(int value, int max) { while (value > max) value -= max; while (value < 0) value += max; return value; } /// <summary> /// 播放完整的动画片段 /// </summary> /// <param name="loop">是否循环播放</param> /// <param name="lerpMode">变换插值模式</param> /// <param name="playbackRate">动画播放倍率</param> public void StartClip(bool loop,LerpMode lerpMode,float playbackRate) { StartClip(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(animationClip.Duration), loop, lerpMode, playbackRate); } /// <summary> /// 根据给定的关键帧索引播放两关键帧之间的动画片段 /// </summary> /// <param name="startFrame">开始时刻的关键帧索引</param> /// <param name="endFrame">结束时刻的关键帧索引</param> /// <param name="loop">是否循环播放</param> /// <param name="lerpMode">变换插值模式</param> /// <param name="playbackRate">动画播放倍率</param> public void StartClip(int startFrame, int endFrame, bool loop,LerpMode lerpMode,float playbackRate) { StartClip(TimeSpan.FromSeconds (animationClip.Keyframes[startFrame].Time), TimeSpan.FromSeconds (animationClip.Keyframes[endFrame].Time), loop,lerpMode,playbackRate); } /// <summary> /// 根据给定的开始时刻和结束时刻播放一段时间内的动画片段 /// </summary> /// <param name="StartTime">开始时刻</param> /// <param name="EndTime">结束时刻</param> /// <param name="loop">是否循环播放</param> /// <param name="lerpMode">变换插值模式</param> /// <param name="playbackRate">动画播放倍率</param> public void StartClip(TimeSpan StartTime, TimeSpan EndTime, bool loop, LerpMode lerpMode,float playbackRate) { this.startTime = StartTime; this.endTime = EndTime; this.loop = loop; this.lerpMode = lerpMode; this.playbackRate =playbackRate; elapsedTime = TimeSpan.FromSeconds(0); currentKeyframeIndex = 0; } /// <summary> /// 暂停播放 /// </summary> public void PauseClip() { Paused = true; } /// <summary> /// 继续播放 /// </summary> public void ResumeClip() { Paused = false; } }
代码较长,但核心代码的原理与上一章是类似的。上一章只能播放完整的动画,而改进的代码可以根据输入的参数播放部分动画。
动画数据文件的创建
现在的问题是:存储动画数据的xml文件从何而来?你可以在XNA代码中使用串行化将AnimationClip类保存为xml文件。 首先在XNA主类中添加一个AnimationClip变量,然后在Initialize()方法中使用第一个构造函数初始化这个类:
protected override void Initialize() { […] List<Keyframe> keyframes = new List<Keyframe>(); // 以下这组关键帧使物体绕一个三角形运动 keyframes.Add(new Keyframe(new Vector3(-16, 0, 16), new Vector3(0, MathHelper.ToRadians(0), 0), * Vector3.One, 0)); keyframes.Add(new Keyframe(new Vector3(16, 0, 16), new Vector3(0, MathHelper.ToRadians(90), 0), * Vector3.One, 3)); keyframes.Add(new Keyframe(new Vector3(0, 0, -16), new Vector3(0, MathHelper.ToRadians(180), 0), * Vector3.One,6)); keyframes.Add(new Keyframe(new Vector3(-16, 0, 16), new Vector3(0, MathHelper.ToRadians(0), 0), * ector3.One, 9)); animation = new AnimationClip(keyframes); base.Initialize(); }
然后在Update方法中编写如下代码:
protected override void Update(GameTime gameTime) { […] // 按下数字键1将动画数据串行化为一个xml文件 if (IsNewKeyPress(Keys.D1)) { // 设置将xml元素缩进,便于阅读 XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; using (XmlWriter writer = XmlWriter.Create(@"E:/AnimationData.xml", settings)) { XmlSerializer serializer = new XmlSerializer(typeof(AnimationClip)); serializer.Serialize(writer, animation); } } base.Update(gameTime); }
运行XNA程序,然后按下数字键1,你就会在E盘根目录下找到一个名为AnimationData的xml文件,看看它的内容:
<?xml version="1.0" encoding="utf-8" ?> <animationclip xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Duration>9</Duration> <Keyframes> <Keyframe> <Position> <X>-16</X> <Y>0</Y> <Z>16</Z> </Position> <Rotation> <X>0</X> <Y>0</Y> <Z>0</Z> </Rotation> <Scale> <X>1</X> <Y>1</Y> <Z>1</Z> </Scale> <Time>0</Time> </Keyframe> <Keyframe> <Position> <X>16</X> <Y>0</Y> <Z>16</Z> </Position> <Rotation> <X>0</X> <Y>1.57079637</Y> <Z>0</Z> </Rotation> <Scale> <X>1</X> <Y>1</Y> <Z>1</Z> </Scale> <Time>3</Time> </Keyframe> <Keyframe> <Position> <X>0</X> <Y>0</Y> <Z>-16</Z> </Position> <Rotation> <X>0</X> <Y>3.14159274</Y> <Z>0</Z> </Rotation> <Scale> <X>1</X> <Y>1</Y> <Z>1</Z> </Scale> <Time>6</Time> </Keyframe> <Keyframe> <Position> <X>-16</X> <Y>0</Y> <Z>16</Z> </Position> <Rotation> <X>0</X> <Y>0</Y> <Z>0</Z> </Rotation> <Scale> <X>1</X> <Y>1</Y> <Z>1</Z> </Scale> <Time>9</Time> </Keyframe> </Keyframes> </animationclip>
至此,动画数据已经保存到磁盘了。在后面的应用中,你已经无需上述XNA代码,现在可以将它们删除了。
在XNA中播放动画
有了动画数据类,你就可以播放动画了。
首先将刚才创建的AnimationData.xml文件导入到XNA程序。然后在XNA主类中添加AnimationClip和AnimationPlayer变量,在Initialize()方法中进行初始化,我们使用的是AnimationClip类的第二个构造函数,别忘了调用AnimationPlayer类的StartClip方法开始播放动画:
protected override void Initialize() { […] animation = new AnimationClip("AnimationData.xml"); animationPlayer = new AnimationPlayer(animation); // 播放1秒至5秒间的动画 animationPlayer.StartClip(TimeSpan.FromSeconds (1), TimeSpan.FromSeconds (5), LerpMode.Linear ,1.0f); // 播放第2帧至第4帧的动画 //animationPlayer.StartClip(1, 3, false, LerpMode.Linear, 1.0f); // 播放完整动画 //animationPlayer.StartClip(false, LerpMode.Linear, 1.0f); // 以三倍速度循环播放完整动画 //animationPlayer.StartClip(true, LerpMode.Linear, 3f); base.Initialize(); }
在Update中实现了按数字键2可以暂停\播放动画,代码不在文中列出了,请看源代码。
程序截图如下:
这种方法最大的缺点在于此代码只适用于PC平台,在Xbox、Phone和Zune平台上不能正常工作,要能够跨平台使用动画数据类,需要通过内容管道加载动画数据,我们将在下一篇文章中加以讨论。
文件下载(已下载 1198 次)发布时间:2011/9/2 上午7:23:57 阅读次数:6634