Creating Snake Using Tkinter's Canvas Widget (Part 1)

Hello, and welcome to another post on GUI development with Tkinter. This week we're taking a break from learning about the technical details of Tkinter, and we're going to recreate the popular arcade game Snake using Tkinter's very powerful Canvas widget.

This post is going to be a bit more advanced, so if you've never used Tkinter before, I'd recommend you check out our earlier posts, just so you understand a little more about how Tkinter works.

You can find the finished code for this little project in the following link, but I'd really recommend coding along with me, as it'll help make the concepts stick: https://github.com/tecladocode/tkinter-snake/blob/master/app.py

You'll also find a small number of assets that we use in the app, so make sure you grab them!

If you'd prefer to watch a video, Jose has created an awesome walk through for you on YouTube!

Re-creating the Snake game with Python!
Re-creating the Snake game with Python!
39:39

Setting up the main window

First we're going to write a lot of our boilerplate Tkinter code, importing the tkinter module and setting up the main application window.

import tkinter as tk

root = tk.Tk()
root.mainloop()

This is all stuff we've seen before in earlier posts.

We're also going to add a few new pieces of configuration now that we're working on a serious application. We're going to add a title for the application window, and we're going to prevent the window from being resized. Our snake game is going to be played on a fixed dimension board, so allowing the window to be resized doesn't really make a lot of sense.

We're also going to add line which performs pixel scaling, which will make our text and images appear a lot sharper on higher dpi displays.

import tkinter as tk

root = tk.Tk()
root.title("Snake")
root.resizable(False, False)
root.tk.call("tk", "scaling", 4.0)

root.mainloop()

root.title is pretty straightforward, but the other two lines might warrant some explanation.

root.resizable is what we're using to prevent the window from being resized. It takes two different arguments, because we can prevent the window being resized along only the x or y axis if we want. Here we've disabled resizing in both dimensions by passing in False for both values.

The root.tk.call("tk", "scaling", 4.0) line tells Tkinter to use 4x pixel scaling. We could have set a different value, such as 2.0 here: I just opted for a higher level of scaling to account for very dense displays.

With that, we have our main window working and set up how we want it. Now let's take a look at adding our Canvas widget, which will make up the meat of the application.

Defining the board

For this application we're going to take an object oriented approach to writing our Tkinter code, which isn't something we've looked at before.

If you're not familiar with object oriented programming, we have a complete video course you can take, or a free 30-day text course which covers everything you'd need to know.

For our application, we're going to be defining a Snake class, which will be a child of the Canvas widget. This means we'll have access to the properties and methods available to Canvas objects, but we'll also be defining a great deal of our own.

To start, let's define our Snake class, and define the __init__ method, so we can pass in some configuration. We're also going to be making use of super to access the __init__ method of the Canvas widget, so that we can define some initial configuration of the Canvas itself.

import tkinter as tk


class Snake(tk.Canvas):
    def __init__(self):
        super().__init__(
            width=600, height=620, background="black", highlightthickness=0
        )


root = tk.Tk()
root.title("Snake")
root.resizable(False, False)
root.tk.call("tk", "scaling", 4.0)

root.mainloop()

Here we've created a 600 by 620 pixel Canvas widget with a black background, and no border highlight.

However, at the moment nothing will show up in our application window, because we haven't specified a geometry manager to use, and we also haven't instantiated our class.

We're going to be using the pack geometry manager in this app, since the layout is incredibly simple, and we're going to making the call to the pack method within the __init__ method of our Snake class. All of our configuration is going to live inside this __init__ method.

We're also going to instantiate an object of our Snake class and call it board.

import tkinter as tk


class Snake(tk.Canvas):
    def __init__(self):
        super().__init__(
            width=600, height=620, background="black", highlightthickness=0
        )

        self.pack()


root = tk.Tk()
root.title("Snake")
root.resizable(False, False)
root.tk.call("tk", "scaling", 4.0)

board = Snake()

root.mainloop()

With that, we now have a very exciting black box:

Let's start putting some things in it.

Adding items to our Canvas

We're going to be adding a few different types of element to our Canvas. We have some png images from our assets folder, some build in shapes, and some text.

