Article Options
Premium Sponsor
Premium Sponsor

 »  Home  »  Windows Development  »  Creating a simple game using GDI+ in C#
Creating a simple game using GDI+ in C#
by Andreas Papathanasis | Published  08/18/2002 | Windows Development | Rating:
Andreas Papathanasis

Andreas Papathanasis is a starting-up, 23-year old developer who has been into Microsoft's .NET technology since late 2001. He concentrates on ASP.NET Web Forms and Windows Forms applications using primarily C#.

Andreas works for a software company in Athens, Greece. In his spare time, he likes to drive, head off to quiet places with close friends and annoy his neighbors, while occasionally someone has to remind him that he still has work to do to earn his Physics degree. Andreas can be reached at harm@compulink.gr

 

View all articles by Andreas Papathanasis...
Creating a simple game using GDI+ in C#

Article source code: gdiplus_tetris.zip

In this article, we are going to demonstrate some of the processes involved in the creation of a simple 2D game - namely, a Tetris clone. The main objective is to give the reader some familiarity with GDI+, the successor to GDI, which is used for simple 2D graphics in the .NET world. Windows contains many controls that are used to display data and various kinds of information. However, it would be impossible to cover the needs of every application developer in a set of standard controls. Many kinds of programs will not use the standard windows controls at all, as they need to display their own graphics, shapes and images (examples of such programs can be Graph applications or games). For these reasons, developers have an API through which they can control exactly how their application controls the graphics it contains. This API is examined in the next few paragraphs, from the scope of a developer who writes managed code in .NET. The programming language used throughout the walkthrough will be C#, although a conversion to VB.NET should be rather simple, as the focus of the code is the use of the appropriate .NET classes and their methods, rather than the language itself.

Introduction to GDI+

Before getting our hands dirty with code, it would be wise to study the major graphics techniques we will be using. The System.Drawing namespace contains all the helper classes that will help us accomplish our goal. There is also the more advanced System.Drawing.Drawing2D namespace, that might be the center of an entire other article, and the specialized System.Drawing.Printing, System.Drawing.Text and System.Drawing.Imaging, that provide printing, typography and image handling services, appropriately. We won't be going into any of those here. Getting back to the System.Drawing area, let's examine the most important classes we will encounter on the way. The Graphics class is the fundamental one here, and it represents a display context. To make a long story short, a display context can be considered as a generic object which among others provides some methods for drawing classic shapes (lines, curves etc.) as well as text, using various line styles ("pens") and fill styles ("brushes"). It's most useful characteristic is that the programmer does not have to deal with specific hardware or even output devices. The functions and the way they are used are the same on computers with different graphics hardware, even for creating graphics on a printer rather than the monitor.

A programmer does not explicitly create Graphics objects using the new operator. Most of the time, the object is obtained indirectly by arguments passed from the operational system to the application - we will see exactly how in a little while, on the code for our game. Once we have the object in our hands, it's very easy to draw shapes using its self-describing methods. For example, the DrawImage() method of the Graphics object displays an image, while the DrawLine(), DrawRectangle(), DrawCurve() and DrawArc() are used to create basic shapes. There are some helper classes in the System.Drawing namespace that can be passed as parameters to these methods. The Pen class is used to specify a line style to use in drawing and the Brush class specifies a fill style for closed shapes. There are some classes helpful in specifying areas and locations in the client area (by the way, a client area is the part of the window that an application has available to display it's data, that is the whole window minus the title bar, menu/tool bars, status and scroll bars). The Point class represents a single pixel, whose coordinates are kept in the X and Y member variables of the class. X is an integer that represents the pixel's distance from the leftmost part of the client window, and Y is the distance from the top. Similarly, the Rectangle class represents a rectangular area. One of its constructors takes two Point objects (for the upper left and lower right of the rectangle).

Redrawing graphics - the Invalidate() method

