Hello friends, Let’s see how we can create a Tic Tac Toe puzzle game (3X3) step by step using Windows Form and C# DotNet in Visual Studio without using any complex/advance codes neither external libraries.
Prerequisites
- Basic c# programming knowledge
- Basic windows form application development knowledge
- Visual studio 2010+
Step 1.
Start a new Windows Form Project in Visual Studio which will provide you the default form template and add a picture box as shows in the below screenshot.
Step 2.
Add code to load canvas using bitmap/graphics classes and bind the bitmap in the picture box. (You can go to the code behind by right click on the form and select view code menu)
public Form1()
{
InitializeComponent();
loadCanvas();
}
Bitmap canvasBitmap;
Graphics canvasGraphics;
int canvasWidth = 15;
int canvasHeight = 20;
int[,] canvasDotArray;
int dotSize = 20;
private void loadCanvas()
{
// Resize the picture box based on the dotsize and canvas size
pictureBox1.Width = canvasWidth * dotSize;
pictureBox1.Height = canvasHeight * dotSize;
// Create Bitmap with picture box's size
canvasBitmap = new Bitmap(pictureBox1.Width, pictureBox1.Height);
canvasGraphics = Graphics.FromImage(canvasBitmap);
canvasGraphics.FillRectangle(Brushes.LightGray, 0, 0, canvasBitmap.Width, canvasBitmap.Height);
// Load bitmap into picture box
pictureBox1.Image = canvasBitmap;
// Initialize canvas dot array. by default all elements are zero
canvasDotArray = new int[canvasWidth, canvasHeight];
}
As in the code we maintain a canvas dot array to understand which are blocks are filled with shapes. The following picture will give more idea on this logic.
Let's run the application and see if the canvas loads correctly
Step 3.
Add Shape and Handler Classes to store/manage shapes.
This shape class will allow us to create/store shapes using two dimensional arrays.
namespace Tetris
{
class Shape
{
public int Width;
public int Height;
public int[,] Dots;
}
}
The following shape handler class will load shapes array in the constructor and have method to get a shape from the array on a random basis.
using System;
namespace Tetris
{
static class ShapesHandler
{
private static Shape[] shapesArray;
// static constructor : No need to manually initialize
static ShapesHandler()
{
// Create shapes add into the array.
shapesArray = new Shape[]
{
new Shape {
Width = 2,
Height = 2,
Dots = new int[,]
{
{ 1, 1 },
{ 1, 1 }
}
},
new Shape {
Width = 1,
Height = 4,
Dots = new int[,]
{
{ 1 },
{ 1 },
{ 1 },
{ 1 }
}
},
new Shape {
Width = 3,
Height = 2,
Dots = new int[,]
{
{ 0, 1, 0 },
{ 1, 1, 1 }
}
},
new Shape {
Width = 3,
Height = 2,
Dots = new int[,]
{
{ 0, 0, 1 },
{ 1, 1, 1 }
}
},
new Shape {
Width = 3,
Height = 2,
Dots = new int[,]
{
{ 1, 0, 0 },
{ 1, 1, 1 }
}
},
new Shape {
Width = 3,
Height = 2,
Dots = new int[,]
{
{ 1, 1, 0 },
{ 0, 1, 1 }
}
},
new Shape {
Width = 3,
Height = 2,
Dots = new int[,]
{
{ 0, 1, 1 },
{ 1, 1, 0 }
}
}
// new shapes can be added here..
};
}
// Get a shape form the array in a random basis
public static Shape GetRandomShape()
{
var shape = shapesArray[new Random().Next(shapesArray.Length)];
return shape;
}
}
}
Step 4.
Now let's go back to the form class to add method to consume the random
shape getter method and adjust the currentX variable to make the shape drawn in
the center position
int currentX;
int currentY;
private Shape getRandomShapeWithCenterAligned()
{
var shape = ShapesHandler.GetRandomShape();
// Calculate the x and y values as if the shape lies in the center
currentX = 7;
currentY = -shape.Height;
return shape;
}
Code to move the shape one step down/side and draw it into the canvas using a different bitmap/graphics classes.
// returns if it reaches the bottom or touches any other blocks
private bool moveShapeIfPossible(int moveDown = 0, int moveSide = 0)
{
var newX = currentX + moveSide;
var newY = currentY + moveDown;
// check if it reaches the bottom or side bar
if (newX < 0 || newX + currentShape.Width > canvasWidth
|| newY + currentShape.Height > canvasHeight)
return false;
// check if it touches any other blocks
for (int i = 0; i < currentShape.Width; i++)
{
for (int j = 0; j < currentShape.Height; j++)
{
if (newY + j > 0 && canvasDotArray[newX + i, newY + j] == 1 && currentShape.Dots[j, i] == 1)
return false;
}
}
currentX = newX;
currentY = newY;
drawShape();
return true;
}
Bitmap workingBitmap;
Graphics workingGraphics;
private void drawShape()
{
workingBitmap = new Bitmap(canvasBitmap);
workingGraphics = Graphics.FromImage(workingBitmap);
for (int i = 0; i < currentShape.Width; i++)
{
for (int j = 0; j < currentShape.Height; j++)
{
if (currentShape.Dots[j, i] == 1)
workingGraphics.FillRectangle(Brushes.Black, (currentX + i) * dotSize, (currentY + j) * dotSize, dotSize, dotSize);
}
}
pictureBox1.Image = workingBitmap;
}
Code to update the canvas dot array and do the game over check once the shape reaches the bottom or touches other shapes.
private void updateCanvasDotArrayWithCurrentShape()
{
for (int i = 0; i < currentShape.Width; i++)
{
for (int j = 0; j < currentShape.Height; j++)
{
if (currentShape.Dots[j, i] == 1)
{
checkIfGameOver();
canvasDotArray[currentX + i, currentY + j] = 1;
}
}
}
}
private void checkIfGameOver()
{
if (currentY < 0)
{
timer.Stop();
MessageBox.Show("Game Over");
Application.Restart();
}
}
Add a timer and consume the above methods to make the shapes dropping
back to back and do the game end check.
Shape currentShape;
Timer timer = new Timer();
public Form1()
{
InitializeComponent();
loadCanvas();
currentShape = getRandomShapeWithCenterAligned();
timer.Tick += Timer_Tick;
timer.Interval = 500;
timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
var isMoveSuccess = moveShapeIfPossible(moveDown: 1);
// if shape reached bottom or touched any other shapes
if (!isMoveSuccess)
{
// copy working image into canvas image
canvasBitmap = new Bitmap(workingBitmap);
updateCanvasDotArrayWithCurrentShape();
// get next shape
currentShape = getRandomShapeWithCenterAligned();
}
}
Not let’s run the application
Step 5.
Now let's add methods to turn the shape and rollback the turn. The rolling back happens once the player tries to turn the shape but that move is not available because of it reaches the bottom or touches other shapes.
namespace Tetris
{
class Shape
{
public int Width;
public int Height;
public int[,] Dots;
private int[,] backupDots;
public void turn()
{
// back the dots values into backup dots
// so that it can be simply used for rolling back
backupDots = Dots;
Dots = new int[Width, Height];
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
Dots[i, j] = backupDots[Height - 1 - j, i];
}
}
var temp = Width;
Width = Height;
Height = temp;
}
// the rolling back occurs when player rotating the shape
// but it will touch other shapes and needs to be rolled back
public void rollback()
{
Dots = backupDots;
var temp = Width;
Width = Height;
Height = temp;
}
}
}
Add keystroke events to move and turn shape. On clicking the right/left arrow keys the shape should move side wise, clicking on the top arrow key should turn the shape and clicking on the down arrow key should make the shape move faster.
public Form1()
{
// previous codes....
this.KeyDown += Form1_KeyDown;
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
var verticalMove = 0;
var horizontalMove = 0;
// calculate the vertical and horizontal move values
// based on the key pressed
switch (e.KeyCode)
{
// move shape left
case Keys.Left:
verticalMove--;
break;
// move shape right
case Keys.Right:
verticalMove++;
break;
// move shape down faster
case Keys.Down:
horizontalMove++;
break;
// rotate the shape clockwise
case Keys.Up:
currentShape.turn();
break;
default:
return;
}
var isMoveSuccess = moveShapeIfPossible(horizontalMove, verticalMove);
// if the player was trying to rotate the shape, but
// that move was not possible - rollback the shape
if (!isMoveSuccess && e.KeyCode == Keys.Up)
currentShape.rollback();
}
Step 6.
Now let's add new controls in the form in order to show the next shape, current score and level. (Picture box to show next shape and labels to show score and game level)
Add code to clear all the rows which are filled with shapes and increment the score, level and
speed values based on the cleared rows count.
int score;
public void clearFilledRowsAndUpdateScore()
{
// check through each rows
for (int i = 0; i < canvasHeight; i++)
{
int j;
for (j = canvasWidth - 1; j >= 0; j--)
{
if (canvasDotArray[j, i] == 0)
break;
}
if (j == -1)
{
// update score and level values and labels
score++;
label1.Text = "Score: " + score;
label2.Text = "Level: " + score / 10;
// increase the speed
timer.Interval -= 10;
// update the dot array based on the check
for (j = 0; j < canvasWidth; j++)
{
for (int k = i; k > 0; k--)
{
canvasDotArray[j, k] = canvasDotArray[j, k - 1];
}
canvasDotArray[j, 0] = 0;
}
}
}
// Draw panel based on the updated array values
for (int i = 0; i < canvasWidth; i++)
{
for (int j = 0; j < canvasHeight; j++)
{
canvasGraphics = Graphics.FromImage(canvasBitmap);
canvasGraphics.FillRectangle(
canvasDotArray[i, j] == 1 ? Brushes.Black : Brushes.LightGray,
i * dotSize, j * dotSize, dotSize, dotSize
);
}
}
pictureBox1.Image = canvasBitmap;
}
Add code to get and show the next shape in the side panel
Bitmap nextShapeBitmap;
Graphics nextShapeGraphics;
private Shape getNextShape()
{
var shape = getRandomShapeWithCenterAligned();
// Codes to show the next shape in the side panel
nextShapeBitmap = new Bitmap(6 * dotSize, 6 * dotSize);
nextShapeGraphics = Graphics.FromImage(nextShapeBitmap);
nextShapeGraphics.FillRectangle(Brushes.LightGray, 0, 0, nextShapeBitmap.Width, nextShapeBitmap.Height);
// Find the ideal position for the shape in the side panel
var startX = (6 - shape.Width) / 2;
var startY = (6 - shape.Height) / 2;
for (int i = 0; i < shape.Height; i++)
{
for (int j = 0; j < shape.Width; j++)
{
nextShapeGraphics.FillRectangle(
shape.Dots[i, j] == 1 ? Brushes.Black : Brushes.LightGray,
(startX + j) * dotSize, (startY + i) * dotSize, dotSize, dotSize);
}
}
pictureBox2.Size = nextShapeBitmap.Size;
pictureBox2.Image = nextShapeBitmap;
return shape;
}
Update the form constructor and timer click event methods to clear the filled
rows and show the next shape. Recently it was assigning a random shape from the list to the current shape variable. Here after it will assign the random shape in to the next shape variable and then it will be assigned to the current shape variable on dropping the next shape.
Shape currentShape;
Shape nextShape;
public Form1()
{
// Previous Code....
currentShape = getRandomShapeWithCenterAligned();
nextShape = getNextShape();
// Previous Code....
}
private void Timer_Tick(object sender, EventArgs e)
{
// Previous Code....
// if shape reached bottom or touched any other shapes
if (!isMoveSuccess)
{
// Previous Code....
// get next shape
currentShape = nextShape;
nextShape = getNextShape();
clearFilledRowsAndUpdateScore();
}
}
Let’s run the final code
You can try adding new interesting shapes in the shape handler class.
Enjoy Coding…!!!
Is it easily possible to have different colors for the different shapes?
ReplyDeleteYes, we can add a color property in the Shape class, which gets assigned with random colors from the constructor method. And when we draw the shape use that property instead of Black color.
DeleteVery well written about develop tetris brick game. If you want videos of brick game, please download vidmate.You can not download this app from play store but from other website. You must be wondering why vidmate.app has been removed from play store despite being so popular. Play Store had some policy of its own which Vidmate app has removed from play store for not following this app. But no problem, you can enjoy by downloading this app from other website. This app has all the facilities for your entertainment be it anything from TV to online news, movies, viral videos etc. All the facilities are absolutely free for you. You can also download brick game and Vidmate from 9apps
ReplyDeletecent
ReplyDeletewhy are you using a separate class to implement the rotation of the shape?
ReplyDelete