一个用于 Windows Phone、PC 和 Xbox 的 XNA 动画精灵组件






4.78/5 (8投票s)
一个动画精灵组件的概念验证,我正在开发它以在 Windows Phone、PC 和 Xbox 上的 XNA 项目中运行。该组件允许动画信息成为项目内容的一部分,并且是朝着允许艺术家完全掌控动画创建迈出的一步。
引言
动画的基础众所周知;通过在静止图像组之间快速切换,我们可以创造出物体正在移动的幻觉。简单地制作动画并不是一个挑战。最近在开发一款游戏时,我发现需要想出一种更好的方法来管理我的动画状态。通常,屏幕上可能会显示多个对象,它们都来自同一个精灵图集。但是每个对象在动画中可能处于不同的状态。还有一个问题是如何将精灵坐标导入程序。虽然您可以将这些坐标输入到程序代码中,但这并不是最理想的解决方案;如果图像中的某些内容发生变化,则需要修改代码。本文附带的代码的目的是解决这两个问题:管理动画状态和将信息作为内容管理。
得出我需要一个更好解决方案的第二天,我坐了几个小时的车去参加家庭聚会。我下面记录的是我在开车前一晚想出来的,并在开车时打出来的解决方案。这是一个概念验证,而不是最终代码,但我认为其他人可以轻松扩展以适应自己的需求。
必备组件
要利用本文中的信息,您需要已经熟悉 XNA 和精灵处理。作为 .NET 技术,这也意味着您需要熟悉 C# 和 Visual Studio。对于软件,您需要一台运行 Windows Vista 或更高版本的 PC,并且您需要安装 Windows Phone 开发人员工具(即使您不打算针对 Windows Phone)。虽然下载标签是针对 Windows Phone 的,但它实际上是一个包含 Windows Phone、Xbox 和 PC 开发工具的包。
运行项目
我知道有些人希望在不需要下载和设置环境的情况下看到项目运行。为了方便这部分人,我上传了一个 YouTube 视频,其中我简要介绍了项目并运行了它。
精灵动画基础
渲染精灵时,您通常会有一个包含一个或多个图像的源。您指定包含要显示的图像的边界矩形的坐标,以及要显示它的屏幕坐标。如果要制作精灵动画,则需要为动画的每一帧提供一个图像作为参考。您需要按顺序在源图像集之间切换。XNA 每秒最多可以渲染 60 帧到屏幕(可能更快,但我们暂时忽略它)。但是您可能没有为这 60 帧中的每一帧都提供一个不同版本的图像。您需要适当地确定何时切换到下一张图像。
为了更好地理解,如果首先下载一个精灵图集来查看会有帮助。外面有无数的精灵图集。我决定 Bing 搜索一个 马里奥兄弟 游戏的精灵图集,发现数量不少。我找到了 这个小宝贝。这实际上不是任天堂制作的,而是任天堂游戏粉丝制作的。最棒的是,这张图集上的精灵组织得很好并且有标签。有一个马里奥跑步的精灵我要用。所以我使用了 Paint.Net 剪裁图像并擦除白色背景,这样图像就会是透明的。
当您的眼睛跟随精灵图集中的每张图像时,您可以看到动画将如何呈现。理想情况下,精灵中的图像应该间隔均匀。对于来自可用内存以千字节为单位计算的时代的图像,您可能会发现情况并非如此,因为可用内存对决策的影响大于便利性。制作这个精灵的人似乎只试图最小化图像之间的空间,所以它们没有均匀间隔。但我认为这个动画很酷,它唤起了回忆,所以我坚持使用它。我还从另一篇 CodeProject.com 文章中抓取了一张带有旋转螺旋桨的飞机图像,并找到了一只鸟扇动翅膀的图像。有了这三张预先存在的图像,我发现让它们动起来需要不同程度的努力(这也给了我未来如何改进这段代码的想法)。我将从最简单的图像(来自 CodeProject.com 文章的飞机)开始,然后转向更复杂的图像。
非常感谢 Jonas Follesø 关于游戏 1945 for PocketPC 的文章,并将其发布在 CPOL 下。我从那里获取了飞机图像。
飞机动画
文章中的一个文件名为 BigPlanes.bmp。我将其从位图转换为 PNG,以便使其透明。图像包含三架飞机的精灵。每架飞机都有一个动画螺旋桨。这些图像间隔均匀。第一架飞机从坐标 (1,1) 开始,宽度和高度均为 64 像素。第一架飞机图像之后是一个灰色的 1 像素边框,然后第二架飞机从位置 (66,1) 开始。由于 1 像素边框和图像 64 像素的宽度,您可以通过 (n*65)+1 获得第 n 架飞机的起始点。这使得将此精灵图集转换为动画变得容易得多。我不必查找每个图像的起始点。
我只打算动画化图集中的前两架飞机。我将它们作为同一对象不同状态的动画。我得到了三张图像 {(1,1), (66, 1), (131, 1)} 和另外三张图像 {(196,1), (261, 1), (326, 1)} 的起始 X 和 Y 位置,并将它们放入我的代码中。现在,假设屏幕每秒更新 30 次,但我只想让这个动画每秒发生 10 帧。在 30fps 下,玩家将能够在屏幕上平滑地移动飞机。因此,我需要跟踪在推进帧之前已经过去了多少时间。在下面的代码中,我动画化了飞机,并提供了一种从一种飞机动画切换到另一种飞机动画的方法。我省略了一些您可能期望在 XNA 程序中找到的典型代码片段,以便突出显示特定于此任务的部分。
Rectangle[] _planeSourceSpriteLocation_1 = new Rectangle[]
{
new Rectangle( 1, 1, 64, 64),
new Rectangle( 66, 1, 64, 64),
new Rectangle(131, 1, 64, 64),
};
Rectangle[] _planeSourceSpriteLocation_2 = new Rectangle[]
{
new Rectangle(196, 1, 64, 64),
new Rectangle(261, 1, 64, 64),
new Rectangle(326, 1, 64, 64),
};
Rectangle[] _planeSourceSpriteLocation_current;
int _frameNumber;
TimeSpan _frameLength;
Vector2 _position = new Vector2(100,100);
Texture2D _planeTexture;
void SetPlaneSource(int i)
{
_planeSourceSpriteLocation_current= (i==1)?
_planeSourceSpriteLocation_2 :
_planeSourceSpriteLocation_1;
}
protected override void LoadContent()
{
///code for loading sprites here
SetPlaneSource(1);
}
protected override void Update(GameTime gameTime)
{
_frameLength += gameTime.ElapsedGameTime;
//if the frame has been displayed for more than 0.1 seconds then
//advance to the next frame.
if(_frameLength.TotalSeconds > 0.1d)
{
_frameLenth -= TimeSpan.FromSeconds(0.1);
++_frameNumber;
//Ensure we haven't advanced past the last frame by looping
//back to the first frame.
if(_frameNumber >
您可以看到,对于单个精灵,要跟踪和处理的信息量很大。这不是您希望用于多个精灵的方式;代码会很快变得混乱。在本文结束之前,我将展示一种组织这些信息的方法。
动画鸟
对于我找到的另一张鸟的精灵图集,图像没有均匀间隔,但这并没有造成太大的使用障碍。这只鸟正在扇动翅膀,但它的头部是静止的。所以当我定义我的边界矩形时,我确保鸟的头部在每个矩形中都处于相同的位置。如果不是这样,这只鸟在动画时就会显得摇晃。一旦我有了我的绑定矩形,剩下的任务就和飞机一样了。
前面的示例中的代码将适用于此鸟的动画,尽管我的矩形数组将有 4 个矩形而不是 3 个。
马里奥跑步动画
在尝试动画马里奥时,遇到了第一个真正的障碍。那个精灵中的图像没有均匀间隔。所以我使用了和鸟一样的技术;我选择图像的一个静止部分,并获得相对于该静止位置的坐标。当我这样做时,我遇到了第二个问题;图像的宽度不同。如果我使用一个与其中一个图像的最小宽度匹配的边界矩形,那么在某些帧中,动画的一部分会被裁剪。如果我使用一个与最大宽度匹配的矩形,那么我将无意中导致相邻图像的一部分不适当地出现在某些帧中。我可以使用不同宽度的矩形,以便每个矩形都能适当地绑定其预期的帧,而不会溢出到其他帧,但这样做会强制修改 Draw
代码以适应需要略微不同绘制的矩形。我考虑过为此制作一个解决方案,但后来放弃了。适应这样的图像会使代码变得相当复杂。经过深思熟虑,我决定将其要求为与动画关联的所有精灵都具有相同的长度。我没有在代码中强制执行此操作,但我也不支持它。
表示精灵动画
我已经运行了一些简单的精灵动画场景,并且对代码需要做什么有了很好的了解。接下来是将动画精灵所需的信息组织到类中的一种方法。
指定精灵坐标
精灵图集本质上只是一张包含许多小图像的图像。通常,当您从精灵图集中绘制图像时,您必须指定两组坐标。让我们看一下典型的将精灵绘制到屏幕上的调用。我已将数据类型放在注释中。
spriteBatch.Draw(
/*Texture2D*/ sourceTexture,
/*Rectangle*/ destinationRectangle,
/*Rectangle*/ sourceRectangle,
/*Color*/ tint
);
目标矩形将取决于您的对象需要在屏幕上绘制的确切位置,这将取决于您的对象当前在游戏世界中的位置。但是源矩形的变化会更小。每次绘制相同的对象时,纹理的源坐标都将在编译时确定。因此,所寻求的解决方案的一个属性是它需要能够跟踪精灵图集中的源坐标。每个源矩形坐标还将与精灵需要显示的时间长度相关联。如果我有一个以每秒 15 帧播放的动画,那么与每一帧关联的时间长度将是 1/15 秒。
public class SpriteSource
{
public TimeSpan FrameLength { get; set; }
public double FrameLengthSeconds
{
get { return FrameLength.TotalSeconds; }
set { FrameLength = TimeSpan.FromSeconds(value); }
}
public Rectangle SourceRectangle { get; set; }
}
FrameLengthSeconds
字段看起来是多余的,但由于此数据将保存到文件中,因此它必须是可序列化的。TimeSpan
成员不可序列化,因此我添加了一个类型为 double
的别名字段 (FrameLengthSeconds
),它将序列化。
将坐标分组到帧中
一个动画是帧的集合,所以我需要能够将我的帧组织成一个列表。一个泛型列表类满足这个需求。但是任何给定的动画对象都可以与多个动画相关联。例如,您可能对一个人走路和一个人跑步有不同的动画。您可能对玩家处于正常模式与加强模式有不同的动画。或者您可能对角色可以行走的每个方向都有一个动画。因此,我的动画将包含帧的集合,但每个对象也可以包含动画的集合。我更喜欢在我的类中按名称引用动画,用于存储属于动画的帧。我还需要能够识别在特定时间索引处应该显示哪个帧。由于显示的动画将取决于对象的状态,我借鉴了 XAML 概念,并将表示特定状态下对象的帧集合称为 VisualState
。
public class VisualState
{
public VisualState()
{
SpriteSourceList = new List<SpriteSource>();
}
public SpriteSource this[TimeSpan index]
{
get {
if(TotalLength.Ticks>0)
while (index > TotalLength)
index -= TotalLength;
int i = 0;
while(index>SpriteSourceList[i].FrameLength)
{
++i;
index -= SpriteSourceList[i].FrameLength;
}
return SpriteSourceList[i];
}
}
public TimeSpan TotalLength
{
get { return TimeSpan.FromTicks(
SpriteSourceList.Sum((ss) => ss.FrameLength.Ticks)); }
}
public string Name { get; set; }
public List<SpriteSource> SpriteSourceList { get; set; }
}
将动画分组为单个对象
由于任何对象都可以有多个动画,因此列表非常适合将这些动画分组为一个实体。为了方便起见,我还为动画集合命名(这样做没有功能上的原因,但在调试时,能够将集合与名称关联起来很有帮助),并且还添加了一个成员来引用动画将从中提取图像的 Texture2D
。
public class AnimatedSprite
{
public string Name { get; set; }
public string PreferredTextureName { get; set; }
[XmlIgnore]
internal Texture2D CurrentTexture { get; set; }
public void SetTexture(Texture2D sourceTexture)
{
CurrentTexture = sourceTexture;
}
public List<VisualState> VisualStateList { get; set; }
public AnimatedSprite()
{
VisualStateList = new List<VisualState>();
}
public AnimatedSpriteInstance CreateInstance()
{
return new AnimatedSpriteInstance(this);
}
}
上面代码中有两件事我还没有谈到。CreateInstance()
方法尚未解释,CurrentTexture
标记为 internal
的原因也尚未解释。我稍后会回到 CurrentTexture
上的 internal
标记。让我们谈谈 CreateInstance()
。
跟踪动画实例
屏幕上可能会有多个项目使用相同的动画。为每个对象保留每个动画的精确副本会是多余的。但也有必须特定于每个实例的数据。因此,我将公共数据与实例数据分开了。AnimatedSprite
类包含公共数据。每个拥有自己动画的对象都需要拥有实例数据,这些数据可以通过 CreateInstance()
类获取。此方法实例化一个新的 AnimatedSpriteInstance()
。我没有将此类的构造函数设为公共,因此创建它的唯一方法是首先拥有一个 AnimatedSprite
,然后调用 CreateInstance()
方法。
此类包含的最重要信息是对创建实例的动画精灵的引用(在 Parent
属性中)、动画的时间进度(在 _timeOffset
字段中)以及状态,它决定了正在使用哪个动画集(在 PresentState
属性中,但通过 StateName
属性进行操作)。如果出于某种原因我决定动画的特定实例应该从不同的纹理中提取,我公开了一个名为 TextureOverride
的属性,它默认为 null
。将其设置为另一个纹理将导致动画实例使用该新纹理。将其设置回 null
将导致它恢复为原始纹理。还有一个 Position
成员,您可以使用它来设置精灵在屏幕上出现的位置。
动画通过 AnimationInstance.Update(GameTime)
推进。一般来说,您会在 Draw
方法中尽早调用它。但是,如果动画需要与某个定时事件同步,那么最好在游戏的 Update()
方法中调用它。如果游戏速度减慢,Draw()
方法可能不会在每个周期都调用,但 Update
总是会被调用。一旦达到最后一帧,所有动画都将循环。因此,AnimatedSpriteInstance.Update()
方法将在动画结束时重置 _timeOffset
。
Draw
方法将负责绘制精灵的适当帧。您只需提供要使用的 SpriteBatch
实例即可。您可能还记得 SpriteBatch
方法有许多重载的 Draw
方法。我在此示例代码中只使用了一个,但您可能希望更改它以使用提供更多选项的重载方法之一。您可以通过在 AnimatedSpriteInstance.Draw()
方法上添加新成员来公开这些选项,或者您可以添加新属性来保存将传递给附加参数的值。例如,我添加了一个 Color Tint { get; set; }
属性,它将作为动画的 Tine
参数传递。
public class AnimatedSpriteInstance
{
private TimeSpan _timeOffset;
public Vector2 Position { get; set; }
public AnimatedSprite Parent { get; internal set; }
public SpriteBatch TargetSpriteBatch { get; set; }
public Texture2D TextureOverride { get; set; }
public VisualState PresentState { get; protected set; }
public Color Tint { get; set; }
private string _stateName;
public string StateName
{
get { return _stateName; }
set
{
VisualState newState =
(from VisualState s in Parent.VisualStateList where
s.Name.Equals(value) select s).FirstOrDefault();
if(newState==null)
throw new IndexOutOfRangeException(String.Format(
"There is no state named {0} in this sprite", value));
_stateName = value;
PresentState = newState;
}
}
public AnimatedSpriteInstance()
{
Tint = Color.White;
TextureOverride = null;
Reset();
}
public AnimatedSpriteInstance(AnimatedSprite parent):this()
{
Parent = parent;
Reset();
}
public void Reset()
{
_timeOffset = TimeSpan.Zero;
if (Parent != null)
{
PresentState = Parent.VisualStateList[0];
_stateName = PresentState.Name;
}
}
public void Update(GameTime gameTime)
{
_timeOffset += gameTime.ElapsedGameTime;
while (_timeOffset >= PresentState.TotalLength)
_timeOffset -= PresentState.TotalLength;
}
public void Draw(SpriteBatch targetBatch)
{
var spriteSource = PresentState[_timeOffset].SourceRectangle;
targetBatch.Draw( (TextureOverride ?? Parent.CurrentTexture),
Position, spriteSource, Color.White);
}
public void Draw()
{
Draw(TargetSpriteBatch);
}
}
类图
使动画支持成为内容
我编写这段代码的部分原因是我想开始从我的代码中移除动画的细节,并将其放入另一个资源中,以便没有编程技能的人可以修改和创建动画。我的第一步是允许在 XML 文件中指定精灵位置信息。虽然我预计大多数艺术家不熟悉 XML 文件,但这是朝着人性化(艺术家)解决方案迈进的一小步。下一步是创建一个工具,以便某人可以图形化地指定精灵的位置。我不会在本文中解决这部分。所以我只想在这里谈论创建 XML 文件并在 XNA 项目中使用它。在将此代码用于更多场景之后,我将讨论创建用于操作此信息的图形工具。我不知道未来的需求会给代码带来什么变化,并且暂时不想拥有一个可能需要随着我调整动画代码而更新的图形编辑器。一旦动画代码进一步成熟并处理更广泛的场景,我才会考虑制作一个图形工具。
XNA 有一个名为内容管道的设施,用于处理项目中的内容。如果您是 XNA 的新手,您可能已经愉快地使用内容管道,而无需考虑其内部工作原理。内容管道已经拥有处理一些常见文件类型(MP3、PNG、3D 模型格式等)所需的功能,我希望扩展它也能处理我的动画。完成后,我将能够对为 PC、Xbox 和 Windows Phone 制作的游戏使用相同的技术。
内容管道基础
通过内容管道的资产由四种不同的对象处理:导入器、处理器、写入器和读取器。导入器读取资产并将其传递给处理器。处理器查看从资产读取的数据并对其进行解释,构建另一个对象,该对象表示加载内容时您希望数据采用的形式。写入器将对象序列化为流。而读取器将在运行时由游戏用于加载保存的内容。XNA 定义了用于实现这些类型对象的每种类型的通用基类。
在这四种类型的对象中,其中三种在设计时使用(导入器、处理器、写入器),一种在运行时使用(读取器)。这三个设计时类可以是同一个项目的一部分。运行时类必须随项目部署,并且必须为游戏将运行的每个平台创建运行时类的一个版本。在我的例子中,运行时项目将只是共享相同文件的项目,因为从 Windows Phone 到 PC 到 Xbox 的源代码没有区别。让我们更详细地了解我是如何实现这些类的。我首先创建了一个新项目。在新项目对话框中,XNA 组下有一个内容扩展项目的选项。创建项目后,我开始向其中添加以下内容。
内容导入器
XNA 定义了通用基类 ContentImporter<T>
来实现内容导入器。此类只将数据加载到可以传递给处理器的内容中,但除此之外不对文件进行任何处理。内容导入器必须声明其处理的文件类型的扩展名,并定义要使用的处理器以及一个友好的显示名称。所有这些信息都通过类上的 ContentImporterAttribute
传递。
[ContentImporter(".animatedSprite",
DefaultProcessor = "AnimatedSpriteProcessor",
DisplayName = "Animated Sprite Importer")]
public class AnimatedSpriteImporter: ContentImporter<AnimatedSpriteDescription>
{
public override AnimatedSpriteDescription Import(
string filename, ContentImporterContext context)
{
string spriteXml = File.ReadAllText(filename);
return new AnimatedSpriteDescription(
Path.GetFileNameWithoutExtension(filename), spriteXml);
}
}
AnimatedSpriteDescription
是我定义的一个类,用于保存文件的内容。它只包含两个字段,AnimatedSpriteXml
和 AnimationName
,两者都是字符串。
public class AnimatedSpriteDescription
{
public string AnimatedSpriteXml { get; set; }
public string AnimationName { get; set; }
public AnimatedSpriteDescription(string name, string xml)
{
AnimationName = name;
AnimatedSpriteXml = xml;
}
}
内容处理器
内容处理器将从 ContentImporter
接收到的数据转换为对象。用于实现 ContentProcessor
的通用基类,您可能已经猜到,是 ContentProcessor<InputType, OutputType>
。InputType
需要您的导入器类型,OutputType
应设置为处理器生成的类型。我刚刚向您介绍了导入器。我的处理器的输出是一个 AnimatedSprite
;与我在本文开头描述的类型相同。派生类中唯一需要重写的方法是 Process
方法。
[ContentProcessor(DisplayName = "Animated Sprite Processor")]
public class AnimatedSpriteProcessor :
ContentProcessor<AnimatedSpriteDescription, AnimatedSprite>
{
public override AnimatedSprite Process(AnimatedSpriteDescription input,
ContentProcessorContext context)
{
XmlSerializer xs = new XmlSerializer(typeof(AnimatedSprite));
StringReader sr = new StringReader(input.AnimatedSpriteXml);
var entry = (AnimatedSprite)xs.Deserialize(sr);
//----
//additional processing done here
//----
return entry;
}
}
由于我选择了 XML 格式,您可以看到我不需要进行太多处理。
内容写入器
您的资产将经过的最后一个设计时组件是派生自 ContentTypeWriter<ContentType>
的类。它有几个需要重写的方法。正如您所料,有一个 Write
方法必须序列化您的内容。作为参数,它接收一个 ContentWriter
和由 ContentProcessor
生成的需要写入的对象。还有两个其他方法也需要重写:GetRuntimeType
和 GetRuntimeReader
。GetRuntimeType
方法应该返回一个字符串,该字符串标识程序集以及程序集中必须实例化以保存您内容的类型。GetRuntimeType
返回一个字符串,该字符串标识程序集以及程序集中包含 ContentReader
的类。
class AnimatedSpriteWriter: ContentTypeWriter<AnimatedSprite>
{
protected override void Write(ContentWriter output, AnimatedSprite value)
{
XmlSerializer xs = new XmlSerializer(typeof(AnimatedSprite));
System.IO.StringWriter sw = new StringWriter();
xs.Serialize(sw, value);
output.Write(value.Name);
output.Write(sw.ToString());
}
public override string GetRuntimeType(
Microsoft.Xna.Framework.Content.Pipeline.TargetPlatform targetPlatform)
{
return "J2i.Net.AnimatedSriteLibrary, AnimatedSprite, " +
"Version=1.0.0.0, Culture=neutral";
}
public override string GetRuntimeReader(
Microsoft.Xna.Framework.Content.Pipeline.TargetPlatform targetPlatform)
{
return "J2i.Net.AnimatedSriteLibrary, AnimatedSpriteReader, " +
"Version=1.0.0.0, Culture=neutral";
}
}
ContentTypeReader
我之前谈到的其他三个内容类都在您的 PC 上执行,而您正在构建游戏。它们都不会被推送到运行游戏的机器或设备上。ContentTypeReader
与这些不同之处在于它必须随游戏一起分发。由于您的 XNA 游戏可以在三个平台(Windows Phone、Xbox 和 PC)中的一个上运行,因此您需要三个版本的内容读取器。所有三个的源代码可以是相同的。您甚至可以使用相同的文件来编译所有三个版本。但这些平台中的每一个都有自己的可执行文件二进制格式,因此您不会在所有三个平台上都有完全相同的二进制文件。目前,我将只针对 PC。添加 Windows Phone 和 Xbox 所需的支持是一项微不足道的努力,因此我将把这项任务留到以后。
由于 ContentTypeReader
将与我之前构建的 AnimatedSprite
使用相同的代码,因此我发现将其作为同一项目的一部分是合适的。我向该项目添加了一个新类,并使其继承自 ContentTypeReader<AnimatedSprite>
并重写了 Read
方法。作为参数,此方法接收对 ContentReader
的引用以及一个实例化对象,内容可以写入该对象。由于我使用 XML 序列化来写入资产,因此一旦我从 ContentTypeReader
读取数据,就可以使用 XmlSerializer
类上的 Deserialize
方法。
public class AnimatedSpriteReader : ContentTypeReader<AnimatedSprite>
{
protected override AnimatedSprite Read(ContentReader input,
AnimatedSprite existingInstance)
{
var xs = new System.Xml.Serialization.XmlSerializer(typeof(AnimatedSprite));
string name = input.ReadString();
string info = input.ReadString();
System.IO.StringReader sr = new System.IO.StringReader(info);
var animatedSprite = (AnimatedSprite)xs.Deserialize(sr);
return animatedSprite;
}
}
创建动画
创建动画只需键入一个包含正确坐标的 XML 文件并为其提供 .animatedSprite 扩展名。我已在下面键入飞机动画的代码。您可能会注意到,每个帧都有自己的 FrameLengthSeconds
设置。这是因为并非每个帧都必须具有相同的长度。我还为动画提供了两种状态;一种称为 Main
,将是动画的默认状态,另一种称为 Green
。
<?xml version="1.0" encoding="utf-8"?>
<AnimatedSprite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Plane Map</Name>
<VisualStateList>
<!-- The first visual state for the main animation -->
<VisualState>
<Name>Main</Name>
<SpriteSourceList>
<SpriteSource>
<FrameLengthSeconds>0.1</FrameLengthSeconds>
<SourceRectangle>
<X>1</X>
<Y>1</Y>
<Width>64</Width>
<Height>64</Height>
</SourceRectangle>
</SpriteSource>
<SpriteSource>
<FrameLengthSeconds>0.1</FrameLengthSeconds>
<SourceRectangle>
<X>67</X>
<Y>1</Y>
<Width>64</Width>
<Height>64</Height>
</SourceRectangle>
</SpriteSource>
<SpriteSource>
<FrameLengthSeconds>0.1</FrameLengthSeconds>
<SourceRectangle>
<X>133</X>
<Y>1</Y>
<Width>64</Width>
<Height>64</Height>
</SourceRectangle>
</SpriteSource>
</SpriteSourceList>
</VisualState>
<!-- The second visual state for the Green animation -->
<VisualState>
<Name>Green</Name>
<SpriteSourceList>
<SpriteSource>
<FrameLengthSeconds>0.1</FrameLengthSeconds>
<SourceRectangle>
<X>199</X>
<Y>1</Y>
<Width>64</Width>
<Height>64</Height>
</SourceRectangle>
</SpriteSource>
<SpriteSource>
<FrameLengthSeconds>0.1</FrameLengthSeconds>
<SourceRectangle>
<X>265</X>
<Y>1</Y>
<Width>64</Width>
<Height>64</Height>
</SourceRectangle>
</SpriteSource>
<SpriteSource>
<FrameLengthSeconds>0.1</FrameLengthSeconds>
<SourceRectangle>
<X>331</X>
<Y>1</Y>
<Width>64</Width>
<Height>64</Height>
</SourceRectangle>
</SpriteSource>
</SpriteSourceList>
</VisualState>
</VisualStateList>
</AnimatedSprite>
为了使用精灵,我需要对我的 XNA 游戏中的内容项目做几件事
- 添加对动画设计时项目的引用
- 将 .animatedSprite 文件添加到内容项目
- 将图像资源添加到内容项目
在游戏项目中,还需要做一些事情。我需要添加对包含运行时信息的项目的引用。我将 ContentTypeReader
和精灵的类声明都放在同一个项目中,尽管这不是强制性的。一旦完成,我就可以加载精灵动画、图像并开始显示它们。请记住,您必须创建 AnimatedSpriteInstance
的实例才能在屏幕上显示内容。一旦创建了精灵实例,我需要调用的唯一方法是 Update
和 Draw
,以使其保持动画。该类将自行跟踪所有其他内容。
private AnimatedSprite _animatedSprite;
private Texture2D _spriteTexture;
private AnimatedSpriteInstance _spriteInstance;
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
//Load assets
_spriteTexture = Content.Load<Texture2D>("allies");
_animatedSprite = Content.Load<AnimatedSprite>("MyAnimatedSprite");
SpriteFont sf = Content.Load<SpriteFont>("DebugFont");
//Give the animatedSprite object its sprite sheet
_animatedSprite.SetTexture(_spriteTexture);
//Create an instance of the animation for display
_spriteInstance = _animatedSprite.CreateInstance();
}
protected override void Update(GameTime gameTime)
{
_spriteInstance.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// TODO: Add your drawing code here
spriteBatch.Begin();
_spriteInstance.Draw(spriteBatch);
spriteBatch.End();
base.Draw(gameTime);
}
当动画具有多个视觉状态时,我可以通过设置实例上的 StateName
值来在状态之间切换。
_spriteInstance.StateName="NewState";
在其他平台上运行
我之前提到过,您需要为打算运行的每个平台制作一个运行时代码版本。刚开始时,我只在 PC 上运行这段代码。让它在 Windows Phone 或 Xbox 上运行几乎不费力气。首先,让我们看看项目布局。
我将游戏作为一个 Windows XNA 项目、游戏的内容项目、一个游戏类库(包含运行时类)和内容扩展项目(包含设计时组件)。在这些项目中,只有 Windows XNA 游戏项目和类库需要转换。其他项目将保持不变。右键单击游戏项目并选择将项目复制到 Xbox 360 或 Windows Phone 的选项。
游戏及其所依赖的类库都将被复制到 Windows Phone 或 Xbox 项目中。也许我应该使用“复制”这个词,因为这两个项目引用的是相同的源文件。如果您在一个项目中更改了文件,由于文件是共享的,您也会在另一个项目中看到更改。当您尝试编译项目时,它会失败,指出找不到 System.Xml.Serialization.XmlSerialization
的类定义。在 Windows Phone 和 Xbox 上,这个类存在于它自己的程序集中,而在 PC 上,它存在于 System.Xml.dll 程序集中。因此,您需要在运行时项目和游戏项目中都添加对 System.Xml.Serialization.dll 程序集的引用。
未来改进
我写这篇是为了概念验证。它还有一些其他功能我想支持,但与其尝试一次性完成所有我想做的事情,我决定最好从小处着手。我的目标之一是在乘车途中完成这项工作,这限制了我能做多少。虽然我不认为时间限制是坏事;这个项目的目标是获得一个最初 令人满意 的东西。在使用我目前所拥有的东西时,我有一些领悟,例如需要支持“调试”精灵。我实现了一个快速解决方案来满足我的需求,以便我可以在任何时间点看到正在使用的帧索引。将来我还需要支持以非自然尺寸绘制精灵以及旋转支持。目前,我将只记录未来的需求,并对这段代码进行足够的修改,以满足我目前正在开发的游戏的需求(我不想被范围蔓延分散注意力)。我还添加了一个 Scale
属性来放大图像,尽管代码中的实现不是我希望在最终代码中拥有的实现。