Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

The Bricks Game for Silverlight

0.00/5 (No votes)
8 Sep 2010 1  
An article exploring the use of MVVM, styles and templates in game programming in Silverlight 4

Bricks!

Table of Contents

Introduction

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.

Youtube Video

You can save some of your time by taking a look at the YouTube video I uploaded, in the link below:

System Requirements

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:

  • Visual Web Developer 2010 Express

    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.

    The Bricks Solution

    Solution

    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:

    Project Description
    Bricks.Silverlight This is the Silverlight project itself, containing the UI and the Model-View-ViewModel logic.
    Bricks.Silverlight.Core This Silverlight Class Library project holds the core of the Bricks! funcionality.
    Bricks.Silverlight.Web This project is the start up project that contains the application's entry page, besides some resource files.

    Intro Menu

    The Intro Menu consists of the Bricks "logo" and some game instructions.

    Intro

    Figure 2: Intro Menu

    The MVVM Pattern

    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.

    MVVM

    Figure 3: Basic structure of the MVVM Pattern

    The complete binding mappings are described in the table below:

    View (BricksView) ViewModel (BricksViewModel)
    Visual Element Type Property Bound Property Type Converter
    pnlIntro StackPanel Visibility IsIntroVisible bool BooleanToVisibilityConverter
    pnlGameOver StackPanel Visibility IsGameOverVisible bool BooleanToVisibilityConverter
    pnlGamePaused StackPanel Visibility IsGamePausedVisible bool BooleanToVisibilityConverter
    txtScore TextBlock Text Score int (none)
    txtHiScore TextBlock Text HiScore int (none)
    txtLines TextBlock Text Lines int (none)
    txtLevel TextBlock Text Level int (none)
    lstBoard ListBox ItemsSource Bricks ObservableNotifiableCollection<IBrick> (none)
    lstNext ListBox ItemsSource Level ObservableNotifiableCollection<IBrick> (none)
    btnStart Button ButtonService.Command StartCommand ICommand (none)
    btnStart Button IsEnabled StartCanExecute bool (none)
    btnPause Button ButtonService.Command PauseCommand ICommand (none)
    btnPause Button IsEnabled PauseCanExecute bool (none)
    btnStop Button ButtonService.Command StopCommand ICommand (none)
    btnStop Button IsEnabled StopCanExecute bool (none)

    Some important notes about the bindings above:

    • Bindings to panel visibility: there are 3 panels, pnlIntro, pnlGameOver and pnlGamePaused which can be visible or invisible, depending on the values of the corresponding bool properties on the ViewModel side. The problem is that the Visibility property is not a boolean, but rather an enumeration, which can be Visible or Collapsed, and there is no automatic conversion between bool and Visibility. This is why we use the BooleanToVisibilityConverter, to map the values correctly. And here comes the main function of the BooleanToVisibilityConverter helper class:
              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;
              }
      
    • Bindings to int: In this case the conversion of int values to the Text properties of the txtScore, txtHiScore, txtLines, txtLevel visual elements is done automatically. Notice that in the setter below we have to use the OnPropertyChanged("Score") call, so that the View knows that the property was updated, and then updates itself accordingly.
              public int Score
              {
                  get
                  {
                      return score;
                  }
                  set
                  {
                      score = value;
                      OnPropertyChanged("Score");
                  }
              }
      
    • Bindings to list boxes: The Listbox class has an ItemsSource property, that could be bound to a Collection property. But instead, we bind it to a ObservableNotifiableCollection property, so that the Listbox can both listen to changes in the collection item list and changes in the item properties inside the list. The snippet below shows that the implementation on the ViewModel side is quite simple:
              public ObservableNotifiableCollection<IBrick>> Bricks
              {
                  get
                  {
                      return bricks;
                  }
                  set
                  {
                      bricks = value;
                  }
              }
      
    • Bindings to buttons: In this case, we bind 2 properties: One of them is the IsEnabled property, bound to simple bool properties in the ViewModel side, and the second is the ButtonService.Command attached property. As some readers may know, there are some techniques that could be used to bind commands, but I prefer to use the ButtonService helper class, proposed initially by Patrick Cauldwell in his blog. I like it specially because of its simplicity, as we can see in the example below:
              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>.

    The Power of Styles and Templates

    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.

    Intro

    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:

        <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!

    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:

    Intro

    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!

    The Bricks Sheet Background

    Intro

    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:

                <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>
    

    Game Logic

    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:

    Diagram

    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:

    BricksBoard

    Figure 9: The BricksBoard Class

    • BricksBoard Constructor: Notice below that the constructor receives an instance of and IPresenter class. This is so because of the Inversion of Control (IoC) pattern, sometimes called Dependency Injection (DI) pattern:
              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).

    • InitializeArray method: This method is responsible for "cleaning" the board and game status. That is, scores are cleared, and board is initialized:
              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();
              }
      
      The base class InitializeArray() method clears the board by creating an empty 2-dimensional array of 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;
                      }
                  }
              }
      
    • GetRandomShape method: this one randomly generates the nextTetromino that will fall from the board's top. This helps the player to think ahead for next move while dealing with the current move:
              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;
              }
      
    • ProcessNextMove method: This method is invoked by the BricksViewModel class in a timely fashion, depending on the game speed. This is done by the Tick event of the timer in the BricksViewModel class:
              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:

              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);
                  }
              }
      
    • StartRandomShape function: This function is called to instantiate a new random tetromino, placing it on the top of the board and in the middle of the board witdh. The function returns true if there's room for a new tetromino, of false otherwise, indicating that the board is full and that the game is over:
              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;
              }
      
    • RemovePieceFromCurrentPosition method: this method "detaches" the falling tetromino from its position, clearing the underlying board positions:
              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;
                          }
                      }
                  }
              }
      
    • TestPieceOnPosition function: this function tests whether a determined tetromino can be placed at a given position, that is: 1) every tetromino brick must fall in a position inside the board dimensions, and 2) every tetromino must fall in an empty space:
              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;
              }
      
    • PutPieceOnPosition method: updates the board with the current tetromino shape. In the end, the BricksPresenter receives a notification to update both scores and board.
              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);
                  }
              }
      
    • RemoveCompletedRows method: At each player's move, the game must test if some horizontal line is completed with bricks. If this is the case, the line is cleared in the board, and the BricksPresenter is updated to reflect the new board configuration. At each row completion, the score is incremented in 10 points. At each 10 rows completed, the level is incremented by 1:
              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;
              }
      
    • MoveLeft, MoveRight, MoveDown, Rotate90 and Rotate270 functions: these functions just propagates to the moving tetromino, so let's take a look the corresponding methods in the BaseShape object. Notice that these methods do just what their names tell, moving the tetramino left, right, down (if possible, of course!), and rotating, that is, transposing columns and rows in an ordered way:
              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;
              }
      

    Bonus: Creating Funny Brick Animations

    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.

            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".

    Bonus: Templating Funny Buttons

    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:

    • First of all, we must define the setters for the regular button properties:
              <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>
      
    • Then we must define a setter for a the Template property:
                  <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>
      
    In the Normal state, the button's border is defined by a Path element, which is defined by a PolyBezierSegment described by the curvy lines you see here.
    In the Focused state, the button gets a blue bold border, so that you can see which button is focused.
    In the MouseOver state, the button's background is highlighted.
    In the Disabled state, the button's opacity is lowered.

    Final Considerations

    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.

    History

    • 2010-08-29: Initial version.
    • 2010-09-07: Buttons templating added.
  • License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here