【51CTO译文】引言
到底能多快地为运行Windows Phone 7的现代化手机开发一款功能完备的游戏?为此你需要具备什么条件?又会面临什么样的难题?
我们将在开发小游戏期间力图解答上述问题。为了营造一种实际的环境,这个功能完备的应用程序开发后将提交到Windows Phone Marketplace。
我们将审查游戏开发过程的整个周期,从游戏构思开始,到游戏发布到Marketplace结束,这个过程的任何细节都不会遗漏。
游戏构思
我们不会仿效其他游戏。我们考虑的是开发一款新颖、有趣、绚丽的游戏——是专门为使用方向感应器和触摸屏的手机而开发的。你只要将手机往不同的侧边倾斜,就可以在方向感应器的帮助下操纵小球,好像小球就在方形盒子里面滚动。
画面上有多个彩球。它们试图撞到白球,给白球着上自己的颜色。
用户只要用指尖点一下彩球,就可以击碎彩球。你点一下球后,就会变成色彩鲜艳的飞溅物,然后会消失,之后新的球会出现。每击碎一个球,就能得到更高的分数。用户能玩的关卡数量没有限制。
玩家的目标应该是得到尽可能高的分数。
为了让玩家可以使用方向感应器(而不仅仅击碎球),为游戏添加了更多的球(黑球)。除非黑球被着上别的颜色,否则不会被击碎。
2D图形对这款游戏来说绰绰有余。此外,需要播放音乐,与方向感应器和触摸屏进行交互。
开发环境
为了针对Windows Phone 7开发应用程序,我们需要Visual C# 2010简易版和Windows Phone开发者工具(Windows Phone Developer Tools)。两者都是免费产品。
然后进入到微软网站,下载和安装下列工具:
Windows Phone开发者工具无法安装到Windows XP上,不过你可以采取这个办法。然而,手机模拟器可能无法在Windows XP上运行,因而***还是安装Windows 7。
如果你已经拥有Visual Studio 2010,***在上面安装Windows Phone开发者工具,因为Visual C# 2010 简易版用起来明显更不方便。
技术选择:XNA对决Silverlight
安装了Windows Phone开发者工具后,可以选择用Studio来创建新的项目类型:Windows Phone应用程序(Windows Phone Application)和Windows Phone游戏(Windows Phone Game)。
哪一个更适合我们呢?
***个项目是面向Windows Phone 7的Silverlight应用程序。相应地,它可以访问几乎所有的类,这些类在通常的Silverlight中得到表示。它有XAML和控件以及Studio中的设计器等。
第二个项目是XNA 4.0.应用程序。它没有任何控件;不过,它有游戏周期,可以快速访问硬件图形的功能特性。
从名称和技术描述来判断,我们需要的是XNA。Silverlight有没有可能也适合开发游戏?用Silverlight for Windows Phone简单测试一下,就会发现这项技术的局限性:数百个同时运动的小图像让手机进入幻灯片模式。如果我们用XNA进行同样的测试,一切都快速运行(拥有数量更多的图像)。
这就是为什么我们用XNA来开发游戏。遗憾的是,没有控件;但目前看来控件也没什么用处。
上手XNA
我们不妨用Studio来创建Windows Phone游戏项目。该项目与游戏类一同出现:
public class Game1 : Microsoft.Xna.Framework.Game
不妨定义用户在玩游戏期间如何握持手机。假设一只手握持手机(旋转手机即可操纵白球)、另一只手点彩球更为舒适。这意味着,我们需要纵向游戏模式。我们为游戏构建器添加了这种模式。
- public Game1()
- {
- graphics = new GraphicsDeviceManager(this);
- Content.RootDirectory = "Content";
- graphics.SupportedOrientations = DisplayOrientation.Portrait;
- graphics.PreferredBackBufferWidth = 480;
- graphics.PreferredBackBufferHeight = 800;
- graphics.IsFullScreen = true;
- }
然后在这个类中,我们最感兴趣的是两项功能:Update(更新)和Draw(绘制)。
- protected override void Update(GameTime gameTime)
- {
- // Allows the game to exit(允许游戏退出)
- if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
- this.Exit();
- // TODO: Add your update logic here(TODO:在这里添加更新逻辑)
- base.Update(gameTime);
- }
- protected override void Draw(GameTime gameTime)
- {
- GraphicsDevice.Clear(Color.CornflowerBlue);
- // TODO: Add your drawing code here(TODO:在这里添加绘制代码)
- base.Draw(gameTime);
- }
***项功能专门用于游戏逻辑处理。我们会得到用户动作(从方向感应器到触摸屏)方面的数据,并算出所有游戏物体的新位置。
第二项功能专门用于绘制屏幕上的游戏物体。
有必要说明:这两项功能执行的总时间不该超过游戏周期的一次迭代。这在手机上相当于33毫秒,因为游戏周期的频率是每秒30帧。如果功能执行的时间超过这个值,游戏速度就会很慢,一些帧就会被遗漏,或者更糟糕的是,游戏会忽略用户动作。
这个游戏实际上是实时应用程序。它对垃圾收集器(Garbage Collector)的动作会有怎样的反应?***是避免垃圾收集器突然干预的情况,那样我们可以顺利地处理物体,不用在每次迭代时创建成千上万物体。一般来说,我们不需要经常创建物体,因为游戏世界的所有物体相对来说历时长久。我们会使用已有物体的结构或链接作为特征参数。这样一来,垃圾收集器不会阻止我们做所需的动作。
我们不妨试着编译和启动应用程程序。在几台电脑上,你可能会看到表明XNA无法在模拟器上启动的消息。这样一来,你可能要创建Windows游戏项目,并附加来自Windows Phone游戏项目的文件(Add As Link,即添加为链接)。同样这些XNA代码可以在手机和Windows(以及XBOX 360)上运行。
***,有一个窗口填满了紫色。这是由于GraphicsDevice.Clear(Color.CornflowerBlue)功能执行的结果。
游戏类中有一个SpriteBatch对象类。我们在绘制时需要这个对象。它含有处理2D图形和文本的所有必要方法。
不过我们仍缺少最重要的方面:游戏内容。#p#
游戏内容
游戏内容是游戏必不可少的不同资源,比如纹理(用于绘制的图像)、字体(输出文本时需要)和声音。
我们在创建Windows Phone游戏项目的同时,还创建了名称是“Content”的另一个项目。我们应该为这个项目添加图像和声音文件。
此外,我们应该设定用于这个项目中文本输出的所有字体。
在Content.Load(assetName)函数的帮助下,将内容装入到程序很简单。游戏类中拥有Content属性。我们连同内容添加到项目中的资源名称应该设成assetName。通常来说,这仅仅是没有扩展名的文件名。要使用的内容类型(Texture2D、SpriteFont或SoundEffect)应该设成T。
内容装入不是一种快速操作,这就是为什么我们在游戏开始时会装入所有内容,以便需要时可以使用。
现在一切已准备好,我们可以开始开发游戏了。
游戏逻辑
所有游戏逻辑都可以放入到GameController这个单独的类中。现在,游戏类中的Update功能看起来如同这样:
- protected override void Update(GameTime gameTime)
- {
- // Allows the game to exit(允许游戏退出)
- if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
- this.Exit();
- GameController.Update(gameTime);
- base.Update(gameTime);
- }
注意:我们把GameTime类的对象发送到GameController.Update。它拥有ElapsedGameTime属性,该发展表明自上一次调用Update功能上以来过去了多长时间。这个时间通常是固定的,相当于手机上的33毫秒。不过,***不要依靠它,而是在所有计算中都使用发送的值。
我们在需要计算游戏世界物体的运动时,将使用ElapsedGameTime。比如说,如果我们需要计算白球的新位置(受手机倾斜的影响),它看起来会这样:
position += (float)(acceleration * gameTime.ElapsedGameTime.TotalSeconds);
小球和飞溅物(它们一出现)的半径按这种方式来计算:
radius += (float)(GrowSpeed * gameTime.ElapsedGameTime.TotalSeconds);
所有计算会用不同的类来进行。每个类都有各自相似的Update(gameTime)功能。GameController.Update会调用它们。#p#
绘制图形
绘制图形发送到GameController类。游戏类中的Draw功能现在看起来这样:
- protected override void Draw(GameTime gameTime)
- {
- GraphicsDevice.Clear(Color.Black);
- spriteBatch.Begin();
- GameController.Draw(spriteBatch);
- spriteBatch.End();
- base.Draw(gameTime);
- }
注意:我们只将SpriteBatch对象类发送到GameController.Draw。它只用于绘制。我们这里不需要GameTime,因为所有必要的计算已经在Update功能中进行。
随后,GameController.Draw会从得到游戏世界逻辑的类调用相似的Draw(spriteBatch)功能。
绘制本身看起来相当简单:
spriteBatch.Draw(texture, position, color);
这时我们只能显示准备好的纹理。绘制2D基本图形(直线、长方形和椭圆形等)的功能并不由XNA来提供。
可以通过从内容装入纹理来得到纹理:
Texture2D texture = Content.Load
纹理已经被装入了;所有位置已被计算出来;不过,我们可以“调整”颜色!如果我们需要原始纹理颜色,就要有Color.White。
如果我们需要另一种颜色,应该用这种颜色布局来绘制纹理。如果我们要用阿尔法通道(alpha channel)来设定颜色,那么就能透明地绘制纹理。那样,我们就能轻松自如地显现和隐匿物体以及颜色变化(这些正是我们在游戏中需要的)。
为了混合两种颜色,我们使用Color.Lerp函数。为了增加阿尔法通道,应使用Color.FromNonPremultiplied 函数。
另外还有spriteBatch.Draw函数的其他变种,允许变化和扩展文本,这也是我们的游戏所需要的。
文本输出
文本输出就跟图形输出一样简单:
spriteBatch.DrawString(spriteFont, text, position, color);
你可以通过从内容装入SpriteFont类的对象来得到对象:
SpriteFont spriteFont = Content.Load
你还可以在这里以同样的方式设置字体颜色。如果你用阿尔法通道设置颜色,文本将是透明的。
如果你用浮动位置设置文本输出的坐标,那么文本在显示时可能会有点失真。这就是为什么在流畅的文本移动没必要时,我们要将坐标舍入到整数。
spriteBatch.DrawString函数有其他变种,允许改变和扩展文本。有必要记住:这种运动会引起文本表述错误。之所以会出现这种情况,是因为XNA并不处理原始向量字体,但能处理栅格表示;项目一编译好,就会创建栅格,随后添加到内容中。#p#
触摸屏
为了定义用户在何处点击屏幕,应该从Microsoft.Xna.Framework.Input.Touch.TouchPanel类得到数据:
- foreach (var item in TouchPanel.GetState())
- {
- if (item.State == TouchLocationState.Pressed
- || item.State == TouchLocationState.Moved)
- {
- // Get item.Position(得到item.Postion)
- }
- }
因此,我们就能得到用户触摸的所有屏幕点。不过,我们需要关于屏幕触摸一次性动作(点击、在屏幕上移动手指时)的数据。这些数据是跟踪屏幕按钮(比如暂停按钮)触摸所必可不少的。为了得到这些数据,应使用手势支持。
在游戏开始时,应该表明我们需要支持手势(点击):
TouchPanel.EnabledGestures = GestureType.Tap;
然后,我们可以在游戏周期的每次迭代得到手势:
- while (TouchPanel.IsGestureAvailable)
- {
- GestureSample gesture = TouchPanel.ReadGesture();
- if (gesture.GestureType == GestureType.Tap)
- {
- // Get guesture.Position(得到guesture.Position)
- }
- }
方向感应器
为了得到关于手机倾斜的数据,就要使用Microsoft.Devices.Sensors.Accelerometer类。遗憾的是,我们无法直接从方向感应器得到数据,因为它只支持事件模式。这就是为什么应该使用创建对象和订阅事件的附属类:
- accelerometer = new Microsoft.Devices.Sensors.Accelerometer();
- accelerometer.ReadingChanged += AccelerometerChanged;
- accelerometer.Start();
在事件处理器中,我们会记住加速值,并保存起来,以便以后使用:
- private void AccelerometerChanged(object sender, AccelerometerReadingEventArgs e)
- {
- vector = new Vector3((float)e.X, (float)e.Y, (float)e.Z);
- }
加速向量含有三个轴(X轴、Y轴和Z轴)的信息,但我们先只需要头两个轴(用于手机的纵向模式)。这就是为什么在我们的坐标系中返回加速的属性看起来这样:
- public Vector2 Acceleration
- {
- get
- {
- return new Vector2(vector.X, -vector.Y);
- }
- }
是的,这个加速会被施加于白球。#p#
播放声音
可以在SoundEffect类的对象中Play函数的帮助下,播放XNA中的声音。这个类的对象可以从内容装入:
SoundEffect sound = Content.Load
只要这么处理,就可以播放声音了。
窗口
许多东西已准备好:白球通过方向感应器来运动,其余球会追白球;一旦撞上,就会变成飞溅物,会计算分数,随后播放声音。看起来像是一切准备就绪?不,没有这么简单。
现在我们需要创建窗口;开始窗口(主菜单)、暂停对话窗口和游戏结束窗口(显示得分数和记录)。
面向Windows Phone的XNA没有窗口,这就是为什么我们得自己创建窗口。不过,实际创建起来比较简单。
用Parent、Children、Bounds、Visible和Enabled这些主要功能以及Update和Draw两个函数创建一个基本控件就够了。然后,应该创建几个子类:Window、Button和Label等。
之后,就很容易在窗口中显示元素。
保存游戏状态
手机上的游戏可以随时暂停,只要点击Home按钮或借助其他任何外部事件。这就是为什么我们要注意随时保存游戏状态,等游戏启动后,可以进一步恢复这个状态。
不妨使用System.IO.IsolatedStorage.IsolatedStorageSettings.ApplicationSettings类来保存状态(及设置)。这个类实现了IDictionary接口。我们不妨把所有游戏世界物体(但愿物体数量不多)的的当前状态概括到这个词典中,并调用IsolatedStorageSettings.ApplicationSettings.Save()函数。
退出游戏时,游戏会保存起来。为此,不妨覆盖游戏类中的OnExiting函数:
- protected override void OnExiting(object sender, EventArgs args)
- {
- GameController.SaveState();
- base.OnExiting(sender, args);
- }
游戏恢复以类似方式进行。应用程序启动时,我们收到来自IsolatedStorageSettings.ApplicationSettings的数据,恢复所有游戏世界物体。
应用程序的激活和停用
我们的这个应用程序并不总是处于活动状态。有时它可能被停用了(比如来电时),需要再次激活。
为了跟踪这些事件,不妨覆盖游戏类中的OnActivated函数和OnDeactivated函数。
在应用程序停用期间,我们让游戏处于暂停模式,那样用户回来后,关于恢复游戏的对话消息会出现。
此外,为了不浪费处于停用状态的手机的计算资源,我们在游戏类中Update函数的开头添加下列代码:
- if (!IsActive)
- {
- SuppressDraw();
- return;
- }
#p#启动画面
我们的游戏(确切地说,是游戏内容)在几秒钟内装入,在这个装入期间只显示黑色屏幕。我们应该向用户表明应用程序在运行中。为此,应该绘制一个漂亮的启动画面(splash screen),在游戏启动时显示:
如果你只是往Silverlight应用程序里面添加针对Windows Phone的SplashScreenImage.jpg文件,启动画面会自动显示。
不过这对XNA项目来说行不通。
我们得改动内容的装入。首先,我们为启动画面做好纹理,然后在***次调用Draw函数的过程中绘制纹理。然后,我们装入其余内容,启动游戏。直到其余内容都装入,启动画面才显示。
现在,这个游戏看起来漂亮多了。
手机中的游戏位置
为了让我们的游戏出现在手机中的Games部分(只要点击手机开始屏幕上的XBOX LIVE按钮,就能访问它),需要编辑项目中的WMAppManifest.xml文件。应该在这个文件中,而不是在Genre="Apps.Normal"这一行中编写Genre="Apps.Games"。
另外我们把游戏名称和描述放入到同一个文件(分别是Title和Description属性)。删除不必要的需求(section)。在这个部分中,我们只留下方向感应器支持。这个版本的游戏不需要其他部分。
该项目应该有两幅图片:GameThumbnail.png和Background.png。
Games部分的游戏显示区需要***幅图片。手机的开始屏幕需要第二幅图片。这两幅图片的大小都应该是173×173像素。
试用模式
由于我们开发的这个游戏将是收费游戏,需要添加试用模式支持。试用功能已经内置到平台中,在Microsoft.Phone.Marketplace.LicenseInformation类的帮助下完成:
- var license = new Microsoft.Phone.Marketplace.LicenseInformation();
- return license.IsTrial();
由于该函数相当慢,我们不会在每次游戏周期迭代时调用它。相反,我们只会在应用程序激活时才调用它(在游戏类的OnActivated函数中,结果将作为变量来保存)。
如果应用程序在试用模式下启动,那么在试用几分钟后,就会出现提供游戏购买(或开始新游戏)的窗口。
点击购买按钮时,我们就会调用、在Windows Phone Marketplace中显示应用程序:
- var market = new Microsoft.Phone.Tasks.MarketplaceDetailTask();
- market.Show();
购买后,用户可以回到应用程序,继续玩游戏。
后退按钮
一切几乎已准备好,我们要把应用程序发送到Windows Phone Marketplace。在发布之前,该应用程序经过微软的认真测试;几天后,微软给出了结论:拒绝接受。原因出在哪里?
问题出在我们在Update函数中留下的代码。
- protected override void Update(GameTime gameTime)
- {
- // Allows the game to exit(允许游戏退出)
- if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
- this.Exit();
- ...
- }
后退(Back)这个硬件按钮必须以某种方式来工作,也就是让用户回到前一个屏幕。如果用户在应用程序的开始屏幕(主游戏菜单)上,可以使用该按钮来退出应用程序。如果处在暂停模式下,该按钮可以让用户回到游戏中。
在我们这个情况下,后退按钮总是调用应用程序退出。不妨清除这个代码,让后退按钮能够正确工作,再次发送给微软进行审查。重复审查所用时间比较少。#p#
结果
是的,这个应用程序得到了接受!现在,该游戏出现在Windows Phone Marketplace中了。
我们在游戏开发上总共花了两周时间(发布到Marketplace又花了两周时间)。***,我们拥有了一个漂亮而诱人的游戏:
***WP7游戏网站的编辑人员评论了这款游戏。
提交一周后,这个游戏就跻身于Windows Phone Marketplace***的100款收费游戏中。
我们从这次游戏开发中能够得到什么样的结论?答案是:为Windows Phone 7开发游戏其实简单得很!
51CTO观点
诺基亚日前与微软正式签订Windows Phone 7合作协议,并放弃Symbian系统的研发业务,这足以表明Windows Phone操作系统将成为诺基亚主要的智能手机平台。诺基亚与微软的强强联手将会给开发者带来良好的开发环境,学习Windows Phone平台对开发者来说会是个不错的选择。
【51CTO.com独家特稿,非经授权谢绝转载,合作媒体转载请注明原文作者及出处!】
【编辑推荐】
- 微软发布WP软件包 吸引iOS开发人员
- 如何在Windows Phone 7 3D开发中使用纹理贴图
- 使用IronRuby开发Windows Phone 7应用程序
- 独立开发者分享手机游戏开发经验
- SocialTimes:手机社交游戏开发秘籍