Windows is a very complex operating system. To completely understand how everything works is really not a trivial task, and even seemingly simple operations require much work from the computer behind the scenes. One of these "simple" tasks, which we will run into again when designing our game, is the following: Imagine a user is running two programs simultaneously, each one in its separate window. The first program might display that day's appointments he has arranged, and the second is a browser, displaying some web site. Suddenly, the user moves his browser over a part of the first program. Effectively, that part of the first program's window has disappeared, as another window must be displayed on top of it. Of course, we have the feeling that the information stored there is not actually lost - windows must have a way to redraw whatever was there in the first place. And that's true, for if we move away from the first window our browser again, the missing part of the appointments program is visible again. What is actually happening? No, the part that appears again was not retrieved directly for memory. It would be a waste of resources for the operating system to store every program's video display in memory - imagine that we might have many windows, one on top of the other. In fact, Windows only tells the application itself that it has to redraw a part of it's display because of a special event (like the moving of another window), after that, the application is responsible for what happens. The most common action for the application is, of course, to redraw that specific area of the display.

Windows accomplishes most of it's magic by sending special events to every application. It's the operating system's way of "telling" the programs of what has happened in the computer or it's peripherals so that they can perform an appropriate function. Fundamental events include mouse clicks and key presses - there are actually many other events sent. In our case of interest, the events are called Paint events and Windows automatically sends them to any program that needs to redraw part of its user interface. In our example above, as soon as we moved the browser window back to its original place, Windows would send to the appointment program, a paint event. It would "say" to that program that something happened and it needs to redraw a specific part of its window. Information on what part needs redrawing would also be passed along with the event. Fortunately, this message sending and redrawing is handled automatically for classic windows controls - the operating system knows when it needs to repaint something and how the repainting will be done. Actually, the Form class has an OnPaint method (it is overridden from the Control class), which handles the automatic redrawing.

All the convenient graphics management provided by Windows, stops when it comes to GDI+. Windows will no longer automatically update parts of your application window that have shapes drawn on them by GDI+. In this case, we will have to override the OnPaint method we mentioned before, and specify exactly how the redrawing will take place. Of course, our program will probably need to update its graphics not only when another window moves on top of it, but when it is minimized and maximized again. In our Tetris clone, we will have to redraw our falling piece every second or so. That is, we will actually send our program a message that will tell it to redraw part (or all) of its graphics. When the program gets this message, it will call the OnPaint method for the requested area, as it would have done when it was told by Windows that a specific area needs to be redrawn because of another window moving by - it is actually the same thing. The way we tell our program to repaint its graphics is the Invalidate() method. We find this in the Control class, and of course in the Form class, which inherits it. That method can be called without any parameters, and in this case it will request the redraw to occur in all the control's client area. But updating large parts of the graphics display in short intervals is a very resource - demanding procedure, so in most cases we must call the method specifying only the area where the updated graphics must appear. For example, when a block falls down one place in our game, we won't have to update everything in our graphics display, just the area that contains both the old and the new position of the piece. That area will be usually passed as a Rectangle object. We will demonstrate this in a number of cases in the following section, where we examine the actual code for the game.

Starting the code - The Blocks Class

First of all, we will need to create a class that will represent the falling blocks in the Tetris game -we will instantiate an object from this class for every new falling piece we have. See Listing 1 for Blocks.cs code.

The first part of the class is standard, boring member declarations which should be self-explanatory. Every piece will have its own width and height, which will be expressed in units. We will be using our own metric system with one unit being the smallest part a block has. Therefore, the large straight Tetris block will be one unit wide and four units high (or the contrary, when it is being reversed). The fundamental part of the blocks is a small square image (10x10 pixels in our game), stored in the block.gif file, which must be present in the project directory for the class to compile. The whole playing field will be ten units wide and twenty high. The most interesting part in the class declaration is the Shape member: it is a two dimensional boolean array, with no specific bounds set and which will be used to represent the exact shape for the block. Every block is made of small fundamental squares. If we consider the rectangle that fully contains the shape, it is made of either "active" (represented as true in our array) or "inactive" (false) parts. A look in the following shape should make it clearer - it demonstrates how the "cross" piece is represented in code, as a two dimensional boolean array:

      

