最后宣传下我的工作室:Http://gamekingdom.us
第 4 章XNA里的2D动画
4.1 播放游戏精灵动画
第二章里我们提到了如何把2D的纹理用SpriteBatch显示。对于游戏而言,一个精灵(sprite,在游戏编程里指一个对象的原型,比如一个战士,一个怪物)不可能不做动作,就拿简单的走动来说,手脚一定会是要动的,那么如何真实得展现一个人物在屏幕上走动呢?在2D游戏编程里,我们通常就是把人物的动作拆成一帧帧图片,通过连续播放来欺骗人眼产生动画的效果,其实胶片电影也是这个原理。
如下图4-1,是一个小兵的行走动画,可以看到是由12张图片组成,那么如何能连续循环播放呢
图4-1
一想到循环,我们就很容易想到for循环,如果我们把这12张图片的纹理放到一个Texture2D[] 数组里那么,通过for循环不就可以实现了么?
for(int i=0;i<SoldierTextures.Count;i++) //伪代码
{
spriteBatch.Draw(SoldierTextures[i]);
}
当然,这个循环只能循环一遍,如果做到从头到尾不停的循环呢?
前面我们提到XNA里的Update函数,它就是不停执行的一个函数,执行的时间间隔是一个固定值.我们就可以把这个函数当做是一个特殊的for循环.
我们需要用上一个全局的计数器FrameCount.
int FrameCount=0;
void Update() //在update函数里改变计数器
{
FrameCount++;
if(FrameCount>SoilderTextures.Length-1)//如果播完最后一帧
{
FrameCount=0; //就回到第一帧
}
}
void Draw() //在Draw函数里绘制出纹理
{
spriteBatch.Draw(SoilderTextures[FrameCount]);
}
以上代码都是伪代码,便于大家理解,实际编程中的代码比这里要复杂点。
1.在GameMainScreen类里写上构造函数:
Texture2D[] soilderTextures;
public GameMainScreen()
{
soilderTextures=new Texture2D[12];//初始化士兵纹理数组
}
2.把12张士兵跑动图片放到Content项目下的一个Enemy文件夹的子文件夹Run中,如果4-2:
3.用for循环加载这12张图片纹理,如下:
public override void LoadContent()
{
base.LoadContent();
playerTexture = ScreenManager.Game.Content.Load<Texture2D>("Player/1");
for (int i = 0; i < 12;i++ )
{
soilderTextures[i] =ScreenManager.Game.Content.Load<Texture2D>("Enemy/Run/"+(i+1));
}
}
4.在Draw里绘制出当前帧:
public override void Draw(GameTime gameTime)
{
ScreenManager.SpriteBatch.Begin();
ScreenManager.SpriteBatch.Draw(soilderTextures[FrameCount],new Vector2(100,200), Color.White);
ScreenManager.SpriteBatch.End();
}
图4-2
5.在Update里更新当前帧的位置,依次向后播放:
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
base.Update(gameTime, otherScreenHasFocus,coveredByOtherScreen);
float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
FrameCount++;
if(FrameCount > soilderTextures.Length - 1)//如果播完最后一帧
{
FrameCount = 0; //就回到第一帧
}
}
6.在模拟器最后运行效果如图4-3:
我们执行上面写好的代码后,就在模拟器里发现人物就循环播放动画了,不过出现了新问题:士兵的跑动动作频率不够合理,像一个超人一样飞速得在奔跑。
这是为什么呢?
因为人物动画帧播放的时间间隔其实是要远大于update函数执行的时间间隔的,比如说人物动画是1秒播放一帧,而update函数是1/30秒(0.3333秒)播放一帧。那么我们如何处理这个问题呢?
在回到上面的代码里,其实只要控制 FrameCount++执行的时间间隔就可以了。如何控制?那么我们需要用到另外一个计数器updateCount;
updateCount++;
if(updateCount>TimeSpan) //伪代码,TimeSpan为时间间隔量
{
FrameCount++;
updateCount=0;
}
这样我们通过修改TimeSpan的值就能控制FrameCount++执行的时间间隔了。比如TimeSpan=30, 那么update函数要执行30次,FrameCount++才执行一次。也就说FrameCount++执行的时间间隔为 30*0.33333秒=1秒。
经过合理调整,我们发现TimeSpan=2时,士兵的动作频率最协调,代码如下:
int updateCount=0,timeSpan=2;
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
base.Update(gameTime,otherScreenHasFocus, coveredByOtherScreen);
float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
updateCount++;
if(updateCount > timeSpan) // timeSpan为时间间隔量
{
FrameCount++;
updateCount = 0;
}
if(FrameCount > soilderTextures.Length - 1)//如果播完最后一帧
{
FrameCount = 0; //就回到第一帧
}
}
写完以上代码,我们就能看到精灵动画能够自如的播放了。不过这个小兵精灵并不能移动,所以有些别扭。那么我们下面一节就要研究如何让精灵移动起来。
4.2游戏精灵的矢量移动
我们在前面第二章讲过可以指定得把一个2D纹理绘制在特定的坐标位置上。在WP7的XNA里,坐标系如下,原点的位置是左上角,X轴向右延伸,Y轴向下延伸。
在XNA里,2维坐标用Vector2对象来表示:
Vector2 position = new Vector2(X,Y);
了解了XNA里2维坐标概念后,我们还需要知道一个概念就是运动矢量,什么叫运动矢量呢?比如我向右走,也就是笔直沿X轴增大的方向走,这样我行走的方向就确定了,但是我行走的时候可快可慢,这就涉及到速度的问题。运动矢量就包含了方向和速度两个概念。在2维坐标系里,运动矢量也用Vector2 对象来表示。我用20单位的速度向右走就可以表示为 new Vector2(20,0); 很容易理解此时在X轴方向上以20单位的速度在增大(向右运动),在Y轴上没有变化。
那么随着时间流逝,我们就能得到运动中的精灵在当前时刻所在坐标位置,用矢量计算公式:
Vector2 speed = new Vector2(20,0);
Vector2 EndPosition = position + speed*elipsetime;
计算出EndPosition后,我们就在Draw方法里
spriteBatch.Draw(Texture2D,EndPosition);
这样我们的精灵就能移动了,改变speed运动矢量,我们就能控制精灵的运动速度和方向。当然,要让精灵停下来也很简单,speed = new Vector2(0,0)就可以了。
改进后的代码如下:
int FrameCount= 0;
Vector2 endPosition = new Vector2(0,200);
public override void Draw(GameTime gameTime)
{
float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
float totalTime = (float)gameTime.TotalGameTime.TotalSeconds;
ScreenManager.SpriteBatch.Begin();
ScreenManager.SpriteBatch.Draw(soilderTextures[FrameCount], endPosition,Color.White);
ScreenManager.SpriteBatch.End();
}
int updateCount=0,timeSpan=2;
Vector2 speed=new Vector2(100,0);
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
base.Update(gameTime,otherScreenHasFocus, coveredByOtherScreen);
float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
updateCount++;
if(updateCount > timeSpan) //TimeSpan为时间间隔量
{
FrameCount++;
updateCount = 0;
}
if(FrameCount > soilderTextures.Length - 1)//如果播完最后一帧
{
FrameCount = 0; //就回到第一帧
}
endPosition += elapsedTime * speed;
if(endPosition.X>800)
{
endPosition = new Vector2(0,200);
}
}
按F5运行模拟器后,我们会看到一个不停奔跑的士兵。
4.3制作可控的游戏精灵
在手机游戏里,主角一般都是方向可控的,如下图4-4是一个带十字方向键的iphone游戏界面:
图4-4
结合我们第3章讲的WP7的触控操作,我们也能实现十字方向键控制的游戏精灵。
1. 首先找到一个十字键图片,把它绘制到手机左下角,如图4-5:
图4-5
2. 设定4个方向点的矩形范围。
Rectangle topRect = newRectangle
{
Location = newPoint { X = 40, Y = 380 },
Width = 30,
Height = 30,
};
Rectangle bottomRect = new Rectangle
{
Location = new Point { X = 40, Y = 460 },
Width = 30,
Height = 30,
};
Rectangle leftRect = new Rectangle
{
Location = new Point { X = 2, Y = 410 },
Width = 30,
Height = 30,
};
Rectangle rightRect = new Rectangle
{
Location = new Point { X = 80, Y = 410 },
Width = 30,
Height = 30,
};
2. 重写HandleInput函数,接受触控操作:
public override void HandleInput(InputHelperinput)
{
TouchCollection touchState = TouchPanel.GetState();
foreach(TouchLocation tl in touchState)
{
if(topRect.Contains(new Point{ X = (int)tl.Position.X, Y = (int)tl.Position.Y }) && tl.State == TouchLocationState.Pressed)
{
speed = new Vector2(0,-100);
}
if(bottomRect.Contains(new Point { X = (int)tl.Position.X,Y = (int)tl.Position.Y }) &&tl.State==TouchLocationState.Pressed)
{
speed = new Vector2(0,100);
}
if(leftRect.Contains(new Point{ X = (int)tl.Position.X, Y = (int)tl.Position.Y }) && tl.State == TouchLocationState.Pressed)
{
speed = new Vector2(-100,0);
}
if(rightRect.Contains(new Point { X = (int)tl.Position.X,Y = (int)tl.Position.Y }) && tl.State== TouchLocationState.Pressed)
{
speed = new Vector2(100,0);
}
if(tl.State==TouchLocationState.Released)
{
speed = new Vector2(0,0);
}
}
}
3. 写好以上代码后,按F5调试,在游戏主界面,我们就看到如图4-6的效果:
图4-6
当然这只是个简单的DEMO,士兵后退,向上,向下的动画都不对,不过原理和士兵前进的动画一样。更复杂的动画,我们会在下一章来讲解。
准备写一系列XNA里设计2D游戏的文章,这是第一章。
1.XNA里的hello world
1.1创建XNA游戏开发项目
我们初学程序的时候,总是爱写一个hello world 来体验下。可以说写出hello world 是我们了解一个语言或者框架最简单的做法。那么我们如何在wp7(windows phone 7)里用xna游戏框架在屏幕输出hello world呢。
其实微软的开发工具对开发者而言是很友好的。我们打开vs2010 Express for Windows Phone,选择File->New Project菜单,新建项目XNAGameSample,如图1-1:

