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

2006 - 2024,推荐分辨率1024*768以上,推荐浏览器Chrome、Edge等现代浏览器,截止2021年12月5日的访问次数:1872万9823 站长邮箱

沪ICP备18037240号-1

沪公网安备 31011002002865号