Let's start with the images, since that's by far the most involved process. We're going to use two different methods to add the images to our Canvas. The first is responsible for actually loading the images into our application, and the second is going to be responsible for putting things on the Canvas.

Loading our image assets

So, our first step is loading our assets. We have two different images in our assets folder: a little green square for the snake segments, and a little purple square for the fruit. If you want to get more elaborate with your tokens, go for it!

We're going to define a load_assets method which will take care of both assets.

Inside load_assets we're going to define a try / except block so that we can handle any potential errors that arise from loading the files. We're not going to do anything fancy here. We're just going to print the error and destroy the root window object.

Inside the try block we're going to load and process the images. In order to work with our png images we need to make use of the PIL (Python Imaging Library), so we're going to need to import it. It doesn't come with Python, so you're going to have to install it using pip.

pip install pillow

If you've set up a virtual environment with pipenv like we have in our repository, you should install it using pipenv instead.

We need both the Image class and the ImageTk class from PIL, so our import is going to look like this:

from PIL import Image, ImageTk

Once we've processed our images in the try block, we can call the load_assets method inside of __init__.

You can see what this looks like below:

import tkinter as tk
from PIL import Image, ImageTk


class Snake(tk.Canvas):
    def __init__(self):
        super().__init__(
            width=600, height=620, background="black", highlightthickness=0
        )

        self.load_assets()
        self.pack()

    def load_assets(self):
        try:
            self.snake_body_image = Image.open("./assets/snake.png")
            self.snake_body = ImageTk.PhotoImage(self.snake_body_image)

            self.food_image = Image.open("./assets/food.png")
            self.food = ImageTk.PhotoImage(self.food_image)
        except IOError as error:
            print(error)
            root.destroy()


root = tk.Tk()
root.title("Snake")
root.resizable(False, False)
root.tk.call("tk", "scaling", 4.0)

board = Snake()

root.mainloop()

When we want to add the snake body to the Canvas, we can now use self.snake_body as an image, and the same goes for any other assets we define in this way.

Adding our images to the Canvas

Now that we've loaded out assets, we can start adding things to the Canvas. We're going to be using another custom method for this called create_objects.

The Canvas widget actually comes with a method called create_image, which is what we're going to be using to add the image assets to the Canvas. This method takes an x and y position for the object, a processed image passed to the image parameter, and an optional tag which we're going to define for all of our Canvas items, as it makes it much easier to find them later in our code.

First up, let's add the snake's body. We're going to start the game with a snake that has three body segments, so we're going to have to add three separate objects to the canvas.

It seems to me that it would be useful to keep track of where all of the snake segments are, since we're later going to have to check for collisions, etc., so I think we should specify the starting coordinate for the snake in our __init__ method, and then we can update the coordinates every time the snake moves.

class Snake(tk.Canvas):
    def __init__(self):
        super().__init__(
            width=600, height=620, background="black", highlightthickness=0
        )

        self.snake_positions = [(100, 100), (80, 100), (60, 100)]

        self.load_assets()
        self.pack()

Our self.snake_positions property is bound to a list of tuples, where each tuple contains an x and a y coordinate.

We can now add a snake segment to each of these coordinates inside our create_objects method. Since we're repeating the same bit of code over and over, I'm going to use a for loop and some destructuring to add each segment:

class Snake(tk.Canvas):
    def __init__(self):
        super().__init__(
            width=600, height=620, background="black", highlightthickness=0
        )

        self.snake_positions = [(100, 100), (80, 100), (60, 100)]

        self.load_assets()
        self.create_objects()

        self.pack()

    def load_assets(self):
        try:
            self.snake_body_image = Image.open("./assets/snake.png")
            self.snake_body = ImageTk.PhotoImage(self.snake_body_image)

            self.food_image = Image.open("./assets/food.png")
            self.food = ImageTk.PhotoImage(self.food_image)
        except IOError as error:
            print(error)
            root.destroy()

    def create_objects(self):
        for x_position, y_position in self.snake_positions:
            self.create_image(
                x_position, y_position, image=self.snake_body, tag="snake"
            )

If we run our code, we now have some nice green squares on our canvas representing our snake!

Adding the food is very similar. First we're going to define a new property to keep track of where the food is, and then we're going to use create_image to add the self.food image to the Canvas.

