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!
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!