XNA动画的实现9——RobotGame动画的实现
概览
RobotGame是XNA官网一个大型的综合性示例,这个示例几乎涉及到一个完整游戏的方方面面,非常有参考价值,不过XNA 2.0之后就停止了更新,要在XNA 4.0平台上正常运行,你需要修改比较多的代码才行。
简而言之,RobotGame的场景管理使用的就是XNA官网的SceneManager示例中的代码,用SceneGraph管理各个SceneNode,FontManager.cs管理字体,TextManager管理文字显示,InputComponentManager管理输入,SoundManager.cs管理声音,ResourceManager.cs封装类框架的ContentManager用于管理资源,还实现了AI、碰撞检测等功能。场景信息、玩家信息、敌人信息、粒子系统都实现了脚本化,从xml文件加载。
本篇文章专注于此游戏动画的实现。
注意:此系列文章的8暂时跳过,原定计划是讲解动画混合,但有些细节问题没有想清楚,有可能要成坑了。
动画数据类
RobotGame的动画数据类都是放在引擎项目RobotGameData中Object→Animation目录中的。 其中最小的动画单元关键帧Keyframe放置于KeyFrameSequence.cs中,它使用一个结构体而不是类来定义的,代码如下:
public struct KeyFrame { public bool HasTranslation; public bool HasRotation; public bool HasScale; public Vector3 Translation; public Quaternion Rotation; public Vector3 Scale; }
然后多个关键帧组成一个关键帧序列KeyFrameSequence,代码如下:
/// <summary> /// KeyFrameSequence是动画的基本单位,它包含一个bone的关键帧。 /// 在两个关键帧之间进行插值运算。 /// </summary> [Serializable] public class KeyFrameSequence { #region 字段 public String BoneName = String.Empty; /// <summary> /// 关键帧数量 /// </summary> public int KeyCount = 0; /// <summary> /// 动画播放时间 /// </summary> public float Duration = 0.0f; /// <summary> /// 每个关键帧之间的时间间隔 /// </summary> public float KeyInterval = 0.0f; public bool HasTranslation = false; public bool HasRotation = false; public bool HasScale = false; public bool HasTime = false; /// <summary> /// 如果这个标志物设置为true,整个动画的关键帧的translation值都会和第一个关键帧相同。 /// </summary> public bool FixedTranslation = false; /// <summary> /// 如果这个标志物设置为true,整个动画的关键帧的rotation值都会和第一个关键帧相同。 /// </summary> public bool FixedRotation = false; /// <summary> /// 如果这个标志物设置为true,整个动画的关键帧的scale值都会和第一个关键帧相同。 /// </summary> public bool FixedScale = false; /// <summary> /// 集合中的translation值 /// </summary> public List<Vector3> Translation = null; /// <summary> /// 集合中的旋转值 /// </summary> public List<Quaternion> Rotation = null; /// <summary> /// 集合中的scale值 /// </summary> public List<Vector3> Scale = null; /// <summary> /// 集合中的time值 /// </summary> public List<float> Time = null; #endregion /// <summary> /// 根据关键帧索引值获取关键帧的变换矩阵 /// </summary> /// <param name="keyIndex">关键帧索引</param> public Matrix GetMatrix(int keyIndex) { Matrix mat = Matrix.Identity; if (HasRotation ) // 使用四元数计算旋转 { if (FixedRotation ) mat = Matrix.CreateFromQuaternion(Rotation[0]); else mat = Matrix.CreateFromQuaternion(Rotation[keyIndex]); } if (HasTranslation ) // 计算平移 { if (FixedRotation ) mat.Translation = Translation[0]; else mat.Translation = Translation[keyIndex]; } if (HasScale ) // 计算缩放 { if (FixedRotation ) mat = mat * Matrix.CreateScale(Scale[0]); else mat = mat * Matrix.CreateScale(Scale[keyIndex]); } return mat; } /// <summary> /// 根据动画播放时间和播放模式计算变换矩阵 /// </summary> /// <param name="localTime">动画播放时间</param> /// <param name="mode">播放模式</param> /// <returns>变换矩阵</returns> public Matrix GetInterpolateMatrix(float localTime, AnimPlayMode mode) { int index1 = 0; // 第一个关键帧索引 int index2 = 0; // 第二个关键帧索引 float interpolateTime = 0.0f; // 根据播放时间和播放模式计算关键帧索引和插值时间 CalculateKeyFrameIndex(localTime, mode, out index1, out index2, out interpolateTime); // 然后根据关键帧索引和插值时间计算这两个关键帧之间的插值变换矩阵 return GetInterpolateMatrix(index1, index2, interpolateTime); } /// <summary> /// 根据关键帧索引和插值时间计算两个关键帧之间的插值变换矩阵 /// </summary> /// <param name="keyIndex1">第一个关键帧索引</param> /// <param name="keyIndex2">第二个关键帧索引</param> /// <param name="t">插值时间(0.0至1.0之间)</param> /// <returns>经过插值的变换矩阵</returns> public Matrix GetInterpolateMatrix(int keyIndex1, int keyIndex2, float t) { Matrix mat = Matrix.Identity; // 使用四元数的Slerp方法对旋转进行插值 if (HasRotation ) { Quaternion q = Quaternion.Identity; if (FixedRotation ) q = Rotation[0]; else q = Quaternion.Slerp(Rotation[keyIndex1], Rotation[keyIndex2], t); mat = Matrix.CreateFromQuaternion(q); } // 使用Lerp方法对平移进行插值 if (HasTranslation ) { // 将经过插值的平移信息添加到矩阵 if (FixedTranslation ) mat.Translation = Translation[0]; else mat.Translation = Vector3.Lerp(Translation[keyIndex1], Translation[keyIndex2], t); } // 使用Lerp方法对缩放进行插值 if (HasScale ) { Vector3 v = Vector3.Zero; if (FixedScale ) v = Scale[0]; else v = Vector3.Lerp(Scale[keyIndex1], Scale[keyIndex2], t); mat = mat * Matrix.CreateScale(v); } return mat; } /// <summary> /// 根据动画播放时间和播放模式获取插值过的关键帧 /// </summary> /// <param name="localTime">动画播放时间</param> /// <param name="mode">播放模式</param> public KeyFrame GetInterpolateKeyFrame(float localTime, AnimPlayMode mode) { int index1 = 0; // 第一个关键帧索引 int index2 = 0; // 第二个关键帧索引 float interpolateTime = 0.0f; CalculateKeyFrameIndex(localTime, mode, out index1, out index2, out interpolateTime); // 根据参数interpolateTime计算第一个和第二个关键帧之间的插值关键帧 return GetInterpolateKeyFrame(index1, index2, interpolateTime); } /// <summary> /// 根据关键帧索引和插值时间计算两个关键帧之间的插值关键帧 /// </summary> /// <param name="keyIndex1">第一个关键帧索引</param> /// <param name="keyIndex2">第二个关键帧索引</param> /// <param name="t">插值时间(0.0至1.0之间)</param> /// <returns>经过插值的关键帧</returns> public KeyFrame GetInterpolateKeyFrame(int keyIndex1, int keyIndex2, float t) { KeyFrame keyFrame; // 使用四元数的Slerp方法对旋转进行插值 if (HasRotation ) { if (FixedRotation ) keyFrame.Rotation = Rotation[0]; else keyFrame.Rotation = Quaternion.Slerp( Rotation[keyIndex1], Rotation[keyIndex2], t); } else { keyFrame.Rotation = Quaternion.Identity; } keyFrame.HasRotation = HasRotation; // 使用Lerp方法对平移进行插值 if (HasTranslation ) { if (FixedTranslation ) keyFrame.Translation = Translation[0]; else keyFrame.Translation = Vector3.Lerp( Translation[keyIndex1], Translation[keyIndex2], t); } else { keyFrame.Translation = Vector3.Zero; } keyFrame.HasTranslation = HasTranslation; // 使用Lerp方法对缩放进行插值 if (HasScale ) { if (FixedScale ) keyFrame.Scale = Scale[0]; else keyFrame.Scale = Vector3.Lerp(Scale[keyIndex1], Scale[keyIndex2], t); } else { keyFrame.Scale = Vector3.One; } keyFrame.HasScale = HasScale; return keyFrame; } /// <summary> /// 根据动画播放时间和播放模式计算两个关键帧索引和插值时间(0和1之间) /// </summary> public void CalculateKeyFrameIndex(float localTime, AnimPlayMode mode, out int index1, out int index2, out float interpolateTime) { index1 = 0; // 第一个关键帧索引 index2 = 0; // 第二个关键帧索引 interpolateTime = 0.0f; // 计算第一个关键帧索引 if (HasTime) index1 = GetKeyFrameIndex(localTime); else index1 = (int)(localTime / KeyInterval); // 根据播放模式计算第二个关键帧索引 switch (mode) { case AnimPlayMode.Once: // 只播放一次 { // 如果index1已经是最后一个索引,则index2就是index1,否则index2=index1+1 index2 = (index1 >= KeyCount - 1 ? index1 : index1 + 1); } break; case AnimPlayMode.Repeat: // 循环播放 { // 如果index1是最后一个索引, 则index2=0 index2 = (index1 >= KeyCount - 1 ? 0 : index1 + 1); } break; default: throw new NotSupportedException("Not supported play mode"); } if (index1 >= KeyCount - 1) { index1 = index2 = KeyCount - 1; interpolateTime = 1.0f; } else { if (HasTime ) { interpolateTime = (localTime - Time[index1]) / (Time[index2] - Time[index1]); } else { interpolateTime = HelperMath.CalculateModulo(localTime, KeyInterval) / KeyInterval; } } } /// <summary> /// 根据动画播放时间返回包含这个时间的初始关键帧 /// </summary> public int GetKeyFrameIndex(float localTime) { int startIndex, endIndex, middleIndex; startIndex = 0; endIndex = KeyCount - 1; if (localTime >= Time[endIndex]) { return endIndex; } do { // 以下代码实际上是二分法快速搜索算法 middleIndex = (startIndex + endIndex) / 2; if ((endIndex - startIndex) <= 1) { break; } else if (Time[middleIndex] < localTime) { startIndex = middleIndex; } else if (Time[middleIndex] > localTime) { endIndex = middleIndex; } else { startIndex = middleIndex; break; } } while ((endIndex - startIndex) > 1); return startIndex; } }
然后是AnimationSequence类,它包含了一个关键帧序列KeyFrameSequence的集合,数据是从xml文件中加载的,代码如下:
/// <summary> /// 动画的基本单位,包含多个bone的关键帧数据,从一个XML(.Animation)文件中加载。 /// </summary> [Serializable] public class AnimationSequence { public int KeyFrameSequenceCount = 0; /// <summary> /// 动画播放时间 /// </summary> public float Duration = 0.0f; /// <summary> /// 动画片段KeyFrameSequence的集合 /// </summary> public List<KeyFrameSequence> KeyFrameSequences = null; /// <summary> /// 根据索引获取动画片段KeyFrameSequence /// </summary> public KeyFrameSequence GetKeyFrameSequence(int index) { return KeyFrameSequences[index]; } /// <summary> /// 根据索引获取动画片段KeyFrameSequence的bone名称 /// </summary> /// <param name="index">索引</param> /// <returns></returns> public string GetKeyFrameSequenceBoneName(int index) { return GetKeyFrameSequence(index).BoneName; } }
与前面文章的比较
前面文章中关键帧Keyframe是一个类,此处是一个结构,使用结构通常比类快一些,而且还添加了HasTranslation,HasRotation,HasScale三个变量,这样可以避免不必要的插值运算,但是使用结构带来的缺点是破坏了数据的封装性,不过问题不大。
KeyFrameSequence类和AnimationSequence类的组合类似于前面的AnimationClip类,KeyFrameSequence类保存的不是关键帧集合,而是关键帧中平移、缩放、旋转信息的集合,而且在这个类中实现了插值操作,而前文是在AnimationPlayer类中进行的。我认为前文中的将数据和操作分开处理的方法更加理想。
前面的文章中bone信息是用bone索引表示的,放在了Keyframe中,而RobotGame的bone信息是用bone名称表示的,比起使用索引速度会慢一点,而且bone信息是放在了KeyFrameSequence中,这样一个动画片段只能对应一个bone,这样的做法各有利弊,优点是动画结构比较清晰,而缺点是一个复杂的动画往往需要几个KeyFrameSequence混合而成。比如说,一个机器人的奔跑动画,它的手臂bone、大腿bone…都要变化,用前面文章的方法,可以将这些bone动画都放在一个AnimationClip中,而RobotGame中只能手臂bone创建一个KeyFrameSequence、大腿bone创建一个KeyFrameSequence…,然后将这些KeyFrameSequence集合放置在一个AnimationSequence类中,一个AnimationSequence表示一个动画(前面的文章中一个AnimationClip就可以表示一个动画)。
最后在后面提到的GameAnimatedModel类中使用List
内容管道
动画数据AnimationSequence是通过内容管道从后缀名为.Animation的xml文件中加载的,因为RobotGame开发版本是XNA2.0,因此它没有用到XNA4.0(XNA 3.1?)改进的串行化功能,而是使用.NET的IO进行数据的串行化。
具体代码在RobotGameProcessor库中,其中内容导入器和内容处理器代码放在AnimationProcessor.cs中,代码如下:
public class ImportContentData { AnimationSequence data = null; public AnimationSequence Data { get { return data; } } public ImportContentData() { this.data = new AnimationSequence(); } public ImportContentData(AnimationSequence data) { this.data = data; } } /// <summary> /// 处理AnimationSequence类的自定义内容管道导入器 /// </summary> [ContentImporter(".Animation", CacheImportedData = true, DefaultProcessor = "AnimationProcessor")] public class AnimationImporter : ContentImporter<ImportContentData> { public override ImportContentData Import(string filename, ContentImporterContext context) { Stream stream = File.OpenRead(Path.Combine("Content", filename)); XmlTextReader reader = new XmlTextReader(stream); XmlSerializer serializer = new XmlSerializer(typeof(AnimationSequence)); AnimationSequence data = (AnimationSequence)serializer.Deserialize(reader); return new ImportContentData(data); } } /// <summary> /// 处理AnimationSequence类的自定义内容管道处理器 /// </summary> [ContentProcessor] public class AnimationProcessor : ContentProcessor<ImportContentData, WriteContentData> { public override WriteContentData Process(ImportContentData input, ContentProcessorContext context) { return new WriteContentData(input.Data); } }
使用旧方法必须编写CotentTypeWriter和ContentTypeReader,否则XNA程序不知如何写入和读取自定义的AnimationSequence数据,而使用XNA4.0的新方法可以不写这两个类,它在绝大部分情况下会根据反射自动生成这些代码。
CotentTypeWriter的代码位于AnimationWriter.cs中:
public class WriteContentData { AnimationSequence data = null; public AnimationSequence Data { get { return data; } } public WriteContentData() { this.data = new AnimationSequence(); } public WriteContentData(AnimationSequence data) { this.data = data; } } /// <summary> /// 用于将AnimationSequence数据保存为XNA格式的Content Pipeline类 /// </summary> [ContentTypeWriter] public class AnimationWriter : ContentTypeWriter<WriteContentData> { ContentWriter output = null; protected override void Write(ContentWriter output, WriteContentData value) { this.output = output; WriteAnimationSequence(value.Data); } public override string GetRuntimeType(TargetPlatform targetPlatform) { return typeof(AnimationSequence).AssemblyQualifiedName; } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return "RobotGameData.GameObject.AnimationReader, " + "RobotGameData, Version=1.0.0.0, Culture=neutral"; } private void WriteAnimationSequence(AnimationSequence value) { output.Write(value.KeyFrameSequenceCount); output.Write(value.Duration); for (int i = 0; i < value.KeyFrameSequences.Count; i++) { WriteKeyFrameSequence(value.KeyFrameSequences[i]); } } private void WriteKeyFrameSequence(KeyFrameSequence value) { output.Write(value.BoneName); output.Write(value.KeyCount); output.Write(value.Duration); output.Write(value.KeyInterval); output.Write(value.HasTranslation); output.Write(value.HasRotation); output.Write(value.HasScale); output.Write(value.HasTime); output.Write(value.FixedTranslation); output.Write(value.FixedRotation); output.Write(value.FixedScale); // 写入位置数据 output.Write(value.Translation.Count); for (int i = 0; i < value.Translation.Count; i++) output.Write(value.Translation[i]); // 写入旋转数据 output.Write(value.Rotation.Count); for (int i = 0; i < value.Rotation.Count; i++) output.Write(value.Rotation[i]); // 写入缩放数据 output.Write(value.Scale.Count); for (int i = 0; i < value.Scale.Count; i++) output.Write(value.Scale[i]); // 写入时间数据 output.Write(value.Time.Count); for (int i = 0; i < value.Time.Count; i++) output.Write(value.Time[i]); } }
读取内容的ContentTypeReader不是放在Content项目中的,而是放在XNA项目中(如果你在PC平台上使用无所谓,但是在Xbox平台不能读取类库,必须写在XNA程序中),它位于RobotGameData项目中Object→Animation目录中,代码如下:
/// <summary> /// Content Pipeline类,用于加载XNB格式的AnimationSequence数据 /// </summary> public class AnimationReader : ContentTypeReader<AnimationSequence> { ContentReader input = null; protected override AnimationSequence Read(ContentReader input, AnimationSequence existingInstance) { this.input = input; return ReadAnimationSequence(); } private AnimationSequence ReadAnimationSequence() { AnimationSequence animationSequence = new AnimationSequence(); animationSequence.KeyFrameSequenceCount = input.ReadInt32(); animationSequence.Duration = input.ReadSingle(); if (animationSequence.KeyFrameSequenceCount > 0) { animationSequence.KeyFrameSequences = new List<KeyFrameSequence>(); for (int i = 0; i < animationSequence.KeyFrameSequenceCount; i++) animationSequence.KeyFrameSequences.Add(ReadKeyFrameSequence()); } return animationSequence; } private KeyFrameSequence ReadKeyFrameSequence() { KeyFrameSequence keyFrameSequence = new KeyFrameSequence(); keyFrameSequence.BoneName = input.ReadString(); keyFrameSequence.KeyCount = input.ReadInt32(); keyFrameSequence.Duration = input.ReadSingle(); keyFrameSequence.KeyInterval = input.ReadSingle(); keyFrameSequence.HasTranslation = input.ReadBoolean(); keyFrameSequence.HasRotation = input.ReadBoolean(); keyFrameSequence.HasScale = input.ReadBoolean(); keyFrameSequence.HasTime = input.ReadBoolean(); keyFrameSequence.FixedTranslation = input.ReadBoolean(); keyFrameSequence.FixedRotation = input.ReadBoolean(); keyFrameSequence.FixedScale = input.ReadBoolean(); // 读取位置数据 int translationCount = input.ReadInt32(); if (translationCount > 0) { keyFrameSequence.Translation = new List<Vector3>(); for (int i = 0; i < translationCount; i++) keyFrameSequence.Translation.Add(input.ReadVector3()); } // 读取旋转数据 int rotationCount = input.ReadInt32(); if (rotationCount > 0) { keyFrameSequence.Rotation = new List<Quaternion>(); for (int i = 0; i < rotationCount; i++) keyFrameSequence.Rotation.Add(input.ReadQuaternion()); } // 读取缩放数据 int scaleCount = input.ReadInt32(); if (scaleCount > 0) { keyFrameSequence.Scale = new List<Vector3>(); for (int i = 0; i < scaleCount; i++) keyFrameSequence.Scale.Add(input.ReadVector3()); } // 读取时间数据 int timeCount = input.ReadInt32(); if (timeCount > 0) { keyFrameSequence.Time = new List<float>(); for (int i = 0; i < timeCount; i++) keyFrameSequence.Time.Add(input.ReadSingle()); } return keyFrameSequence; } }
已经制作好的动画数据文件都放在Content项目Animation目录中,这个示例只使用了玩家控制的机器人Grund,所以它的动画数据放在了Animation→Players→Grund目录下,共有46个动画,比如说Low_Run.Animation表示机器人下半身奔跑的动画,Up_ReloadShotgun.Animation表示机器人上半身加载散弹枪弹药的动画。
RobotGame中机器人的动画都分为上半身和下半身动画,这样动画更加灵活性。例如下半身动画执行奔跑动画,而上半身可以执行挥臂的奔跑动画,也可以执行切换武器的动画,这样不同的组合就可以执行更多类型的动画。你可以在XNA程序中使用Content.Load
动画播放类
负责动画播放的是AnimationBinder类和AnimationBlender类。 AnimationBinder类类似于前面文章中的AnimationPlayer类,只不过插值操作已经在KeyFrameSequence类中完成了,这个类的主要功能就是根据播放模式(单次还是循环)设置动画本身播放的时间。
/// <summary> /// 这个类根据播放时间计算动画关键帧。 /// 一个Bone动画对应一个AnimtionBinder。 /// 之后为了实现动画混合,需要混合两个AnimationBinder中的关键帧。 /// </summary> public class AnimationBinder { #region 字段 AnimPlayMode playMode = AnimPlayMode.Repeat; float localTime = 0.0f; float timeScaleFactor = 1.0f; KeyFrameSequence keyFrameSequence = null; #endregion #region 属性 public KeyFrameSequence KeyFrameSequence { get { return keyFrameSequence; } } public AnimPlayMode PlayMode { get { return playMode; } } public float LocalTime { get { return localTime; } } public float Duration { get { return KeyFrameSequence.Duration; } } public float ScaleFactor { get { return timeScaleFactor; } } public bool IsPlayDone { get { return (LocalTime >= Duration); } } #endregion /// <summary> /// 设置动画播放的模式。 /// </summary> /// <param name="mode">动画播放模式</param> public void SetPlayMode(AnimPlayMode mode) { playMode = mode; } /// <summary> /// 设置动画播放的时间。 /// </summary> public void SetTime(float time) { localTime = time; } /// <summary> /// 设置动画播放速率。 /// 如果这个值大于1,动画播放速度变快。 /// 如果小于1则变慢。 /// </summary> public void SetTimeScaleFactor(float scaleFactor) { timeScaleFactor = scaleFactor; } /// <summary> /// 设置KeyFrameSequence。 /// </summary> public void SetKeyFrameSequence(KeyFrameSequence keyFrame) { keyFrameSequence = keyFrame; } /// <summary> /// 绑定KeyFrameSequence。 /// </summary> /// <param name="keyFrame">动画片段</param> /// <param name="startTime">动画的开始时间</param> /// <param name="timeScaleFactor">动画播放速率</param> /// <param name="mode">动画播放模式</param> public void BindKeyFrameSequence(KeyFrameSequence keyFrame, float startTime, float timeScaleFactor, AnimPlayMode mode) { SetKeyFrameSequence(keyFrame); SetTime(startTime); SetTimeScaleFactor(timeScaleFactor); SetPlayMode(mode); } /// <summary> /// 初始化 /// </summary> public void Initialize() { SetKeyFrameSequence(null); SetTime(0.0f); SetTimeScaleFactor(1.0f); SetPlayMode(AnimPlayMode.Repeat); } /// <summary> /// 复制AnimationBinder /// </summary> public void CopyTo(AnimationBinder target) { target.SetKeyFrameSequence(KeyFrameSequence); target.SetTime(LocalTime); target.SetTimeScaleFactor(ScaleFactor); target.SetPlayMode(PlayMode); } /// <summary> /// 计算指定时间的关键帧。 /// </summary> /// <param name="time">指定的时间</param> /// <returns>关键帧</returns> public KeyFrame GetKeyFrame(float time) { if (KeyFrameSequence == null) { throw new InvalidOperationException("Sequence is not set."); } // 如果ScaleFactor为零则退出。 (默认为1.0) if (ScaleFactor != 0.0f) { // 获取动画已经播放的时间。 localTime += (float)(ScaleFactor * time); switch (PlayMode) { case AnimPlayMode.Once: // 播放一次 { if (localTime > KeyFrameSequence.Duration) localTime = KeyFrameSequence.Duration; } break; case AnimPlayMode.Repeat: // 循环播放 { // 如果动画播放时间大于动画时间则将播放时间回退到开头 if (localTime > Duration) localTime = HelperMath.CalculateModulo(localTime, KeyFrameSequence.Duration); } break; default: throw new NotSupportedException("Not supported play mode"); } return KeyFrameSequence.GetInterpolateKeyFrame(localTime, PlayMode); } return new KeyFrame(); } /// <summary> /// 计算指定时间的变换矩阵 /// </summary> /// <param name="time">指定的时间</param> /// <returns>关键帧的变换矩阵</returns> public Matrix GetKeyFrameMatrix(float time) { if (KeyFrameSequence == null) { throw new InvalidOperationException("Sequence is not set."); } // 如果ScaleFactor为零则退出。 (默认为1.0) if (ScaleFactor != 0.0f) { // 获取动画已经播放的时间。 localTime += (float)(ScaleFactor * time); switch (PlayMode) { case AnimPlayMode.Once: // 只播放一次 { if (localTime > KeyFrameSequence.Duration) localTime = KeyFrameSequence.Duration; } break; case AnimPlayMode.Repeat: // 循环播放 { // 如果动画播放时间大于动画时间则将播放时间回退到开头 if (localTime > Duration) localTime = HelperMath.CalculateModulo(localTime, KeyFrameSequence.Duration); } break; default: throw new NotSupportedException("Not supported play mode"); } return KeyFrameSequence.GetInterpolateMatrix(localTime, PlayMode); } return Matrix.Identity; } }
AnimationBlender类负责混合两个AnimationBinder的动画。代码如下:
/// <summary> /// 这个类混合两个动画关键帧。 /// 模型的每个bone都对应一个AnimationBlender,而AnimationBlender有1个或2个AnimationBinder。 /// </summary> public class AnimationBlender : INamed { #region 字段 /// <summary> /// 第一个AnimationBinder /// </summary> AnimationBinder firstBinder = null; /// <summary> /// 第二个AnimationBinder /// </summary> AnimationBinder secondBinder = null; string name = String.Empty; int bindCount = 0; float blendTime = 0.0f; float elapsedTime = 0.0f; #endregion #region 属性 public string Name { get { return name; } set { name = value; } } public float BlendTime { get { return blendTime; } } public AnimationBinder AnimationBinder { get { return firstBinder; } } public int BindCount { get { return bindCount; } } public bool IsEmpty { get { return (BindCount == 0); } } #endregion /// <summary> /// 构造函数。 /// </summary> public AnimationBlender() { this.firstBinder = new AnimationBinder(); this.secondBinder = new AnimationBinder(); this.bindCount = 0; } /// <summary> /// 插入新KeyFrameSequence, 这个KeyFrameSequence是定义在AnimationBinder中的。 /// 如果已经有两个AnimationBinder,则移除第一个,将第二个变为第一个, /// 而新KeyFrameSequence插入到第二个AnimationBinder。 /// 如果两个AnimationBinder都为空,则插入到第一个AnimationBinder中 /// </summary> /// <param name="keyFrame">要插入的KeyFrameSequence</param> /// <param name="startTime">动画的开始时间</param> /// <param name="blendTime">两个动画的混合时间</param> /// <param name="timeScaleFactor">动画播放速率</param> /// <param name="mode">播放模式</param> public void AddKeyFrameSequence(KeyFrameSequence keyFrame, float startTime, float blendTime, float timeScaleFactor, AnimPlayMode mode) { this.blendTime = blendTime; this.elapsedTime = startTime; if (this.BlendTime == 0.0f) { ClearAllBinder(); firstBinder.BindKeyFrameSequence(keyFrame, startTime, timeScaleFactor, mode); } else { // 如果已经有了两个AnimationBinder则移除第一个 if (this.bindCount == 2) ShiftBinder(); if (this.bindCount == 0) { firstBinder.BindKeyFrameSequence(keyFrame, startTime, timeScaleFactor, mode); } else if (this.bindCount == 1) { secondBinder.BindKeyFrameSequence(keyFrame, startTime, timeScaleFactor, mode); } } this.bindCount++; } /// <summary> /// 计算动画中指定时间的关键帧变换矩阵。 /// 如果有两个AnimationBinder,则进行混合运算 /// </summary> /// <param name="time">XNA程序每次更新时间流逝的时间</param> /// <returns>变换矩阵</returns> public Matrix GetKeyFrameMatrix(float time) { Matrix keyFrameMatrix = Matrix.Identity; if (firstBinder == null) return keyFrameMatrix; this.elapsedTime += time; // 如果有多个AnimationBinder,则需要混合动画 if (bindCount > 1) { float t = this.elapsedTime / this.blendTime; // 混合结束 if (t > 1.0f) { keyFrameMatrix = secondBinder.GetKeyFrameMatrix(time); ShiftBinder(); } // 计算混合变换矩阵 else { KeyFrame[] sourceKeyFrame = new KeyFrame[2]; KeyFrame targetKeyFrame = new KeyFrame(); // 首先获取两个AnimationBinder中的对应关键帧 sourceKeyFrame[0] = firstBinder.GetKeyFrame(time); sourceKeyFrame[1] = secondBinder.GetKeyFrame(time); // 计算混合关键帧 { // 对平移进行插值操作 if (sourceKeyFrame[0].HasTranslation && sourceKeyFrame[1].HasTranslation) { targetKeyFrame.Translation = Vector3.Lerp( sourceKeyFrame[0].Translation, sourceKeyFrame[1].Translation, t); targetKeyFrame.HasTranslation = true; } else if (sourceKeyFrame[0].HasTranslation) { targetKeyFrame.Translation = sourceKeyFrame[0].Translation; targetKeyFrame.HasTranslation = true; } else if (sourceKeyFrame[1].HasTranslation) { targetKeyFrame.Translation = sourceKeyFrame[1].Translation; targetKeyFrame.HasTranslation = true; } // 对缩放进行插值操作 if (sourceKeyFrame[0].HasScale && sourceKeyFrame[1].HasScale) { targetKeyFrame.Scale = Vector3.Lerp( sourceKeyFrame[0].Scale, sourceKeyFrame[1].Scale, t); targetKeyFrame.HasScale = true; } else if (sourceKeyFrame[0].HasScale) { targetKeyFrame.Scale = sourceKeyFrame[0].Scale; targetKeyFrame.HasScale = true; } else if (sourceKeyFrame[1].HasScale) { targetKeyFrame.Scale = sourceKeyFrame[1].Scale; targetKeyFrame.HasScale = true; } // 对旋转进行插值操作 if (sourceKeyFrame[0].HasRotation && sourceKeyFrame[1].HasRotation) { targetKeyFrame.Rotation = Quaternion.Slerp( sourceKeyFrame[0].Rotation, sourceKeyFrame[1].Rotation, t); targetKeyFrame.HasRotation = true; } else if (sourceKeyFrame[0].HasRotation) { targetKeyFrame.Rotation = sourceKeyFrame[0].Rotation; targetKeyFrame.HasRotation = true; } else if (sourceKeyFrame[1].HasRotation) { targetKeyFrame.Rotation = sourceKeyFrame[1].Rotation; targetKeyFrame.HasRotation = true; } } // 最后,使用经过计算的混合关键帧创建一个矩阵 { // 是否包含旋转 ? if (targetKeyFrame.HasRotation) { // 计算旋转 keyFrameMatrix = Matrix.CreateFromQuaternion( targetKeyFrame.Rotation); } // 是否包含平移 ? if (targetKeyFrame.HasTranslation) { // 计算平移 keyFrameMatrix.Translation = targetKeyFrame.Translation; } // 是否包含缩放 ? if (targetKeyFrame.HasScale) { // 计算缩放 keyFrameMatrix = keyFrameMatrix * Matrix.CreateScale(targetKeyFrame.Scale); } } return keyFrameMatrix; } }// if 如果有多个AnimationBinder,则需要混合动画 else { // 没有混合或混合已结束 keyFrameMatrix = firstBinder.GetKeyFrameMatrix(time); } return keyFrameMatrix; } /// <summary> /// 移动AnimationBinder的位置。 /// 如果已经有两个AnimationBinder, /// 则首先移除第一个AnimationBinder,然后将第二个AnimationBinder移动到第一个AnimationBinder中。 /// </summary> private void ShiftBinder() { this.firstBinder.Initialize(); this.secondBinder.CopyTo(firstBinder); this.secondBinder.Initialize(); this.bindCount--; } private void ClearAllBinder() { this.firstBinder.Initialize(); this.secondBinder.Initialize(); this.bindCount = 0; } }
模型类
RobotGame引擎中的模型类放置在RobotGameData项目的Object目录下,继承结构为NodeBase→GameNode→GameSceneNode→GameModel→GameAnimateModel,本示例只用于演示动画的播放,因此删除了许多无关代码,只保留了GameAnimateModel类,代码如下:
/// <summary> /// 包含并处理动画数据的Model类 /// </summary> public class GameAnimateModel { #region 字段 ContentManager content; protected Model model; Matrix rootAxis = Matrix.Identity; Matrix worldTransform = Matrix.Identity; private Matrix[] boneTransforms = null; protected Matrix[] BoneTransforms { get { return boneTransforms; } set { boneTransforms = value; } } protected List<AnimationBlender> animationBlenderList = new List<AnimationBlender>(); protected List<AnimationSequence> animationList = new List<AnimationSequence>(); #endregion #region 属性 public Matrix TransformedMatrix { get { return rootAxis * worldTransform; } } public Matrix RootAxis { get { return rootAxis; } set { rootAxis = value; } } public int AnimationCount { get { return animationList.Count; } } #endregion /// <summary> /// 构造函数 /// </summary> public GameAnimateModel(string modelName,ContentManager content) { this.content = content; this.model = content.Load <Model>(modelName); // 将模型的绝对变换矩阵保存到boneTransforms this.boneTransforms = new Matrix[this.model.Bones.Count]; this.model.CopyAbsoluteBoneTransformsTo(this.boneTransforms); if (animationBlenderList.Count == 0) { // 将所有的bone名称添加到AnimationBinder for (int i = 0; i < model.Bones.Count; i++) { AnimationBlender animationBlender = new AnimationBlender(); animationBlender.Name = model.Bones[i].Name; animationBlenderList.Add(animationBlender); } } } /// <summary> /// 使用储存在AnimationBlender中的关键帧更新要实施动画的bone矩阵 /// </summary> public virtual void Update(GameTime gameTime) { if (animationList.Count > 0) { for(int i=0; i<this.model.Bones.Count; i++) { ModelBone bone = this.model.Bones[i]; AnimationBlender Blender = animationBlenderList[i]; if (Blender.IsEmpty == false) { // 如果在AnimationBinder中存在KeyFrameSequence, // 则计算变换矩阵 if (Blender.AnimationBinder != null) { // 获取关键帧的变换矩阵 bone.Transform = Blender.GetKeyFrameMatrix( (float)gameTime.ElapsedGameTime.TotalSeconds); } } } } this.model.CopyAbsoluteBoneTransformsTo(this.boneTransforms); } /// <summary> /// 绘制模型 /// </summary> public virtual void Draw(Matrix view,Matrix projection) { foreach (ModelMesh mesh in model.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = boneTransforms[mesh.ParentBone.Index]*TransformedMatrix ; effect.View = view; effect.Projection = projection; } mesh.Draw(); } } /// <summary> /// 设置根节点的旋转 /// </summary> /// <param name="rotation">旋转矩阵</param> public void SetRootAxis(Matrix rotation) { RootAxis = rotation; } /// <summary> /// 在集合中添加一个动画 /// </summary> /// <param name="animation">动画片段</param> /// <returns>添加的动画的索引</returns> public int AddAnimation(AnimationSequence animation) { if (animation == null) { throw new ArgumentNullException("animation"); } animationList.Add(animation); return animationList.IndexOf(animation); } /// <summary> /// 在集合中添加一个动画 /// </summary> /// <param name="fileName">动画文件(.Animation)</param> /// <returns>添加的动画的索引</returns> public int AddAnimation(string fileName) { return AddAnimation(content.Load<AnimationSequence>(fileName)); } /// <summary> /// 根据索引获取一个动画 /// </summary> /// <param name="index">动画索引</param> public AnimationSequence GetAnimation(int index) { return animationList[index]; } /// <summary> /// 根据bone名称获取AnimationBlender /// </summary> public AnimationBlender FindAnimationBlenderByBoneName(string boneName) { for (int i = 0; i < animationBlenderList.Count; i++) { if (animationBlenderList[i].Name == boneName) return animationBlenderList[i]; } return null; } /// <summary> /// 根据索引播放bone动画 /// </summary> /// <param name="index">动画索引</param> /// <param name="playMode">动画播放模式</param> public bool PlayAnimation(int index, AnimPlayMode playMode) { return PlayAnimation(index, 0.0f, 0.0f, 1.0f, playMode); } /// <summary> /// 根据索引值播放动画 /// </summary> /// <param name="index">动画索引</param> /// <param name="startTime">动画的开始时间</param> /// <param name="blendTime">blending time of the animation</param> /// <param name="timeScaleFactor">动画播放模式(默认为1.0)</param> /// <param name="playMode">动画播放模式</param> public bool PlayAnimation(int index, float startTime, float blendTime, float timeScaleFactor, AnimPlayMode playMode) { AnimationSequence animation = GetAnimation(index); if (animation != null) { // 将AnimationSequence绑定到AnimationBinder for( int i=0; i<animation.KeyFrameSequences.Count; i++) { KeyFrameSequence sequence = animation.KeyFrameSequences[i]; AnimationBlender blender = FindAnimationBlenderByBoneName(sequence.BoneName); if (blender == null) { throw new InvalidOperationException( "The animation specified a bone (\"" + sequence.BoneName + "\") that the model (\"" + "\") does not have."); } // 初始化KeyFrameSequence信息 blender.AddKeyFrameSequence(sequence, startTime, blendTime, timeScaleFactor, playMode); } return true; } return false; } /// <summary> /// 获取动画播放的时间 /// </summary> /// <param name="boneName">bone名称</param> /// <returns>当前播放时间</returns> public float GetBonePlayTime(string boneName) { for (int i = 0; i < animationBlenderList.Count; i++) { if (animationBlenderList[i].Name == boneName) { return animationBlenderList[i].AnimationBinder.LocalTime; } } return 0.0f; } }
这个类包含了负责绘制模型,处理动画的代码,只要调用PlayAnimation方法就可以播放对应的动画。
从引擎RobotGameData继承的RobotGame项目中控制玩家动画的主要是GamePlayer类,它的继承层次为GameAnimateModel→GameUnit→Gameplayer,GamePlayer类非常复杂,对机器人的所有控制和播放相应的动画都是在这个类中完成的,针对本文只分析动画实现,所以删除了大部分无关代码,只保留了与动画相关的代码,而且也移除了GameUnit,让GamePlayer直接从GameAnimateModel类继承,代码如下:
#region Action定义 /// <summary> /// 上半身动画 /// </summary> public enum LowerAction { Unknown = 0, Idle, Run, Damage, Walk, BackwardWalk, LeftTurn, RightTurn, ForwardDead, BackwardDead, LeftDead, RightDead, BoosterPrepare, BoosterActive, BoosterFinish, BoosterBreak, BoosterLeftTurn, BoosterRightTurn, Count } /// <summary> /// 下半身动画 /// </summary> public enum UpperAction { Unknown = 0, Idle, Run, Damage, WeaponChange, ForwardNonFire, LeftNonFire, RightNonFire, ForwardMachineGunFire, LeftMachineGunFire, RightMachineGunFire, ReloadMachineGun, ForwardShotgunFire, LeftShotgunFire, RightShotgunFire, ReloadShotgun, ForwardHandgunFire, LeftHandgunFire, RightHandgunFire, ReloadHandgun, ForwardDead, BackwardDead, LeftDead, RightDead, BoosterPrepare, BoosterActive, BoosterFinish, BoosterBreak, BoosterLeftTurn, BoosterRightTurn, Count } #endregion /// <summary> /// 这个类是RobotGame中最重要的一个类,它处理机器人的所有行为。 /// 在接收到用户的输入之后,它会处理机器人的移动,行为,动画和武器。 /// 所有的行为都是定义在这个类中的。 /// </summary> public class GamePlayer : GameAnimateModel { #region 字段 // 当前的动作 LowerAction currentLowerAction = LowerAction.Unknown; UpperAction currentUpperAction = UpperAction.Unknown; LowerAction prepareLowerAction = LowerAction.Unknown; UpperAction prepareUpperAction = UpperAction.Unknown; bool isOverwriteLowerAction = false; bool isOverwriteUpperAction = false; // 动画索引数组 int[] indexLowerAnimation = null; int[] indexUpperAnimation = null; // 默认动画播放速度 float defaultAnimationScaleFactor = 1.0f; #endregion #region 属性 public LowerAction CurrentLowerAction { get { return this.currentLowerAction; } } public UpperAction CurrentUpperAction { get { return this.currentUpperAction; } } #endregion /// <summary> /// 构造函数 /// </summary> public GamePlayer(string modelName,ContentManager content): base(modelName,content) { // 加载动画 LoadAnimationData(@"Animation/Players/Grund/"); } /// <summary> /// 更新动画 /// </summary> public override void Update(GameTime gameTime) { // 播放下半身动画 if (this.prepareLowerAction != LowerAction.Unknown || this.isOverwriteLowerAction) { PlayLowerAction(this.prepareLowerAction, 0.0f); } // 播放上半身动画 if (this.prepareUpperAction != UpperAction.Unknown || this.isOverwriteUpperAction) { PlayUpperAction(this.prepareUpperAction, 0.0f); } base.Update(gameTime); } #region 加载动画数据 /// <summary> /// 加载所有动画数据 /// </summary> /// <param name="path">动画文件夹,不包含文件名称</param> public void LoadAnimationData(string path) { int lowerAnimCount = (int)LowerAction.Count; int upperAnimCount = (int)UpperAction.Count; indexLowerAnimation = new int[lowerAnimCount]; indexUpperAnimation = new int[upperAnimCount]; // 加载下半身动画数据 { indexLowerAnimation[(int)LowerAction.Idle] = AddAnimation(path + "Low_Idle"); indexLowerAnimation[(int)LowerAction.Run] = AddAnimation(path + "Low_Run"); indexLowerAnimation[(int)LowerAction.Damage] = AddAnimation(path + "Low_Damage"); indexLowerAnimation[(int)LowerAction.Walk] = AddAnimation(path + "Low_Walk"); indexLowerAnimation[(int)LowerAction.LeftTurn] = AddAnimation(path + "Low_TurnLeft"); indexLowerAnimation[(int)LowerAction.RightTurn] = AddAnimation(path + "Low_TurnRight"); indexLowerAnimation[(int)LowerAction.BackwardWalk] = AddAnimation(path + "Low_WalkBack"); // Dead lower animation indexLowerAnimation[(int)LowerAction.ForwardDead] = AddAnimation(path + "Low_DeathFront"); indexLowerAnimation[(int)LowerAction.BackwardDead] = AddAnimation(path + "Low_DeathBack"); indexLowerAnimation[(int)LowerAction.LeftDead] = AddAnimation(path + "Low_DeathLeft"); indexLowerAnimation[(int)LowerAction.RightDead] = AddAnimation(path + "Low_DeathRight"); // Booster lower animation indexLowerAnimation[(int)LowerAction.BoosterPrepare] = AddAnimation(path + "Low_BoosterStart"); indexLowerAnimation[(int)LowerAction.BoosterActive] = AddAnimation(path + "Low_BoosterMove"); indexLowerAnimation[(int)LowerAction.BoosterFinish] = AddAnimation(path + "Low_BoosterEnd"); indexLowerAnimation[(int)LowerAction.BoosterBreak] = AddAnimation(path + "Low_BoosterMiss"); indexLowerAnimation[(int)LowerAction.BoosterLeftTurn] = AddAnimation(path + "Low_BoosterTurnLeft"); indexLowerAnimation[(int)LowerAction.BoosterRightTurn] = AddAnimation(path + "Low_BoosterTurnRight"); } // 加载上半身动画 { indexUpperAnimation[(int)UpperAction.Idle] = AddAnimation(path + "Up_Idle"); indexUpperAnimation[(int)UpperAction.Run] = AddAnimation(path + "Up_Run"); indexUpperAnimation[(int)UpperAction.Damage] = AddAnimation(path + "Up_Damage"); indexUpperAnimation[(int)UpperAction.WeaponChange] = AddAnimation(path + "Up_Change"); // Load empty weapon fire upper animation indexUpperAnimation[(int)UpperAction.ForwardNonFire] = AddAnimation(path + "Up_NonAttackFront"); indexUpperAnimation[(int)UpperAction.LeftNonFire] = AddAnimation(path + "Up_NonAttackLeft"); indexUpperAnimation[(int)UpperAction.RightNonFire] = AddAnimation(path + "Up_NonAttackRight"); // MachineGun fire upper animation indexUpperAnimation[(int)UpperAction.ForwardMachineGunFire] = AddAnimation(path + "Up_AttackFrontBase"); indexUpperAnimation[(int)UpperAction.LeftMachineGunFire] = AddAnimation(path + "Up_AttackLeftBase"); indexUpperAnimation[(int)UpperAction.RightMachineGunFire] = AddAnimation(path + "Up_AttackRightBase"); indexUpperAnimation[(int)UpperAction.ReloadMachineGun] = AddAnimation(path + "Up_ReloadBase"); // Shotgun fire upper animation indexUpperAnimation[(int)UpperAction.ForwardShotgunFire] = AddAnimation(path + "Up_AttackFrontShotgun"); indexUpperAnimation[(int)UpperAction.LeftShotgunFire] = AddAnimation(path + "Up_AttackLeftShotgun"); indexUpperAnimation[(int)UpperAction.RightShotgunFire] = AddAnimation(path + "Up_AttackRightShotgun"); indexUpperAnimation[(int)UpperAction.ReloadShotgun] = AddAnimation(path + "Up_ReloadShotgun"); // Handgun fire upper animation indexUpperAnimation[(int)UpperAction.ForwardHandgunFire] = AddAnimation(path + "Up_AttackFrontHandgun"); indexUpperAnimation[(int)UpperAction.LeftHandgunFire] = AddAnimation(path + "Up_AttackLeftHandgun"); indexUpperAnimation[(int)UpperAction.RightHandgunFire] = AddAnimation(path + "Up_AttackRightHandgun"); indexUpperAnimation[(int)UpperAction.ReloadHandgun] = AddAnimation(path + "Up_ReloadHandgun"); // Dead upper animation indexUpperAnimation[(int)UpperAction.ForwardDead] = AddAnimation(path + "Up_DeathFront"); indexUpperAnimation[(int)UpperAction.BackwardDead] = AddAnimation(path + "Up_DeathBack"); indexUpperAnimation[(int)UpperAction.LeftDead] = AddAnimation(path + "Up_DeathLeft"); indexUpperAnimation[(int)UpperAction.RightDead] = AddAnimation(path + "Up_DeathRight"); // Booster upper animation indexUpperAnimation[(int)UpperAction.BoosterPrepare] = AddAnimation(path + "Up_BoosterStart"); indexUpperAnimation[(int)UpperAction.BoosterActive] = AddAnimation(path + "Up_BoosterMove"); indexUpperAnimation[(int)UpperAction.BoosterFinish] = AddAnimation(path + "Up_BoosterEnd"); indexUpperAnimation[(int)UpperAction.BoosterBreak] = AddAnimation(path + "Up_BoosterMiss"); indexUpperAnimation[(int)UpperAction.BoosterLeftTurn] = AddAnimation(path + "Up_BoosterTurnLeft"); indexUpperAnimation[(int)UpperAction.BoosterRightTurn] = AddAnimation(path + "Up_BoosterTurnRight"); } } #endregion #region 播放动画 /// <summary> /// 根据action播放下半身动画 /// </summary> /// <param name="action">下半身action</param> public void PlayLowerAction(LowerAction action) { PlayLowerAction(action, 0.0f); } /// <summary> /// 根据action播放下半身动画 /// </summary> /// <param name="action">下半身action</param> /// <param name="startTime">动画的开始时间</param> public void PlayLowerAction(LowerAction action, float startTime) { AnimPlayMode playMode = AnimPlayMode.Repeat; float blendTime = 0.0f; switch (action) { case LowerAction.Idle: { blendTime = 0.5f; } break; case LowerAction.Run: { blendTime = 0.3f; } break; case LowerAction.Damage: { playMode = AnimPlayMode.Once; blendTime = 0.2f; } break; case LowerAction.Walk: { blendTime = 0.3f; } break; case LowerAction.BackwardWalk: { blendTime = 0.3f; } break; case LowerAction.LeftTurn: case LowerAction.RightTurn: { blendTime = 0.5f; } break; case LowerAction.ForwardDead: case LowerAction.BackwardDead: case LowerAction.LeftDead: case LowerAction.RightDead: { playMode = AnimPlayMode.Once; blendTime = 0.2f; } break; case LowerAction.BoosterPrepare: case LowerAction.BoosterFinish: case LowerAction.BoosterActive: case LowerAction.BoosterBreak: case LowerAction.BoosterLeftTurn: case LowerAction.BoosterRightTurn: { playMode = AnimPlayMode.Once; blendTime = 0.2f; } break; default: throw new NotSupportedException("Not supported an animation"); } // 播放下半身动画 PlayAnimation(indexLowerAnimation[(int)action], startTime, blendTime, this.defaultAnimationScaleFactor, playMode); this.currentLowerAction = action; this.prepareLowerAction = LowerAction.Unknown; this.isOverwriteLowerAction = false; } /// <summary> /// 根据action播放上半身动画。 /// </summary> /// <param name="action">上半身action</param> public void PlayUpperAction(UpperAction action) { PlayUpperAction(action, 0.0f); } /// <summary> /// 根据action播放上半身动画。 /// </summary> /// <param name="action">上半身action</param> /// <param name="startTime">动画开始时间</param> public void PlayUpperAction(UpperAction action, float startTime) { AnimPlayMode playMode = AnimPlayMode.Repeat; float blendTime = 0.0f; switch (action) { case UpperAction.Idle: { blendTime = 0.5f; } break; case UpperAction.Run: { blendTime = 0.3f; } break; case UpperAction.Damage: { playMode = AnimPlayMode.Once; blendTime = 0.2f; } break; case UpperAction.WeaponChange: { playMode = AnimPlayMode.Once; blendTime = 0.2f; } break; case UpperAction.ForwardNonFire: case UpperAction.LeftNonFire: case UpperAction.RightNonFire: { playMode = AnimPlayMode.Once; blendTime = 0.5f; } break; case UpperAction.ForwardMachineGunFire: case UpperAction.ForwardShotgunFire: case UpperAction.ForwardHandgunFire: case UpperAction.LeftMachineGunFire: case UpperAction.LeftShotgunFire: case UpperAction.LeftHandgunFire: case UpperAction.RightMachineGunFire: case UpperAction.RightShotgunFire: case UpperAction.RightHandgunFire: { playMode = AnimPlayMode.Once; blendTime = 0.5f; } break; case UpperAction.ReloadMachineGun: case UpperAction.ReloadShotgun: case UpperAction.ReloadHandgun: { playMode = AnimPlayMode.Once; blendTime = 0.5f; } break; case UpperAction.ForwardDead: case UpperAction.BackwardDead: case UpperAction.LeftDead: case UpperAction.RightDead: { playMode = AnimPlayMode.Once; blendTime = 0.2f; } break; case UpperAction.BoosterPrepare: case UpperAction.BoosterFinish: case UpperAction.BoosterActive: case UpperAction.BoosterBreak: case UpperAction.BoosterLeftTurn: case UpperAction.BoosterRightTurn: { playMode = AnimPlayMode.Once; blendTime = 0.2f; } break; default: throw new NotSupportedException("Not supported an animation"); } // 播放上半身动画 PlayAnimation(indexUpperAnimation[(int)action], startTime, blendTime, this.defaultAnimationScaleFactor, playMode); this.currentUpperAction = action; this.prepareUpperAction = UpperAction.Unknown; this.isOverwriteUpperAction = false; } #endregion }
要弄懂动画实现的流程,强烈建议你在适当的地方设置断点,看一看程序执行的整个过程。
下面我以奔跑动画为例详细分析一下。
在XNA程序主类Game1.cs中初始化一个GamePlayer时,在它的基类构造函数中创建了AnimationBlender类的集合animationBlenderList中,这个集合的大小就是模型Grund的Bone大小,为48个,它的Name属性设置为Bone的Name属性。然后调用自己的LoadAnimation方法从Content项目的“Animation/Players/Grund/”加载所有的动画数据,共有46个AnimationSequence类型的对象,这些对象都保存在基类的animationList集合中。
要播放奔跑动画需要调用GamePlayer中的以下两个方法:
player.PlayLowerAction(UpperAction.Run); player.PlayUpperAction(LowerAction.Run);
在以上两个方法中的swtich分支中判断应该循环播放并将blendTime设置为0.3秒,最终都要调用基类的PlayAnimation方法进行,在PlayAnimation方法中需要知道动画的索引,下半身奔跑动画Low_Run索引值为1,持续时间0.8秒,含有10个KeyframeSequence,这10个KeyframeSequence分别对应以下10个bone:
索引 | BoneName | Keyframe数量 |
---|---|---|
0 | L Calf | 25 |
1 | L Foot | 25 |
2 | L Thigh | 25 |
3 | L Toe0Nub | 1 |
4 | Pelvis | 1 |
5 | R Calf | 25 |
6 | R Foot | 25 |
7 | R Thigh | 25 |
8 | R Toe0Nub | 1 |
9 | ROOT | 25 |
下半身奔跑动画Up_Run索引值为18,持续时间0.8秒,含有10个KeyframeSequence,这10个KeyframeSequence分别对应以下10个bone:
索引 | BoneName | Keyframe数量 |
---|---|---|
0 | HeadNub | 1 |
1 | Clavicle | 1 |
2 | L Forearm | 25 |
3 | L UpperArm | 25 |
4 | Neck_NA | 1 |
5 | R Clavicle | 1 |
6 | R Forearm | 25 |
7 | R UpperArm | 25 |
8 | Spine | 1 |
9 | Spine1 | 25 |
在PlayAnimation方法中,主要就是调用AniamtionBlender类的AddKeyFrameSequence方法将以上20个KeyframeSequence根据BoneName放入animationBlenderList的对应位置。然后在GameAnimatedModel类的Update方法中,调用AnmationBlender的GetKeyFrameMatrix方法设置模型对应的Bone矩阵。因为这20个BoneName没有一个相同,所以AniamtionBlender类只是简单地返回其中第一个AnimationBinder的GetKeyFrameMatrix方法返回对应的变换矩阵。
碰撞检测
因为这篇文章专注于RobotGame的动画,所以下面只是非常简单地介绍它的碰撞检测基本原理。大致说来,首先需要自定义内容处理器在Model的Tag属性中添加顶点和包围球信息,在引擎的Collision目录下有所有的碰撞检测代码,既用到了四叉树(QuadTree),又用到了XNA框架自带的Intersects方法。 特别是在进行模型与场景的碰撞检测中,事实上制作了三个模型,第一个模型是实际显示的场景模型France.FBX,它包含纹理,而France_Col_Hit.FBX和France_Col_Move.FBX只包含顶点信息用于子弹的碰撞和机器人移动的碰撞,它们在游戏中只加载不显示,为了演示它们的差别,在本示例中你可以按右Control键切换三者的显示。
示例截图如下:
文件下载(已下载 1164 次)
发布时间:2011/9/19 上午8:10:51 阅读次数:7444