Every object that is of class Blocks must have a way to draw itself on the screen. That's what the Draw method does. It only accepts a Graphics object (we will see later, on the main program, how we will acquire the Graphics object so that we can pass it to this method). Using this object we draw fundamental squares that make up the block as needed, based on the Top and Left coordinates of the block, and of course it's Shape. Graphics objects have a method, DrawImage(), that can be used to display any Image object, scaled in the region specified by it's second parameter (a Rectangle object). We had declared an image early in the class, and loaded it from the file block.gif, using the static method FromFile() of the Image class in the System.Drawing namespace. The constants blockImageWidth and blockImageHeight represent the actual width and height of the block image, and are used in various calculations.

In the code above we have also included some constructors. Most of the time we will use the one that only accepts an integer and creates one of the pre-defined shapes. If you really followed up to this point, you will probably note that in the way the shapes are defined in this constructor, one can very easily add new, custom block shapes. All you have to do is define your block's width and height and then explicitly set all the members of the shape matrix to true or false, according to the block you are designing. The shape above the previous paragraph can be a big help to anyone who wants to understand how to create custom blocks.

Main program

After our background work in the Blocks class, we are ready to move on the main game's code. It would be wiser to split up our code a little bit, by creating a separate windows user control that will contain just the playing field of our game-named appropriately PlayingField.cs. That way the whole application will gain in both maintainability and scalability. See Listing 2 for PlayingField.cs code.

Don't forget that the PlayingField control will need to access the Blocks class created earlier, so it's important to reference that class' assembly (if they are not in the same assembly) and add a using clause for that class namespace in the beginning. No action is necessary if both classes are in the same project and namespace.

The above class has some members that are used internally to keep information on the status of the game. The currentBlock object, of type Block, is the currently active piece in our game. There's also nextBlock member, also of type Block. This variable keeps in memory next piece to be entered into the game. We have also added a timer, which is activated regularly and is used to move the falling piece to its next location. The Level property affects the game speed (by setting the timer's interval) while the score variable keeps the current points scored. The Field private variable is a two dimensional boolean array, sized 10x20 and it represents the state of the playing field. Array values that are true represent game areas that have a fallen square. All the other areas on the game that are clear have the value of false in the Field array.

Let's take a look at our important override of the OnPaint method; an override that we have already discussed why it must happen. Windows knows that it must call our overridden version of OnPaint whenever redrawing needs to take place, and it also should pass the events arguments along with it. These specific arguments are represented in the .NET framework by the PaintEventArgs class, passed on to our method. As you can see from the code, the class instance that is given to us also contains the Graphics object (display context) used in the operation. That's how we get the Graphics object to then use it in various drawing operations. We have divided all of our drawing code in three separate functions, and pass each one the display object so that they can access the client area's graphics. This way, the graphics are redrawn every time Windows tells our application they need to - our application window being restored from the minimized position, another window passed on top of it etc. What happens when we need to display updated information ourselves? We call the OnPaint method ourselves (remember, the operating system also knows to call it when the need arises), but not directly - that would be a really bad programming technique. We do it through the Invalidate() method that was described earlier. Take a look at the MovePieceDown() method, which is called every time the timer ticks. After it increments the Top property of the current block by one, effectively moving it one place lower, it calls Invalidate() for the area covering both the block's old and new positions. The Invalidate() method always calls the OnPaint() method, and in this case our overridden version of it. The rectangle we passed saves system resources as it instructs the program to only update the specified region.

Finally, note that in the end of our override version of the OnPaint method, we again call the base class' OnPaint version, passing the same PaintEventArgs. This ensures that the application really takes care of all the redrawing that needs to be done in every aspect, not only drawing done by the programmer but by that done by Windows itself. It's a good habit to always include this directive on the end of every overridden OnPaint method - failure to do so may result in unexpected program behavior.

