Click here to Skip to main content
Click here to Skip to main content

A Simple Snake Game in WPF

, 26 Mar 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
How to program a simple Canvas Snake Game in WPF

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)  {

    /* This method is used to paint a frame of the snake´s body
     * each time it is called. */

    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);

    // Restrict the tail of the snake
    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, 

// Restrict the tail of the snake
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);

    /* Here user can change the speed of the snake.
     * Possible speeds are FAST, MODERATE, SLOW and DAMNSLOW */
    timer.Interval = MODERATE;
    timer.Start();

    this.KeyDown += new KeyEventHandler(OnButtonKeyDown);
    paintSnake(startingPoint);
    currentPosition = startingPoint;

    // Instantiate Food Objects
    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.  

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)
{
    // Expand the body of the snake to the direction of movement

    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;
    }

    // Restrict to boundaries of the Canvas
    if ((currentPosition.X < 5) || (currentPosition.X > 620) ||
        (currentPosition.Y < 5) || (currentPosition.Y > 380))
        GameOver();

    // Hitting a bonus Point causes the lengthen-Snake Effect
    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;

            // In the case of food consumption, erase the food object
            // from the list of bonuses as well as from the canvas
            bonusPoints.RemoveAt(n);
            paintCanvas.Children.RemoveAt(n);
            paintBonus(n);
            break;
        }
        n++;
    }

    // Restrict hits to body of Snake

    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. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Achilleas Portarinos
Software Developer CST AG
Germany Germany
No Biography provided
Follow on   Google+

Comments and Discussions

 
Questionsnake and ladder game project PinmemberMember 1093902010-Jul-14 18:00 
QuestionMVVM? Pinmemberchrisba11-Mar-14 10:19 
GeneralMy vote of 5 PinmemberTed Milne2-Dec-12 21:40 
SuggestionSome issues PinmemberFlorian Rappl25-Mar-12 12:08 
GeneralRe: Some issues PinmemberAchilleas Portarinos25-Mar-12 12:23 
AnswerRe: Some issues PinprotectorAspDotNetDev25-Mar-12 14:45 
GeneralRe: Some issues PinmemberFlorian Rappl25-Mar-12 21:54 
SuggestionRe: Some issues PinmvpJani Giannoudis26-Mar-12 3:15 
GeneralRe: Some issues PinmemberAchilleas Portarinos26-Mar-12 3:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141022.1 | Last Updated 26 Mar 2012
Article Copyright 2012 by Achilleas Portarinos
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid