Hi,
The idea of a contest which leads to the creation of an article has excited me
and since I've got a son, Marco, a 15 years old student, potentially gifted for programming,
we've started, everyone on his own, preparing an article about the Windows Phone
7. Already after some days, comparing the two articles, the difference was
clear. I've left behind my article and here, instead, I've posted the work of my
son Marco, for two reasons I think you can share:
- The article appears (evaluate your own) very detailed and clear.
- Given the Marco's young age, the opinion of really qualified people can be very important, even
for the education of the child
This is the Marco's article:
Contents
-
Introduction
Introduction of the whole article
-
Game basics
A little introduction about the general working of the game
-
Core
This section is about the core of the game: from the cells to the levels
-
Cells
The basic part of the whole game: the cell
-
IoC (Inversion of Control)
A little tutorial about the Inversion of Control
-
A container for Windows Phone 7
Here we'll analyze a container made to work on the Windows Phone 7 platform
-
Movement controllers
Analysis of the movement controllers
-
Goals
Goals of the game: food eaten, elapsed time and length of the snake
-
Levels
Analysis of the most complex object of the whole application, focused on the localization and the movement of the snake
-
Enemy snakes
Enemy snakes of the game; focused on the usage of a path finder algorithm
-
Saving and loading levels
Little explanation of the structure of the XML file which holds the levels
-
UI and design
This section is about the design and the UI of the application
Introduction
First of all, I apologize for my bad English, but I'm 15 years old and I'm still
studying it at school.
This article talks about the development of an application for the new Windows
Phone 7 platform: Snake Mobile. The idea of the game is only an opportunity to
demonstrate that in a simple application like the Snake, we find a lot of the
basic concepts of the development for the Windows Phone 7 platform.
I'm not a designer, in fact the game isn't visually attractive, but this
application hasn't been developed to be deployed to the public, but to put
inside a single videogame a lot of the fundamental programming concepts. To
achieve this goal, sometimes I've used solutions less convenient than others, to create a scenario in which it was necessary to use a particular programming technique.
An example of this is the Inversion of Control: using the IoC in a scenario like
the one that will be described later is pure madness, but I've decided to use
this design pattern the same way, to make its comprehension simpler and to
create a container that works on the Windows Phone 7 platform, since that in
bigger applications this may be useful.
Choosing to develop a game, in fact, means creating a complex application in
which are combined various elements like graphics, audio, data, etc... This
article is quite long and can be boring to read it all, but my goal was to be as
complete as possible in the exposure and, above all, I didn't want to take
anything for granted, in fact, in some particular situations, I also added some
deepening links. I also added to the end of the article a list containing all
the independent components of the project that you can take away and add freely
in yours.
Finally, I remember you, that this article could be improved further: if you see any mistakes that I haven't noticed
or any improvement that I can do, please let me know so that I can update the
article and let the other people benefit from the improvements.
Are you ready? Let's start!
Game basics
The basic idea of the game, is that we have a Grid
, which will be filled with some cells.
We'll store the instances of the Cell
class in a bi-dimensional array; the index is used to retrieve the cell from the position on the grid. Example: a wall located on (2, 5) will be stored in the array using the index [2, 5].
To move the snake, we have a Point
object, that represents the direction of the snake, and a Timer
which makes the snake move (actually, it updates the CellType
property of the cells; we'll see later what it is). The enemy snakes work the same way.
The movement controller raises the Up
, Down
, Left
and Right
events, which are handled by the Level
object, that changes the direction.
At each tick of the main timer, there is a call to check if the goal
is accomplished; if it returns true, the level is completed, otherwise, nothing happens.
However, what I've said before, will be analyzed deeper during the following
part of the article.
Game core
In this part we'll talk about the most important things of the application:
here is analyzed everything, from the simplest components, like the goals or the cells,
to the most complex controls like the Level object.
The paragraph of the movement controllers contains a parenthesis about the IoC (Inversion of Control) using Castle.Windsor.
It isn't for educational purposes; it's only used to show how it can be useful when you want to make your project open to new solutions and to make your objects reusable and more flexible.
However, we'll talk about this later.
Cells
The basic part of the game is the cell: this object has a property, CellType
,
that represents the content of the cell. In the constructor, we bind it to the
Content
property, using the CellType2ContentConverter
.
This converter transforms a value of the CellType enum to a visual object,
returning the corresponding image.
The most interesting member is the HitTest
method: this method returns a value
indicating what should happen when a cell hits another cell.
public HitTestResult HitTest(Cell other) {
if (this.CellType == CellType.Free || other.CellType == CellType.Free)
return HitTestResult.None;
else if (this.CellType == CellType.Food || other.CellType == CellType.Food)
return HitTestResult.Eat;
else if (this.CellType == CellType.DeathFood || other.CellType == CellType.DeathFood)
return HitTestResult.Death;
else if (this.CellType == CellType.Wall || other.CellType == CellType.Wall)
return HitTestResult.Death;
else
return HitTestResult.Cut;
}
IoC (Inversion of Control)
Let's open a parenthesis about IoC!
(
Let we see what Wikipedia says:
"Inversion of control (IoC) is an abstract principle describing an aspect of some software architecture designs in which the flow of control of a system is inverted in comparison to procedural programming. In traditional programming the flow is controlled by a central piece of code. Using Inversion of Control, this
"central control" as a design principle is left behind."
I think it's better if we translate it in a human language...
Let's start from an example: we have to build an object that is able to extract
the content of a tag in a HTML page. Everyone of us would write:
public class TagGetter
{
public string GetContent(string pageUrl, string tagName)
{
string page = new System.Net.WebClient().DownloadString(pageUrl);
string tagContent = System.Text.RegularExpressions.Regex.Match(page, "<" + tagName + ">[^>]*</" + tagName + ">").Value;
tagContent = tagContent.Substring(tagName.Length + 2, tagContent.Length - 2 * (tagName.Length + 2) - 1);
return tagContent;
}
}
Now analyze what this object does: first, downloads the page using the
System.Net.WebClient
class, then gets the tag using the Regex
,
and finally returns the found value. Maybe, you're thinking what's going wrong
with this piece of code, and I answer you saying: nothing. Objectively, this
class is perfect: downloads and extracts the tag well, and if you try extracting
the title tag of the Google homepage, it returns "Google". But, try to see this
class from a more general point of view: there are 2 main bugs, that require a
different approach to be solved.
- This class does too many things! A principle of good design is the
SoC (Separation of Concerns).
According to this principle, a single object must do only one simple task and do
it well. We should split this class into 2 different objects: one to download the
page, and one to extract the tag.
- What if the desired document is not reachable by the HTTP protocol? We should change the body of the method adding (or replacing) this feature with another, like the FTP. The process of extracting the tag can be improved, but this needs the changing of the method body, too. I know this can appear senseless, but try to imagine a particular situation, in which this approach would lead to a better performance. In fewer words, the TagGetter object has a too deep knowledge of the
global mechanism, and that's not good, because this leads to bad application design.
Since I'm going to solve these problems using Castle.Windsor (we'll see later
what is), I have to use the same words of its documentation to explain the concepts of component and
service:
"A component is a small unit of reusable code. It should implement and expose
just one service, and do it well. In practical terms, a component is a class
that implements a service (interface). The interface is the contract of the
service, which creates an abstraction layer so you can replace the service
implementation without effort."
Now that we know what a service and a component are, we can go straight to the
solution of the problem.
The TagGetter class does too much, and we have to split it: the two tasks of this
object are really generic and can be performed in several ways, so, we have to
create a service (interface) that defines the actions that an object can do, without writing the concrete implementation (component). Here are the 2
interfaces, one to download the file, the other to extract the content of a
tag.
public interface IPageDownloader {
string Download(string url);
}
public interface ITagExtrator {
string Extract(string content, string tagName);
}
Now we should write the concrete implementation of these interfaces.
public class PageDownloader : IPageDownloader {
public string Download(string url) {
return new System.Net.WebClient().DownloadString(url);
}
}
public class TagExtractor : ITagExtrator {
public string Extract(string content, string tagName) {
string tagContent = System.Text.RegularExpressions.Regex.Match(content, "<" + tagName + ">[^>]*</" + tagName + ">").Value;
tagContent = tagContent.Substring(tagName.Length + 2, tagContent.Length - 2 * (tagName.Length + 2) - 1);
return tagContent;
}
}
Uhm... here comes a little problem... how do we pass these objects to the main
TagGetter class? Simple: we change the constructor of this class to accept 2
parameters of type IPageDownloader
e ITagExtractor
, than we store them in some
variables. Here is the new code:
public class TagGetter
{
private IPageDownloader _pageDownloader;
private ITagExtrator _tagExtractor;
public TagGetter(IPageDownloader pageDownloader, ITagExtrator tagExtractor)
{
_pageDownloader = pageDownloader;
_tagExtractor = tagExtractor;
}
public string GetContent(string url, string tagName)
{
string page = _pageDownloader.Download(url);
string tagContent = _tagExtractor.Extract(page, tagName);
return tagContent;
}
}
As you can notice, the code of the method TagGetter.GetContent()
is more clean
and simple, and it doesn't care of how to download the page or extract the tag:
the implementations of the interfaces will do it! In this way, we can easily
change the downloader or the extractor without changing the main TagGetter
class! Also, we can easily reuse the single component in another application or
we can write specific tests for only one component.
However, this has a disadvantage: to call this method we should write:
string title = new TagGetter(new PageDownloader(), new TagExtractor()).GetContent("http://www.google.it", "title");
This isn't bad, but it's too long and complex for me. Here is where
Castle.Windsor comes to rescue us. In a few words, Castle.Windsor
is a container that you can configure to contain some objects, and than you can
get them later when you need. It's a sort of big box containing some objects;
there is an index written outside on which are matched the services and the
components. When you need a downloader, you look for a IPageDownloader
and you
find an instance of the PageDownloader
class. I think the example
is more explicative than the explanation.
WindsorContainer container = new WindsorContainer();
container.Register(Component
.For<IPageDownloader>()
.ImplementedBy<PageDownloader>());
container.Register(Component
.For<ITagExtrator>()
.ImplementedBy<TagExtractor>());
container.Register(Component
.For<TagGetter>());
TagGetter getter = container.Resolve<TagGetter>();
string title = getter.GetContent("http://www.google.it", "title");
As you can notice, the first 3 calls to Register()
are used to
register the interfaces and their concrete implementation (the container can be
configured using a XML file, too), but the most
interesting is the fourth: the call to Resolve()
returns a new
instance of the TagGetter class. But if you remember, this class has a
constructor with 2 parameters, so what's happened? When you call the Resolve()
method, the WindsorContainer checks the parameters of the constructor and, if
there is a registered service or component compatible with the type of the
parameter, the container automatically creates the right new instances (according
to the configuration) and so, the trick is done.
This is only a parenthesis about IoC, and I didn't show even the smallest part of
what we can do with the IoC or what Castle.Windsor can do, so I close this
paragraph with these references:
) - I haven't forgotten the opening bracket at the beginning of the paragraph!
Castle.Windsor is cool, but...
... but cannot be used on Windows Phone 7 devices. Yes, you read correctly.
Castle.Windsor cannot be used on Windows Phone 7 devices.
Uhm... and now? What can we do?
At the beginning I started to look for another version of Castle.Windsor working
on Windows Phone 7, but I've found nothing interesting. At this moment I started
to worry. Luckily, I've had an idea: I'll make my own container! A container can
seem very long and complex, but a class that has only some basic features is
very very simple.
In our case, a very simple container would have been enough, but, since I'm
crazy, I've written a container with some other functions. Here they are:
|
Method name
|
Description
|
|
ChangeRegistration<T>(Type)
|
Changes the registration for the type T . If the type T isn't registered, will be
made a the registration of the type T
|
|
Deregister<T>()
|
Deregisters the type T
|
|
GetComponentTypeForService<T>() + 1 ovrl. |
Returns the type associated with the service T
|
|
Register(WP7ContainerEntry) + 4 ovrl. |
Registers a new entry in the container
|
|
Resolve<T>() + 1 ovrl. |
Resolves the registration for the type T
|
Another important object to analyze is WP7ContainerEntry
:
|
Member name
|
Description
|
|
Component
|
The type of the component represented by this entry
|
|
Params
|
Dictionary<string, object> containing the properties to inject (used to inject
parameters in the constructor)
|
|
Service
|
The type of the
service represented by this entry
|
|
For<T>() + 1 ovrl. |
Static method which creates a new WP7ContainerEntry for the provided type T
|
|
ImplementedBy<T>() + 1 ovrl. |
Sets the Component property
|
|
Parameters(Dictionary<string, object>) |
Sets the
Params property
|
Instead of writing all these useless words, let the code speak: let's see an
example of the usage of this classes. Imagine we have something like this:
In a few words, we have a class, MyClass
, which inherits from MyBaseClass
and
implements IMyInterface
; the constructor of this class needs an UsefulObject
as
parameter. In a scenario like this, you can use the container in a lot of
manners, for instance, this:
WP7Container container = new WP7Container();
container.Register<UsefulObject>();
container.Register<IMyInterface, MyClass>();
IMyInterface myObj = container.Resolve<IMyInterface>();
The first thing we have to do is creating a new instance of the container, and
then, registering all the types we need. Whenever you want, now you can call the
Resolve
method, and get an instance of one of the registered types.
A nice thing to notice is that we don't need to pass any parameters to the
constructor of the class: the container automatically finds the constructor that
we can call, passing to it whatever it needs, even by resolving other types. In
this case, the container notes that the only available constructor needs an
UsefulObject
, so, to create a new MyClass
object, passes to the constructor a
new UsefulObject
. If there are no constructors that the container
can call, an exception is thrown.
WP7Container container = new WP7Container();
container.Register<IMyInterface, MyClass>(new Dictionary<string, object>() {
{ "PropertyOfTheInterface", "interface" },
{ "PropertyOfTheClass", 10 },
{ "PropertyOfTheBaseClass", "base" }
});
container.Register<UsefulObject>(new Dictionary<string, object>() {
{ "UsefulProperty", "injected!" }
});
UsefulObject usefulObj = container.Resolve<UsefulObject>();
IMyInterface obj = container.Resolve<IMyInterface>();
This time, instead, when we register the types, we specify as parameters the name
of the property we want to inject and its value: in this way, when we call
Resolve
, our object has already some properties set. The parameters
can also be used to inject values to the constructor: in this case, we must
specify the name of the parameter of the constructor and its value. The call to
Resolve<UsefulObject>
returns a new UsefulObject
, where UsefulProperty
is set to
"injected!". The call to Resolve<IMyInterface>
, instead, creates a new MyClass
object and set its properties (they can belong to the base class, or they can be an
implementation of an interface; it doesn't matter); during the call to the
constructor, is called Resolve<UsefulObject>
: in this way, will be passed to the
constructor of MyClass
a new UsefulObject
with the UsefulProperty
property set
to "injected!". In fact, if you evaluate
((MyClass)obj).UsefulStructFromTheConstructor.UsefulProperty
, you see that the
property has value "injected!".
What WP7Container
does is nothing, if we compare it to Castle.Windsor, but I
think it's enough for a very little class like this, isn't it? But now
it's time to dig a little deeper in the classes to discover how they work.
WP7ContainerEntry
is only a class containing some data, so it doesn't need a lot
of explanations; on the contrary, WP7Container
can be a little more interesting.
This is the basic concept: we have a list of WP7ContainerEntry
; we should be
able to add and remove entries from that list (using Register
and Deregister
methods) and resolve a type (Resolve
method). The core of the
whole class is the method ResolveEntry
: it transforms a WP7ContainerEntry
to the object that represents, finds the right constructor and injects the parameters.
It can be very difficult to write a method like this, but it you know what
reflection is, the trick is done. However, here is the code:
ConstructorInfo ctor =
entry.Component.GetConstructors()
.FirstOrDefault(x =>
x.GetParameters().All(y =>
(entry.Params.ContainsKey(y.Name) && y.ParameterType.IsAssignableFrom(entry.Params[y.Name].GetType()))
|| IsThereEntryForType(y.ParameterType)
)
);
if (ctor == null)
throw new ArgumentException(string.Format("Cannot create type {0}: constructor not found. Try with different parameters.", entry.Component.FullName));
ParameterInfo[] ctorParams = ctor.GetParameters();
object[] pars = new object[ctorParams.Count()];
for (int i = 0; i < pars.Length; i++) {
ParameterInfo p = ctorParams[i];
if (entry.Params.ContainsKey(p.Name))
pars[i] = entry.Params[p.Name];
else
pars[i] = this.Resolve(p.ParameterType);
}
object obj = ctor.Invoke(pars);
foreach (var x in entry.Params) {
PropertyInfo prop = entry.Component.GetProperty(x.Key);
if (prop != null) {
if (!prop.PropertyType.IsAssignableFrom(x.Value.GetType()))
throw new InvalidCastException(string.Format("Cannot cast from {0} to {1}", x.Value.GetType().FullName, prop.PropertyType.FullName));
else
prop.SetValue(obj, x.Value, null);
}
}
return obj;
What we want to do is creating a new instance of the component of the entry, so,
what we must do is finding a constructor that we can call. To achieve this goal,
we reiterate through all the available constructors of the type and we find the
first one whose parameters are all available, or because they are inside the
parameters dictionary, or because they are registered in the container. Once
we've found the constructor, we must find the parameters: we check each param
and, if it's in the dictionary, we take the supplied value, otherwise, we
resolve the type. Next step is creating the object and storing it in a variable.
The last thing to do is injecting the parameters: we reiterate through all the
supplied entries of the dictionary and check if we can set the value of the
property.
As I've said before, this class has limited features: I advice you to use this
one only if you cannot use Castle.Windsor, even if I've tried to
maintain the
same syntax.
Movement controllers
The movement controllers are the components that have to raise events when the
user sends the input to change the direction of the snake. Since there are many ways to change the direction,
we have to create an abstraction layer and then its implementations. Also, if we
want to add a new controller, it's really simple: what we need is only develop
that component.
This is the
service for the controllers:
|
public interface IMovementController
{
event EventHandler Up;
event EventHandler Down;
event EventHandler Left;
event EventHandler Right;
bool IsVisual { get; }
FrameworkElement GetVisual();
}
|
As you can see, this interface contains 4 events (one for each direction); they
are raised when the user sends the corresponding input. The next member (the
IsVisual
property) returns a value indicating whether the
controller needs a visual object to work properly; the GetVisual()
method,
instead, returns that object.
Now it's time to speak about components (implementations of the
IMovementController
interface)!
The first controller we'll develop is the ArrowMovementController
:
it's based on a visual control made up of 4 arrows, one for each direction. Each of them is a different button
which raises the corresponding event. I won't describe this component, because
it's just a restyling of the classic button, and we'll talk about graphics in
the second part of the article. However, it
should look like this:
Goals
Each level must have a target to achieve; since there are a lot of different
types of goal, we have to create the common service and then we can develop the
single components. This is the abstraction layer:
|
public interface IGoal
{
bool IsAccomplished(GoalEventArgs e);
Uri ImageUri { get; }
}
|
As you can see, the interface is very simple. The main method is
IsAccomplished()
: the e
argument of type GoalEventArgs
contains some data (like
the eaten food or the length of the snake); the method should check these values
and return true
, if the goal is accomplished, otherwise false
.
The ImageUri
property returns only the Uri of the corresponding image.
For example, this is the FoodEatenGoal
:
public class FoodEatenGoal : IGoal
{
private int _Needs;
public FoodEatenGoal(int needs)
{
_Needs = needs;
}
public bool IsAccomplished(GoalEventArgs e)
{
return e.EatenFood >= _Needs;
}
public Uri ImageUri { get { return new Uri("/SnakeMobile.Core;component/Images/Food.png", UriKind.Relative); } }
}
The constructor needs a int
parameter, which is the minimum amount of food that
the snake must eat before the goal is accomplished; the IsAccomplished()
method
returns whether the snake has eaten enough food.
The other 2 goals (GrowUpGoal
and TimeGoal
) work in the same way, and I won't
write about them.
Levels
So, we've arrived here, at the most complex part of the whole application...
Are you ready? Let's start!
Let's start from the target of this class: Level
must manage the whole game
level and raise some events when something interesting happens. These events are
Win
and Loose
. How their name says, the first is raised when the user wins the
game, achieving all the goals of the level; the second one, instead, when the
snake is killed by an enemy snake, or when it hits a wall. The third event
(FoodAdded
) is marked as internal because it must be handled only
by some internal classes, and it's used to notify to an enemy snake when a new
food is added. The need of this event will be explained later, when we'll
talk about the FoodCatcherSnake
.
Now it's time to speak about how the Level
object works.
Let's start from the constructor. It needs a lot of parameters; each of them is
then stored in a private field and used later. Here is a list of them:
IMovementController movementController
: movement controller to use
to move the main snake
int speed
: speed of the game. Actually it's only the interval of the timers.
IEnumerable<IGoal> goal
: all the goals the the user has to
achieve to
complete the level
Cell[,] cells
: two-dimensional array containing all the cells of the game,
already filled with the right content (CellType property already set)
IEnumerable<Point> snakeCells
: list of the coordinates of the cells of the snake
IEnumerable<IEnemySnake> enemySnakes
: list of the enemy snakes in the level. We'll talk about this later.
This is the body of the constructor:
InitializeComponent();
switch (System.Threading.Thread.CurrentThread.CurrentUICulture.Name.ToLower())
{
case "it-it":
this.Resources.MergedDictionaries.Add(new ResourceDictionary() {
Source = new Uri("/SnakeMobile.Core;component/Resources/it-IT.xaml", UriKind.Relative)
});
break;
default:
this.Resources.MergedDictionaries.Add(new ResourceDictionary() {
Source = new Uri("/SnakeMobile.Core;component/Resources/en-US.xaml", UriKind.Relative)
});
break;
}
_speed = speed;
_gridSize = new Size(cells.GetLength(0), cells.GetLength(1));
_cells = cells;
_goals = goal;
_snakeQueue = new Queue<Cell>();
foreach (Point p in snakeCells)
_snakeQueue.Enqueue(cells[(int)p.X, (int)p.Y]);
_originalSnakeCells = snakeCells;
_enemySnakes = enemySnakes;
_originalCells = new Cell[(int)_gridSize.Width, (int)_gridSize.Height];
CloneAndAssign(cells, _originalCells);
_originalSnakeQueue = new Queue<Cell>();
foreach (Point p in snakeCells)
_originalSnakeQueue.Enqueue(_originalCells[(int)p.X, (int)p.Y]);
movementController.Up += MovementController_Up;
movementController.Down += MovementController_Down;
movementController.Left += MovementController_Left;
movementController.Right += MovementController_Right;
if (movementController.IsVisual)
ContentPresenterMovementController.Content = movementController.GetVisual();
_snakeTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(speed) };
_snakeTimer.Tick += SnakeTimer_Tick;
_enemySnakesTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(speed * 2) };
_enemySnakesTimer.Tick += EnemySnakesTimer_Tick;
this.Loaded += UserControl_Loaded;
this.SizeChanged += UserControl_SizeChanged;
As you can see, in the constructor we just store the params in some private
fields, initialize the timers, add handlers and so on... A part that needs some
explanation is the switch statement: the application is developed using the English
language, but since I'm Italian, I decided to make the whole game localizable.
This means that the app is available in English and Italian, and it
automatically switches the language according to the global language of the
phone. Obviously, for a French user the game will be in English, because it's
the default language.
Making an application localizable seems very boring, but I can grant you that the
most annoying part is.. is... nothing!
This is the idea: you must have a source for all the localized strings (or
everything else that has to be localized), and when you need a string, you just
have to get it from that source. In this case, the source is a
ResourceDictionary
containing some strings. It should be something like this:
en-US.xaml
|
it-IT.xaml
|
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
-->
<sys:String x:Key="OK">OK</sys:String>
<sys:String x:Key="Retry">Retry</sys:String>
-->
<sys:String x:Key="Goal_FoodEaten">Food eaten</sys:String>
<sys:String x:Key="Goal_GrowUp">Length</sys:String>
<sys:String x:Key="Goal_Time">Time</sys:String>
-->
<sys:String x:Key="Overlay_Text_ReportGoal">Goals report</sys:String>
<sys:String x:Key="Overlay_Text_Death">You are dead!</sys:String>
<sys:String x:Key="Overlay_Text_Win">Level completed</sys:String>
<sys:String x:Key="Overlay_ButtonText_Return">Return to menu</sys:String>
</ResourceDictionary>
|
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
-->
<sys:String x:Key="OK">OK</sys:String>
<sys:String x:Key="Retry">Riprova</sys:String>
-->
<sys:String x:Key="Goal_FoodEaten">Cibo mangiato</sys:String>
<sys:String x:Key="Goal_GrowUp">Lunghezza</sys:String>
<sys:String x:Key="Goal_Time">Tempo</sys:String>
-->
<sys:String x:Key="Overlay_Text_ReportGoal">Riepilogo obiettivi</sys:String>
<sys:String x:Key="Overlay_Text_Death">Sei morto!</sys:String>
<sys:String x:Key="Overlay_Text_Win">Livello completato</sys:String>
<sys:String x:Key="Overlay_ButtonText_Return">Ritorna al menu</sys:String>
</ResourceDictionary>
|
As you can see, in these little dictionaries there are some pairs of strings, one
in English an the other in Italian. Important: same strings in different
languages must have the same key. Why this? Because when you need a string,
e.g. "Retry", you just call this.Resources["Retry"]
and you get the localized string. Coming back to the constructor of the Level
object, the switch statement is used to fill the resources of the control with
the localized strings,
so that a call to this.Resources["Retry"]
returns "Riprova", if we are in Italy,
otherwise, "Retry", in all the other countries.
Come back to the Level object. Maybe now you're asking where the game come to
life. Here is the answer: in the UserControl_Loaded()
method (handler of the
Loaded
event of the base class UserControl
). Here...
uhm... the words are useless now, let the code speak!
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
MainOverlay.Text = Resources["Overlay_Text_ReportGoal"] as string;
MainOverlay.AdditionalContent = _goals.BuildReport(32, 22);
MainOverlay.BackgroundColor = Colors.Orange;
MainOverlay.Show(new KeyValuePair<string,>(Resources["OK"] as string, () => {
_beginTime = DateTime.Now;
_snakeTimer.Start();
_enemySnakesTimer.Start();
for (int i = 0; i < _gridSize.Width; i++)
MainGrid.ColumnDefinitions.Add(new ColumnDefinition());
for (int i = 0; i < _gridSize.Height; i++)
MainGrid.RowDefinitions.Add(new RowDefinition());
for (int x = 0; x < _gridSize.Width; x++)
for (int y = 0; y < _gridSize.Height; y++) {
Cell cell = _cells[x, y];
MainGrid.Children.Add(cell);
Grid.SetColumn(cell, x);
Grid.SetRow(cell, y);
}
}));
}</string,>
But... but... what's MainOverlay? It's an OverlayMessage
!
SnakeMobile.Core.OverlayMessage is a control that allows you to create a sort of
"message layer" containing some text, some buttons and an optional content. The
Show()
methods needs a KeyValuePair<string, Action>[]
; the key of
each KeyValuePair is the text of the button, and its value is the delegate to
invoke when the button is clicked. I've written this control because I sometimes
need to ask something to the user, and each action to do when the user clicks on
a button is different case by case, so, using an OverlayMessage, I can avoid
writing a lot of useless code to manage different handlers.
Now, it's time for the most interesting part of the application: the control of
the snake.
In the first part of the previous picture, I've put the snake in a grid (head is
at (2, 1)), and we assume that it would like to eat some food on the left.
The direction of the snake is represented by a Point
object (stored in the
private field _direction
). Its coords are only -1, 0, 1, because they represent
the variation of the coords between the head cell and the next cell. An example
may clear the ideas. The head is at (2, 1) and the user wants to move the snake
on the left. The left direction is the point (-1, 0), because if we sum it to
the coords of the head, we obtain the coords of the cell that will be the next
head (blue circle in the second part of the picture).
(2, 1) + (-1, 0) = (1, 1)
The snake cells are managed using a Queue<Cell>
: in the constructor, we add these
cells, so that at the top of the queue there's the tail and at the bottom the
head.
So, to make the snake move, we have to do a lot of actions:
- First, we change the
CellType
property of the last cell, so that it becomes a
normal segment of tail
- Then, we call
Queue<T>.Dequeue()
, removing the top element
- We update the top cell, so that it becomes the last tail of the snake
- Finally, we can enqueue the new head cell. Obviously, the new head cell is
calculated using the concept of the direction explained before.
- At this point, the actions to do are finished, and we start again from the first
point.
I won't write about the code to implement this, because it's simple, but very
very long.
However, the SnakeTimer_Tick
method (handler of the Tick
event of the timer that
makes the snake move) does other 2 things of which we haven't spoken about.
What happens when a snake hits a wall? Or how can it grow up? When should the
level finish? We'll discover the answer to all these questions in the next
episode! No, no, I was joking, and the answer is here, and it's simple, too.
Between the first and the second step of the procedure explained before, there's
a little detail that I must show you: after the calculation of the next head
cell, we must see what happens when the head hits the next cell.
bool grown = false;
switch (nextCell.HitTest(head))
{
case HitTestResult.Eat:
_foodEaten++;
TxtFoodEaten.Text = _foodEaten.ToString();
grown = true;
AddFood();
break;
case HitTestResult.Death:
KillSnake();
return;
case HitTestResult.Cut:
if (_snakeQueue.Contains(nextCell))
foreach (Cell cutted in _snakeQueue.Cut(_snakeQueue.IndexOf(nextCell)))
cutted.CellType = CellType.Free;
else
_enemySnakes.ForEach(x => x.Cut(nextCell));
break;
case HitTestResult.None:
break;
}
if (!grown) _snakeQueue.Dequeue().CellType = CellType.Free;
As you can see, we switch on the result of the hit test between the head cell and
the next one. If the result is Eat (one of the cells is Food), we increment the
field _foodEaten
and set the variable grown
to false, because this allows us to
skip the step number 2 (dequeuing the last tail). If the result is Death (one of
the cells is a Wall or a DeathFood), the snake dies and the game ends. If the
result is Cut (a snake cuts another snake), we check if the cut snake is the
user's snake, otherwise, we try to cut the enemy snakes (we'll talk about the
field _enemySnakes
later).
The latest unanswered question is "When the game ends?": it depends on when you
achieve all the goals of the level. Each level, to be completed, has its own set
of goals to achieve. If you remember, the constructor of the Level object needs
a parameter representing the goals of the level; these goals are stored in the
field _goals. At the end of the SnakeTimer_Tick
method, we check
them all if they have been completed. If so, we stop all the timers and show a
message to the user to inform him that the level has been finished and he has won.
GoalEventArgs gea = new GoalEventArgs(_snakeQueue.Count, _foodEaten, DateTime.Now.Subtract(_beginTime));
if (_goals.All(x => x.IsAccomplished(gea))) {
_snakeTimer.Stop();
_enemySnakesTimer.Stop();
MainOverlay.AdditionalContent = null;
MainOverlay.Text = Resources["Overlay_Text_Win"] as string;
MainOverlay.BackgroundColor = Colors.Green;
MainOverlay.Show(new KeyValuePair<string,>(Resources["Overlay_ButtonText_Return"] as string, () => RaiseEvent(this.Win, this, gea)));
}</string,>
Enemy snakes
Actually, there's one last aspect of the game of which we haven't spoken about:
the enemy snakes. If you notice, some enemy snakes move randomly, and others
point directly to a food cell. To solve the problem, I've created the interface
which contains the actions that an enemy snake has to do:
As you can see, there are only one property and 3 methods: the property is the
Level
object which contains the enemy snake, and it must be set as soon as it's
possible. Reset
has the task of resetting all the private fields to restore the
initial state of the snake; Cut
, instead, must cut the snake in the given cell.
The most interesting thing is the Move
method: how the name says, this method
must make the snake move, and here the 2 types of snakes (randomly moving one,
and food catcher one) are different. The RandomlyMovingSnake
is the simplest
one, because it moves without any logical scope, and, for this, we won't speak
about it. On the contrary, we'll explain the FoodCatcherSnake
,
because it's more
complex than the first one, and we don't like simple things, right? :P
This snake has to point directly to some food cells. But... but... how to find
the path to the food? Searching on Google some path finding algorithms, I've
found a lot of implementation of the A* (really impressing, you should see one
of them in action!), but they were too much for my needs! Luckily, after some
other searches I've found this.
The only thing I've modified is the return value of the function FindPath
from
int[,]
to IEnumerable<Point>
, nothing else. (I
advice you to read the article explaining the MazeSolver, otherwise, you couldn't
understand the following)
In the constructor we initialize the MazeSolver by setting the walls, i.e. the cells with the CellType
property set to Wall
or DeathFood. In the Move method, instead, we use the MazeSolver to find the
path to follow:
if (_pathToFollow.Count() == 0 ||
_cells[(int)_pathToFollow.PeekLast().X, (int)_pathToFollow.PeekLast().Y].CellType != CellType.Food) {
List<point> lst = new List<point>();
for (int x = 0; x < _gridSize.Width; x++)
for (int y = 0; y < _gridSize.Height; y++)
if (_cells[x, y].CellType == CellType.Food)
lst.Add(new Point(x, y));
_foodCells = lst;
if (_foodCells.Count() == 0) throw new Exception();
Point furthestFood = _foodCells.OrderBy<point,>(x => Math.Abs((int)x.X) + Math.Abs((int)x.Y)).First();
_snakeQueue.ForEach(x => _mazeSolver[Grid.GetColumn(x), Grid.GetRow(x)] = 1);
_mazeSolver[Grid.GetColumn(head), Grid.GetRow(head)] = 0;
_pathToFollow = new Queue<point>(_mazeSolver.FindPath(Grid.GetColumn(head), Grid.GetRow(head), (int)furthestFood.X, (int)furthestFood.Y).Skip(1));
_snakeQueue.ForEach(x => _mazeSolver[Grid.GetColumn(x), Grid.GetRow(x)] = 0);
if (_pathToFollow.Count == 0) return;
}</point></point,></point></point>
If a new path is needed, first, we find all the cells containing a food, then, we
find the furthest one, and store the path in a Queue<Point>
.
At each call of the Move method, we peek an element from this queue and move the
snake at these coords.
Saving and loading levels
As you can see, in the game there are a pinch of default levels; obviously, each
of them isn't a different class: in the Levels folder there are some XML files
representing the default levels; at runtime, they are loaded by the method
Parse
of the LevelParser
class, which returns an instance of the Level class,
already initialized.
Before we speak about the parsing method, we need to know how an XML file is.
Here is the structure:
The root element Level
has a single attribute Speed
, representing the speed of
the game (the interval between each tick of the main snake timer). Inside it
there are a lot of other different types of nodes:
Info
: this section contains some information about the level, like the title
and the description. The IsResource
attribute is a value indicating whether the
strings contained in the Title
and Description
nodes are the key to look in the
resource dictionary.
Goals
: container for the goals of the level. Each goal is represented by a Goal
node, and its attributes Type
and Param
are respectively the type of the snake
(RandomlyMovingSnake or FoodCatcherSnake) and the parameter to pass to the
constructor.
- Cells: this node is the representation of the initial state of the game grid.
The Width and Height attributes are, as their names say, the size of the grid.
The content of this node is a bit particular: there is a series of chars put on
different lines; each char is a single cell, and this is the legend:
- # => Free cell
- W => Wall cell
- D => DeathFood cell
- F => Food cell
- L => LastTail cell
- T => Tail cell
- H => Head cell
- / => EnemyLastTail cell
- - => EnemyTail cell
- * => EnemyHead cell
UI and design
So, we arrived here.
The core part of the game is now completed, and it's time to build the graphical
part of the game. Here we'll speak about graphical controls, pages, navigation,
and some aspects more relevant to the game, like the main menu or the level
selection screen.
Navigation
If you are a "Silverlighter", the concept of the navigation isn't new for you,
but a revision is never bad, right?
However, let's start from the beginning.
Imagine you're using your browser: you navigate through links to visit pages.
When you're bored of the current page, or you've made a mistake, you can press
the "Back" button, and you are magically taken to the previous page. The idea is
the same for the Windows Phone 7: you use your apps, but you must be able to go
back to the previous page, only by clicking the "Back" button.
Don't be afraid! It's not difficult to do this! The most of the work is done by
the system: you have only to implement the forward navigation. When you want to
change page, is enough calling the Navigate
method of the NavigationService
class (you can get an instance of this class by accessing the property with the
same name belonging to the current Page
instance); the parameter is a Uri
pointing to the next page.
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
btnNewGame.Click += BtnNewGame_Click;
}
private void BtnNewGame_Click(object sender, RoutedEventArgs e) {
this.NavigationService.Navigate(new Uri("/MyApp;component/Page2.xaml", UriKind.RelativeOrAbsolute));
}
}
Finished. You have to do nothing else: the "Back" action is automatically handled
by the system. Obviously, you can handle this event and change the
behaviour.
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
btnNewGame.Click += BtnNewGame_Click;
BackKeyPress += new EventHandler<System.ComponentModel.CancelEventArgs>(MainPage_BackKeyPress);
}
void MainPage_BackKeyPress(object sender, System.ComponentModel.CancelEventArgs e)
{
if (MessageBox.Show("Are you sure you want to go back?", "", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel)
e.Cancel = true;
}
private void BtnNewGame_Click(object sender, RoutedEventArgs e) {
this.NavigationService.Navigate(new Uri("/MyApp;component/Page2.xaml", UriKind.RelativeOrAbsolute));
}
}
In this case, the only change I've done is a message box which asks to the user if
he really wants to go back; if the answer is "ok", nothing happens and the page
changes, otherwise, we cancel the event using e.Cancel = true
, and the action is
canceled.
Transitions
In the previous chapter we've spoken about navigation, but, if you notice, it's
very ugly, because the next page is replaced to the previous one without any
effects. This is where the Silverlight for Windows Phone Toolkit comes to
rescue us. (download link)
The first thing to do (after downloading the toolkit and adding the references to
its assembly Microsoft.Phone.Controls.Toolkit), is changing the frame from
PhoneApplicationFrame
to TransitionFrame
. Open App.xaml.cs and change the
following in the InitializePhoneApplication
method:
private void InitializePhoneApplication()
{
if (phoneApplicationInitialized)
return;
RootFrame = new Microsoft.Phone.Controls.TransitionFrame(); RootFrame.Navigated += CompleteInitializePhoneApplication;
}
But... what have we done? To answer this question, we must go a bit back:
(Image taken from MSDN)
This is the composition of every Windows Phone 7 application. The topmost
container is a PhoneApplicationFrame
; this object contains a
PhoneApplicationPage
, which hosts your content. By calling
NevigationService.Navigate
, we tell the frame to change its content page, so, if
we replace the deafult frame with another able to make some transition effects,
the trick is done. And this is exactly what we've done before: the
TransitionFrame
, in fact, is a special frame which animates the pages during the
navigation
However, now it's time to choose the transition effects: luckily, even this is
simple. The toolkit provides some very useful attached properties that we can
set for each page. NavigationInTransition
and NavigationOutTransition
are,
respectively, the animation to use when the user navigates to the page and when
the user exits the page. Both of them have 2 important properties: Backward
and
Forward
. The first one is the animation to use during the backward navigation,
and the other in the forward navigation.
When you navigate from a page to another, the first one begins a ForwardOut
animation, while the second one, a ForwardIn. During a backward navigation, the
first page begins a BackwardOut animation, while the second one, a BackwardIn.
Here is the code to use to apply a transition effect on a page:
<phone:PhoneApplicationPage
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit">
-->
<toolkit:TransitionService.NavigationInTransition>
<toolkit:NavigationInTransition>
<toolkit:NavigationInTransition.Backward>
-->
<toolkit:TurnstileTransition Mode="BackwardIn"/>
</toolkit:NavigationInTransition.Backward>
<toolkit:NavigationInTransition.Forward>
-->
<toolkit:TurnstileTransition Mode="ForwardIn"/>
</toolkit:NavigationInTransition.Forward>
</toolkit:NavigationInTransition>
</toolkit:TransitionService.NavigationInTransition>
-->
<toolkit:TransitionService.NavigationOutTransition>
<toolkit:NavigationOutTransition>
<toolkit:NavigationOutTransition.Backward>
-->
<toolkit:TurnstileTransition Mode="BackwardOut"/>
</toolkit:NavigationOutTransition.Backward>
<toolkit:NavigationOutTransition.Forward>
-->
<toolkit:TurnstileTransition Mode="ForwardOut"/>
</toolkit:NavigationOutTransition.Forward>
</toolkit:NavigationOutTransition>
</toolkit:TransitionService.NavigationOutTransition>
-->
</phone:PhoneApplicationPage>
Obviously, you can choose whatever effect you want, instead of
TurnstileTransition
, like RollTransition
, RotateTransition
, SlideTransition
or SwivelTransition
.
Rounded glowing button
However, it's time to come back to our game. Let's start from the main menu!
As you can see, this is a menu made up of 3 items (the others are useless): the
first one, Continue, is used to resume the current game; New game erases all the
data and starts a new game; Settings, instead, navigates to the settings page.
Each item is a Button
that I've templated to change its
appearance. I've used
Microsoft Expression Blend 4 For Windows Phone to achieve this result (this tool
is included in the Windows Phone 7 Developer Tools), and this is how I've done.
I state that I'm not a designer, I'm only a programmer which tells his
experiences with Blend, so, if you want to tell me how to improve my design
skills, any advice would be welcome!
If you cannot read everything in the images, you can click them to enlarge them.
-
Create a new project, and add a new Button
on the main page. Right-click the
button and select Edit Template > Create Empty
-
In the following window, create a new resource dictionary with name
"RoundedGlowingButton.xaml" and set the name of the resource to
"RoundedGlowingButton".
-
Now, you are editing the template of the button. Add a new ellipse and make it
fill all the available area, then remove the stroke and bind its Fill
property
to the Background
property of the templated parent (Template binding >
Background).
-
Duplicate the ellipse and change its Fill
property: set a radial brush
from white to transparent.
-
Select the Gradient tool and modify the brush to obtain something like
this (the light must seem coming from the bottom).
-
Duplicate again the first ellipse and set its Fill
to white, then change its
Opacity
to 20%.
-
Right-click the last ellipse and convert it to a Path
object (Path > Convert
to Path).
-
Select the Direct Selection tool and change the points of the path to make
them like this.
-
Now, it's time to show the content of the button: open the other assets, type
"content" in the search box and select the ContentPresenter
control.
-
Add a new ContentPresenter
and center it horizontally and vertically, then bind
its Content
property to the Content
property of the templated parent.
-
Duplicate another time the first ellipse we've created and place it on the back,
then make it a little bigger by setting a scaling of 1.5.
-
Change its Fill property to a radial brush from white to transparent, then select
the Gradient tool and set the white gradient stop on the border of the button,
and the other one a bit farther.
-
Now, the most complex part is finished! But what if we add some animations? I
think it's not a bad idea! So, select the States tab and set the default
transition duration to 0.1 seconds; click on the arrow near "Pressed" and
select * > Pressed.
-
Now click "Pressed". As you can see the whole drawing area has a
red border now: this means that Blend is recording the changes to transform in
storyboards, I mean, in this way, every change we make, is changed in
storyboard, which is played when the button enters the selected state ("Pressed"
in our case). In other words, when we click the button, is played a storyboard
which applies the changes we've made using Blend. If you want to see a preview
of this storyboard you can activate the button Transition preview and
select different states: Blend automatically plays these storyboards and you can
see their effects directly in the drawing area. However, now that you've
selected the Pressed state, select the InnerGlow ellipse and
change the first gradient stop of the fill to #FF3D71D8 (or whatever color you
want).
Triggers
Another interesting thing you can notice in the main menu is the triggers. But,
first of all: what is a trigger?
A trigger is an object which calls an action when an event is fired. An example
here may clean the ideas: i.e., we want to make a trigger that allows us to
navigate to a page when the user clicks on a button (like in the application).
So, in order to achieve the result, we have to do some things:
- First of all, we have to make us sure that there is a reference to the assembly
System.Windows.Interactivity
, because everything we are talking about is
residing into this assembly
- Then we have to make our custom action: let's create a class which inherits from
TriggerAction<T>
, where T
is the type of the object to which this action can be
attached, in our case T
is Button
.
public class NavigationTrigger : TriggerAction<Button>
{
#region Uri property
public static readonly DependencyProperty UriProperty =
DependencyProperty.Register("Uri", typeof(Uri), typeof(NavigationTrigger), new PropertyMetadata(null));
public Uri Uri
{
get { return (Uri)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
#endregion
protected override void Invoke(object parameter)
{
((App)Application.Current).RootFrame.Navigate(this.Uri);
}
}
As you can notice, there are the dependency property Uri
and the overriding method Invoke
.
The most important part of the class is the method: it's what's called when the
event is fired. Here we write the heart of the trigger, so, I've written the
code for the navigation to the uri indicated by the Uri
property.
- Now the code behind is finished; what we must do is, from now on, edit only the
XAML code. So, let's open our XAML file and add some new xmlns:
<phone:PhoneApplicationPage
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:local="clr-namespace:SnakeMobile">
...
</phone:PhoneApplicationPage>
- At last, we must change the code of our button in this way:
<Button Content="Settings">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:NavigationTrigger Uri="/SnakeMobile;component/Settings.xaml" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
With this code, we register an EventTrigger
for our button which, when the event
Click
is fired, calls our action NavigationTrigger
which navigates to
/SnakaMobile;component/Settings.xaml
As you've seen, we've created with a few lines of code an unit of code that we
can use wherever we want, only by using the XAML code. Creating a trigger only
for a navigation action, can seem wasted time, but I've done so because I want
to explain you some of the features of Silverlight for Windows Phone, even if
they can lead me to write some useless code. Imagine a big business application:
the use of the triggers leads you to waste a lot less time.
However, if you want to deepen the concept of the triggers, I advice you this
article, which talks about behaviors, too: Silverlight and WPF Behaviours and Triggers - Understanding, Exploring And Developing Interactivity using C#, Visual Studio and Blend
A little math refresher: the polar coordinate system
Before starting to speak about the circular selector, we have to do a little math
refresher.
Let's start from a thing everybody knows: the Cartesian plane. This is what
Wikipedia says:
"A Cartesian coordinate system specifies each point uniquely in a plane by a pair of numerical coordinates, which are the signed distances from the point to two fixed perpendicular directed lines, measured in the same unit of length.
Each reference line is called a coordinate axis or just axis of the system, and
the point where they meet is its origin."
Translated into a human language, this means that in our plane we have two
perpendicular lines (called axis), and each point of the plane is identified by
a pair of numbers indicating, respectively, the projection of the point on the
X-axis, and on the Y-axis.
In this example, I've put 4 points on the plane: the text near each of them
represents the coordinates of the point. E.g., the blue point is (4, 5) because,
horizontally, it's 4 units far from the origin, and, vertically, 5 units.
But you should already know this things, right? Obviously, the answer is yes, so
we go straight forward to our new system: the polar coordinates system. As
usual, let's see what wikipedia says:
"In mathematics, the polar coordinate system is a two-dimensional coordinate system in which each point on a plane is determined by a distance from a fixed point and an angle from a fixed direction."
This may seem more complex than the Cartesian system, but analyze better the
text: we have a fixed point and a fixed direction from which we start measuring
the angles. Each point of the plane now has 2 different coordinates type: a
number and an angle.
The blue point: the first number (7) is the distance from the fixed point (center
of the system) and the second one (60°) is the angle between the point and the
fixed direction (horizontal line). For the green point, the previous graph is
self-explicative.
When we start speaking about curves on the plane, we can define each point as:
Here, θ is the angle of the point and r(θ) is a function returning
the distance of the point from the origin.
However, why have I told you all these things? Why should I use the polar system
instead of the Cartesian one? Sincerely, I don't know if there is an accurate
way to choose between them, but I think I can give you an example to make you
understand what I think: the circle. Imagine a circle with the center on the
origin: these are the two equations.
Cartesian plane:
Here, we have an equation of the second degree, where r is the radius of
the circle. (Not impossible, but ugly, isn't it?)
|
Polar system:
Here, the equation is very simple: for each value of theta, we return the radius
of the circle. This means that for each angle, the distance from the fixed point
is always the radius of the circle.
|
The example of the circle is simple, but try to imagine something a bit more
complex, e.g. a spiral:
(Image taken from Wikipedia)
I don't know if there is a way to do the same thing in Cartesian coordinates, but
I know that this is the polar equation of this Archimedean spiral:
Simple, isn't it? Changing the parameter a will turn the spiral, while
b controls the distance between the arms.
The polar system is cool, but... how can I implement this system in my
application?? The .NET Framework understands only Cartesian coordinates!! The
problem is quickly solved: we have only to make a conversion function from the
polar system to the Cartesian one.
The reason of these 2 equations is really simple:
(Image taken from Wikipedia)
As you can see, our point with polar coordinates (r, θ) can be placed in a Cartesian plane;
using the trigonometric functions sine and cosine we can calculate the
projections of the point on the X-axis and on the Y-axis.
But now, it's time to let the code speak! This is the conversion function:
private Point PolarToCartesian(double r, double theta) {
return new Point(r * Math.Cos(theta), r * Math.Sin(theta));
}
And now? Nothing. We can finally start talking about our CircularSelector
.
Circular selector
And so, this is a screenshot of our control; basically, it's made up of a certain
number of items (circular sectors), and each of them is customizable by setting
header, color or visibility. However, this is the punctual list of every
interesting property of this control (the properties are dependency properties,
so you can bind them):
|
Property name
|
Property type
|
Description
|
|
SweepDirection
|
SweepDirection
|
One of the values of the System.Windows.Media.SweepDirection enum
indicating the sweep direction (clockwise or counterclockwise)
|
|
LinesBrush
|
Brush
|
Brush used to draw the border of each item
|
|
AlwaysDrawLines
|
bool
|
Gets or sets a value indicating whether the border of each item should be drawed even if it's not visible
|
|
SelectedItemPushOut
|
double
|
Gets or sets a value indicating the amount of pixels
of which the selected item must go out
|
|
FontFamily
|
FontFamily
|
Family of the font used for the headers
|
|
FontSize
|
double
|
Size of the font used for the headers
|
|
FontStyle
|
FontStyle
|
Style of the font used for the headers
|
|
FontWeight
|
FontWeight
|
Weight of the font used for the headers
|
|
Foreground
|
Brush
|
Brush used to paint the foreground of the headers
|
|
ItemsSource
|
CircularSelectorItem[]
|
Array containing all the elements
|
|
SelectedItem
|
CircularSelectorItem
|
Gets or sets the currently selected item
|
Each element is a CircularSelectorItem
object; this object has the following
properties (even these are dependency property):
|
Property name
|
Property type
|
Description
|
|
Color
|
Color
|
Gets or sets the color of the item
|
|
IsVisible
|
bool
|
Gets or sets the visibility of the item
|
|
Header
|
string
|
Gets or sets the header text of the item
|
|
Tag
|
object
|
Gets or sets an object containing some additional informations
|
Well, now our small overview is finished, we can finally start talking about
code! The CircularSelectorItem
is boring: it's only a simple class with some
properties! So, we'll directly go to the CircularSelector
object.
Where can we start? Maybe from the class diagram? Yes, I think so!
As you can see, there are a lot of properties and methods: their meaning has already been
explained, but now I'll dig deeper in the UpdatePaths
method.
This method updates all the circular sectors of the control; it's called
whenever the control changes its size, its ItemsSource or its SweepDirection.
Its code is really complex, so, we'll split it in a lot of single pieces:
-
First thing to do: clean all the existing objects.
ClearPaths();
The function ClearPaths
makes only a call to base.Children.Clear()
(remember that CircularSelector
inherits from Panel
)
-
We check if there is at least one item in the items source
if (this.ItemsSource == null || this.ItemsSource.Count() == 0)
return;
-
Now we can calculate and store some useful information, like the measure of the angle of each sector,
the available size and the radius of the circle
double angle = Math.PI * 2 / this.ItemsSource.Count();
double size = Math.Min(this.ActualWidth, this.ActualHeight);
if (double.IsNaN(size) || size == 0)
return;
double radius = size / 2;
-
Here it's a bit more complex:
double cumulativeAngle = 0;
List<Point> pointList = new List<Point>();
angles = new Dictionary<int, double>();
for (int i = 0; i < this.ItemsSource.Count(); i++) {
pointList.Add(PolarToCartesian(radius, cumulativeAngle)
.Multiply((this.SweepDirection == SweepDirection.Counterclockwise ? 1 : -1), -1)
.Offset(radius, radius));
angles.Add(i, cumulativeAngle);
cumulativeAngle += angle;
}
Here we
find the points of the circumference that we'll use later to draw the arcs.
Maybe a drawing may help... In a few words, we are looking for the red points:
Another strange thing, is the
first line inside the for
statement: there are 2 extension methods
I've created to help the manipulation of the points. Here is the code:
public static Point Offset(this Point p, double x, double y) { p.X += x; p.Y += y; return p; }
public static Point Multiply(this Point p, double x, double y) { p.X *= x; p.Y *= y; return p; }
The code is really simple and need no explanations.
But the interesting thing is because I've done so. Let's start from the origins:
in our control the origin is on the center of the container and the Y-axis is
directed downwards. But what we really have when using the polar coordinates
system is a bit different: first, the origin of the system is the upper left
corner of the container and, then, the Y-axis is upwards. So, we have to do some
transformations to our points, in particular:
So, the first image is our initial situation (described above); the second one
tells us to invert the Y-axis; the last one, instead, is the offset of the
origin. Translated in code, this means that:
- We have to convert our polar coordinate in Cartesian one
- We have to multiply the value of the Y coordinate of the point by -1
- If the sweep direction is clockwise, we need to multiply even the X coordinate by -1
- And at last, we have to offset our point to make it center
Another strange thing, is
the second line inside the for
statement: here we store our angle
in a dictionary using as key the index of the sector. This may seem a bit
useless now, but later we'll see that it's not so.
-
for (int i = 0; i < this.ItemsSource.Count(); i++ ) {
CircularSelectorItem item = this.ItemsSource.ElementAt(i);
if (this.AlwaysDrawLines == false && item.IsVisible == false)
continue;
Path path = new Path() {
Stroke = this.LinesBrush,
StrokeThickness = 1,
Fill = (item.IsVisible ? CreateBackgroundBrush(item.Color, i) : new SolidColorBrush(Colors.Transparent))
};
PathGeometry geom = new PathGeometry();
PathFigure fig = new PathFigure();
geom.Figures.Add(fig);
path.Data = geom;
fig.StartPoint = new Point(radius, radius);
Point p1 = pointList[i];
Point p2 = pointList[(i + 1 == pointList.Count ? 0 : i + 1)];
fig.Segments.Add(new LineSegment() { Point = p1 });
fig.Segments.Add(new ArcSegment() {
Point = p2,
Size = new Size(radius, radius),
IsLargeArc = angle > Math.PI,
SweepDirection = this.SweepDirection, RotationAngle = 0
});
fig.Segments.Add(new LineSegment() { Point = fig.StartPoint });
base.Children.Add(path);
path.Tag = new object[] { item, null };
if (item.IsVisible) path.MouseLeftButtonUp += Path_MouseLeftButtonUp;
if (item.IsVisible) {
TextBlock txt = new TextBlock() {
Text = item.Header,
Foreground = this.Foreground,
FontFamily = this.FontFamily,
FontSize = this.FontSize,
FontStyle = this.FontStyle,
FontWeight = this.FontWeight
};
Point middlePoint = new Point((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2);
txt.Margin = new Thickness(middlePoint.X - txt.ActualWidth / 2, middlePoint.Y - txt.ActualHeight / 2, 0, 0);
base.Children.Add(txt);
txt.MouseLeftButtonUp += Path_MouseLeftButtonUp;
((object[])path.Tag)[1] = txt;
txt.Tag = path.Tag;
}
}
This may be the most complex part of the method.
The first lines are simple: we iterate through all the items in the ItemsSource
and check if we have to draw the current item. From here on, it's a bit more
complex. First, we create a new Path
object and set its properties
(CreateBackgroundBrush
is simple function which transforms a Color
object into a
Brush
); then, we create a new PathGeometry
with a PathFigure
.
The next block draws the circular sector:
The starting point is the center of the control.
From that point, we create a line to the arc start point (we've found every one
of them a bit earlier, in the previous code block). Then, we draw an arc from
the start point to the end point, and, finally, we can close our path by adding
the last line from the arc end point to the starting point. To understand the
logic of the ArcSegment
object, I advice you this excellent post called "The Mathematics of ArcSegment".
If the item is visible,
we add a handler for its MouseLeftButtonUp
event; in the handler we only change
the SelectedItem
property to the clicked item. Only if the item is visible, we
create a new TextBlock
to display the header of the sector; then we add the same
handler for the MouseLeftButtonUp
event.
If you've noticed,
we set the same Tag
property of the 2 objects to an array of objects containing
the istance of the CircularSelectorItem
represented by the sector, and the
TextBlock
used for the header. You'll discover later because I've done so, when
we'll speak about the SelectedItem
property.
We've finally finished the UpdatePaths
method, and we can immediately start
analyzing another focal point of the control: the SelectedItem
property.
public CircularSelectorItem SelectedItem
{
get { ... }
set {
CircularSelectorItem current = SelectedItem;
if (value != current && base.Children.Count > 0) {
if (ItemsSource == null || ItemsSource.Contains(value) == false)
throw new ArgumentException("Item not in the ItemsSource");
object[] obj = new object[] { };
Path path = null;
int i = 0;
if (current != null) {
path = base.Children.OfType<Path>().First(x => ((object[])x.Tag)[0] == current);
obj = (object[])path.Tag;
i = this.ItemsSource.IndexOf((CircularSelectorItem)obj[0]);
AnimatePath(path, false, i);
if (obj[1] != null) AnimatePath((FrameworkElement)obj[1], false, i);
}
path = base.Children.OfType<Path>().First(x => ((object[])x.Tag)[0] == value);
obj = (object[])path.Tag;
i = this.ItemsSource.IndexOf((CircularSelectorItem)obj[0]);
AnimatePath(path, true, i);
if (obj[1] != null) AnimatePath((FrameworkElement)obj[1], true, i);
SetValue(SelectedItemProperty, value);
OnSelectionChanged(new SelectionChangedEventArgs(
new List<CircularSelectorItem>(new CircularSelectorItem[] { current }),
new List<CircularSelectorItem>(new CircularSelectorItem[] { value })
));
}
}
}
Once we've stored in a variable the current item, we check if the previous value
is different from the new one and if there is at least one path already drawn.
If so, we check if the ItemsSource contains the new item, and we throw an
exception if it's false. After all these tests, we animate the current selected
path and the new one. To find the Path
object representing a
CircularSelectorItem
, we look for the Path
object with Tag
property containing
that item. In the end, we store the new value and raise the event.
A nice feature of the CircularSelector
are the animations used when the selected
item changes. This is done by the AnimatePath
method:
private void AnimatePath(FrameworkElement path, bool isSelected, int i) {
if (angles == null || angles.Count == 0)
return;
Storyboard story = new Storyboard();
TranslateTransform transform = new TranslateTransform();
path.RenderTransform = transform;
double radius = Math.Min(this.ActualWidth, this.ActualHeight) / 2;
double middleAngle = angles[i] + Math.PI * 2 / this.ItemsSource.Count() / 2;
Point endPoint =
PolarToCartesian(this.SelectedItemPushOut, middleAngle)
.Multiply((this.SweepDirection == SweepDirection.Counterclockwise ? 1 : -1), -1)
.Offset(radius, radius);
Point difference = endPoint.Offset(-radius, -radius);
DoubleAnimation animX = new DoubleAnimation() {
From = (isSelected ? 0 : difference.X),
To = (isSelected ? difference.X : 0),
Duration = TimeSpan.FromMilliseconds(200)
};
Storyboard.SetTarget(animX, transform);
Storyboard.SetTargetProperty(animX, new PropertyPath("X"));
DoubleAnimation animY = new DoubleAnimation() {
From = (isSelected ? 0 : difference.Y),
To = (isSelected ? difference.Y : 0),
Duration = TimeSpan.FromMilliseconds(200)
};
Storyboard.SetTarget(animY, transform);
Storyboard.SetTargetProperty(animY, new PropertyPath("Y"));
story.Children.Add(animX);
story.Children.Add(animY);
story.Begin();
}
This method is really simple: first, we create a new TranslateTransform
and we
add it to the path, then we calculate the end point (destination point) and, at
last, we do the difference with the center to calculate the difference along the
X and Y axis. After that, we set the animations and begin the storyboard.
Isolated storage: files and settings
Even this time, if you are a "Silverlighter", the isolated storage shouldn't be
new for you, but a little refresher is never bad, isn't it?
Let's analyze the term: "storage" means that it's a sort of repository, and
"isolated" means that is only for your application. So, the isolated storage is
a repository where you can put your files and your folders, and it's not
accessible by other applications, as you cannot access the storage of the other
applications.
The use of the isolated storage is compulsory, because there's no way to store
persistent data: Windows Phone 7, in fact, doesn't allow you to write or read
outside of your isolated storage. From MSDN:
Isolated storage enables managed applications to create and maintain local storage. The mobile architecture is similar to the Silverlight-based applications on Windows. All I/O operations are restricted to isolated storage and do not have direct access to the underlying operating system file system. Ultimately, this helps to provide security and prevents unauthorized access and data corruption.
Application developers have the ability to store data locally on the phone, again leveraging all the benefits of isolated storage including protecting data from other applications.
In other words, this is the schema representing the isolated storage:
Every necessary class to manage the isolated storage resides into the
System.IO.IsolatedStorage
namespace.
The first thing to do is obtaining a reference to the storage scoped to our
application; to achieve this goal, we call the static method
GetUserStoreForApplication
of the class IsolatedStorageFile
.
The object we obtain is a sort of "file system manager": in fact, with this, we
can create or delete file and folders, read and write files. To write data in a
file, we must create a new file and a stream: we call the OpenFile
method to get
a stream that we'll pass to the constructor of the StreamWriter
class. Once
that's done, we can use the StreamWriter
to write data in our file. Translated
in code, this means:
using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream fileStream = storage.OpenFile("MyFile.txt", System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write))
using (StreamWriter writer = new StreamWriter(fileStream))
writer.Write("Hello from isolated storage!");
I advice you to use the using
statements, because they allow you to
save a lot of time, avoiding a lot of calls to Dispose
and Close
to clean up the
memory and close all the streams.
To read data from a file, instead, the procedure is very similar to the previous
one:
string readText = "";
using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream fileStream = storage.OpenFile("MyFile.txt", System.IO.FileMode.Open, System.IO.FileAccess.Read))
using (StreamReader reader = new StreamReader(fileStream))
readText = reader.ReadToEnd();
The only differences are that this time we use a StreamReader
instead of a
StreamWriter
, and that the file is opened using System.IO.FileAccess.Read
instead of System.IO.FileAccess.Write
.
The isolated storage can be easily used to store the settings of our application,
i.e., in a XML file. We can create a new XML file, add nodes, write values, read
them later, etc... This procedure is simple, but a bit long. If you've noticed,
in the previous schema explaining the isolated storage, I've divided the
settings from all the other things. I've done so because we can treat the
settings in a different (and simpler) manner.
Wouldn't be simpler treating the settings like a common dictionary, avoiding I/O
actions? Here is where the IsolatedStorageSettings
class comes to rescue us. By
the static property ApplicationSettings
, we obtain the settings scoped to our
application; the returned object is a IDictionary<string, object>
, already
filled with our settings! An example here may clean the ideas:
IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
settings["String"] = "value";
settings["Bool"] = true;
settings["DateTime"] = DateTime.Now;
settings.Save();
This means that, the first time that our application is launched, our settings
are empty and we add new keys to our dictionary, like a common dictionary. In
the end, we save all the settings by calling the Save
method.
Remember to call this method, otherwise, nothing will be saved, because these
are all in-memory changes! The next time that our code is executed, we call
IsolatedStorageSettings.ApplicationSettings
and we obtain an
IsolatedStorageSettings
object already filled with our data! The
rest of the code, so, updates the dictionary and saves it again.
Settings
The settings page is particular because is the example of a strange thing. But
before, we should talk about the code behind! This is the diagram of the
SnakeMobile.Core.Settings.Settings
class:
Let's take a general look to this class: it inherits from DependencyObject
because
Acceleration
, Sound
, SondFx
and TotalTimePlayed
are dependency property; OnAccelerationChanged
, OnAccelerationChanged
, OnAccelerationChanged
and OnAccelerationChanged
are
the callback methods for the changing of the previous dependency properties.
Load
and Save
methods allow us to load and save the settings to the isolated
storage; RestoreDefaultSettings
, instead, restores all the default settings.
An interesting property is Instance
, but before of speaking about
it, we must go a little back in the time, before this class was created.
I need a class which can hold the
settings of my application, so a static class with static properties is perfect,
because the settings must be valid for the whole application, not for a single
instance of the class. But I must remember that I'm using XAML, so, the binding
is compulsory if I want to save code, and I MUST save code, so I need a class
with bindable properties. This class, so, must expose some dependency properties
(and so it must inherit from DependencyObject), and must be static. In other
words, I must only write some static dependency properties inside a static
class!
EH?!
What I've just said is impossible! I
can't bind to static properties, so I need to bind to instance properties;
instance properties means constructor, and constructor CAN'T mean static class!
The problem, so, is bigger that what I've thought... How can I bind to a static
property inside a static class?
think... think... think... EUREKA!
The static class mustn't be static,
but must act as if it was! The settings class mustn't be static and all the
properties must be normal instance properties. In this way, it can inherit from
DependencyObject and the properties can be dependency properties. Now, I don't
make the class static, but, instead, I make it singleton: I make the constructor
private, and I create a static read-only property called "Instance", and a static
field "_instance". This property must return the "_instance" field, and, if the
field is null, sets it to a new instance of the Settings class, and then returns
"_instance".
Translating in code
what I've just said, this is the most important part:
public class Settings : DependencyObject
{
private Settings() { }
private static Settings _instance = null;
public static Settings Instance {
get {
if (_instance == null)
return _instance = new Settings();
return _instance;
}
}
}
But the story isn't finished!
Now, the trick is done! In my
Settings page, I set the DataContext to the instance of the class, so that I can
use binding inside XAML!
This means that in the Settings.xaml.cs (code behind file of Settings.xaml) I
must add only this line of code...
public partial class Settings : PhoneApplicationPage
{
public Settings()
{
InitializeComponent();
this.DataContext = SnakeMobile.Core.Settings.Settings.Instance;
}
}
... to be able to use two-way binding inside XAML:
<toolkit:ToggleSwitch x:Name="ToggleSound" IsChecked="{Binding Sound, Mode=TwoWay}" />
In that way, without writing any code to handle the events of the controls, we
can write a complete settings page, only using bindings.
About the graphical controls, I should tell you something about the Pivot
, but
I'm not a designer, so, I advice you to read this article of Jeff Wilcox, if you
want to know something about this control and its brother Panorama
: Panorama and Pivot controls for Windows Phone developers
Build action: Content or Resource?
(explanation about Visual Studio)
Before we start talking about the audio, I need to make me sure you have
understood the difference between Content and Resource. If you already know the
difference, you can skip this paragraph and go to the next one, about the audio.
This paragraph talks about a little difference, but that gave me some trouble;
and I want to make me sure you've understood the difference between them,
because I don't want you to waste a lot of time, as I did. But let's start from
the beginning:
When you click on an element in the Solution Explorer of Visual Studio, in the
properties window you can see the property of the element you've clicked. The
one we'll analyze is the Build Action. Its values can be a lot, but we'll
focus on Resource and Content, because they are the most common
(and most confused).
-
Resource
When you use this value, the file you've selected (e.g. 0.xml) is included in the
resources of the DLL, instead of the XAP package.
In fact, if you take a look into the XAP package (using WinRAR or any other
program to manage archives) you can notice that there is no file with the name
of 0.xml:
But if you decompile the DLL (using the .NET Reflector) you can see that it's in
the resources:
-
Content
Content, instead, is the opposite of Resource: the file (e.g. Music.wav),
if Build Action is set to Content, is included inside the XAP package.
Inside the DLL, in fact, there isn't a Music.wav...
... because it's inside the XAP package:
To close this paragraph, I want to give you 2 advices when you use Content:
- Notice how the structure of the folders that you have in Visual Studio is recreated inside the XAP package.
- This options produces these effects only if you are working on the WP7 project.
I mean, if you have 2 projects (like in this application), one that is the
Windows Phone application, and the other that is a simple DLL, if you set the
Build Action to Content of a file inside the DLL, you get no effect (the file
isn't inside the XAP package).
Audio & XNA
One last thing before we start: we must remember that we are programming a
Silverlight application, so, to use XNA, we must do some tricks. If you write an
XNA application from the ground up, you have to do nothing, but if you want to
make XNA and Silverlight work in a single Silverlight application, it's enough a
little trick. Here it is: XNA is a big framework, with a lot of objects, and who
knows what's behind what we see! But one thing is sure: XNA needs that its
internal messages are processed. XNA is able to do this automatically, if the
application is written entirely for XNA, but this isn't our case, so, what can
we do? Simple: we must write a class that dispatches the messages of the XNA
framework! Luckily, exists the method
Microsoft.Xna.Framework.FrameworkDispatcher.Update
, which dispatches the message
for us, so, the only thing we have to do is calling regularly this method.
public class XNAAsyncDispatcher : IApplicationService
{
private DispatcherTimer frameworkDispatcherTimer;
public XNAAsyncDispatcher(TimeSpan dispatchInterval)
{
this.frameworkDispatcherTimer = new DispatcherTimer();
this.frameworkDispatcherTimer.Tick += new EventHandler(frameworkDispatcherTimer_Tick);
this.frameworkDispatcherTimer.Interval = dispatchInterval;
}
void IApplicationService.StartService(ApplicationServiceContext context) { this.frameworkDispatcherTimer.Start(); }
void IApplicationService.StopService() { this.frameworkDispatcherTimer.Stop(); }
void frameworkDispatcherTimer_Tick(object sender, EventArgs e) { Microsoft.Xna.Framework.FrameworkDispatcher.Update(); }
}
public partial class App : Application
{
public App()
{
this.ApplicationLifetimeObjects.Add(new XNAAsyncDispatcher(TimeSpan.FromMilliseconds(50)));
}
}
(If you don't know what an application lifetime object is, I advice you to read this
short post of Shawn Wildermuth: The Application Class and Application Services in Silverlight 3)
So, this is the code. In the constructor of our dispatcher (XNAAsyncDispatcher
)
we create a DispatcherTimer
which calls the FrameworkDispatcher.Update
method.
Nothing else, this is the dispatcher class. The last step is adding it to the
collection of the lifetime objects, so, in the constructor of the App
class, we
add a new XNAAsyncDispatcher
to the ApplicationLifetimeObjects
list.
Well... now that XNA is ready to give us full power, we can start!
What we have to do first, is knowing that all the audio we have can be divided in
songs and sound effects: the first ones are a continuous piece of music, while
the second ones are a brief sound played in specific moments. For instance, a
song is the background music of a game, while a sound effect is the noise played
when the snake eats some food.
When we start using the XNA framework to play music, we must be careful to follow
the Windows Phone 7 Application Certification Requirements.
6.5.1 Initial Launch Functionality
When the user is already playing music on the phone when the application is
launched, the application must not pause, resume, or stop the active music in
the phone MediaQueue by calling the Microsoft.Xna.Framework.Media.MediaPlayer
class.
If the application plays its own background music or adjusts background
music volume, it must ask the user for consent to stop playing/adjust the
background music (e.g. message dialog or settings menu).
This requirement does
not apply to applications that play sound effects through the
Microsoft.Xna.Framework.Audio.SoundEffect class, as sound effects will be mixed
with the MediaPlayer. The SoundEffect class should not be used to play
background music.
In a few words, you should check that the user isn't already playing music,
otherwise, you should ask him for consent to stop his music and play yours.
Another important point is that, you should play sound effects like sound
effects, because the music cannot be mixed, while the effects can be
simultaneously played.
How to check if the user is already playing some music? If
Microsoft.Xna.Framework.Media.MediaPlayer.GameHasControl
is true, you can play
your music, because the user is listening to nothing; if it's false, you should
ask him for the consent to stop the background music. However, we'll see later
an example of this.
If we have two different classifications of the audio, we'll have two different
ways to manage them using XNA: to play songs, we'll use the Song
object, and
to play sound effects, we'll use SoundEffect
. They reside
inside the Microsoft.Xna.Framework.Audio and Microsoft.Xna.Framework.Media namespaces, but first of all, we need to add a reference to the main assembly of the XNA
framework Microsoft.Xna.Framework.dll
:
Another important thing to do is preparing the audio: the audio files must be in
WAV format to work with XNA, even if MP3 is now supported by the Song
object
(not by SoundEffect
). Another important thing is that, in Visual
Studio, we must set the Build Action property of the audio file to Content.
But now it's time to let the code speak:
Song song = Song.FromUri("BackgroundMusic", new Uri("Sounds/Music.wav", UriKind.RelativeOrAbsolute));
SoundEffect soundEffect = SoundEffect.FromStream(TitleContainer.OpenStream("Sounds/Death.wav"));
To create a Song
from a file, we call the static method FromUri
of the Song
object; the first parameter is the name of the song, while the second one is the
Uri
of the file, relative to the XAP package (remember that we've set Build
Action to Content). To load a SoundEffect
, instead, we call the static method
FromStream
of the class SoundEffect
; the parameter is the stream containing the
audio file. TitleContainer.OpenStream
returns a Stream pointing to a file inside
the XAP package.
Once we've created our objects, we have to play them, isn't it? To play a
SoundEffect
, is enough call its Play
method; while to play a Song
we need a
media player, so, we must call MediaPlayer.Play
and pass as parameter our Song
object.
MediaPlayer.Play(song);
soundEffect.Play();
Coming back to our application, the class which is responsible of managing the sounds is
SnakeMobile.Core.Sounds
:
The 3 properties are the paths of the audio files (relative to the XAP package).
These properties are initialized inside the App.xaml.cs file:
public partial class App : Application
{
public App()
{
SnakeMobile.Core.Sounds.BackgroundMusicUri = new Uri("Sounds/Music.wav", UriKind.RelativeOrAbsolute);
SnakeMobile.Core.Sounds.DeathSoundPath = "Sounds/Death.wav";
SnakeMobile.Core.Sounds.EatSoundPath = "Sounds/Eat.wav";
}
}
The other methods, instead, are divided into 2 categories: PlayBackgroundMusic
and StopBackgroundMusic
are used, as their name says, to play and stop
background music; PlayEffect
, instead, plays a single sound effect.
public static void PlayBackgroundMusic() {
if (Settings.Settings.Instance.Sound == false)
return;
if (BackgroundMusicUri != null && MediaPlayer.GameHasControl) {
MediaPlayer.IsRepeating = true;
MediaPlayer.Play(Song.FromUri("BackgroundMusic", BackgroundMusicUri));
}
}
For instance, let's analyze PlayBackgroundMusic
(the others are very similar):
the first thing to is checking if the user has enabled the music inside the game
or has disabled it by the Settings page. Then we check if the user isn't playing
any music (MediaPlayer.GameHasControl
); at this point, we can play the song and
enable the repeating.
Portable components
What do you want from a programming article?
You may want 2 things: you may want to know something theoretical, like a design
pattern, a way to solve a problem and some other things like these; or you may
want to take some components to import them in your project. For the first one,
the solution is reading the article, which explains how I've built this
application from the ground up; for the second one, instead, I can give you a
list of all the independent component you can take away from this project and
use in yours. Remember that you are free to use, modify and improve the code of
the whole application, but if you apply a modification to one of these
components, please, let me know, so that I can update the project and let other
people take advantage of the improvements.
Here is the list:
-
Circular selector
The control used in the page of the level selection to select the level.
/SnakeMobile/InternalControls/CircularSelector.cs
/SnakeMobile/InternalControls/CircularSelectorItem.cs
-
Line CheckBox
Style for
the CheckBoxes used in the playing page to enable or disable an option using a line.
/SnakeMobile.Core/Resources/GlobalStyle.xaml
-
OverlayMessage
Control to produce a message with custom buttons.
/SnakeMobile.Core/InternalControls/OverlayMessage.xaml
/SnakeMobile.Core/InternalControls/OverlayMessage.xaml.cs
-
Rounded glowing button
Style for buttons to make a rounded glowing button.
/SnakeMobile/Resources/RoundedGlowingButton.xaml
-
WP7 Container
Container for Windows Phone 7 used to apply the Inversion of Control.
/SnakeMobile.Core/WP7Container.cs