A part of GDI+, which deserves to be mentioned separately, is the drawing of text strings. One would perhaps imagine that displaying text on the screen might be a simple task, but in most cases it is not. It gets somewhat complicated when the programmer needs to place many text strings on the display. Careful algorithms will usually need to be designed if your GDI+ application needs to handle large strings, as sentences tend to fall one over the other or leave the edges of the client area, making the application look very unprofessional. Our involvement with string handling in the above code is not that complicated, however, and only demonstrates the basics. All of the action takes place in the small DrawTextInfo method, which is called from the OnPaint() method as needed to update the information on the game using text (score and current level). We first create an object of Font class, which as its name implies, represents a text font. The constructor of the class accepts a string with the name of the font (if that name is not found on the system running the application no exception is thrown but a default font is used instead) and it's size, as a float. Then, we just call the DrawString() method of the Graphics object, passing along the actual string to be displayed, our font, a brush (we used one of the brushes provided "out of the box" from the framework, and specifically from the System.Drawing.Brushes class) and the coordinates of the location where the string placement will occur. Note that the second string's starting point height is equal to the height of the font we use (Height is a property of the Font class). That way we ensure that the sentences won't fall one over the other in any case - that might have happened if we had calculated the height of the starting point for the second string by ourselves, and hard-coded it into our application.

Completing the project

Actually, that's all the code that there is for the game. All we need to do from now on is to add the PlayingField control to a Windows Form. If you are using Visual Studio .NET, you should first create new Windows Application Project and then add to the project the files below (Blocks.cs and PlayingField.cs), using the Add Existing File option. Before compiling, make sure the block.gif image file is in the program directory (you can download this file below). It is possible to add other controls to the form, in order to control the game (for example, it would be nice to display the next piece and in fact, the necessary code for that is already in the PlayingField.cs source code above - one would only need to call a slightly different Draw() method for the nextBlock member, to show it in another area of the form, outside of the playing field. We leave that as an "exercise" to the reader!). Another control could be used to change the Level member of the PlayingField class - which in turn, controls the falling speed of the blocks. However, when creating other controls, make sure with extra code that the PlayingField always has focus, or it won't catch the keyboard events and won't respond to key commands.

See article source code for complete listing of all code presented in this article.

Listing 1. Blocks.cs

using System;
using System.Drawing;

namespace Tetris
{
  public class Block
  {
    private short _width;
    private short _height;
    private bool[,] _shape;
    private short _top;
    private short _left;

    // The image that represent the fundamental square that makes up 
    // the blocks
    // The file block.gif must be present with the 
    Image blockImage = Image.FromFile("block.gif");

    private const int blockImageWidth = 10;
    private const int blockImageHeight = 10;

    // The distance that the block has from the top of the playing field.
    // Ranges from 1 to 20
    public short Top
    {
      get
      {
        return _top;
      }
      set
      {
        if ((value > -1) && (value < 21))
        {
          _top = value;
          if (_top == 0)_top=1;
        }
        else
        {
          throw new ArgumentException("Top can't be less than 1 or " + 
                                      "more than 20");
        }
      }
    }

    // The distance that the block has from the left of the playing field.
    // Ranges from 0 to 9
    public short Left
    {
      get
      {
        return _left;
      }
      set
      {
        if ((value > -1) && (value < 10))
          _left = value;
        else
          throw new ArgumentException("Left can't be less than 0 or " + 
                                      "more than 10");
      }
    }

    // How many units wide the block is. Ranges from 1 to 4
    public short Width
    {
      get
      {
        return _width;
      }
      set
      {
        if ((value > 0) && (value < 5))
          _width = value;
        else
          throw new ArgumentException("Height can't be less than 1 or " + 
                                      "more than 4");
      }
    }

