June 2015 • 5 min read
This main purpose of this post is to describe how the game was built. Most of it is already covered in the video tutorial though. I just added all the Yoruba nonsense.
The game consists of:
- A snake eating eba.
- Health bar.
- Exciting animations for a 2D game.
- Pause menu.
- About Menu.
- If he eats himself three times, he dies.
- Bushes at the edge are poisonous so they drain all his health.
- You get bonus points after eating a particular amount at intervals.
Pygame is a standard Python library for making games. It’s very popular and has great support. It’s the main module for the game. Pyganim is used here to add animations to the game.
In this phase we:
- Import dependencies.
- Setup the game screen (Text, Display Width and Height, Icon, Caption, Colors etc).
import pygame import pyganim import os import random
images_path = os.getcwd() + “\images\”
# Setting the display width and height display_width = 800 display_height = 600 gameDisplay = pygame.display.set_mode((display_width,display_height)) pygame.display.set_caption(‘Ejo’)
The way the animations work is mostly to create frames and go through them at pre-defined speeds. To make an infinite animation, you can create an infinite loop. For example:
game_overAnim = pyganim.PygAnimation([(images_path + 'game_over2.png', 150), (images_path + 'game_over3.png', 100)]) bonusAnim = pyganim.PygAnimation([(images_path+'bonus_text1.png', 150), (images_path+'bonus_text2.png', 100)]) while game_over is True: game_overAnim.play()
The pygame screen is a cartesian plane. What you have to do is write content to the screen at specified coordinates. After writing, you need to call
message_to_screen is a helper function used throughout the game to well, display a message to the screen. First, it gets the rendered font from calling
text_objects and determines where the centre of the message should be.
def message_to_screen(msg, color, y_displace=0, x_displace=0, size="small", font=None, font_size=None): """ :param msg: :param color: :param y_displace: :param x_displace: :param size: :param font: :param font_size: :return: """ text_surf, text_rect = text_objects(msg, color, size, font, font_size) text_rect.center = (display_width/2) + x_displace, (display_height/2) + y_displace gameDisplay.blit(text_surf, text_rect) def text_objects(text, color, size = None, font = None, fontSize = None): """ :param text: :param color: :param size: :param font: :param fontSize: :return: """ text_surface = None if size == 'small': text_surface = small_font.render(text, True, color) elif size == 'medium': text_surface = med_fontButton.render(text, True, color) elif size == 'large': text_surface = large_font.render(text, True, color) elif font is not None: font = pygame.font.Font(r'C:\Users\Ope O\Downloads\Fonts' + '\\' + font , fontSize) text_surface = font.render(text, True, color) return text_surface, text_surface.get_rect()
gameDisplay.blit(image, x, y) is used to display images to the screen. You’d come across it often in the code.
The snake is a list, which is appended to increase the length etc. The main characteristics of the snake are:
- Length - As long as the items in the list, plus the length. Each item that’s not the head has a block size of 20.
- Health - Starts off at 90. Reduced by 30 if snake eats itself and totally if you enter the barrier.
- Skin - Each entry in the list (that is not the head) is displayed using an image loaded from source.
- Head - First list element. An image is used to depict this too.
Direction - The snake moves in a direction by continuously adding to the coordinates in the main game loop. The default block size is 20.
for event in pygame.event.get(): if event.type == pygame.QUIT: game_exit = True if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: direction = 'left' lead_x_change = -block_size lead_y_change = 0 elif event.key == pygame.K_RIGHT: direction = 'right' lead_x_change = block_size lead_y_change = 0 elif event.key == pygame.K_UP: direction = 'up' lead_y_change = -block_size lead_x_change = 0 elif event.key == pygame.K_DOWN: direction = 'down' lead_y_change = block_size lead_x_change = 0 elif event.key == pygame.K_p: pause() lead_x += lead_x_change lead_y += lead_y_change snake_head = list() snake_head.append(lead_x) snake_head.append(lead_y) snake_list.append(snake_head)
When the direction is changed, the snake head needs to be rotated to face the direction it’s moving.
def Snake(block_size, snake_list): """ functionality for rotation :param block_size: :param snake_list: :return: """ head = None if direction == 'right': head = pygame.transform.rotate(img, 270) if direction == 'left': head = pygame.transform.rotate(img, 90) if direction == 'up': head = img if direction == 'down': head = pygame.transform.rotate(img, 180) gameDisplay.blit(head, (snake_list[-1], snake_list[-1])) for XnY in snake_list[:-1]: gameDisplay.blit(skin, (XnY, XnY))
- Position - Each part of the snake’s body has it’s own (x & y) coordinates.
Most of the functionality of the game is implemented by running while loops, including what is called the main game loop. The loop is only exited on some user input e.g. exit.
Going back to calling
pygame.display.update, the display needs to be updated after each iteration of the loop to ensure graphics are rendered properly.
In the while loops, various conditions are checked to determine if an action needs to be taken. Some examples of this are:
- The snake head intersecting with a body part.
- User pressing pause.
- Snake head meeting the barrier.
Snake head intersecting with eba.
# And this is the code for when the snake 'eats' an apple if (rand_apple_x < lead_x < rand_apple_x + AppleThickness) or ( rand_apple_x < lead_x + block_size < rand_apple_x + AppleThickness): if rand_apple_y < lead_y < rand_apple_y + AppleThickness: rand_apple_x, rand_apple_y = randAppleGen() score_value += 4 snake_length += 4 elif rand_apple_y < lead_y + block_size < rand_apple_y + AppleThickness: rand_apple_x, rand_apple_y = randAppleGen() snake_length += 4 score_value += 4
The eba is displayed using an image loaded from source. It’s main functionality is to be generated in pseudo-random positions for the user to chase, and re-generate when it’s ‘eaten’.
The apple thickness is a global variable set by default to 30.
def randAppleGen(): """ :return: """ rand_apple_x = round(random.randrange(40, display_width - AppleThickness - 40)) rand_apple_y = round(random.randrange(30, display_height - AppleThickness - 30)) return rand_apple_x, rand_apple_y
The score is text displayed at the upper right corner.
def score_update(score): """ :param score: :return: """ text = small_font.render('Score: ' + str(score), True, white) gameDisplay.blit(text, [display_width - 765,20])
def health_bars(snake_health): """ :param snake_health: :return: """ if snake_health > 75: snake_health_color = green elif snake_health > 50: snake_health_color = yellow else: snake_health_color = red health_text = small_font.render('Health: ', True, white) gameDisplay.blit(health_text,[display_width-210, 20]) pygame.draw.rect(gameDisplay, black , (display_width-131, 25, 92, 22)) pygame.draw.rect(gameDisplay, white , (display_width-130, 26, 90, 20)) pygame.draw.rect(gameDisplay, snake_health_color , (display_width-130, 26, snake_health, 20))
Running in Docker
I tried to put the app in a container for the purpose of this post, but couldn’t. I’ll publish another post soon about that whole process and why it might not be a dead-end.
Tbh though, all you really need is to have Python on your machine and probably ability to create a virtualenv.
- Different levels e.g. random obstacles get introduced at higher levels.
- High Score.
If you want to see the entire picture, the repo is open-source here. Feel free to clone and contribute.