Porting - microsoft/sudokumaster-wp GitHub Wiki
The application contains two pages, the main page which contains the game board, and the high scores page. Main page is similar to Qt application’s main view;
Graphics from the Qt application were used as-is. The Qt application is designed for a bit lower resolution (640x360), but the existing images scaled nicely on the 800x480 screen, though the cell background was resized to improve performance. The statistics and buttons on the bottom of the screen differ a bit. The Silverlight application is not a full screen application, and vertically stacked statistics didn’t fit nicely on the page when the system tray on top of the page and the application bar buttons are visible, so the statistics are stacked horizontally side-by-side. Silverlight application has no Options or Exit buttons. Exit button is not really needed at all, since the device buttons (Back, Start and Search) must exist on all WP 7 devices. Only Metro UI style new game and high scores buttons exist in application bar menu. Qt application used a separate options menu view for these;
The high scores are listed on a separate page while Qt's listview pops up on top of the main view;
Silverlight application contains two additions; scrollable Top 20 list, and the number of moves made to solve the puzzle is included.
When starting the application a splashscreen is displayed. With Qt the splashscreen was implemented by changing the main qml file from SplashScreen.qml
to main.qml at application startup with a timer.
With Silverlight this is easy and no coding is needed. When you create a new Silverlight project, a splashscreenimage.jpg
file is added to the project, and when the application is launched, the splash screen is displayed and will remain displayed until the navigation to the first page is complete. Just replace the splashscreen.jpg
with suitable one.
The algorithm for generating new puzzles is rather slow. It takes 2-3 seconds to generate a puzzle on the device, and a wait note (or progress bar) was needed. Silverlight's standard ProgressBar
control was not used. The Qt application used a spinning image (circle) animation, and a similar animation was implemented for Silverlight application. The WaitNote
is a UserControl
containing a rectangle filled with semi-transparent image. The animation rotates the rectangle forever, 360 degrees every 1.5 seconds. On silverlight we cut down the time to do full 360 rotation to 0.5 second which gives the impression of being blazing fast.
<Rectangle.Resources>
<Storyboard x:Name="spinAnimation">
<DoubleAnimation
Storyboard.TargetName="Transform"
Storyboard.TargetProperty="Angle"
By="360"
Duration="0:0:0.5"
AutoReverse="False"
RepeatBehavior="Forever" />
</Storyboard>
</Rectangle.Resources>
The animation is defined very similarly in WaitNote.qml
;
NumberAnimation on rotation {
id: animation
loops: Animation.Infinite
from: 0
to: 360
duration: 1500
}
The animations look exactly the same on both applications, but on device the Silverlight animation ran much smoother;
The WaitNote
is created in xaml but is hidden. The generation of a puzzle would block the UI thread and the animation would not show up at all, so a new thread is created for the puzzle generation;
private void NewGame()
{
…
// Display wait note (spinning circle)
waitIndicator.Visibility = System.Windows.Visibility.Visible;
waitIndicator.StartSpin();
…
// Disable databinding while generating puzzle
DataContext = null;
// Puzzle generation takes couple of seconds, do it in another thread
ThreadPool.QueueUserWorkItem(dummy =>
{
// generating puzzle doesn't touch UI so it can run on another thread
game.GeneratePuzzle();
// switching to UI thread to modify UI components
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
DataContext = game.Model; // let's turn on databinding again
gameTimer.Start();
gameStartTime = DateTime.Now;
gameState = GameState.Ongoing;
UpdateStatus();
waitIndicator.Visibility = System.Windows.Visibility.Collapsed;
waitIndicator.StopSpin();
});
});
...
}
The same problem was solved with a WorkerScript
on Qt application;
WorkerScript {
id: puzzleWorker
source: "gameLogic.js"
onMessage: {
Functions.numbers = messageObject.board;
empties = messageObject.boardEmpties;
boardChanged();
viewLoader.close();
gameOn = true;
mainBoard.focus = true;
mainTimer.start();
}
}
…
// gamelogic.js
WorkerScript.onMessage = function(message) {
generatePuzzle(message.rands);
WorkerScript.sendMessage( {board: numbers, boardEmpties: empties} );
}
The gameLogic.js
in Qt application contains the code for puzzle generation, and the code needed to check player’s actions (placing a number on the board). The JavaScript
code was easy to port to C#, since it contains just elemental arithmetic and logic with integer arrays. For example;
JavaScript:
/*
* Creates an array with numbers 1-9 in it, ordered randomly.
*/
function fillRandOrder()
{
randOrder = new Array();
var isSet = 0;
var rand;
for (var i = 0; i < DIM; i++) {
while (!isSet) {
rand = Math.floor(Math.random()*DIM)+1;
for (var j = 0; j < DIM; j++)
if (randOrder[j] && (rand ## randOrder[j]))
break;
if (j ## DIM) {
randOrder[i] = rand;
isSet = 1;
}
}
isSet = 0;
}
}
C#:
/// <summary>
/// Creates an array with numbers 1-9 in it, ordered randomly.
/// </summary>
private void FillRandOrder()
{
randOrder = new int[rowLength];
bool isSet = false;
int rand, j;
for (int i = 0; i < rowLength; i++)
{
while (!isSet)
{
rand = randGen.Next(columnLength) + 1;
for (j = 0; j < columnLength; j++)
if (rand ## randOrder[j])
break;
if (j ## columnLength)
{
randOrder[i] = rand;
isSet = true;
}
}
isSet = false;
}
}
So, what was needed to port the JavaScipt
to C#:
- Put the functions in a class (
GameLogic
) - Replace Arrays() with int[]
- Replace vars with int and bool
- Use Random instead of Math.random
- Check the scope of variables
This is enough to get the JavaScript
to compile, but some additions were made to the GameLogic
to bind it with the UI.
MainPage.xaml
contains a grid, BoardGrid
, of 9 rows and 9 columns. The grid is initially empty, and is
filled in CreateGrid
method with instances of Cells. Each Cell is a UserControl
containing a TextBlock
for the cell’s value. Button -control was not used, since the cell needed to be quite small (~50 pix), and displaying the number on a button so small proved to be quite impossible.
Couple of animations was needed for the cell. The cell turns to red when touched, and goes back to original when released. This is achieved by putting a red element on top of the cell, and changing its opacity;
<!-- Animations -->
<Grid.Resources>
<!-- Makes the cell go gradually from white/black to red -->
<Storyboard x:Name="fadeInAnimation">
<DoubleAnimation Storyboard.TargetName="Foreground"
Storyboard.TargetProperty="Opacity" From="0.0"
To="1.0" Duration="0:0:1" />
</Storyboard>
<!-- Makes the cell go gradually from red to white/black -->
<Storyboard x:Name="fadeOutAnimation">
<DoubleAnimation Storyboard.TargetName="Foreground"
Storyboard.TargetProperty="Opacity" From="1.0"
To="0.0" Duration="0:0:1" />
</Storyboard>
<!-- Blinks the cell twice between white and red -->
<Storyboard x:Name="blinkAnimation">
<DoubleAnimation Storyboard.TargetName="Foreground"
Storyboard.TargetProperty="Opacity" From="0.0"
To="1.0" AutoReverse="True" RepeatBehavior="2x"
Duration="0:0:0.5" />
</Storyboard>
</Grid.Resources>
When the cell is touched, an instance of NumberSelection
control is created and displayed.
The number pad on Qt application is made of a GridView
containing Rectangles with Text on them. The same
approach works with Silverlight, Grid with Rectangles and TextBlocks
on them;
Qt application used pop-up transitions in all views, while the Silverlight application uses fade in/out animations. It is possible to do a similar pop-up animation with Silverlight by using a transform animation, but it was a bit jerky, fade animation was better looking. The visual difference is minor, since the animation needed to be short (~0.3s) with the number selection dialog.
Transition {
to: "open"
PropertyAnimation {
properties: "scale, x, y"
duration: animSpeed
easing.type: Easing.OutBack
easing.overshoot: 1.8
}
}
<Storyboard x:Name="fadeInAnimation">
<DoubleAnimation
Storyboard.TargetName="LayoutRoot"
Storyboard.TargetProperty="Opacity"
From="0.0" To="0.8" Duration="0:0:0.3"
/>
</Storyboard>
Highscores page is simple one; background, some text and a listbox. Highscores are listed on a ListBox
, where each item contains multiple TextBlocks
. This is achieved by defining ItemTemplate
for the ListBox
;
<!-- And the listbox containing the scores sbelow the text. -->
<ListBox Name="HighscoreList" FontSize="28" Margin="0,160,0,0"
Foreground="White" HorizontalAlignment="Stretch">
<!-- Define item template. Each item in the listbox is constructed
from four textblocks; position, name, time and moves.-->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Name="ListBoxItemGrid" HorizontalAlignment="Stretch">
<!-- Columns for the textblocks -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10*" />
<ColumnDefinition Width="55*" />
<ColumnDefinition Width="25*" />
<ColumnDefinition Width="10*" />
</Grid.ColumnDefinitions>
<!-- Note the data binding ('{Binding *}'). The list box
is populated by binding the list of scores with
"HighscoreList.ItemsSource = scores;" in the constructor
of HighscoresPage. -->
<TextBlock Grid.Column="0" Text="{Binding Index}" />
<TextBlock Grid.Column="1" Text="{Binding Name}" />
<TextBlock Grid.Column="2" Text="{Binding Time}" />
<TextBlock Grid.Column="3" Text="{Binding Moves}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Items in the ListBox
are bind to the highscore List by setting the ItemsSource
of the listbox;
public static List<HighscoreItem> scores;
...
public Highscores()
{
InitializeComponent();
HighscoreList.ItemsSource = scores;
}
In the ItemTemplate
we defined data bindings for every TextBlock
, Text="{Binding Index}"
, Text="{Binding Name}"
, etc, and the HighscoreItem
has matching public properties Index, Name, Time and Moves.
When manipulating the HighscoreItems
in the scores –list, the changes are updated to the UI automatically.
The scores are stored into a XML file using XmlSerializer
, while Qt application used sqlite database (dbHandling.js).
Qt application doesn't have any sound effects, so this is a new feature. Initially the goal was to use only Silverlight's components, in this case the MediaElement
. The MediaElement
has several limitations, for example, only one MediaElement
can be used at a time, and it stops all other media playback on the phone - which actually prevents you from getting an application certified (if not used properly). Decision was made to use XNA's SoundEffect
instead.
Sound effects are played when;
- user clicks on a cell
- user clicks a number on the number selection dialog
- user has made an invalid move
- the game ends
A simple PlaySound
method was enough, since the soundscape is so small;
static public void PlaySound(string soundFile)
{
using (var stream = TitleContainer.OpenStream(soundFile))
{
var effect = SoundEffect.FromStream(stream);
FrameworkDispatcher.Update();
effect.Play();
}
}
The application supports both, portrait and landscape modes. This was implemented by splitting the MainPage
into a grid with multiple columns and rows in xaml, and when the orientation changes, the UI elements in the grid are assigned to different rows and columns. This required suprisingly lot of code, since the layout differs a lot between the two orientations - especially in the statistics grid, which is almost rebuilt in the code. This could also be achieved with VisualStateManger, but that would require a bit of a different approach.
private void PhoneApplicationPage_OrientationChanged(object sender,OrientationChangedEventArgs e)
{
if (e.Orientation ## PageOrientation.Landscape ||
e.Orientation ## PageOrientation.LandscapeLeft ||
e.Orientation ## PageOrientation.LandscapeRight)
{
Logo.SetValue(Grid.RowProperty, 1);
Logo.SetValue(Grid.ColumnSpanProperty, 1);
BoardGrid.SetValue(Grid.RowProperty, 0);
BoardGrid.SetValue(Grid.ColumnProperty, 1);
BoardGrid.SetValue(Grid.RowSpanProperty, 3);
BoardGrid.SetValue(Grid.ColumnSpanProperty, 2);
...
}
else
{
Logo.SetValue(Grid.RowProperty, 0);
Logo.SetValue(Grid.ColumnSpanProperty, 2);
BoardGrid.SetValue(Grid.RowProperty, 1);
BoardGrid.SetValue(Grid.ColumnProperty, 0);
BoardGrid.SetValue(Grid.RowSpanProperty, 1);
BoardGrid.SetValue(Grid.ColumnSpanProperty, 2);
...
}
}
The layout of the HighScores
page was so simple, that it didn't need a handler for the orientation
events - the layout defined in xaml worked ok in both orientations.
With Qt the orientation was handled by setting the margins of UI elements in qml depending on the orientation. For example, the statistics image on main view;
Image {
id: statics
property int imageSize: 20
source: "gfx/statistic.png"
width: 150
height: 130
anchors {
bottom: parent.bottom
bottomMargin: portrait ? 5 : 100
left: parent.left
leftMargin: portrait ? parent.width/2-width/2 : 10
}
...
With WP 7 the application is basically terminated when user does something that puts the app in the background, such as pressing the Start -button. Only the current page and page history of the application are stored automatically. The game state needs to be stored by the application, when it is about to be deactivated.
MainPage
listens for the Deactivated –event, and stores the game state (contents of the cells, moves, time, etc) into a file (gamestate.dat) when the application is deactivated. Upon startup the application restores the state, if the gamestate.dat
file exists. MainPage
cannot listen for Activated
events, since it is not instantiated when the event occurs, only the main Application, App
class can receive these events.
public MainPage()
{
…
PhoneApplicationService.Current.Deactivated += new EventHandler<DeactivatedEventArgs>(App_Deactivated);
RestoreState();
}
The App.cs
has pre-created handlers for application lifetime events, as defined in App.xaml
;
<Application.ApplicationLifetimeObjects>
<!--Required object that handles lifetime events for the application-->
<shell:PhoneApplicationService
Launching="Application_Launching" Closing="Application_Closing"
Activated="Application_Activated" Deactivated="Application_Deactivated"/>
</Application.ApplicationLifetimeObjects>
The high score list is loaded in App.cs
's Application_Launching
and Application_Activated
event handlers, but the game state is not. The reason for this is that if you use App.cs
's handlers, you’ll need to have static members, or maintain the game state in App, since the pages, MainPage
and HighscoresPage
, do not exist when the event is received. High score list can easily be static, it is just one List, but the game state is spread to various classes and members. Didn’t want to make major changes to the application since the game itself was already working, and restoring the state in constructor worked ok.