    // How many units high the block is. Ranges from 1 to 4 
    public short Height
    {
      get
      {
        return _height;
      }
      set
      {
        if ((value > 0) && (value < 5))
          _height = value;
        else
          throw new ArgumentException("Height can't be less than 1 or " + 
                                      "more than 4");
      }
    }

    // This is a two dimension block that sets the exact shape of the block. 
    public bool[,] Shape
    {
      get
      {
        return _shape;
      }
      set
      {
        if (value.Length == _width * _height)
          _shape = value;
        else
          throw new InvalidOperationException("Specified index length does " +
                                              "not match block dimensions");
      }
    }

    // Method used to display the Block object, using the provided 
    // Graphics object
    public void Draw(Graphics dc)
    {
      for (int i = 0; i < this.Width; i ++)
      {
        for (int j = 0; j < this.Height; j++)
        {
          if (this.Shape[i,j] == true)
          {
            Rectangle r1 = new Rectangle((this.Left + i) * blockImageWidth, 
                                        (this.Top + j) * blockImageHeight, 
                                        blockImageWidth, blockImageHeight);
            dc.DrawImage(blockImage, r1);
          }
        }
      }
    }

    // PUBLIC CONSTRUCTORS FOR THE BLOCK CLASS//
    public Block(){}

    public Block(short width, short height)
    {
      this._width = width;
      this._height = height;
      this._top = 0;
      this._left = 5;
    }

    // This will be the commonly used constructor. It initializes
    // standard Tetris blocks, based on the code passed.
    public Block(int code)
    {
      bool[,] shp1;

      switch (code)
      {
        case 1: //"straight":
          this._width = 1;
          this._height = 4;
          this._top = 0;
          this._left = 5;
          shp1 = new bool[_width,_height];
          shp1[0,0] = true;
          shp1[0,1] = true;
          shp1[0,2] = true;
          shp1[0,3] = true;
          this.Shape = shp1;
          break;

        case 2: //"rectangle":
          this._width = 2;
          this._height = 2;
          this._top = 0;
          this._left = 4;
          shp1 = new bool[_width,_height];
          shp1[0,0] = true;
          shp1[0,1] = true;
          shp1[1,0] = true;
          shp1[1,1] = true;
          this.Shape = shp1;
          break;

        case 3: //"cornerleft":
          this._width = 3;
          this._height = 2;
          this._top = 0;
          this._left = 4;
          shp1 = new bool[_width,_height];
          shp1[0,0] = true;
          shp1[1,0] = true;
          shp1[2,0] = true;
          shp1[0,1] = true;
          shp1[1,1] = false;
          shp1[2,1] = false;

          this.Shape = shp1;
          break;

        case 4: //"cornerright":
          this._width = 3;
          this._height = 2;
          this._top = 0;
          this._left = 4;
          shp1 = new bool[_width,_height];

          shp1[0,0] = true;
          shp1[1,0] = true;
          shp1[2,0] = true;
          shp1[0,1] = false;
          shp1[1,1] = false;
          shp1[2,1] = true;

          this.Shape = shp1;
          break;

        case 5: //"cross":
          this._width = 3;
          this._height = 2;
          this._top = 0;
          this._left = 4;
          shp1 = new bool[_width,_height];
          shp1[0,0] = true;
          shp1[1,0] = true;
          shp1[2,0] = true;
          shp1[0,1] = false;
          shp1[1,1] = true;
          shp1[2,1] = false;

          this.Shape = shp1;
          break;

        case 6: //"zigzag1":
          this._width = 2;
          this._height = 3;
          this._top = 0;
          this._left = 4;
          shp1 = new bool[_width,_height];
          shp1[0,0] = false;
          shp1[1,0] = true;
          shp1[0,1] = true;
          shp1[1,1] = true;
          shp1[0,2] = true;
          shp1[1,2] = false;

          this.Shape = shp1;
          break;

        case 7: //"zigzag2":
          this._width = 2;
          this._height = 3;
          this._top = 0;
          this._left = 4;
          shp1 = new bool[_width,_height];
          shp1[0,0] = true;
          shp1[1,0] = false;
          shp1[0,1] = true;
          shp1[1,1] = true;
          shp1[0,2] = false;
          shp1[1,2] = true;

          this.Shape = shp1;
          break;


        default:
          throw new ArgumentException("Invalid block code");
      }

    }

  }
}

