Programming a Tetris Game in C# - Full Guide

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello and welcome in this video you will learn how to program this tetris game in c-sharp using wpf let's begin by setting up the project go ahead and open visual studio click the create a new project button and then choose wpf as the project template i will call the project tetris and leave it in the default folder now it will ask about the target framework and net 5.0 is fine before we start writing any code let me show you how we are going to represent the game grid tetris is played on a grid with 20 rows and 10 columns but we will add two extra rows at the top for spawning blocks these two rows will be hidden so they won't be visible during gameplay we will count the rows from top to bottom and the columns from left to right just like we do for matrices so the origin is this top left cell at row 0 and column 0. we will represent the game grid using a two-dimensional integer array let's say that this is the current state of the game in this case the two-dimensional array will look like this empty cells are represented with the value 0. a cyan tile is represented with the value 1 a blue tile has value 2 an orange tile has value 3 and a yellow tile has value 4. a green tile has value 5 a purple tile has value 6 and finally a red tile has value 7. note that the current block the one that can still be controlled by the player is not reflected in the array it will be stored separately as you will see in a minute let's return to the editor and write a class to represent the game grid i will create a new class and call it game grid [Music] this class will hold a two-dimensional rectangular array the first dimension is the row and the second dimension is the column we will also create properties for the number of rows and columns and then we will define an indexer to provide easy access to the array with this in place we can use indexing directly on a gamegrid object the constructor will take the number of rows and columns as parameters this way the class could also be used in a micro or mega version of tetris with untraditional grid size in the body we save the number of rows and columns and initialize the array let's create a few convenience methods the first one will check if a given row and column is inside the grid or not to be inside the grid the row must be greater than or equal to 0 and less than the number of rows similarly for the column but of course it must be less than the number of columns next we will write a method that checks if a given cell is empty or not it must be inside the grid and the value at that entry in the array must be zero we also want a method which checks if an entire row is full and one which checks if a row is empty and of course when there are full rows they need to be cleared and the rows above should be moved down here is how we will clear the full rows i assume that the game grid looks like this and then i drop an eye block on the right side we will use a variable called cleared which contains the number of rows cleared then we start at the bottom row and check if it's full in this case it isn't so we simply move on to the next row this row is full so we clear it and increment the cleared variable the third row is not full but since we have cleared one row below it it will be moved down by one row the fourth row is full so we clear it and increment cleared neither of the remaining rows are full so they will be moved down by two rows because cleared equals two this process can be stopped when an empty row is encountered or you can just move down the remaining empty rows okay so let's write a method which clears a row and one that moves a row down by a certain number of rows with this in place we can now implement a clear full rows method the cleared variable starts at 0 and we move from the bottom row towards the top we check if the current row is full and if it is we clear it and increment cleared otherwise if cleared is greater than zero then we move the current row down by the number of cleared rows and in the end we return the number of cleared rows great now the gamegrid class is done let's see how the blocks work and how we can represent them in tetris there are seven different blocks each consisting of four tiles here i have shown the blocks in the state they are in when they are spawned each block appears to rotate around a specific point which we mark with a dot most blocks rotate around the center of a tile for example the t block rotates around this tile the eye block however rotates around this point which is not the center of a tile and the o block rotates around this point so it doesn't appear to rotate at all there are many ways to represent the blocks but here is how we will do it imagine that there is a bounding box or grid surrounding each block this grid is just large enough to fit the block in all four rotation states we define the top left cell to be the origin at row 0 and column 0 exactly like we did for the game grid for each of the 4 rotation states we will simply store the occupied positions in this bounding grid in state 0 we have tiles at row 1 column 0 row 1 column 1 row 1 column 2 and row 1 column 3. we store the tile positions for the remaining states in the same way and then we store an integer indicating which rotation state the block is currently in this way we just have to change an integer to rotate a block all right we have covered rotation but the player can also move a block from side to side and down this can be represented easily by storing a row offset and a column offset in this example we have a row offset of 4 and a column offset of 2. to see why that is we need to consider the bounding grid of the block now we can see that the origin of the bounding grid is at row 4 and column 2 and that's why we have that offset when we want to move a block all we have to do is change the offset to represent a position or cell in a grid we will add a symbol position class this class will store a row and a column and then we will give it a simple constructor next up we will create a blog class it will be an abstract class and then we will write a subclass for each specific block we need the following data for each block a two dimensional position array which contains the tile positions in the four rotation states a start offset which decides where the block spawns in the grid and an integer id which we need to distinguish the blocks besides that we will store the current rotation state and the current offset in the constructor we set the offset equal to the start offset let's write a method which returns the grid positions occupied by the block factoring in the current rotation and offset the method loops over the tile positions in the current rotation state and adds the row offset and column offset the next method rotates the block 90 degrees clockwise we do that by incrementing the current rotation state wrapping around to zero if it's in the final state similarly we will add a method to rotate counter-clockwise next we will add a move method which moves the block by a given number of rows and columns finally let's add a reset method which resets the rotation and position the block class is now done let's write a subclass for the eye block first let's store the tile positions for the four rotation states and now we will fill out the required properties the id should be one the start offset should be -1 3 this will make the block spawn in the middle of the top row and for the tiles property we return the tiles array above that's all we have to do the functionality is in the base class i won't fill out the data for all seven blocks in this video but let's take a look at the o block because it's quite unique this block is unique because it occupies the same positions in every rotation state we could copy and paste the same positions four times but that is unnecessary the code will work just fine if we only provide one rotation state you will write the remaining subclasses yourself i will show you all the data you need so just pause the video and fill out the data as you can see on the right i have now finished the subclasses for all the blocks now we will add a block queue class which is responsible for picking the next block in the game it will contain a block array with an instance of the 7 block classes which we will recycle we also need a random object and finally a property for the next block in the queue when we write the ui we will preview this block so the player knows what's coming you could also store an array here containing the next few blocks and preview all of them let's write a method which returns a random block in the constructor we initialize the next block with a random block the last method we need returns the next block and updates the property since we don't want to return the same block twice in a row we keep picking until we get a new one that's it for the block queue class before we start writing the ui let's create a gamestate class which will handle the interactions between the parts we have written so far first we add a property with a backing field for the current block when we update the current block the reset method is called to set the correct start position and rotation next let's add properties for the game grid the block queue and a game over boolean in the constructor we initialize the game grid with 22 rows and 10 columns we also initialize the block queue and use it to get a random block for the current block property now we will write an important method which checks if the current block is in a legal position or not the method loops over the tile positions of the current block and if any of them are outside the grid or overlapping another tile then we return false otherwise if we get through the entire loop we return true you will see why this method is important in a second next we write a method to rotate the current block clockwise but only if it's possible to do so from where it is the strategy we use is simply rotating the block and if it ends up in an illegal position then we rotate it back let's write a method to rotate counterclockwise which works in the same way we also need methods for moving the current block left and right our strategy will be the same as above we try to move it and if it moves to an illegal position then we move it back of course the block can also be moved down but we need two other methods before we add that the first one will check if the game is over if either of the hidden rows at the top are not empty then the game is lost the next method will be called when the current block cannot be moved down first it loops over the tile positions of the current block and sets those positions in the game grid equal to the block's id then we clear any potentially full rows and check if the game is over if it is we set our gameover property to true if not we update the current block now we can write a move down method it works just like the other move methods except that we call the placeblock method in case the block cannot be moved down at this point we have written a lot of code without seeing anything interesting on the screen so let's import some assets and get started on the ui we need to create an assets folder here are the assets i'm going to use you can download them by clicking the link in the description below there's an image for every tile and block in the game including an empty towel and an empty block additionally there is an icon and a gradient which we will use as the background you can of course also create your own assets if you want to either way select them and drag and drop them into the assets folder in the project select them all right click go to properties and make sure the build action is set to resource now we are ready to work on the ui here we are in the mainwindow.saml file let's change the title to tetris and i will set the default height to 600 i will set the minimum width and height to 600 the background is going to be dark so let's use a white foreground i will set the font to segoe ui light and the font size to 28 we also need a key down event handler so we can detect when the player presses a key let's move down to the grid for the layout we need two rows the height of the top row is set to auto and the other row will use the remaining height then we need three columns [Music] for the middle column we set the width to auto and the remaining width is shared between the other two columns let's use the gradient in the assets folder as the background the game grid will be drawn on a canvas control in row 1 and column 1. [Music] we make the background a dark gray color and set the width to 250 and height to 500 that's 25 pixels per visible cell in the game grid [Music] if we run the program it looks fine but there's a problem the canvas doesn't scale with the window size to fix that we wrap the canvas in a view box we just have to specify the grid row and column on the view box instead of the canvas and let's add a small margin to the bottom as well [Music] now the canvas scales with the size of the window while still preserving the correct aspect ratio on the canvas we will set clip to bounce to true and add a loaded event handler clip to bounce ensures that any children that extends outside the bounce of the canvas will not be shown and we need this for our hidden rows above the game grid we will display the score so let's add a text block in row 0 and column 1. in row 1 column 0 we will show what block is currently on hold we add a stack panel containing a text block with the text hold and an image control for the image of the health block [Music] [Music] on the right side of the canvas we will show the player what the next block will be again we use a stack pedal with a text block and an image [Music] the final thing we need is a simple game over menu it will show the player's final score and contain a button for restarting the game we use another grid with a semi-transparent background and make it span both rows and all three columns [Music] inside the grid we add a stack panel and inside that there will be a text block with the text game over [Music] below that we will show the player's final score and finally a green play again button with a click event handler initially this menu should be hidden so we have to set visibility to hidden on the grid that's it for the ui let's bring it to live i have navigated to this file which is known as the code behind in this video i will not use the mvvm pattern we will just keep it simple and control the ui from here first let's set up an array containing the tile images [Music] the order here is not random at entry 0 we have the empty tile the order of the remaining tiles matches the block ids for example the i block has id1 and should be cyan therefore we have the scientist at index 1. similarly the o block has id4 and it's yellow therefore we have the yellow tile at index 4. we need a similar array for the block images which will be used to show the health block and the next block again the order matches the ids next we will declare a two-dimensional array of image controls [Music] the idea is that there is one image control for every cell in the game grid we will set this up in a second and then of course we need a game state object [Music] let's create a method to set up the image controls correctly in the canvas the image controls array will have 22 rows and 10 columns just like the game grid next we create a variable for the width and height of each cell recall that we set the canvas width to 250 and the canvas height to 500 this gives us 25 pixels for each visible cell now we loop through every row and column in the game grid for each position we create a new image control with 25 pixels width and height and then we have to position this image control correctly recall that we count rows from top to bottom and columns from left to right so we set the distance from the top of the canvas to the top of the image equal to r minus 2 times cell size the -2 is to push the top hidden rows up so they are not inside the canvas similarly the distance from the left side of the canvas to the left side of the image should be c times cell size next we make the image a chart of the canvas and add it to our array which will be returned outside the loop just to be clear we now have a two-dimensional array with one image for every cell in the game grid the two top rows which were used for spawning are placed above the canvas so they are hidden alright in the constructor we can initialize the image controls array by calling this method now it's super easy to draw the game grid [Music] we loop through all positions for each position we get the start id [Music] and set the source of the image at this position using the id drawing the current block is also easy all we have to do is loop through the tile positions and update the image sources in the same way we just did let's add a draw method which draws both the grid and the current block we will call the draw method when the game canvas has loaded if we start the program we can see an empty grid the current block is also there but it's above the canvas so we cannot see it let's detect some key presses first of all if the game has ended then pressing a key should not do anything [Music] we will use the arrow keys for movement [Music] the up arrow will rotate the block clockwise and the set button will rotate counterclockwise [Music] we will also add a default case where we simply return and outside the switch we call our draw method the default case ensures that we only redraw if the player pressed the key that actually does something here is the game so far the block can be moved down and from side to side we can also rotate it both clockwise and counterclockwise if we move a block all the way down it is placed in the grid and we get a new one we can also clear rows but tetris would be pretty easy if the block didn't move down by itself so let's add a game loop method it has to be async because we want to wait without blocking the ui first we draw the game state then we add a loop which runs until the game is over in the body we wait for 500 milliseconds move the block down and redraw we start the game loop when the canvas has loaded now this is starting to look more like tetris the next thing we will do is handle game over up here in the game loop we know that the game is over when we exit the loop so we just have to make the game over menu visible when the game is lost we see the game over menu but this button doesn't work yet so let's fix that the player can click method is called when the player presses the play again button here we will create a fresh game state hide the game over menu and restart the game loop [Music] now we can restart the game the next thing i want to fix is the spawn position our blocks spawn in the two hidden rows but if they space in row 2 and 3 the top visible rows it would look better if they spawned there so let's head over to the game state class in the setup for the current block just below the reset call we will move the block down by two rows if nothing is in the way now the blocks spawn nicely another thing is when you lose the game you cannot really see which block killed you we can fix that by showing a few pixels of row number one which is currently hidden let's open up the ui and change the height of the canvas to 510 and then we go to the code behind and scroll up to the setup game canvas method when we position the images vertically we will add 10 pixels now we can see just a little bit of row number 1 which is nice when we lose the game let's preview the next block now we can always see which block will come next next we will let us go so let's add a property to the game state class in this project the score will just be the total number of rows cleared so we have to head down to the place block method recall that clearful rows returns the number of cleared rows so we can just increment the score by that amount let's go back to the code behind and set the score text in the draw method [Music] we also have to set the final score in the game over menu now we have a working score and if we lose the game we also see the final score here in most tetris games i have played there's also an option to hold a block let's go back to the game state class and add a property for the hilt block and i can hold boolean in the constructor we set can hold to true and then we create a hold block method if we cannot hold then we just return [Music] if there is no block on hold we set the health block equal to the current block and the current block equal to the next block if there is a block on hold we have to swap the current block and the hilt block in the end we set can hold to false so we cannot just spam hold the last thing we have to do is scroll down to the place block method and add can hold equals true under the line where we update the current block then we go back to the code behind and call hold block when the player presses c and then we create a method which shows the health block and we will call it from draw now we can hold a block and we can see which one is on hold on the left side the next thing we will add is a hard drop feature it enables the player to press one button which will move the block down not just by one row but by as many as possible we go back to the game state class and here we will write a little helper method it takes a position and returns the number of empty cells immediately below it with this method we can find out how many rows the current block can be moved down we invoke it for every tile in the current block and check the minimum [Music] and then we create a drop block method [Music] it moves the current block down as many rows as possible and then places it in the grid [Music] to the code behind we will call drop block when the spacebar is pressed that's great but it's even more useful if you can see where the block will land so let's add a so-called ghost block [Music] the cells where the block will land are found by adding the drop distance to the tile positions of the current block then we set the opacity of the corresponding image controls to 0.25 and update the source setting the opacity is a nice trick we just have to remember to reset it when we draw the grid and the current block draw ghost block will be called from the draw method and it has to be called before draw block now it's super easy to see where the block will land there is one final feature we will add to this game the current block should move down faster and faster as the player increases their score let's add a few constants to control the delay between moving the block down [Music] i use 1000 75 and 25 but feel free to play around with these values in the game loop we add a delay variable [Music] when the game starts the delay will be max delay for each point the player gets the delay is decreased by delay decrease but it can never go below min delay in the beginning the block moves down very slowly but after increasing the score the block moves much faster the game is done but let's add an icon open the solution explorer right click on the project click properties and go down to the icon and type in assets backslash icon.ico you have now programmed a tetris game in c-sharp with a lot of cool features i really hope you enjoyed this video and that you learned something please consider leaving a like and subscribing to my channel thank you so much for watching
Info
Channel: OttoBotCode
Views: 1,767
Rating: undefined out of 5
Keywords: tetris, game, c#, programming, development, wpf, how, to, how-to, write, code, in, no, engine, without, ottobotcode, tutorial, step-by-step, step, by, detailed, explained, understand, dotnet, visual, studio
Id: jcUctrLC-7M
Channel Id: undefined
Length: 39min 34sec (2374 seconds)
Published: Thu Nov 18 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.