Windows Phone 版 Pirates!






4.92/5 (52投票s)
一款受 Angry Birds 启发的 Windows Phone 2D 物理游戏。
目录
引言
你好,海盗! 在本文中,我们将介绍一款 Windows Phone 游戏(或者至少是游戏的开端……),并希望最终能带来很多乐趣。 但在乐趣的背后,是构建它所需的基本技术。 我将分步介绍,并希望在文章结束时引起你的兴趣。
Windows Phone 版《海盗!》是一款使用 C# 和 XNA、并结合 Farseer 物理引擎开发的游戏。 该游戏的设计很大程度上受到了 Rovio 的《愤怒的小鸟》游戏的启发,并向其致敬,该游戏在 2011 年底已达到 5 亿次下载量。《愤怒的小鸟》可能让全世界许多人都沉迷其中,但我本人并不沉迷于游玩本身,而是沉迷于追求构建类似游戏所需的那些零碎技术。
用一门装满炮弹的大炮代替小鸟。 用一艘装满海盗的大型海盗船代替猪。 你的任务是瞄准大炮,摧毁所有海盗。(免责声明:这个故事与当前的“SOPA”和“PIPA”无关……)
这里有一个简短的视频,让你一窥我们正在讨论的内容。
本文旨在展示如何使用这种新的加速计模拟功能,并附带一个简单的应用程序来使用它。
系统要求
要使用本文提供的 Windows Phone 版《海盗!》,您必须直接从 Microsoft 下载并安装以下 100% 免费的开发工具。
无论您是熟悉还是初次接触 Silverlight 和 XNA Game Studio 编程,Visual Studio 2010 Express for Windows Phone 都提供了您开始构建 Windows Phone 应用程序所需的一切。
Windows Phone 软件开发工具包 (SDK) 7.1 为您提供了开发 Windows Phone 7.0 和 Windows Phone 7.5 设备上的应用程序和游戏所需的所有工具。
Farseer 物理引擎
Farseer 是一个出色的开源物理引擎,它基于最初的 Box 2D 开源项目(顺便说一句,《愤怒的小鸟》游戏使用了 Box2D)。 两者的区别在于,Box2D 是用C++编写的(并且已被移植到多种语言),而 Farseer 是用C#编写的,面向 Silverlight 和 XNA。 此外,Farseer 网站声称该框架具有比原始 Box2D 版本更多的功能。
将游戏调整为每秒 60 帧
为了在您的设备上达到 WP Mango 提供的最高每秒帧数(约 60 帧/秒),您需要像这样更改下载的 Farseer 源代码。
- 双击“Samples XNA WP7”解决方案。 选择“Upgrade Windows Phone Projects...”将所有项目升级到最新的 Windows Phone 7.5 Mango。
- 在游戏类的构造函数中,添加此事件处理程序。
_graphics.PreparingDeviceSettings += new EventHandler<PreparingDeviceSettingsEventArgs>(_graphics_PreparingDeviceSettings);
- 然后添加此事件。
void _graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e) { e.GraphicsDeviceInformation.PresentationParameters.PresentationInterval = PresentInterval.One; }
- 在游戏类的 Initialize 方法中,确保将 PresentationInterval 参数修改为 PresentationInterval.One。
protected override void Initialize() { base.Initialize(); this.GraphicsDevice.PresentationParameters.PresentationInterval = Microsoft.Xna.Framework.Graphics.PresentInterval.One; ...
幸运的是,我已经将本文的源代码更新为 Farseer 物理引擎的编译版本,该版本符合最新的 Windows Phone 7.5 (mango),并以最佳帧率运行。 这对我以及对使用 Farseer 引擎进行游戏开发感兴趣的读者来说都很有用。
将图像转换为物理对象
游戏中的所有动态对象都是从纹理图像创建的。 这是 Farseer 引擎的一项非常好的功能,它使我们的工作变得更加轻松。 Farseer 内置函数可以从我们的迷宫平面图像创建实体对象,前提是我们将空白区域保留为透明颜色,如下所示。
Farseer 使用 BayazitDecomposer.ConvexPartition
方法,该方法可以从一个大的凹多边形创建出较小的凸多边形。
TextureBody textureBody;
//load texture that will represent the physics body
var physicTexture = ScreenManager.Content.Load<Texture2D>(string.Format("Samples/{0}", physicTextureName));
//Create an array to hold the data from the texture
uint[] data = new uint[physicTexture.Width * physicTexture.Height];
//Transfer the texture data to the array
physicTexture.GetData(data);
//Find the vertices that makes up the outline of the shape in the texture
Vertices textureVertices = PolygonTools.CreatePolygon(data, physicTexture.Width, false);
//The tool return vertices as they were found in the texture.
//We need to find the real center (centroid) of the vertices for 2 reasons:
//1. To translate the vertices so the polygon is centered around the centroid.
Vector2 centroid = -textureVertices.GetCentroid() + bodyOrigin - centerScreen;
textureVertices.Translate(ref centroid);
//2. To draw the texture the correct place.
var origin = -centroid;
//We simplify the vertices found in the texture.
textureVertices = SimplifyTools.ReduceByDistance(textureVertices, 4f);
//Since it is a concave polygon, we need to partition it into several smaller convex polygons
List<Vertices> list = BayazitDecomposer.ConvexPartition(textureVertices);
//Adjust the scale of the object for WP7's lower resolution
_scale = 1f;
//scale the vertices from graphics space to sim space
Vector2 vertScale = new Vector2(ConvertUnits.ToSimUnits(1)) * _scale;
foreach (Vertices vertices in list)
{
vertices.Scale(ref vertScale);
}
处理输入循环
处理输入循环 (Handle Input Loop) 是由 Farseer 引擎提供的,它让我们有机会检测和处理用户的各种手势。
此方法负责
- 检测左
摇杆 移动并将其转化为大炮的移动。 - 检测“A”右侧按钮并射击大炮。
public override void HandleInput(InputHelper input, GameTime gameTime)
{
var cannonCenter = new Vector2(0, 0);
var cannonLength = 10f;
var leftX = input.VirtualState.ThumbSticks.Left.X;
var leftY = input.VirtualState.ThumbSticks.Left.Y;
var cos = -leftX;
var sin = leftY;
var newBallPosition = cannonCenter + new Vector2(cos * cannonLength, sin * cannonLength);
if (leftX < 0)
{
lastThumbSticksLeft.X = leftX;
lastThumbSticksLeft.Y = leftY;
}
if (leftX != 0 || leftY != 0)
{
var newAngle = cannonAngle + leftY / gameTime.ElapsedGameTime.Milliseconds;
if (newAngle < GamePredefinitions.MaxCannonAngle &&
newAngle > GamePredefinitions.MinCannonAngle)
{
cannonAngle = newAngle;
}
}
if (input.VirtualState.IsButtonDown(Buttons.A))
{
cannonBall.ResetHitCount();
smokeTracePositions.Clear();
VibrateController.Default.Start(TimeSpan.FromMilliseconds(20));
cannonBall.Body.AngularVelocity = 0;
cannonBall.Body.LinearVelocity = new Vector2(0, 0);
cannonBall.Body.SetTransform(new Vector2(0, 0), 0);
cannonBall.Body.ApplyLinearImpulse(new Vector2(GamePredefinitions.Impulse * (float)System.Math.Cos(cannonAngle),
GamePredefinitions.Impulse * (float)System.Math.Sin(cannonAngle)));
PlaySound("Audio/cannon.wav");
}
base.HandleInput(input, gameTime);
}
更新循环
在 XNA 框架中,当游戏确定需要处理游戏逻辑时,就会调用更新循环 (Update Loop)。 这可能包括游戏状态的管理、用户输入的处理或模拟数据的更新。 通过重写此方法,我们可以为我们的《海盗》游戏添加特定的逻辑。
在 XNA 中,
方法中计算/更新位置。
- 类的目的是
- 计算新的云层位置。有 3 层云,每层云以自己的速度移动。
- 在 4 种现有海浪纹理之间交替显示海浪外观。
- 根据游戏状态转换移动相机(即,一开始游戏显示海盗船,然后相机移动到您的船)。
- 沿着炮弹轨迹更新相机位置。 这有助于跟踪游戏中的动作。
- 在需要时控制相机缩放。
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
if (cannonBall.Body.LinearVelocity == Vector2.Zero)
{
cannonBall.Body.SetTransform(Vector2.Zero, 0f);
}
switch (seaStep)
{
case 0:
seaTexture = sea1Texture;
break;
case 1:
seaTexture = sea2Texture;
break;
case 2:
seaTexture = sea3Texture;
break;
case 3:
seaTexture = sea4Texture;
break;
}
lastSeaStepTime = lastSeaStepTime.Add(gameTime.ElapsedGameTime);
if (lastSeaStepTime.TotalSeconds > 1)
{
lastSeaStepTime = TimeSpan.Zero;
seaStep++;
if (seaStep == 4)
seaStep = 0;
}
lastCloudStep1Time = lastCloudStep1Time.Add(gameTime.ElapsedGameTime);
lastCloudStep2Time = lastCloudStep2Time.Add(gameTime.ElapsedGameTime);
lastCloudStep3Time = lastCloudStep3Time.Add(gameTime.ElapsedGameTime);
if (lastCloudStep1Time.TotalMilliseconds > GamePredefinitions.CloudStep1MaxTimeMs)
{
lastCloudStep1Time = TimeSpan.Zero;
cloudStep1++;
if (cloudStep1 == GamePredefinitions.MaxCloudStep)
cloudStep1 = 0;
}
if (lastCloudStep2Time.TotalMilliseconds > GamePredefinitions.CloudStep2MaxTimeMs)
{
lastCloudStep2Time = TimeSpan.Zero;
cloudStep2++;
if (cloudStep2 == GamePredefinitions.MaxCloudStep)
cloudStep2 = 0;
}
if (lastCloudStep3Time.TotalMilliseconds > GamePredefinitions.CloudStep3MaxTimeMs)
{
lastCloudStep3Time = TimeSpan.Zero;
cloudStep3++;
if (cloudStep3 == 800)
cloudStep3 = 0;
}
var ballCenter = ConvertUnits.ToDisplayUnits(cannonBall.Body.WorldCenter);
var ballX = ballCenter.X;
var ballY = ballCenter.Y;
var cameraX = GamePredefinitions.CameraInitialPosition.X;
var cameraY = GamePredefinitions.CameraInitialPosition.Y;
if (gameStateMachine.CurrentSate == GameState.Playing)
{
if (ballX < -scrollableViewport.Width / 6)
{
cameraX = -scrollableViewport.Width / 6;
}
else if (ballX > scrollableViewport.Width / 6)
{
cameraX = scrollableViewport.Width / 6;
}
else
{
cameraX = ballX;
}
if (ballY < -scrollableViewport.Height / 6)
{
cameraY = -scrollableViewport.Height / 6;
}
else if (ballY > scrollableViewport.Height / 6)
{
cameraY = scrollableViewport.Height / 6;
}
else
{
cameraY = ballY;
}
Camera.Position = new Vector2(cameraX, cameraY);
}
else if (gameStateMachine.CurrentSate == GameState.ShowingPirateShip)
{
if (gameStateMachine.EllapsedTimeSinceLastChange().TotalMilliseconds > GamePredefinitions.TotalTimeShowingPirateShipMs)
{
gameStateMachine.ChangeState(GameState.ScrollingToStartPlaying);
}
}
else if (gameStateMachine.CurrentSate == GameState.ScrollingToStartPlaying)
{
var newCameraPosX = Camera.Position.X + (float)-10.0 * (gameTime.ElapsedGameTime.Milliseconds);
Camera.Position = new Vector2(newCameraPosX, scrollableViewport.Height / 6);
if (Camera.Zoom < 1.0f)
{
Camera.Zoom += gameTime.ElapsedGameTime.Milliseconds / GamePredefinitions.CameraZoomRate;
}
if (Camera.Position.X < -scrollableViewport.Width / 6)
{
Camera.Position = new Vector2(-scrollableViewport.Width / 6, Camera.Position.Y);
gameStateMachine.ChangeState(GameState.Playing);
}
}
if (cannonBall.HitCount == 0)
{
if (smokeTracePositions.Count() == 0)
{
smokeTracePositions.Add(cannonBall.Body.Position);
}
else
{
var lastBallPosition = smokeTracePositions.Last();
var currentBallPosition = cannonBall.Body.Position;
var deltaX = Math.Abs((lastBallPosition.X - currentBallPosition.X));
var deltaY = Math.Abs((lastBallPosition.Y - currentBallPosition.Y));
if (deltaX * deltaX + deltaY * deltaY > GamePredefinitions.SmokeTraceSpace * GamePredefinitions.SmokeTraceSpace)
{
smokeTracePositions.Add(cannonBall.Body.Position);
}
}
}
}
也许所有这些中最重要的一点是:更新炮弹的位置(以及飞行过程中留下的烟迹)。
绘制循环
XNA 框架在需要绘制一帧时会调用绘制循环 (Draw loop)。 我们重写此方法来绘制《海盗》游戏所需的所有帧。
此循环完全处理游戏的呈现部分。
- 它绘制背景纹理。
- 它绘制天空、云彩、海洋和船只。
- 它绘制炮弹、大炮和烟迹。
- 它绘制海盗和其他场景中的对象。
- 它绘制分数和高分。
public override void Draw(GameTime gameTime)
{
ScreenManager.SpriteBatch.Begin(0, null, null, null, null, null, Camera.View);
if (gameStateMachine.CurrentSate != GameState.None)
{
var skyRect = GamePredefinitions.SkyTextureRectangle;
var seaRect = GamePredefinitions.SeaTextureRectangle;
ScreenManager.SpriteBatch.Draw(skyTexture, skyRect, Color.White);
ScreenManager.SpriteBatch.Draw(cloud1Texture, new Rectangle(skyRect.X + cloudStep1, skyRect.Y, skyRect.Width, skyRect.Height), Color.White);
ScreenManager.SpriteBatch.Draw(cloud2Texture, new Rectangle(skyRect.X + cloudStep2 * 2, skyRect.Y, skyRect.Width, skyRect.Height), Color.White);
ScreenManager.SpriteBatch.Draw(cloud3Texture, new Rectangle(skyRect.X + cloudStep3 * 3, skyRect.Y, skyRect.Width, skyRect.Height), Color.White);
ScreenManager.SpriteBatch.Draw(seaTexture, seaRect, Color.White);
ScreenManager.SpriteBatch.Draw(pirateShipTexture, GamePredefinitions.PirateShipTextureRectangle, Color.White);
ScreenManager.SpriteBatch.Draw(royalShipTexture, GamePredefinitions.RoyalShipTextureRectangle, Color.White);
}
smokeTracePositions.ForEach(e =>
ScreenManager.SpriteBatch.Draw(smokeTraceTexture, ConvertUnits.ToDisplayUnits(e) + GamePredefinitions.CannonCenter - new Vector2(10, 10),
null, Color.White, 0f, Vector2.Zero, _scale, SpriteEffects.None, 0f));
foreach (var textureBody in textureBodies)
{
ScreenManager.SpriteBatch.Draw(textureBody.DisplayTexture, ConvertUnits.ToDisplayUnits(textureBody.Body.Position),
null, Color.White, textureBody.Body.Rotation, textureBody.Origin, _scale, SpriteEffects.None, 0f);
}
ScreenManager.SpriteBatch.Draw(cannonTexture, GamePredefinitions.CannonCenter,
null, Color.White, cannonAngle, new Vector2(40f, 29f), 1f, SpriteEffects.None,
0f);
var hiScoreText = string.Format("hi score: {0}", scoreManager.GetHiScore());
var scoreText = string.Format("score: {0}", scoreManager.GetScore());
DrawScoreText(0, 0, hiScoreText);
DrawScoreText(0, 30, scoreText);
ScreenManager.SpriteBatch.End();
_border.Draw();
base.Draw(gameTime);
}
首先,我们绘制天空。然后我们绘制 3 层云(一层速度不同),然后我们绘制海洋,然后绘制船只。最后我们绘制动态元素:炮弹、大炮和分数。
最终思考
希望您喜欢!如果您有什么要说的,请在此页面正下方的新讨论区开始您的讨论,我将非常乐意知道您的想法。
历史
- 2012-01-31:初始版本。
- 2012-02-01:Farseer Dll 和《海盗》游戏源代码完全分离;
重构 PiratesDemo 类。