Listing 2. PlayingField.cs

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;

namespace Tetris
{
  public class PlayingField : System.Windows.Forms.UserControl
  {
    private System.ComponentModel.IContainer components;

    #region Component Designer generated code

    private void InitializeComponent()
    {
      this.components = new System.ComponentModel.Container();
      this.timer1 = new System.Windows.Forms.Timer(this.components);
      // 
      // timer1
      // 
      this.timer1.Enabled = true;
      this.timer1.Interval = 150;
      this.timer1.Tick += new System.EventHandler(this.timer1_tick);
      // 
      // PlayingField
      // 
      this.BackColor = System.Drawing.Color.PapayaWhip;
      this.Name = "PlayingField";
      this.Size = new System.Drawing.Size(102, 202);
      this.KeyPress += new System.Windows.Forms.KeyPressEventHandler
                           (this.Field_KeyPress);

    }
    #endregion


    private int score = 0;
    private bool[,] Field = new bool[11,21];
    Image blockImage = Image.FromFile("block.gif");
    Block currentBlock;
    private System.Windows.Forms.Timer timer1;
    public Block nextBlock;
    private int linesMoved = 0;
    private bool _paused = false;
    public int maxNumberofBlocks = 7;

    private int _level = 1;

    // Current playing level, affects game speed and points awarded 
    // for each completed line
    public int Level
    {
      get
      {
        return _level;
      }
      set
      {
        if ((value > 0) && (value < 11))
        {
          _level = value;
          this.timer1.Interval = (1000 / value);
        }
        else throw new ArgumentException("Level must be between 1 and 10");
      }
    }

    public PlayingField()
    {
      InitializeComponent();
      timer1.Interval = (1000 / Level);
      StartNewGame();
    }

    private void timer1_tick(object sender, System.EventArgs e)
    {
      MovePieceDown();
    }

    // Gets called on every tick of the timer. Moves the current piece 
    // downwards and performs various checks. 
    private bool MovePieceDown()
    {
      linesMoved += 1;

      if (currentBlock == null)
        CreateNewBlock();

      currentBlock.Top += 1;
      Invalidate(new Rectangle(currentBlock.Left * 10, 
                               currentBlock.Top * 10 - 10, 
                               currentBlock.Width * 10, 
                               currentBlock.Height * 10 + 10));

      // Check if piece has hit the floor
      if (currentBlock.Top + currentBlock.Height > 20)
      {
        MovePieceToPile();
        CreateNewBlock();

        return true;
      }

      // Check if any piece part has hit the pile
      for (int i = 0; i < currentBlock.Width; i ++)
      {
        for (int j = 0; j < currentBlock.Height; j++)
        {
          int fx, fy;
          fx = currentBlock.Left + i;
          fy = currentBlock.Top + j;
          if ((currentBlock.Shape[i,j] == true) 
               && (Field[fx, fy + 1] == true))
          {
            MovePieceToPile();
            CreateNewBlock();
            return true;
          }
        }
      }

      return false;
    }

    // Check if there are any completed lines, clear them if there are
    private void CheckForLines()
    {
      for (int j = 20; j > 0; j--)
      {
        bool fullLine = true;

        for (int i = 0; i < 10; i++)
        {
          if (Field[i, j] == false)
          {
            fullLine = false;
            break;
          }
        }
        if (fullLine)
          ClearLine(j);
      }
    }

    // Clear all blocks in specified line. 
    private void ClearLine(int lineNumber)
    {
      for (int j = lineNumber; j > 0; j--)
      {
        for (int i = 0; i < 10; i++)
          Field[i, j] = Field[i, j - 1];
      }
      score += 10 * Level;
      Invalidate(new Rectangle(new Point(0, 0), 
                 new Size(102, 2 + (10 * lineNumber))));
    }


    // Update fallen pieces with current block. This function is called 
    // when the current block falls on another block, or the bottom of 
    // the playing field. 
    private void MovePieceToPile()
    {
      for (int i = 0; i < currentBlock.Width; i ++)
      {
        for (int j = 0; j < currentBlock.Height; j++)
        {
          int fx, fy;
          fx = currentBlock.Left + i;
          fy = currentBlock.Top + j;
          if (currentBlock.Shape[i,j] == true)
            Field[fx, fy] = true;

          CheckForLines();
          CheckForGameOver();
        }
      }
    }

    private void CheckForGameOver()
    {
      if (linesMoved == 1)
      {
        timer1.Enabled = false;

        // Uncomment the following line to get a game over message, 
    // leave as is to restart immediately
        //MessageBox.Show("Game Over!", "Blocks Game", MessageBoxButtons.OK, 
                          MessageBoxIcon.Stop);

        StartNewGame();
      }

    }

    public void StartNewGame()
    {
      score = 0;
      Invalidate();
      timer1.Enabled = true;
      // Clear the playing field
      for (int i = 0; i < 11; i++)
      {
        for (int j = 0; j < 21; j++)
          Field[i, j] = false;
      }
    }

    public void CreateNewBlock()
    {
      linesMoved = 0;


      Random rdm1;

      // Replace current block with what was previously the next block
      if (nextBlock != null)
        currentBlock = nextBlock;
      else
      {
        currentBlock = new Block(1);
        rdm1 = new Random(unchecked((int)DateTime.Now.Ticks)); 

        nextBlock = new Block(rdm1.Next(1, maxNumberofBlocks + 1));
      }

      // Create a random next block
      rdm1 = new Random(unchecked((int)DateTime.Now.Ticks)); 

      nextBlock = new Block(rdm1.Next(1, maxNumberofBlocks + 1));

      Parent.Invalidate();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
      Graphics dc = e.Graphics;

      DrawFallenBlocks(dc);
      DrawCurrentBlock(dc);

      DrawTextInfo(dc);

      base.OnPaint(e);
    }

    // Display text information on the top left corner of the control
    private void DrawTextInfo(Graphics dc)
    {
      Font fnt1 = new Font("Tahoma", 7F);
      dc.DrawString("Score: " + score.ToString(), fnt1, Brushes.Aqua, 0, 0);
      dc.DrawString("Level: " + Level.ToString(), fnt1, Brushes.Aqua, 0, 
                    fnt1.Height);
    }


    // Display falling black
    private void DrawCurrentBlock(Graphics dc)
    {
      if (currentBlock == null)
        return;

      currentBlock.Draw(dc);

    }

    // Displays the blocks already on the pile
    private void DrawFallenBlocks(Graphics dc)
    {
      for (int i = 0; i < 11; i++)
      {
        for (int j = 0; j < 21; j++)
        {
          if (Field[i, j] == true)
          {
            Rectangle r1 = new Rectangle(i * 10, (j - 1) * 10, 10, 10);
            dc.DrawImage(blockImage, r1);
          }
        }
      }

    }

    // Pauses / unpauses the game
    public void Pause()
    {
      if (_paused)
      {
        _paused = false;
        timer1.Enabled = true;
      }
      else
      {
        _paused = true;
        timer1.Enabled = false;
      }
    }

