Programming a Snake Game in C# - Full Guide

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello and welcome in this video we will program a snake game in c-sharp using wpf for the ui my game will look exactly like this but you can easily change the size of the grid or how fast the snake moves ouch let's open up visual studio and get started first we must create a new project for the game the project template we need is wpf application i will call the project snake and for the target framework i will use dotnet 6.0 before we begin open the solution explorer right click on your project go to properties and then go to build general depending on the version of the net you are using nullable may be enabled by default this feature is supposed to help you avoid null reference exceptions but i think it can be confusing so i will disable it if you enable it then everything will still work but you might see some warnings all right let's import the assets for this game to keep things organized we start by creating an assets folder here are the assets for the game you can download them from the link in the description of this video there is an image of the snake's body and its head we also have darker versions which are for when the snake dies and the game is over then there is the food of course and an empty square as well note that all of these images have a border so they form a grid when they are placed next to each other finally we have the font we will use and this s which will be the application icon to import them just drag and drop them into the assets folder inside visual studio select all of them right click go to properties and set build action to resource this will embed the assets inside the game's executable great we have now set up the project before we start coding let me explain how we will represent the game snake is played on a grid like this one of course during the game this grid won't be empty some positions will be occupied by the snake and one position will contain food my grid has 15 rows and 15 columns but you can choose a different size if you want to we use the convention that rows increase from top to bottom so the origin at row 0 and column 0 is the top left position the grid will be represented using a two-dimensional array empty positions are represented with value zero positions containing part of the snake are represented with value one note that this includes the snake's head there is no special value for that the position containing food is represented with value 2. our code will be easier to understand if we have names for these values so let's begin by creating an enum for them back in visual studio i will add a new class called grid value we don't need any of the imports at the top and i will change it to a public enum a position in the grid can be empty it can contain part of the snake or it can contain food i will also add one more enum value here called outside this value won't be stored in the grid array but it will be convenient in the code when the snake tries to move outside the grid the next class we need represents a direction in the grid we encode a direction as two integers a row offset and a column offset this will make sense in a minute first we add a private constructor which takes the row and column offsets as parameters in the body we set our properties equal to these values because the constructor is private no other class can create an instance of the direction class and that is intentional we only need four directions left right up and down which we will add as static variables to move left from a given position in the grid we must leave the row unchanged and subtract one column therefore the row offset should be zero and the column offset should be minus one to move right don't change the row but add one column to move up subtract one row and don't change the column and finally to move down [Music] add one row and don't change the column let's add a method called opposite it will be convenient later as the name suggests it returns a direction's opposite the easiest way to do this is to return a new direction with opposite row and column offsets next i want to overwrite equals and get hash code so the direction class can be used as the key in the dictionary this can of course be done manually but it's easier to let visual studio do it for us just press ctrl dot and then choose generate equals and get hash code both row offset and column offset should be used so make sure they are checked and if we check generate operators then it will also overload the equality and inequality operators voila now we have meaningful implementations of equals and get hash code we also have overloads of the equality and inequality operators so we can use them to compare directions perfect we also need a class that represents a position in the grid [Music] this class stores a row and a column let's add a constructor which takes the row and column as parameters in the body we save these values in our properties next we need a method called translate [Music] it returns the position we get by moving one step in the given direction so we return a new position here [Music] the row should be this positions row plus the directions row offset and for the column we need this positions column plus the directions column offset just like for the direction class we want to overwrite equals and get hash code and of course we want the operators as well as you can see it generated something very similar to what it did in the direction class alright now we have all the little helper classes we need to implement the game logic let's create a class which stores the current state of the game i will call this class gamestate we need a few properties here the number of rows and columns in the grid and then the grid itself which is a two-dimensional rectangular array of grid values the snake also has a direction which dictates where it will move next and then we need a score property and finally a game over boolean it will also be convenient to keep a list containing the positions currently occupied by the snake we use a linked list because it allows us to add and delete from both ends of the list we use the convention that the first element is the head of the snake and the last element is the tail the final variable we need is a random object [Music] it will be used to figure out where the food should spawn next we need a constructor it takes the number of rows and columns in the grid as parameters first we store these numbers in our properties and initialize our 2d array with the correct size at this point every position in the array will contain gridvalue.empty because it's the first enum value when the game starts i want this next direction to be right let's create a method which adds the snake to the grid i want it to appear in the middle row in column one two three as you can see here first we create a variable for the middle row if you use an even number of rows for your game then this row will be slightly closer to the top next we loop over the columns from one to three inside the loop we set the grid entry at r comma c to gridvalue.snake we must also remember to add this position to the snake positions list now we can call ed snake from the constructor we also need to add some food let's start by creating a method which returns all empty grid positions here we loop through all rows and columns inside the loop we check if the grid at r comma c is empty if so we yield return that position now we can write the add food method first we create a list of empty positions it is theoretically possible to beat snake in which case there wouldn't be any empty positions if someone actually does that it would be a bomber if the game crashed so if there are no empty positions we simply return in the general case we pick an empty position at random [Music] and set the corresponding array entry to gridvalue.food another way to do this would be to repeatedly generate random positions and check if they are empty if you choose this approach then just make sure you have a way of detecting when there are none of them ok add food should be called in the constructor just like add snake great let's add a few snake-related helper methods the first one returns the position of this next hit we can easily get this position from the linked list we need a similar method for the tail position we can get this position from the other end of the list the next method returns all the snake positions as an eye innumerable note that these methods are public later we will use head position to add ice to the snake and you could also add a special tail using tail position the snake positions method will be handy when the snake dies and we turn it dark green next we will add two methods for modifying the snake the first one is called add hit it adds the given position to the front of the snake making it the new head so we must add this position to the front of our list and set the corresponding entry of the grid array to gridvalue.snick the next method is for removing the tail we start by getting the current tail position then we make that position empty in the grid and remove it from the linked list these two methods will be super useful when we have to move the snake great now we need to add some public methods for modifying the game state the first one changes the snake's direction for now it will simply set the direction property to the direction parameter this is a bit too simplistic but we will come back and change it once the problem becomes apparent we need a way to move the snake so let me show you how that works here is an example game state this next direction is up so it should move to this square here to do this we actually don't have to move all the snake positions we just have to remove the tail and add the new head position remember that there is no special grid value for the hit so if we look at the 2d array then the move looks like this we set the tail position to 0 and the new head position to 1. that is the general case but let's take a look at this state here this next direction is left but there is food at that position in this case we do not remove the tail we simply add the new head position so now the snake is a little bit longer here is another state this time the snake's direction is right but it cannot move that way because it would move outside the grid in this case we won't move anything but the game will be over the same thing is true for this state the direction of the snake is down but moving down would make the snake hit itself so nothing has to be moved and the game will end let's open up the editor again and implement these cases first we need a method which checks if a given position is outside the grid or not the next method takes a position as parameter and returns what the snake would hit if it moved there i call this method will hit in the general case it will just return what is stored in the grid at that position but there are two special cases we must handle if the new head position would be outside the grid then we return our spatial grid value called outside let me show you the other special case in this state the snake's direction is up so the new head position would be where the tail is currently at what should happen in this case in my opinion the tail and hit move simultaneously so the move should succeed like this to handle this we must check if the new head position is the same as the current tail position and if it is then the tail will move out of the way and that square will be empty maybe it makes more sense to you that the game should end in this case if so then just get rid of this check alright now we can write a public move method it will move the snake one step in the current direction first we get the new head position then we check what the head will hit using our will hit method if hit is grid value dot outside or gridvalue.snick then we set game over to true if the snake will move into an empty position we remove the current tail and add the new head and if it hits a position with food then we don't remove the tail but we do add the new hit we should also increment the score and spawn the food somewhere else we will return to the game state later to fix a few things but for now let's load the image assets i will just close the three classes that we are done with and add a new one this class will be called images let's make it a public static class this class will basically be a container for all our image assets we can get rid of all the imports except for system we also need system.windows.media and system.windows.media.imaging for convenience let's add a private method called load image it loads the image with the given file name and returns it as an image source [Music] now we can add static variables for each of our image assets like this [Music] and similarly for the rest of the images [Music] if you are coding along with me just pause the video and add all of them next let's open up app.saml here we will define a few color resources and prepare the font for our game we start with the background color i'm using a dark purple color we also need a background color for the grid this color is a slightly brighter shade of purple i will also add the color of the grid lines this is the same color i have used for the image assets border next up is the text color it is almost white but not completely the final color we need is for an overlay which will be displayed on top of the grid before the game starts [Music] this color is black with around 50 opacity remember that we also have a font in the assets folder we will also add that as a resource now we have everything we need to code the ui so let's open up mainwindow.sample here i will set the window title to snake and the initial height to 500. next we set background to our background color resource [Music] and foreground to text color [Music] and font family to our main font the controls we add to the window will inherit these values by default let's make the window open at the center of the screen [Music] and set the application icon for the layout we need two rows [Music] the top row has height set to auto and the bottom row will use the remaining height in the top row we will display the score for that we can use a text block we will call it score text and center align it horizontally and vertically as [Music] well initially the actual text should be score zero [Music] it's currently too small so let's set the font size to 22. we will also set the margin to 10. the bottom row will contain the grid where the game is actually played let me show you how that will work as we have seen the game state contains a grid array like this one in the ui we will add a grid of image controls with the same number of rows and columns the idea is that this grid of images will always reflect what is stored in the game state for an empty grid position we show the empty image asset for a snake position we show the body asset and for the food position we show the food asset later we will also add ice to the snake using the head acid and when the snake dies we will use the dead snake images as well we are not going to draw grid lines explicitly all we have to do is place the image assets next to each other and give this grid of images a background color one drawback of this approach is that the edge lines are only half as thick as the inner grid lines to fix that we will add an outer border with the grid line collar ok so first we add the outer border let's name it grid border and place it in row 1 of the main grid the border should have the same color as the grid lines [Music] and i will set the thickness to 1.5 inside the border we will add a uniform grid for the images its name should be game grid it's important that each cell in the grid is a square if the cells are rectangular then our game won't look right so let's set the width and height to 400. [Music] the size of the grid must match the number of rows and columns you use so if you have twice as many columns as rows then the width must be twice the height for the background we use our grid background color [Music] there is a small problem here i think you can see what it is if i change the thickness of the border to 10 the middle square is our uniform grid but the border doesn't fit tightly around it that's because the borders horizontal and vertical alignment are set to stretch by default so let's set both to center beautiful now we can set the border thickness back to 1.5 before anything interesting can happen we must add image controls to the game grid but we will not do it here instead we will add the images from mainwindow.saml.cs which is known as the code behind here we need two variables for the number of rows and columns i'm using 15 rows and 15 columns next i will add a 2d image array for our image controls this array will make it super easy to access the image for a given position in the grid now we can write a method called setup grid it will add the required image controls to the game grid and return them in a 2d array for easy access we start by creating a 2d array next we have to set the number of rows and columns on the game grid then we loop over all grid positions [Music] for each of them we create a new image initially i want its source to be the empty image asset we store this image in the 2d array [Music] and add it as a child of the game grid outside the loops we return the images array in the constructor we call this method and save the returned array and grid images let's try to start the program we have a grid nice but if you squint you may notice that there are small gaps between the images our current setup also doesn't do a good job at scaling them to fix both issues go to mainwindow.saml and locate the uniform grid here we will set snaps to device pixels to true and render options.bitmap scaling mode to high quality that's better but you might still see a small gap between the border and the grid to fix that go to the border and set render options dot edge mode to aliased great now everything looks how it should let's make this game playable we need event handlers for two window events the loaded event [Music] and the key down event back in the code behind we will add a dictionary which maps grid values to image sources [Music] if a grid position is empty we want to display the empty image asset if a position contains part of the snake we want to show the body image and if a position contains food then we show the food image [Music] note that i have omitted the type after the new keyword you can only do this in newer versions of c sharp so if you are having problems then just write the type again okay we also need a game state object which we initialize in the constructor now we can write a method called draw grid [Music] this method will look at the grid array in the game state and update the grid images to reflect it it loops through every grid position [Music] inside the loop we get the grid value at the current position and set the source for the corresponding image using our dictionary we will call draw grid from a more general draw method [Music] right now it's a bit redundant but it will do a few more things soon all right let's scroll up to window loaded here we will call draw and then start the program we have a visual representation of the grid now we just have to handle some keyboard inputs this window key down method is called when the user presses a key if the game is over then pressing a key shouldn't do anything so we simply return otherwise we check which key was pressed i will use the arrow keys but you can use other keys if you want to if the user presses the left arrow key we change this next direction to lift and similarly for the other arrow keys [Music] now we can change the snake's direction but we need to move it at regular intervals for that we add an async game loop method the loop will run until the game is over in the body we add a small delay i'm using 100 milliseconds but you can change this to make the game slower or faster after the delay we call the move method and then draw the new game state that's it for the game loop now we can make window loaded async and start the game loop check out the program now the snake moves and we can change its direction when we eat food the snake grows and if i die then the snake simply stops moving note that the skull did not change when the snake was eating but that's easy to fix in draw we set the score text to score followed by the actual score stalled in the game state now the score updates when the snake eats but there's another problem watch this the snake stopped moving which means the game is over but why well it's because the snake was moving down and then i pressed the up key let me explain here is an example game state this next direction is up but then the user presses the down key in our current implementation this will immediately change the direction to down this is bad because now the snake tries to move here that position already contains part of the snake so the game ends obviously that shouldn't happen but how do we fix it here is an idea we should never change to the opposite direction so in this example where the direction is up pressing the down key should do absolutely nothing this is a great idea but it's not quite enough consider what happens if i quickly press right and then down before the snake has a chance to move the direction will change to right and immediately after that it will change to down when it's time to move we have the same exact problem as before but what should happen in this case ideally it should move right first and then move down next time here is how we can accomplish that when the user presses a key we don't change direction immediately instead we save direction changes in a buffer you can think of them as predetermined moves when it's time for the snake to move we check the buffer if it's not empty we get the oldest entry set this next direction to that and then move it for the next move we do exactly the same we must still avoid changing from one direction to its opposite and fitting the buffer with redundant direction changes how that works will be clear when we implement this encode so let's do that now first we navigate to the game state class here we add a variable for the buffer [Music] a linked list is a good choice for the operations we need in change direction we won't change the direction immediately [Music] instead we will check if the change can be made and if so add it to the buffer in a minute we will replace the comment by an actual check to implement it we need a helper method called get last direction [Music] it returns the snake's last predetermined direction so if the buffer is empty it just returns the current direction otherwise it returns the last or most recently added direction change now we can implement can change direction [Music] it returns true if the given direction can be added to the buffer and false otherwise if there are already two direction changes stored in the buffer then we consider the buffer to be full and return false you could also use 3 as the buffer size but it is important to have a maximum size if not the user could spam keys and pre-determine the snake's movements way too far into the future if there is space in the buffer we get the last predetermined direction [Music] and return true if the new direction is not the same as the last direction [Music] and they are not opposites [Music] great let's replace the comment in change direction with the actual method call now we just have to make a small change in the move method we check if there is a direction change in the buffer [Music] if so we change this next direction accordingly and remove that direction change from the buffer that's it let's see if it works now i cannot lose the game by making the snake move in the opposite direction and if i click two keys very quickly then the snake performs both direction changes in sequence as intended it starts to look like a real game now but look what happens when i resize the window the game doesn't scale very well at the moment let's go to mainwindow.saml we are going to wrap the main grid in a view box so i'll select the entire grid cut it then add a view box and paste the grid inside if you look in the designers to the left you can see that there is no space underneath the actual game grid so on the board we're just going to add a small margin [Music] and it's probably a good idea to set a minimum width and height for the window now the game scales much better at the moment you must be ready to play as soon as the program starts we are also lacking a way of restarting the game so let's work on that now in mainwindow.saml we add a dark overlay with some text on top of the game grid this overlay will be inside the main grid here we add a border called overlay it must be in row one and have the semi-transparent overlay collar this border must have the same size as the grid border we could do this manually but there is a more convenient way we bind the width to the actual width of the grid border and same thing for the height we must also use the same margin and we also set edge mode to aliased this should ensure that the overlay takes up the exact same space as the grid border inside the overlay border we add a text block let's call it overlay text and set the text to press any key to start it looks a little small so let's set the font size to 26 we will also center align it both horizontally and vertically [Music] i will also set text wrapping to wrap [Music] this will make sure that the text fits inside the overlay even if you have more rows than columns perfect let me scroll to the top here we have this event handler for the loaded event we don't actually need it anymore so you can just get rid of it we do need is an event handler for the preview key down event i will explain how it works in a second let's navigate to the code behind we start by adding a boolean called game running it is false by default which is what we need next we turn window loaded into a regular async method called run game [Music] this method should also hide the overlay when the user presses a key for the first time we should call run game this could be done with some if statements inside window key down but i will do it inside window preview key down instead i'll start by moving it up here next to window key down and make it async how does it work when the user presses a key then window preview key down is called and after that window key down is also called but we can do something clever if the overlay is visible we set the event's handle property to true this will prevent window key down from being called so while the overlay is visible a key press will only cause window preview key down to be called if the game is not already running we set game running to true await run game [Music] and when that method is complete we set game running back to false let's start the program and see what happens the game opens with an empty grid and with the overlay visible if i press a key then the game starts it's much better but still a little abrupt so we will add a simple countdown it will be an async method with this signature [Music] in the body we loop from three down to one for each iteration we make the overlay text display the value of i [Music] followed by a small delay [Music] we can call this method from run game it should be done after the call to draw okay it's time to see if it works are you ready awesome the next thing we will do is provide a way to restart the game to do that we add a new async method called show game over [Music] it starts with a one second delay and then makes the overlay visible again [Music] in show countdown we change the overlay text so we must change it back to press any key to start back in the run game method we know that when the game loop ends then the game is over so we will call show game over after that [Music] and we also create a fresh game state for the next game let's see if it works when the snake hits the wall there is a one second delay and then our overlay becomes visible again now i can press a button and play one more time i think now is a good time to add some googly eyes to the snake we will do that using the asset called hit.png in this image the eyes are facing up so when the snake is moving in a different direction we must apply a rotation to make this easy we add a dictionary which maps directions to rotations [Music] [Music] for the up direction we don't need any rotation [Music] for the right direction we must rotate 90 degrees [Music] for down we need 180 degrees [Music] and for lift we need 270 degrees [Music] with that in place we can write a method called draw snakehead [Music] first we get the position of the snake's head [Music] and the grid image for that position next we set its source to the head image at this point we must apply a rotation to the image so the eyes are facing in the correct direction first we get the number of degrees from the dictionary [Music] and then we rotate the image by that amount [Music] that's it in the draw method we will call draw snakehead after draw grid [Music] let me show you how it looks yeah that's not quite it the eyes are facing in the right direction though what is wrong here the problem is that the images are rotating around the upper left corner and not the center we can fix that in setup grid when we create the images we just have to set render transform origin to the point 0.5 comma 0.5 [Music] this will make the images rotate around the center point i will also reset the render transform of the images in draw grid [Music] this ensures that the only rotated image is the one with the snake's head okay check this out now it is easier to see where the snake is moving but i want to make it a bit more clear when the snake dies so let's add a method called draw dead snake [Music] it starts by creating a list containing all the snake positions [Music] the order in this list is from head to tail next we add a loop [Music] it has the same number of iterations as there are positions inside the loop we grab the position at index i [Music] and decide the image source for that position if i is zero [Music] we need the image called deadhead [Music] otherwise we need the dead body image for the head we don't have to worry about rotation that's because the image will already be rotated correctly by the draw hit method next we set the source for the image at the current position [Music] we add a small delay now we just have to call it from show game over let's check it out that looks pretty nice in my opinion and it looks even better for a longer snake what about other grid sizes you can easily change the number of rows and columns as long as they are equal but if i choose more columns than rows for example then this happens that's because we have set the size of the game grid to 400 by 400 so the width to height ratio doesn't match the column to row ratio this can be adjusted manually of course but we can also add the line of code instead of grid i will leave the height at 400 but adjust the width so it matches the column to row ratio [Music] thank you for watching this video if it was helpful then consider leaving a like and subscribing to my channel see you in the next video
Info
Channel: OttoBotCode
Views: 112,810
Rating: undefined out of 5
Keywords: 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, snake, snake game, snake-game, ottobot, grid, tutorials, program, c-sharp, csharp, ui, user, user-interface, user interface, beginner, beginners
Id: uzAXxFBbVoE
Channel Id: undefined
Length: 82min 1sec (4921 seconds)
Published: Tue Aug 09 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.