As it is being presented here, Bricks! pays a tribute to the legendary game without offering any considerable changes in the original game logic.
Besides the fact that it is yet-another-Silverlight-game, what I'm trying to do here is to provide Code Project readers with some (hopefully) useful tips of how to program games like this in Silverlight 4. My first goal here is to stir up curiosity about the game, and then (hopefully) inspire some readers in a near future to come up with new projects and articles. So, if after reading the article and playing with the application you feel some willingness to write something, to program something or even to think about it, my job here will be more than fulfilled.
You can save some of your time by taking a look at the YouTube video I uploaded, in the link below:
If you already have Visual Studio 2010, that's enough run the application. If you don't, you can download the following 100% free development tool directly from Microsoft:
Important Note: Please don't try opening the solution with Visual Studio 2008, because it will not work. If you don't have VS2010, download the Visual Web Developer 2010 in the link above. I assure you that you will not be disappointed.
Figure 1: Solution structure
The Visual Studio 2010 solution is made up by three projects: Bricks.Silverlight, Bricks.Silverlight.Core and Bricks.Silverlight.Web, as we can see in the following table:
The Intro Menu consists of the Bricks "logo" and some game instructions.
Figure 2: Intro Menu
In this project I used the Model-View-ViewModel (MVVM) pattern. As some of the readers may know, this pattern originated with WPF and Silverlight technologies, and, as a rough explanation, in MVVM the user interface (composed by "views", residing in the .XAML/.XAML.cs files) "hands over" control to a set of generic classes, called "ViewModels", so that any interacions from the user part on the View side reflects on the underlying ViewModel classes, and vice-versa.
As we can see from the Figure 3 below, the View doesn't access the data directly, because instead it relies on the bindings provided by its ViewModel counterpart. The binding is the glue that holds View and ViewModel together. As an example, the score TextBlock which represents the Score value and is named "txtScore" in a View has a binding to a Int32 property named "Score" in the ViewModel. Any changes to the "Score" property reflects back in the txtScore element on the View side. On the other hand, a Button element named "Start" has a binding to an ICommand property named "StartCommand" in the ViewModel, so that any time the user clicks the button would automatically invoke the DoStart() method on the ViewModel side.
TextBlock
Int32
txtScore
Button
ICommand
DoStart()
Figure 3: Basic structure of the MVVM Pattern
The complete binding mappings are described in the table below:
Some important notes about the bindings above:
pnlIntro
pnlGameOver
pnlGamePaused
bool
Visibility
Visible
Collapsed
BooleanToVisibilityConverter
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Visibility rv = Visibility.Visible; try { var x = bool.Parse(value.ToString()); if (x) { rv = Visibility.Visible; } else { rv = Visibility.Collapsed; } } catch (Exception) { } return rv; }
public int Score { get { return score; } set { score = value; OnPropertyChanged("Score"); } }
Listbox
Collection
ObservableNotifiableCollection
public ObservableNotifiableCollection<IBrick>> Bricks { get { return bricks; } set { bricks = value; } }
IsEnabled
ButtonService.Command
ButtonService
public ICommand StartCommand { get { return new RelayCommand(() => DoStart()); } } public bool StartCanExecute { get { return startCanExecute; } set { startCanExecute = value; OnPropertyChanged("StartCanExecute"); } }
The Figure 4 below illustrates how some elements of the View and ViewModel interact with each other: some of the visual elements in the View have bound properties in the ViewModel side:
View
ViewModel
Figure 4: How the View and ViewModel counterparts interact with each other.
But wait a moment! Have you noticed that those 2 boards are, in fact, 2 listboxes? Have you also noticed that these listboxes are mapped to collections? More specifically, they are bound to properties in the ViewModel, of the type ObservableNotifiableCollection<IBrick>.
ObservableNotifiableCollection<IBrick>
At the core of the game UI engine lays the MVVM pattern with all the binding mechanism. But what really really enables the MVVM with this game is the ability to transform an ordinary Listbox into a 10 x 16 rectangular board to hold our bricks.
The template I used here was inspired by Beatriz Stollnitz's very cool example of transforming a simple Listbox into a solar system.
Figure 5: How Beatriz Stollnitz managed to transform a WPF Listbox into a Solar System through Styles and templating.
After seen this, I realized that it would be much easier to program the game using MVVM (through bindings) than using traditional UI Elements manipulations. Simply put: I wouldn't need to care about positioning the visual elements that represents the bricks (in the view part). I would care only about the underlying, abstract model representation of those bricks. Then the MVVM would do the rest, by updating the view (and hence the Listbox) automatically for me.
The real problem was that Bea Stollnitz had originally written that for WPF, while I wanted to use the same techniques in Silverlight. I reached a point where I couldn't tell if it was feasible or not. So I put my hands on it. The great problem while porting Bea's XAML example to Silverlight was this part:
<Style TargetType="ListBoxItem"> <Setter Property="Canvas.Left" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/> <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/> (…) </Style>
The XAML above describe the styling/templating for each Planet in Bea's solar system. All I wanted to do was to do the same for Bricks! game, so instead of Planets positioning, I would have Bricks positioning, which in turn would bind to the Left and Top properties on the ViewModel side, like this:
Planet
<Style TargetType="ListBoxItem"> <Setter Property="Canvas.Left" Value="{Binding Path=Left}"/> <Setter Property="Canvas.Top" Value="{Binding Path=Top}"/> (…) </Style>
But have you noticed those "Canvas.Left" and "Canvas.Top" properties inside the ListBoxItem tag? Although these properties are inside the ListBoxItem element, they are the so-called attached properties: actually they are defined in the parent element (that is, Canvas element. The bad news is that, as I had found out, this kind of templating using attached properties simply doesn't work with Silverlight 4. But fortunately for me, after some hours researching the issue, I found a very nice blog post from David Anson of Microsoft, describing a workaround using the SetterValueBindingHelper, that definitely solved te problem!
ListBoxItem
Canvas
SetterValueBindingHelper
Now that I had the workaround, the solution for the problem above was the snippet below:
<Style TargetType="ListBoxItem"> <Setter Property="local:SetterValueBindingHelper.PropertyBinding"> <Setter.Value> <local:SetterValueBindingHelper> <local:SetterValueBindingHelper Type="System.Windows.Controls.Canvas, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" Property="Left" Binding="{Binding Path=Left, Mode=TwoWay}"/> <local:SetterValueBindingHelper Type="Canvas" Property="Top" Binding="{Binding Path=Top, Mode=TwoWay}"/> </local:SetterValueBindingHelper> </Setter.Value> </Setter> </Style>
Now pay attention to how templating and styling turns a boring Listbox into an exciting, colorful game board:
Figure 6: How I managed to transform a Silverlight Listbox into the game board through Styles and templating.
Again, many thanks to Bea Stollnitz and David Anson for this!
Figure 7: Bricks Sheet Background
Here you can see the nice background of our game. It's pretty easy to do this with XAML with Visual Studio 2010. In fact, so easy that I didn't feel any need to use Expression Blend in this project. The more you practice with XAML, the more you feel natural to create your designs with XAML, and more productive you become.
Each sheet perforation is represented by an Ellipse element, and each sheet line is made by a cyan Border element with no defined height:
Ellipse
Border
<StackPanel Width="400" Background="White" HorizontalAlignment="Left" Margin="0,20,0,0"> <Border BorderBrush="Cyan" BorderThickness="0.5" Margin="0,5"/> <Border BorderBrush="Cyan" BorderThickness="0.5" Margin="0,5"/> <Border BorderBrush="Cyan" BorderThickness="0.5" Margin="0,5"/> . .(some lines removed for the sake of readability) . </StackPanel> <StackPanel Width="400" HorizontalAlignment="Left" Margin="0,20,0,0"> <Ellipse Width="10" Height="10" HorizontalAlignment="Left" Fill="Black" Margin="5,5"/> <Ellipse Width="10" Height="10" HorizontalAlignment="Left" Fill="Black" Margin="5,5"/> <Ellipse Width="10" Height="10" HorizontalAlignment="Left" Fill="Black" Margin="5,5"/> . .(some lines removed for the sake of readability) . </StackPanel>
The heart of the application logic lays at the BricksBoard class. You can see below a simple diagram showing the relationship between the core classes and the BricksBoard class:
BricksBoard
Figure 8: The Core Class Diagram
For the sake of brevity, we could explain most of the game logic by describing some of the BricksBoard members:
Figure 9: The BricksBoard Class
IPresenter
public BricksBoard(IPresenter presenter) { this.presenter = presenter; this.width = 10; this.height = 16; InitializeArray(); next = GetRandomShape(); }
Once the BricksBoard is instantiated, an external dependence of IPresenter is "injected", which in turn will "control" the new BricksBoard instance (hence "Inversion of Control"). The board size is hard-coded with the 10x16 dimension and the board array is initialized. In the end of the constructor, a new "Next" shape is generated (the "Next" shape defines the shape that will fall from the top of the board after the current shape gets stuck in the the top end of the stack).
public override void InitializeArray() { score = 0; level = 1; lines = 0; if (shape != null) { shape.Y = 0; } next = GetRandomShape(); presenter.UpdateScoreView(score, hiScore, lines, level, next); base.InitializeArray(); }
IBrick
public virtual void InitializeArray() { shapeArray = new IBrick[width, height]; for (int row = 0; row < height; row++) { for (int column = 0; column < width; column++) { shapeArray[column, row] = null; } } }
private IShape GetRandomShape() { IShape newShape = null; Random randomClass = new Random(); int randomCode = randomClass.Next((int)ShapeCodes.I, (int)ShapeCodes.Z + 1); switch (randomCode) { case (int)ShapeCodes.I: newShape = new StickShape(); newShape.Color = Colors.Cyan; break; case (int)ShapeCodes.J: newShape = new JShape(); newShape.Color = Colors.Blue; break; case (int)ShapeCodes.L: newShape = new LShape(); newShape.Color = Colors.Orange; break; case (int)ShapeCodes.O: newShape = new OShape(); newShape.Color = Colors.Yellow; break; case (int)ShapeCodes.S: newShape = new SShape(); newShape.Color = Colors.Green; break; case (int)ShapeCodes.T: newShape = new TShape(); newShape.Color = Colors.Purple; break; case (int)ShapeCodes.Z: newShape = new ZShape(); newShape.Color = Colors.Red; break; } ((BaseShape)newShape).Presenter = presenter; presenter.UpdateScoreView(score, hiScore, lines, level, newShape); return newShape; }
BricksViewModel
Tick
void timer_Tick(object sender, EventArgs e) { foreach (var b in bricks) { b.X = b.X; b.Y = b.Y; } presenter.Tick(); }
Then the Tick method in the BricksPresenter class invokes the ProcessNextMove method:
BricksPresenter
ProcessNextMove
public void Tick() { BricksBoard.ProcessNextMove(); }
The ProcessNextMove method itself updates the board, placing a new random piece if needed, moving down the current tetromino, if possible, removing the completed rows and updating the score in the View side, and finishing the current game if the board is already full:
public void ProcessNextMove() { if (shape == null) { StartRandomShape(); } bool couldMoveDown = true; if (!shape.Anchored) { RemovePieceFromCurrentPosition(shape); couldMoveDown = shape.MoveDown(); } else { bool full = !StartRandomShape(); if (full) { InitializeArray(); GameOver(); return; } else { couldMoveDown = shape.MoveDown(); } } if (!couldMoveDown) { RemoveCompletedRows(); } if (presenter != null) { presenter.UpdateBoardView(GetStringFromShapeArray(), shapeArray, width, height); } }
public bool StartRandomShape() { if (shape != null && !shape.Anchored) { this.RemovePieceFromCurrentPosition(shape); } shape = next; next = GetRandomShape(); shape.ContainerBoard = this; int x = (this.Width - shape.Width) / 2; bool ret = this.TestPieceOnPosition(shape, x, 0); if (ret) { this.PutPieceOnPosition(shape, x, 0); } return ret; }
public void RemovePieceFromCurrentPosition(IShape shape) { for (int row = 0; row < shape.Height; row++) { for (int column = 0; column < shape.Width; column++) { if (shape.ShapeArray[column, row] != null) { shapeArray[column + shape.X, row + shape.Y] = null; } } } }
public bool TestPieceOnPosition(IShape shape, int x, int y) { for (int row = 0; row < shape.Height; row++) { for (int column = 0; column < shape.Width; column++) { //is the position out of range? if (column + x < 0) return false; if (row + y < 0) return false; if (column + x >= width) return false; if (row + y >= height) return false; //will the shape collide in the board? if ( shapeArray[column + x, row + y] != null && shape.ShapeArray[column, row] != null) { return false; } } } return true; }
public void PutPieceOnPosition(IShape shape, int x, int y) { if (!TestPieceOnPosition(shape, x, y)) throw new CantSetShapePosition(); for (int row = 0; row < shape.Height; row++) { for (int column = 0; column < shape.Width; column++) { if (shape.ShapeArray[column, row] != null) { shapeArray[column + x, row + y] = shape.ShapeArray[column, row]; } } } shape.X = x; shape.Y = y; if (presenter != null) { presenter.UpdateBoardView(GetStringFromShapeArray(), shapeArray, width, height); } }
private bool RemoveCompletedRows() { bool completed = false; int row = height - 1; while (row >= 0) { completed = true; for (int column = 0; column < width; column++) { if (shapeArray[column, row] == null) { completed = false; break; } } if (completed) { IBrick[] removedBricks = new IBrick[width]; for (int column = 0; column < width; column++) { removedBricks[column] = shapeArray[column, row]; } shape = null; for (int innerRow = row; innerRow > 0; innerRow--) { for (int innerColumn = 0; innerColumn < width; innerColumn++) { shapeArray[innerColumn, innerRow] = shapeArray[innerColumn, innerRow - 1]; shapeArray[innerColumn, innerRow - 1] = null; } } score += 10 * level; if (score > hiScore) { hiScore = score; } lines++; level = 1 + (lines / 10); presenter.UpdateScoreView(score, hiScore, lines, level, next); } else { row--; } } if (presenter != null) { presenter.UpdateBoardView(GetStringFromShapeArray(), shapeArray, width, height); } if (completed) { RemoveCompletedRows(); } return completed; }
BaseShape
public bool MoveLeft() { bool test = false; if (!anchored) { if (containerBoard == null) throw new NullContainerBoardException(); containerBoard.RemovePieceFromCurrentPosition(this); test = containerBoard.TestPieceOnPosition(this, this.X - 1, this.Y); if (test) { containerBoard.RemovePieceFromCurrentPosition(this); containerBoard.PutPieceOnPosition(this, this.X - 1, this.Y); } } return test; } public bool MoveRight() { bool test = false; if (!anchored) { if (containerBoard == null) throw new NullContainerBoardException(); containerBoard.RemovePieceFromCurrentPosition(this); test = containerBoard.TestPieceOnPosition(this, this.X + 1, this.Y); if (test) { containerBoard.PutPieceOnPosition(this, this.X + 1, this.Y); } } return test; } public bool MoveDown() { bool test = false; if (!anchored) { containerBoard.RemovePieceFromCurrentPosition(this); //should anchor if shape can't move down from current position if (!containerBoard.TestPieceOnPosition(this, this.X, this.Y + 1)) { containerBoard.PutPieceOnPosition(this, this.X, this.Y); this.Anchor(); } else { if (containerBoard == null) throw new NullContainerBoardException(); test = containerBoard.TestPieceOnPosition(this, this.X, this.Y + 1); if (test) { containerBoard.PutPieceOnPosition(this, this.X, this.Y + 1); } } } return test; } public bool Rotate90() { bool test = false; if (!anchored) { if (containerBoard == null) throw new NullContainerBoardException(); IBrick[,] newShapeArray = new IBrick[height, width]; IBrick[,] oldShapeArray = new IBrick[width, height]; for (int row = 0; row < height; row++) { for (int column = 0; column < width; column++) { newShapeArray[height - row - 1, column] = shapeArray[column, row]; oldShapeArray[column, row] = shapeArray[column, row]; } } containerBoard.RemovePieceFromCurrentPosition(this); int w = width; int h = height; this.width = h; this.height = w; this.shapeArray = newShapeArray; if (containerBoard.TestPieceOnPosition(this, this.X, this.Y)) { containerBoard.PutPieceOnPosition(this, this.X, this.Y); } else { this.width = w; this.height = h; this.shapeArray = oldShapeArray; containerBoard.PutPieceOnPosition(this, this.X, this.Y); } } return test; } public bool Rotate270() { bool test = false; if (!anchored) { if (containerBoard == null) throw new NullContainerBoardException(); containerBoard.RemovePieceFromCurrentPosition(this); IBrick[,] newShapeArray = new IBrick[height, width]; IBrick[,] oldShapeArray = new IBrick[width, height]; for (int row = 0; row < height; row++) { for (int column = 0; column < width; column++) { newShapeArray[row, width - column - 1] = shapeArray[column, row]; oldShapeArray[column, row] = shapeArray[column, row]; } } int w = width; int h = height; this.width = h; this.height = w; this.shapeArray = newShapeArray; if (containerBoard.TestPieceOnPosition(this, this.X, this.Y)) { containerBoard.PutPieceOnPosition(this, this.X, this.Y); } else { this.width = w; this.height = h; this.shapeArray = oldShapeArray; containerBoard.PutPieceOnPosition(this, this.X, this.Y); } } return test; }
This part is not really necessary for the game, but I thought it would add something to the look and feel of the game. At first I was not happy enough with the square, static bricks, so I wanted to create some movements, so that the bricks appeared to be "shaking".
I managed to do this by creating a custom class, called ctlBrick, that inherits from the Grid class. Each ctlBrick instance represents a different brick on the screen.
ctlBrick
Grid
public void GenerateRandomPoints() { this.Children.Remove(path); if (color != Colors.Transparent) { double h = this.Height; double w = this.Width; Random rnd = new Random(); p00 = new Point(2 + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude)); p01 = new Point(2 + rnd.Next(-amplitude, amplitude), 1 * h / 4 + rnd.Next(-amplitude, amplitude)); p02 = new Point(2 + rnd.Next(-amplitude, amplitude), 3 * h / 4 + rnd.Next(-amplitude, amplitude)); p03 = new Point(2 + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude)); p30 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude)); p31 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), 1 * h / 4 + rnd.Next(-amplitude, amplitude)); p32 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), 3 * h / 4 + rnd.Next(-amplitude, amplitude)); p33 = new Point(-2 + w + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude)); p10 = new Point(1 * w / 4 + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude)); p20 = new Point(3 * w / 4 + rnd.Next(-amplitude, amplitude), 2 + rnd.Next(-amplitude, amplitude)); p13 = new Point(1 * w / 4 + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude)); p23 = new Point(3 * w / 4 + rnd.Next(-amplitude, amplitude), -2 + h + rnd.Next(-amplitude, amplitude)); var figures = new PathFigureCollection(); var pathSegmentCollection1 = new PathSegmentCollection(); var pathSegmentCollection2 = new PathSegmentCollection(); var pathSegmentCollection3 = new PathSegmentCollection(); var pathSegmentCollection4 = new PathSegmentCollection(); PointCollection pointCollection = new PointCollection(); pointCollection.Add(p10); pointCollection.Add(p20); pointCollection.Add(p30); pointCollection.Add(p31); pointCollection.Add(p32); pointCollection.Add(p33); pointCollection.Add(p23); pointCollection.Add(p13); pointCollection.Add(p03); pointCollection.Add(p02); pointCollection.Add(p01); pointCollection.Add(p00); pathSegmentCollection4.Add(new PolyBezierSegment() { Points = pointCollection }); figures.Add(new PathFigure() { StartPoint = p00, Segments = pathSegmentCollection4, IsClosed = true }); path = new Path() { Data = new PathGeometry() { Figures = figures }, Stroke = new SolidColorBrush(Colors.Black), StrokeThickness = 2, Fill = new SolidColorBrush(color) }; this.Children.Add(path); } }
The code above is what gives each brick a kind of "shaking" appearance. The "shaking" square is actually composed by a Path element, which contains a PolyBezierSegment class that involves the Grid element. The PolyBezierSegment as shown above defines a collection of random points that roughly imitate a square, but that slightly moves left, right, up and down, giving an impression of a "shaking square".
Path
PolyBezierSegment
Another nice feature in Silverlight is the one that allows you to create templates for standard visual elements, such as buttons.
In our case, the standard button element doesn't look so good for the other elements on the screen. We have colorful bricks and the funny Comic Sans typeface, so the standard Silverlight button just doesn't fit in. But fortunately we can work on the templates, so that the regular buttons can share the same look and feel of the rest of the application. Here's how it works:
<Style TargetType="Button"> <Setter Property="FontFamily" Value="Comic Sans MS"/> <Setter Property="FontSize" Value="12"/> <Setter Property="FontWeight" Value="Bold"/> <Setter Property="Foreground" Value="#FF000000"/> <Setter Property="Padding" Value="3"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderBrush"> <Setter.Value> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFA3AEB9" Offset="0"/> <GradientStop Color="#FF8399A9" Offset="0.375"/> <GradientStop Color="#FF718597" Offset="0.375"/> <GradientStop Color="#FF617584" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter>
Template
<Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid ShowGridLines="False"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"/> <!--When the mouse is over the button, the button background is highlighted. --> <VisualState x:Name="MouseOver"> <Storyboard> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" To="#FFF0F0F0"/> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#FFF0F0F0"/> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#FFF0F000"/> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#FFF0F000"/> </Storyboard> </VisualState> <!--When the button is pressed, the button gets a blue bold border. --> <VisualState x:Name="Pressed"> <Storyboard> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" To="#FFFFFFFF"/> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#FFF0F0F0"/> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#FFE0E000"/> <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#FFFFFFFF"/> </Storyboard> </VisualState> <!--When the button is disabled, the button's opacity is lowered to a bit more than 50%. --> <VisualState x:Name="Disabled"> <Storyboard> <DoubleAnimation Duration="0" Storyboard.TargetName="DisabledVisualElement" Storyboard.TargetProperty="Opacity" To=".55"/> </Storyboard> </VisualState> </VisualStateGroup> <!--When the button is focused, the button's FocusVisualElement's opacity is set to 100%. --> <VisualStateGroup x:Name="FocusStates"> <VisualState x:Name="Focused"> <Storyboard> <DoubleAnimation Duration="0" Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Opacity" To="1"/> </Storyboard> </VisualState> <VisualState x:Name="Unfocused" /> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border x:Name="Background" Grid.Column="1" Margin="0,5,0,0" CornerRadius="0" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}"> <Grid Margin="1" ShowGridLines="False"> <Path Stroke="Blue" StrokeThickness="2" x:Name="BackgroundGradient"> <Path.Data> <PathGeometry> <PathFigureCollection> <PathFigure StartPoint="10,0" IsClosed="True"> <PathFigure.Segments> <PolyBezierSegment Points=" 30,5 50,-5 75,0 80,10 80,20 75,30 50,25 30,35 10,30 5,20 5,10 10,0"/> </PathFigure.Segments> </PathFigure> </PathFigureCollection> </PathGeometry> </Path.Data> <Path.Fill> <LinearGradientBrush x:Name="BackgroundAnimation" StartPoint=".7,0" EndPoint=".7,1" Opacity="1" > <GradientStop Color="#FFF0F0F0" Offset="0"/> <GradientStop Color="#FFF0F0F0" Offset="0.5"/> <GradientStop Color="#FFC0C000" Offset="0.5"/> <GradientStop Color="#FFC0C000" Offset="1"/> </LinearGradientBrush> </Path.Fill> </Path> </Grid> </Border> <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" Grid.Column="1" /> <Rectangle x:Name="DisabledVisualElement" Grid.Column="1" RadiusX="0" RadiusY="3" Fill="#FFFFFFFF" Opacity="0" IsHitTestVisible="false" /> <Path Grid.Column="1" StrokeThickness="3" x:Name="FocusVisualElement" Margin="0,5,0,0" Opacity="0" IsHitTestVisible="false"> <Path.Stroke> <LinearGradientBrush> <GradientStop Color="Blue" Offset="0"/> <GradientStop Color="Blue" Offset="1"/> </LinearGradientBrush> </Path.Stroke> <Path.Data> <PathGeometry> <PathFigureCollection> <PathFigure StartPoint="10,0" IsClosed="True"> <PathFigure.Segments> <PolyBezierSegment Points="30,5 50,-5 75,0 80,10 80,20 75,30 50,25 30,35 10,30 5,20 5,10 10,0"/> </PathFigure.Segments> </PathFigure> </PathFigureCollection> </PathGeometry> </Path.Data> </Path> </Grid> </Grid> </ControlTemplate> </Setter.Value>
That's it. I worked on it for the last 2 weeks, learned many things and it was a lot of fun for me. I hope it to be also useful and fun for you.
If you have any comments, suggestions, complaints about the article or the game, please let me know! Your feedback will be very appreciated.
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
General News Suggestion Question Bug Answer Joke Rant Admin
Math Primers for Programmers