    // Respond to key controls
    private void Field_KeyPress(object sender, 
        System.Windows.Forms.KeyPressEventArgs e)
    {
      switch (e.KeyChar)
      {
        case ',':

          // Move Left
          for (int i = 0; i < currentBlock.Width; i ++)
          {
            for (int j = 0; j < currentBlock.Height; j++)
            {
              int fx, fy;
              fx = currentBlock.Left + i;
              fy = currentBlock.Top + j;
              if (fx > 0)
              {
                if ((currentBlock.Shape[i,j] == true) 
                    && (Field[fx - 1, fy + 1] == true))
                {
                  return;
                }
              }
            }
          }

          if (currentBlock.Left > 0)
          {
            currentBlock.Left -= 1;
            Invalidate(new Rectangle(currentBlock.Left * 10 - 20, 
                       currentBlock.Top * 10 - 10,
                       (currentBlock.Width) * 10 + 30,
                       currentBlock.Height * 10 + 10));
          }

          break;

        case '.':

          // Move Right

          for (int i = 0; i < currentBlock.Width; i ++)
          {
            for (int j = 0; j < currentBlock.Height; j++)
            {

              int fx, fy;
              fx = currentBlock.Left + i;
              fy = currentBlock.Top + j;
              if (fx > 0)
              {
                if ((currentBlock.Shape[i,j] == true) 
                    && (Field[fx + 1, fy + 1] == true))
                {
                  return;
                }
              }
            }
          }

          if (currentBlock.Left + currentBlock.Width  < 10)
          {
            currentBlock.Left += 1;
            Invalidate(new Rectangle((currentBlock.Left) * 10 - 20, 
                       currentBlock.Top * 10 - 10, 
                       (currentBlock.Width) * 10 + 20, 
                       currentBlock.Height * 10 + 10));
          }
          break;

        case ' ':
          // Rotate Piece
          Block rotatedBlock = new Block();
          rotatedBlock.Width = currentBlock.Height;
          rotatedBlock.Height = currentBlock.Width;
          rotatedBlock.Left = currentBlock.Left;
          rotatedBlock.Top = currentBlock.Top;

          // Don't rotate if the new rotated shape exceeds screen dimensions
          if (((rotatedBlock.Left + rotatedBlock.Width) > 10) 
              || ((rotatedBlock.Top + rotatedBlock.Height) > 19))
            return;

          rotatedBlock.Top = currentBlock.Top;
          rotatedBlock.Shape = new bool[currentBlock.Height, currentBlock.Width];

          for (int i = 0; i < currentBlock.Width; i++)
          {
            for (int j = 0; j < currentBlock.Height; j++)
            {
              rotatedBlock.Shape[j, i] = currentBlock.Shape[i, 
                       (currentBlock.Height - 1) - j];
            }
          }
          // If the new piece collapses into existing fallen pieces, 
          // cancel the rotation
          for (int i = 0; i < rotatedBlock.Width; i ++)
          {
            for (int j = 0; j < rotatedBlock.Height; j++)
            {
              int fx, fy;
              fx = rotatedBlock.Left + i;
              fy = rotatedBlock.Top + j;
              if (Field[fx,fy] == true)
                return;
            }
          }

          // Confirm the  rotation
          currentBlock = rotatedBlock;

          Invalidate(new Rectangle(currentBlock.Left * 10 - 30, 
                     currentBlock.Top * 10 - 30, 
                     currentBlock.Width * 10 + 60, 
                     currentBlock.Height * 10 + 60));

          break;

        case '/':
          // Drop Piece
          while(!MovePieceDown()){}

          break;

        //pause (key P)
        case 'P':
          this.Pause();
          break;
        case 'p':
          this.Pause();
          break;

        default:
          break;
      }
    }

    protected override void Dispose( bool disposing )
    {
      if( disposing )
      {
        if(components != null)
        {
          components.Dispose();
        }
      }
      base.Dispose( disposing );
    }
  }
}
How would you rate the quality of this article?
1 2 3 4 5
Poor Excellent
Tell us why you rated this way (optional):

Article Rating
The average rating is: No-one else has rated this article yet.

Article rating:3.57446808510638 out of 5
 94 people have rated this page
Article Score45640
Sponsored Links