Among my early articles were two that showed how to write a simple ASCII art Snake game for the command line in c# (see part 1 and part 2). Recently, someone called Mizukage Yagura requested that I continue this series, specifically to address (a) the Snake moving continuously in a direction until a different arrow key is pressed, and (b) collisions between the Snake and walls or itself. We will address these two points by continuing where we left off in part 2.
Continuous movement
We had already seen in "C# Threading: Bouncing Ball" how we could use a Thread to allow a ball to move continuously while still allowing the command line to wait for user input. This is no different for the Snake. You will first need to add the following statement near the top of your source code:
using
System.Threading;
In order to allow the Snake to move in a direction, we need to store that direction. One way to do this is to use an integer, and for instance let 1 mean left, 2 mean right, 3 mean up, and 4 mean down. An even neater way to do this is to declare an enumeration:
enum
Direction
{ Left, Right, Up, Down };
The values in this enumeration work pretty much like integers, but they are much more readable when you're working with them, as you'll see shortly.
Within the Program class, we can now add a Direction variable, which is where we'll store the current direction in which the Snake is moving:
static
Direction
direction;
Just before our game loop (the while (true) bit), we can now set an initial direction (right in this case), and create a thread that will handle the continuous movement. In that thread we are passing a Move() method, which we haven't created yet; but we'll get there soon enough.
direction
= Direction.Right;
Thread
thread = new
Thread(Move);
thread.IsBackground
= true;
thread.Start();
We are now going to make our Snake game work pretty much like "C# Threading: Bouncing Ball": all movement will be handled on our background thread, while keyboard input will be handled in the loop in the main thread. Thus, we can simplify the game loop as follows:
while
(true)
{
//
handle input
ConsoleKeyInfo
keyInfo = Console.ReadKey(true);
switch(keyInfo.Key)
{
case
ConsoleKey.Escape:
return;
case
ConsoleKey.UpArrow:
direction
= Direction.Up;
break;
case
ConsoleKey.DownArrow:
direction
= Direction.Down;
break;
case
ConsoleKey.LeftArrow:
direction
= Direction.Left;
break;
case
ConsoleKey.RightArrow:
direction
= Direction.Right;
break;
}
}
We can now create our Move() method. This mostly contains the old code that took care of moving the Snake, but we put it in its own game loop, add a delay at the end to wait for a short period of time before making the next move, and actually compute the next head location based on the current direction, which is this bit:
var
next = snake[0];
switch(direction)
{
case
Direction.Left:
if
(next.X > 0)
next.X--;
break;
case
Direction.Right:
if
(next.X < 79)
next.X++;
break;
case
Direction.Up:
if
(next.Y > 0)
next.Y--;
break;
case
Direction.Down:
if
(next.Y < 24)
next.Y++;
break;
}
To cut a long story short, once you've arranged things a little bit, your code should look something like this:
using
System;
using
System.Collections.Generic;
using
System.Threading;
namespace
CsSnake
{
struct
Location
{
public
int
X;
public
int
Y;
public
Location(int
x, int
y)
{
this.X
= x;
this.Y
= y;
}
};
enum
Direction
{ Left, Right, Up, Down };
class
Program
{
static
Direction
direction;
static
List<Location>
snake = new
List<Location>();
static
Location
star = new
Location(60,
20);
public
static
void
Main(string[]
args)
{
Console.OutputEncoding
= System.Text.Encoding.GetEncoding(1252);
Console.Title
= "Snake
Ranch";
Location
head = new
Location(40,
12);
snake.Add(head);
direction
= Direction.Right;
Thread
thread = new
Thread(Move);
thread.IsBackground
= true;
thread.Start();
while
(true)
{
//
handle input
ConsoleKeyInfo
keyInfo = Console.ReadKey(true);
switch(keyInfo.Key)
{
case
ConsoleKey.Escape:
return;
case
ConsoleKey.UpArrow:
direction
= Direction.Up;
break;
case
ConsoleKey.DownArrow:
direction
= Direction.Down;
break;
case
ConsoleKey.LeftArrow:
direction
= Direction.Left;
break;
case
ConsoleKey.RightArrow:
direction
= Direction.Right;
break;
}
}
}
public
static
void
Move()
{
while
(true)
{
var
next = snake[0];
switch(direction)
{
case
Direction.Left:
if
(next.X > 0)
next.X--;
break;
case
Direction.Right:
if
(next.X < 79)
next.X++;
break;
case
Direction.Up:
if
(next.Y > 0)
next.Y--;
break;
case
Direction.Down:
if
(next.Y < 24)
next.Y++;
break;
}
Console.Clear();
//
show snake
foreach
(Location
location in
snake)
{
Console.SetCursorPosition(location.X,
location.Y);
Console.ForegroundColor
= ConsoleColor.White;
Console.Write((char)
178);
Console.ResetColor();
}
//
show star
Console.SetCursorPosition(star.X,
star.Y);
Console.ForegroundColor
= ConsoleColor.Yellow;
Console.Write('*');
Console.ResetColor();
snake.Insert(0,
next);
if
(next.X == star.X && next.Y == star.Y)
{
Random
random = new
Random();
star.X
= random.Next(0, 80);
star.Y
= random.Next(0, 25);
}
else
snake.RemoveAt(snake.Count
- 1);
Thread.Sleep(300);
}
}
}
}
Handling collisions
Now, we want something to happen when the Snake bumps into a wall or itself.Since we are storing the location of each part of the Snake in a list, it is easy to just check whether the new head has bumped into any of those locations. First, let's change the while (true) loop in the Move() method so that it can exit when we change a flag:
bool
quit = false;
while
(!quit)
At the end of the loop that actually shows the snake, we can now add the following collision-checking logic:
if
(next.X == location.X && next.Y == location.Y)
{
quit
= true;
break;
}
This will effectively make the game stop when your Snake hits itself:
Now, in order to collide with walls, we need to actually store those walls somewhere. Since the command line window is effectively an 80x25 grid, it makes sense to store them in a data structure that resembles a grid. A 2D array is a pretty good choice for this kind of thing. But in this case we can simplify things further by using an array of strings, which effectively works out to be a 2D array of characters. We can thus declare our grid within the Program class:
static string[] grid = new string[]
{
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"X X",
"X X X X",
"X X X X",
"X X XXXXXXXXXXXX XXXXXXXXXXXX X X",
"X X X X",
"X X X X",
"X X X X",
"X X X X",
"X X XXXXXXXXXXXXXXXX X X",
"X X X X X X",
"X X X X X",
"X X XXX X X",
"X X XXX X X",
"X X X X X",
"X X X X X X",
"X X XXXXXXXXXXXXXXXX X X",
"X X X X",
"X X X X",
"X X X X",
"X X XXXXXXXXXXXX XXXXXXXXXXXX X X",
"X X X X",
"X X X X",
"X X",
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
Now immediately after Console.Clear(), we can draw our grid, which is just a matter of writing the strings in the array:
Console.Clear();
for
(int
i = 0; i < grid.Length; i++)
{
Console.Write(grid[i]);
}
Note that since the walls in the grid never change, this is pretty wasteful since we can just draw it once at the beginning and then only draw the Snake at every move, taking care to clear the square where the snake's tail was; however let's keep it simple.
We can now add an additional condition to our collision detection logic to cater for wall collisions:
if
(next.X == location.X && next.Y == location.Y
||
grid[next.X][next.Y] == 'X')
{
quit
= true;
break;
}
Finally, let's adjust the Snake's starting location so that it isn't at a wall location:
Location
head = new
Location(50,
12);
...and do the same for the star:
static
Location
star = new
Location(60,
15);
...and now we have something that handles collisions:
This is still far from complete, and one of the issues you'll notice from the above screenshot is that there is nothing to prevent the star from appearing over a wall. However, the basic collision detection mechanism is there and it's working - it's up to you to continue developing the game and refine it into a complete product. :)