Windows Phone 版 Pirates!






4.92/5 (52投票s)
一款受 Angry Birds 启发的 Windows Phone 2D 物理游戏。
 
 
目录
引言
 你好,海盗! 在本文中,我们将介绍一款 Windows Phone 游戏(或者至少是游戏的开端……),并希望最终能带来很多乐趣。 但在乐趣的背后,是构建它所需的基本技术。 我将分步介绍,并希望在文章结束时引起你的兴趣。
你好,海盗! 在本文中,我们将介绍一款 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 类。


