Table of Contents
So far, game development with Windows Phone has been a lot of fun. In this article I'm going to present the "Snail Run!" game, which is a traditional maze, Pac-Man kind of game.
Besides of the fun, the points of interest here are truly serious: the combined Silverlight and XNA development in the same solution, the use of the "Mappy" map creation tool, the
importer/processor of maps inside the Visual Studio game project, and how to implement the path finding algorithm, most precisely the "A* search algorithm" to give life to the game
To use the Snail Run for Windows Phone provided with this article, you must download and install the following 100% free development tool directly from Microsoft:
Visual Studio 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.
Windows Phone is well known for providing two distinct framework for developers: Silverlight and XNA. While Silverlight is suitable and enough for most of the application needs,
due to the inherent page navigation, flexibility of XAML markup language, built-in transformations, storyboard and animations, advanced databinding, vectorial rendering, panorama
and pivot applications, just to name a few, while on the other hand the update/draw loops, visual and audio effects and the fast sprite rendering engine of XNA Framework usually makes
it into the developers' choice when it comes to game development.
If you already developed Windows Phone games using XNA framework alone, you probably faced and missed the lack of page navigation, the XAML and binding and other benefits of
Silverlight development. Some trivial application requirements, such as rendering a simple listbox, may require a lot of effort in XNA. Fortunately, this architecture dilemma is
finally over once you decide to create a new Visual Studio project, choosing a project template called Windows Phone Silverlight and XNA Application.
This solution template for a project named "MyGame" creates a new solution with three projects:
- MyGame, which is a Silverlight project but also that contains references for the XNA framework. This is where Silverlight and XNA are rendered together.
- MyGameLib, which is an XNA-only project. You can use this project to reuse existing XNA code or separate XNA code from your Silverlight code.
- MyGameLibContent: the content pipeline project where you can find the assets for the solution.
When you run the application, you'll see that it looks pretty much like a regular Silverlight application. This is the MainPage.xaml page:
The only noticeable exception is the central button, that obviously navigates to the game page:
<!---->
<Button Height="100" Content="Change to game page" Click="Button_Click" />
private void Button_Click(object sender, RoutedEventArgs e)
{
NavigationService.Navigate(new Uri("/GamePage.xaml", UriKind.Relative));
}
As for the game page: the "GamePage.xaml" makes it also sound like a regular Silverlight page, but that's not the case. GamePage is where Silverlight
and XNA are really mixed together. For now you'll see that the XAML for this page looks empty, so later on we'll have to do a little work on it:
<phone:PhoneApplicationPage
x:Class="MyGame.GamePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="480"
shell:SystemTray.IsVisible="False">
-->
</phone:PhoneApplicationPage>
Now let's take a loot at the code behind class for GamePage page:
First, you'll notice that some XNA elements are present in this class: ContentManager, GameTimer and SpriteBatch:
public partial class GamePage : PhoneApplicationPage
{
ContentManager contentManager;
GameTimer timer;
SpriteBatch spriteBatch;
public GamePage()
{
InitializeComponent();
contentManager = (Application.Current as App).Content;
timer = new GameTimer();
timer.UpdateInterval = TimeSpan.FromTicks(333333);
timer.Update += OnUpdate;
timer.Draw += OnDraw;
}
Then you have the OnNavigatedTo class, where the SpriteBatch is instantiated, the GameTimer is started
and the XNA rendering is turned on. Also, this is the place where you load the content (sprites, sounds, and so on) for the game.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
timer.Start();
base.OnNavigatedTo(e);
}
Whenever you navigate away from the GamePage, the XNA rendering is disabled and the timer is stopped:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
timer.Stop();
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(false);
base.OnNavigatedFrom(e);
}
If you are already a XNA developer, you'll notice that the OnUpdate method is the replacement for the familiar XNA's Update method.
This is where you run logic such as updating the world, checking for collisions, gathering input, and playing audio.
private void OnUpdate(object sender, GameTimerEventArgs e)
{
}
Along with the OnUpdate method, the OnDraw is also a counterpart for the familiar XNA Update method. As you can see,
there's not much here, but this is where all the rendering of the game will happen:
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.CornflowerBlue);
}
As you can see from the above code, the default Silverlight and XNA template is nice enough to save you some time and effort, but unfortunately it leaves some
important parts unexplained, despite the comments spread all over the code. In the next sections of the article we'll try to deal with these gaps.
The first make-up is done in the MainPage page. As seen earlier, these page is an ordinary Silverlight page. It is the entry point for our application, so
it would be interesting to put here the links for user options, such as settings, leaderboards, about page and the mos obvious, game start. I didn't changed the functionality at all,
just created a foreground image and applied some animations to the bubbles that appear on the background, to give the game a submarine atmosphere.
The interesting point here is the making of the bubble animation. The MainPage itself has an instance of the SplashScreen control, which in turn
is responsible for the animations:
<!---->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ctr:SplashScreen x:Name="splashScreen" VerticalAlignment="Center" Grid.ColumnSpan="3" Grid.RowSpan="3"/>
<Image Source="MainPage.png" MouseLeftButtonUp="Image_MouseLeftButtonUp"></Image>
</Grid>
The SplashScreen code behind class creates exactly 10 Ellipse elements, each one of which is subjected to an independent animation.
Each animation cause the corresponding bubble to move rapidly from the bottom of the Canvas to the top of it. Besides, an EasingFunction is applied to
the animations, so that the movement looks more natural. The whole animation process is beyond the scope of this article, but here is the code, in case you want to investigate how
it works:
private void CreateBubbles()
{
var linearBubbleBrush = new LinearGradientBrush()
{ StartPoint = new Point(1, 0), EndPoint = new Point(0, 1) };
linearBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0xFF, 0x00, 0x20, 0x40), 0.0));
linearBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
var radialBubbleBrush = new RadialGradientBrush() {
Center = new Point(0.25, 0.75), RadiusX = .3, RadiusY = .2, GradientOrigin = new Point(0.35, 0.75) };
radialBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.0));
radialBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
for (var i = 0; i < 10; i++)
{
var diameter = 10 + (i % 4) * 10;
var ms = 2000 + i % 7 * 500;
var ellBubble = new Ellipse()
{
Width = diameter,
Height = diameter,
Stroke = linearBubbleBrush,
Fill = radialBubbleBrush,
StrokeThickness = 3
};
ellBubble.SetValue(Canvas.LeftProperty,
i * (40.0 + 40.0 - diameter / 2));
ellBubble.SetValue(Canvas.TopProperty, 0.0 + 40.0 - diameter / 2);
cnvBubbles.Children.Add(ellBubble);
var leftAnimation = new DoubleAnimation()
{
From = 40.0 * i,
To = 40.0 * i,
Duration = TimeSpan.FromMilliseconds(ms)
};
var topAnimation = new DoubleAnimation()
{
From = 400,
To = 0,
Duration = TimeSpan.FromMilliseconds(ms)
};
var opacityAnimation = new DoubleAnimation()
{
From = 1.0,
To = 0.0,
Duration = TimeSpan.FromMilliseconds(ms)
};
Storyboard.SetTarget(leftAnimation, ellBubble);
Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
Storyboard.SetTarget(topAnimation, ellBubble);
Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
Storyboard.SetTarget(opacityAnimation, ellBubble);
Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));
leftAnimation.EasingFunction = new BackEase()
{ Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
topAnimation.EasingFunction = new BackEase()
{ Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
var sb = new Storyboard();
sb.Children.Add(leftAnimation);
sb.Children.Add(topAnimation);
sb.Children.Add(opacityAnimation);
sb.RepeatBehavior = RepeatBehavior.Forever;
bubbles.Add(ellBubble);
storyBoards.Add(sb);
sb.Begin();
}
}
This section explains how to get Silverlight and XNA to work together. I took me some time to figure it out how to do it.
First, you have to add content to the project. The only types of content we have are sprites and level maps. Sprite is a common concept that doesn't require further explanation.
We have graphic contents for the Snail, the Pearl, Squid and Starfish. These are the characters of our game. Level maps are maps created by a third party, open source tool called MapWin
that are then incorporated to our game content.
Now, let's take a look at code behind class located at GamePage.xaml.cs. The noticeable elements inside this class scope are:
- The already known ContentManager, GameTimer and SpriteBatch instances.
- The Camera2d instance. As the name implicates, it is used to scroll/zoom while we run across the game maze.
- The UIElementRenderer is the most important part: it will allow us to render Silverlight content into a texture, thus we can put them along with XNA-generated graphics.
- GameSettings is for predefined game settings, such as speed, screen resolution and so on.
- The ScoreManager manages, well... the score.
- The Level class contains information regarding the levels of the game.
.
.
.
public partial class GamePage : PhoneApplicationPage
{
ContentManager contentManager;
GameTimer timer;
SpriteBatch spriteBatch;
Camera2d camera;
UIElementRenderer elementRenderer;
GameSettings settings = new GameSettings();
ScoreManager scoreManager = new ScoreManager();
GameStateMachine stateMachine = new GameStateMachine();
List<Level> levels = new List<Level>();
int CurrentLevelNumber = 1;
double minUpdateTimeSpanMs = 20;
double accumulatedUpdateTimeSpanMs = 0;
double minChaseTimeSpanMs = 500;
double accumulatedChaseTimeSpanMs = 0;
Texture2D seaTexture;
public GamePage()
.
.
.
Now we move on to the class constructor. Here we are preparing our game for first use, so many things are set up in the constructor. For example, the contentManager
is first instantiated, and then it is ready to be used. The timer is an instance of XNA framework's Timer class. This class allows the control of the
two important events, Update and Draw. Here we notice how the events are bound to the methods OnUpdate and OnDraw. Then we have
the camera instance, which is intended to show just one part of the maze each time. The TouchPanel.EnabledGestures property is set to allow the gestures: Flick,
Hold and Tap. The page's DataContext is set to the scoreManager instance (this is later used for Silverlight data binding purposes). And finally we load the
game levels.
public GamePage()
{
InitializeComponent();
contentManager = (Application.Current as App).Content;
timer = new GameTimer();
timer.UpdateInterval = TimeSpan.FromTicks(166667);
timer.Update += OnUpdate;
timer.Draw += OnDraw;
camera = new Camera2d(2, 0, settings.ScreenHeight, settings.ScreenWidth, settings.CameraCenter);
LayoutUpdated += new EventHandler(GamePage_LayoutUpdated);
TouchPanel.EnabledGestures = GestureType.Flick | GestureType.Hold | GestureType.Tap;
this.DataContext = scoreManager;
LoadLevels();
}
Then it's time to load and initialize our content. This is done through the page's OnNavigatedTo event function:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
timer.Start();
foreach (var level in levels)
{
level.Initialize();
}
seaTexture = contentManager.Load<Texture2D>(string.Format("{0}/Sea", settings.SpritesRoot));
base.OnNavigatedTo(e);
scoreManager.Level = CurrentLevelNumber;
stateMachine.GameStateChanged += new GameStateMachine.GameStateChangedHandler(stateMachine_GameStateChanged);
stateMachine.ChangeState(GameState.PreparingToStartLevel);
}
The OnUpdate method is started by the update of the current game level. Notice that the CurrentLevel.Update method is not called all times, but
only when the elapsed time reaches a preconfigured amount of time. This is done to avoid unnecessary processing.
private void OnUpdate(object sender, GameTimerEventArgs e)
{
UpdateLevel(e);
ChaseSnail(e);
HandleInput();
UpdateCamera();
}
private void UpdateLevel(GameTimerEventArgs e)
{
accumulatedUpdateTimeSpanMs += e.ElapsedTime.TotalMilliseconds;
if (accumulatedUpdateTimeSpanMs > minUpdateTimeSpanMs)
{
accumulatedUpdateTimeSpanMs = 0;
CurrentLevel.Update(e.ElapsedTime);
}
}
Then comes the searching algorithm which is used by squids to pursue the snail. This is done by calling the ChaseSnail on each squid object.
The code below shows that this is done only when the game is in the state Playing, otherwise there would be no reason for calling the function.
private void ChaseSnail(GameTimerEventArgs e)
{
if (stateMachine.CurrentSate == GameState.Playing)
{
accumulatedChaseTimeSpanMs += e.ElapsedTime.TotalMilliseconds;
if (accumulatedChaseTimeSpanMs > minChaseTimeSpanMs)
{
accumulatedChaseTimeSpanMs = 0;
CurrentLevel.Squids.ForEach(x =>
{
if (x.ChaseMovementQueue.Count == 0)
{
x.ChaseSnail();
}
});
}
}
}
Then there is the validation of game input. The snail is moved up, down, left and right every time the user performs a Flick gesture. When the user taps the screen,
the snail just halts.
private void HandleInput()
{
if (TouchPanel.IsGestureAvailable)
{
GestureSample gesture = TouchPanel.ReadGesture();
switch (gesture.GestureType)
{
case GestureType.Flick:
var x = gesture.Delta.X;
var y = gesture.Delta.Y;
CurrentLevel.Flick(x, y);
break;
case GestureType.Tap:
CurrentLevel.Snail.EnqueueMove(Gesture.Tap);
break;
default:
break;
}
}
}
Finally, there is the camera update. Normally the camera tries to follow the squid movements. But when the squid reaches the edges of the maze, the camera stops moving.
So, there is a restricte area for the camera moves.
private void UpdateCamera()
{
var newCameraPos = CurrentLevel.Snail.Position * settings.TileWidth;
if (newCameraPos.X < camera.ViewportWidth / 4)
{
newCameraPos.X = camera.ViewportWidth / 4;
}
else if (newCameraPos.X > CurrentLevel.MapWidth * settings.TileWidth -
camera.ViewportWidth / (camera.Zoom * 2) + settings.MapTopLeftCorner.X)
{
newCameraPos.X = CurrentLevel.MapWidth * settings.TileWidth -
camera.ViewportWidth / (camera.Zoom * 2) + settings.MapTopLeftCorner.X;
}
if (newCameraPos.Y < camera.ViewportHeight / 4)
{
newCameraPos.Y = camera.ViewportHeight / 4;
}
else if (newCameraPos.Y > CurrentLevel.MapHeight * settings.TileWidth -
camera.ViewportHeight / (camera.Zoom * 2) + settings.MapTopLeftCorner.Y)
{
newCameraPos.Y = CurrentLevel.MapHeight * settings.TileWidth -
camera.ViewportHeight / (camera.Zoom * 2) + settings.MapTopLeftCorner.Y;
}
camera.Pos = newCameraPos;
}
Then we have the OnDraw method. Depending on the game state, it has to draw differently:
private void OnDraw(object sender, GameTimerEventArgs e)
{
switch (stateMachine.CurrentSate)
{
case GameState.PreparingToStartLevel:
case GameState.GameOver:
case GameState.LevelCompleted:
case GameState.LevelFailed:
case GameState.Paused:
DrawCentralMessage(e);
break;
case GameState.Playing:
DrawPlaying(e);
break;
}
}
When the game is in a state other than Playing, it must show messages. These messages are defined in Silverlight XAML, and right here
is the way to render Silverlight content alongside XNA: the elementRenderer.Render(); renders the Silverlight page (that is, constructs all visual elements)
and then the spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White); tells the application to render the Silverlight content in the game's
sprite batch.
private void DrawCentralMessage(GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.Black);
elementRenderer.Render();
this.spriteBatch.Begin();
spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);
this.spriteBatch.End();
}
The DrawPlaying code is divided in smaller functions for the sake of simplicity:
private void DrawPlaying(GameTimerEventArgs e)
{
DrawBackground();
DrawMaze(e);
DrawAnimals(e);
DrawSilverlight();
}
First, we draw the background, that is, the sea texture:
private void DrawBackground()
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.Black);
this.spriteBatch.Begin();
spriteBatch.Draw(seaTexture, Vector2.Zero,
new xna.Rectangle(0, 0, settings.ScreenWidth, settings.ScreenHeight),
Color.White);
this.spriteBatch.End();
}
Then we draw the maze on top of it:
private void DrawMaze(GameTimerEventArgs e)
{
this.spriteBatch.Begin(SpriteSortMode.BackToFront,
BlendState.AlphaBlend, null, null, null, null,
camera.Transformation(spriteBatch.GraphicsDevice));
this.spriteBatch.Draw((float)e.ElapsedTime.TotalSeconds,
CurrentLevel.Map2D, settings.MapTopLeftCorner, Color.White);
this.spriteBatch.End();
}
And then the animals are rendered:
private void DrawAnimals(GameTimerEventArgs e)
{
this.spriteBatch.Begin(SpriteSortMode.BackToFront,
BlendState.AlphaBlend, null, null, null, null,
camera.Transformation(spriteBatch.GraphicsDevice));
CurrentLevel.Snail.Draw(e.ElapsedTime, spriteBatch,
settings.MapTopLeftCorner, 0);
foreach (var pearl in CurrentLevel.Pearls)
{
pearl.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
}
foreach (var starFish in CurrentLevel.StarFishes)
{
starFish.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
}
foreach (var squid in CurrentLevel.Squids)
{
squid.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
}
this.spriteBatch.End();
}
Finally, we draw the Silverlight texture on top of everything. This silverlight part is just the score information
that appears at the top of the screen during the game play.
private void DrawSilverlight()
{
elementRenderer.Render();
this.spriteBatch.Begin();
spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);
this.spriteBatch.End();
}
You can read useful further information at MSDN article entitled "How to: Combine Silverlight and the XNA Framework in a Windows Phone Application".
This game uses the classic path finding algorithm called "A*" (pronounced "A Star"). According to Wikipedia:
As A* traverses the graph, it follows a path of the lowest known cost, keeping a sorted priority queue
of alternate path segments along the way. If, at any point, a segment of the path being traversed has a higher
cost than another encountered path segment, it abandons the higher-cost path segment and traverses the lower-cost
path segment instead. This process continues until the goal is reached.
Illustration of A* search for finding path from a start node to a goal node in a robot motion planning problem.
The empty circles represent the nodes in the open set, i.e., those that remain to be explored, and the filled ones are
in the closed set. Color on each closed node indicates the distance from the start: the greener, the further.
One can firstly see the A* moving in a straight line in the direction of the goal, then when hitting the obstacle, it
explores alternative routes through the nodes from the open set. (Source: Wikipedia)
The algorithm mentioned above is just perfect solution for our problem. Fortunately, I borrowed one
of the wonderful Sacha Barber's contribution to the Code Project (dealing with finding the best path between any two stations of the London Underground) and
made myself a version of it. I only had to change the concepts: Sacha's article deals with a person trying to find the optimal path between the current station and the,
respecting the geographical connections between those stations. On the other side, in Snail Quest the person is represented by the squid. The desired station is the
cell where the snail is found, the stations are the empty cells inside the maze, and instead of connection between stations, we now have connections between empty
cells, which are the "corridors" inside the maze.
public List<MovementType> DoSearch(Vector2 squidPosition, Vector2 snailPosition)
{
pathsSolutionsFound = new List<List<Vector2>>();
pathsAgenda = new List<List<Vector2>>();
List<Vector2> pathStart = new List<Vector2>();
pathStart.Add(squidPosition);
pathsAgenda.Add(pathStart);
while (pathsAgenda.Count() > 0 && pathsAgenda.Count() < 100)
{
List<Vector2> currPath = pathsAgenda[0];
pathsAgenda.RemoveAt(0);
if (currPath.Count(
x => x.Equals(snailPosition)) > 0)
{
pathsSolutionsFound.Add(currPath);
break;
}
else
{
Vector2 currPosition = currPath.Last();
List<Vector2> successorPositions =
GetSuccessorsForPosition(currPosition);
foreach (var successorPosition in successorPositions)
{
if (!currPath.Contains(successorPosition) &&
pathsSolutionsFound.Count(x => x.Contains(successorPosition)) == 0)
{
List<Vector2> newPath = new List<Vector2>();
foreach (var station in currPath)
newPath.Add(station);
newPath.Add(successorPosition);
pathsAgenda.Add(newPath);
}
}
}
}
if (pathsSolutionsFound.Count() > 0)
{
var solutionPath = pathsSolutionsFound[0];
var movementList = new List<MovementType>();
var Vector2 = solutionPath[0];
for (var i = 1; i < solutionPath.Count(); i++)
{
var movement = MovementType.None;
if (solutionPath[i].X > Vector2.X)
movement = MovementType.Right;
if (solutionPath[i].X < Vector2.X)
movement = MovementType.Left;
if (solutionPath[i].Y > Vector2.Y)
movement = MovementType.Bottom;
if (solutionPath[i].Y < Vector2.Y)
movement = MovementType.Top;
movementList.Add(movement);
Vector2 = solutionPath[i];
}
return movementList;
}
return null;
}
As I discovered at the beginning of the development of this game, creating scenarios with repetitive blocks is really a boring task.
Fortunately, there is a very interesting map tool for game developers: Mappy Win32. It allows the development of
large game scenarios with a small set of common graphical blocks. All the old school games (think Mario Bros, Pac-Man, Sonic, etc.) were
made by using a limited number of blocks (or "tiles" as they are so often called).
You can download Mappy Tool freely at this website.
Let's see how to create a level scene with Mappy: First, open the File menu, and choose New Map.
Then we configure our map to work with 25 x 15 tiles, having each tile 32 x 32 pixels:
The next important step is import the Tile Strip, which is the set of 32 x 32 blocks containing the limited tiles used by our map:
Finally, we draw our map freely, using the blocks imported previously.
The last step is to save the file. We save it with the Level1.FMP name in the \SnailRun\SnailRun\SnailRunLibContent\Maps\ folder.
Notice that the file is now part of the content of our game:
But wait, this .FMP extension is proprietary to the Mappy application. How can the XNA import/process this kind of file? The answer is, it can't.
It does know nothing about .FMP extensions. This is why we have to do it by ourselves. Fortunately, again, some smart people already did the hard job for us.
Although XNA framework can handle various kinds of files, unfortunately there is a limited number of them it can read and process. This is why, if some new kind
of file is involved, we need to implement a new content pipeline extension by ourselves. Fortunately, I found the
XNA Content Pipeline Extension to Mappy Maps(.FMP) at Codeplex, developed by Brazilian game developer Luciano José for the Windows and XBox platforms.
"Project Description
This XNA Library to Mappy Maps helps XNA Developers to integrate the Tile Map maked up in the Mappy Tool with your XNA Project(Windows and Xbox 360).
This library allows you just drag and drop your archive(.FMP) at the Content Project in the XNA Project(Windows and Xbox 360).
Create your tile map with the Mappy Tool(http://www.tilemap.co.uk/mappy.php) for integrating with this XNA Library.
See my article(in portuguese) about this library at SharpGames(the Brazilian XNA Community):
http://www.sharpgames.net/Artigos/Artigo/tabid/58/selectmoduleid/376/ArticleID/1585/reftab/54/Default.aspx
Visit the Brazilian XNA Community: http://www.sharpgames.net/"
It works by reading the .FMP file and reading the underlying structures/objets inside it. This way we can effectively "read" the map, and use the data to find routes,
test collisions with obstacles, and so on.
Unfortunately, like I said, XNA Content Pipeline Extension for Mappy is targeted only to Windows and XBox platforms. But with some time I was able to port
it to Windows Phone platform and got it to work. I then compiled it in release mode and included as a reference in the project.
After you add Level1.FMP to the Content project, you still have to configure the content processor properties:
In the end, everything is working and everybody is happy:
That's it! I hope you liked both game and the concepts presented here. And if have questions, complaints, ideas, then please leave your comments below.
- 2012-02-29: Initial version.