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 阅读次数:7148