For now we're just going to specify a static position for the starting food location, but later on we're going to define a method to randomly place the food on the Canvas making sure to avoid the snake.

The position of the food is once again a tuple containing an x and a y coordinate, so we can unpack this tuple inside the create_image method using * to provide an x and a y coordinate value in one go:

class Snake(tk.Canvas):
    def __init__(self):
        super().__init__(
            width=600, height=620, background="black", highlightthickness=0
        )

        self.snake_positions = [(100, 100), (80, 100), (60, 100)]
        self.food_position = (200, 200)

        self.load_assets()
        self.create_objects()

        self.pack()

    def load_assets(self):
        try:
            self.snake_body_image = Image.open("./assets/snake.png")
            self.snake_body = ImageTk.PhotoImage(self.snake_body_image)

            self.food_image = Image.open("./assets/food.png")
            self.food = ImageTk.PhotoImage(self.food_image)
        except IOError as error:
            print(error)
            root.destroy()

    def create_objects(self):
        for x_position, y_position in self.snake_positions:
            self.create_image(
                x_position, y_position, image=self.snake_body, tag="snake"
            )
        
        self.create_image(*self.food_position, image=self.food, tag="food")

We now have a nice purple square on the Canvas representing the food:

And with that, all of our images are added!

Adding the other Canvas objects

We may have all the images added, but we're not done putting things on the Canvas. We still need to add some text which will display the current score, and we need a box which will define the playable area.

In order to add the score text to the Canvas we need to define another property where we'll keep track of the current score.

Adding text to the Canvas works very similarly to adding images, we just use the create_text method instead. Once again, our first two arguments are a set of x and y coordinates, but we're also going to provide values for text, tag, fill, and font.

text is just the actual characters we want to position, and tag is going to be used in a similar way to your images. It lets us quickly get a hold of the canvas widget should we need it for some reason,

fill is used to set the text colour, and here we've gone for simple white, written using hexadecimal: #fff. If you want a different colour, you can change this to whatever you want!

font will be used to set the font size in our case, but you can also specify a typeface as well if you like. To do this, you pass in a tuple of values with the typeface first.

Our updated create_objects method looks like this:

def create_objects(self):
    self.create_text(
        35, 12, text=f"Score: {self.score}", tag="score", fill="#fff", font=10
    )

    for x_position, y_position in self.snake_positions:
        self.create_image(
            x_position, y_position, image=self.snake_body, tag="snake"
        )
    
    self.create_image(*self.food_position, image=self.food, tag="food")

And we'll also add self.score = 0 to our __init__ method.

Finally, we're going to add a border to the playable area so that players know where the boundaries are. For this, we're going to use one of the shapes native to Tkinter's Canvas widget, which we create with another built-in method: create_rectangle.

When defining a rectangle in Tkinter Canvas, we actually need four values. The first two values are the x and y coordinates for the top left corner, and the second two values are the x and y coordinated for the bottom right corner. From these values, Tkinter can figure out the placement of the other corners.

Our coordinates are going to be a little weird looking, because I've added an offset of 3 pixels so that when the snake travels along one of the edges, it's not touching the border. It just looks nicer with a little gap. if you want to get rid of the gap, you can just round all of the numbers to the nearest 10.

In addition to specifying the coordinates for our rectangle, we're also going to define an outline colour by passing in a hexadecimal colour value to the outline parameter.

def create_objects(self):
    self.create_text(
        35, 12, text=f"Score: {self.score}", tag="score", fill="#fff", font=10
    )

    for x_position, y_position in self.snake_positions:
        self.create_image(
            x_position, y_position, image=self.snake_body, tag="snake"
        )
    
    self.create_image(*self.food_position, image=self.food, tag="food")
    self.create_rectangle(7, 27, 593, 613, outline="#525d69")

And with that, all of our items are on the Canvas:

Wrapping Up

With our Canvas all set up, I think now is a good place to stop. In the next post we'll take care of adding all the functionality, and by the end of it, we'll have a working snake application ready for you to enjoy!

I hope you're enjoying creating this app with me, and if you're interested in learning more about GUI development, we've just released a whole course on the topic! Check out the GUI Development with Python and Tkinter course!