Everybody knows and loves the famous game snake. We will re-create in this article with Kivy. While we are writing the program are we going to learn about the detection of widgets colliding as well as animating widgets.
Creating the Snakes Head
Our game will happen on a playfield which shall be 600 by 600 pixels big. We can set the window size of our app like this.
class SnakeApp(App):
def __init__(self, **kwargs):
super(SnakeApp, self).__init__(**kwargs)
Window.size = (600, 600)
Next is the playfield itself. We need to position and move our head in a grid. There is a GridLaoyut in Kivy but even though the name might sound tempting it does not work the way we need it. A better attempt is to use the FloatLayout which allows us to position our head wherever we want it. We only need to write a proper function for it.
Our playfield shall be cut into 20 x 20 squares. That is done in the __init__ function. Here we also add the head. Since the FloatLayout is completely free in positioning a widget are we going to do some math. The head widget knows its postiion on the grid and we simply calculate the position by multiplying that with the widgets size in x and y direction.
class Playfield(FloatLayout):
def __init__(self, **kwargs):
super(Playfield, self).__init__(**kwargs)
self.rows = Window.size[0] / 20
self.cols = Window.size[1] / 20
self.widget_size = Window.size[0] / 20
self.head = Head()
self.add_my_widget(self.head)
def add_my_widget(self, widget):
'''add a new widget on the grid'''
widget.pos = (widget.grid_pos[0] * self.widget_size, widget.grid_pos[1] * self.widget_size)
widget.size_hint = (None, None)
widget.size = (self.widget_size, self.widget_size)
widget.id = 'head'
self.add_widget(widget)
The head needs to be defined as well. As a default the head will be positioned on the grid position 10, 10 which translates to the middle of the window.
class Head(Widget):
def __init__(self, grid_pos= None, **kwargs):
super(Head, self).__init__(**kwargs)
#position head in middle of the screen
if grid_pos is None:
grid_pos = [10, 10]
self.grid_pos = grid_pos
Finally our kv file, which is very short in this case defines that our head is a simple red rectangle and the root widget Playfield has no children in the beginning.
Playfield:
<Head>:
canvas.before:
Color:
rgba: 1,0,0,1
Rectangle:
size: self.size
pos: self.pos
Running these lines should give you a rectangle in the middle of your window. But that would of course be quite boring without any motion.
Therefore are we going to slightly modify the code. First we use the Clock module of Kivy to call an update function in regular intervals. The function is initiated during the __init__ method of the Playfield.
class Playfield(FloatLayout):
def __init__(self, **kwargs):
super(Playfield, self).__init__(**kwargs)
self.rows = Window.size[0] / 20
self.cols = Window.size[1] / 20
self.widget_size = Window.size[0] / 20
self.head = Head()
self.add_my_widget(self.head)
Clock.schedule_interval(lambda dt: self.move_head(self.head), 0.1)
def add_my_widget(self, widget):
'''add a new widget on the grid'''
widget.pos = (widget.grid_pos[0] * self.widget_size, widget.grid_pos[1] * self.widget_size)
widget.size_hint = (None, None)
widget.size = (self.widget_size, self.widget_size)
widget.id = 'head'
self.add_widget(widget)
def move_head(self, widget):
'''redraw the widget according to last position plus direction of motion'''
widget.grid_pos[0] = widget.grid_pos[0] + widget.direction[0]
widget.grid_pos[1] = widget.grid_pos[1] + widget.direction[1]
widget.pos = (widget.grid_pos[0] * self.widget_size, widget.grid_pos[1] * self.widget_size)
Finally, we need to listen for inputs from the keyboard, we will be using the arrow keys here, and set the direction into which the head shall go during the next update of the screen. To store the direction the Vector module of Kivy is quite helpful although a simple list could do the same.
class Head(Widget):
def __init__(self, grid_pos= None, direction = None, **kwargs):
super(Head, self).__init__(**kwargs)
#position head in middle of the screen
if grid_pos is None:
grid_pos = [10, 10]
self.grid_pos = grid_pos
#no direction by default. Widget does not move until arrow is pressed
if direction is None:
direction = Vector(0,0)
self.direction = direction
self._keyboard = Window.request_keyboard(self._keyboard_closed, self, 'text')
self._keyboard.bind(on_key_down=self._on_keyboard_down)
def _keyboard_closed(self):
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
if keycode[1] == 'up':
self.direction = (0, 1)
if keycode[1] == 'down':
self.direction = (0, -1)
if keycode[1] == 'left':
self.direction = (-1, 0)
if keycode[1] == 'right':
self.direction = (1, 0)
return True
That looks much better now. We have a moving head.
Adding the snakes body
Now that we move our snake head along the playfield is it time to add a body to make it more and more difficult for the player to navigate on the playfield. To begin with we add a green rectangle as Body in our kv file. Feel free to change the colors to whatever you like. E.g. blue and yellow if your players might be colour blind.
Playfield:
<Head>:
canvas.before:
Color:
rgba: 1,0,0,1
Rectangle:
size: self.size
pos: self.pos
<Body>:
canvas.before:
Color:
rgba: 0,1,0,1
Rectangle:
size: self.size
pos: self.pos
The body class in the python file holds the same grid_pos variable as the head.
class Body(Widget):
def __init__(self, grid_pos = None,**kwargs):
super(Body, self).__init__(**kwargs)
if grid_pos is None:
grid_pos = [0,0]
self.grid_pos = grid_pos
Now that we have a rule defined for how the Body looks like we have to add it. For starters we are going to create a body part in every cycle of the scheduled clock function move_head. The head is going to be sized and positioned just like the head using the add_my_widget function. The updated Playfield class looks like this.
class Playfield(FloatLayout):
def __init__(self, **kwargs):
super(Playfield, self).__init__(**kwargs)
self.rows = Window.size[0] / 20
self.cols = Window.size[1] / 20
self.widget_size = Window.size[0] / 20
self.head = Head()
self.add_my_widget(self.head)
Clock.schedule_interval(lambda dt: self.move_head(self.head), 0.1)
def add_my_widget(self, widget):
'''add a new widget on the grid'''
widget.pos = (widget.grid_pos[0] * self.widget_size, widget.grid_pos[1] * self.widget_size)
widget.size_hint = (None, None)
widget.size = (self.widget_size, self.widget_size)
self.add_widget(widget)
def move_head(self, widget):
'''redraw the widget according to last position plus direction of motion'''
widget.grid_pos[0] = widget.grid_pos[0] + widget.direction[0]
widget.grid_pos[1] = widget.grid_pos[1] + widget.direction[1]
widget.pos = (widget.grid_pos[0] * self.widget_size, widget.grid_pos[1] * self.widget_size)
#add new part to body
grid_pos_x = widget.grid_pos[0] - widget.direction[0]
grid_pos_y = widget.grid_pos[1] - widget.direction[1]
body = Body(grid_pos = [grid_pos_x, grid_pos_y])
self.add_my_widget(body)
Running the code now will show you a beautiifully growing snake which will populate the playfield with its green body until nothing from the playfield is left to see. After this success we are now going to work on ending the game once the player leaves the playfield or collides with his own body.
Detecting collisions and ending the game
As we have seen by now, our snake can just move anywhere it wants to go. Out of the screen, back in and above itself. Everything is consequence free and the player will soon be bored. Therefore we are going to add collision detection for the snakes head with the body and with the screens limits. Also we are going to make the game go faster over time to make it more challenging.
The Playfield class is gonne be upgraded again to do so.
To increase the speed of the game over time are we going to introduce a second clock in the__init__ method which stops our exisitng clock and restarts it with a faster interval regularly.
Checking for collisions with the screen is also fairly simple. The check is going to be added in the move_head function after the head is repositioned. The only thing we have to do is to check if the right, left, top or bottom edge of the widget is at a position which is not within the screen dimensions. This is done in the head_out_of_playfield funtion.
Checking for collisions of the head with his own body parts is also quickly done. The function body_collision is simply going through all children widgets of the playfield. The check itself is done by the built-in Kivy method collide_widget.
If any ot the collision checks returns True we are going to call the function restart_game which stops all running clocks and calls the __init__ function again.
A little graphic modification is made as well. The widget size is now two pixels smaller than the grid dimension. This is going to ass a little space between all widgets on the screen giving a better visual control over the game.
class Playfield(FloatLayout):
def __init__(self, **kwargs):
'''initiate the playfield'''
super(Playfield, self).__init__(**kwargs)
#remove all existing widgets from previous game
self.clear_widgets()
#create chess board pattern on float layout
self.rows = 20
self.cols = 20
self.widget_size = Window.size[0] / 20 - 2
#add head
self.head = Head()
self.add_my_widget(self.head)
#start clock to move the head
self.update_interval = 1.0
self.move_head_clock = Clock.schedule_interval(lambda dt: self.move_head(self.head), self.update_interval)
#start clock to make the game faster
self.speed_up_clock = Clock.schedule_interval(lambda dt: self.speed_up(), 10)
def add_my_widget(self, widget):
'''add a new widget on the grid'''
widget.pos = (widget.grid_pos[0] * (self.widget_size + 2) + 1, widget.grid_pos[1] * (self.widget_size + 2) + 1)
widget.size_hint = (None, None)
widget.size = (self.widget_size, self.widget_size)
self.add_widget(widget)
def move_head(self, widget):
'''redraw the widget according to last position plus direction of motion'''
direction = Vector(widget.direction)
old_position = list(widget.grid_pos)
widget.grid_pos[0] = old_position[0] + direction[0]
widget.grid_pos[1] = old_position[1] + direction[1]
widget.pos = (widget.grid_pos[0] * (self.widget_size + 2) + 1, widget.grid_pos[1] * (self.widget_size + 2) +1)
#game over if head leaves playfield
if self.head_out_of_playfield(widget):
self.restart_game()
return
#game over if head collides with body
if self.body_collision():
self.restart_game()
return
#add new part to body
else:
body = Body(grid_pos = [old_position[0], old_position[1]])
self.add_my_widget(body)
def head_out_of_playfield(self, widget):
'''Check if head is still within the playfield'''
#TODO: Stop game once head is out of playfield
if widget.x < 0 or widget.x > self.width:
return True
if widget.y < 0 or widget.y > self.height:
return True
else:
return False
def body_collision(self):
'''Check if the head collides with the body'''
for element in self.children[:-1]:
if self.children[-1].collide_widget(element):
return True
else:
return False
def speed_up(self):
'''decrease the interval in which the head is moved one grid position'''
self.update_interval = self.update_interval -0.1
print(self.update_interval)
self.move_head_clock.cancel()
self.move_head_clock = Clock.schedule_interval(lambda dt: self.move_head(self.head), self.update_interval)
def restart_game(self):
'''stop all clocks and restart the game'''
self.move_head_clock.cancel()
self.speed_up_clock.cancel()
self.__init__()
Running the code will now show us a growing and increasingly hasty snake crawling along the playfield. Once there is a collision the game will restart.
Creating a fruit to catch and finishing touches on the game
Following the Wikipedia entry about snake should our snake not grow based on a time basis but once it catches a fruit. So let's add a randomly matreializing fruit and add body parts following such a catch.
Defining the fruit will be done in a separate class
class Fruit(Widget):
def __init__(self, grid_pos = None,**kwargs):
super(Fruit, self).__init__(**kwargs)
if grid_pos is None:
grid_pos = [0,0]
self.grid_pos = grid_pos
The move_head() method will be updated to make sure the fruit is not affectd during the motion of the snake itsels. An additional function fruit_collision() will take care of detecting a collision with the snake.
class Playfield(FloatLayout):
def __init__(self, **kwargs):
'''initiate the playfield'''
super(Playfield, self).__init__(**kwargs)
#remove all existing widgets from previous game
self.clear_widgets()
#create chess board pattern on float layout
self.rows = 20
self.cols = 20
self.widget_size = Window.size[0] / 20 - 2
#add first fruit
self.fruit = Fruit(grid_pos= (10,11))
self.add_my_widget(self.fruit)
# add head
self.head = Head()
self.add_my_widget(self.head)
#start clock to move the head
self.update_interval = 1.0
self.move_head_clock = Clock.schedule_interval(lambda dt: self.move_head(self.head), self.update_interval)
#start clock to make the game faster
self.speed_up_clock = Clock.schedule_interval(lambda dt: self.speed_up(), 10)
def add_my_widget(self, widget):
'''add a new widget on the grid'''
widget.pos = (widget.grid_pos[0] * (self.widget_size + 2) + 1, widget.grid_pos[1] * (self.widget_size + 2) + 1)
widget.size_hint = (None, None)
widget.size = (self.widget_size, self.widget_size)
self.add_widget(widget)
def move_head(self, widget):
'''redraw the widget according to last position plus direction of motion'''
direction = Vector(widget.direction)
old_position_head = list(widget.grid_pos)
old_position_last_body_part = list(self.children[0].grid_pos)
#move the snake
#exclude last child = fruit and next to last child = head
if len(self.children) > 2:
for i in range(0, len(self.children)-2):
self.children[i].grid_pos = list(self.children[i+1].grid_pos)
self.children[i].pos = (self.children[i].grid_pos[0] * (self.widget_size + 2) + 1, self.children[i].grid_pos[1] * (self.widget_size + 2) + 1)
# move head
widget.grid_pos[0] = old_position_head[0] + direction[0]
widget.grid_pos[1] = old_position_head[1] + direction[1]
widget.pos = (widget.grid_pos[0] * (self.widget_size + 2) + 1, widget.grid_pos[1] * (self.widget_size + 2) + 1)
#game over if head leaves playfield
if self.head_out_of_playfield(widget):
self.restart_game()
return
#game over if head collides with body
if self.body_collision():
self.restart_game()
return
#add body part if fruit is collected
if self.fruit_collision(widget):
body = Body(grid_pos=old_position_last_body_part)
self.add_my_widget(body)
self.fruit.grid_pos = (randrange(0,19), randrange(0,19))
self.fruit.pos = (self.fruit.grid_pos[0] * (self.widget_size + 2) + 1, self.fruit.grid_pos[1] * (self.widget_size + 2) + 1)
return
else:
return
def head_out_of_playfield(self, widget):
'''Check if head is still within the playfield'''
#TODO: Stop game once head is out of playfield
if widget.x < 0 or widget.x > self.width:
return True
if widget.y < 0 or widget.y > self.height:
return True
else:
return False
def body_collision(self):
'''Check if the head collides with the body'''
#ignore last element(head) and next to last element(fruit)
for element in self.children[:-2]:
if self.children[-2].collide_widget(element):
return True
else:
return False
def speed_up(self):
'''decrease the interval in which the head is moved one grid position'''
self.update_interval = self.update_interval -0.1
print(self.update_interval)
self.move_head_clock.cancel()
self.move_head_clock = Clock.schedule_interval(lambda dt: self.move_head(self.head), self.update_interval)
def fruit_collision(self, widget):
if widget.collide_widget(self.fruit):
print('you hit the fruit')
return True
else:
return False
def restart_game(self):
'''stop all clocks and restart the game'''
self.move_head_clock.cancel()
self.speed_up_clock.cancel()
self.__init__()
That's it. The complete snake game at your hands. Make it faster, harder and work on the transitions between the different clocks.
Source
Find the up-to-date code on GitHub:
https://github.com/HaikoKrais/SnakeApp