Table of Contents
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.
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 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.
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:
- 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.
- In the constructor of game class, add this event handler:
_graphics.PreparingDeviceSettings +=
new EventHandler<PreparingDeviceSettingsEventArgs>(_graphics_PreparingDeviceSettings);
- Then add this event:
void _graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
{
e.GraphicsDeviceInformation.PresentationParameters.PresentationInterval = PresentInterval.One;
}
- 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.
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:
Farseer uses the BayazitDecomposer.ConvexPartition
method, which creates smaller convex polygons out of a single big concave polygon:
TextureBody textureBody;
var physicTexture = ScreenManager.Content.Load<Texture2D>(string.Format("Samples/{0}", physicTextureName));
uint[] data = new uint[physicTexture.Width * physicTexture.Height];
physicTexture.GetData(data);
Vertices textureVertices = PolygonTools.CreatePolygon(data, physicTexture.Width, false);
Vector2 centroid = -textureVertices.GetCentroid() + bodyOrigin - centerScreen;
textureVertices.Translate(ref centroid);
var origin = -centroid;
textureVertices = SimplifyTools.ReduceByDistance(textureVertices, 4f);
List<Vertices> list = BayazitDecomposer.ConvexPartition(textureVertices);
_scale = 1f;
Vector2 vertScale = new Vector2(ConvertUnits.ToSimUnits(1)) * _scale;
foreach (Vertices vertices in list)
{
vertices.Scale(ref vertScale);
}
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);
}
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 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.
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.
- 2012-01-31: Initial version.
- 2012-02-01: Complete separation between Farseer dlls and Pirates Game source code;
Code refactoring of PiratesDemo class.