На этот раз я собираюсь разработать простую игру Tic Tac Toe (Noughts and Crosses) для Windows Phone 7. Я начну с Custom User Control, который будет использоваться в качестве кнопок X / O (с использованием визуальных состояний), а затем создам игровую доску. Если вы не знаете, что это за игра, вы можете найти больше информации в Википедии .
Исходный код
Последняя версия игры Tic Tac Toe находится на сайте CodePlex .
Шаг 1: Создание нового проекта
Прежде всего вам нужно создать новый проект. Для этого откройте Visual Studio 2010 -> Файл -> Новый проект -> выберите там приложение Windows Phone, как показано на рисунке ниже.
Шаг 2: Создание пользовательского UserControl для кнопок X / O
Я создал видео, описывающее, как я создал пользовательскую кнопку X / O (видео было записано во время установки CTP-версии Visual Studio для Windows Phone):
[HD] Windows Phone 7 Пользовательский элемент управления Tic Tac Toe от EugeneDotnet из Eugene Dotnet на Vimeo .
Вот разметка XAML для пользовательского элемента управления TicTacToeButton (обратите внимание на визуальные состояния):
<UserControl x:Class="WindowsPhoneTicTacToe.TicTacToeButton" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" d:DesignWidth="60" d:DesignHeight="60" Cursor="Hand"> <Grid x:Name="LayoutRoot" Width="60" Height="60" Opacity="1" Background="Transparent"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="TicTacToeButtonStates"> <VisualState x:Name="Default"/> <VisualState x:Name="Cross"> <Storyboard> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="path" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="path1" d:IsOptimized="True"/> <ColorAnimation Duration="0" To="White" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)" Storyboard.TargetName="path1" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="2" Storyboard.TargetProperty="(Shape.StrokeThickness)" Storyboard.TargetName="path1" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="1.024" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="path1" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="0.5" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="path1" d:IsOptimized="True"/> <ColorAnimation Duration="0" To="White" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)" Storyboard.TargetName="path" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="2" Storyboard.TargetProperty="(Shape.StrokeThickness)" Storyboard.TargetName="path" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="1.048" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="path" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="path" d:IsOptimized="True"/> </Storyboard> </VisualState> <VisualState x:Name="Nought"> <Storyboard> <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="path2" d:IsOptimized="True"/> <ColorAnimation Duration="0" To="White" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)" Storyboard.TargetName="path2" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="2" Storyboard.TargetProperty="(Shape.StrokeThickness)" Storyboard.TargetName="path2" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="1.084" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="path2" d:IsOptimized="True"/> <DoubleAnimation Duration="0" To="-3.083" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="path2" d:IsOptimized="True"/> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Path x:Name="path" Data="M-1.5894572E-07,-3.5527137E-15 C4.1656914,4.3908639 15.52839,10.918503 17.333334,16.333334 C22.318741,17.330414 26.620918,23.479309 29.333334,28 C31.796015,32.104469 36.741779,36.356804 40.333332,39.666668" Fill="{x:Null}" Margin="7.333,12,11.333,7.333" Opacity="0" StrokeStartLineCap="Flat" StrokeEndLineCap="Flat" Stroke="Black" StrokeThickness="1" StrokeMiterLimit="10" StrokeLineJoin="Miter" UseLayoutRounding="False" RenderTransformOrigin="0.5,0.5"> <Path.RenderTransform> <CompositeTransform/> </Path.RenderTransform> </Path> <Path x:Name="path1" Data="M40.666668,-3.5527137E-15 C35.157913,3.7949185 27.844921,7.0956602 23.666666,12.666667 C20.548512,16.824207 16.592587,20.476198 13.333333,24.666666 C8.6571426,30.678911 4.3249264,36.545231 0,42.666668" Fill="{x:Null}" Margin="6,12,12.333,4.333" Opacity="0" StrokeStartLineCap="Flat" StrokeEndLineCap="Flat" Stroke="Black" StrokeThickness="1" StrokeMiterLimit="10" StrokeLineJoin="Miter" UseLayoutRounding="False" RenderTransformOrigin="0.5,0.5"> <Path.RenderTransform> <CompositeTransform/> </Path.RenderTransform> </Path> <Path x:Name="path2" Data="M21.386688,0.66666698 C18.104256,0.75869775 17.090931,-0.16339432 14.386687,2.0000002 C12.42321,3.5707819 10.433279,3.7392514 8.7200203,5.666667 C6.4274411,8.2458191 5.5025487,7.6524167 4.3866873,11 C3.5323884,13.562898 3.0594554,15.975595 2.3866875,18.666666 C1.0282598,24.100378 -1.137174,26.809351 0.72002077,33 C1.3220947,35.006912 3.1331961,37.458286 4.0533543,39.666668 C5.114325,42.212997 6.5792308,43.366936 9.7200203,44.333332 C15.411503,46.08456 20.752144,46 27.053354,46 C31.401215,46 34.719112,42.44838 36.053352,36.666668 C37.463272,30.557018 38.386688,25.955296 38.386688,19.333334 C38.386688,12.770806 34.890102,10.843581 33.053352,5.3333335 C29.067465,4.004704 25.056402,1.8348579 21.386688,3.1789145E-07 C21.386688,0.22222254 21.386688,0.44444478 21.386688,0.66666698 z" Fill="{x:Null}" Margin="7.28,9.333,13.333,3.666" Opacity="0" StrokeStartLineCap="Flat" StrokeEndLineCap="Flat" Stroke="Black" StrokeThickness="1" StrokeMiterLimit="10" StrokeLineJoin="Miter" UseLayoutRounding="False" RenderTransformOrigin="0.5,0.5"> <Path.RenderTransform> <CompositeTransform/> </Path.RenderTransform> </Path> </Grid> </UserControl>
Добавьте следующий код в код позади элемента управления, который позволяет переключаться между визуальными состояниями:
private bool? isCross = null; public bool? IsCross { get { return isCross; } set { VisualStateManager.GoToState(this, (value.HasValue ? (value.Value ? "Cross" : "Nought") : "Default" ) , false); isCross = value; } }
Изменение этого свойства заставит TicTacToeButton изменить его визуальное состояние на «крест», «ноль» или «по умолчанию». Если значение в установщике равно «null», то к кнопке применено состояние «Default» (что означает, что ничего не отображается), если значение «true», состояние кнопки изменится на «Cross» и, наконец, если значение «false», то «Ноль». Например, если состояние TicTacToeButton «Default», то ни один из игроков еще не нажал эту кнопку. Также убедитесь, что для сетки LayoutRoot в TicTacToeButton непрозрачность установлена в 1.
Шаг 3: Изменение задания по умолчанию
После создания нового проекта мы можем изменить страницу XAML по умолчанию с MainPage.xaml на Gameboard.xaml. Это можно сделать в файле WMAppManifest.xml (пакет свойств).
Там вам нужно изменить значение NavigationPage элемента DefaultTask:
<Tasks> <DefaultTask Name ="_default" NavigationPage="Gameboard.xaml"/> </Tasks>
Шаг 4: Создание игровой доски
Теперь давайте перейдем к нашей игровой доске. Создайте новую портретную страницу Windows Phone и назовите ее Gameboard. Там нам нужно изменить ContentGrid в XAML. Мы добавим сетку для игровой доски с 9 экземплярами TicTacToeButton, 2 горизонтальными линиями и 2 вертикальными линиями в качестве внутренних границ, текстовым блоком, отображающим текущее состояние приложения, и надписью «Играть снова?» кнопка. Советую вам использовать «Microsoft Expression Blend 4» (с поддержкой Silverlight 4), например, для рисования пользовательских линий в виде границ сетки, чтобы они выглядели более реалистично. ContentGrid XAML теперь будет выглядеть так:
<Grid x:Name="ContentGrid" Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock Height="53" HorizontalAlignment="Left" Margin="72,78,0,0" Name="tbStatus" Text="TextBlock" VerticalAlignment="Top" Width="340" FontSize="32" TextAlignment="Center" Grid.Row="0" /> <Canvas x:Name="canvasGameboard" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="1" Height="200" Width="200"> <Grid x:Name="gridGameboard"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="10"/> <RowDefinition Height="Auto"/> <RowDefinition Height="10"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="10"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="10"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Path Grid.Column="1" Data="M64.333336,4.6666665 C62.924431,20.164593 64.127037,36.65147 65.333336,52.333332 C66.558273,68.257599 63.498032,84.145035 65,100.66666 C66.361046,115.63819 64.666664,132.64523 64.666664,148.33333 C64.666664,156.89328 64.333336,165.07155 64.333336,173.66667 C64.333336,182.08456 67.718994,189.40503 66,198 C65.777779,197.55556 65.623878,197.06978 65.333336,196.66667 C65.241455,196.5392 65.090111,196.46207 65,196.33333 C64.909889,196.20461 64.758545,196.12747 64.666664,196 C64.521393,195.79845 64.534637,195.18771 64.333336,195.33333" Margin="3.365,4.667,2.513,1" Grid.RowSpan="5" StrokeStartLineCap="Flat" Stretch="Fill" StrokeEndLineCap="Flat" Stroke="White" StrokeThickness="2" StrokeMiterLimit="10" StrokeLineJoin="Miter" UseLayoutRounding="False"/> <Path Grid.Column="3" Data="M134.33333,4.3333335 C132.37093,20.567755 133.83511,35.186302 135.33333,51.666668 C136.78621,67.648277 134,82.84716 134,99 C134,115.52766 130.90732,134.11053 133.66667,150.66667 C135.1199,159.38612 133.33333,166.49176 133.33333,175 C133.33333,183.39162 131.75618,189.55441 134,197.66667" Margin="1.521,3.333,3.247,1.333" Grid.RowSpan="5" StrokeStartLineCap="Flat" Stretch="Fill" StrokeEndLineCap="Flat" Stroke="White" StrokeThickness="2" StrokeMiterLimit="10" StrokeLineJoin="Miter" UseLayoutRounding="False"/> <Path Grid.ColumnSpan="5" Data="M195.66667,65.333336 C180.5721,67.724548 165.25514,67.598076 149.66667,65 C132.7099,62.173874 116.64419,66.333336 99.666664,66.333336 C67.673195,66.333336 35.10807,66.211624 3,65" Margin="2,3.016,3.333,1.958" Grid.Row="1" StrokeStartLineCap="Flat" Stretch="Fill" StrokeEndLineCap="Flat" Stroke="White" StrokeThickness="2" StrokeMiterLimit="10" StrokeLineJoin="Miter" UseLayoutRounding="False"/> <Path Grid.ColumnSpan="5" Data="M2.3333333,136.66667 C10.924497,136.66667 18.328968,134.33333 26.666666,134.33333 C34.2099,134.33333 40.833973,135.33333 48.333332,135.33333 C63.764233,135.33333 79.204323,135.33333 94.666664,135.33333 C128.60835,135.33333 161.59547,137.31277 195.33333,134.66667" Margin="1.333,3.333,3.667,2.333" Grid.Row="3" StrokeStartLineCap="Flat" Stretch="Fill" StrokeEndLineCap="Flat" Stroke="White" StrokeThickness="2" StrokeMiterLimit="10" StrokeLineJoin="Miter" UseLayoutRounding="False"/> <local:TicTacToeButton HorizontalAlignment="Left" x:Name="ticTacToeButton1" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonDown="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.Column="2" HorizontalAlignment="Left" Margin="1,0,0,0" x:Name="ticTacToeButton2" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonDown="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.Column="4" HorizontalAlignment="Left" Margin="2,0,0,0" x:Name="ticTacToeButton3" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonDown="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.ColumnSpan="2" Grid.Row="2" HorizontalAlignment="Left" Margin="2,1,0,0" x:Name="ticTacToeButton4" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonUp="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.Column="2" Grid.Row="2" HorizontalAlignment="Left" Margin="1,0,0,0" x:Name="ticTacToeButton5" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonUp="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.Column="4" Grid.Row="2" HorizontalAlignment="Left" Margin="2,1,0,0" x:Name="ticTacToeButton6" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonUp="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.ColumnSpan="2" Grid.Row="4" HorizontalAlignment="Left" Margin="2,0,0,0" x:Name="ticTacToeButton7" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonUp="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.Column="2" Grid.Row="4" HorizontalAlignment="Left" Margin="1,0,0,0" x:Name="ticTacToeButton8" VerticalAlignment="Top" Height="60" Width="60" MouseLeftButtonUp="button_MouseLeftButtonDown" /> <local:TicTacToeButton Grid.Column="4" Grid.Row="4" VerticalAlignment="Top" MouseLeftButtonUp="button_MouseLeftButtonDown" HorizontalAlignment="Left" Height="60" Width="60" Margin="2,0,0,0" x:Name="ticTacToeButton9" /> </Grid> </Canvas> <Button Content="Play again?" Height="70" HorizontalAlignment="Right" Margin="0,20,123,0" Name="btnReplay" VerticalAlignment="Top" Width="230" Click="btnReplay_Click" Grid.Row="2" /> </Grid>
Шаг 5: игровая логика
Я не буду описывать каждую часть файла кода в моем уроке, вы можете просмотреть полный код приложения на CodePlex (ссылка выше). Определенно, наиболее важным методом является событие щелчка мыши на одной из кнопок TicTacToe. Этот метод проверяет, является ли игра все еще активной (если есть несколько нажатых кнопок и ни один из игроков не выиграл), изменяет состояние нажатой кнопки TicTacToeButton, проверяет, выиграл ли текущий игрок и закончилась ли игра. Вот код для этого метода:
void button_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (IsActive) { TicTacToeButton button = sender as TicTacToeButton; // if button haven't been pressed before if (button != null && !button.IsCross.HasValue) { button.IsCross = IsCross; if (!CheckGameboard()) { // change visual state of a button IsCross = !IsCross; } else { // game over IsActive = false; } } } }
Вот видео демонстрация: