Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Pirates! for Windows Phone

0.00/5 (No votes)
2 Feb 2012 4  
A 2D physics game for Windows Phone inspired on Angry Birds

Table of Contents

Introduction

hoy, Mate! In this article we are presenting a Windows Phone game (or at least the beginning of a game...) and hopefully it's going to be lots of fun in the end. But behind of the fun lies the basic technologies needed to build it. I'll present then in steps, and I hope to capture you interest until the end of the article.

"Pirates!" for Windows Phone is a game build in C# and XNA, using Farseer Physics Engine. The idea of the game is largely inspired on and also pays tribute to Rovio's "Angry Birds" game, which reached 500 million downloads at the end of 2011. Angry Birds may have addicted many people around the world, but from my part, I'm not addicted to the playing itself, but to pursuing the pieces needed to build a game like that.

Instead of birds, a cannon full of cannon balls. Instead of pigs, a big pirate ship full of pirates. Your mission here is to aim the cannon and destroy all pirates. (disclaimer: this story has nothing to do with the ongoing "SOPA" and "PIPA" thing...)

Here comes a short video that to give you a glimpse of what we are talking about here:

This article is intended to show how to play with this new accelerometer emulation, accompanied by a simple application that use it.

System Requirements

To use the Pirates! for Windows Phone provided with this article, you must download and install the following 100% free development tool directly from Microsoft:

  • Visual Web Developer 2010 Express for Windows Phone
    Whether you’re familiar with, or new to, Silverlight and XNA Game Studio programming, Visual Studio 2010 Express for Windows Phone provides everything you need to get started building Windows Phone apps.

  • Windows Phone SDK 7.1
    The Windows Phone Software Development Kit (SDK) 7.1 provides you with all of the tools that you need to develop applications and games for both Windows Phone 7.0 and Windows Phone 7.5 devices.
  • Farseer Physics Engine

    Farseer is a wonderful open source physics engine, based on the original Box 2D open source project (as an aside, the Angry Birds game uses Box2D). The difference is that Box2D is written in C++ (and has been ported to many languages), while Farseer is made with C# aiming Silverlight and XNA. Also, the Farseer website claims this framework has many other features other than the ones provided by the original Box2D release.

    Adjusting The Game To 60 Frames Per Second

    In order to achieve the maximum frames per second (around 60 fps) provided by WP Mango in your device, you have to change the Farseer source code you downloaded like this:

    1. Double click the "Samples XNA WP7" solution. Choose the "Upgrade Windows Phone Projects..." to upgrade them all to the latest Windows Phone 7.5 Mango.
    2. In the constructor of game class, add this event handler:
                  _graphics.PreparingDeviceSettings += 
      new EventHandler<PreparingDeviceSettingsEventArgs>(_graphics_PreparingDeviceSettings);
      
    3. Then add this event:
              void _graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)    
      {            
          e.GraphicsDeviceInformation.PresentationParameters.PresentationInterval = PresentInterval.One;
      }
      
    4. In Initialize method of game class, be sure to modify the PresentationInterval parameter to PresentationInterval.One:
              protected override void Initialize()       
          {            
              base.Initialize();
              this.GraphicsDevice.PresentationParameters.PresentationInterval = 
              Microsoft.Xna.Framework.Graphics.PresentInterval.One;
              ...
      

    Fortunately for us all, I already updated the article's source code with a compiled version of Farseer Physics Engine compliant to the latest Windows Phone 7.5 (mango) and running the best frame rate possible. This is going to be useful for me and for readers interested in game development with Farseer Engine.

    Converting Images Into Physical Objects

    All dynamic objects in the game are created from the texture images. This is a very nice feature of Farseer engine that makes our life much easier. The farseer built-in functions create a solid object from our labyrinth plain image, provided that we leave the empty spaces with the transparent color, like this:

    piratesprite.png

    Farseer uses the BayazitDecomposer.ConvexPartition method, which creates smaller convex polygons out of a single big concave polygon:

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

    The Handle Input Loop

    The Handle Input Loop is exposed by Farseer Engine and gives us the opportunity to detect and handle the users gestures.

    This method is responsible for:

    • Detecting the left ThumbStick movement and translating it into cannon movements.
    • Detecting the "A" right button and shooting the cannon.
    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);
            }
    

    The Update Loop

    In XNA framework, the Update Loop is invoked when the game has determined that game logic needs to be processed. This might include the management of the game state, the processing of user input, or the updating of simulation data. By overriding this method, we can add logic which is specific to our Pirates game.

    In XNA, The Update method works in combination with the Draw method, and you must take into account that each one has its own specific role. That is, never try to draw sprites inside the Update method, and never ever calculate/update positions inside the Draw method.

    The purpose of the Update class here is:

    • To calculate the new cloud positions. There are 3 layers of clouds and each layer moves at its own speed.
    • To alternate the sea appearance between one of the 4 existing sea textures.
    • To move the camera according to the game state transitions (that is, at first the game is showing you the pirate ship, then the camera moves to your ship).
    • To update the camera position along the path described by the cannon ball. This is useful to keep track of the game action.
    • To control the camera zoom when needed.
    • And maybe the most important of all: to update the cannon ball position (along with the smoke trace left while flying)
    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);
                        }
                    }                
                }
            }
    

    One very importante aspect of the Update method is the use of GameTime parameter. This parameter tells us how much time passed since the last time the game was updated. As you can see above, checking the ElapsedGameTime property of the the GameTime parameter is crucial so that we can correctly calculate the game play without the interference of the processor speed fluctuation. Otherwise, you might see the game faster or slower depending on how much processing is being done by the phone at a given moment.

    The Draw Loop

    The Draw loop is called by the XNA framework it is time to draw a frame. We override this method to draw all the frames needed for our Pirates game.

    This loop handles entirely the presentation part of the game:

    • It draws the background texture
    • It draws the sky, clouds, sea and ships
    • It draws the cannon ball, the cannon and the smoke traces
    • It draws the pirates and other objects on the scene.
    • It draws the score and high scores.
    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);
            }
    

    First, we draw the sky. Then we draw 3 levels of clouds (one with a different speed), then we draw the sea, then the ships. Finally we draw the dynamic elements: the cannon ball, the cannon, and the score.

    Final Considerations

    I hope you enjoyed it! If you have something to say, please start a new discussion in the section right below in this page, I'll be very glad to know what you're thinking.

    History

    • 2012-01-31: Initial version.
    • 2012-02-01: Complete separation between Farseer dlls and Pirates Game source code;
      Code refactoring of PiratesDemo class.

    License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here