图1-1
于是我们看到如下图1-2这样的解决方案:

XNAGameSample和XNAGameSampleContent.
在我们项目储存的硬盘上我们可以看到图1-3这样的目录结构:

图1-3
其中XNAGameSampleContent项目是我们用来管理游戏资源文件的项目,我们以后用到贴图,模型,声音等资源都会放在这里管理。
XNAGameSample是我们的主项目。
在XNAGameSample项目里我们开打game1.cs文件,这是游戏运行的主入口,如图1-4:

图1-4
在Game1.cs里面有Initialize,LoadContent,UnloadContent,Update,Draw五个方法。
那么这五个方法各自在游戏里起什么作用呢?在后面的章节里我们会了解到这五个方法各自的用途。
1.2 update 和draw
从函数命名上,我们可以猜测得到LoadContent是负责加载游戏资源的。Draw 是用来绘制游戏界面的。那么Update呢,从字面意思上我们可以看到是”更新”的意思,那么这个方法里更新什么呢?
举一个简单的游戏场景来说。如果我们绘制一个手拿盾牌的小兵,让他从手机屏幕的左边跑到右边,那么我们怎么做呢。
首先,我们会用LoadContent来加载这个小兵所使用到的所有贴图资源。然后用Draw 方法把贴图绘制到屏幕制定的位置。但是我们要求的是小兵是移动的,从左边跑到右边,在跑的过程中还涉及到帧动画的变化。那么这些问题我们都需要在Update里来处理。简单来说,我们需要在Update里改变小兵的坐标位置和变换所用到的贴图资源。
我们需要了解的是Update 和Draw 在游戏运行过程中是按一定时间间隔不停被调用的方法,这样我们才能看到连续的动画。用流程图我们可以这样来描述:

我们在WP7上写XNA游戏的时候,需要了解一个很重要的概念,就是刷新率是多少?通俗的说,1秒钟Draw和Update 被调用多少次。
只有弄清楚这个问题,我们才能控制2D游戏动画的播放速度。WP7里xna的刷新率默认为30fps.也就是说1秒钟Draw 和Update 要被调用30次。如图1-4,我们展开Game1函数,可以看到如下代码:
public Game1()
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
其中有一段英文注释是"Frame rate is 30 fps by default for Windows Phone.",意思是Windows Phone 的默认FPS是30.
1.3 LoadContent 和 UnloadContent
在1.2章节里我们已经简单介绍了LoadContent和UnloadContent的作用,如果我们需要在WP7里输出一段文字,我们就需要用到LoadContent了,在XNA里我们要经常接触到SpriteFont对象,这个对象是负责控制输出字体的,在XNA里字体的样式我们需要用一个XML文件来控制。如图1-5,我们在XNAGameSampleContent项目里创建一个新的文件夹“Fonts”,然后我们在XNAGameSampleContent项目里新建的Fonts文件夹上点击右键选择Add,然后选择New Item,再选择sprite font ,把Name改成default.spritefont,最后点击 add。

图1-5
然后,如图1-6,我们修改default.spritefont的内容为:

图1-6
做好了上面的准备工作,我们在Game1.cs里增加一个新的对象font,然后在LoadContent函数里添加如下代码:
如图1-7:
图1-7
接着在Draw函数里写下如下代码:
spriteBatch.DrawString(font, "Hello world", new Vector2(200,100), Color.Black);
spriteBatch.End();
如图1-8:

图1-8
然后我们只需要在工具条的最右边选择"Window Phone 7 Emulator"按下F5,在模拟器模式下就能运行这个小小的demo了,如图1-9:

图1-9
那么我们怎么要在真机上调试呢?在下一章节我们就会讲到。
1.4调试和部署XNA项目
首先,我们需要装上Zune,我们的wp7手机也需要解锁。解锁需要我们拥有WP7开发者账号。注册地址:http://create.msdn.com/。如图1-10:
Tips:如果你有.edu的学生邮箱,又通过dreamSpark认证,就可以免去$99的年费。

图1-10
准备好这些后,我们用数据线连上WP7真机,如果WP7被识别,会自动启动Zune。如图1-11:

图1-11
然后我们在vs2010 Express for Windows Phone的工具条最右边选择"Windows Phone 7 Device"。再按F5。等几秒钟后,我们的项目编译后产生的XAP文件就被部署到真机里运行了。
demo源代码下载地址:/Files/wangergo/XNAGameSample1.rar

