Introduction
My effort is to try and explain some basic concepts of programming based on really simple words and examples.
Background
The main idea behind a snake game (as in most such games) is to "fool" the user into thinking that a series of frames
is in reality a moving object. Such a concept is fabricated in my implementation. The "motion" effect is based on a series of Timer
events,
each of which is "ticking" among specific intervals. The head of the snake is being drawn on a new position slightly misplaced to the direction of motion.
The end of its tail is erased from our canvas, thus creating an illusion of motion.
Using the code
The design of this simple game is ade on Microsoft Visual Studio 2010 using the Windows Presentation Foundation (aka WPF) coding scheme. A XAML file (the default declarative markup language used for creating GUIs for WPF applications using the .NET framework) is used to create the canvas, which our snake will use as a playground.
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Snake!" Height="422" Width="642" ResizeMode="NoResize">
<Grid Background="Black">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Canvas Name="paintCanvas" Background="White"
Grid.Column="1" HorizontalAlignment="Stretch" MaxWidth="642" MaxHeight="422"></Canvas>
</Grid>
<Window>
Nothing really surprising here. The syntax is the simplest we need to have in order for a simple WPF form to be created (using the design panel of Microsoft´s Visual Studio here will do the most coding part for you). On top of the main Grid we simply add a canvas. The size of the canvas has to be precise because we use the same values to limit the motion of our snake.
MaxWidth="642" MaxHeight="422"
Another important note here: Why did I choose to use the "canvas" element? This is certainly not the best implementation since there is significant lag at the creation of the graphics after some time, when the size of the snake increases. Still, it is enough for the purpose of this article, which is to show the power of the "UIElementCollection
" Children.
This collection describes the graphical elements that are placed on the canvas. So, I use each "Tick" of your DispatcherTimer
to draw a piece (in our case circle) of the body of the snake on the canvas. To achieve this, I use the method paintSnake
, giving an argument of type Point to it which describes the current position of the head of the snake.
private void paintSnake(Point currentposition) {
Ellipse newEllipse = new Ellipse();
newEllipse.Fill = snakeColor;
newEllipse.Width = headSize;
newEllipse.Height = headSize;
Canvas.SetTop(newEllipse, currentposition.Y);
Canvas.SetLeft(newEllipse, currentposition.X);
int count = paintCanvas.Children.Count;
paintCanvas.Children.Add(newEllipse);
snakePoints.Add(currentposition);
if (count > length)
{
paintCanvas.Children.RemoveAt(count - length + 9);
snakePoints.RemoveAt(count - length);
}
}
Draw your attention on the
paintCanvas.Children.Add(newEllipse);
command. This command is the way to actually draw the piece of circle on the canvas. Moreover, as described at the comment of the code,
if (count > length)
{
paintCanvas.Children.RemoveAt(count - length + 10);
snakePoints.RemoveAt(count - length);
}
I count the elements of the UIElement
Collection and if they exceed in size those already painted on the canvas (minus 10 pieces which
are the "red" pieces of food) I erase the ending piece of the snake body, the tail. Combined with the DispatchTimer
effect,
we create the illusion of the motion on the canvas.
The game is initialized as follows:
public Window1()
{
InitializeComponent();
DispatcherTimer timer = new DispatcherTimer();
timer.Tick += new EventHandler(timer_Tick);
timer.Interval = MODERATE;
timer.Start();
this.KeyDown += new KeyEventHandler(OnButtonKeyDown);
paintSnake(startingPoint);
currentPosition = startingPoint;
for (int n = 0; n < 10; n++)
{
paintBonus(n);
}
}
I start with the initialization of the Grid
and the Canvas
objects. I create a DispatcherTimer
object and set its ticking
intervals after I assign an EventHandler to it. Finally I start the timer
Object.
A KeyEventHandler
will take care of the player´s keystrokes as he tries to move the snake around. The paintBonus()
method uses
a random generator in a loop to draw the first ten random food objects on the canvas.
private void paintBonus(int index)
{
Point bonusPoint = new Point(rnd.Next(5, 620), rnd.Next(5, 380));
Ellipse newEllipse = new Ellipse();
newEllipse.Fill = Brushes.Red;
newEllipse.Width = headSize;
newEllipse.Height = headSize;
Canvas.SetTop(newEllipse, bonusPoint.Y);
Canvas.SetLeft(newEllipse, bonusPoint.X);
paintCanvas.Children.Insert(index, newEllipse);
bonusPoints.Insert(index, bonusPoint);
}
It works in the same spirit as the paintSnake()
method above. At this point though we need a new list of Point
objects which we will check later whether they are eaten or not. This list is obviously called bonusPoints
So elements are drawn, the head of the snake also, we are ready to play! But for the actual game to start, we need to handle the gameplay. We have two events, one is the pressing of the controlling keys and the other is the ticking of the timer.
private void OnButtonKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Down:
if (previousDirection != (int)MOVINGDIRECTION.UPWARDS)
direction = (int)MOVINGDIRECTION.DOWNWARDS;
break;
case Key.Up:
if (previousDirection != (int)MOVINGDIRECTION.DOWNWARDS)
direction = (int)MOVINGDIRECTION.UPWARDS;
break;
case Key.Left:
if (previousDirection != (int)MOVINGDIRECTION.TORIGHT)
direction = (int)MOVINGDIRECTION.TOLEFT;
break;
case Key.Right:
if (previousDirection != (int)MOVINGDIRECTION.TOLEFT)
direction = (int)MOVINGDIRECTION.TORIGHT;
break;
}
previousDirection = direction;
}
The content here is pretty self explanatory. We handle the pressing of the arrow button keys, checking first if the current direction of movement
is not exactly the opposite to the new one. We don´t want our snake to hit its own body, do we?
The rules we have to follow are :
- Don´t crash on the wall.
- Don´t hit your own body.
- Eat the food Objects.
Finally, we need to setup the action on every tick of our timer. Here, we check if the snake is moving properly, according to the game rules (did I mention that the canvas
and Grid
must be non-resizable?).
private void timer_Tick(object sender, EventArgs e)
{
switch (direction)
{
case (int)MOVINGDIRECTION.DOWNWARDS:
currentPosition.Y += 1;
paintSnake(currentPosition);
break;
case (int)MOVINGDIRECTION.UPWARDS:
currentPosition.Y -= 1;
paintSnake(currentPosition);
break;
case (int)MOVINGDIRECTION.TOLEFT:
currentPosition.X -= 1;
paintSnake(currentPosition);
break;
case (int)MOVINGDIRECTION.TORIGHT:
currentPosition.X += 1;
paintSnake(currentPosition);
break;
}
if ((currentPosition.X < 5) || (currentPosition.X > 620) ||
(currentPosition.Y < 5) || (currentPosition.Y > 380))
GameOver();
int n = 0;
foreach (Point point in bonusPoints)
{
if ((Math.Abs(point.X - currentPosition.X) < headSize) &&
(Math.Abs(point.Y - currentPosition.Y) < headSize))
{
length += 10;
score += 10;
bonusPoints.RemoveAt(n);
paintCanvas.Children.RemoveAt(n);
paintBonus(n);
break;
}
n++;
}
for (int q = 0; q < (snakePoints.Count - headSize*2); q++)
{
Point point = new Point(snakePoints[q].X, snakePoints[q].Y);
if ((Math.Abs(point.X - currentPosition.X) < (headSize)) &&
(Math.Abs(point.Y - currentPosition.Y) < (headSize)) )
{
GameOver();
break;
}
}
}
First thing´s first: we need to draw the snake. Yes, the main use of the timer Object as mentioned above is the drawing of the constantly moving graphics.
Thus, we check the direction of motion and we draw a piece of the body of the snake towards this direction. On every tick.
We check with a simple if clause whether the snake´s head is within the predefined boundaries. If not, we call the GameOver()
method to display the score and finish the game.
We test if a food object was consumed. This is tested by measuring the difference of the distance of the snake´s head from each one of the food Objects on the canvas.
If the difference is smaller that the head size of the snake then the food object is considered as "consumed". In this case, we erase it from the bonus collection
list and from the canvas. Then we create a new one.
Finally we need to check if the snake´s head has hit its own body. So we measure the difference of the distances X and Y of the head of the snake from each one
of the remaining points of the body. (The points of the "neck", that is those points immediately after the "head" circle are excluded so as to avoid
the "commit-suicide" effects).
Points of Interest
To summarize the content of this article, one can find information in here which describe the fundamentals of creating visuals. This is by no means a general method
to create games, nor is the canvas element of WPF the absolute tool for the trick. Still, it provides a good teaching example. Feel free to post your comments.
History
No history yet. Hopefully I can manage to post an updated version for windows phone platform soon enough.