Snake - Part 8: Keeping it going

Clocking the speed at…

Fortunately, pygame has a built-in tool to keep the time in cheque.

pygame.time.Clock()

With this tool we can use the method .tick(x) to limit how often per second the loop is progressing.

Unfortunately, this also has a severe impact on the detection of keys pressed.
My solution is to have a high tick rate, but not moving on every single tick.
Simply have a counter variable, which counts the ticks that have passed in a second and only call the move funtion on certain counter values.

  class Game():
      def __init__(self, window_width, window_height, playing_field_size):
          self._is_running = True
          pygame.init()
          self._display_size = (window_width,window_height)
          self._playing_field_size = playing_field_size
+         self.fps = 60
          self.on_init()
+         self.clock = pygame.time.Clock()
         self.run()
...
      def run(self):    
       ...
          self.draw()
+         self.clock.tick( self.fps )
          self.render()

The above code initalizes the Clock, sets the target ticks per second and waits for a tick to pass between every draw and render, limiting the tick rate.

  class Snake(Game):
      def on_init(self):
          self.food = Food(self._playing_field_size)
          self.player = player(self._playing_field_size)
+         self.fpsCounter = 0
      def on_event(self, event):
          if event.type == pygame.KEYDOWN:
              if event.key in (pygame.K_UP,pygame.K_DOWN, pygame.K_RIGHT,pygame.K_LEFT):
                  self.player.turn(event.key)
+     def limitFps(self, fps):
+         if self.fpsCounter >= self.fps // fps: 
+             self.fpsCounter = 0
+             return True
+         self.fpsCounter = self.fpsCounter + 1
+         return False
+     def draw(self):
+         if self.limitFps(2): 
+             if self.player.coords[-1] == self.food.coords:
+                 self.player.move(True)
+                 self.food.reset()
+             else:
+                 self.player.move()

        self.drawFood()
        self.draw_player()

Note the algorithm setting the pace.
We are checking the counter against the tickrate divided by our desired frame rate.
E.g. if our tick rate is 60 and our frame rate is 2, then the counter will count to 60/2=30 before resetting.

Furthermore, we have moved moving, eating and generating new food from the events section to the game loop.


Some small adjustments

Illegal movement

You might have noticed, that the snake can turn in on itself.
A basic measure against this is to check if the change of direction requested would mean a 180° turnaround.

Of course you can do better, but this suffices for the means of this tutorial.

class player():
 ...
    def turn(self,event):
        if event == pygame.K_UP    and not self.direction == "D": self.direction = "U"
        if event == pygame.K_DOWN  and not self.direction == "U": self.direction = "D"
        if event == pygame.K_RIGHT and not self.direction == "L": self.direction = "R"
        if event == pygame.K_LEFT  and not self.direction == "R": self.direction = "L"

Collisions

To check for collisions, we first need to know how to identify a collision.
In the case of snake, good checks would be:

  1. Check if the X or Y coordinate of the head are our of bounds, i.e. -1 or the playing field size
    • Note that this works, because the top left square of our board has the coordinates (0,0), so a field of size 20x20 would have the bottom right square sitting at (19,19)
  2. Check if the Coordinates of the newly places snakes head is already contained somewhere within the tail of the snake
    def checkCollisions(self): 
        if self.coords[-1][0] in (-1,self._playing_field_size): self.alive = False
        elif self.coords[-1][1] in (-1,self._playing_field_size): self.alive = False

        if self.coords[-1] in self.coords[0:-1]: self.alive = False

Now, to stop the game after a collision, lets give the player an alive-attribute, which is required to be True for the move() Function to work.

Finally, create a gameOver() Function which is to be run whenever the player is not alive.

As a final little gimmick, I added a respawn function, which overrides self.player with a new player instance, when the current player is not alive and the Space Bar is pressed.

You can download my final version here.
Feel free to build and improve upon it.


Thanks for chiming in!
I’d like to thank you for making it here and I hope you have learned something.

Good bye!