Quick Navigation1. Understanding the Concept2. Setting Up3. Creating Necessary Classes4. Creating an Input Manager5. The Game Loop6. Rendering Graphics7. Creating Game Logic7. Creating Game LogicThis is the lengthy part of the tutorial. Here we will create all the necessary game logic for our Snake game. This includes :
- 4-way grid movement
- Food generation/Snake growth
- Collision and Gameover
- Score
Setting up the SnakeRemember that SnakePart class? Well we're going to start using it now. What we're going to do is create a List of SnakeParts. This will be the easiest way to create a dynamic snake. Go ahead and create the follow variable.
List<SnakePart> snake = new List<SnakePart>();
This will be our list of parts. Now before we move on, I think it'd be best to explain the level size. The size of my picture box is 320x240, I'm groing to break this up into tiles since we're making this grid-based. Each tile with be 16x16 pixels, which leaves 20 tiles across, and 15 tiles down. In case you choose to change any of this later, we're going to create some constants as well.
const int tile_width = 16;
const int tile_height = 16;
This way, in case you decided to go with a bigger picture box or you want larger tiles, you can change any of these values. We have a few more variables we need to create for the game.
bool gameover = false;
int score = 0;
int direction = 0; // Down = 0, Left = 1, Right = 2, Up = 3
SnakePart food_piece = new SnakePart();
Now we need to create a "StartGame" method. This method will reset all values and start the game. In this method, we'll also be creating the head of our snake. The following clears the list of snake parts, then creates the head. The head will appear in the center near the top of the screen. Starting in the down direction.
private void StartGame()
{
gameover = false;
score = 0;
direction = 0;
snake.Clear();
SnakePart head = new SnakePart();
head.X = 20;
head.Y = 5;
snake.Add(head);
GenerateFood();
}
Notice the GenerateFood() line. We'll be creating that in a minute. Let's go ahead and add in the rendering for our snake. For our rendering, we'll be using a "for i in 0...size" iteration. Using this method, we can have our snake's head a different color from the rest of the body. Hop down to our picture box's Paint method.
private void pbCanvas_Paint(object sender, PaintEventArgs e)
{
Graphics canvas = e.Graphics;
for (int i = 0; i < snake.Count; i++)
{
Brush snake_color = i == 0 ? Brushes.Red : Brushes.Black;
canvas.FillRectangle(snake_color, new Rectangle(snake[i].X * tile_width, snake[i].Y * tile_height, tile_width, tile_height));
}
}
This loops through every part, checks to see what "i" is and sets the color based on it. Then it draws a fill rectangle according to the current part's x and y with accordance to the tile size.
Generating FoodTime to generate some food! This method is extremely easy to create. What we're going to do here is make an instance of Random and have a number randomly generate to the canvas's bounds. Using our tile size constants and grabbing the size of the picture box, we'll be able to figure out our tile limit.
private void GenerateFood()
{
int max_tile_w = pbCanvas.Size.Width / tile_width;
int max_tile_h = pbCanvas.Size.Height / tile_height;
Random random = new Random();
food_piece = new SnakePart();
food_piece.X = random.Next(0, max_tile_w);
food_piece.Y = random.Next(0, max_tile_h);
}
With some simple math, we figured out how many tiles there are vertically and horizontally. With this info, we create a new instance of SnakePart and assign it a random x and y in relevance to our bounds. Hop back into our Paint method. We have to make it draw our food now. Add this to the bottom of the method.
canvas.FillRectangle(Brushes.Orange, new Rectangle(food_piece.X * tile_width, food_piece.Y *tile_height, tile_width, tile_height));
Feel free to change the Brush. Now all that's left is to Check Input, Update the Snake, Check for Collision, and Gameover. We're almost done.
Check InputThis part is pretty easy. Remember earlier when we tested out the Input Manager? Well we're basically going to be doing that. Except, we're going to add a couple of more "if" branches. These extra branches will prevent the snake from moving right when he's going left, and moving down when he's already going up, and vica versa.
Things to keep in mind: Point A) We're doing this so the player can't kill himself by accidentally pressing the opposite direction. Point B) We can't have this limitation when the size of our snake is only 1. How are we going to tackle this? Easy.
When moving right, we'll check to see if the size is less than 2 or if the head and the 2nd part are on the same X. If either of these conditions meet, we change directions.We apply these conditions (and slightly modify them) to all other directions. In our Update method, we need to check for "gameover" before we start updating. Then we add our Input logic into the else branch.
private void Update(object sender, EventArgs e)
{
if (gameover)
{
// Gameover Logic
}
else
{
// Input and Snake Logic
}
pbCanvas.Invalidate();
}
Now let's go ahead and create our Input logic. Keep this logic in mind.
Left and Right will check to make sure the head and second part's X match.
Up and Down will check to make sure the head and second part's Y match.
The above logic will be ignored it the snake's size is less than 2. private void Update(object sender, EventArgs e)
{
if (gameover)
{
// Gameover Logic
}
else
{
if (Input.Pressed(Keys.Right))
{
if (snake.Count < 2 || snake[0].X == snake[1].X)
direction = 2;
}
else if (Input.Pressed(Keys.Left))
{
if (snake.Count < 2 || snake[0].X == snake[1].X)
direction = 1;
}
else if (Input.Pressed(Keys.Up))
{
if (snake.Count < 2 || snake[0].Y == snake[1].Y)
direction = 3;
}
else if (Input.Pressed(Keys.Down))
{
if (snake.Count < 2 || snake[0].Y == snake[1].Y)
direction = 0;
}
UpdateSnake();
}
pbCanvas.Invalidate();
}
That's all we need for input (for now). Notice I added the line "UpdateSnake()", just to keep organized, we're going to update the snake in another method.
Moving the SnakeSince each part of the snake is it's own part, we have to loop through the list of snake parts and update each one. How are we going to update the snake exactly? We're going to be using this logic.
Loop through the snake starting from the back.
Set the current part's X and Y to the part in front of it.
Move the head in accordance to it's current direction.Basically, we'll start from the back of the snake. We'll grab the part that's in front of the current part we're on and copy it's coordinates. In our for loop, we'll need to check to see if "i" is equal to 0 or not. In order to start from the back of our snake, we need to set "i" to the size of our snake, minus one, check if it's greater than or equal to zero, then minus one each loop.
private void UpdateSnake()
{
for (int i = snake.Count - 1; i >= 0; i--)
{
if (i == 0)
{
// Head Logic
}
else
{
// Body Logic
}
}
}
Let's go ahead and do the body logic right now, this part is sweet and short.
snake[i].X = snake[i - 1].X;
snake[i].Y = snake[i - 1].Y;
Now let's go and do the head logic. In the head logic, we'll need to do a bit more work.
Update head's position.
Check for collision with the body.
Check for collision outside of the level.
Check for collision with the food object.[/li][/list]
For now, we're just going to update the snake's head. We'll worry about collision a bit later. We'll be using a switch statement in relation to our "direction" variable. This code goes in place of our head logic.
switch (direction)
{
case 0: // Down
snake[i].Y++;
break;
case 1: // Left
snake[i].X--;
break;
case 2: // Right
snake[i].X++;
break;
case 3: // Up
snake[i].Y--;
break;
}
Revisiting the Frame RateNow we're almost ready to test this out, however we have to change the frames-per-second. Earlier we set it up so it'd run 60 frames per second. With a game like snake, we don't need it that high and we certainly don't want it. THe snake would move roughly 60 times every second. Let's try something along the lines of like 4 times every second. In our constructor, just change the gameTimer.Interval.
gameTimer.Interval = 1000 / 4;
One more thing, add "StartGame();" right under gameTimer.Start(), now run your project. You should have perfectly working snake movement.
CollisionCollision is easy. All collision checking will be done inside the head logic. First we'll check to see if the head is outside the screen. Then we'll do another for loop with the body and check to see if the head collided with it's body, then we'll check collision with the food.
Let's start with checking the bounds. We'll use the same simple math we used to generate food to grab the number of vertical and horizontal tiles. Then we'll check to see if the head's X and Y is less than zero or exceeds the number of tiles. Then set the gameover flag to true.
int max_tile_w = pbCanvas.Size.Width / tile_width;
int max_tile_h = pbCanvas.Size.Height / tile_height;
if (snake[i].X < 0 || snake[i].X >= max_tile_w || snake[i].Y < 0 || snake[i].Y >= max_tile_h)
gameover = true;
Alright, time to update with the body. Collision with the body is simple. We create another for loop that starts from "1" and climbs up through the snake's body. For each index, we check to see if the head's coordinates match any of the others.
for (int j = 1; j < snake.Count; j++)
if (snake[i].X == snake[j].X && snake[i].Y == snake[j].Y)
gameover = true;
Now food collision and snake growth. All we do here is check the head's X and Y with the food's X and Y. If collision occurs, we create another instance of "SnakePart". It's coordinates will be set to the last part in the snake's coordinates. Along with adding a new part, we also have to generate a new piece of food and add 1 to our score.
if (snake[i].X == food_piece.X && snake[i].Y == food_piece.Y)
{
SnakePart part = new SnakePart();
part.X = snake[snake.Count - 1].X;
part.Y = snake[snake.Count - 1].Y;
snake.Add(part);
GenerateFood();
score++;
}
And that's it for collision. So what's left? Our DoGameover() method. Then starting a new game when the gameover occurs. Drawing the score in the top corner would be nice as well. Before we move on, feel free to try out what we have so far. You should have a completely working snake that grows as he eats food. Dying works properly as well. You'll notice the game freeze whenever you die.
GameoverAll we're doing here is trying the player's final score, text saying "Gameover" then some text saying "Press Enter to Start Over". Then of course we actually have to detect input. We have to add an "if else" branch in our Paint method as we forgot to do so earlier.
Graphics canvas = e.Graphics;
if (gameover)
{
}
else
{
for (int i = 0; i < snake.Count; i++)
{
Brush snake_color = i == 0 ? Brushes.Red : Brushes.Black;
canvas.FillRectangle(snake_color, new Rectangle(snake[i].X * tile_width, snake[i].Y * tile_height, tile_width, tile_height));
}
canvas.FillRectangle(Brushes.Orange, new Rectangle(food_piece.X * tile_width, food_piece.Y * tile_height, tile_width, tile_height));
}
In the gameover branch, we'll be drawing some text in the center of the screen. We're just going to be using the font attached to our form. We'll go ahead and create our messages as well.
Font font = this.Font;
string gameover_msg = "Gameover";
string score_msg = "Score: " + score.ToString();
string newgame_msg = "Press Enter to Start Over";
Now to center it in the screen horizontally, we need to measure the size of the string. The Graphics class comes with a nice MeasureString method. We'll also need to know the center of the screen so we take the width of our picture box and divide it by 2. We'll start off by measuring our gameover_msg and then drawing that.
SizeF msg_size = canvas.MeasureString(gameover_msg, font);
PointF msg_point = new PointF(center_width - msg_size.Width / 2, 16);
canvas.DrawString(gameover_msg, font, Brushes.White, msg_point);
Now we do the same for our score_msg and newgame_msg.
msg_size = canvas.MeasureString(score_msg, font);
msg_point = new PointF(center_width - msg_size.Width / 2, 32);
canvas.DrawString(score_msg, font, Brushes.White, msg_point);
msg_size = canvas.MeasureString(newgame_msg, font);
msg_point = new PointF(center_width - msg_size.Width / 2, 48);
canvas.DrawString(newgame_msg, font, Brushes.White, msg_point);
That's it for drawing our gameover screen. Feel free to test it out. While we're here in the paint method, let's go ahead and draw the player's score while the game is playing. In the "else" branch of our Paint method, add this to the bottom.
canvas.DrawString("Score: " + score.ToString(), this.Font, Brushes.White, new PointF(4, 4));
And now all that's left is to add our logic to start a new game. This part's really easy. Go into our Update method and all we need are these two lines placed in our Gameover logic.
if (Input.Pressed(Keys.Enter))
StartGame();
And bam! That's it! Our Snake game is completely done! Well, we're done with this tutorial anyways. That doesn't mean you have to be done. Here are a list of things you can do to improve on this.
- When generating food, add a check to make sure it doesn't spawn on the snake's body
- Make the snake move faster after every piece of food it collects (this is done by altering the gameTimer's interval)
- Implement a highscore system of some kind.
I hope you enjoyed the tutorial. If you have any questions feel freek to ask! Also, if you need/want it, the source can be downloaded from the first post.