1、 1 / 12 XNA动画的实现 4 保存动画数据 在上一篇文章中,我们实现了关键帧类实现了复杂的动画,这些复杂动画的数据都是在程序中手动生成的,为了能够重用这些数据,有必要实现下述功能:将动画数据保存为磁盘上的一个文件,使用时只要从 磁盘上读取这个文件构成动画数据类,就可以在内存中使用它了。 要做到这一点,只需使用 .NET框架的 文件 IO功能将动画数据文件串行化。 首先是关键帧数据类 Keyframe.cs,代码如下: / / 关键帧类,表示一个动画片段( AnimationClip)中一个指 定时刻的状态(包含位置、旋转、缩放信息), / 多个关键帧组成一个动画片段。 / public
2、 class Keyframe / / 位置 / public Vector3 Position get;set; / / 旋转 / public Vector3 Rotation get;set; / / 缩放 / public Vector3 Scale get;set; / / 此关键帧离开动画开始时刻的时间,单位为秒 / public double Time get;set; / / 创建一个关键帧 / public Keyframe(Vector3 Position, Vector3 Rotation, Vector3 Scale, double Time) this.Positio
3、n = Position; 2 / 12 this.Rotation = Rotation; this.Scale = Scale; this.Time = Time; / 必须要有一个不带参数的构造 函数用于串行化 private Keyframe() 这个代码与上一篇文章几乎是一样的,但有三点不同: 1 各个属性由原来的只读变成可写,这样做破坏了类的封装性,让用户可以设置属性值,从而导致错误。但是要让这个类可串行化,属性必须可写。 2 Time属性由原来的 TimeSpan类型变成了 double类型。这是因为 .NET框架无法串行化 TimeSpan 类型,导出为空值,所以只能换种类型。
4、下一篇文章的方法可以解决这个问题 。 3 必须要有一个不带参数的构造函数,否则串行化 过程会报错 。 然后是动画数据类 AnimationClip.cs,你需要将数据和方法分离,因此,相对于上一篇文章,这个类删除了方法,只保留了数据,变得简单多了。代码如下: / / 动画片段类,这个类保存一个关键帧集合和动画持续时间。 / public class AnimationClip / / 动画片段的播放长度 / public double Duration get; set; / / 关键帧集合 / public List Keyframes get; set; / / 创建一个动画片段 / /
5、关键帧集合 public AnimationClip(List Keyframes) this.Keyframes = Keyframes; /对关键帧根据时间先后进行排序 Keyframes.Sort(CompareKeyframeTimes); / 动画播放的时间就是最后一个关键帧的 Time属性 3 / 12 this.Duration = KeyframesKeyframes.Count - 1.Time; / / 创建一个动画片段 ,数据是从一个 xml文件加载的 / public AnimationClip(string fileName) / - / 下述代码应该还有改进空间 /
6、 - AnimationClip animationData; / 将 xml文件反串行化获取动画数据 using (XmlReader reader = XmlReader.Create(fileName) XmlSerializer serializer = new XmlSerializer(typeof(AnimationClip); animationData = (AnimationClip)serializer.Deserialize(reader); this.Keyframes = animationData.Keyframes; /对关键帧根据时间先后进行排序 Keyfra
7、mes.Sort(CompareKeyframeTimes); / 动画播放的时间就是最后一个关键帧的 Time属性 this.Duration = KeyframesKeyframes.Count - 1.Time; / 必须要有一个不带参数的构造函数用于串行化 private AnimationClip() / 按照关键帧的时间属性升序排序的 Comparison方法 int CompareKeyframeTimes(Keyframe a, Keyframe b) return a.Time.CompareTo(b.Time); 从代码中我们可以看出 ,首先我们在 AnimationCli
8、p 中添加了一个表示动画持续时间的Duration 属性,它实际上就是关键帧数组中最后一个的时间 Time 属性 , 将它提取 出来可以简化后面的操作 。 而且我们还编写了一个以动画数据名称为参数的构造函数,在这个构造函数中,我们使用了 IO功能从磁盘的动画数据 xml文件中读取了信息构建了 AnimationClip类。 而原来的 AnimationClip 类中的方法移至一个新的类中,我命名为 AnimationPlayer,它负责计算变换矩阵, 控制动画的播放。代码如下: 4 / 12 / / 控制动画播放的类,这个类负责计算变换矩阵,还可以调整动画播放的速度 / public clas
9、s AnimationPlayer private AnimationClip animationClip; / 动画片段 TimeSpan startTime, endTime; / 动画播放开始时刻和结束时刻 TimeSpan elapsedTime; / 动画当前已经播放的时间 int currentKeyframeIndex; / 动画当前播放的关键帧索引 bool loop; / 动画是否循环 float playbackRate = 1.0f; / 动画播放倍率, 1表示正常速度播放 LerpMode lerpMode; / 动画数据的插值模式 Vector3 position,
10、rotation, scale; / 当前时刻的位置、旋转和缩放 / / 动画是否已经播放完成 / public bool Done get; private set; / / 动画是否处于暂停状态 / public bool Paused get; set; / / 变换矩阵 / public Matrix Transform get; private set; / / 创建一个动画播放类 / / 要播放的动画片段 public AnimationPlayer(AnimationClip setAnimationClip) this.animationClip = setAnimationC
11、lip; / / 更新动画数据 5 / 12 / 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; / 进行插值操作 upda
12、teTransforms(); void updateTransforms() / 如果动画播放的时间已经超过指定的时间间隔 . while (elapsedTime = (endTime - startTime) / 如果是循环动画,则将动画播放时间回退到 0 if (loop) elapsedTime -= (endTime - startTime); currentKeyframeIndex = 0; / 否则,将播放时间进行截取 else Done = true; elapsedTime = endTime - startTime; break; / 读取 关键帧集合 IList ke
13、yframes = animationClip.Keyframes; / 首先获取离开整个动画开始时刻的时间,然后根据它获取当前关键帧的索引 / 最后获取从当前帧开始的动画播放时间,这个才能根据这个时间进行插值运算 double totalTime = (elapsedTime+ startTime).TotalSeconds; 6 / 12 while (keyframescurrentKeyframeIndex + 1.Time max) 7 / 12 value -= max; while (value / 播放完整的动画片段 / / 是否循环播放 / 变换插值模式 / 动画播放倍率 p
14、ublic void StartClip(bool loop,LerpMode lerpMode,float playbackRate) StartClip(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(animationClip.Duration), loop, lerpMode, playbackRate); / / 根据给定的关键帧索引播放两关键帧之间的动画片段 / / 开始时刻的关键帧索引 / 结束时刻的关键帧索引 / 是否循环播放 / 变换插值模式 / 动画播放倍率 public void StartClip(int startFrame
15、, int endFrame, bool loop,LerpMode lerpMode,float playbackRate) StartClip(TimeSpan.FromSeconds (animationClip.KeyframesstartFrame.Time), TimeSpan.FromSeconds (animationClip.KeyframesendFrame.Time), loop,lerpMode,playbackRate); / / 根据给定的开始时刻和结束时刻播放一段时间内的动画片段 / / 开始时刻 / 结束时刻 / 是否循环播放 / 变换插值模式 / 动画播放倍率
16、 8 / 12 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); currentKeyframe
17、Index = 0; / / 暂停播放 / public void PauseClip() Paused = true; / / 继续播放 / public void ResumeClip() Paused = false; 代码较长,但核心代码的原理与上一章是类似的。上一章只能播放完整的动画,而改进的代码可以根据输入的参数播放部分动画 。 动画数据文件的创建 现在的问题是:存储动画数据的 xml 文件从何而来?你可以在 XNA 代码中使用串行化将 AnimationClip类保存为 xml文件。 首先在 XNA主类中添加一个 AnimationClip变量,然后在 Initialize()方
18、法中使用第一个构造函数初始化这个类: protected override void Initialize() 9 / 12 List keyframes = new List(); / 以下这组关 键帧使物体绕一个三角形运动 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, MathH
19、elper.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 AnimationC
20、lip(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文件,看看它的内容: 10 / 12 9 -16 0 16 0 0 0 1 1 1 0 16 0 16 0 1.57079637 0 1 1 1 3