65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows Phone 版 Pirates!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (52投票s)

2012 年 1 月 31 日

CPOL

6分钟阅读

viewsIcon

132222

downloadIcon

3474

一款受 Angry Birds 启发的 Windows Phone 2D 物理游戏。

目录

引言

你好,海盗! 在本文中,我们将介绍一款 Windows Phone 游戏(或者至少是游戏的开端……),并希望最终能带来很多乐趣。 但在乐趣的背后,是构建它所需的基本技术。 我将分步介绍,并希望在文章结束时引起你的兴趣。

Windows Phone 版《海盗!》是一款使用 C# 和 XNA、并结合 Farseer 物理引擎开发的游戏。 该游戏的设计很大程度上受到了 Rovio 的《愤怒的小鸟》游戏的启发,并向其致敬,该游戏在 2011 年底已达到 5 亿次下载量。《愤怒的小鸟》可能让全世界许多人都沉迷其中,但我本人并不沉迷于游玩本身,而是沉迷于追求构建类似游戏所需的那些零碎技术。

用一门装满炮弹的大炮代替小鸟。 用一艘装满海盗的大型海盗船代替猪。 你的任务是瞄准大炮,摧毁所有海盗。(免责声明:这个故事与当前的“SOPA”和“PIPA”无关……)

这里有一个简短的视频,让你一窥我们正在讨论的内容。

本文旨在展示如何使用这种新的加速计模拟功能,并附带一个简单的应用程序来使用它。

系统要求

要使用本文提供的 Windows Phone 版《海盗!》,您必须直接从 Microsoft 下载并安装以下 100% 免费的开发工具。

  • 适用于 Windows Phone 的 Visual Web Developer 2010 Express
    无论您是熟悉还是初次接触 Silverlight 和 XNA Game Studio 编程,Visual Studio 2010 Express for Windows Phone 都提供了您开始构建 Windows Phone 应用程序所需的一切。

  • Windows Phone SDK 7.1
    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 源代码。

    1. 双击“Samples XNA WP7”解决方案。 选择“Upgrade Windows Phone Projects...”将所有项目升级到最新的 Windows Phone 7.5 Mango。
    2. 在游戏类的构造函数中,添加此事件处理程序。
                  _graphics.PreparingDeviceSettings += 
      new EventHandler<PreparingDeviceSettingsEventArgs>(_graphics_PreparingDeviceSettings);
      
    3. 然后添加此事件。
              void _graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)    
      {            
          e.GraphicsDeviceInformation.PresentationParameters.PresentationInterval = PresentInterval.One;
      }
      
    4. 在游戏类的 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 内置函数可以从我们的迷宫平面图像创建实体对象,前提是我们将空白区域保留为透明颜色,如下所示。

    piratesprite.png

    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);
                        }
                    }                
                }
            }
    

    也许所有这些中最重要的一点是:更新炮弹的位置(以及飞行过程中留下的烟迹)。更新关于方法的一个非常重要的方面是使用GameTime参数。此参数告诉我们自上次游戏更新以来经过了多长时间。 如上所示,检查ElapsedGameTime方法的一个非常重要的方面是使用属性对于我们能够正确计算游戏玩法至关重要,不受处理器速度波动的影响。否则,您可能会看到游戏速度变快或变慢,具体取决于手机在给定时刻执行的处理量。

    绘制循环

    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 类。
    © . All rights reserved.